@peers-app/peers-sdk 0.26.5 → 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 (194) 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/group-members.d.ts +6 -1
  55. package/dist/data/group-members.js +12 -4
  56. package/dist/data/groups.d.ts +6 -1
  57. package/dist/data/groups.js +12 -4
  58. package/dist/data/orm/sql.data-source.d.ts +9 -0
  59. package/dist/data/orm/sql.data-source.js +47 -2
  60. package/dist/data/orm/sql.data-source.test.js +142 -0
  61. package/dist/data/orm/table-container.js +9 -9
  62. package/dist/data/orm/table-container.test.js +44 -0
  63. package/dist/data/orm/types.d.ts +41 -1
  64. package/dist/data/orm/types.js +159 -28
  65. package/dist/data/orm/types.test.js +129 -0
  66. package/dist/data/package-versions.d.ts +5 -1
  67. package/dist/data/package-versions.js +11 -4
  68. package/dist/data/packages.d.ts +7 -1
  69. package/dist/data/packages.js +13 -4
  70. package/dist/data/peer-types.d.ts +8 -62
  71. package/dist/data/persistent-vars.d.ts +4 -1
  72. package/dist/data/persistent-vars.js +7 -2
  73. package/dist/data/table-signing.test.d.ts +1 -0
  74. package/dist/data/table-signing.test.js +118 -0
  75. package/dist/data/tools.d.ts +29 -221
  76. package/dist/data/users.d.ts +1 -2
  77. package/dist/data/users.js +8 -5
  78. package/dist/define/also-implements.test.d.ts +1 -0
  79. package/dist/define/also-implements.test.js +123 -0
  80. package/dist/define/context.d.ts +77 -0
  81. package/dist/define/context.js +2 -0
  82. package/dist/define/contract-ref.d.ts +177 -0
  83. package/dist/define/contract-ref.js +67 -0
  84. package/dist/define/declare-contract.d.ts +167 -0
  85. package/dist/define/declare-contract.js +220 -0
  86. package/dist/define/declare-contract.test.d.ts +1 -0
  87. package/dist/define/declare-contract.test.js +573 -0
  88. package/dist/define/define-package.d.ts +203 -0
  89. package/dist/define/define-package.js +289 -0
  90. package/dist/define/define.test.d.ts +1 -0
  91. package/dist/define/define.test.js +1010 -0
  92. package/dist/define/empty-fields.d.ts +15 -0
  93. package/dist/define/empty-fields.js +134 -0
  94. package/dist/define/empty-fields.test.d.ts +1 -0
  95. package/dist/define/empty-fields.test.js +55 -0
  96. package/dist/define/events-observables.test.d.ts +1 -0
  97. package/dist/define/events-observables.test.js +1013 -0
  98. package/dist/define/events.d.ts +81 -0
  99. package/dist/define/events.js +81 -0
  100. package/dist/define/generated-field.d.ts +20 -0
  101. package/dist/define/generated-field.js +25 -0
  102. package/dist/define/index.d.ts +28 -0
  103. package/dist/define/index.js +62 -0
  104. package/dist/define/lazy-members.d.ts +8 -0
  105. package/dist/define/lazy-members.js +38 -0
  106. package/dist/define/manifest.d.ts +63 -0
  107. package/dist/define/manifest.js +653 -0
  108. package/dist/define/observables.d.ts +146 -0
  109. package/dist/define/observables.js +187 -0
  110. package/dist/define/serve-members.d.ts +21 -0
  111. package/dist/define/serve-members.js +152 -0
  112. package/dist/define/serve-members.test.d.ts +1 -0
  113. package/dist/define/serve-members.test.js +630 -0
  114. package/dist/define/serve.d.ts +22 -0
  115. package/dist/define/serve.js +58 -0
  116. package/dist/define/system-contracts.d.ts +41 -0
  117. package/dist/define/system-contracts.js +21 -0
  118. package/dist/define/table-handle.d.ts +13 -0
  119. package/dist/define/table-handle.js +36 -0
  120. package/dist/define/table.d.ts +128 -0
  121. package/dist/define/table.js +122 -0
  122. package/dist/define/tool.d.ts +129 -0
  123. package/dist/define/tool.js +91 -0
  124. package/dist/define/vars.d.ts +82 -0
  125. package/dist/define/vars.js +256 -0
  126. package/dist/define/vars.test.d.ts +1 -0
  127. package/dist/define/vars.test.js +39 -0
  128. package/dist/define/wire-value.d.ts +12 -0
  129. package/dist/define/wire-value.js +76 -0
  130. package/dist/define/worker-state.d.ts +15 -0
  131. package/dist/define/worker-state.js +45 -0
  132. package/dist/device/tx-encoding.test.js +10 -0
  133. package/dist/index.d.ts +3 -0
  134. package/dist/index.js +5 -1
  135. package/dist/observable.d.ts +42 -2
  136. package/dist/observable.js +25 -7
  137. package/dist/package-build/build-package.d.ts +60 -0
  138. package/dist/package-build/build-package.js +312 -0
  139. package/dist/package-build/build-package.test.d.ts +1 -0
  140. package/dist/package-build/build-package.test.js +204 -0
  141. package/dist/package-build/cli.d.ts +7 -0
  142. package/dist/package-build/cli.js +218 -0
  143. package/dist/package-build/contract-add.d.ts +35 -0
  144. package/dist/package-build/contract-add.js +97 -0
  145. package/dist/package-build/contract-add.test.d.ts +1 -0
  146. package/dist/package-build/contract-add.test.js +467 -0
  147. package/dist/package-build/contract-source.d.ts +59 -0
  148. package/dist/package-build/contract-source.js +471 -0
  149. package/dist/package-build/freeze-contracts.d.ts +56 -0
  150. package/dist/package-build/freeze-contracts.js +198 -0
  151. package/dist/package-build/freeze-contracts.test.d.ts +1 -0
  152. package/dist/package-build/freeze-contracts.test.js +355 -0
  153. package/dist/package-installer/package-creator.d.ts +4 -0
  154. package/dist/package-installer/package-creator.js +57 -16
  155. package/dist/package-installer/package-creator.test.js +86 -0
  156. package/dist/package-loader/contract-members.d.ts +53 -0
  157. package/dist/package-loader/contract-members.js +31 -0
  158. package/dist/package-loader/contract-package-loader.js +5 -1
  159. package/dist/package-loader/contract-package-loader.test.js +11 -0
  160. package/dist/package-loader/host-authority-loading.test.js +9 -5
  161. package/dist/package-loader/index.d.ts +1 -0
  162. package/dist/package-loader/index.js +1 -0
  163. package/dist/package-loader/isolated-artifact.d.ts +80 -17
  164. package/dist/package-loader/isolated-artifact.js +257 -51
  165. package/dist/package-loader/isolated-artifact.test.js +294 -21
  166. package/dist/package-loader/isolated-runtime.d.ts +22 -7
  167. package/dist/package-loader/isolated-table-gateway.js +30 -11
  168. package/dist/package-loader/isolated-table-gateway.test.js +243 -0
  169. package/dist/package-loader/package-loader.d.ts +8 -1
  170. package/dist/package-loader/package-loader.js +39 -20
  171. package/dist/package-loader/package-loader.test.js +47 -0
  172. package/dist/peer-id.d.ts +25 -0
  173. package/dist/peer-id.js +99 -0
  174. package/dist/serial-json.compat.test.d.ts +1 -0
  175. package/dist/serial-json.compat.test.js +369 -0
  176. package/dist/serial-json.d.ts +41 -5
  177. package/dist/serial-json.js +216 -157
  178. package/dist/serial-json.oracle.d.ts +21 -0
  179. package/dist/serial-json.oracle.js +222 -0
  180. package/dist/serial-json.test.js +315 -0
  181. package/dist/testing/index.d.ts +4 -0
  182. package/dist/testing/index.js +8 -0
  183. package/dist/testing/mock-host-contract.d.ts +123 -0
  184. package/dist/testing/mock-host-contract.js +434 -0
  185. package/dist/testing/mock-host-contract.test.d.ts +1 -0
  186. package/dist/testing/mock-host-contract.test.js +782 -0
  187. package/dist/types/field-type.d.ts +35 -18
  188. package/dist/types/field-type.js +4 -1
  189. package/dist/types/zod-types.d.ts +9 -0
  190. package/dist/types/zod-types.js +8 -2
  191. package/dist/utils.d.ts +1 -25
  192. package/dist/utils.js +10 -90
  193. package/package.json +9 -2
  194. package/testing/package.json +4 -0
