Skip to content

Commit 48f5f5c

Browse files
authored
fix convex-test Date returns (#291)
1 parent 97db607 commit 48f5f5c

4 files changed

Lines changed: 505 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ node_modules
2121
# production
2222
dist
2323
/tmp
24+
.tmp
2425
/out-tsc
2526
**/build
2627

convex/orm/mutations.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,51 @@ describe('M7 Mutations', () => {
277277
});
278278
});
279279

280+
it('should allow returning hydrated Date values from t.run in convex-test', async () => {
281+
const localSubscriptions = convexTable('localSubscriptionsForRunReturn', {
282+
plan: text().notNull(),
283+
referenceId: text().notNull(),
284+
status: text(),
285+
createdAt: timestamp().notNull().defaultNow(),
286+
updatedAt: timestamp().notNull().defaultNow(),
287+
});
288+
const localSchema = defineSchema({
289+
localSubscriptions,
290+
});
291+
const localRelations = defineRelations(
292+
{
293+
localSubscriptions,
294+
},
295+
() => ({})
296+
);
297+
298+
const sub = await convexTest(localSchema).run(async (baseCtx) => {
299+
const ctx = withOrm(baseCtx, localRelations);
300+
const [inserted] = await ctx.orm
301+
.insert(localSubscriptions)
302+
.values({ plan: 'pro', referenceId: 'ref_1', status: 'active' })
303+
.returning();
304+
305+
expect(inserted.createdAt).toBeInstanceOf(Date);
306+
expect(inserted.updatedAt).toBeInstanceOf(Date);
307+
308+
return inserted;
309+
});
310+
311+
expect(typeof sub.createdAt).toBe('number');
312+
expect(typeof sub.updatedAt).toBe('number');
313+
});
314+
315+
it('should preserve convex-test rejection for unsupported class returns', async () => {
316+
class UnsupportedDateReturn {
317+
createdAt = new Date();
318+
}
319+
320+
await expect(
321+
convexTest(schema).run(async () => new UnsupportedDateReturn())
322+
).rejects.toThrow('not a supported Convex type');
323+
});
324+
280325
it('should update rows and return updated values', async ({ ctx }) => {
281326
const db = ctx.orm;
282327
const [user] = await db.insert(users).values(baseUser).returning();

convex/setup.testing.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,96 @@ const convexModules = (import.meta as ImportMetaWithGlob).glob([
2525
]);
2626
const relations = requireSchemaRelations(schema);
2727

28+
type TestIdentity = Parameters<
29+
ReturnType<typeof baseConvexTest>['withIdentity']
30+
>[0];
31+
32+
const serializeDatesForConvexTest = (value: unknown): unknown => {
33+
if (value instanceof Date) {
34+
return value.getTime();
35+
}
36+
37+
if (Array.isArray(value)) {
38+
let serialized: unknown[] | undefined;
39+
40+
for (let index = 0; index < value.length; index += 1) {
41+
const entry = value[index];
42+
const encoded = serializeDatesForConvexTest(entry);
43+
if (encoded !== entry) {
44+
if (!serialized) {
45+
serialized = value.slice();
46+
}
47+
serialized[index] = encoded;
48+
}
49+
}
50+
51+
return serialized ?? value;
52+
}
53+
54+
if (!value || typeof value !== 'object') {
55+
return value;
56+
}
57+
58+
const prototype = Object.getPrototypeOf(value);
59+
const isSimpleObject =
60+
prototype === null ||
61+
prototype === Object.prototype ||
62+
prototype?.constructor?.name === 'Object';
63+
if (!isSimpleObject) {
64+
return value;
65+
}
66+
67+
const record = value as Record<string, unknown>;
68+
let serialized: Record<string, unknown> | undefined;
69+
70+
for (const key in record) {
71+
if (!Object.hasOwn(record, key)) {
72+
continue;
73+
}
74+
75+
const entry = record[key];
76+
const encoded = serializeDatesForConvexTest(entry);
77+
if (encoded !== entry) {
78+
if (!serialized) {
79+
serialized = { ...record };
80+
}
81+
serialized[key] = encoded;
82+
}
83+
}
84+
85+
return serialized ?? value;
86+
};
87+
88+
const wrapConvexTestDateReturns = <Test extends object>(test: Test): Test => {
89+
const runnable = test as Test & {
90+
run: <Output>(fn: (ctx: unknown) => Promise<Output>) => Promise<Output>;
91+
withIdentity?: (identity: TestIdentity) => object;
92+
};
93+
const withIdentity = runnable.withIdentity;
94+
95+
const wrapped = {
96+
...runnable,
97+
run: async <Output>(fn: (ctx: unknown) => Promise<Output>) =>
98+
runnable.run(
99+
async (ctx) => serializeDatesForConvexTest(await fn(ctx)) as Output
100+
),
101+
};
102+
103+
if (!withIdentity) {
104+
return wrapped as Test;
105+
}
106+
107+
return {
108+
...wrapped,
109+
withIdentity: (identity: TestIdentity) =>
110+
wrapConvexTestDateReturns(withIdentity(identity)),
111+
} as Test;
112+
};
113+
28114
export function convexTest<Schema extends SchemaDefinition<any, any>>(
29115
schema: Schema
30116
) {
31-
return baseConvexTest(schema, convexModules);
117+
return wrapConvexTestDateReturns(baseConvexTest(schema, convexModules));
32118
}
33119

34120
export const withOrm = <

0 commit comments

Comments
 (0)