@gaganref/confect-test 10.3.1-fork.9 → 10.3.1-fork.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/TestConfect.d.ts +37 -11
  2. package/dist/TestConfect.d.ts.map +1 -1
  3. package/dist/TestConfect.js +22 -7
  4. package/dist/TestConfect.js.map +1 -1
  5. package/dist/internal/Attempt.d.ts +77 -0
  6. package/dist/internal/Attempt.d.ts.map +1 -0
  7. package/dist/internal/Attempt.js +154 -0
  8. package/dist/internal/Attempt.js.map +1 -0
  9. package/dist/internal/Clock.d.ts +29 -0
  10. package/dist/internal/Clock.d.ts.map +1 -0
  11. package/dist/internal/Clock.js +66 -0
  12. package/dist/internal/Clock.js.map +1 -0
  13. package/dist/internal/ConvexTest.d.ts +66 -0
  14. package/dist/internal/ConvexTest.d.ts.map +1 -0
  15. package/dist/internal/ConvexTest.js +62 -0
  16. package/dist/internal/ConvexTest.js.map +1 -0
  17. package/dist/internal/Execution.d.ts +247 -0
  18. package/dist/internal/Execution.d.ts.map +1 -0
  19. package/dist/internal/Execution.js +1024 -0
  20. package/dist/internal/Execution.js.map +1 -0
  21. package/dist/internal/Globals.d.ts +11 -0
  22. package/dist/internal/Globals.d.ts.map +1 -0
  23. package/dist/internal/Globals.js +88 -0
  24. package/dist/internal/Globals.js.map +1 -0
  25. package/dist/internal/Invocation.d.ts +68 -0
  26. package/dist/internal/Invocation.d.ts.map +1 -0
  27. package/dist/internal/Invocation.js +158 -0
  28. package/dist/internal/Invocation.js.map +1 -0
  29. package/dist/internal/Kernel.d.ts +95 -0
  30. package/dist/internal/Kernel.d.ts.map +1 -0
  31. package/dist/internal/Kernel.js +91 -0
  32. package/dist/internal/Kernel.js.map +1 -0
  33. package/dist/internal/KernelQuery.d.ts +55 -0
  34. package/dist/internal/KernelQuery.d.ts.map +1 -0
  35. package/dist/internal/KernelQuery.js +205 -0
  36. package/dist/internal/KernelQuery.js.map +1 -0
  37. package/dist/internal/KernelRefusal.d.ts +102 -0
  38. package/dist/internal/KernelRefusal.d.ts.map +1 -0
  39. package/dist/internal/KernelRefusal.js +94 -0
  40. package/dist/internal/KernelRefusal.js.map +1 -0
  41. package/dist/internal/KernelValues.d.ts +138 -0
  42. package/dist/internal/KernelValues.d.ts.map +1 -0
  43. package/dist/internal/KernelValues.js +281 -0
  44. package/dist/internal/KernelValues.js.map +1 -0
  45. package/dist/internal/MountedHttp.d.ts +1 -1
  46. package/dist/internal/MountedHttp.d.ts.map +1 -1
  47. package/dist/internal/MountedHttp.js +3 -3
  48. package/dist/internal/MountedHttp.js.map +1 -1
  49. package/dist/internal/ReadSet.d.ts +55 -0
  50. package/dist/internal/ReadSet.d.ts.map +1 -0
  51. package/dist/internal/ReadSet.js +29 -0
  52. package/dist/internal/ReadSet.js.map +1 -0
  53. package/dist/internal/Scheduling.d.ts +71 -0
  54. package/dist/internal/Scheduling.d.ts.map +1 -0
  55. package/dist/internal/Scheduling.js +60 -0
  56. package/dist/internal/Scheduling.js.map +1 -0
  57. package/package.json +5 -5
  58. package/src/TestConfect.ts +84 -19
  59. package/src/internal/Attempt.ts +185 -0
  60. package/src/internal/Clock.ts +95 -0
  61. package/src/internal/ConvexTest.ts +163 -0
  62. package/src/internal/Execution.ts +1795 -0
  63. package/src/internal/Globals.ts +113 -0
  64. package/src/internal/Invocation.ts +316 -0
  65. package/src/internal/Kernel.ts +190 -0
  66. package/src/internal/KernelQuery.ts +405 -0
  67. package/src/internal/KernelRefusal.ts +112 -0
  68. package/src/internal/KernelValues.ts +765 -0
  69. package/src/internal/MountedHttp.ts +13 -6
  70. package/src/internal/ReadSet.ts +108 -0
  71. package/src/internal/Scheduling.ts +126 -0
