@peers-app/peers-sdk 0.26.6 → 0.27.0

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 (182) hide show
  1. package/README.md +3 -1
  2. package/bin/peers-package.js +13 -0
  3. package/define/package.json +4 -0
  4. package/dist/contracts/__tests__/action-dispatch.test.js +56 -76
  5. package/dist/contracts/__tests__/also-implements.test.d.ts +1 -0
  6. package/dist/contracts/__tests__/also-implements.test.js +110 -0
  7. package/dist/contracts/__tests__/builder.test.js +45 -0
  8. package/dist/contracts/__tests__/contract-resolvers.test.js +134 -4
  9. package/dist/contracts/__tests__/frozen-contracts.test.d.ts +1 -0
  10. package/dist/contracts/__tests__/frozen-contracts.test.js +191 -0
  11. package/dist/contracts/__tests__/generic-tool-dispatch.test.js +13 -56
  12. package/dist/contracts/__tests__/host-contract-client.test.d.ts +1 -0
  13. package/dist/contracts/__tests__/host-contract-client.test.js +793 -0
  14. package/dist/contracts/__tests__/host-contract-transport.test.js +15 -0
  15. package/dist/contracts/__tests__/registry.test.js +68 -0
  16. package/dist/contracts/__tests__/target-dispatch.d.ts +11 -0
  17. package/dist/contracts/__tests__/target-dispatch.js +24 -0
  18. package/dist/contracts/__tests__/tool-catalog-service.test.js +37 -1
  19. package/dist/contracts/__tests__/tool-catalog.test.js +12 -3
  20. package/dist/contracts/__tests__/validate.test.js +177 -7
  21. package/dist/contracts/__tests__/workflow-context.test.js +0 -11
  22. package/dist/contracts/builder.d.ts +16 -2
  23. package/dist/contracts/builder.js +44 -3
  24. package/dist/contracts/contract-definition-schema.d.ts +251 -0
  25. package/dist/contracts/contract-definition-schema.js +48 -0
  26. package/dist/contracts/contract-provider-router.d.ts +2 -0
  27. package/dist/contracts/contract-provider-router.js +5 -2
  28. package/dist/contracts/contract-proxy.d.ts +52 -18
  29. package/dist/contracts/contract-proxy.js +144 -203
  30. package/dist/contracts/contract-resolvers.d.ts +6 -4
  31. package/dist/contracts/contract-resolvers.js +32 -9
  32. package/dist/contracts/frozen-contracts.d.ts +59 -0
  33. package/dist/contracts/frozen-contracts.js +86 -0
  34. package/dist/contracts/host-contract-client.d.ts +134 -0
  35. package/dist/contracts/host-contract-client.js +396 -0
  36. package/dist/contracts/host-contract-transport.d.ts +2 -0
  37. package/dist/contracts/host-contract-transport.js +8 -0
  38. package/dist/contracts/index.d.ts +6 -4
  39. package/dist/contracts/index.js +11 -5
  40. package/dist/contracts/registry.d.ts +31 -1
  41. package/dist/contracts/registry.js +100 -6
  42. package/dist/contracts/system/contract-catalog-contract.d.ts +61 -6
  43. package/dist/contracts/system/contract-catalog-contract.js +17 -5
  44. package/dist/contracts/tool-catalog-service.js +26 -6
  45. package/dist/contracts/tool-catalog.d.ts +1 -1
  46. package/dist/contracts/tool-catalog.js +4 -1
  47. package/dist/contracts/types.d.ts +32 -2
  48. package/dist/contracts/validate.d.ts +16 -1
  49. package/dist/contracts/validate.js +139 -25
  50. package/dist/data/change-tracking.d.ts +17 -5
  51. package/dist/data/change-tracking.js +22 -6
  52. package/dist/data/change-tracking.test.d.ts +1 -0
  53. package/dist/data/change-tracking.test.js +62 -0
  54. package/dist/data/orm/sql.data-source.d.ts +9 -0
  55. package/dist/data/orm/sql.data-source.js +47 -2
  56. package/dist/data/orm/sql.data-source.test.js +142 -0
  57. package/dist/data/orm/table-container.js +9 -9
  58. package/dist/data/orm/table-container.test.js +44 -0
  59. package/dist/data/orm/types.d.ts +41 -1
  60. package/dist/data/orm/types.js +159 -28
  61. package/dist/data/orm/types.test.js +129 -0
  62. package/dist/data/peer-types.d.ts +8 -62
  63. package/dist/data/persistent-vars.d.ts +4 -1
  64. package/dist/data/persistent-vars.js +7 -2
  65. package/dist/data/tools.d.ts +29 -221
  66. package/dist/define/also-implements.test.d.ts +1 -0
  67. package/dist/define/also-implements.test.js +123 -0
  68. package/dist/define/context.d.ts +77 -0
  69. package/dist/define/context.js +2 -0
  70. package/dist/define/contract-ref.d.ts +177 -0
  71. package/dist/define/contract-ref.js +67 -0
  72. package/dist/define/declare-contract.d.ts +167 -0
  73. package/dist/define/declare-contract.js +220 -0
  74. package/dist/define/declare-contract.test.d.ts +1 -0
  75. package/dist/define/declare-contract.test.js +573 -0
  76. package/dist/define/define-package.d.ts +203 -0
  77. package/dist/define/define-package.js +289 -0
  78. package/dist/define/define.test.d.ts +1 -0
  79. package/dist/define/define.test.js +1010 -0
  80. package/dist/define/empty-fields.d.ts +15 -0
  81. package/dist/define/empty-fields.js +134 -0
  82. package/dist/define/empty-fields.test.d.ts +1 -0
  83. package/dist/define/empty-fields.test.js +55 -0
  84. package/dist/define/events-observables.test.d.ts +1 -0
  85. package/dist/define/events-observables.test.js +1013 -0
  86. package/dist/define/events.d.ts +81 -0
  87. package/dist/define/events.js +81 -0
  88. package/dist/define/generated-field.d.ts +20 -0
  89. package/dist/define/generated-field.js +25 -0
  90. package/dist/define/index.d.ts +28 -0
  91. package/dist/define/index.js +62 -0
  92. package/dist/define/lazy-members.d.ts +8 -0
  93. package/dist/define/lazy-members.js +38 -0
  94. package/dist/define/manifest.d.ts +63 -0
  95. package/dist/define/manifest.js +653 -0
  96. package/dist/define/observables.d.ts +146 -0
  97. package/dist/define/observables.js +187 -0
  98. package/dist/define/serve-members.d.ts +21 -0
  99. package/dist/define/serve-members.js +152 -0
  100. package/dist/define/serve-members.test.d.ts +1 -0
  101. package/dist/define/serve-members.test.js +630 -0
  102. package/dist/define/serve.d.ts +22 -0
  103. package/dist/define/serve.js +58 -0
  104. package/dist/define/system-contracts.d.ts +41 -0
  105. package/dist/define/system-contracts.js +21 -0
  106. package/dist/define/table-handle.d.ts +13 -0
  107. package/dist/define/table-handle.js +36 -0
  108. package/dist/define/table.d.ts +128 -0
  109. package/dist/define/table.js +122 -0
  110. package/dist/define/tool.d.ts +129 -0
  111. package/dist/define/tool.js +91 -0
  112. package/dist/define/vars.d.ts +82 -0
  113. package/dist/define/vars.js +256 -0
  114. package/dist/define/vars.test.d.ts +1 -0
  115. package/dist/define/vars.test.js +39 -0
  116. package/dist/define/wire-value.d.ts +12 -0
  117. package/dist/define/wire-value.js +76 -0
  118. package/dist/define/worker-state.d.ts +15 -0
  119. package/dist/define/worker-state.js +45 -0
  120. package/dist/device/tx-encoding.test.js +10 -0
  121. package/dist/index.d.ts +3 -0
  122. package/dist/index.js +5 -1
  123. package/dist/observable.d.ts +42 -2
  124. package/dist/observable.js +25 -7
  125. package/dist/package-build/build-package.d.ts +60 -0
  126. package/dist/package-build/build-package.js +312 -0
  127. package/dist/package-build/build-package.test.d.ts +1 -0
  128. package/dist/package-build/build-package.test.js +204 -0
  129. package/dist/package-build/cli.d.ts +7 -0
  130. package/dist/package-build/cli.js +218 -0
  131. package/dist/package-build/contract-add.d.ts +35 -0
  132. package/dist/package-build/contract-add.js +97 -0
  133. package/dist/package-build/contract-add.test.d.ts +1 -0
  134. package/dist/package-build/contract-add.test.js +467 -0
  135. package/dist/package-build/contract-source.d.ts +59 -0
  136. package/dist/package-build/contract-source.js +471 -0
  137. package/dist/package-build/freeze-contracts.d.ts +56 -0
  138. package/dist/package-build/freeze-contracts.js +198 -0
  139. package/dist/package-build/freeze-contracts.test.d.ts +1 -0
  140. package/dist/package-build/freeze-contracts.test.js +355 -0
  141. package/dist/package-installer/package-creator.d.ts +4 -0
  142. package/dist/package-installer/package-creator.js +57 -16
  143. package/dist/package-installer/package-creator.test.js +86 -0
  144. package/dist/package-loader/contract-members.d.ts +53 -0
  145. package/dist/package-loader/contract-members.js +31 -0
  146. package/dist/package-loader/contract-package-loader.js +5 -1
  147. package/dist/package-loader/contract-package-loader.test.js +11 -0
  148. package/dist/package-loader/host-authority-loading.test.js +9 -5
  149. package/dist/package-loader/index.d.ts +1 -0
  150. package/dist/package-loader/index.js +1 -0
  151. package/dist/package-loader/isolated-artifact.d.ts +80 -17
  152. package/dist/package-loader/isolated-artifact.js +257 -51
  153. package/dist/package-loader/isolated-artifact.test.js +294 -21
  154. package/dist/package-loader/isolated-runtime.d.ts +22 -7
  155. package/dist/package-loader/isolated-table-gateway.js +30 -11
  156. package/dist/package-loader/isolated-table-gateway.test.js +243 -0
  157. package/dist/package-loader/package-loader.d.ts +8 -1
  158. package/dist/package-loader/package-loader.js +39 -20
  159. package/dist/package-loader/package-loader.test.js +47 -0
  160. package/dist/peer-id.d.ts +25 -0
  161. package/dist/peer-id.js +99 -0
  162. package/dist/serial-json.compat.test.d.ts +1 -0
  163. package/dist/serial-json.compat.test.js +369 -0
  164. package/dist/serial-json.d.ts +41 -5
  165. package/dist/serial-json.js +216 -157
  166. package/dist/serial-json.oracle.d.ts +21 -0
  167. package/dist/serial-json.oracle.js +222 -0
  168. package/dist/serial-json.test.js +315 -0
  169. package/dist/testing/index.d.ts +4 -0
  170. package/dist/testing/index.js +8 -0
  171. package/dist/testing/mock-host-contract.d.ts +123 -0
  172. package/dist/testing/mock-host-contract.js +434 -0
  173. package/dist/testing/mock-host-contract.test.d.ts +1 -0
  174. package/dist/testing/mock-host-contract.test.js +782 -0
  175. package/dist/types/field-type.d.ts +35 -18
  176. package/dist/types/field-type.js +4 -1
  177. package/dist/types/zod-types.d.ts +9 -0
  178. package/dist/types/zod-types.js +8 -2
  179. package/dist/utils.d.ts +1 -25
  180. package/dist/utils.js +10 -90
  181. package/package.json +9 -2
  182. package/testing/package.json +4 -0