@@ -258,6 +258,7 @@ class SQLDataSource {
258
258
  // @ts-expect-error - need to figure out how to get typescript to understand what's going on here
259
259
  record[this.metaData.primaryKeyName] = (0, utils_1.newid)();
260
260
  }
261
+ this.removeEmptyFields(record);
261
262
  this.applySchemaDefaults(record);
262
263
  const fields = this.metaData.fields.map((field) => `"${field.name}"`).join(", ");
263
264
  const values = this.metaData.fields.map((field) => jsValueToSqlValue(record[field.name]));
@@ -276,8 +277,9 @@ class SQLDataSource {
276
277
  // Explicit catalog patches write only their own columns. Keep ordinary row-save
277
278
  // semantics (omitted optional fields clear) unchanged for all other writes.
278
279
  const catalogPatch = this.tableName === "Tools" && (0, tool_catalog_fields_1.isToolCatalogPatch)(record);
280
+ const defaulted = catalogPatch ? [] : this.removeEmptyFields(record);
279
281
  const updated = catalogPatch ? { ...(await this.get(primaryKey)), ...record } : record;
280
- this.schema.parse(updated);
282
+ this.applyDefaults(record, this.schema.parse(updated), defaulted);
281
283
  const updateFields = this.fieldsForUpdate(record).filter((field) => !catalogPatch || Object.hasOwn(record, field.name));
282
284
  if (updateFields.length === 0) {
283
285
  return updated;
@@ -327,6 +329,47 @@ class SQLDataSource {
327
329
  }
328
330
  }
329
331
  }
