Skip to content

Commit fcc216b

Browse files
authored
Merge pull request #434 from get-convex/ian/validate
Ian/validate
2 parents 43779d2 + 7b1275d commit fcc216b

File tree

5 files changed

+414
-11
lines changed

5 files changed

+414
-11
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/convex-helpers/README.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -600,20 +600,23 @@ await ctx.runMutation(internal.users.update, {
600600
When using validators for defining database schema or function arguments,
601601
these validators help:
602602

603-
1. Add a `Table` utility that defines a table and keeps references to the fields
604-
to avoid re-defining validators. To learn more about sharing validators, read
605-
[this article](https://stack.convex.dev/argument-validation-without-repetition),
606-
an extension of [this article](https://stack.convex.dev/types-cookbook).
607-
2. Add utilties for `partial`, `pick` and `omit` to match the TypeScript type
608-
utilities.
609-
3. Add shorthand for a union of `literals`, a `nullable` field, a `deprecated`
610-
field, and `brandedString`. To learn more about branded strings see
603+
1. Add shorthand for a union of `literals`, a `nullable` field, a `deprecated`
604+
field, a `partial` object, and `brandedString`.
605+
To learn more about branded strings see
611606
[this article](https://stack.convex.dev/using-branded-types-in-validators).
607+
2. A `validate(validator, data)` function validates a value against a validator.
608+
Warning: this does not validate that the value of v.id is an ID for the given table.
609+
3. Add utilties for `partial`, `pick` and `omit` to match the TypeScript type
610+
utilities.
612611
4. Add a `doc(schema, "tableName")` helper to validate a document with system
613612
fields included.
614613
5. Add a `typedV(schema)` helper that is a `v` replacement that also has:
615614
- `doc("tableName")` that works like `doc` above.
616615
- `id("tableName")` that is typed to tables in your schema.
616+
6. Add a `Table` utility that defines a table and keeps references to the fields
617+
to avoid re-defining validators. To learn more about sharing validators, read
618+
[this article](https://stack.convex.dev/argument-validation-without-repetition),
619+
an extension of [this article](https://stack.convex.dev/types-cookbook).
617620

618621
Example:
619622

@@ -666,6 +669,11 @@ const balanceAndEmail = pick(vv.doc("accounts").fields, ["balance", "email"]);
666669

667670
// A validator for all the fields except balance.
668671
const accountWithoutBalance = omit(vv.doc("accounts").fields, ["balance"]);
672+
673+
// Validate against a validator. Can optionally throw on error.
674+
validate(balanceAndEmail, { balance: 123n, email: "[email protected]" });
675+
// Warning: this only validates that `accountId` is a string.
676+
validate(vv.id("accounts"), accountId);
669677
```
670678

671679
## Filter

packages/convex-helpers/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "convex-helpers",
3-
"version": "0.1.68",
3+
"version": "0.1.69-alpha.0",
44
"description": "A collection of useful code to complement the official convex package.",
55
"type": "module",
66
"bin": {

packages/convex-helpers/server/validators.test.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { assert, Equals } from "../index.js";
22
import {
33
any,
44
array,
5+
arrayBuffer,
56
bigint,
67
boolean,
78
brandedString,
@@ -20,6 +21,7 @@ import {
2021
pretendRequired,
2122
string,
2223
typedV,
24+
ValidationError,
2325
} from "../validators.js";
2426
import { convexTest } from "convex-test";
2527
import {
@@ -37,6 +39,7 @@ import { Infer, ObjectType } from "convex/values";
3739
import { expect, test } from "vitest";
3840
import { modules } from "./setup.test.js";
3941
import { getOrThrow } from "convex-helpers/server/relationships";
42+
import { validate } from "../validators.js";
4043

4144
export const testLiterals = internalQueryGeneric({
4245
args: {
@@ -268,3 +271,198 @@ test("validators disallow things when they're wrong", async () => {
268271
} as ExampleFields);
269272
}).rejects.toThrowError("Validator error");
270273
});
274+
275+
describe("validate", () => {
276+
test("validates primitive validators", () => {
277+
// String
278+
expect(validate(string, "hello")).toBe(true);
279+
expect(validate(string, 123)).toBe(false);
280+
expect(validate(string, null)).toBe(false);
281+
282+
// Number
283+
expect(validate(number, 123)).toBe(true);
284+
expect(validate(number, "123")).toBe(false);
285+
expect(validate(number, null)).toBe(false);
286+
287+
// Boolean
288+
expect(validate(boolean, true)).toBe(true);
289+
expect(validate(boolean, false)).toBe(true);
290+
expect(validate(boolean, "true")).toBe(false);
291+
expect(validate(boolean, 1)).toBe(false);
292+
293+
// Null
294+
expect(validate(null_, null)).toBe(true);
295+
expect(validate(null_, undefined)).toBe(false);
296+
expect(validate(null_, false)).toBe(false);
297+
298+
// BigInt/Int64
299+
expect(validate(bigint, 123n)).toBe(true);
300+
expect(validate(bigint, 123)).toBe(false);
301+
expect(validate(bigint, "123")).toBe(false);
302+
});
303+
304+
test("validates array validator", () => {
305+
const arrayOfStrings = array(string);
306+
expect(validate(arrayOfStrings, ["a", "b", "c"])).toBe(true);
307+
expect(validate(arrayOfStrings, [])).toBe(true);
308+
expect(validate(arrayOfStrings, ["a", 1, "c"])).toBe(false);
309+
expect(validate(arrayOfStrings, null)).toBe(false);
310+
expect(validate(arrayOfStrings, "not an array")).toBe(false);
311+
});
312+
313+
test("validates object validator", () => {
314+
const personValidator = object({
315+
name: string,
316+
age: number,
317+
optional: optional(string),
318+
});
319+
320+
expect(validate(personValidator, { name: "Alice", age: 30 })).toBe(true);
321+
expect(
322+
validate(personValidator, { name: "Bob", age: 25, optional: "test" }),
323+
).toBe(true);
324+
expect(validate(personValidator, { name: "Charlie", age: "30" })).toBe(
325+
false,
326+
);
327+
expect(validate(personValidator, { name: "Dave" })).toBe(false);
328+
expect(validate(personValidator, null)).toBe(false);
329+
expect(
330+
validate(personValidator, { name: "Eve", age: 20, extra: "field" }),
331+
).toBe(false);
332+
});
333+
334+
test("validates union validator", () => {
335+
const unionValidator = or(string, number, object({ type: is("test") }));
336+
337+
expect(validate(unionValidator, "string")).toBe(true);
338+
expect(validate(unionValidator, 123)).toBe(true);
339+
expect(validate(unionValidator, { type: "test" })).toBe(true);
340+
expect(validate(unionValidator, { type: "wrong" })).toBe(false);
341+
expect(validate(unionValidator, true)).toBe(false);
342+
expect(validate(unionValidator, null)).toBe(false);
343+
});
344+
345+
test("validates literal validator", () => {
346+
const literalValidator = is("specific");
347+
expect(validate(literalValidator, "specific")).toBe(true);
348+
expect(validate(literalValidator, "other")).toBe(false);
349+
expect(validate(literalValidator, null)).toBe(false);
350+
});
351+
352+
test("validates optional values", () => {
353+
const optionalString = optional(string);
354+
expect(validate(optionalString, "value")).toBe(true);
355+
expect(validate(optionalString, undefined)).toBe(true);
356+
expect(validate(optionalString, null)).toBe(false);
357+
expect(validate(optionalString, 123)).toBe(false);
358+
});
359+
360+
test("throws validation errors when configured", () => {
361+
expect(() => validate(string, 123, { throw: true })).toThrow(
362+
"Validator error",
363+
);
364+
365+
expect(() =>
366+
validate(object({ name: string }), { name: 123 }, { throw: true }),
367+
).toThrow("Validator error");
368+
369+
expect(() =>
370+
validate(
371+
object({ name: string }),
372+
{ name: "valid", extra: true },
373+
{ throw: true },
374+
),
375+
).toThrow("Validator error");
376+
});
377+
378+
test("includes path in error messages", () => {
379+
const complexValidator = object({
380+
user: object({
381+
details: object({
382+
name: string,
383+
}),
384+
}),
385+
});
386+
387+
try {
388+
validate(
389+
complexValidator,
390+
{
391+
user: {
392+
details: {
393+
name: 123,
394+
},
395+
},
396+
},
397+
{ throw: true },
398+
);
399+
fail("Should have thrown");
400+
} catch (e: any) {
401+
expect(e.message).toContain("user.details.name");
402+
}
403+
});
404+
405+
test("includes path for nested objects", () => {
406+
const complexValidator = object({
407+
user: object({
408+
details: object({
409+
name: string,
410+
}),
411+
}),
412+
});
413+
expect(
414+
validate(complexValidator, { user: { details: { name: "Alice" } } }),
415+
).toBe(true);
416+
expect(
417+
validate(complexValidator, { user: { details: { name: 123 } } }),
418+
).toBe(false);
419+
try {
420+
validate(
421+
complexValidator,
422+
{ user: { details: { name: 123 } } },
423+
{ throw: true },
424+
);
425+
fail("Should have thrown");
426+
} catch (e: any) {
427+
expect(e.message).toContain("user.details.name");
428+
}
429+
});
430+
431+
test("includes path for nested arrays", () => {
432+
const complexValidator = object({
433+
user: object({
434+
details: array(string),
435+
}),
436+
});
437+
expect(
438+
validate(complexValidator, { user: { details: ["a", "b", "c"] } }),
439+
).toBe(true);
440+
expect(validate(complexValidator, { user: { details: [1, 2, 3] } })).toBe(
441+
false,
442+
);
443+
try {
444+
validate(
445+
complexValidator,
446+
{ user: { details: ["a", 3] } },
447+
{ throw: true },
448+
);
449+
fail("Should have thrown");
450+
} catch (e: any) {
451+
expect(e.message).toContain("user.details[1]");
452+
}
453+
});
454+
455+
test("validates bytes/ArrayBuffer", () => {
456+
const buffer = new ArrayBuffer(8);
457+
expect(validate(arrayBuffer, buffer)).toBe(true);
458+
expect(validate(arrayBuffer, new Uint8Array(8))).toBe(false);
459+
expect(validate(arrayBuffer, "binary")).toBe(false);
460+
});
461+
462+
test("validates any", () => {
463+
expect(validate(any, "anything")).toBe(true);
464+
expect(validate(any, 123)).toBe(true);
465+
expect(validate(any, null)).toBe(true);
466+
expect(validate(any, { complex: "object" })).toBe(true);
467+
});
468+
});

0 commit comments

Comments
 (0)