@@ -32,6 +32,12 @@ class DBHarness {
32
32
  const result = await this.db.prepare(sql).run(params);
33
33
  return { changes: result.changes };
34
34
  }
35
+ execSync(sql, params = []) {
36
+ return this.db.prepare(sql).run(params);
37
+ }
38
+ getSync(sql, params = []) {
39
+ return this.db.prepare(sql).get(params);
40
+ }
35
41
  async close() {
36
42
  this._db?.close();
37
43
  this._db = null;
@@ -431,6 +437,142 @@ describe("table", () => {
431
437
  await ArraysTable.db.close();
432
438
  });
433
439
  });
440
+ describe("when a field is empty", () => {
441
+ const tableType = {
442
+ name: "test_table_empty_values",
443
+ description: "Empty values",
444
+ primaryKeyName: "rowId",
445
+ fields: [
446
+ { name: "rowId", type: field_type_1.FieldType.id },
447
+ { name: "title", type: field_type_1.FieldType.string },
448
+ { name: "note", type: field_type_1.FieldType.string, optional: true },
449
+ { name: "count", type: field_type_1.FieldType.number, optional: true },
450
+ { name: "when", type: field_type_1.FieldType.Date, optional: true },
451
+ {
452
+ name: "detail",
453
+ type: field_type_1.FieldType.object,
454
+ optional: true,
455
+ fields: [
456
+ { name: "city", type: field_type_1.FieldType.string },
457
+ { name: "zip", type: field_type_1.FieldType.string, optional: true },
458
+ ],
459
+ },
460
+ { name: "tags", type: field_type_1.FieldType.string, isArray: true, optional: true },
461
+ { name: "rank", type: field_type_1.FieldType.number, defaultValue: 5 },
462
+ { name: "priority", type: field_type_1.FieldType.number, optional: true, defaultValue: 2 },
463
+ ],
464
+ };
465
+ const EmptyTable = new sql_data_source_1.SQLDataSource(db, tableType);
466
+ const empty = { note: null, count: undefined, when: null, tags: null };
467
+ beforeAll(async () => {
468
+ await EmptyTable.dropTableIfExists();
469
+ });
470
+ it("leaves out null and undefined optional fields and reads them back unset", async () => {
471
+ const detail = { city: "Austin", zip: null };
472
+ const row = { rowId: (0, utils_1.newid)(), title: "a", ...empty, detail };
473
+ const expected = {
474
+ rowId: row.rowId,
475
+ title: "a",
476
+ detail: { city: "Austin" },
477
+ rank: 5,
478
+ priority: 2,
479
+ };
480
+ expect(await EmptyTable.save(row)).toEqual(expected);
481
+ expect(row).not.toHaveProperty("count");
482
+ expect(await EmptyTable.get(row.rowId)).toEqual(expected);
483
+ const stored = await db.get(`SELECT * FROM "${EmptyTable.tableName}" WHERE rowId = ?`, [
484
+ row.rowId,
485
+ ]);
486
+ expect(stored.note).toBeNull();
487
+ // The caller's nested object is left as it was.
488
+ expect(detail).toEqual({ city: "Austin", zip: null });
489
+ });
490
+ it("clears a field saved empty, and gives an empty field with a default its default", async () => {
491
+ const row = { rowId: (0, utils_1.newid)(), title: "a", note: "set", rank: 7 };
492
+ await EmptyTable.save(row);
493
+ expect(await EmptyTable.get(row.rowId)).toEqual(row);
494
+ expect(await EmptyTable.save({ ...row, note: null, rank: null })).toEqual({
495
+ rowId: row.rowId,
496
+ title: "a",
497
+ rank: 5,
498
+ priority: 2,
499
+ });
500
+ expect(await EmptyTable.get(row.rowId)).toEqual({
501
+ rowId: row.rowId,
502
+ title: "a",
503
+ rank: 5,
504
+ priority: 2,
505
+ });
506
+ });
507
+ it("does the same in synchronous inserts and updates", async () => {
508
+ await EmptyTable.initTable();
509
+ const row = { rowId: (0, utils_1.newid)(), title: "a", note: "set", rank: 7 };
510
+ db.db.transaction(() => EmptyTable.saveSync({ ...row, note: null }))();
511
+ expect(await EmptyTable.get(row.rowId)).toEqual({
512
+ rowId: row.rowId,
513
+ title: "a",
514
+ rank: 7,
515
+ priority: 2,
516
+ });
517
+ db.db.transaction(() => EmptyTable.saveSync({ ...row, rank: null }))();
518
+ expect(await EmptyTable.get(row.rowId)).toEqual({ ...row, rank: 5, priority: 2 });
519
+ });
520
+ it.each([
521
+ null,
522
+ undefined,
523
+ "missing",
524
+ ])("resets an optional default for %s in either update path", async (emptyValue) => {
525
+ const row = { rowId: (0, utils_1.newid)(), title: "a", rank: 7, priority: 9 };
526
+ await EmptyTable.save(row);
527
+ const cleared = {
528
+ rowId: row.rowId,
529
+ title: "a",
530
+ rank: 7,
531
+ ...(emptyValue === "missing" ? {} : { priority: emptyValue }),
532
+ };
533
+ const expected = { rowId: row.rowId, title: "a", rank: 7, priority: 2 };
534
+ expect(await EmptyTable.update({ ...cleared })).toStrictEqual(expected);
535
+ expect(await EmptyTable.get(row.rowId)).toEqual(expected);
536
+ await EmptyTable.update(row);
537
+ expect(EmptyTable.updateSync({ ...cleared })).toStrictEqual(expected);
538
+ expect(await EmptyTable.get(row.rowId)).toEqual(expected);
539
+ });
540
+ it("still refuses an empty required field", async () => {
541
+ const row = { rowId: (0, utils_1.newid)(), title: null };
542
+ await expect(EmptyTable.save(row)).rejects.toThrow(/Expected string/);
543
+ expect(() => EmptyTable.saveSync(row)).toThrow(/Expected string/);
544
+ expect(await EmptyTable.get(row.rowId)).toBeUndefined();
545
+ });
546
+ afterAll(async () => {
547
+ await EmptyTable.dropTableIfExists();
548
+ await EmptyTable.db.close();
549
+ });
550
+ });
551
+ it("keeps legacy nullable enum defaults working without changing their field IR", async () => {
552
+ // FrameMembers.role uses this schema; legacy schemaToFields must remain unchanged.
553
+ const schema = zod_1.z.object({
554
+ rowId: zod_types_1.zodPeerId,
555
+ role: zod_1.z.enum(["observation", "value"]).nullable().default(null),
556
+ });
557
+ const fields = (0, types_1.schemaToFields)(schema);
558
+ expect(fields[1]).toEqual({
559
+ name: "role",
560
+ type: field_type_1.FieldType.any,
561
+ defaultValue: expect.any(Function),
562
+ });
563
+ const source = new sql_data_source_1.SQLDataSource(db, {
564
+ name: "legacy_empty_default",
565
+ description: "Legacy nullable enum default.",
566
+ primaryKeyName: "rowId",
567
+ fields,
568
+ });
569
+ const row = { rowId: (0, utils_1.newid)(), role: null };
570
+ expect(await source.insert(row)).toEqual(row);
571
+ await source.update({ rowId: row.rowId, role: "value" });
572
+ expect(await source.update({ rowId: row.rowId, role: null })).toEqual(row);
573
+ expect(source.updateSync({ rowId: row.rowId, role: null })).toEqual(row);
574
+ await source.dropTableIfExists();
575
+ });
434
576
  });