@@ -0,0 +1,765 @@
1
+ // Derived from convex-test 0.0.65 (index.ts), Copyright Convex, Inc.,
2
+ // licensed under the Apache License, Version 2.0
3
+ // (https://www.apache.org/licenses/LICENSE-2.0). The value, filter, range,
4
+ // validator and argument rules are kept as convex-test states them, so the
5
+ // kernel answers a function the way convex-test does. The storage, attempts
6
+ // and execution around them are the kernel's own.
7
+
8
+ import type { GenericDocument } from "convex/server";
9
+ import {
10
+ CommitTsPlaceholder,
11
+ type GenericId,
12
+ type JSONValue,
13
+ type Value,
14
+ compareValues,
15
+ convexToJson,
16
+ jsonToConvex,
17
+ } from "convex/values";
18
+
19
+ /**
20
+ * A function address inside one test backend: its component and its
21
+ * `module:export` path.
22
+ */
23
+ export type FunctionPath = {
24
+ readonly componentPath: string;
25
+ readonly udfPath: string;
26
+ };
27
+
28
+ export type FilterJson =
29
+ | { $eq: [FilterJson, FilterJson] }
30
+ | { $field: string }
31
+ | JSONValue;
32
+
33
+ export type QueryOperator = { filter: FilterJson } | { limit: number };
34
+ export type Source =
35
+ | { type: "FullTableScan"; tableName: string; order: "asc" | "desc" | null }
36
+ | {
37
+ type: "IndexRange";
38
+ indexName: string;
39
+ range: ReadonlyArray<SerializedRangeExpression>;
40
+ order: "asc" | "desc" | null;
41
+ }
42
+ | {
43
+ type: "Search";
44
+ indexName: string;
45
+ filters: ReadonlyArray<SerializedSearchFilter>;
46
+ };
47
+
48
+ export type SerializedQuery = {
49
+ source: Source;
50
+ operators: Array<QueryOperator>;
51
+ };
52
+
53
+ export type SerializedRangeExpression = {
54
+ type: "Eq" | "Gt" | "Gte" | "Lt" | "Lte";
55
+ fieldPath: string;
56
+ value: JSONValue;
57
+ };
58
+
59
+ export type SerializedSearchFilter =
60
+ | {
61
+ type: "Search";
62
+ fieldPath: string;
63
+ value: string;
64
+ }
65
+ | {
66
+ type: "Eq";
67
+ fieldPath: string;
68
+ value: JSONValue;
69
+ };
70
+
71
+ export type StoredDocument = GenericDocument & {
72
+ _id: DocumentId;
73
+ _creationTime: number;
74
+ };
75
+
76
+ export type Index = {
77
+ indexDescriptor: string;
78
+ fields: string[];
79
+ };
80
+
81
+ export type TableName = string;
82
+
83
+ export type DocumentId = GenericId<TableName>;
84
+
85
+ export function tableNameFromId(id: string) {
86
+ const match = id.match(/^[0-9]+/);
87
+ if (!match || match[0].length === id.length) {
88
+ return null;
89
+ }
90
+ return id.slice(match[0].length);
91
+ }
92
+ export function deserializeConvexErrorData(thrown: unknown) {
93
+ if (
94
+ typeof thrown === "object" &&
95
+ thrown !== null &&
96
+ Symbol.for("ConvexError") in thrown &&
97
+ typeof (thrown as { data: unknown }).data === "string"
98
+ ) {
99
+ try {
100
+ (thrown as { data: unknown }).data = jsonToConvex(
101
+ JSON.parse((thrown as { data: string }).data),
102
+ );
103
+ } catch {
104
+ // Data wasn't a serialized JSON string — leave it untouched.
105
+ }
106
+ }
107
+ return thrown;
108
+ }
109
+
110
+ export function isSimpleObject(value: unknown) {
111
+ const isObject = typeof value === "object";
112
+ const prototype = Object.getPrototypeOf(value);
113
+ const isSimple =
114
+ prototype === null ||
115
+ prototype === Object.prototype ||
116
+ // Objects generated from other contexts (e.g. across Node.js `vm` modules) will not satisfy the previous
117
+ // conditions but are still simple objects.
118
+ prototype?.constructor?.name === "Object";
119
+ return isObject && isSimple;
120
+ }
121
+
122
+ // Commit timestamp placeholders are resolved to `i64::MAX` before commit.
123
+ export const MAX_COMMIT_TS = 2n ** 63n - 1n;
124
+
125
+ export function hasPendingValues(value: Value): boolean {
126
+ if (value instanceof CommitTsPlaceholder) {
127
+ return true;
128
+ }
129
+ if (Array.isArray(value)) {
130
+ return value.some(hasPendingValues);
131
+ }
132
+ if (value !== null && value !== undefined && isSimpleObject(value)) {
133
+ return Object.values(value).some(hasPendingValues);
134
+ }
135
+ return false;
136
+ }
137
+
138
+ // The value as the transaction sees it before committing.
139
+ export function resolvePendingValues(value: Value): Value {
140
+ return substituteCommitTs(value, MAX_COMMIT_TS);
141
+ }
142
+
143
+ // The value as it is committed.
144
+ export function resolveCommittedValues<T extends Value>(
145
+ value: T,
146
+ commitTs: bigint,
147
+ ): T {
148
+ return substituteCommitTs(value, commitTs);
149
+ }
150
+
151
+ export function substituteCommitTs<T extends Value>(
152
+ value: T,
153
+ commitTs: bigint,
154
+ ): T {
155
+ if (!hasPendingValues(value)) {
156
+ return value;
157
+ }
158
+ if (value instanceof CommitTsPlaceholder) {
159
+ return commitTs as T;
160
+ }
161
+ if (Array.isArray(value)) {
162
+ return value.map((v) => substituteCommitTs(v, commitTs)) as T;
163
+ }
164
+ return Object.fromEntries(
165
+ Object.entries(value as object).map(([k, v]) => [
166
+ k,
167
+ substituteCommitTs(v, commitTs),
168
+ ]),
169
+ ) as T;
170
+ }
171
+
172
+ export function evaluateFieldPath(fieldPath: string, document: any) {
173
+ const pathParts = fieldPath.split(".");
174
+ let result: Value | undefined = document;
175
+ for (const p of pathParts) {
176
+ result =
177
+ result !== undefined && result !== null && isSimpleObject(result)
178
+ ? (result as any)[p]
179
+ : undefined;
180
+ }
181
+ return result === undefined ? undefined : resolvePendingValues(result);
182
+ }
183
+
184
+ export function evaluateFilter(
185
+ document: GenericDocument,
186
+ filter: any,
187
+ ): Value | undefined {
188
+ if (filter.$eq !== undefined) {
189
+ return (
190
+ compareValues(
191
+ evaluateFilter(document, filter.$eq[0]),
192
+ evaluateFilter(document, filter.$eq[1]),
193
+ ) === 0
194
+ );
195
+ }
196
+ if (filter.$neq !== undefined) {
197
+ return (
198
+ compareValues(
199
+ evaluateFilter(document, filter.$neq[0]),
200
+ evaluateFilter(document, filter.$neq[1]),
201
+ ) !== 0
202
+ );
203
+ }
204
+ if (filter.$and !== undefined) {
205
+ return filter.$and.every((child: any) => evaluateFilter(document, child));
206
+ }
207
+ if (filter.$or !== undefined) {
208
+ return filter.$or.some((child: any) => evaluateFilter(document, child));
209
+ }
210
+ if (filter.$not !== undefined) {
211
+ return !evaluateFilter(document, filter.$not);
212
+ }
213
+ if (filter.$gt !== undefined) {
214
+ return (
215
+ evaluateFilter(document, filter.$gt[0])! >
216
+ evaluateFilter(document, filter.$gt[1])!
217
+ );
218
+ }
219
+ if (filter.$gte !== undefined) {
220
+ return (
221
+ evaluateFilter(document, filter.$gte[0])! >=
222
+ evaluateFilter(document, filter.$gte[1])!
223
+ );
224
+ }
225
+ if (filter.$lt !== undefined) {
226
+ return (
227
+ evaluateFilter(document, filter.$lt[0])! <
228
+ evaluateFilter(document, filter.$lt[1])!
229
+ );
230
+ }
231
+ if (filter.$lte !== undefined) {
232
+ return (
233
+ evaluateFilter(document, filter.$lte[0])! <=
234
+ evaluateFilter(document, filter.$lte[1])!
235
+ );
236
+ }
237
+ if (filter.$add !== undefined) {
238
+ return (
239
+ (evaluateFilter(document, filter.$add[0]) as number) +
240
+ (evaluateFilter(document, filter.$add[1]) as number)
241
+ );
242
+ }
243
+ if (filter.$sub !== undefined) {
244
+ return (
245
+ (evaluateFilter(document, filter.$sub[0]) as number) -
246
+ (evaluateFilter(document, filter.$sub[1]) as number)
247
+ );
248
+ }
249
+ if (filter.$mul !== undefined) {
250
+ return (
251
+ (evaluateFilter(document, filter.$mul[0]) as number) *
252
+ (evaluateFilter(document, filter.$mul[1]) as number)
253
+ );
254
+ }
255
+ if (filter.$div !== undefined) {
256
+ return (
257
+ (evaluateFilter(document, filter.$div[0]) as number) /
258
+ (evaluateFilter(document, filter.$div[1]) as number)
259
+ );
260
+ }
261
+ if (filter.$mod !== undefined) {
262
+ return (
263
+ (evaluateFilter(document, filter.$mod[0]) as number) %
264
+ (evaluateFilter(document, filter.$mod[1]) as number)
265
+ );
266
+ }
267
+ if (filter.$field !== undefined) {
268
+ return evaluateFieldPath(filter.$field, document);
269
+ }
270
+ if (filter.$literal !== undefined) {
271
+ return evaluateQueryValue(filter.$literal);
272
+ }
273
+ throw new Error(`not implemented: ${JSON.stringify(filter)}`);
274
+ }
275
+
276
+ export function evaluateRangeFilter(
277
+ document: GenericDocument,
278
+ expr: SerializedRangeExpression,
279
+ ) {
280
+ const result = evaluateFieldPath(expr.fieldPath, document);
281
+ const value = evaluateQueryValue(expr.value);
282
+ switch (expr.type) {
283
+ case "Eq":
284
+ return compareValues(result, value) === 0;
285
+ case "Gt":
286
+ return compareValues(result, value) > 0;
287
+ case "Gte":
288
+ return compareValues(result, value) >= 0;
289
+ case "Lt":
290
+ return compareValues(result, value) < 0;
291
+ case "Lte":
292
+ return compareValues(result, value) <= 0;
293
+ }
294
+ }
295
+
296
+ // A value used as a written field.
297
+ export function evaluateValue(value: JSONValue) {
298
+ if (typeof value === "object" && value !== null && "$undefined" in value) {
299
+ return undefined;
300
+ }
301
+ return jsonToConvex(value);
302
+ }
303
+
304
+ // A value used as a query bound or literal.
305
+ export function evaluateQueryValue(value: JSONValue) {
306
+ const evaluatedValue = evaluateValue(value);
307
+ return evaluatedValue === undefined
308
+ ? undefined
309
+ : resolvePendingValues(evaluatedValue);
310
+ }
311
+
312
+ // An index range expression is always a chained list of:
313
+ //
314
+ // 0 or more equality expressions defined with .eq.
315
+ // [Optionally] A lower bound expression defined with .gt or .gte.
316
+ // [Optionally] An upper bound expression defined with .lt or .lte.
317
+ export function validateIndexRangeExpression(
318
+ source: Source & { type: "IndexRange" },
319
+ fields: string[],
320
+ ) {
321
+ let fieldIndex = 0;
322
+ let state: "eq" | "gt" | "lt" | "done" = "eq";
323
+ for (const [filterIndex, filter] of source.range.entries()) {
324
+ if (state === "done") {
325
+ throw new Error(
326
+ `Incorrect operator used in \`withIndex\`, cannot chain ` +
327
+ `more operators after both \`.gt\` and \`.lt\` were already used, ` +
328
+ `got \`${printIndexOperator(filter)}\`.`,
329
+ );
330
+ }
331
+
332
+ const filterType: "eq" | "gt" | "lt" =
333
+ filter.type == "Gt" || filter.type == "Gte"
334
+ ? "gt"
335
+ : filter.type == "Lt" || filter.type == "Lte"
336
+ ? "lt"
337
+ : "eq";
338
+
339
+ switch (`${state}|${filterType}`) {
340
+ // Allow to operate on the current indexed field
341
+ case "eq|eq":
342
+ case "eq|gt":
343
+ case "eq|lt":
344
+ if (filter.fieldPath === fields[fieldIndex]) {
345
+ fieldIndex += 1;
346
+ state = filterType;
347
+ continue;
348
+ }
349
+ throw new Error(
350
+ `Incorrect field used in \`withIndex\`, ` +
351
+ `expected "${fields[fieldIndex]}", got "${filter.fieldPath}"`,
352
+ );
353
+
354
+ // Allow to operate on the previous field (gt and lt must operate on same field)
355
+ case "lt|gt":
356
+ case "gt|lt":
357
+ if (fieldIndex > 0 && filter.fieldPath === fields[fieldIndex - 1]) {
358
+ state = "done";
359
+ continue;
360
+ }
361
+ throw new Error(
362
+ `Incorrect field used in \`withIndex\`, ` +
363
+ `\`.gt\` and \`.lt\` must operate on the same field, ` +
364
+ `expected "${fields[fieldIndex - 1]}", got "${filter.fieldPath}"`,
365
+ );
366
+
367
+ default:
368
+ throw new Error(
369
+ `Incorrect operator used in \`withIndex\`, ` +
370
+ `cannot chain \`.${filter.type.toLowerCase()}()\` ` +
371
+ `after \`.${source.range[filterIndex - 1].type.toLowerCase()}()\``,
372
+ );
373
+ }
374
+ }
375
+ }
376
+
377
+ export function printIndexOperator(filter: SerializedRangeExpression) {
378
+ return `.${filter.type.toLowerCase()}(${filter.fieldPath}, ${JSON.stringify(filter.value)})`;
379
+ }
380
+
381
+ export function validateFieldNames(validator: ValidatorJSON) {
382
+ if (validator.type === "object") {
383
+ Object.keys(validator.value).forEach((fieldName) => {
384
+ if (!isValidIdentifier(fieldName)) {
385
+ throw new Error(
386
+ `Field names must be valid identifiers, got "${fieldName}"`,
387
+ );
388
+ }
389
+ });
390
+ }
391
+
392
+ if (validator.type === "union") {
393
+ validator.value.forEach(validateFieldNames);
394
+ }
395
+ }
396
+
397
+ export function isValidIdentifier(name: string) {
398
+ return /(^_(id|creationTime)$)|^[a-zA-Z][a-zA-Z0-9_]*$/.test(name);
399
+ }
400
+
401
+ export type ObjectFieldType = { fieldType: ValidatorJSON; optional: boolean };
402
+
403
+ export type ValidatorJSON =
404
+ | {
405
+ type: "null";
406
+ }
407
+ | { type: "number" }
408
+ | { type: "bigint" }
409
+ | { type: "commitTs" }
410
+ | { type: "boolean" }
411
+ | { type: "string" }
412
+ | { type: "bytes" }
413
+ | { type: "any" }
414
+ | {
415
+ type: "literal";
416
+ value: JSONValue;
417
+ }
418
+ | { type: "id"; tableName: string }
419
+ | { type: "array"; value: ValidatorJSON }
420
+ | { type: "object"; value: Record<string, ObjectFieldType> }
421
+ | { type: "record"; keys: ValidatorJSON; values: ObjectFieldType }
422
+ | {
423
+ type: "function";
424
+ fnType?: string;
425
+ args?: ValidatorJSON;
426
+ returns?: ValidatorJSON;
427
+ }
428
+ | { type: "union"; value: ValidatorJSON[] };
429
+
430
+ export function validateValidator(validator: ValidatorJSON, value: any) {
431
+ switch (validator.type) {
432
+ case "null": {
433
+ if (value !== null) {
434
+ throw new Error(`Validator error: Expected \`null\`, got \`${value}\``);
435
+ }
436
+ return;
437
+ }
438
+ case "number": {
439
+ if (typeof value !== "number") {
440
+ throw new Error(
441
+ `Validator error: Expected \`number\`, got \`${value}\``,
442
+ );
443
+ }
444
+ return;
445
+ }
446
+ case "bigint": {
447
+ if (typeof value !== "bigint") {
448
+ throw new Error(
449
+ `Validator error: Expected \`bigint\`, got \`${value}\``,
450
+ );
451
+ }
452
+ return;
453
+ }
454
+ case "commitTs": {
455
+ if (
456
+ typeof value !== "bigint" &&
457
+ !(value instanceof CommitTsPlaceholder)
458
+ ) {
459
+ throw new Error(
460
+ `Validator error: Expected \`v.commitTs()\`, got \`${value}\``,
461
+ );
462
+ }
463
+ return;
464
+ }
465
+ case "boolean": {
466
+ if (typeof value !== "boolean") {
467
+ throw new Error(
468
+ `Validator error: Expected \`boolean\`, got \`${value}\``,
469
+ );
470
+ }
471
+ return;
472
+ }
473
+ case "string": {
474
+ if (typeof value !== "string") {
475
+ throw new Error(
476
+ `Validator error: Expected \`string\`, got \`${value}\``,
477
+ );
478
+ }
479
+ return;
480
+ }
481
+ case "bytes": {
482
+ if (!(value instanceof ArrayBuffer)) {
483
+ throw new Error(
484
+ `Validator error: Expected \`ArrayBuffer\`, got \`${value}\``,
485
+ );
486
+ }
487
+ return;
488
+ }
489
+ case "any": {
490
+ return;
491
+ }
492
+ case "literal": {
493
+ if (value !== validator.value) {
494
+ throw new Error(
495
+ `Validator error: Expected \`${
496
+ validator.value as any
497
+ }\`, got \`${value}\``,
498
+ );
499
+ }
500
+ return;
501
+ }
502
+ case "id": {
503
+ if (typeof value !== "string") {
504
+ throw new Error(
505
+ `Validator error: Expected \`string\`, got \`${value}\``,
506
+ );
507
+ }
508
+ if (tableNameFromId(value) !== validator.tableName) {
509
+ throw new Error(
510
+ `Validator error: Expected ID for table "${validator.tableName}", got \`${value}\``,
511
+ );
512
+ }
513
+ return;
514
+ }
515
+ case "array": {
516
+ if (!Array.isArray(value)) {
517
+ throw new Error(
518
+ `Validator error: Expected \`Array\`, got \`${value}\``,
519
+ );
520
+ }
521
+ for (const v of value) {
522
+ validateValidator(validator.value, v);
523
+ }
524
+ return;
525
+ }
526
+ case "union": {
527
+ let isValid = false;
528
+ for (const v of validator.value) {
529
+ try {
530
+ validateValidator(v, value);
531
+ isValid = true;
532
+ break;
533
+ } catch {
534
+ // Ignore
535
+ }
536
+ }
537
+ if (!isValid) {
538
+ throw new Error(
539
+ `Validator error: Expected one of ${validator.value.map((v) => v.type).join(", ")}, got \`${JSON.stringify(convexToJson(value))}\``,
540
+ );
541
+ }
542
+ return;
543
+ }
544
+ case "object": {
545
+ if (typeof value !== "object") {
546
+ throw new Error(
547
+ `Validator error: Expected \`object\`, got \`${value}\``,
548
+ );
549
+ }
550
+ if (!isSimpleObject(value)) {
551
+ throw new Error(
552
+ `Validator error: Expected a plain old JavaScript \`object\`, got \`${value}\``,
553
+ );
554
+ }
555
+ for (const [k, { fieldType, optional }] of Object.entries(
556
+ validator.value,
557
+ )) {
558
+ if (value[k] === undefined) {
559
+ if (!optional) {
560
+ throw new Error(
561
+ `Validator error: Missing required field \`${k}\` in object`,
562
+ );
563
+ }
564
+ } else {
565
+ validateValidator(fieldType, value[k]);
566
+ }
567
+ }
568
+ for (const k of Object.keys(value)) {
569
+ if (validator.value[k] === undefined) {
570
+ throw new Error(
571
+ `Validator error: Unexpected field \`${k}\` in object`,
572
+ );
573
+ }
574
+ }
575
+ return;
576
+ }
577
+ case "function": {
578
+ // The pinned binary, `precompiled-2026-10-28-cccbade`, accepts a string
579
+ // that parses as a function handle
580
+ // (`common/src/schemas/validator.rs:156-164`), and its parser requires a
581
+ // document id after the prefix
582
+ // (`common/src/bootstrap_model/components/handles.rs:100-112`). The
583
+ // kernel mints convex-test's handle format instead, so it accepts a
584
+ // string that parses as that format.
585
+ if (
586
+ typeof value !== "string" ||
587
+ readFunctionHandle(value) === undefined
588
+ ) {
589
+ throw new Error(
590
+ `Validator error: Expected a function handle, got \`${value}\``,
591
+ );
592
+ }
593
+ return;
594
+ }
595
+ case "record": {
596
+ if (
597
+ value === null ||
598
+ typeof value !== "object" ||
599
+ !isSimpleObject(value)
600
+ ) {
601
+ throw new Error(
602
+ `Validator error: Expected a plain old JavaScript \`object\` for a record, got \`${value}\``,
603
+ );
604
+ }
605
+ for (const [k, fieldValue] of Object.entries(value)) {
606
+ validateValidator(validator.keys, k);
607
+ if (fieldValue !== undefined) {
608
+ validateValidator(validator.values.fieldType, fieldValue);
609
+ }
610
+ }
611
+ return;
612
+ }
613
+ default: {
614
+ const unknown: never = validator;
615
+ throw new Error(
616
+ `The test kernel cannot check a validator of type \`${(unknown as { type: string }).type}\`.`,
617
+ );
618
+ }
619
+ }
620
+ }
621
+
622
+ export const JWT_CLAIM_BY_IDENTITY_ATTRIBUTE: Record<string, string> = {
623
+ subject: "sub",
624
+ issuer: "iss",
625
+ name: "name",
626
+ givenName: "given_name",
627
+ familyName: "family_name",
628
+ nickname: "nickname",
629
+ preferredUsername: "preferred_username",
630
+ profileUrl: "profile",
631
+ pictureUrl: "picture",
632
+ email: "email",
633
+ emailVerified: "email_verified",
634
+ gender: "gender",
635
+ birthday: "birthdate",
636
+ timezone: "zoneinfo",
637
+ language: "locale",
638
+ phoneNumber: "phone_number",
639
+ phoneNumberVerified: "phone_number_verified",
640
+ address: "address",
641
+ updatedAt: "updated_at",
642
+ };
643
+
644
+ export function base64UrlEncode(value: string) {
645
+ return base64UrlEncodeBytes(new TextEncoder().encode(value));
646
+ }
647
+
648
+ export function base64UrlEncodeBytes(bytes: Uint8Array) {
649
+ let binary = "";
650
+ for (const byte of bytes) {
651
+ binary += String.fromCharCode(byte);
652
+ }
653
+ return btoa(binary)
654
+ .replace(/\+/g, "-")
655
+ .replace(/\//g, "_")
656
+ .replace(/=+$/, "");
657
+ }
658
+
659
+ // Builds the token `withIdentity` pretends the request was authenticated with.
660
+ // Real tokens are signed by an identity provider; this one is unsecured
661
+ // (`"alg": "none"`, empty signature).
662
+ export function createTestJwt(identity: Record<string, any>) {
663
+ const claims: Record<string, any> = Object.create(null);
664
+ for (const [attribute, value] of Object.entries(identity)) {
665
+ if (value === undefined || attribute === "tokenIdentifier") {
666
+ continue;
667
+ }
668
+ // Custom claims are used as is, known attributes use their claim name.
669
+ const claimName = Object.hasOwn(JWT_CLAIM_BY_IDENTITY_ATTRIBUTE, attribute)
670
+ ? JWT_CLAIM_BY_IDENTITY_ATTRIBUTE[attribute]
671
+ : attribute;
672
+ claims[claimName] = value;
673
+ }
674
+ // The identity provider always determines who the user is.
675
+ claims.iss = identity.issuer;
676
+ claims.sub = identity.subject;
677
+ const header = base64UrlEncode(JSON.stringify({ alg: "none", typ: "JWT" }));
678
+ const payload = base64UrlEncode(JSON.stringify(claims));
679
+ return `${header}.${payload}.`;
680
+ }
681
+
682
+ // Limits the backend puts on tokens from `ctx.auth.signJwt`.
683
+ export function parseArgs(
684
+ args: Record<string, Value> | undefined,
685
+ ): Record<string, Value> {
686
+ if (args === undefined) {
687
+ return {};
688
+ }
689
+ if (!isSimpleObject(args)) {
690
+ throw new Error(
691
+ `The arguments to a Convex function must be an object. Received: ${
692
+ args as any
693
+ }`,
694
+ );
695
+ }
696
+ return args;
697
+ }
698
+
699
+ export function validateFunctionArgs(
700
+ argsValidator: ValidatorJSON,
701
+ args: any,
702
+ functionPath: FunctionPath,
703
+ allowPendingValues: boolean,
704
+ ) {
705
+ if (!allowPendingValues && hasPendingValues(args)) {
706
+ throw new Error(
707
+ `Invalid arguments for "${functionPath.udfPath}": an unresolved ` +
708
+ `placeholder value can only be passed to a query or mutation ` +
709
+ `called from a mutation`,
710
+ );
711
+ }
712
+ validateValidator(argsValidator, resolvePendingValues(args));
713
+ }
714
+
715
+ export function validateReturnValue(
716
+ returnsValidator: ValidatorJSON | null,
717
+ result: any,
718
+ functionType: "query" | "mutation" | "action",
719
+ functionPath: FunctionPath,
720
+ ) {
721
+ if (returnsValidator !== null) {
722
+ try {
723
+ validateValidator(returnsValidator, resolvePendingValues(result));
724
+ } catch (e) {
725
+ throw new Error(
726
+ `Return value validation failed for ${functionType} "${functionPath.udfPath}":\n${(e as Error).message}`,
727
+ { cause: e },
728
+ );
729
+ }
730
+ }
731
+ }
732
+
733
+ const FUNCTION_HANDLE_PREFIX = "function://";
734
+
735
+ export function createFunctionHandle(functionPath: FunctionPath) {
736
+ return `${FUNCTION_HANDLE_PREFIX}${functionPath.componentPath};${functionPath.udfPath}`;
737
+ }
738
+
739
+ /**
740
+ * The function a handle the kernel minted names, or `undefined` for a string
741
+ * that is not `function://<componentPath>;<udfPath>` with a function path.
742
+ */
743
+ function readFunctionHandle(handle: string): FunctionPath | undefined {
744
+ if (!handle.startsWith(FUNCTION_HANDLE_PREFIX)) return undefined;
745
+ const parts = handle.slice(FUNCTION_HANDLE_PREFIX.length).split(";");
746
+ if (parts.length !== 2 || parts[1] === "") return undefined;
747
+ return { componentPath: parts[0]!, udfPath: parts[1]! };
748
+ }
749
+
750
+ export function parseFunctionHandle(handle: string): FunctionPath {
751
+ const functionPath = readFunctionHandle(handle);
752
+ if (functionPath === undefined) {
753
+ throw new Error(`Invalid function handle \`${handle}\``);
754
+ }
755
+ return functionPath;
756
+ }
757
+ export function simpleHash(string: string) {
758
+ let hash = 0;
759
+ for (let i = 0; i < string.length; i++) {
760
+ const char = string.charCodeAt(i);
761
+ hash = (hash << 5) - hash + char;
762
+ hash = hash & hash; // Convert to 32bit integer
763
+ }
764
+ return hash;
765
+ }