Skip to content

serde for undefined #635

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: ian/fix-streams
Choose a base branch
from
Open
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
48 changes: 48 additions & 0 deletions packages/convex-helpers/server/stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,4 +624,52 @@
expect(page1.page.map(stripSystemFields)).toEqual([{ a: 1, b: 3, c: 5 }]);
});
});
test("undefined cursor serialization roundtrips", async () => {
const schema = defineSchema({
foo: defineTable({
a: v.optional(v.number()),
b: v.number(),
}).index("ab", ["a", "b"]),
});
});

test("undefined cursor serialization roundtrips", async () => {
const schema = defineSchema({
foo: defineTable({
a: v.optional(v.number()),
b: v.number(),
}).index("ab", ["a", "b"]),
});
const t = convexTest(schema, modules);
await t.run(async (ctx) => {
await ctx.db.insert("foo", { a: 1, b: 2 });
await ctx.db.insert("foo", { a: undefined, b: 3 });
await ctx.db.insert("foo", { a: 2, b: 4 });
await ctx.db.insert("foo", { a: undefined, b: 5 });
const query = stream(ctx.db, schema).query("foo").withIndex("ab");
const result = await query.paginate({ numItems: 1, cursor: null });
expect(result.continueCursor).toMatch('["$_",');
expect(result.page.map(stripSystemFields)).toEqual([
{ a: undefined, b: 3 },
]);
expect(result.isDone).toBe(false);
const page1 = await query.paginate({
numItems: 2,
cursor: result.continueCursor,
});
expect(page1.page.map(stripSystemFields)).toEqual([
{ b: 5 },
{ a: 1, b: 2 },
]);
expect(page1.isDone).toBe(false);
const page2 = await query.paginate({
numItems: 1,
cursor: page1.continueCursor,

Check failure on line 667 in packages/convex-helpers/server/stream.test.ts

View workflow job for this annotation

GitHub Actions / Test and lint

'schema' is declared but its value is never read.
});
expect(page2.page.map(stripSystemFields)).toEqual([
{ a: undefined, b: 5 },
]);
expect(page2.isDone).toBe(true);
});
});
});
52 changes: 43 additions & 9 deletions packages/convex-helpers/server/stream.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Value } from "convex/values";
import { convexToJson, jsonToConvex } from "convex/values";
import { convexToJson, compareValues, jsonToConvex } from "convex/values";
import type {
DataModelFromSchemaDefinition,
DocumentByInfo,
Expand All @@ -20,9 +20,8 @@ import type {
SystemDataModel,
TableNamesInDataModel,
} from "convex/server";
import { compareValues } from "./compare.js";

export type IndexKey = Value[];
export type IndexKey = (Value | undefined)[];

//
// Helper functions
Expand Down Expand Up @@ -328,7 +327,7 @@ abstract class QueryStream<T extends GenericStreamItem>
};
if (opts.cursor !== null) {
newStartKey = {
key: jsonToConvex(JSON.parse(opts.cursor)) as IndexKey,
key: deserializeCursor(opts.cursor),
inclusive: false,
};
}
Expand All @@ -341,7 +340,7 @@ abstract class QueryStream<T extends GenericStreamItem>
let maxRows: number | undefined = opts.numItems;
if (opts.endCursor) {
newEndKey = {
key: jsonToConvex(JSON.parse(opts.endCursor)) as IndexKey,
key: deserializeCursor(opts.endCursor),
inclusive: true,
};
// If there's an endCursor, continue until we get there even if it's more
Expand Down Expand Up @@ -370,7 +369,7 @@ abstract class QueryStream<T extends GenericStreamItem>
(maxRowsToRead !== undefined && indexKeys.length >= maxRowsToRead)
) {
hasMore = true;
continueCursor = JSON.stringify(convexToJson(indexKey as Value));
continueCursor = serializeCursor(indexKey);
break;
}
}
Expand All @@ -389,9 +388,7 @@ abstract class QueryStream<T extends GenericStreamItem>
isDone: !hasMore,
continueCursor,
pageStatus,
splitCursor: splitCursor
? JSON.stringify(convexToJson(splitCursor as Value))
: undefined,
splitCursor: splitCursor ? serializeCursor(splitCursor) : undefined,
};
}
async collect() {
Expand Down Expand Up @@ -1821,3 +1818,40 @@ function compareKeys(key1: Key, key2: Key): number {
// of key2.kind is valid...
throw new Error(`Unexpected key kind: ${key1.kind as any}`);
}

function serializeCursor(key: IndexKey): string {
return JSON.stringify(
convexToJson(
key.map(
(v): Value =>
v === undefined
? "$_"
: typeof v === "string" && v.endsWith("$_")
? // in the unlikely case their string was "$_" or "$$_" etc.
// we need to escape it. Always add a $ so "$$_" becomes "$$$_"
"$" + v
: v,
),
),
);
}

function deserializeCursor(cursor: string): IndexKey {
return (jsonToConvex(JSON.parse(cursor)) as Value[]).map((v) => {
if (typeof v === "string") {
if (v === "$_") {
// This is a special case for the undefined value.
// It's not a valid value in the index, but it's a valid value in the
// cursor.
return undefined;
}
if (v.endsWith("$_")) {
// in the unlikely case their string was "$_" it was changed to "$$_"
// in the serialization process. If it was "$$_", it was changed to
// "$$$_" and so on.
return v.slice(1);
}
}
return v;
});
}
Loading