435
577
  describe("Tools additive catalog migration", () => {
436
578
  test("catalog readiness retries failed SQLite schema initialization on the same source", async () => {
@@ -8,17 +8,13 @@ const table_1 = require("./table");
8
8
  const table_definitions_system_1 = require("./table-definitions.system");
9
9
  const types_1 = require("./types");
10
10
  /**
11
- * Normalize metaData for structural comparison by stripping fields that can differ
12
- * between in-memory and DB-serialized representations without representing a real schema change.
13
- * Specifically, `defaultValue` contains functions that the DB layer serializes as "__FUNCTION ..."
14
- * strings (via serial-json.ts toJSON), causing false hash mismatches on every load.
15
- * `defaultValue` is not part of the SQLite column schema so excluding it is semantically correct.
11
+ * Normalize metaData for structural comparison by keeping only the field keys that represent a
12
+ * schema change ({@link comparableTableFields}). In particular, `defaultValue` contains functions
13
+ * that the DB layer serializes as "__FUNCTION ..." strings (via serial-json.ts toJSON), which
14
+ * would cause false hash mismatches on every load; it is not part of the SQLite column schema.
16
15
  */
17
16
  function normalizeMetaDataForComparison(metaData) {
18
- return {
19
- ...metaData,
20
- fields: metaData.fields.map(({ defaultValue: _omit, ...rest }) => rest),
21
- };
17
+ return { ...metaData, fields: (0, types_1.comparableTableFields)(metaData.fields) };
22
18
  }
23
19
  /**
24
20
  * Compare two table definition versions and decide whether to update.
@@ -319,6 +315,10 @@ class TableContainer {
319
315
  }
320
316
  }
321
317
  for (const [tableName, fallbackDefinition] of Object.entries(this.fallbackDefinitions)) {
318
+ // A package may supersede a fallback's storage policy when it registers a newer schema.
319
+ // Match table resolution: a full definition takes precedence over its stale fallback.
320
+ if (this.tableDefinitions[tableName] || table_definitions_system_1.systemTableDefinitions[tableName])
321
+ continue;
322
322
  if (fallbackDefinition.metaData.localOnly && !names.includes(tableName)) {
323
323
  names.push(tableName);
324
324
  }
@@ -52,6 +52,36 @@ describe("checkVersionedUpdate", () => {
52
52
  expect((0, table_container_1.checkVersionedUpdate)(100, 50, metaA, metaA)).toBe("skip");
53
53
  expect((0, table_container_1.checkVersionedUpdate)(100, 50, metaA, metaB)).toBe("skip");
54
54
  });
55
+ it("ignores richer field metadata at the same version but still reports column changes", () => {
56
+ const report = jest.spyOn(console, "error").mockImplementation(() => { });
57
+ try {
58
+ const existing = {
59
+ ...metaA,
60
+ fields: [{ name: "detail", type: field_type_1.FieldType.object }],
61
+ };
62
+ const enriched = {
63
+ ...existing,
64
+ fields: [
65
+ {
66
+ type: field_type_1.FieldType.object,
67
+ name: "detail",
68
+ fields: [{ name: "mode", type: field_type_1.FieldType.string, enumValues: ["on", "off"] }],
69
+ future: "ignored",
70
+ },
71
+ ],
72
+ };
73
+ expect((0, table_container_1.checkVersionedUpdate)(1, 1, existing, enriched)).toBe("skip");
74
+ expect(report).not.toHaveBeenCalled();
75
+ expect((0, table_container_1.checkVersionedUpdate)(1, 1, existing, {
76
+ ...enriched,
77
+ fields: [{ name: "detail", type: field_type_1.FieldType.string }],
78
+ })).toBe("skip");
79
+ expect(report).toHaveBeenCalledTimes(1);
80
+ }
81
+ finally {
82
+ report.mockRestore();
83
+ }
84
+ });
55
85
  });
56
86
  // --- Tiered table instance tests ---
57
87
  // Custom Table subclass for testing
@@ -226,4 +256,18 @@ describe("getLocalOnlyTableNames", () => {
226
256
  const localOnlyNames = container.getLocalOnlyTableNames();
227
257
  expect(localOnlyNames.filter((name) => name === defRecord.name)).toHaveLength(1);
228
258
  });
259
+ it("lets a synced code definition supersede a local-only fallback", () => {
260
+ const { container, testMetaData, testSchema, defRecord } = createTestHarness();
261
+ container.registerFromTableDefinitionRecord({
262
+ ...defRecord,
263
+ metaData: { ...testMetaData, localOnly: true, versionNumber: 1 },
264
+ versionNumber: 1,
265
+ });
266
+ expect(container.getLocalOnlyTableNames()).toContain(defRecord.name);
267
+ container.registerTableDefinition({
268
+ metaData: { ...testMetaData, versionNumber: 2 },
269
+ schema: testSchema,
270
+ });
271
+ expect(container.getLocalOnlyTableNames()).not.toContain(defRecord.name);
272
+ });
229
273
  });
@@ -26,12 +26,52 @@ export interface ITableMetaData {
26
26
  versionNumber?: number;
27
27
  }
28
28
  /**
29
- * Convert field metadata to the runtime Zod schema used by table validation.
29
+ * Convert field metadata to the runtime Zod schema the Runtime validates with: table rows and
30
+ * tool arguments.
31
+ *
32
+ * It checks types only: `type`, `isArray`, and `optional`. An object field accepts any object
33
+ * and keeps its keys; nested `fields` and `enumValues` aren't checked, so a row or call from a
34
+ * package built against a newer shape isn't refused. {@link fieldsToShapeSchema} builds the full
35
+ * shape. An optional field refuses `null`: remove empty fields first with
36
+ * {@link withoutEmptyFields}.
30
37
  *
31
38
  * @param fields Serializable field definitions.
32
39
  * @param codeSchema Optional author schema whose refinements are intersected.
33
40
  */
34
41
  export declare function fieldsToSchema(fields: IField[], codeSchema?: z.AnyZodObject): z.AnyZodObject;
42
+ /**
43
+ * Convert field metadata to a Zod schema with every shape the IR carries: nested object
44
+ * `fields` and `enumValues`, as well as types. Objects keep keys their `fields` don't list. For
45
+ * describing a tool's arguments to an assistant's model; the Runtime validates with
46
+ * {@link fieldsToSchema}.
47
+ *
48
+ * @param fields Serializable field definitions.
49
+ */
50
+ export declare function fieldsToShapeSchema(fields: IField[]): z.AnyZodObject;
51
+ /**
52
+ * `value` without its empty fields, as the Runtime stores and delivers it: an optional field
53
+ * that is `null` or `undefined` is left out, in nested object `fields` too (including each
54
+ * object in an array). An empty field with a default is removed so parsing fills the default.
55
+ * A required field without a default keeps its value, so validation still refuses an empty
56
+ * one. Values inside an object or array that `fields` doesn't describe are left as they are.
57
+ * Returns `value` itself when nothing is removed, and a copy otherwise; `value` is never changed.
58
+ *
59
+ * @param fields The fields describing `value`.
60
+ * @param value A row, a tool's arguments, or an event payload.
61
+ */
62
+ export declare function withoutEmptyFields<T>(fields: IField[], value: T): T;
63
+ /**
64
+ * The fields of a table definition as table-definition comparisons see them: name, description,
65
+ * type, optional, isArray, and subType, in a fixed order. Left out: `defaultValue` (the database
66
+ * stores a function default as a string), the keys the IR gained later (`fields` and
67
+ * `enumValues`), and keys this SDK doesn't know, so rebuilding a package with a newer SDK doesn't
68
+ * make the same table version look different. A device keeps the definition it stored until the
69
+ * table's `versionNumber` changes.
70
+ *
71
+ * @internal
72
+ * @param fields A table definition's fields.
73
+ */
74
+ export declare function comparableTableFields(fields: IField[]): Partial<IField>[];
35
75
  /**
36
76
  * Convert a Zod object schema to serializable field metadata.
37
77
  *
@@ -1,44 +1,83 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fieldsToSchema = fieldsToSchema;
4
+ exports.fieldsToShapeSchema = fieldsToShapeSchema;
5
+ exports.withoutEmptyFields = withoutEmptyFields;
6
+ exports.comparableTableFields = comparableTableFields;
4
7
  exports.schemaToFields = schemaToFields;
5
8
  const zod_1 = require("zod");
6
9
  const field_type_1 = require("../../types/field-type");
7
10
  const utils_1 = require("../../utils");
8
11
  /**
9
- * Convert field metadata to the runtime Zod schema used by table validation.
12
+ * Convert field metadata to the runtime Zod schema the Runtime validates with: table rows and
13
+ * tool arguments.
14
+ *
15
+ * It checks types only: `type`, `isArray`, and `optional`. An object field accepts any object
16
+ * and keeps its keys; nested `fields` and `enumValues` aren't checked, so a row or call from a
17
+ * package built against a newer shape isn't refused. {@link fieldsToShapeSchema} builds the full
18
+ * shape. An optional field refuses `null`: remove empty fields first with
19
+ * {@link withoutEmptyFields}.
10
20
  *
11
21
  * @param fields Serializable field definitions.
12
22
  * @param codeSchema Optional author schema whose refinements are intersected.
13
23
  */
14
24
  function fieldsToSchema(fields, codeSchema) {
15
- let argsSchema = zod_1.z.object({});
16
- for (const argSchema of fields) {
17
- const { name, description, type, isArray, defaultValue, optional } = argSchema;
18
- let propertyType;
19
- switch (type) {
20
- case "id":
21
- propertyType = zod_1.z.string().refine((id) => (0, utils_1.isid)(id), { message: "Invalid id" });
22
- propertyType._def._fieldType = field_type_1.FieldType.id;
23
- break;
24
- case "string":
25
- propertyType = zod_1.z.string();
26
- break;
27
- case "number":
28
- propertyType = zod_1.z.number();
29
- break;
30
- case "boolean":
31
- propertyType = zod_1.z.boolean();
32
- break;
33
- case "Date":
34
- propertyType = zod_1.z.date();
35
- break;
36
- case "object":
37
- propertyType = zod_1.z.object({});
38
- break;
39
- default:
40
- propertyType = zod_1.z.any();
25
+ return buildFieldsSchema(fields, false, codeSchema);
26
+ }
27
+ /**
28
+ * Convert field metadata to a Zod schema with every shape the IR carries: nested object
29
+ * `fields` and `enumValues`, as well as types. Objects keep keys their `fields` don't list. For
30
+ * describing a tool's arguments to an assistant's model; the Runtime validates with
31
+ * {@link fieldsToSchema}.
32
+ *
33
+ * @param fields Serializable field definitions.
34
+ */
35
+ function fieldsToShapeSchema(fields) {
36
+ return buildFieldsSchema(fields, true);
37
+ }
38
+ function enumSchema(values) {
39
+ if (values.every((value) => typeof value === "string")) {
40
+ return zod_1.z.enum(values);
41
+ }
42
+ const literals = values.map((value) => zod_1.z.literal(value));
43
+ return literals.length === 1
44
+ ? literals[0]
45
+ : zod_1.z.union(literals);
46
+ }
47
+ function fieldValueSchema(field, shapes, open) {
48
+ if (shapes && field.enumValues?.length) {
49
+ return enumSchema(field.enumValues);
50
+ }
51
+ switch (field.type) {
52
+ case "id": {
53
+ const id = zod_1.z.string().refine((value) => (0, utils_1.isid)(value), { message: "Invalid id" });
54
+ id._def._fieldType = field_type_1.FieldType.id;
55
+ return id;
41
56
  }
57
+ case "string":
58
+ return zod_1.z.string();
59
+ case "number":
60
+ return zod_1.z.number();
61
+ case "boolean":
62
+ return zod_1.z.boolean();
63
+ case "Date":
64
+ return zod_1.z.date();
65
+ case "object": {
66
+ const object = shapes && field.fields ? buildFieldsSchema(field.fields, true) : zod_1.z.object({});
67
+ // An author schema intersected with this one parses the value itself; two parses of
68
+ // one object can't be merged, so only an open object keeps its keys.
69
+ return open ? object.passthrough() : object;
70
+ }
71
+ default:
72
+ return zod_1.z.any();
73
+ }
74
+ }
75
+ function buildFieldsSchema(fields, shapes, codeSchema) {
76
+ let argsSchema = zod_1.z.object({});
77
+ for (const field of fields) {
78
+ const { name, description, isArray, defaultValue, optional } = field;
79
+ const shapeField = codeSchema?.shape?.[name];
80
+ let propertyType = fieldValueSchema(field, shapes, !shapeField);
42
81
  if (isArray) {
43
82
  propertyType = propertyType.array();
44
83
  }
@@ -49,7 +88,6 @@ function fieldsToSchema(fields, codeSchema) {
49
88
  propertyType = propertyType.default(defaultValue);
50
89
  }
51
90
  propertyType = propertyType.describe(description ?? "");
52
- const shapeField = codeSchema?.shape?.[name];
53
91
  if (shapeField) {
54
92
  propertyType = zod_1.z.intersection(propertyType, shapeField);
55
93
  }
@@ -58,6 +96,99 @@ function fieldsToSchema(fields, codeSchema) {
58
96
  }
59
97
  return argsSchema;
60
98
  }
99
+ /**
100
+ * `value` without its empty fields, as the Runtime stores and delivers it: an optional field
101
+ * that is `null` or `undefined` is left out, in nested object `fields` too (including each
102
+ * object in an array). An empty field with a default is removed so parsing fills the default.
103
+ * A required field without a default keeps its value, so validation still refuses an empty
104
+ * one. Values inside an object or array that `fields` doesn't describe are left as they are.
105
+ * Returns `value` itself when nothing is removed, and a copy otherwise; `value` is never changed.
106
+ *
107
+ * @param fields The fields describing `value`.
108
+ * @param value A row, a tool's arguments, or an event payload.
109
+ */
110
+ function withoutEmptyFields(fields, value) {
111
+ if (!isPlainRecord(value)) {
112
+ return value;
113
+ }
114
+ let copy;
115
+ for (const field of fields) {
116
+ if (!Object.hasOwn(value, field.name)) {
117
+ continue;
118
+ }
119
+ const current = value[field.name];
120
+ let next = current;
121
+ if (current === null || current === undefined) {
122
+ if (!field.optional && field.defaultValue === undefined) {
123
+ continue;
124
+ }
125
+ next = undefined;
126
+ }
127
+ else if (field.fields) {
128
+ next =
129
+ field.isArray && Array.isArray(current)
130
+ ? withoutEmptyItems(field.fields, current)
131
+ : withoutEmptyFields(field.fields, current);
132
+ }
133
+ if (next === current && next !== undefined) {
134
+ continue;
135
+ }
136
+ copy ??= { ...value };
137
+ if (next === undefined) {
138
+ delete copy[field.name];
139
+ }
140
+ else {
141
+ // Define rather than assign, so a "__proto__" field stays a field.
142
+ Object.defineProperty(copy, field.name, {
143
+ value: next,
144
+ enumerable: true,
145
+ writable: true,
146
+ configurable: true,
147
+ });
148
+ }
149
+ }
150
+ return (copy ?? value);
151
+ }
152
+ function withoutEmptyItems(fields, items) {
153
+ let copy;
154
+ items.forEach((item, index) => {
155
+ const next = withoutEmptyFields(fields, item);
156
+ if (next !== item) {
157
+ copy ??= [...items];
158
+ copy[index] = next;
159
+ }
160
+ });
161
+ return copy ?? items;
162
+ }
163
+ function isPlainRecord(value) {
164
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
165
+ return false;
166
+ }
167
+ const prototype = Object.getPrototypeOf(value);
168
+ return prototype === Object.prototype || prototype === null;
169
+ }
170
+ /**
171
+ * The fields of a table definition as table-definition comparisons see them: name, description,
172
+ * type, optional, isArray, and subType, in a fixed order. Left out: `defaultValue` (the database
173
+ * stores a function default as a string), the keys the IR gained later (`fields` and
174
+ * `enumValues`), and keys this SDK doesn't know, so rebuilding a package with a newer SDK doesn't
175
+ * make the same table version look different. A device keeps the definition it stored until the
176
+ * table's `versionNumber` changes.
177
+ *
178
+ * @internal
179
+ * @param fields A table definition's fields.
180
+ */
181
+ function comparableTableFields(fields) {
182
+ return fields.map((field) => {
183
+ const comparable = {};
184
+ for (const key of ["name", "description", "type", "optional", "isArray", "subType"]) {
185
+ if (field[key] !== undefined) {
186
+ comparable[key] = field[key];
187
+ }
188
+ }
189
+ return comparable;
190
+ });
191
+ }
61
192
  /**
62
193
  * Convert a Zod object schema to serializable field metadata.
63
194
  *
@@ -277,4 +277,133 @@ describe("types", () => {
277
277
  expect(() => schema.parse({ recordId: validId, label: "hello" })).not.toThrow();
278
278
  expect(() => schema.parse({ recordId: "", label: "hello" })).toThrow();
279
279
  });
280
+ describe("the richer IR", () => {
281
+ const fields = [
282
+ { name: "mode", type: field_type_1.FieldType.string, enumValues: ["on", "off"] },
283
+ { name: "note", type: field_type_1.FieldType.string, optional: true },
284
+ {
285
+ name: "where",
286
+ type: field_type_1.FieldType.object,
287
+ fields: [
288
+ { name: "city", type: field_type_1.FieldType.string },
289
+ { name: "unit", type: field_type_1.FieldType.string, enumValues: ["F", "C"], optional: true },
290
+ ],
291
+ },
292
+ { name: "tags", type: field_type_1.FieldType.object, isArray: true },
293
+ ];
294
+ const valid = {
295
+ mode: "on",
296
+ where: { city: "Austin", unit: "F", extra: 1 },
297
+ tags: [{ a: 1 }],
298
+ };
299
+ it("fieldsToSchema checks types and keeps an object's keys", () => {
300
+ const schema = (0, types_1.fieldsToSchema)(fields);
301
+ expect(schema.parse(valid)).toEqual(valid);
302
+ // An empty optional field is removed first (withoutEmptyFields); null itself isn't a value.
303
+ expect(schema.safeParse({ ...valid, note: null }).success).toBe(false);
304
+ // Enum values and nested fields describe; the Runtime doesn't enforce them.
305
+ expect(schema.safeParse({ ...valid, mode: "dim", where: { city: 3 } }).success).toBe(true);
306
+ expect(schema.safeParse({ ...valid, mode: 1 }).success).toBe(false);
307
+ expect(schema.safeParse({ ...valid, mode: null }).success).toBe(false);
308
+ expect(schema.safeParse({ ...valid, where: "Austin" }).success).toBe(false);
309
+ });
310
+ it("fieldsToSchema still defers an object to an intersected author schema", () => {
311
+ const codeSchema = zod_1.z.object({ when: zod_1.z.object({ at: zod_1.z.coerce.date() }) });
312
+ const schema = (0, types_1.fieldsToSchema)([{ name: "when", type: field_type_1.FieldType.object }], codeSchema);
313
+ expect(schema.parse({ when: { at: "2026-01-01" } }).when.at).toBeInstanceOf(Date);
314
+ });
315
+ it("fieldsToShapeSchema checks enum values and nested fields", () => {
316
+ const schema = (0, types_1.fieldsToShapeSchema)(fields);
317
+ expect(schema.parse(valid)).toEqual(valid);
318
+ expect(schema.safeParse({ ...valid, mode: "dim" }).success).toBe(false);
319
+ expect(schema.safeParse({ ...valid, where: { city: 3 } }).success).toBe(false);
320
+ expect(schema.safeParse({ ...valid, where: { city: "Austin", unit: "K" } }).success).toBe(false);
321
+ const mixed = (0, types_1.fieldsToShapeSchema)([
322
+ { name: "size", type: field_type_1.FieldType.any, enumValues: [1, "large", true] },
323
+ { name: "one", type: field_type_1.FieldType.number, enumValues: [3] },
324
+ ]);
325
+ expect(mixed.safeParse({ size: true, one: 3 }).success).toBe(true);
326
+ expect(mixed.safeParse({ size: false, one: 3 }).success).toBe(false);
327
+ expect(mixed.safeParse({ size: 1, one: 4 }).success).toBe(false);
328
+ });
329
+ it("comparableTableFields keeps only the keys a table version is compared on", () => {
330
+ expect((0, types_1.comparableTableFields)([
331
+ {
332
+ type: field_type_1.FieldType.object,
333
+ name: "where",
334
+ defaultValue: {},
335
+ fields: [],
336
+ enumValues: ["x"],
337
+ optional: true,
338
+ future: 1,
339
+ },
340
+ ])).toEqual([{ name: "where", type: field_type_1.FieldType.object, optional: true }]);
341
+ });
342
+ });
343
+ describe("withoutEmptyFields", () => {
344
+ const fields = [
345
+ { name: "title", type: field_type_1.FieldType.string },
346
+ { name: "note", type: field_type_1.FieldType.string, optional: true },
347
+ {
348
+ name: "where",
349
+ type: field_type_1.FieldType.object,
350
+ optional: true,
351
+ fields: [
352
+ { name: "city", type: field_type_1.FieldType.string },
353
+ { name: "unit", type: field_type_1.FieldType.string, optional: true },
354
+ ],
355
+ },
356
+ {
357
+ name: "stops",
358
+ type: field_type_1.FieldType.object,
359
+ isArray: true,
360
+ fields: [{ name: "at", type: field_type_1.FieldType.string, optional: true }],
361
+ },
362
+ { name: "data", type: field_type_1.FieldType.any, optional: true },
363
+ ];
364
+ it("removes null and undefined optional fields, in declared nested fields too", () => {
365
+ const value = {
366
+ title: "a",
367
+ note: null,
368
+ where: { city: "Austin", unit: null },
369
+ stops: [{ at: null }, { at: "noon" }],
370
+ data: { inner: null },
371
+ extra: null,
372
+ };
373
+ expect((0, types_1.withoutEmptyFields)(fields, value)).toEqual({
374
+ title: "a",
375
+ where: { city: "Austin" },
376
+ stops: [{}, { at: "noon" }],
377
+ // Values the fields don't describe, and keys they don't list, are left as they are.
378
+ data: { inner: null },
379
+ extra: null,
380
+ });
381
+ expect(value.note).toBeNull();
382
+ expect(value.where.unit).toBeNull();
383
+ expect((0, types_1.withoutEmptyFields)(fields, { ...value, note: undefined, where: null })).not.toHaveProperty("where");
384
+ });
385
+ it("keeps an empty required field, and returns the value itself when nothing is removed", () => {
386
+ const required = { title: null, stops: [] };
387
+ expect((0, types_1.withoutEmptyFields)(fields, required)).toBe(required);
388
+ const clean = { title: "a", where: { city: "Austin" }, stops: [{ at: "noon" }] };
389
+ expect((0, types_1.withoutEmptyFields)(fields, clean)).toBe(clean);
390
+ expect((0, types_1.withoutEmptyFields)(fields, null)).toBeNull();
391
+ expect((0, types_1.withoutEmptyFields)(fields, ["a"])).toEqual(["a"]);
392
+ });
393
+ it("deletes own undefined keys at every described depth", () => {
394
+ const value = { note: undefined, where: { unit: undefined }, stops: [{ at: undefined }] };
395
+ const cleaned = (0, types_1.withoutEmptyFields)(fields, value);
396
+ expect(cleaned).toStrictEqual({ where: {}, stops: [{}] });
397
+ expect(value).toHaveProperty("note");
398
+ expect(value.where).toHaveProperty("unit");
399
+ });
400
+ it("lets parsing fill a default for null just as for a missing field", () => {
401
+ const defaulted = [{ name: "rank", type: field_type_1.FieldType.number, defaultValue: 5 }];
402
+ for (const value of [{ rank: null }, { rank: undefined }, {}]) {
403
+ const cleaned = (0, types_1.withoutEmptyFields)(defaulted, value);
404
+ expect(cleaned).toStrictEqual({});
405
+ expect((0, types_1.fieldsToSchema)(defaulted).parse(cleaned)).toStrictEqual({ rank: 5 });
406
+ }
407
+ });
408
+ });
280
409
  });