Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 16 additions & 8 deletions packages/convex-helpers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,20 +600,23 @@ await ctx.runMutation(internal.users.update, {
When using validators for defining database schema or function arguments,
these validators help:

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

Example:

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

// A validator for all the fields except balance.
const accountWithoutBalance = omit(vv.doc("accounts").fields, ["balance"]);

// Validate against a validator. Can optionally throw on error.
validate(balanceAndEmail, { balance: 123n, email: "[email protected]" });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth putting a oneline v.id warning here?

// Warning: this only validates that `accountId` is a string.
validate(vv.id("accounts"), accountId);
```

## Filter
Expand Down
2 changes: 1 addition & 1 deletion packages/convex-helpers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "convex-helpers",
"version": "0.1.68",
"version": "0.1.69-alpha.0",
"description": "A collection of useful code to complement the official convex package.",
"type": "module",
"bin": {
Expand Down
4 changes: 2 additions & 2 deletions packages/convex-helpers/react/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export function useSessionQuery<Query extends SessionFunction<"query">>(
): FunctionReturnType<Query> | undefined {
const [sessionId] = useSessionId();
const skip = args[0] === "skip" || !sessionId;
const originalArgs = args[0] === "skip" ? {} : args[0] ?? {};
const originalArgs = args[0] === "skip" ? {} : (args[0] ?? {});

const newArgs = skip ? "skip" : { ...originalArgs, sessionId };

Expand Down Expand Up @@ -256,7 +256,7 @@ export function useSessionId(): readonly [
if (!ctx.ssrFriendly && ctx.sessionId === undefined) {
throw new Error("Session ID invalid. Clear your storage?");
}
return [ctx.sessionId!, ctx.refreshSessionId, ctx.sessionIdPromise] as const;
return [ctx.sessionId, ctx.refreshSessionId, ctx.sessionIdPromise] as const;
}

/**
Expand Down
198 changes: 198 additions & 0 deletions packages/convex-helpers/server/validators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {
any,
array,
arrayBuffer,
bigint,
boolean,
brandedString,
Expand All @@ -20,6 +21,7 @@
pretendRequired,
string,
typedV,
ValidationError,

Check failure on line 24 in packages/convex-helpers/server/validators.test.ts

View workflow job for this annotation

GitHub Actions / Test and lint

'ValidationError' is declared but its value is never read.
} from "../validators.js";
import { convexTest } from "convex-test";
import {
Expand All @@ -37,6 +39,7 @@
import { expect, test } from "vitest";
import { modules } from "./setup.test.js";
import { getOrThrow } from "convex-helpers/server/relationships";
import { validate } from "../validators.js";

export const testLiterals = internalQueryGeneric({
args: {
Expand Down Expand Up @@ -268,3 +271,198 @@
} as ExampleFields);
}).rejects.toThrowError("Validator error");
});

describe("validate", () => {
test("validates primitive validators", () => {
// String
expect(validate(string, "hello")).toBe(true);
expect(validate(string, 123)).toBe(false);
expect(validate(string, null)).toBe(false);

// Number
expect(validate(number, 123)).toBe(true);
expect(validate(number, "123")).toBe(false);
expect(validate(number, null)).toBe(false);

// Boolean
expect(validate(boolean, true)).toBe(true);
expect(validate(boolean, false)).toBe(true);
expect(validate(boolean, "true")).toBe(false);
expect(validate(boolean, 1)).toBe(false);

// Null
expect(validate(null_, null)).toBe(true);
expect(validate(null_, undefined)).toBe(false);
expect(validate(null_, false)).toBe(false);

// BigInt/Int64
expect(validate(bigint, 123n)).toBe(true);
expect(validate(bigint, 123)).toBe(false);
expect(validate(bigint, "123")).toBe(false);
});

test("validates array validator", () => {
const arrayOfStrings = array(string);
expect(validate(arrayOfStrings, ["a", "b", "c"])).toBe(true);
expect(validate(arrayOfStrings, [])).toBe(true);
expect(validate(arrayOfStrings, ["a", 1, "c"])).toBe(false);
expect(validate(arrayOfStrings, null)).toBe(false);
expect(validate(arrayOfStrings, "not an array")).toBe(false);
});

test("validates object validator", () => {
const personValidator = object({
name: string,
age: number,
optional: optional(string),
});

expect(validate(personValidator, { name: "Alice", age: 30 })).toBe(true);
expect(
validate(personValidator, { name: "Bob", age: 25, optional: "test" }),
).toBe(true);
expect(validate(personValidator, { name: "Charlie", age: "30" })).toBe(
false,
);
expect(validate(personValidator, { name: "Dave" })).toBe(false);
expect(validate(personValidator, null)).toBe(false);
expect(
validate(personValidator, { name: "Eve", age: 20, extra: "field" }),
).toBe(false);
});

test("validates union validator", () => {
const unionValidator = or(string, number, object({ type: is("test") }));

expect(validate(unionValidator, "string")).toBe(true);
expect(validate(unionValidator, 123)).toBe(true);
expect(validate(unionValidator, { type: "test" })).toBe(true);
expect(validate(unionValidator, { type: "wrong" })).toBe(false);
expect(validate(unionValidator, true)).toBe(false);
expect(validate(unionValidator, null)).toBe(false);
});

test("validates literal validator", () => {
const literalValidator = is("specific");
expect(validate(literalValidator, "specific")).toBe(true);
expect(validate(literalValidator, "other")).toBe(false);
expect(validate(literalValidator, null)).toBe(false);
});

test("validates optional values", () => {
const optionalString = optional(string);
expect(validate(optionalString, "value")).toBe(true);
expect(validate(optionalString, undefined)).toBe(true);
expect(validate(optionalString, null)).toBe(false);
expect(validate(optionalString, 123)).toBe(false);
});

test("throws validation errors when configured", () => {
expect(() => validate(string, 123, { throw: true })).toThrow(
"Validator error",
);

expect(() =>
validate(object({ name: string }), { name: 123 }, { throw: true }),
).toThrow("Validator error");

expect(() =>
validate(
object({ name: string }),
{ name: "valid", extra: true },
{ throw: true },
),
).toThrow("Validator error");
});

test("includes path in error messages", () => {
const complexValidator = object({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the array pathPrefix is also interesting

user: object({
details: object({
name: string,
}),
}),
});

try {
validate(
complexValidator,
{
user: {
details: {
name: 123,
},
},
},
{ throw: true },
);
fail("Should have thrown");
} catch (e: any) {
expect(e.message).toContain("user.details.name");
}
});

test("includes path for nested objects", () => {
const complexValidator = object({
user: object({
details: object({
name: string,
}),
}),
});
expect(
validate(complexValidator, { user: { details: { name: "Alice" } } }),
).toBe(true);
expect(
validate(complexValidator, { user: { details: { name: 123 } } }),
).toBe(false);
try {
validate(
complexValidator,
{ user: { details: { name: 123 } } },
{ throw: true },
);
fail("Should have thrown");
} catch (e: any) {
expect(e.message).toContain("user.details.name");
}
});

test("includes path for nested arrays", () => {
const complexValidator = object({
user: object({
details: array(string),
}),
});
expect(
validate(complexValidator, { user: { details: ["a", "b", "c"] } }),
).toBe(true);
expect(validate(complexValidator, { user: { details: [1, 2, 3] } })).toBe(
false,
);
try {
validate(
complexValidator,
{ user: { details: ["a", 3] } },
{ throw: true },
);
fail("Should have thrown");
} catch (e: any) {
expect(e.message).toContain("user.details[1]");
}
});

test("validates bytes/ArrayBuffer", () => {
const buffer = new ArrayBuffer(8);
expect(validate(arrayBuffer, buffer)).toBe(true);
expect(validate(arrayBuffer, new Uint8Array(8))).toBe(false);
expect(validate(arrayBuffer, "binary")).toBe(false);
});

test("validates any", () => {
expect(validate(any, "anything")).toBe(true);
expect(validate(any, 123)).toBe(true);
expect(validate(any, null)).toBe(true);
expect(validate(any, { complex: "object" })).toBe(true);
});
});
Loading
Loading