332
+ /**
333
+ * Remove `record`'s empty fields in place, so a row is written and returned as it's stored:
334
+ * an optional field that is `null` or `undefined` is left out, and so is an empty field with a
335
+ * default, which gets the default. Returns the defaulted fields' names. A required field
336
+ * without a default keeps its value for validation to refuse.
337
+ */
338
+ removeEmptyFields(record) {
339
+ const writable = record;
340
+ const cleaned = (0, types_1.withoutEmptyFields)(this.metaData.fields, writable);
341
+ const defaulted = [];
342
+ for (const field of this.metaData.fields) {
343
+ // A missing optional column is cleared on update too. Its default must win over NULL;
344
+ // a missing required column still keeps the stored value for older syncing peers.
345
+ if (field.defaultValue !== undefined &&
346
+ writable[field.name] == null &&
347
+ (Object.hasOwn(writable, field.name) || field.optional)) {
348
+ defaulted.push(field.name);
349
+ }
350
+ if (!Object.hasOwn(writable, field.name)) {
351
+ continue;
352
+ }
353
+ if (!Object.hasOwn(cleaned, field.name)) {
354
+ delete writable[field.name];
355
+ }
356
+ else if (cleaned[field.name] !== writable[field.name]) {
357
+ writable[field.name] = cleaned[field.name];
358
+ }
359
+ }
360
+ return defaulted;
361
+ }
362
+ /** Copy the parsed defaults requested by {@link removeEmptyFields} onto `record`. */
363
+ applyDefaults(record, parsed, names) {
364
+ for (const name of names) {
365
+ if (parsed[name] === undefined) {
366
+ delete record[name];
367
+ }
368
+ else {
369
+ record[name] = parsed[name];
370
+ }
371
+ }
372
+ }
330
373
  /**
331
374
  * Columns to SET on update. Required fields that are missing are omitted so
332
375
  * older peers without a new column do not overwrite a local value with NULL.
@@ -366,6 +409,7 @@ class SQLDataSource {
366
409
  // @ts-expect-error - need to figure out how to get typescript to understand what's going on here
367
410
  record[this.metaData.primaryKeyName] = (0, utils_1.newid)();
368
411
  }
412
+ this.removeEmptyFields(record);
369
413
  this.applySchemaDefaults(record);
370
414
  const fields = this.metaData.fields.map((field) => `"${field.name}"`).join(", ");
371
415
  const values = this.metaData.fields.map((field) => jsValueToSqlValue(record[field.name]));
@@ -383,7 +427,8 @@ class SQLDataSource {
383
427
  // @ts-expect-error - need to figure out how to get typescript to understand what's going on here
384
428
  record[this.metaData.primaryKeyName] = (0, utils_1.newid)();
385
429
  }
386
- this.schema.parse(record);
430
+ const defaulted = this.removeEmptyFields(record);
431
+ this.applyDefaults(record, this.schema.parse(record), defaulted);
387
432
  const primaryKey = record[this.metaData.primaryKeyName];
388
433
  const updateFields = this.fieldsForUpdate(record);
389
434
  if (updateFields.length === 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
  *