@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.
- package/README.md +3 -1
- package/bin/peers-package.js +13 -0
- package/define/package.json +4 -0
- package/dist/contracts/__tests__/action-dispatch.test.js +56 -76
- package/dist/contracts/__tests__/also-implements.test.d.ts +1 -0
- package/dist/contracts/__tests__/also-implements.test.js +110 -0
- package/dist/contracts/__tests__/builder.test.js +45 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +134 -4
- package/dist/contracts/__tests__/frozen-contracts.test.d.ts +1 -0
- package/dist/contracts/__tests__/frozen-contracts.test.js +191 -0
- package/dist/contracts/__tests__/generic-tool-dispatch.test.js +13 -56
- package/dist/contracts/__tests__/host-contract-client.test.d.ts +1 -0
- package/dist/contracts/__tests__/host-contract-client.test.js +793 -0
- package/dist/contracts/__tests__/host-contract-transport.test.js +15 -0
- package/dist/contracts/__tests__/registry.test.js +68 -0
- package/dist/contracts/__tests__/target-dispatch.d.ts +11 -0
- package/dist/contracts/__tests__/target-dispatch.js +24 -0
- package/dist/contracts/__tests__/tool-catalog-service.test.js +37 -1
- package/dist/contracts/__tests__/tool-catalog.test.js +12 -3
- package/dist/contracts/__tests__/validate.test.js +177 -7
- package/dist/contracts/__tests__/workflow-context.test.js +0 -11
- package/dist/contracts/builder.d.ts +16 -2
- package/dist/contracts/builder.js +44 -3
- package/dist/contracts/contract-definition-schema.d.ts +251 -0
- package/dist/contracts/contract-definition-schema.js +48 -0
- package/dist/contracts/contract-provider-router.d.ts +2 -0
- package/dist/contracts/contract-provider-router.js +5 -2
- package/dist/contracts/contract-proxy.d.ts +52 -18
- package/dist/contracts/contract-proxy.js +144 -203
- package/dist/contracts/contract-resolvers.d.ts +6 -4
- package/dist/contracts/contract-resolvers.js +32 -9
- package/dist/contracts/frozen-contracts.d.ts +59 -0
- package/dist/contracts/frozen-contracts.js +86 -0
- package/dist/contracts/host-contract-client.d.ts +134 -0
- package/dist/contracts/host-contract-client.js +396 -0
- package/dist/contracts/host-contract-transport.d.ts +2 -0
- package/dist/contracts/host-contract-transport.js +8 -0
- package/dist/contracts/index.d.ts +6 -4
- package/dist/contracts/index.js +11 -5
- package/dist/contracts/registry.d.ts +31 -1
- package/dist/contracts/registry.js +100 -6
- package/dist/contracts/system/contract-catalog-contract.d.ts +61 -6
- package/dist/contracts/system/contract-catalog-contract.js +17 -5
- package/dist/contracts/tool-catalog-service.js +26 -6
- package/dist/contracts/tool-catalog.d.ts +1 -1
- package/dist/contracts/tool-catalog.js +4 -1
- package/dist/contracts/types.d.ts +32 -2
- package/dist/contracts/validate.d.ts +16 -1
- package/dist/contracts/validate.js +139 -25
- package/dist/data/change-tracking.d.ts +17 -5
- package/dist/data/change-tracking.js +22 -6
- package/dist/data/change-tracking.test.d.ts +1 -0
- package/dist/data/change-tracking.test.js +62 -0
- package/dist/data/group-members.d.ts +6 -1
- package/dist/data/group-members.js +12 -4
- package/dist/data/groups.d.ts +6 -1
- package/dist/data/groups.js +12 -4
- package/dist/data/orm/sql.data-source.d.ts +9 -0
- package/dist/data/orm/sql.data-source.js +47 -2
- package/dist/data/orm/sql.data-source.test.js +142 -0
- package/dist/data/orm/table-container.js +9 -9
- package/dist/data/orm/table-container.test.js +44 -0
- package/dist/data/orm/types.d.ts +41 -1
- package/dist/data/orm/types.js +159 -28
- package/dist/data/orm/types.test.js +129 -0
- package/dist/data/package-versions.d.ts +5 -1
- package/dist/data/package-versions.js +11 -4
- package/dist/data/packages.d.ts +7 -1
- package/dist/data/packages.js +13 -4
- package/dist/data/peer-types.d.ts +8 -62
- package/dist/data/persistent-vars.d.ts +4 -1
- package/dist/data/persistent-vars.js +7 -2
- package/dist/data/table-signing.test.d.ts +1 -0
- package/dist/data/table-signing.test.js +118 -0
- package/dist/data/tools.d.ts +29 -221
- package/dist/data/users.d.ts +1 -2
- package/dist/data/users.js +8 -5
- package/dist/define/also-implements.test.d.ts +1 -0
- package/dist/define/also-implements.test.js +123 -0
- package/dist/define/context.d.ts +77 -0
- package/dist/define/context.js +2 -0
- package/dist/define/contract-ref.d.ts +177 -0
- package/dist/define/contract-ref.js +67 -0
- package/dist/define/declare-contract.d.ts +167 -0
- package/dist/define/declare-contract.js +220 -0
- package/dist/define/declare-contract.test.d.ts +1 -0
- package/dist/define/declare-contract.test.js +573 -0
- package/dist/define/define-package.d.ts +203 -0
- package/dist/define/define-package.js +289 -0
- package/dist/define/define.test.d.ts +1 -0
- package/dist/define/define.test.js +1010 -0
- package/dist/define/empty-fields.d.ts +15 -0
- package/dist/define/empty-fields.js +134 -0
- package/dist/define/empty-fields.test.d.ts +1 -0
- package/dist/define/empty-fields.test.js +55 -0
- package/dist/define/events-observables.test.d.ts +1 -0
- package/dist/define/events-observables.test.js +1013 -0
- package/dist/define/events.d.ts +81 -0
- package/dist/define/events.js +81 -0
- package/dist/define/generated-field.d.ts +20 -0
- package/dist/define/generated-field.js +25 -0
- package/dist/define/index.d.ts +28 -0
- package/dist/define/index.js +62 -0
- package/dist/define/lazy-members.d.ts +8 -0
- package/dist/define/lazy-members.js +38 -0
- package/dist/define/manifest.d.ts +63 -0
- package/dist/define/manifest.js +653 -0
- package/dist/define/observables.d.ts +146 -0
- package/dist/define/observables.js +187 -0
- package/dist/define/serve-members.d.ts +21 -0
- package/dist/define/serve-members.js +152 -0
- package/dist/define/serve-members.test.d.ts +1 -0
- package/dist/define/serve-members.test.js +630 -0
- package/dist/define/serve.d.ts +22 -0
- package/dist/define/serve.js +58 -0
- package/dist/define/system-contracts.d.ts +41 -0
- package/dist/define/system-contracts.js +21 -0
- package/dist/define/table-handle.d.ts +13 -0
- package/dist/define/table-handle.js +36 -0
- package/dist/define/table.d.ts +128 -0
- package/dist/define/table.js +122 -0
- package/dist/define/tool.d.ts +129 -0
- package/dist/define/tool.js +91 -0
- package/dist/define/vars.d.ts +82 -0
- package/dist/define/vars.js +256 -0
- package/dist/define/vars.test.d.ts +1 -0
- package/dist/define/vars.test.js +39 -0
- package/dist/define/wire-value.d.ts +12 -0
- package/dist/define/wire-value.js +76 -0
- package/dist/define/worker-state.d.ts +15 -0
- package/dist/define/worker-state.js +45 -0
- package/dist/device/tx-encoding.test.js +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -1
- package/dist/observable.d.ts +42 -2
- package/dist/observable.js +25 -7
- package/dist/package-build/build-package.d.ts +60 -0
- package/dist/package-build/build-package.js +312 -0
- package/dist/package-build/build-package.test.d.ts +1 -0
- package/dist/package-build/build-package.test.js +204 -0
- package/dist/package-build/cli.d.ts +7 -0
- package/dist/package-build/cli.js +218 -0
- package/dist/package-build/contract-add.d.ts +35 -0
- package/dist/package-build/contract-add.js +97 -0
- package/dist/package-build/contract-add.test.d.ts +1 -0
- package/dist/package-build/contract-add.test.js +467 -0
- package/dist/package-build/contract-source.d.ts +59 -0
- package/dist/package-build/contract-source.js +471 -0
- package/dist/package-build/freeze-contracts.d.ts +56 -0
- package/dist/package-build/freeze-contracts.js +198 -0
- package/dist/package-build/freeze-contracts.test.d.ts +1 -0
- package/dist/package-build/freeze-contracts.test.js +355 -0
- package/dist/package-installer/package-creator.d.ts +4 -0
- package/dist/package-installer/package-creator.js +57 -16
- package/dist/package-installer/package-creator.test.js +86 -0
- package/dist/package-loader/contract-members.d.ts +53 -0
- package/dist/package-loader/contract-members.js +31 -0
- package/dist/package-loader/contract-package-loader.js +5 -1
- package/dist/package-loader/contract-package-loader.test.js +11 -0
- package/dist/package-loader/host-authority-loading.test.js +9 -5
- package/dist/package-loader/index.d.ts +1 -0
- package/dist/package-loader/index.js +1 -0
- package/dist/package-loader/isolated-artifact.d.ts +80 -17
- package/dist/package-loader/isolated-artifact.js +257 -51
- package/dist/package-loader/isolated-artifact.test.js +294 -21
- package/dist/package-loader/isolated-runtime.d.ts +22 -7
- package/dist/package-loader/isolated-table-gateway.js +30 -11
- package/dist/package-loader/isolated-table-gateway.test.js +243 -0
- package/dist/package-loader/package-loader.d.ts +8 -1
- package/dist/package-loader/package-loader.js +39 -20
- package/dist/package-loader/package-loader.test.js +47 -0
- package/dist/peer-id.d.ts +25 -0
- package/dist/peer-id.js +99 -0
- package/dist/serial-json.compat.test.d.ts +1 -0
- package/dist/serial-json.compat.test.js +369 -0
- package/dist/serial-json.d.ts +41 -5
- package/dist/serial-json.js +216 -157
- package/dist/serial-json.oracle.d.ts +21 -0
- package/dist/serial-json.oracle.js +222 -0
- package/dist/serial-json.test.js +315 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +8 -0
- package/dist/testing/mock-host-contract.d.ts +123 -0
- package/dist/testing/mock-host-contract.js +434 -0
- package/dist/testing/mock-host-contract.test.d.ts +1 -0
- package/dist/testing/mock-host-contract.test.js +782 -0
- package/dist/types/field-type.d.ts +35 -18
- package/dist/types/field-type.js +4 -1
- package/dist/types/zod-types.d.ts +9 -0
- package/dist/types/zod-types.js +8 -2
- package/dist/utils.d.ts +1 -25
- package/dist/utils.js +10 -90
- package/package.json +9 -2
- 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.
|
|
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
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
});
|
package/dist/data/orm/types.d.ts
CHANGED
|
@@ -26,12 +26,52 @@ export interface ITableMetaData {
|
|
|
26
26
|
versionNumber?: number;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Convert field metadata to the runtime Zod schema
|
|
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
|
*
|
package/dist/data/orm/types.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
*
|