From 9fd20342ccbb8108aeda8ee6fe2dee4e3ca05b4c Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sat, 31 Oct 2026 00:25:46 +0100 Subject: [PATCH 01/45] refactor: clear the avoidable ratchet findings of pack 0.2.17 (#50) * refactor: look up the index groups, container keys, floating values and schemas by own key * refactor: read class prototypes through a checked helper * refactor(modifiers): give the event handler alias no type parameter * refactor(encryption): check that the auth-tag methods are callable * refactor(table): collect the mixins without reading any * refactor(database): insert the fixture rows without an any local * test: read the stored rows, the module exports and the sort keys without any * chore: lower the ratchet ceilings * refactor(modifiers): type the receiver of the generated accessors * chore: lower the no-unsafe-return ceiling * refactor(records): look up the unlock arguments and the floating values by own key * chore: lower the no-inherited-key-lookup ceiling * refactor(table): assert the mixin subclass to the declared return type * refactor(modifiers): restore the container unlock lookup to its base form --- guardrails.ratchet.json | 11 +++++------ src/common.ts | 8 +++++--- src/database.ts | 6 ++++-- src/modifiers/common.ts | 27 ++++++++++----------------- src/modifiers/encryption.ts | 5 +++-- src/modifiers/internal/kinds.ts | 17 +++++++++++++++++ src/modifiers/internal/records.ts | 23 +++++++++++++++++++++-- src/schema.ts | 11 +++++------ src/table.ts | 19 ++++++++++++++----- src/tests/database.test.ts | 3 ++- src/tests/helpers/other-instance.ts | 4 ++-- src/tests/integration.test.ts | 15 +++++++++------ 12 files changed, 97 insertions(+), 52 deletions(-) diff --git a/guardrails.ratchet.json b/guardrails.ratchet.json index 711bfd2..7e303ca 100644 --- a/guardrails.ratchet.json +++ b/guardrails.ratchet.json @@ -1,8 +1,7 @@ { "pack": "0.2.17", "rules": { - "antelopejs/no-inherited-key-lookup": 9, - "antelopejs/no-presence-only-type-guard": 2, + "antelopejs/no-inherited-key-lookup": 1, "anti-slop/no-chained-type-assertions": 1, "anti-slop/no-known-value-widening": 2, "anti-slop/no-runtime-typeof": 1, @@ -10,10 +9,10 @@ "anti-slop/no-unknown-returns": 1, "anti-slop/no-unsafe-dictionary-type": 18, "anti-slop/require-safety-comment-for-type-assertion": 15, - "typescript/no-unnecessary-type-parameters": 8, - "typescript/no-unsafe-argument": 18, - "typescript/no-unsafe-assignment": 16, - "typescript/no-unsafe-return": 17 + "typescript/no-unnecessary-type-parameters": 7, + "typescript/no-unsafe-argument": 15, + "typescript/no-unsafe-assignment": 10, + "typescript/no-unsafe-return": 12 }, "oversizeFiles": {} } diff --git a/src/common.ts b/src/common.ts index 870f0b8..f5232df 100644 --- a/src/common.ts +++ b/src/common.ts @@ -1,6 +1,8 @@ import type { Schema } from "@antelopejs/interface-database"; import type { FieldType } from "@antelopejs/interface-database/schema"; +import { isObjectLike } from "./modifiers/internal/kinds"; + export type Constructible = new ( ...args: A ) => T; @@ -54,7 +56,7 @@ export class DatumStaticMetadata { isCrossInstance: boolean = false, isUnique: boolean = false, ) { - if (!(group in this.indexes)) { + if (!Object.hasOwn(this.indexes, group)) { this.indexes[group] = []; } this.indexes[group].push(key); @@ -79,9 +81,9 @@ export function getMetadata( return existingMetadata; } - const prototype: object | null = Object.getPrototypeOf(target); + const prototype: unknown = Object.getPrototypeOf(target); const inheritedMetadata = - inherit && prototype + inherit && isObjectLike(prototype) ? (Reflect.getOwnMetadata(metadataClass.key, prototype) as | TMetadata | undefined) diff --git a/src/database.ts b/src/database.ts index 37c240d..b94c0c5 100644 --- a/src/database.ts +++ b/src/database.ts @@ -98,8 +98,10 @@ async function insertFixtureData( if (rows.length === 0) { return; } - const payload: any = rows.length === 1 ? rows[0] : rows; - await schema.instance().table(tableName).insert(payload); + await schema + .instance() + .table(tableName) + .insert(rows.length === 1 ? rows[0] : rows); } function toFixtureRows( diff --git a/src/modifiers/common.ts b/src/modifiers/common.ts index 6d8079c..be3fbda 100644 --- a/src/modifiers/common.ts +++ b/src/modifiers/common.ts @@ -8,6 +8,7 @@ import { isOnlyEvents, isTwoWay, isWriteOnly, + prototypeOf, } from "./internal/kinds"; import { dataByKey, @@ -15,6 +16,8 @@ import { fieldsByKey, floatingByKey, floatingOf, + getInternal, + type InternalHolder, unlockArgsOf, withRef, } from "./internal/records"; @@ -116,9 +119,9 @@ interface InternalType { data: Record; } -type ModifierEvent = ( +type ModifierEvent = ( this: Modifier, - object: T, + object: { constructor: any }, field: PropertyKey, ...args: unknown[] ) => void; @@ -181,7 +184,7 @@ export class ModifiersDynamicMetadata { } get(field: PropertyKey, internal: InternalType) { - if (field in this.floating) { + if (Object.hasOwn(this.floating, field)) { return floatingByKey(this)[field]; } if (!this.canGet(field)) { @@ -258,16 +261,6 @@ export class ModifiersDynamicMetadata { } } -function getInternal(object: any): InternalType { - if (!object._internal) { - object._internal = { - meta: {}, - data: {}, - }; - } - return object._internal; -} - /** * Builds a table instance from unlocked plain data: each key is assigned, so * modifiers lock the fields they can. Then runs the `fromPlainData` event. @@ -277,7 +270,7 @@ export function fromPlainData( TableClass: T, ): InstanceType { const shallowCopy: typeof rawObject = {}; - Object.setPrototypeOf(shallowCopy, TableClass.prototype); + Object.setPrototypeOf(shallowCopy, prototypeOf(TableClass)); for (const [key, val] of Object.entries(rawObject)) { shallowCopy[key] = val; } @@ -312,7 +305,7 @@ export function fromDatabase( TableClass: T, ): InstanceType { const shallowCopy = { ...rawObject }; - Object.setPrototypeOf(shallowCopy, TableClass.prototype); + Object.setPrototypeOf(shallowCopy, prototypeOf(TableClass)); triggerEvent(shallowCopy, "fromDatabase", rawObject); return shallowCopy; } @@ -384,13 +377,13 @@ export function attachModifier< } else { fieldsByKey(metaTable)[field] = [entry]; Object.defineProperty(TableClass.prototype, field, { - get: function () { + get: function (this: InternalHolder) { return getMetadata(this, ModifiersDynamicMetadata).get( field, getInternal(this), ); }, - set: function (value) { + set: function (this: InternalHolder, value) { getMetadata(this, ModifiersDynamicMetadata).set( field, getInternal(this), diff --git a/src/modifiers/encryption.ts b/src/modifiers/encryption.ts index 4a9c853..a88fb2c 100644 --- a/src/modifiers/encryption.ts +++ b/src/modifiers/encryption.ts @@ -11,6 +11,7 @@ import { } from "node:crypto"; import { attachModifier, MixinSymbol, TwoWayModifier } from "./common"; +import { isCallable } from "./internal/kinds"; type Options = { /** Encryption algorithm (for supported algorithms refer to {@link createCipheriv}) */ @@ -46,13 +47,13 @@ function date_decoder(_key: string, value: any) { function hasAuthTag( cipher: Cipher, ): cipher is Cipher & Pick { - return "getAuthTag" in cipher; + return "getAuthTag" in cipher && isCallable(cipher.getAuthTag); } function acceptsAuthTag( decipher: Decipher, ): decipher is Decipher & Pick { - return "setAuthTag" in decipher; + return "setAuthTag" in decipher && isCallable(decipher.setAuthTag); } /** diff --git a/src/modifiers/internal/kinds.ts b/src/modifiers/internal/kinds.ts index c23ef1e..42ad9c9 100644 --- a/src/modifiers/internal/kinds.ts +++ b/src/modifiers/internal/kinds.ts @@ -1,4 +1,5 @@ /** @internal @packageDocumentation */ +import type { Constructible } from "../../common"; import type { Modifier, OneWayModifier, TwoWayModifier } from "../common"; export function isWriteOnly(m: Modifier): m is OneWayModifier { @@ -27,6 +28,22 @@ export function isCallable(value: unknown): value is CallableFunction { return typeof value === "function"; } +/** Whether a value is an object or a function: what a prototype chain holds. */ +export function isObjectLike(value: unknown): value is object { + return ( + (typeof value === "object" || typeof value === "function") && value !== null + ); +} + +/** The object the instances of a class inherit from. */ +export function prototypeOf(target: Constructible): object { + const prototype: unknown = target.prototype; + if (!isObjectLike(prototype)) { + throw new TypeError("A class prototype must be an object."); + } + return prototype; +} + export function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } diff --git a/src/modifiers/internal/records.ts b/src/modifiers/internal/records.ts index 60a7dc8..13dfb4c 100644 --- a/src/modifiers/internal/records.ts +++ b/src/modifiers/internal/records.ts @@ -10,6 +10,22 @@ type ModifiedFieldList = ModifiersStaticMetadata["fields"][string]; type ModifiedField = ModifiedFieldList[number]; type InternalRecord = Parameters[1]; +export interface InternalHolder { + constructor: any; + _internal?: InternalRecord; +} + +/** The modifier state of a table instance, created on first use. */ +export function getInternal(object: InternalHolder): InternalRecord { + if (!object._internal) { + object._internal = { + meta: {}, + data: {}, + }; + } + return object._internal; +} + export function fieldsByKey(meta: ModifiersStaticMetadata): { [field: PropertyKey]: ModifiedFieldList; } { @@ -32,6 +48,9 @@ export function unlockArgsOf( const byKey: { [field: PropertyKey]: ModifiersDynamicMetadata["fields"][string]; } = meta.fields; + if (!Object.hasOwn(byKey, field)) { + throw new TypeError(`${String(field)} holds no modifier arguments`); + } return byKey[field]; } @@ -44,9 +63,9 @@ export function fieldTypesByKey(meta: DatumStaticMetadata): { export function floatingOf( meta: { floating: Record }, field: PropertyKey, -): V { +): V | undefined { const byKey: { [field: PropertyKey]: V } = meta.floating; - return byKey[field]; + return Object.hasOwn(byKey, field) ? byKey[field] : undefined; } export function floatingByKey(meta: { floating: Record }): { diff --git a/src/schema.ts b/src/schema.ts index 1f4f25a..4e02736 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -8,7 +8,7 @@ import { Schema } from "@antelopejs/interface-database"; import type { Table } from "./table"; import { DatumStaticMetadata, getMetadata } from "./common"; -const schemaTableRegistry: Record>> = {}; +const schemaTableRegistry = new Map>>(); export const RegisterTable: ( tableName: string, @@ -20,15 +20,14 @@ export const RegisterTable: ( metadata.schemaName = schemaName; metadata.schema = Schema.get(schemaName); - if (!(schemaName in schemaTableRegistry)) { - schemaTableRegistry[schemaName] = {}; - } - schemaTableRegistry[schemaName][tableName] = target; + const tables = schemaTableRegistry.get(schemaName) ?? {}; + tables[tableName] = target; + schemaTableRegistry.set(schemaName, tables); }, ); export function getTablesForSchema( schemaName: string, ): Record> | undefined { - return schemaTableRegistry[schemaName]; + return schemaTableRegistry.get(schemaName); } diff --git a/src/table.ts b/src/table.ts index 29f81cc..46c7e08 100644 --- a/src/table.ts +++ b/src/table.ts @@ -132,10 +132,12 @@ export class Table { | ExtractTableMeta> > { const c = extendTable(this); - for (const mixin of others - .map((otherClass) => otherClass.prototype && new otherClass()) - .map((other) => other?.[MixinSymbol]) - .filter((mixin) => mixin?.prototype)) { + for (const mixin of others.flatMap((otherClass) => { + const mixin = otherClass.prototype + ? new otherClass()[MixinSymbol] + : undefined; + return mixin?.prototype ? [mixin] : []; + })) { for (const key of Object.getOwnPropertyNames(mixin.prototype)) { if (key !== "constructor" && !(key in c.prototype)) { Object.defineProperty(c.prototype, key, { @@ -157,7 +159,14 @@ export class Table { } } } - return c; + type Combined = TableClass< + InstanceType & + UnionToIntersection>>>, + ConstructorParameters, + | ExtractTableMeta> + | ExtractTableMeta> + >; + return c as Combined; } } diff --git a/src/tests/database.test.ts b/src/tests/database.test.ts index cd19952..df7cf0a 100644 --- a/src/tests/database.test.ts +++ b/src/tests/database.test.ts @@ -182,7 +182,8 @@ describe("Database - initialization", () => { delete val._id; delete val._instance; } - const sortById = (a: any, b: any) => a.id.localeCompare(b.id); + const sortById = (a: { id: string }, b: { id: string }) => + a.id.localeCompare(b.id); expect(result.sort(sortById)).to.deep.equal(testData.sort(sortById)); }); it("handles multiple tables", async () => { diff --git a/src/tests/helpers/other-instance.ts b/src/tests/helpers/other-instance.ts index 1977978..c9730d1 100644 --- a/src/tests/helpers/other-instance.ts +++ b/src/tests/helpers/other-instance.ts @@ -35,7 +35,7 @@ function isModuleExports(value: unknown): value is object { } function exportsOf(loaded: Module): object { - const exported = loaded.exports; + const exported: unknown = loaded.exports; if (!isModuleExports(exported)) { throw new Error( `${loaded.filename} does not export an object or a function`, @@ -88,7 +88,7 @@ export function CreateOtherInstance(folders: string[]): ModuleRequire { if (isOwned) { return load(filename, parent); } - const required = Module.prototype.require.call(parent, request); + const required: unknown = Module.prototype.require.call(parent, request); if (!isModuleExports(required)) { throw new Error(`${request} does not export an object or a function`); } diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts index e65fb0e..d6f53f2 100644 --- a/src/tests/integration.test.ts +++ b/src/tests/integration.test.ts @@ -53,7 +53,7 @@ describe("Integration - real database operations", () => { const userId = insertResult[0]; const retrievedUserFromModel = await userModel.get(userId); - const retrievedUserFromDatabase = await schemaInstance + const retrievedUserFromDatabase: unknown = await schemaInstance .table("users") .get(userId) .run(); @@ -69,7 +69,7 @@ describe("Integration - real database operations", () => { expect(retrievedUserFromModel?.secretData).to.deep.equal({ token: "secret-token-123", }); - expect(retrievedUserFromDatabase?.secretData).to.not.deep.equal({ + expect(retrievedUserFromDatabase).to.not.have.deep.property("secretData", { token: "secret-token-123", }); @@ -81,7 +81,10 @@ describe("Integration - real database operations", () => { expect(isPasswordValid).to.equal(true); } - const dbUser = await schemaInstance.table("users").get(userId).run(); + const dbUser: unknown = await schemaInstance + .table("users") + .get(userId) + .run(); expect(dbUser).to.have.property("email").that.equals("test@example.com"); expect(dbUser).to.have.property("name").that.equals("John Doe"); }); @@ -237,13 +240,13 @@ describe("Integration - real database operations", () => { expect(retrievedData?.creditCard).to.equal("4111-1111-1111-1111"); expect(retrievedData?.ssn).to.equal("123-45-6789"); - const dbData = await schemaInstance + const dbData: unknown = await schemaInstance .table("sensitive_data") .get(dataId) .run(); expect(dbData).to.have.property("userId").that.equals("user123"); - expect(dbData?.creditCard).to.not.equal("4111-1111-1111-1111"); - expect(dbData?.ssn).to.not.equal("123-45-6789"); + expect(dbData).to.not.have.property("creditCard", "4111-1111-1111-1111"); + expect(dbData).to.not.have.property("ssn", "123-45-6789"); }); }); From 161c3e2066b7604330574fe6edd06e83bea71ad5 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sat, 31 Oct 2026 02:43:32 +0100 Subject: [PATCH 02/45] fix(metadata): upgrade the table metadata stored by an older copy of the package (#55) * test(metadata): pin the metadata stored by an older copy of the package * fix(metadata): upgrade the table metadata stored by an older copy of the package --- guardrails.ratchet.json | 2 +- src/common.ts | 30 +++++++-- src/tests/stored_metadata.test.ts | 101 ++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 src/tests/stored_metadata.test.ts diff --git a/guardrails.ratchet.json b/guardrails.ratchet.json index 7e303ca..f6038c5 100644 --- a/guardrails.ratchet.json +++ b/guardrails.ratchet.json @@ -8,7 +8,7 @@ "anti-slop/no-unknown-parameters": 8, "anti-slop/no-unknown-returns": 1, "anti-slop/no-unsafe-dictionary-type": 18, - "anti-slop/require-safety-comment-for-type-assertion": 15, + "anti-slop/require-safety-comment-for-type-assertion": 14, "typescript/no-unnecessary-type-parameters": 7, "typescript/no-unsafe-argument": 15, "typescript/no-unsafe-assignment": 10, diff --git a/src/common.ts b/src/common.ts index f5232df..c9b96d3 100644 --- a/src/common.ts +++ b/src/common.ts @@ -27,6 +27,12 @@ export type DatumGenerator = ( export interface MetadataClass extends Constructible { key: symbol; + /** + * Brings metadata stored under the shared key by an older copy of the + * package to this copy's shape. Returns `stored` itself when it is already + * current. + */ + upgrade?: (stored: TMetadata) => TMetadata; } /** @@ -37,6 +43,16 @@ export class DatumStaticMetadata { "@antelopejs/interface-database-decorators/DatumStaticMetadata@1", ); + /** + * Copies 0.1.8 and 0.1.9 of the package store the metadata without + * `uniqueIndexes`, under the same key and class shape as this copy. + */ + public static upgrade(stored: DatumStaticMetadata): DatumStaticMetadata { + return Object.hasOwn(stored, "uniqueIndexes") + ? stored + : Object.assign(new DatumStaticMetadata(), stored); + } + public tableName?: string; public schemaName?: string; public readonly indexes: Record> = {}; @@ -78,15 +94,19 @@ export function getMetadata( | TMetadata | undefined; if (existingMetadata) { - return existingMetadata; + const upgraded = metadataClass.upgrade?.(existingMetadata); + if (upgraded && upgraded !== existingMetadata) { + Reflect.defineMetadata(metadataClass.key, upgraded, target); + } + return upgraded ?? existingMetadata; } const prototype: unknown = Object.getPrototypeOf(target); const inheritedMetadata = - inherit && isObjectLike(prototype) - ? (Reflect.getOwnMetadata(metadataClass.key, prototype) as - | TMetadata - | undefined) + inherit && + isObjectLike(prototype) && + Reflect.hasOwnMetadata(metadataClass.key, prototype) + ? getMetadata(prototype, metadataClass, false) : undefined; const metadata = inheritedMetadata ?? new metadataClass(target); Reflect.defineMetadata(metadataClass.key, metadata, target); diff --git a/src/tests/stored_metadata.test.ts b/src/tests/stored_metadata.test.ts new file mode 100644 index 0000000..30f42ab --- /dev/null +++ b/src/tests/stored_metadata.test.ts @@ -0,0 +1,101 @@ +import { expect } from "chai"; +import { Index, Table } from "../table"; +import { DatumStaticMetadata, getMetadata } from "../common"; + +/** + * The table metadata as copies 0.1.8 and 0.1.9 of the package store it under + * the shared metadata key, before `uniqueIndexes` was added to its shape. + */ +class LegacyDatumStaticMetadata { + public tableName?: string; + public schemaName?: string; + public readonly indexes: Record> = {}; + public readonly crossInstanceIndexes = new Set(); + public readonly fields: DatumStaticMetadata["fields"] = {}; + public primary: string = "_id"; + public generator?: DatumStaticMetadata["generator"]; + public schema?: DatumStaticMetadata["schema"]; + + addIndex(key: string, group: string, isCrossInstance: boolean = false) { + if (!Object.hasOwn(this.indexes, group)) { + this.indexes[group] = []; + } + this.indexes[group].push(key); + if (isCrossInstance) { + this.crossInstanceIndexes.add(group); + } + } +} + +function declaredByLegacyCopy(tableClass: typeof Table) { + const legacy = new LegacyDatumStaticMetadata(); + legacy.tableName = "legacy_rows"; + legacy.schemaName = "legacy-schema"; + legacy.primary = "code"; + legacy.addIndex("email", "email"); + legacy.addIndex("name", "search", true); + legacy.addIndex("city", "search"); + Reflect.defineMetadata(DatumStaticMetadata.key, legacy, tableClass); + return legacy; +} + +describe("Metadata - stored by an older copy of the package", () => { + it("reads the metadata as the current shape", async () => { + class Row extends Table {} + declaredByLegacyCopy(Row); + + const metadata = getMetadata(Row, DatumStaticMetadata); + + expect(metadata).to.be.instanceOf(DatumStaticMetadata); + expect([...metadata.uniqueIndexes]).to.deep.equal([]); + expect(metadata.uniqueIndexes.has("email")).to.equal(false); + }); + + it("keeps what the older copy stored", async () => { + class Row extends Table {} + declaredByLegacyCopy(Row); + + const metadata = getMetadata(Row, DatumStaticMetadata); + + expect(metadata.tableName).to.equal("legacy_rows"); + expect(metadata.schemaName).to.equal("legacy-schema"); + expect(metadata.primary).to.equal("code"); + expect(metadata.indexes).to.deep.equal({ + email: ["email"], + search: ["name", "city"], + }); + expect([...metadata.crossInstanceIndexes]).to.deep.equal(["search"]); + }); + + it("answers the same metadata on every read", async () => { + class Row extends Table {} + declaredByLegacyCopy(Row); + + expect(getMetadata(Row, DatumStaticMetadata)).to.equal( + getMetadata(Row, DatumStaticMetadata), + ); + }); + + it("records a unique index declared after the older copy", async () => { + class Row extends Table {} + declaredByLegacyCopy(Row); + + Index({ unique: true })(Row.prototype, "code"); + + const metadata = getMetadata(Row, DatumStaticMetadata); + expect([...metadata.uniqueIndexes]).to.deep.equal(["code"]); + expect(metadata.indexes.code).to.deep.equal(["code"]); + }); + + it("starts a subclass from the metadata the older copy stored", async () => { + class Base extends Table {} + declaredByLegacyCopy(Base); + class Child extends Base {} + + const metadata = getMetadata(Child, DatumStaticMetadata); + + expect(metadata).to.be.instanceOf(DatumStaticMetadata); + expect(metadata.uniqueIndexes.has("email")).to.equal(false); + expect(metadata.indexes).to.have.property("email"); + }); +}); From 5508dafc11ae9b53c93b183f51e84753a788eecd Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sat, 31 Oct 2026 02:43:37 +0100 Subject: [PATCH 03/45] fix(modifiers): look up a container entry by own key (#56) * test(modifiers): pin lookups by names shared with Object.prototype members * fix(modifiers): look up a container entry by own key --- guardrails.ratchet.json | 1 - src/modifiers/common.ts | 5 ++- src/tests/own_key_lookups.test.ts | 65 +++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 src/tests/own_key_lookups.test.ts diff --git a/guardrails.ratchet.json b/guardrails.ratchet.json index f6038c5..d6739fa 100644 --- a/guardrails.ratchet.json +++ b/guardrails.ratchet.json @@ -1,7 +1,6 @@ { "pack": "0.2.17", "rules": { - "antelopejs/no-inherited-key-lookup": 1, "anti-slop/no-chained-type-assertions": 1, "anti-slop/no-known-value-widening": 2, "anti-slop/no-runtime-typeof": 1, diff --git a/src/modifiers/common.ts b/src/modifiers/common.ts index be3fbda..9fa8804 100644 --- a/src/modifiers/common.ts +++ b/src/modifiers/common.ts @@ -103,7 +103,10 @@ export class ContainerModifier< : { [key]: value }; } public override unlock(locked_value: Record, key: string) { - return key === "*" ? locked_value : locked_value[key]; + if (key === "*") { + return locked_value; + } + return Object.hasOwn(locked_value, key) ? locked_value[key] : undefined; } public override unlockrequest( data: DatabaseDev.ValueProxy>, diff --git a/src/tests/own_key_lookups.test.ts b/src/tests/own_key_lookups.test.ts new file mode 100644 index 0000000..58afad4 --- /dev/null +++ b/src/tests/own_key_lookups.test.ts @@ -0,0 +1,65 @@ +import { expect } from "chai"; +import { getTablesForSchema, RegisterTable } from "../schema"; +import { Index, Table } from "../table"; +import { DatumStaticMetadata, getMetadata } from "../common"; +import { ContainerModifier } from "../modifiers/common"; +import { LocalizationModifier } from "../modifiers/localization"; + +const PROTOTYPE_MEMBER_NAMES = [ + "toString", + "constructor", + "hasOwnProperty", + "valueOf", +]; + +describe("Lookups by name - names shared with Object.prototype members", () => { + it("answers no tables for a schema nothing registered", async () => { + for (const name of PROTOTYPE_MEMBER_NAMES) { + expect(getTablesForSchema(name)).to.equal(undefined); + } + }); + + for (const name of PROTOTYPE_MEMBER_NAMES) { + it(`answers nothing for ${name} in a container modifier`, async () => { + expect(new ContainerModifier().unlock({ en: "x" }, name)).to.equal( + undefined, + ); + }); + + it(`answers the stored ${name} entry of a container modifier`, async () => { + expect( + new ContainerModifier().unlock( + Object.fromEntries([[name, "stored"]]), + name, + ), + ).to.equal("stored"); + }); + + it(`falls back when a localized field is read in the locale ${name}`, async () => { + const modifier = new LocalizationModifier(); + Object.assign(modifier, { options: { fallbackLocale: "en" } }); + + expect(modifier.unlock({ en: "Hello" }, name)).to.equal("Hello"); + }); + + it(`groups an index named ${name}`, async () => { + class Row extends Table {} + Index()(Row.prototype, name); + Index({ group: name })(Row.prototype, "other"); + + expect(getMetadata(Row, DatumStaticMetadata).indexes).to.deep.equal( + Object.fromEntries([[name, [name, "other"]]]), + ); + }); + + it(`registers the tables of a schema named ${name}`, async () => { + class Row extends Table {} + RegisterTable("rows", name)(Row); + + expect(getTablesForSchema(name)).to.deep.equal({ rows: Row }); + expect(getTablesForSchema(`${name}-never-registered`)).to.equal( + undefined, + ); + }); + } +}); From 352bb345802dfd5050c47179f0479c805c1c57f2 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sat, 31 Oct 2026 02:43:43 +0100 Subject: [PATCH 04/45] fix(encryption): revive only the date marker the encoder writes (#57) * test(encryption): pin which stored objects are revived as dates * fix(encryption): revive only the date marker the encoder writes * refactor(encryption): type the stored values the date reviver reads --- guardrails.ratchet.json | 5 +- src/modifiers/encryption.ts | 40 ++++++--- src/modifiers/internal/kinds.ts | 4 + .../modifiers/encryption_date_marker.test.ts | 86 +++++++++++++++++++ 4 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 src/tests/modifiers/encryption_date_marker.test.ts diff --git a/guardrails.ratchet.json b/guardrails.ratchet.json index d6739fa..59341f6 100644 --- a/guardrails.ratchet.json +++ b/guardrails.ratchet.json @@ -3,15 +3,14 @@ "rules": { "anti-slop/no-chained-type-assertions": 1, "anti-slop/no-known-value-widening": 2, - "anti-slop/no-runtime-typeof": 1, "anti-slop/no-unknown-parameters": 8, "anti-slop/no-unknown-returns": 1, "anti-slop/no-unsafe-dictionary-type": 18, "anti-slop/require-safety-comment-for-type-assertion": 14, "typescript/no-unnecessary-type-parameters": 7, - "typescript/no-unsafe-argument": 15, + "typescript/no-unsafe-argument": 11, "typescript/no-unsafe-assignment": 10, - "typescript/no-unsafe-return": 12 + "typescript/no-unsafe-return": 11 }, "oversizeFiles": {} } diff --git a/src/modifiers/encryption.ts b/src/modifiers/encryption.ts index a88fb2c..705d911 100644 --- a/src/modifiers/encryption.ts +++ b/src/modifiers/encryption.ts @@ -11,7 +11,7 @@ import { } from "node:crypto"; import { attachModifier, MixinSymbol, TwoWayModifier } from "./common"; -import { isCallable } from "./internal/kinds"; +import { isCallable, isRecord, isString } from "./internal/kinds"; type Options = { /** Encryption algorithm (for supported algorithms refer to {@link createCipheriv}) */ @@ -33,15 +33,35 @@ function date_encoder(this: any, key: string) { : this[key]; } -function date_decoder(_key: string, value: any) { - return value !== null && - typeof value === "object" && - Object.keys(value).length === 2 && - Object.keys(value).includes("type") && - Object.keys(value).includes("value") && - value.type === "Date" - ? new Date(value.value) - : value; +type StoredValue = + | null + | boolean + | number + | string + | Date + | StoredValue[] + | { [key: string]: StoredValue }; + +/** + * The Date a stored marker `{ type: "Date", value }` stands for, when `value` + * is a text `toUTCString` produces, as `date_encoder` writes it. Any other + * object was stored by the caller. + */ +function dateOfMarker(candidate: StoredValue): Date | undefined { + if ( + !isRecord(candidate) || + Object.keys(candidate).length !== 2 || + candidate.type !== "Date" || + !isString(candidate.value) + ) { + return undefined; + } + const date = new Date(candidate.value); + return date.toUTCString() === candidate.value ? date : undefined; +} + +function date_decoder(_key: string, value: StoredValue) { + return dateOfMarker(value) ?? value; } function hasAuthTag( diff --git a/src/modifiers/internal/kinds.ts b/src/modifiers/internal/kinds.ts index 42ad9c9..7343405 100644 --- a/src/modifiers/internal/kinds.ts +++ b/src/modifiers/internal/kinds.ts @@ -28,6 +28,10 @@ export function isCallable(value: unknown): value is CallableFunction { return typeof value === "function"; } +export function isString(value: unknown): value is string { + return typeof value === "string"; +} + /** Whether a value is an object or a function: what a prototype chain holds. */ export function isObjectLike(value: unknown): value is object { return ( diff --git a/src/tests/modifiers/encryption_date_marker.test.ts b/src/tests/modifiers/encryption_date_marker.test.ts new file mode 100644 index 0000000..ac507fc --- /dev/null +++ b/src/tests/modifiers/encryption_date_marker.test.ts @@ -0,0 +1,86 @@ +import { expect } from "chai"; +import { Field, Table } from "../../table"; +import { Encrypted, EncryptionModifier } from "../../modifiers/encryption"; + +type Stored = + | null + | string + | number + | Date + | Stored[] + | { [key: string]: Stored }; + +class Card extends Table.with(EncryptionModifier) { + @Encrypted({ secretKey: "12345678901234567890123456789012" }) + @Field("any") + declare payload: Stored; +} + +class DatedCard extends Table.with(EncryptionModifier) { + @Encrypted({ secretKey: "12345678901234567890123456789012" }) + @Field("any") + declare payload: { at: Date }; +} + +function roundTrip(stored: Stored) { + const card = new Card(); + card.payload = stored; + return card.payload; +} + +describe("Modifiers - encryption - Date marker", () => { + it("returns a Date as a Date", async () => { + const date = new Date("2022-12-01T10:20:30Z"); + + const read = roundTrip({ at: date }); + + expect(read).to.deep.equal({ at: date }); + }); + + it("returns a Date held at the top level as a Date", async () => { + const date = new Date("2022-12-01T10:20:30Z"); + + const read = roundTrip(date); + + expect(read).to.be.instanceOf(Date); + expect(read).to.deep.equal(date); + }); + + it("returns an invalid Date as an invalid Date", async () => { + const dated = new DatedCard(); + dated.payload = { at: new Date(Number.NaN) }; + + expect(dated.payload.at).to.be.instanceOf(Date); + expect(Number.isNaN(dated.payload.at.getTime())).to.equal(true); + }); + + for (const stored of [ + { type: "Date", value: 123 }, + { type: "Date", value: "not a date" }, + { type: "Date", value: "2022-12-01" }, + { type: "Date", value: null }, + { type: "Date", value: ["Thu, 01 Dec 2022 10:20:30 GMT"] }, + { type: "Date", value: { year: 2022 } }, + ]) { + it(`returns ${JSON.stringify(stored)} as stored`, async () => { + expect(roundTrip(stored)).to.deep.equal(stored); + expect(roundTrip({ nested: stored })).to.deep.equal({ nested: stored }); + }); + } + + it("returns an object with other keys than the marker's as stored", async () => { + const stored = { + type: "Date", + value: "Thu, 01 Dec 2022 10:20:30 GMT", + note: "kept", + }; + + expect(roundTrip(stored)).to.deep.equal(stored); + }); + + it("returns an object of another type as stored", async () => { + const stored = { type: "Money", value: "Thu, 01 Dec 2022 10:20:30 GMT" }; + + expect(roundTrip(stored)).to.deep.equal(stored); + }); +}); From b439da5a7be2adf32a780b740c5c990d00174a15 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sat, 31 Oct 2026 03:19:33 +0100 Subject: [PATCH 05/45] fix(metadata): copy the metadata a subclass inherits instead of sharing it (#54) * test(metadata): pin the metadata of subclasses of a table * fix(metadata): copy the metadata a subclass inherits instead of sharing it * test(metadata): pin the schema binding of a subclass built before RegisterSchema * fix(metadata): bind a subclass to the schema registered under its schema name * refactor(modifiers): copy the modifier lists of a parent from the record helpers * refactor(modifiers): keep listsCopied private to the record helpers --- src/common.ts | 38 ++++- src/internal/bound-schema.ts | 16 ++ src/model.ts | 3 +- src/modifiers/common.ts | 9 + src/modifiers/internal/records.ts | 17 ++ src/relation.ts | 14 ++ src/table.ts | 6 +- src/tests/metadata_inheritance.test.ts | 197 ++++++++++++++++++++++ src/tests/subclass_schema_binding.test.ts | 86 ++++++++++ 9 files changed, 382 insertions(+), 4 deletions(-) create mode 100644 src/internal/bound-schema.ts create mode 100644 src/tests/metadata_inheritance.test.ts create mode 100644 src/tests/subclass_schema_binding.test.ts diff --git a/src/common.ts b/src/common.ts index c9b96d3..d7bbe72 100644 --- a/src/common.ts +++ b/src/common.ts @@ -27,6 +27,12 @@ export type DatumGenerator = ( export interface MetadataClass extends Constructible { key: symbol; + /** + * Builds the metadata a subclass starts from out of its parent's, copying + * whatever a decorator on the subclass would write to. Without it, the + * subclass shares its parent's metadata object. + */ + inherit?: (parent: TMetadata) => TMetadata; /** * Brings metadata stored under the shared key by an older copy of the * package to this copy's shape. Returns `stored` itself when it is already @@ -43,6 +49,33 @@ export class DatumStaticMetadata { "@antelopejs/interface-database-decorators/DatumStaticMetadata@1", ); + /** + * The metadata of a subclass of a table: a copy of its parent's, so that + * decorators on the subclass never write into the parent or its siblings. + * The bound schema is not copied: `RegisterSchema` binds the parent after a + * subclass may have been read, so the subclass looks the schema up by name. + */ + public static inherit(parent: DatumStaticMetadata): DatumStaticMetadata { + const copy = new DatumStaticMetadata(); + copy.tableName = parent.tableName; + copy.schemaName = parent.schemaName; + for (const [group, keys] of Object.entries(parent.indexes)) { + copy.indexes[group] = [...keys]; + } + for (const group of parent.crossInstanceIndexes) { + copy.crossInstanceIndexes.add(group); + } + for (const group of parent.uniqueIndexes) { + copy.uniqueIndexes.add(group); + } + for (const [name, type] of Object.entries(parent.fields)) { + copy.fields[name] = type; + } + copy.primary = parent.primary; + copy.generator = parent.generator; + return copy; + } + /** * Copies 0.1.8 and 0.1.9 of the package store the metadata without * `uniqueIndexes`, under the same key and class shape as this copy. @@ -108,7 +141,10 @@ export function getMetadata( Reflect.hasOwnMetadata(metadataClass.key, prototype) ? getMetadata(prototype, metadataClass, false) : undefined; - const metadata = inheritedMetadata ?? new metadataClass(target); + const metadata = + inheritedMetadata && metadataClass.inherit + ? metadataClass.inherit(inheritedMetadata) + : (inheritedMetadata ?? new metadataClass(target)); Reflect.defineMetadata(metadataClass.key, metadata, target); return metadata; } diff --git a/src/internal/bound-schema.ts b/src/internal/bound-schema.ts new file mode 100644 index 0000000..ad50eca --- /dev/null +++ b/src/internal/bound-schema.ts @@ -0,0 +1,16 @@ +/** @internal @packageDocumentation */ + +import { Schema } from "@antelopejs/interface-database"; + +import type { DatumStaticMetadata } from "../common"; + +/** + * The schema a table is bound to: the one `RegisterSchema` bound it to, else + * the one registered under its schema name. A subclass starts without a bound + * schema, so it follows its parent's schema whenever `RegisterSchema` binds it. + */ +export function boundSchema({ schema, schemaName }: DatumStaticMetadata) { + return ( + schema ?? (schemaName === undefined ? undefined : Schema.get(schemaName)) + ); +} diff --git a/src/model.ts b/src/model.ts index 9ce5609..d157ca5 100644 --- a/src/model.ts +++ b/src/model.ts @@ -18,6 +18,7 @@ import { triggerEvent, } from "./modifiers/common"; import { isRecord } from "./modifiers/internal/kinds"; +import { boundSchema } from "./internal/bound-schema"; import { type Constructible, DatumStaticMetadata, @@ -332,7 +333,7 @@ function schemaOf(cl: DataModel): Schema { assert(schema, `Schema not found for '${cl.schemaName}'`); return schema; } - const schema = getMetadata(cl.tableClass, DatumStaticMetadata).schema; + const schema = boundSchema(getMetadata(cl.tableClass, DatumStaticMetadata)); assert( schema, `${cl.tableClass.name} is not bound to a schema: call RegisterSchema("${cl.schemaName}") in the module that declares it`, diff --git a/src/modifiers/common.ts b/src/modifiers/common.ts index 9fa8804..8b67cb1 100644 --- a/src/modifiers/common.ts +++ b/src/modifiers/common.ts @@ -11,6 +11,7 @@ import { prototypeOf, } from "./internal/kinds"; import { + copyModifierLists, dataByKey, eventsByKey, fieldsByKey, @@ -140,6 +141,14 @@ export class ModifiersStaticMetadata { "@antelopejs/interface-database-decorators/ModifiersStaticMetadata@1", ); + /** + * The metadata of a subclass of a table: a copy of its parent's lists, so + * modifiers attached on the subclass never reach the parent or its siblings. + */ + public static inherit(parent: ModifiersStaticMetadata) { + return copyModifierLists(new ModifiersStaticMetadata(), parent); + } + public fields: Record> = {}; public events = new Map< diff --git a/src/modifiers/internal/records.ts b/src/modifiers/internal/records.ts index 13dfb4c..166346c 100644 --- a/src/modifiers/internal/records.ts +++ b/src/modifiers/internal/records.ts @@ -94,3 +94,20 @@ export function withRef( } return ret; } + +/** The entries of a record or map with every list copied, not shared. */ +function listsCopied( + entries: Iterable, +): Array<[K, V[]]> { + return Array.from(entries, ([key, list]) => [key, [...list]]); +} + +/** Fills `copy` with the modified fields and events of `parent`, every list copied. */ +export function copyModifierLists( + copy: ModifiersStaticMetadata, + parent: ModifiersStaticMetadata, +): ModifiersStaticMetadata { + copy.fields = Object.fromEntries(listsCopied(Object.entries(parent.fields))); + copy.events = new Map(listsCopied(parent.events)); + return copy; +} diff --git a/src/relation.ts b/src/relation.ts index 00dfb2d..a338715 100644 --- a/src/relation.ts +++ b/src/relation.ts @@ -31,6 +31,20 @@ export class RelationStaticMetadata { "@antelopejs/interface-database-decorators/RelationStaticMetadata@1", ); + /** + * The metadata of a subclass of a table: a copy of its parent's, so that + * relations declared on the subclass never reach the parent or its siblings. + */ + public static inherit( + parent: RelationStaticMetadata, + ): RelationStaticMetadata { + const copy = new RelationStaticMetadata(); + for (const [field, options] of Object.entries(parent.relations)) { + copy.relations[field] = options; + } + return copy; + } + public readonly relations: Record = {}; } diff --git a/src/table.ts b/src/table.ts index 46c7e08..a3a96ce 100644 --- a/src/table.ts +++ b/src/table.ts @@ -15,6 +15,7 @@ import { import { MixinSymbol, type MixinType } from "./modifiers/common"; import { fieldTypesByKey } from "./modifiers/internal/records"; import { type Constructible, DatumStaticMetadata, getMetadata } from "./common"; +import { boundSchema } from "./internal/bound-schema"; type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( k: infer I, @@ -105,11 +106,12 @@ export class Table { if (isSchemaInstance(target)) { return target.table(tableName); } + const schema = boundSchema(metadata); assert( - metadata.schema, + schema, `${this.name} is not bound to a schema: call RegisterSchema("${metadata.schemaName}") in the module that declares it`, ); - return metadata.schema.instance(target).table(tableName); + return schema.instance(target).table(tableName); } /** diff --git a/src/tests/metadata_inheritance.test.ts b/src/tests/metadata_inheritance.test.ts new file mode 100644 index 0000000..0ee642d --- /dev/null +++ b/src/tests/metadata_inheritance.test.ts @@ -0,0 +1,197 @@ +import { expect } from "chai"; +import { RegisterTable } from "../schema"; +import { Field, Fixture, Index, Table } from "../table"; +import { Relation, RelationStaticMetadata } from "../relation"; +import { DatumStaticMetadata, getMetadata } from "../common"; +import { Hashed, HashModifier } from "../modifiers/hash"; +import { + attachModifier, + getModifiedFields, + ModifiersStaticMetadata, + OneWayModifier, +} from "../modifiers/common"; + +const schemaName = "metadata-inheritance-schema"; + +function fieldsOf(tableClass: typeof Table) { + return Object.keys(getMetadata(tableClass, DatumStaticMetadata).fields); +} + +describe("Metadata - subclasses of a table", () => { + it("keeps the table name and fields of each subclass apart", async () => { + class Base extends Table { + @Field("string") + declare createdBy: string; + } + + @RegisterTable("inheritance_users", schemaName) + class Users extends Base { + @Field("string") + declare email: string; + } + + @RegisterTable("inheritance_posts", schemaName) + class Posts extends Base { + @Field("string") + declare title: string; + } + + expect(getMetadata(Users, DatumStaticMetadata).tableName).to.equal( + "inheritance_users", + ); + expect(getMetadata(Posts, DatumStaticMetadata).tableName).to.equal( + "inheritance_posts", + ); + expect(getMetadata(Base, DatumStaticMetadata).tableName).to.equal( + undefined, + ); + expect(fieldsOf(Base)).to.deep.equal(["createdBy"]); + expect(fieldsOf(Users)).to.deep.equal(["createdBy", "email"]); + expect(fieldsOf(Posts)).to.deep.equal(["createdBy", "title"]); + }); + + it("keeps the indexes of each subclass apart", async () => { + class Base extends Table { + @Index() + declare slug: string; + } + + class Users extends Base { + @Index({ group: "slug", crossInstance: true }) + declare handle: string; + } + + class Posts extends Base { + @Index({ unique: true }) + declare title: string; + } + + const base = getMetadata(Base, DatumStaticMetadata); + const users = getMetadata(Users, DatumStaticMetadata); + const posts = getMetadata(Posts, DatumStaticMetadata); + + expect(base.indexes).to.deep.equal({ slug: ["slug"] }); + expect(users.indexes).to.deep.equal({ slug: ["slug", "handle"] }); + expect(posts.indexes).to.deep.equal({ slug: ["slug"], title: ["title"] }); + expect([...base.crossInstanceIndexes]).to.deep.equal([]); + expect([...users.crossInstanceIndexes]).to.deep.equal(["slug"]); + expect([...base.uniqueIndexes]).to.deep.equal([]); + expect([...posts.uniqueIndexes]).to.deep.equal(["title"]); + }); +}); + +describe("Metadata - subclasses of a table with fixtures, modifiers and relations", () => { + it("keeps the generator of each subclass apart", async () => { + class Base extends Table { + @Field("string") + declare name: string; + } + + @Fixture(() => ({ name: "user" })) + class Users extends Base {} + + @Fixture(() => ({ name: "post" })) + class Posts extends Base {} + + const generators = [Base, Users, Posts].map( + (tableClass) => getMetadata(tableClass, DatumStaticMetadata).generator, + ); + + expect(generators[0]).to.equal(undefined); + expect(generators[1]).to.be.a("function"); + expect(generators[2]).to.be.a("function"); + expect(generators[1]).to.not.equal(generators[2]); + }); + + it("keeps the modified fields of each subclass apart", async () => { + class Base extends Table.with(HashModifier) { + @Hashed() + declare token: string; + } + + class Users extends Base { + @Hashed() + declare password: string; + } + + class Posts extends Base { + @Hashed() + declare secret: string; + } + + expect(getModifiedFields(new Base(), HashModifier)).to.deep.equal([ + "token", + ]); + expect(getModifiedFields(new Users(), HashModifier)).to.deep.equal([ + "token", + "password", + ]); + expect(getModifiedFields(new Posts(), HashModifier)).to.deep.equal([ + "token", + "secret", + ]); + }); + + it("keeps the modifier events of each subclass apart", async () => { + class Stamp extends OneWayModifier { + public override lock(_locked: string | undefined, value: string) { + return value; + } + + public toDatabase() {} + } + + class Base extends Table { + declare createdBy: string; + } + attachModifier(Base, Stamp, "createdBy", {}); + + class Users extends Base { + declare email: string; + } + attachModifier(Users, Stamp, "email", {}); + + const events = (tableClass: typeof Table) => + getMetadata(tableClass, ModifiersStaticMetadata).events.get("toDatabase"); + + expect(events(Base)).to.have.length(1); + expect(events(Users)).to.have.length(2); + }); + + it("keeps the relations of each subclass apart", async () => { + class Target extends Table {} + + class Base extends Table { + @Relation({ to: () => Target }) + declare owner: string; + } + + class Users extends Base { + @Relation({ to: () => Target }) + declare team: string; + } + + const relationsOf = (tableClass: typeof Table) => + Object.keys(getMetadata(tableClass, RelationStaticMetadata).relations); + + expect(relationsOf(Base)).to.deep.equal(["owner"]); + expect(relationsOf(Users)).to.deep.equal(["owner", "team"]); + }); + + it("starts a subclass from the metadata its parent holds", async () => { + class Base extends Table { + @Field("string") + declare createdBy: string; + } + + @RegisterTable("inheritance_inherited", schemaName) + class Registered extends Base {} + + class Child extends Registered {} + + expect(getMetadata(Child, DatumStaticMetadata).tableName).to.equal( + "inheritance_inherited", + ); + expect(fieldsOf(Child)).to.deep.equal(["createdBy"]); + }); +}); diff --git a/src/tests/subclass_schema_binding.test.ts b/src/tests/subclass_schema_binding.test.ts new file mode 100644 index 0000000..bbaa0bd --- /dev/null +++ b/src/tests/subclass_schema_binding.test.ts @@ -0,0 +1,86 @@ +import { expect } from "chai"; +import { Schema } from "@antelopejs/interface-database"; +import { DatumStaticMetadata, getMetadata } from "../common"; +import { boundSchema } from "../internal/bound-schema"; +import { RegisterTable } from "../schema"; +import { RegisterSchema } from "../database"; +import { BasicDataModel, GetModel } from "../model"; +import { Field, Table } from "../table"; + +describe("Schema binding - subclasses of a table", () => { + it("binds a subclass whose model is built before RegisterSchema ran", async () => { + @RegisterTable("binding_users", "binding-before-schema") + class Users extends Table { + @Field("string") + declare name: string; + } + class Admin extends Users {} + + const AdminModel = BasicDataModel(Admin); + await RegisterSchema("binding-before-schema"); + + const model = GetModel(AdminModel, "binding-instance"); + expect(model.database.build()[0].options.id).to.equal( + "binding-before-schema", + ); + expect(Admin.table().build()[0].options.id).to.equal( + "binding-before-schema", + ); + }); + + it("binds a subclass declared after RegisterSchema ran", async () => { + @RegisterTable("binding_late_users", "binding-after-schema") + class Users extends Table { + @Field("string") + declare name: string; + } + await RegisterSchema("binding-after-schema"); + class Admin extends Users {} + + const model = GetModel(BasicDataModel(Admin), "binding-instance"); + expect(model.database.build()[0].options.id).to.equal( + "binding-after-schema", + ); + expect(Admin.table().build()[0].options.id).to.equal( + "binding-after-schema", + ); + }); + + it("binds a subclass that registers its own table to its own schema", async () => { + @RegisterTable("binding_parents", "binding-parent-schema") + class Parent extends Table { + @Field("string") + declare name: string; + } + @RegisterTable("binding_children", "binding-child-schema") + class Child extends Parent {} + + await RegisterSchema("binding-parent-schema"); + await RegisterSchema("binding-child-schema"); + + expect(Parent.table().build()[0].options.id).to.equal( + "binding-parent-schema", + ); + expect(Child.table().build()[0].options.id).to.equal( + "binding-child-schema", + ); + }); + + it("follows the schema its parent is bound to when RegisterSchema runs again", async () => { + @RegisterTable("binding_rebound", "binding-rebound-schema") + class Users extends Table { + @Field("string") + declare name: string; + } + await RegisterSchema("binding-rebound-schema"); + class Admin extends Users {} + const adminMetadata = getMetadata(Admin, DatumStaticMetadata); + const first = boundSchema(adminMetadata); + + await RegisterSchema("binding-rebound-schema"); + + const second = Schema.get("binding-rebound-schema"); + expect(second).to.not.equal(first); + expect(boundSchema(adminMetadata)).to.equal(second); + }); +}); From 9f1e2d346f6e19cf553908852b48cd0718c47c75 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sat, 31 Oct 2026 07:02:25 +0100 Subject: [PATCH 06/45] fix(modifiers): look up modified fields and event names by own key (#53) * test(modifiers): pin modifier fields and events named like Object.prototype members * fix(modifiers): look up modified fields and event names by own key * test(modifiers): pin that a modifier on a field named __proto__ is refused * fix(modifiers): refuse a modifier on a field named __proto__ * refactor(modifiers): keep the refused-field check with the record helpers --- src/modifiers/common.ts | 6 +- src/modifiers/internal/records.ts | 22 ++++ src/tests/modifiers/inherited_names.test.ts | 118 ++++++++++++++++++++ 3 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 src/tests/modifiers/inherited_names.test.ts diff --git a/src/modifiers/common.ts b/src/modifiers/common.ts index 8b67cb1..5f070b7 100644 --- a/src/modifiers/common.ts +++ b/src/modifiers/common.ts @@ -11,6 +11,7 @@ import { prototypeOf, } from "./internal/kinds"; import { + assertModifiableField, copyModifierLists, dataByKey, eventsByKey, @@ -370,7 +371,8 @@ export function attachModifier< const metaTable = getMetadata(TableClass, ModifiersStaticMetadata); if (!isOnlyEvents(entry.modifier)) { - if (field in metaTable.fields) { + assertModifiableField(TableClass.name, entry.id, field); + if (Object.hasOwn(metaTable.fields, field)) { const list = fieldsByKey(metaTable)[field]; const last = list[list.length - 1]; if (isWriteOnly(last.modifier)) { @@ -416,7 +418,7 @@ export function attachModifier< for (const event of Object.getOwnPropertyNames(Modifier.prototype)) { if ( - !(event in ignoredEventNames) && + !Object.hasOwn(ignoredEventNames, event) && isCallable(Modifier.prototype[event]) ) { if (!metaTable.events.has(event)) { diff --git a/src/modifiers/internal/records.ts b/src/modifiers/internal/records.ts index 166346c..486c7c0 100644 --- a/src/modifiers/internal/records.ts +++ b/src/modifiers/internal/records.ts @@ -26,6 +26,28 @@ export function getInternal(object: InternalHolder): InternalRecord { return object._internal; } +/** + * Throws when a modifier cannot be attached to the field named `field`: the + * two names the table's own structures live under. + */ +export function assertModifiableField( + tableName: string, + modifierId: string, + field: PropertyKey, +): void { + const reason = + field === "constructor" + ? "the field would replace the class constructor that locates the metadata of every instance" + : field === "__proto__" + ? "the field would replace the prototype of the record of modified fields and never be recorded" + : undefined; + if (reason) { + throw new Error( + `Cannot add Modifier ${modifierId} on ${tableName}.${String(field)}: ${reason}.`, + ); + } +} + export function fieldsByKey(meta: ModifiersStaticMetadata): { [field: PropertyKey]: ModifiedFieldList; } { diff --git a/src/tests/modifiers/inherited_names.test.ts b/src/tests/modifiers/inherited_names.test.ts new file mode 100644 index 0000000..190a3e5 --- /dev/null +++ b/src/tests/modifiers/inherited_names.test.ts @@ -0,0 +1,118 @@ +import { expect } from "chai"; +import { Table } from "../../table"; +import { Hashed, HashModifier } from "../../modifiers/hash"; +import { LocalizationModifier, Localized } from "../../modifiers/localization"; +import { + attachModifier, + getModifiedFields, + OneWayModifier, + triggerEvent, +} from "../../modifiers/common"; + +const PROTOTYPE_MEMBER_NAMES = [ + "toString", + "valueOf", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "toLocaleString", +]; + +describe("Modifiers - names shared with Object.prototype members", () => { + for (const name of PROTOTYPE_MEMBER_NAMES) { + it(`attaches a modifier to a field named ${name}`, async () => { + class Account extends Table.with(HashModifier) {} + Hashed()(Account.prototype, name); + + expect(getModifiedFields(new Account(), HashModifier)).to.deep.equal([ + name, + ]); + expect(Object.getOwnPropertyDescriptor(Account.prototype, name)) + .to.have.property("set") + .that.is.a("function"); + }); + + it(`attaches a second modifier to a field named ${name}`, async () => { + class Content extends Table.with(HashModifier, LocalizationModifier) {} + Localized()(Content.prototype, name); + Hashed()(Content.prototype, name); + + expect(getModifiedFields(new Content(), HashModifier)).to.deep.equal([ + name, + ]); + expect( + getModifiedFields(new Content(), LocalizationModifier), + ).to.deep.equal([name]); + }); + } + + it("refuses a modified field named constructor", async () => { + class Account extends Table.with(HashModifier) {} + + expect(() => Hashed()(Account.prototype, "constructor")).to.throw( + "Account.constructor", + ); + expect(Account.prototype.constructor).to.equal(Account); + }); + + for (const [label, decorate] of Object.entries({ Hashed, Localized })) { + it(`refuses a field named __proto__ with ${label}`, async () => { + const Base = Table.with(HashModifier, LocalizationModifier); + class Account extends Base { + declare email: string; + } + Hashed()(Account.prototype, "email"); + + expect(() => decorate()(Account.prototype, "__proto__")).to.throw( + "Account.__proto__", + ); + expect(() => decorate()(Account.prototype, "__proto__")).to.throw( + "Account.__proto__", + ); + expect(Object.getPrototypeOf(Account.prototype)).to.equal(Base.prototype); + expect(getModifiedFields(new Account(), HashModifier)).to.deep.equal([ + "email", + ]); + }); + } + + it("keeps the fields of two tables apart", async () => { + class Left extends Table.with(HashModifier) {} + class Right extends Table.with(HashModifier) {} + Hashed()(Left.prototype, "toString"); + Hashed()(Right.prototype, "valueOf"); + + expect(getModifiedFields(new Left(), HashModifier)).to.deep.equal([ + "toString", + ]); + expect(getModifiedFields(new Right(), HashModifier)).to.deep.equal([ + "valueOf", + ]); + }); + + for (const eventName of ["toString", "valueOf", "hasOwnProperty"]) { + it(`registers a modifier method named ${eventName} as an event`, async () => { + const calls: string[] = []; + + class Account extends Table { + declare nickname: string; + } + class Watcher extends OneWayModifier { + public override lock(_locked: string | undefined, value: string) { + return value; + } + } + Object.defineProperty(Watcher.prototype, eventName, { + value: (_row: Account, field: PropertyKey) => { + calls.push(String(field)); + }, + configurable: true, + }); + attachModifier(Account, Watcher, "nickname", {}); + + triggerEvent(new Account(), eventName); + + expect(calls).to.deep.equal(["nickname"]); + }); + } +}); From 734fb5912e50f54ef9f508b0a8585741d414895e Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sat, 31 Oct 2026 07:12:22 +0100 Subject: [PATCH 07/45] docs: typecheck every example and drop hand-written version prose (#58) * docs: typecheck every example and drop hand-written version prose * fix(docs): stop the docs program from emitting declarations --- .github/workflows/ci.yml | 2 +- README.md | 2 +- docs/2.table-definitions.md | 24 ++++++++++++++++++++---- docs/3.table-modifiers.md | 10 +++++----- docs/4.data-models.md | 26 +++++++++++++------------- docs/5.parameter-decoration.md | 12 ++++++------ docs/tsconfig.json | 3 +++ 7 files changed, 49 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90ab75f..9ee1268 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ permissions: jobs: ci: - uses: AntelopeJS/.github/.github/workflows/ci-node.yml@v1 + uses: AntelopeJS/.github/.github/workflows/ci-node.yml@v1-campaign with: docs-files: | README.md diff --git a/README.md b/README.md index f764e5d..006ab5f 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ export async function start() { ## Compatibility -Requires AntelopeJS core `1.x` and the following peer dependencies: +The package declares these `@antelopejs/*` peer dependencies: diff --git a/docs/2.table-definitions.md b/docs/2.table-definitions.md index ba78eb9..6efcdfc 100644 --- a/docs/2.table-definitions.md +++ b/docs/2.table-definitions.md @@ -79,6 +79,8 @@ Every index is scoped to the schema instance, so queries on a single instance al The option never changes query results. Without it, cross-instance queries that name the index still return the same rows but run slower, and the driver logs a warning once per index. Leave it unset for indexes that are only queried within one instance, since each extra index costs storage and write time. ```typescript +import { Field, Index, Table } from "@antelopejs/interface-database-decorators"; + class User extends Table { @Index({ crossInstance: true }) @Field("string") @@ -93,6 +95,8 @@ For a compound index, setting `crossInstance: true` on any field of the group ap Set `unique: true` to refuse a second row with the same key in one instance. The write that would store it throws `DuplicateKeyError` from `@antelopejs/interface-database`, naming the index and its fields; test it with `IsDuplicateKeyError`. Rows of different instances may share a key. ```typescript +import { Field, Index, Table } from "@antelopejs/interface-database-decorators"; + class Template extends Table { @Index({ unique: true }) @Field("string") @@ -138,7 +142,7 @@ class UserActivity extends Table { `@Field(type)` declares the field type for a property. The type is forwarded verbatim into the `TableDefinition.fields` map handed to the adapter. It accepts the full `FieldType` union -- primitive tokens (`"string"`, `"number"`, ...), arrays, nested records, or an io-ts codec. -```typescript +```typescript fragment import * as t from "io-ts"; import { Table, @@ -167,7 +171,7 @@ Properties without `@Field` are omitted from `fields`. `@Field` stacks freely wi The `Relation` decorator declares a link from a field to another table. It is **declarative metadata only**: database implementations do not enforce it (no foreign key constraint, no referential validation). Introspection tooling consumes it to expose the links between tables — for example to render schema diagrams or navigate related records. -```typescript +```typescript fragment import { Table, Field, @@ -252,7 +256,13 @@ The generator function: ### Async Generators -```typescript +```typescript fragment +import { + Field, + Fixture, + Table, +} from "@antelopejs/interface-database-decorators"; + @Fixture(async () => { const defaults = await loadDefaultConfig(); return defaults.map((cfg) => ({ @@ -336,7 +346,13 @@ await RegisterSchema("myapp"); `Table.table()` returns the AQL table a class describes, in the schema `RegisterSchema` bound it to: -```typescript +```typescript fragment +import { + RegisterSchema, + RegisterTable, + Table, +} from "@antelopejs/interface-database-decorators"; + @RegisterTable("users", "myapp") class User extends Table { declare name: string; diff --git a/docs/3.table-modifiers.md b/docs/3.table-modifiers.md index 12571a7..436588b 100644 --- a/docs/3.table-modifiers.md +++ b/docs/3.table-modifiers.md @@ -78,7 +78,7 @@ class User extends Table.with(HashModifier) { declare password: string; verifyPassword(plainPassword: string): boolean { - return this.testHash("password", plainPassword); + return this.testHash("password", plainPassword) === true; } } ``` @@ -101,7 +101,7 @@ class User extends Table.with(HashModifier) { The `HashModifier` mixin adds a `testHash` method to table instances: -```typescript +```typescript fragment testHash(field: string, value: unknown): boolean ``` @@ -135,7 +135,7 @@ class Product extends Table.with(LocalizationModifier) { #### Usage -```typescript +```typescript fragment const product = new Product(); product._id = "prod-123"; product.price = 99.99; @@ -158,7 +158,7 @@ console.log(german.name); // "Premium Headphones" (fallback) The wildcard key `"*"` retrieves or sets all translations at once at runtime. `localize()` does not change the property's TypeScript type, so a field declared as `string` needs an explicit view for bulk access: -```typescript +```typescript fragment // Get all translations const translations = product.localize("*") as unknown as { name: Record; @@ -185,7 +185,7 @@ translations.name = { The `LocalizationModifier` mixin adds a `localize` method: -```typescript +```typescript fragment localize(locale: string, fields?: Array): this ``` diff --git a/docs/4.data-models.md b/docs/4.data-models.md index 67302a9..7525d1b 100644 --- a/docs/4.data-models.md +++ b/docs/4.data-models.md @@ -6,7 +6,7 @@ Data models provide a high-level interface for interacting with database tables. The `BasicDataModel` function creates a model class for a specific table. It wraps the underlying AQL table with methods that automatically apply modifier transformations. -```typescript +```typescript fragment import { BasicDataModel } from "@antelopejs/interface-database-decorators"; import { User } from "./user.table"; @@ -26,7 +26,7 @@ When the table class has been decorated with `@RegisterTable`, the `tableName` p A model instance requires a `SchemaInstance` from the database interface: -```typescript +```typescript fragment import { Schema } from "@antelopejs/interface-database"; const schema = Schema.get("myapp")!; @@ -51,7 +51,7 @@ Use `model.table` to compose AQL queries before execution. The static `fromDatab ### Read -```typescript +```typescript fragment // Get by primary key const user = await userModel.get("user-123"); @@ -64,7 +64,7 @@ const allUsers = await userModel.getAll(); ### Create -```typescript +```typescript fragment // Insert a single document const ids = await userModel.insert({ email: "alice@example.com", @@ -80,7 +80,7 @@ const batchIds = await userModel.insert([ ### Update -```typescript +```typescript fragment // Update by ID await userModel.update("user-123", { name: "Updated Name" }); @@ -92,7 +92,7 @@ await userModel.update({ _id: "user-123", name: "Updated Name" }); ### Delete -```typescript +```typescript fragment await userModel.delete("user-123"); ``` @@ -114,7 +114,7 @@ Use `insert` or `update` to perform a write; conversion alone does not save anyt Convert a plain JavaScript object into a Table class instance. Modifier transformations (like encryption and hashing) are applied during this conversion. -```typescript +```typescript fragment const user = UserModel.fromPlainData({ _id: "user-123", email: "user@example.com", @@ -126,7 +126,7 @@ const user = UserModel.fromPlainData({ Convert a raw database record into a Table class instance. This attaches the class prototype and triggers the `fromDatabase` modifier event. -```typescript +```typescript fragment const user = UserModel.fromDatabase(rawDbRecord); ``` @@ -134,7 +134,7 @@ const user = UserModel.fromDatabase(rawDbRecord); Convert a Table class instance into a plain object suitable for database storage. Modifier events like `toDatabase` are triggered. -```typescript +```typescript fragment const dbReady = UserModel.toDatabase(userInstance); ``` @@ -142,7 +142,7 @@ const dbReady = UserModel.toDatabase(userInstance); `Model.validate(obj)` runs every io-ts codec attached via `@Field` against the matching property of `obj`. String-token fields and properties without `@Field` are skipped. -```typescript +```typescript fragment const result = UserModel.validate({ name: "Alice", age: 30 }); if (!result.ok) { console.error(result.errors); // [{ field: "age", message: "not number" }, ...] @@ -151,7 +151,7 @@ if (!result.ok) { Pass `{ validate: true }` to `insert` or `update` to gate the write on validation. On failure the call throws an `Error` whose `errors` property holds the per-field details; on success the original object passes through unchanged. -```typescript +```typescript fragment await userModel.insert({ name: "Alice", age: 30 }, { validate: true }); await userModel.update("user-123", { name: "Bob" }, { validate: true }); ``` @@ -164,7 +164,7 @@ Extend the generated model class to add custom query methods: The `register` method below checks for an existing email to provide early feedback. It does not guarantee uniqueness under concurrent writes; enforce that constraint in the database where supported and handle conflicts. -```typescript +```typescript fragment const UserModelBase = BasicDataModel(User, "users"); class CustomUserModel extends UserModelBase { @@ -188,7 +188,7 @@ class CustomUserModel extends UserModelBase { The `GetModel` function retrieves or creates a cached model instance for a given schema: -```typescript +```typescript fragment import { GetModel } from "@antelopejs/interface-database-decorators"; const userModel = GetModel(UserModel, "instance-id"); diff --git a/docs/5.parameter-decoration.md b/docs/5.parameter-decoration.md index f6400ef..110d8c2 100644 --- a/docs/5.parameter-decoration.md +++ b/docs/5.parameter-decoration.md @@ -10,7 +10,7 @@ The `Model` decorator injects a cached model instance. It can be used as either Pass a fixed instance ID to bind the model to a specific schema instance: -```typescript +```typescript fragment import { Controller, Get } from "@antelopejs/interface-api"; import { Model } from "@antelopejs/interface-database-decorators"; import { UserModel } from "./user.model"; @@ -31,7 +31,7 @@ Pass a callback function to determine the instance ID at request time. The callb Authorize access to the requested tenant before reading or writing through the injected model. The callback selects an instance; a URL parameter alone is not proof that the caller belongs to that tenant. -```typescript +```typescript fragment import { Controller, Get, Parameter } from "@antelopejs/interface-api"; import { Model } from "@antelopejs/interface-database-decorators"; import { UserModel } from "./user.model"; @@ -40,7 +40,7 @@ class MultitenantController extends Controller("/tenants") { @Get("/:tenantId/users") async getTenantUsers( @Parameter("tenantId") tenantId: string, - @Model(UserModel, (ctx) => `tenant-${ctx.params.tenantId}`) + @Model(UserModel, (ctx) => `tenant-${ctx.routeParameters.tenantId}`) userModel: InstanceType, ) { return await userModel.getAll(); @@ -52,7 +52,7 @@ class MultitenantController extends Controller("/tenants") { The `Model` decorator also works as a property decorator. The model is initialized when the controller handles a request: -```typescript +```typescript fragment import { Controller, Get, Parameter } from "@antelopejs/interface-api"; import { Model } from "@antelopejs/interface-database-decorators"; import { UserModel } from "./models"; @@ -77,7 +77,7 @@ class UsersController extends Controller("/users") { The `Model` decorator uses `GetModel` internally, which caches model instances by class and instance ID. Multiple decorators referencing the same model class and instance ID share a single instance: -```typescript +```typescript fragment import { GetModel } from "@antelopejs/interface-database-decorators"; // These return the same instance @@ -94,7 +94,7 @@ Model decorators compose naturally with other API decorators for building comple This example demonstrates injection, not a complete public API policy. Before exposing writes, reject malformed or non-object JSON, allow only permitted fields, validate their values, and authorize the operation. `JSON.parse` and `@Model` do not provide these checks; string-token `@Field` declarations do not validate input either. -```typescript +```typescript fragment import { Controller, Get, diff --git a/docs/tsconfig.json b/docs/tsconfig.json index d7f31e7..035ac0b 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -2,6 +2,9 @@ "extends": "../tsconfig.json", "compilerOptions": { "rootDir": "..", + "module": "esnext", + "moduleResolution": "bundler", + "declaration": false, "noEmit": true, "composite": false, "incremental": false From 0cf86eea8c8b92e22c6cea8298ddcae6c15d0889 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sat, 31 Oct 2026 13:54:47 +0100 Subject: [PATCH 08/45] feat(database-decorators)!: type the fixture, model and modifier event contracts (#62) * test(modifiers): pin modifier fields and events named like Object.prototype members * fix(modifiers): look up modified fields and event names by own key * build: run pnpm 10.33.0 pnpm before 10.20 fails a frozen install of some file: tarball dependencies (ENOENT in the store index), which the campaign tarball pins need. * build(deps): test against the interface-database 0.3.0 tarball Producer: AntelopeJS/interface-database feat/typed-query-stages at 4939b45, version 0.3.0. The devDependency range ^0.1.11 excludes 0.3.0, so the contract suite never ran against it. * test: read the schema stage's id through interface-database 0.3.0's stage union The recorded options are typed per stage now, so the tests match the schema stage and its id instead of reading options.id off any stage. * test(modifiers): pin that a modifier on a field named __proto__ is refused * fix(modifiers): refuse a modifier on a field named __proto__ * refactor(modifiers): keep the refused-field check with the record helpers * test: read the schema stage's id through interface-database 0.3.0's stage union in the subclass binding tests The subclass binding tests landed after the 0.3.0 tarball pin was written, and read options.id off any stage; the recorded options are typed per stage now. * feat(database-decorators)!: type the fixture generator by its table class Fixture now checks that the class it is called with is the decorated class or a subclass of it, so it calls the generator with that class without an assertion. The stored generator gives plain objects instead of records of unknown values. BREAKING CHANGE: DatumGeneratorOutput is `Row | Row[] | undefined` over objects (`Row = object` by default) instead of records of unknown values. * feat(database-decorators)!: type the model's schema name and index lookups BasicDataModel keeps the schema name its table class registers into, which is undefined for a table class without @RegisterTable; the type now says so instead of asserting a string. getBy takes an index named by a string field of the table and keys typed as interface-database's selection keys. BREAKING CHANGE: DataModel.schemaName is `string | undefined`. getBy's index is `Extract` and its keys are `IndexKey` (a value proxy or a string, number or boolean), no longer `any`. * feat(database-decorators)!: type the auto-date event handlers by the field they stamp BREAKING CHANGE: AutoDateModifier.insert and update take `(object: Partial>, field: F)` instead of a record of unknown values and a string. * feat(database-decorators)!: refuse @Index on a symbol-keyed field An index names a database field, so the decorator throws for a symbol key instead of recording the symbol as a string. BREAKING CHANGE: @Index on a symbol-keyed property throws at class declaration. * feat(database-decorators)!: drop the once-used type parameters of getModifiedFields and toDatabase getModifiedFields takes any modifier class and toDatabase takes the table instance as `{ constructor: any }`. getBy's IndexKey is derived from Table.getAll instead of interface-database's common subpath. BREAKING CHANGE: getModifiedFields has one type parameter (the object's type) instead of two; toDatabase has none. * feat(database-decorators)!: type the stored unlock arguments as unknown values The arguments a modifier is unlocked or locked with are whatever the caller passed for it, so ModifiersDynamicMetadata.fields records them as unknown arrays and the engine no longer spreads `any` into modifier methods. BREAKING CHANGE: ModifiersDynamicMetadata.fields is `Record>` instead of `any[]` lists. * feat(database-decorators)!: record each modifier event with its handler attachModifier now stores the modifier method it found for an event next to the field and the modifier, so triggerEvent calls that method with the modifier as `this` instead of looking it up by name through two assertions. BREAKING CHANGE: the entries of ModifiersStaticMetadata.events are `[field, modified, handler]`, with the new exported ModifierEvent type for the handler. * build: set the package version to 0.2.0 * chore: lower the ratchet ceilings to the findings left after the contract changes * test: assert the error type of the symbol-keyed index refusal, not its wording * build(deps): test against interface-database 6bef278 --- .campaign/tgz/SHA256SUMS | 1 + .../antelopejs-interface-database-6bef278.tgz | Bin 0 -> 117250 bytes .campaign/tgz/ranges.tsv | 3 ++ README.md | 10 ++-- package.json | 11 ++-- pnpm-lock.yaml | 48 +++++++++++++++--- pnpm-workspace.yaml | 2 + src/common.ts | 13 +++-- src/model.ts | 24 +++++++-- src/modifiers/autodate.ts | 4 +- src/modifiers/common.ts | 44 ++++++++-------- src/modifiers/internal/kinds.ts | 23 ++++++++- src/modifiers/internal/records.ts | 9 +++- src/table.ts | 28 +++++++--- src/tests/cross_instance.test.ts | 10 +++- src/tests/database.test.ts | 10 +++- src/tests/metadata_inheritance.test.ts | 14 +++++ src/tests/subclass_schema_binding.test.ts | 42 ++++++++------- src/tests/table.test.ts | 13 +++++ 19 files changed, 233 insertions(+), 76 deletions(-) create mode 100644 .campaign/tgz/SHA256SUMS create mode 100644 .campaign/tgz/antelopejs-interface-database-6bef278.tgz create mode 100644 .campaign/tgz/ranges.tsv create mode 100644 pnpm-workspace.yaml diff --git a/.campaign/tgz/SHA256SUMS b/.campaign/tgz/SHA256SUMS new file mode 100644 index 0000000..5d86e53 --- /dev/null +++ b/.campaign/tgz/SHA256SUMS @@ -0,0 +1 @@ +b622e0659f1b0cd8a1a6455061b85e6e7e5a2a6fdbbab1add9cc576b65e7bda5 antelopejs-interface-database-6bef278.tgz diff --git a/.campaign/tgz/antelopejs-interface-database-6bef278.tgz b/.campaign/tgz/antelopejs-interface-database-6bef278.tgz new file mode 100644 index 0000000000000000000000000000000000000000..560d7eefbdd96cfc0d28745ce6ed25e44e48de12 GIT binary patch literal 117250 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 literal 0 HcmV?d00001 diff --git a/.campaign/tgz/ranges.tsv b/.campaign/tgz/ranges.tsv new file mode 100644 index 0000000..41af30c --- /dev/null +++ b/.campaign/tgz/ranges.tsv @@ -0,0 +1,3 @@ +@antelopejs/interface-database package.json devDependencies ^0.1.11 +@antelopejs/interface-database package.json pnpm.overrides +@antelopejs/interface-database pnpm-workspace.yaml overrides diff --git a/README.md b/README.md index 006ab5f..5bfdd31 100644 --- a/README.md +++ b/README.md @@ -92,11 +92,11 @@ The package declares these `@antelopejs/*` peer dependencies: -| Package | Supported | Tested with | -| -------------------------------- | ----------------- | ----------- | -| `@antelopejs/interface-api` | `>=0.0.15 <1.0.0` | `0.0.15` | -| `@antelopejs/interface-core` | `>=0.2.4 <1.0.0` | `0.2.4` | -| `@antelopejs/interface-database` | `>=0.1.11 <1.0.0` | `0.1.11` | +| Package | Supported | Tested with | +| -------------------------------- | ----------------- | -------------------------------------------------------------- | +| `@antelopejs/interface-api` | `>=0.0.15 <1.0.0` | `0.0.15` | +| `@antelopejs/interface-core` | `>=0.2.4 <1.0.0` | `0.2.4` | +| `@antelopejs/interface-database` | `>=0.1.11 <1.0.0` | `file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz` | diff --git a/package.json b/package.json index e3b74d3..c0de0b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@antelopejs/interface-database-decorators", - "version": "0.1.11", + "version": "0.2.0", "description": "AntelopeJS decorators for database tables and typed data models, with encrypted, hashed and localized fields", "keywords": [ "antelopejs", @@ -147,7 +147,7 @@ "@antelopejs/core": "^1.16.0", "@antelopejs/interface-api": "^0.0.15", "@antelopejs/interface-core": "^0.2.4", - "@antelopejs/interface-database": "^0.1.11", + "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz", "@antelopejs/tooling-configs": "^0.2.17", "@types/chai": "^4.3.20", "@types/mocha": "^10.0.10", @@ -172,7 +172,12 @@ "@antelopejs/interface-core": ">=0.2.4 <1.0.0", "@antelopejs/interface-database": ">=0.1.11 <1.0.0" }, - "packageManager": "pnpm@10.6.5", + "packageManager": "pnpm@10.33.0", + "pnpm": { + "overrides": { + "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz" + } + }, "antelopeJs": { "standalone": true, "test": "src/antelope.test.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 329068e..04bd286 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,10 +4,16 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + '@antelopejs/interface-database': file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz + importers: .: dependencies: + '@antelopejs/interface-database': + specifier: file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz + version: file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz(@antelopejs/interface-core@0.2.4) randomstring: specifier: ^1.3.1 version: 1.3.1 @@ -24,9 +30,6 @@ importers: '@antelopejs/interface-core': specifier: ^0.2.4 version: 0.2.4 - '@antelopejs/interface-database': - specifier: ^0.1.11 - version: 0.1.11(@antelopejs/interface-core@0.2.4) '@antelopejs/tooling-configs': specifier: ^0.2.17 version: 0.2.17(eslint-plugin-perfectionist@5.11.0(eslint@10.10.0(jiti@2.7.0))(typescript@5.9.3))(knip@6.34.0)(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001)) @@ -97,8 +100,9 @@ packages: '@antelopejs/interface-core@0.2.4': resolution: {integrity: sha512-2qF6/KwahCQvvNuC/piUvPWD4wq2Npmco941HMg2/bMBoJKbhMH16WcXXRvPYznNSkU0U+6vqK6KE+2N5NojSA==} - '@antelopejs/interface-database@0.1.11': - resolution: {integrity: sha512-uv6mB1Lghmlc9z+oWzntmzD1ZNBAmrj+2+7MItv6zClEolbqw5hfAt8+c3IPq2yLfWDfhpqWme2m0eTwsAm0RQ==} + '@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz': + resolution: {integrity: sha512-iFR2hP69eGn4xlxYiOXOhbuvirLgXQ25LJk0/sP84VJ4xPgxfrwX6YEfrVbhvpUjWLlnWXly+om5qKgU1CQZ6Q==, tarball: file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz} + version: 0.3.0 peerDependencies: '@antelopejs/interface-core': '>=0.2.4 <1.0.0' @@ -456,48 +460,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-arm64-musl@0.147.0': resolution: {integrity: sha512-gd7VX/FDVOw6mjQcu45iIcp4QkgybgJwh3a0OFG2NxmPCj628mQWD96QGu1kK8+mZF9qK4b/gIEyC63vQoB7+Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-parser/binding-linux-ppc64-gnu@0.147.0': resolution: {integrity: sha512-HnAzcfki7dSUNHf510Q2NmbJlz8Ys7rn8l9l588Pkx0tYe1BHLZnmELIgqizJ4WPhHGSwN8Ce+B/menVxS3odA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-riscv64-gnu@0.147.0': resolution: {integrity: sha512-qlkOL6wT44U+fT5s/+sR6Shx0OdwvQF83JyIPZUxG/ovqZF5/7atOtjH+JPZ5/7ATQLbFBBSmghcy/+2NVB/ew==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-riscv64-musl@0.147.0': resolution: {integrity: sha512-DlefD7L7sMXs/3hIBH23Egk0phj8kG0SA81dVGOQ3S1ekjOlmTLH2E+F2Thwfh1slKx6aH+lNc5fQYAw0GU7/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-parser/binding-linux-s390x-gnu@0.147.0': resolution: {integrity: sha512-Xqpagk/031IvZ4svrk2FF01YEqM/iN3MJV3SVZadKg/CsGlDCGoREqKHXYnoV5+8SfGe/m6RM1szXFLusTu/Uw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-x64-gnu@0.147.0': resolution: {integrity: sha512-QioQOeUbI4ATUr0S2z88uA3Cds2R3Mm5Ge7U8XNYtlTb2GJF3rWlcj70z0AJhhOlbdm0YgVjqPBldUNbFylDIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-x64-musl@0.147.0': resolution: {integrity: sha512-NXy1tv/OdC+pPTwf9RiCZWPK53V/Xq/2cjSnjOSyKopajdaDqMIkgtDY+jXZemp2e8px5FeWfY2L2LwhKZQovg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxc-parser/binding-openharmony-arm64@0.147.0': resolution: {integrity: sha512-GpGWZ6oKz4bjCWW9Mz5pCaGPyk2Aaze6zEoaslIQqpSLtpx5pXj/ap5gUNb5Jn2LIbqWyjkGLX9yv3NMuNcBVQ==} @@ -565,41 +577,49 @@ packages: resolution: {integrity: sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.24.2': resolution: {integrity: sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': resolution: {integrity: sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': resolution: {integrity: sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': resolution: {integrity: sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': resolution: {integrity: sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.24.2': resolution: {integrity: sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.24.2': resolution: {integrity: sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==} cpu: [x64] os: [linux] + libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.24.2': resolution: {integrity: sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==} @@ -668,48 +688,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-arm64-musl@0.66.0': resolution: {integrity: sha512-tTkF2Dmx4nGAjmBlZb+UtTGqR/EK4ZrW9qBfzte07a9XWqzoGGKzpFFlyNDhQe+Uwql94+ReCTeNbhOXscw1Dg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-ppc64-gnu@0.66.0': resolution: {integrity: sha512-F3cKHUav4yXOHn6GFnwpBhSYsJOYKKf9eqO/9jlEuqPxNw9zb98E9ZFct79gcg8pibUGkbveEu9WDlmXJpDzKw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-gnu@0.66.0': resolution: {integrity: sha512-K5fDaNZfDyQMYA/3qL21bqyN0X9T15LLwwbFPt2aHc94+ZG7bh0vZEsy2y7NlRnjjHFSwN+Hzg6ldJtbOriH4Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-musl@0.66.0': resolution: {integrity: sha512-44Yc+I+qOmTElRcEhm5hUKIUJEQIOugymz4ua4tB0Wox7tGAfIbjzmXz/HDAtw1Ij6gmBwZlzh4hc9679RhWeA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-s390x-gnu@0.66.0': resolution: {integrity: sha512-1e29Eg9hEj2kRBB19M0seIehPbbXHCk35GvImjDvb79rjjYjXCRmtbUNHJcgoktZAMIzXrTbxDBKmTc1V4bg3A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-gnu@0.66.0': resolution: {integrity: sha512-vODY1UQo10gngn0+D4xHKU84F1Twm1LqrzV4SqPXvmQKSd87paehvZ6jqA5wKs6XQrlWul9clYMDVHcoW9CPMA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-musl@0.66.0': resolution: {integrity: sha512-YDzXx2JsT4+HL4MdkVrYjO55NS5lUKNm8rLC4ZPou8+seu0v0jhecSh+ufoO6+xEa8gccEezMlI2WHJi4ApUgw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxfmt/binding-openharmony-arm64@0.66.0': resolution: {integrity: sha512-mJjUYd8lj0+j4JkYyEM+5qKBf1Rnrpgjn/SVYKJhicVDqLz566ooa7Fs8zflPqt+dnZDV7X054rVIQX6ZcQNlQ==} @@ -812,48 +840,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-arm64-musl@1.81.0': resolution: {integrity: sha512-k5iAp3dNxW0/uDCBY+WSm8jKB2szu7SkEQZdgRRpDXvuDd69vvDcqhB3A/pWCfCwXyenjNjFn9Td1fVoyAc+Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxlint/binding-linux-ppc64-gnu@1.81.0': resolution: {integrity: sha512-TFqLja3uYmVSte6nof9GWrex9Z8WgdZrNiLC6Te5rXGDqXB2y4j/26iFhwosXiAFqDhE9JJVuuCkDKLwptTn1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-gnu@1.81.0': resolution: {integrity: sha512-UEcySvGS0NOVo7h7n7CYyJL9+6gFAh7Zc/ToDXVScFvzHSTIxtzkMVU30rmQ6+nQ1LF+UdiRDdJajpDu+OylLg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-musl@1.81.0': resolution: {integrity: sha512-H+diDbhD00+wI1IRP8Kz88x/lat+DgtoBJzoTthS16xkTJGNaEkfb8gzmd1rzc/2uDQQMl7GNl+JFUacVeWxIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxlint/binding-linux-s390x-gnu@1.81.0': resolution: {integrity: sha512-8znJ/5TekjOKg1j1Acho4PJMdiAHLtlcXuWEiipOhAMV6rQcXdmDdXCbheyDczN6TjBwiNfjcP81k4AthrKRzw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-gnu@1.81.0': resolution: {integrity: sha512-Q2Wj70yFsvn5QjlmifFzbj4H+kJy53bwqc41o1fzoM7MpLV1NIbhg/LpWXRfC6KOkSAdUx1Wd8VJsdPmhp/HRA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-musl@1.81.0': resolution: {integrity: sha512-cPInHp/ddEe5qkyK2IiyQ8Q3Mp2oLLEhhsGgTK2oZx4L6+llGam1H1yBvJZ7qHfOXj8N3hxBS8sj4tO+gtFlIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxlint/binding-openharmony-arm64@1.81.0': resolution: {integrity: sha512-0CQxSX4ajqm07AHBf5U33qQzXKdd7wtq/oTL/7vpY6RNNuxrRi8W4bqUV1Jyu/vj+9KmxQyDhxfeVX1nQL6kfg==} @@ -2776,7 +2812,7 @@ snapshots: dependencies: reflect-metadata: 0.2.2 - '@antelopejs/interface-database@0.1.11(@antelopejs/interface-core@0.2.4)': + '@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz(@antelopejs/interface-core@0.2.4)': dependencies: '@antelopejs/interface-core': 0.2.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..4d6f254 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +overrides: + "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz" diff --git a/src/common.ts b/src/common.ts index d7bbe72..c3b5fec 100644 --- a/src/common.ts +++ b/src/common.ts @@ -17,10 +17,17 @@ export type DeepPartial = { : T[K]; }; -export type DatumGeneratorOutput = - | Record - | Array> +/** + * The rows a fixture generator gives for its table: one, several or none. + */ +export type DatumGeneratorOutput = + | Row + | Row[] | undefined; +/** + * A fixture generator of a table class, called with the class registered into + * the schema: the decorated class or a subclass of it. + */ export type DatumGenerator = ( tableClass: Constructible, ) => DatumGeneratorOutput | Promise; diff --git a/src/model.ts b/src/model.ts index d157ca5..e417762 100644 --- a/src/model.ts +++ b/src/model.ts @@ -27,7 +27,11 @@ import { } from "./common"; export type DataModel = { - readonly schemaName: string; + /** + * Schema the table class registers into with `@RegisterTable`; `undefined` + * for a table class that does not. + */ + readonly schemaName: string | undefined; readonly tableClass?: Constructible; new (database: DatabaseDev.SchemaInstance): { readonly database: DatabaseDev.SchemaInstance; @@ -65,6 +69,15 @@ function isCodec(type: FieldType): type is CodecLike & FieldType { ); } +/** + * One key {@link BasicDataModel}'s `getBy` matches against an index: a value, + * or a value proxy, as `Table.getAll` takes it. + */ +export type IndexKey = Exclude< + Parameters["getAll"]>[0], + unknown[] +>; + type InstanceIdProvider = (ctx: RequestContext) => InstanceId | undefined; function isInstanceIdProvider( @@ -99,7 +112,7 @@ export function BasicDataModel( // SAFETY: Model.fromDatabase returns undefined only for a falsy row, and the rows a table read or an index lookup returns are stored records, never null or undefined; the cast removes that undefined from the element type. dataR.map((row) => Model.fromDatabase(row)) as T[]; const Model = class Model { - public static readonly schemaName = schemaName as string; + public static readonly schemaName: string | undefined = schemaName; /** * Table class this model was built from. */ @@ -184,9 +197,9 @@ export function BasicDataModel( * @param keys Index value(s) * @returns Array of Table class instances. */ - public getBy(index: keyof T, ...keys: any[]) { + public getBy(index: Extract, ...keys: IndexKey[]) { return this.table - .getAll(keys.length === 1 ? keys[0] : keys, index) + .getAll(keys.length === 1 ? keys[0] : keys, index) .then((dataR) => toInstances(dataR)); } @@ -329,7 +342,8 @@ function parseUpdateArgs(args: UpdateArgs): ParsedUpdate { function schemaOf(cl: DataModel): Schema { if (!cl.tableClass) { - const schema = Schema.get(cl.schemaName); + const schema = + cl.schemaName === undefined ? undefined : Schema.get(cl.schemaName); assert(schema, `Schema not found for '${cl.schemaName}'`); return schema; } diff --git a/src/modifiers/autodate.ts b/src/modifiers/autodate.ts index 7774044..3be1f82 100644 --- a/src/modifiers/autodate.ts +++ b/src/modifiers/autodate.ts @@ -17,11 +17,11 @@ type Options = { * does not require the Table class to incorporate a mixin. */ export class AutoDateModifier extends Modifier { - public insert(object: Record, field: string) { + public insert(object: Partial>, field: F) { object[field] = new Date(); } - public update(object: Record, field: string) { + public update(object: Partial>, field: F) { if (this.options.type === "updated") { object[field] = new Date(); } else { diff --git a/src/modifiers/common.ts b/src/modifiers/common.ts index 5f070b7..d93cebc 100644 --- a/src/modifiers/common.ts +++ b/src/modifiers/common.ts @@ -3,7 +3,7 @@ import type * as DatabaseDev from "@antelopejs/interface-database"; import { type Constructible, getMetadata } from "../common"; import { ignoredEventNames, - isCallable, + isEventHandler, isLocking, isOnlyEvents, isTwoWay, @@ -124,7 +124,11 @@ interface InternalType { data: Record; } -type ModifierEvent = ( +/** + * A modifier method run for an event of the table instance (`insert`, + * `toDatabase`, ...), with the modifier as `this`. + */ +export type ModifierEvent = ( this: Modifier, object: { constructor: any }, field: PropertyKey, @@ -154,7 +158,7 @@ export class ModifiersStaticMetadata { public events = new Map< string, - Array<[field: string, modified: ModifiedField]> + Array<[field: string, modified: ModifiedField, handler: ModifierEvent]> >(); } @@ -169,7 +173,11 @@ export class ModifiersDynamicMetadata { * name. Reads return them; `unlock` and `lock` assign them again. */ floating: Record = {}; - fields: Record> = {}; + /** + * The arguments each modifier of a field was unlocked or locked with, by + * field name, then by modifier name. + */ + fields: Record> = {}; constructor(target: any) { this.staticMeta = getMetadata(target.constructor, ModifiersStaticMetadata); @@ -327,9 +335,7 @@ export function fromDatabase( * Converts a table instance to the locked record to store, `_internal` * included and floating values left out. Then runs the `toDatabase` event. */ -export function toDatabase( - object: T, -): Record { +export function toDatabase(object: { constructor: any }): Record { const result = { ...object }; triggerEvent(object, "toDatabase", result); return result; @@ -417,14 +423,12 @@ export function attachModifier< } for (const event of Object.getOwnPropertyNames(Modifier.prototype)) { - if ( - !Object.hasOwn(ignoredEventNames, event) && - isCallable(Modifier.prototype[event]) - ) { + const handler: unknown = Modifier.prototype[event]; + if (!Object.hasOwn(ignoredEventNames, event) && isEventHandler(handler)) { if (!metaTable.events.has(event)) { metaTable.events.set(event, []); } - eventsByKey(metaTable).get(event)?.push([field, entry]); + eventsByKey(metaTable).get(event)?.push([field, entry, handler]); } } } @@ -442,10 +446,10 @@ function fieldsModifiedBy( ); } -export function getModifiedFields< - T extends { constructor: any }, - M extends Constructible, ->(object: T, Modifier: M): (keyof T)[] { +export function getModifiedFields( + object: T, + Modifier: Constructible, +): (keyof T)[] { const metaTable = getMetadata(object.constructor, ModifiersStaticMetadata); return fieldsModifiedBy(metaTable, Modifier); } @@ -561,9 +565,9 @@ export function triggerEvent( const eventEntries = metaTable.events.get(event); if (!eventEntries) return; - for (const [field, { id, modifier, metaRef }] of eventEntries) { - withRef(internal, id, modifier, metaRef, (modifier: Modifier) => - (modifier[event])(object, field, ...args), - ); + for (const [field, { id, modifier, metaRef }, handler] of eventEntries) { + withRef(internal, id, modifier, metaRef, (modifier) => { + handler.call(modifier, object, field, ...args); + }); } } diff --git a/src/modifiers/internal/kinds.ts b/src/modifiers/internal/kinds.ts index 7343405..3424312 100644 --- a/src/modifiers/internal/kinds.ts +++ b/src/modifiers/internal/kinds.ts @@ -1,6 +1,11 @@ /** @internal @packageDocumentation */ import type { Constructible } from "../../common"; -import type { Modifier, OneWayModifier, TwoWayModifier } from "../common"; +import type { + Modifier, + ModifierEvent, + OneWayModifier, + TwoWayModifier, +} from "../common"; export function isWriteOnly(m: Modifier): m is OneWayModifier { return "lock" in m && !("unlock" in m); @@ -28,6 +33,14 @@ export function isCallable(value: unknown): value is CallableFunction { return typeof value === "function"; } +/** + * Whether a modifier prototype member is a method the engine can run for an + * event: any function takes the instance, the field and the event arguments. + */ +export function isEventHandler(value: unknown): value is ModifierEvent { + return typeof value === "function"; +} + export function isString(value: unknown): value is string { return typeof value === "string"; } @@ -48,6 +61,14 @@ export function prototypeOf(target: Constructible): object { return prototype; } +/** Whether `candidate` is the class `base` or a class extending it. */ +export function isSameOrSubclass( + candidate: Constructible, + base: Base, +): candidate is Base { + return candidate === base || base.isPrototypeOf(candidate); +} + export function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } diff --git a/src/modifiers/internal/records.ts b/src/modifiers/internal/records.ts index 486c7c0..e8668e2 100644 --- a/src/modifiers/internal/records.ts +++ b/src/modifiers/internal/records.ts @@ -2,6 +2,7 @@ import type { DatumStaticMetadata } from "../../common"; import type { Modifier, + ModifierEvent, ModifiersDynamicMetadata, ModifiersStaticMetadata, } from "../common"; @@ -58,7 +59,13 @@ export function eventsByKey( meta: ModifiersStaticMetadata, ): Map< string, - Array<[field: PropertyKey, modified: ModifiedFieldList[number]]> + Array< + [ + field: PropertyKey, + modified: ModifiedFieldList[number], + handler: ModifierEvent, + ] + > > { return meta.events; } diff --git a/src/table.ts b/src/table.ts index a3a96ce..5762e8d 100644 --- a/src/table.ts +++ b/src/table.ts @@ -8,11 +8,11 @@ import type { FieldType } from "@antelopejs/interface-database/schema"; import { type Class, type ClassDecorator, - MakeClassDecorator, MakePropertyDecorator, } from "@antelopejs/interface-core/decorators"; import { MixinSymbol, type MixinType } from "./modifiers/common"; +import { isSameOrSubclass, isString } from "./modifiers/internal/kinds"; import { fieldTypesByKey } from "./modifiers/internal/records"; import { type Constructible, DatumStaticMetadata, getMetadata } from "./common"; import { boundSchema } from "./internal/bound-schema"; @@ -210,7 +210,11 @@ export interface IndexOptions { export const Index = MakePropertyDecorator( (target, propertyKey, options?: IndexOptions) => { const metadata = getMetadata(target.constructor, DatumStaticMetadata); - const key = propertyKey; + assert( + isString(propertyKey), + `@Index on ${String(propertyKey)}: a database index names a field, never a symbol`, + ); + const key = propertyKey; metadata.addIndex( key, options?.group || key, @@ -234,11 +238,19 @@ type AwaitableArray = Promise | T | T[]; /** * Database Table class decorator to create default data on table creation. * - * @param generator Data generator + * @param generator Data generator, called with the table class registered + * into the schema: the decorated class or a subclass of it */ -export const Fixture: ( +export function Fixture( generator: (p: T) => AwaitableArray>>, -) => ClassDecorator = MakeClassDecorator((target, generator) => { - const metadata = getMetadata(target, DatumStaticMetadata); - metadata.generator = generator as unknown as DatumStaticMetadata["generator"]; -}); +): ClassDecorator { + return (target: T) => { + getMetadata(target, DatumStaticMetadata).generator = (tableClass) => { + assert( + isSameOrSubclass(tableClass, target), + `${tableClass.name} does not extend ${target.name}, whose fixture it inherited`, + ); + return generator(tableClass); + }; + }; +} diff --git a/src/tests/cross_instance.test.ts b/src/tests/cross_instance.test.ts index 7854f2d..2207858 100644 --- a/src/tests/cross_instance.test.ts +++ b/src/tests/cross_instance.test.ts @@ -114,7 +114,10 @@ describe("Tables and models across interface instances", () => { function stagesOf(query: Database.Table) { return query .build() - .map(({ stage, options }) => [stage, options?.id] as const); + .map( + ({ stage, options }) => + [stage, options && "id" in options ? options.id : undefined] as const, + ); } function BasicDataModelReadsOtherInstanceTest() { @@ -148,7 +151,10 @@ function GetModelFromOtherInstanceTest() { const ItemModel = Decorators.BasicDataModel(Item); const model = Other.GetModel(ItemModel, "t3"); expect(model.database).to.be.instanceOf(Database.SchemaInstance); - expect(model.database.build()[0].options.id).to.equal(boundSchemaId); + expect(model.database.build()[0]).to.deep.include({ + stage: "schema", + options: { id: boundSchemaId }, + }); } function LockFromOtherInstanceTest() { diff --git a/src/tests/database.test.ts b/src/tests/database.test.ts index df7cf0a..3e10171 100644 --- a/src/tests/database.test.ts +++ b/src/tests/database.test.ts @@ -222,8 +222,14 @@ describe("Database - initialization", () => { } const model = GetModel(BasicDataModel(Reloaded), "late-instance"); - expect(model.database.build()[0].options.id).to.equal("test-late-schema"); - expect(Reloaded.table().build()[0].options.id).to.equal("test-late-schema"); + expect(model.database.build()[0]).to.deep.include({ + stage: "schema", + options: { id: "test-late-schema" }, + }); + expect(Reloaded.table().build()[0]).to.deep.include({ + stage: "schema", + options: { id: "test-late-schema" }, + }); }); it("populates TableDefinition.fields from @Field declarations", async () => { @RegisterTable("orders", "test-fields-schema") diff --git a/src/tests/metadata_inheritance.test.ts b/src/tests/metadata_inheritance.test.ts index 0ee642d..652fb07 100644 --- a/src/tests/metadata_inheritance.test.ts +++ b/src/tests/metadata_inheritance.test.ts @@ -103,6 +103,20 @@ describe("Metadata - subclasses of a table with fixtures, modifiers and relation expect(generators[1]).to.not.equal(generators[2]); }); + it("calls an inherited fixture with the subclass registered", async () => { + @Fixture((tableClass) => ({ name: tableClass.name })) + class Base extends Table { + @Field("string") + declare name: string; + } + class Users extends Base {} + + const generator = getMetadata(Users, DatumStaticMetadata).generator; + + expect(await generator?.(Users)).to.deep.equal({ name: "Users" }); + expect(await generator?.(Base)).to.deep.equal({ name: "Base" }); + }); + it("keeps the modified fields of each subclass apart", async () => { class Base extends Table.with(HashModifier) { @Hashed() diff --git a/src/tests/subclass_schema_binding.test.ts b/src/tests/subclass_schema_binding.test.ts index bbaa0bd..311e158 100644 --- a/src/tests/subclass_schema_binding.test.ts +++ b/src/tests/subclass_schema_binding.test.ts @@ -20,12 +20,14 @@ describe("Schema binding - subclasses of a table", () => { await RegisterSchema("binding-before-schema"); const model = GetModel(AdminModel, "binding-instance"); - expect(model.database.build()[0].options.id).to.equal( - "binding-before-schema", - ); - expect(Admin.table().build()[0].options.id).to.equal( - "binding-before-schema", - ); + expect(model.database.build()[0]).to.deep.include({ + stage: "schema", + options: { id: "binding-before-schema" }, + }); + expect(Admin.table().build()[0]).to.deep.include({ + stage: "schema", + options: { id: "binding-before-schema" }, + }); }); it("binds a subclass declared after RegisterSchema ran", async () => { @@ -38,12 +40,14 @@ describe("Schema binding - subclasses of a table", () => { class Admin extends Users {} const model = GetModel(BasicDataModel(Admin), "binding-instance"); - expect(model.database.build()[0].options.id).to.equal( - "binding-after-schema", - ); - expect(Admin.table().build()[0].options.id).to.equal( - "binding-after-schema", - ); + expect(model.database.build()[0]).to.deep.include({ + stage: "schema", + options: { id: "binding-after-schema" }, + }); + expect(Admin.table().build()[0]).to.deep.include({ + stage: "schema", + options: { id: "binding-after-schema" }, + }); }); it("binds a subclass that registers its own table to its own schema", async () => { @@ -58,12 +62,14 @@ describe("Schema binding - subclasses of a table", () => { await RegisterSchema("binding-parent-schema"); await RegisterSchema("binding-child-schema"); - expect(Parent.table().build()[0].options.id).to.equal( - "binding-parent-schema", - ); - expect(Child.table().build()[0].options.id).to.equal( - "binding-child-schema", - ); + expect(Parent.table().build()[0]).to.deep.include({ + stage: "schema", + options: { id: "binding-parent-schema" }, + }); + expect(Child.table().build()[0]).to.deep.include({ + stage: "schema", + options: { id: "binding-child-schema" }, + }); }); it("follows the schema its parent is bound to when RegisterSchema runs again", async () => { diff --git a/src/tests/table.test.ts b/src/tests/table.test.ts index 7277bfb..d43a614 100644 --- a/src/tests/table.test.ts +++ b/src/tests/table.test.ts @@ -1,3 +1,4 @@ +import { AssertionError } from "node:assert"; import { expect } from "chai"; import { MixinSymbol } from "../modifiers/common"; import { Field, Fixture, Index, Table } from "../table"; @@ -50,6 +51,18 @@ describe("Table - decorators", () => { expect(metadata.indexes.user_search).to.deep.equal(["name", "email"]); expect(metadata.indexes.age_range).to.deep.equal(["age"]); }); + it("refuses an index on a symbol-keyed field", () => { + const secret = Symbol("secret"); + const declare = () => { + class TestTable extends Table { + @Index() + [secret]!: string; + } + return TestTable; + }; + + expect(declare).to.throw(AssertionError); + }); it("creates table with fixture data", async () => { const testData = [ { id: "1", name: "Test User 1" }, From a9d2f49fadd404ab22fd1a176af38f2bf70472ec Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sat, 31 Oct 2026 17:01:46 +0100 Subject: [PATCH 09/45] chore(ratchet): lower ceilings to the measured counts (#64) --- guardrails.ratchet.json | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/guardrails.ratchet.json b/guardrails.ratchet.json index 59341f6..22cb134 100644 --- a/guardrails.ratchet.json +++ b/guardrails.ratchet.json @@ -1,14 +1,13 @@ { "pack": "0.2.17", "rules": { - "anti-slop/no-chained-type-assertions": 1, "anti-slop/no-known-value-widening": 2, "anti-slop/no-unknown-parameters": 8, "anti-slop/no-unknown-returns": 1, - "anti-slop/no-unsafe-dictionary-type": 18, - "anti-slop/require-safety-comment-for-type-assertion": 14, - "typescript/no-unnecessary-type-parameters": 7, - "typescript/no-unsafe-argument": 11, + "anti-slop/no-unsafe-dictionary-type": 14, + "anti-slop/require-safety-comment-for-type-assertion": 7, + "typescript/no-unnecessary-type-parameters": 5, + "typescript/no-unsafe-argument": 5, "typescript/no-unsafe-assignment": 10, "typescript/no-unsafe-return": 11 }, From 0bbffc88f6de645aa2068cb95b0f98ffe1ea1b0f Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sat, 31 Oct 2026 20:59:45 +0100 Subject: [PATCH 10/45] refactor(encryption): read the value date_encoder encodes through one unknown local (#66) --- src/modifiers/encryption.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/modifiers/encryption.ts b/src/modifiers/encryption.ts index 705d911..4dc0568 100644 --- a/src/modifiers/encryption.ts +++ b/src/modifiers/encryption.ts @@ -25,12 +25,13 @@ type Meta = Record; type LockedType = [ciphertext: string, iv: string, authTag?: string]; function date_encoder(this: any, key: string) { - return this[key] instanceof Date + const held: unknown = this[key]; + return held instanceof Date ? { - value: this[key].toUTCString(), + value: held.toUTCString(), type: "Date", } - : this[key]; + : held; } type StoredValue = From fcb0a29ca762b37a24efefc889f9f9cff992728c Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sat, 31 Oct 2026 20:59:50 +0100 Subject: [PATCH 11/45] feat(database-decorators)!: name the row forms the conversions read and give (#67) * feat(database-decorators)!: name the row forms the conversions read and give A table row has three forms: the instance, the plain form and the stored form. The conversions typed the last two as `any` or open dictionaries, which hid what every caller already relies on. They now name them: - `PlainData` is what `fromPlainData` reads: every field optional and possibly partial, a field under a container modifier possibly its value per key, the plain form 40 consumer calls already write. - `PlainRow` is what `toPlainData` gives: the set fields of `T` by name. - `StoredRow` is what `toDatabase` gives: typed `T` as `Table` reads it, with `_internal` declared. - `fromDatabase` takes the row as `Table` types it, and gives it back with the class's prototype. - `update(obj)` without an id takes `_id` in its type, as the docs said. - `DatumStaticMetadata.primary` is the literal `"_id"`: nothing writes it. - `unlockrequest` reads the stored form through `ValueProxy#cast` instead of an `any`; its signature keeps its answers. `_internal.data`, `floating` and the modifier state stop being `any`: what they hold is known to the modifiers that wrote them, so they read as `unknown`. BREAKING CHANGE: `fromDatabase`, `fromPlainData`, `toDatabase`, `toPlainData` and the matching `BasicDataModel` statics no longer accept or return `any`. `triggerEvent` loses a type parameter nothing reads. `primary` no longer takes another string. * feat(database-decorators)!: keep reads of a stored row unchecked `fromDatabase` and the model's static take what the database answered, as before. A typed parameter measured +2 `no-unsafe-*` rows in interface-data-api and three compile breaks, and it cleared nothing here: the two rows that stay are the driver boundary, and an `as` in their place would only trade one finding for another. `triggerEvent` keeps its type parameter for the same reason: without it, that `any` copy is a new `no-unsafe-argument`. `StoredRow` is no longer `T`: a stored row has no prototype, so typing it as the class made every spread of it a `no-misused-spread`. It now reads as the set fields. The model's own writes keep the `T` they build. * fix(modifiers): keep assigning a floating value by a plain assignment's rules * chore(suppressions): lower the SAFETY budget to the comments left --- docs/4.data-models.md | 6 +- guardrails.suppressions.json | 2 +- skills/database-decorators-interface/SKILL.md | 3 +- src/common.ts | 20 ++- src/database.ts | 12 +- src/model.ts | 44 ++++--- src/modifiers/common.ts | 114 ++++++++++-------- src/modifiers/internal/records.ts | 14 ++- src/tests/integration.test.ts | 5 + src/tests/row_forms.test.ts | 59 +++++++++ 10 files changed, 199 insertions(+), 80 deletions(-) create mode 100644 src/tests/row_forms.test.ts diff --git a/docs/4.data-models.md b/docs/4.data-models.md index 7525d1b..3276ce0 100644 --- a/docs/4.data-models.md +++ b/docs/4.data-models.md @@ -88,7 +88,7 @@ await userModel.update("user-123", { name: "Updated Name" }); await userModel.update({ _id: "user-123", name: "Updated Name" }); ``` -> **Important:** When calling `update` without a separate ID argument, the object must include the primary key field. +> **Important:** When calling `update` without a separate ID argument, the object must include the primary key field. Its type requires `_id`. ### Delete @@ -122,6 +122,8 @@ const user = UserModel.fromPlainData({ }); ``` +The object is a `PlainData`: every field is optional and may be partial, and a field under a container modifier may hold its value per key, as a localized field holds one text per locale. `toPlainData` gives the reverse form, a `PlainRow`. + ### `fromDatabase(obj)` Convert a raw database record into a Table class instance. This attaches the class prototype and triggers the `fromDatabase` modifier event. @@ -132,7 +134,7 @@ const user = UserModel.fromDatabase(rawDbRecord); ### `toDatabase(obj)` -Convert a Table class instance into a plain object suitable for database storage. Modifier events like `toDatabase` are triggered. +Convert a Table class instance into a plain object suitable for database storage. Modifier events like `toDatabase` are triggered. The result is a `StoredRow`: the set fields, with no prototype, and its modified fields locked in `_internal` instead of at the top level. ```typescript fragment const dbReady = UserModel.toDatabase(userInstance); diff --git a/guardrails.suppressions.json b/guardrails.suppressions.json index d45133d..47b5e35 100644 --- a/guardrails.suppressions.json +++ b/guardrails.suppressions.json @@ -2,6 +2,6 @@ "rules": {}, "tsExpectError": 0, "tsIgnore": 0, - "safety": 5, + "safety": 4, "config": {} } diff --git a/skills/database-decorators-interface/SKILL.md b/skills/database-decorators-interface/SKILL.md index 164cd92..451d648 100644 --- a/skills/database-decorators-interface/SKILL.md +++ b/skills/database-decorators-interface/SKILL.md @@ -132,7 +132,8 @@ class Chef extends Table.with( - `Model.validate` (and `{ validate: true }` on insert/update) only checks fields whose `@Field` type is an io-ts-style codec with `.decode`; plain string field tokens are skipped. Update validates with `{ partial: true }` semantics. -- `update(obj)` without an explicit id asserts that the object carries the primary key (`_id` by default). +- `update(obj)` without an explicit id takes an object typed with the primary key `_id`, and asserts at run time that it carries it. +- A row has three forms. `fromPlainData` reads a `PlainData` (every field optional, a localized field possibly one text per locale), `toPlainData` gives a `PlainRow`, and `toDatabase` gives a `StoredRow`, its modified fields locked in `_internal`. `fromDatabase` takes the row the database answered, unchecked. - Indexes are scoped to the schema instance. Add `@Index({ crossInstance: true })` only for indexes also queried across all instances (`CROSS_INSTANCE`); it never changes results, only speed. On a group, one member setting it applies it to the whole group. diff --git a/src/common.ts b/src/common.ts index c3b5fec..5645b23 100644 --- a/src/common.ts +++ b/src/common.ts @@ -17,6 +17,24 @@ export type DeepPartial = { : T[K]; }; +/** + * A table row as `toPlainData` gives it: the fields of `T` that are set, by + * name, without the row's prototype. + */ +export type PlainRow = { [K in Extract]?: T[K] }; + +/** + * A table row as `fromPlainData` reads it: every field is optional and may be + * partial, and a field under a container modifier, such as a localized one, may + * hold its value per key. + */ +export type PlainData = { + [K in Extract]?: + | T[K] + | DeepPartial[K] + | Record[K]>; +}; + /** * The rows a fixture generator gives for its table: one, several or none. */ @@ -99,7 +117,7 @@ export class DatumStaticMetadata { public readonly crossInstanceIndexes = new Set(); public readonly uniqueIndexes = new Set(); public readonly fields: Record = {}; - public primary: string = "_id"; + public primary = "_id" as const; public generator?: DatumGenerator; /** * Schema the table was registered into by `RegisterSchema`. diff --git a/src/database.ts b/src/database.ts index b94c0c5..44c4c5a 100644 --- a/src/database.ts +++ b/src/database.ts @@ -7,10 +7,14 @@ import type { Table } from "./table"; import { getTablesForSchema } from "./schema"; import type { DatumGeneratorOutput } from "./common"; import { DatumStaticMetadata, getMetadata } from "./common"; -import { fromPlainData, toDatabase, triggerEvent } from "./modifiers/common"; +import { + fromPlainData, + type StoredRow, + toDatabase, + triggerEvent, +} from "./modifiers/common"; type TableDefinitions = Record>; -type TableEntry = ReturnType; /** * Registers a schema with the database adapter and runs any fixtures. @@ -107,7 +111,7 @@ async function insertFixtureData( function toFixtureRows( fixtureData: DatumGeneratorOutput, tableClass: Class, -): TableEntry[] { +): StoredRow
[] { const entries = fixtureData ? Array.isArray(fixtureData) ? fixtureData @@ -120,6 +124,6 @@ function toFixtureRows( }); } -function isTableEntry(value: unknown): value is TableEntry { +function isTableEntry(value: unknown): value is object { return typeof value === "object" && value !== null; } diff --git a/src/model.ts b/src/model.ts index e417762..d870661 100644 --- a/src/model.ts +++ b/src/model.ts @@ -14,6 +14,7 @@ import { import { fromDatabase, fromPlainData, + type StoredRow, toDatabase, triggerEvent, } from "./modifiers/common"; @@ -24,6 +25,7 @@ import { DatumStaticMetadata, type DeepPartial, getMetadata, + type PlainData, } from "./common"; export type DataModel = { @@ -38,7 +40,7 @@ export type DataModel = { readonly table: DatabaseDev.Table; }; fromDatabase(obj: any): T | undefined; - fromPlainData(obj: any): T; + fromPlainData(obj: PlainData | T): T; }; export type FieldError = { field: string; message: string }; @@ -123,7 +125,7 @@ export function BasicDataModel( * @param obj Plain data object containing the table fields * @returns Table class instance */ - public static fromPlainData(obj: any) { + public static fromPlainData(obj: PlainData | T): T { return obj instanceof dataType ? obj : fromPlainData(obj, dataType); } @@ -143,7 +145,7 @@ export function BasicDataModel( * @param obj Table class instance * @returns Database-ready data */ - public static toDatabase(obj: any) { + public static toDatabase(obj: PlainData | T): StoredRow { const instance = obj instanceof dataType ? obj : Model.fromPlainData(obj); return toDatabase(instance); } @@ -232,10 +234,10 @@ export function BasicDataModel( } if (allErrors.length > 0) throw makeValidationError(allErrors); } - const converter = (entry: any) => { + const converter = (entry: DeepPartial) => { const instance = Model.fromPlainData(entry); triggerEvent(instance, "insert"); - return Model.toDatabase(instance); + return toDatabase(instance); }; return this.table .insert(Array.isArray(obj) ? obj.map(converter) : converter(obj)) @@ -256,7 +258,7 @@ export function BasicDataModel( options?: ValidateOptions, ): Promise; public update( - obj: DeepPartial, + obj: DeepPartial & KeyedRow, options?: ValidateOptions, ): Promise; public async update(...args: UpdateArgs) { @@ -268,17 +270,13 @@ export function BasicDataModel( if (parsed.id !== undefined) { const instance = Model.fromPlainData(parsed.data); triggerEvent(instance, "update"); - return this.table - .get(parsed.id) - .update(Model.toDatabase(instance)) - .run(); + return this.table.get(parsed.id).update(toDatabase(instance)).run(); } const instance = Model.fromPlainData(parsed.data); triggerEvent(instance, "update"); - // SAFETY: parsed.data is the DeepPartial object the caller passed as the first argument (the id form was handled above), so reading the property named by the table's primary key is defined and gives undefined when absent; the next line asserts it is present. - const id = (parsed.data as any)[primaryKey]; + const id = parsed.data[primaryKey]; assert(id !== undefined, "Missing primary key in object update."); - return this.table.get(id).update(Model.toDatabase(instance)).run(); + return this.table.get(id).update(toDatabase(instance)).run(); } /** @@ -308,11 +306,16 @@ function cacheKeyOf(instanceId: InstanceId | undefined): string | symbol { export type ValidateOptions = { validate?: boolean }; -type ParsedUpdate = { - id?: string; - data: DeepPartial; - options?: ValidateOptions; -}; +/** The part of a row an update without an id finds the row by. */ +type KeyedRow = Record; + +type ParsedUpdate = + | { id: string; data: DeepPartial; options?: ValidateOptions } + | { + id?: undefined; + data: DeepPartial & KeyedRow; + options?: ValidateOptions; + }; function makeValidationError(errors: FieldError[]): Error { const summary = errors.map((e) => `${e.field} (${e.message})`).join(", "); @@ -324,7 +327,10 @@ type IdUpdateArgs = [ data: DeepPartial, options?: ValidateOptions, ]; -type ObjectUpdateArgs = [data: DeepPartial, options?: ValidateOptions]; +type ObjectUpdateArgs = [ + data: DeepPartial & KeyedRow, + options?: ValidateOptions, +]; type UpdateArgs = IdUpdateArgs | ObjectUpdateArgs; function isIdUpdate(args: UpdateArgs): args is IdUpdateArgs { diff --git a/src/modifiers/common.ts b/src/modifiers/common.ts index d93cebc..17d047c 100644 --- a/src/modifiers/common.ts +++ b/src/modifiers/common.ts @@ -1,6 +1,11 @@ import type * as DatabaseDev from "@antelopejs/interface-database"; -import { type Constructible, getMetadata } from "../common"; +import { + type Constructible, + getMetadata, + type PlainData, + type PlainRow, +} from "../common"; import { ignoredEventNames, isEventHandler, @@ -20,6 +25,8 @@ import { floatingOf, getInternal, type InternalHolder, + type InternalRecord, + type ModifierState, unlockArgsOf, withRef, } from "./internal/records"; @@ -119,10 +126,12 @@ export class ContainerModifier< } } -interface InternalType { - meta: Record; - data: Record; -} +/** + * A table row as the database stores it: the fields of `T` that are set, with + * no prototype, and its modified fields locked in `_internal` instead of at the + * top level. + */ +export type StoredRow = Partial & InternalHolder; /** * A modifier method run for an event of the table instance (`insert`, @@ -138,7 +147,7 @@ export type ModifierEvent = ( interface ModifiedField { id: string; modifier: Modifier; - metaRef: { _ref: any }; + metaRef: { _ref: ModifierState | undefined }; } export class ModifiersStaticMetadata { @@ -172,7 +181,7 @@ export class ModifiersDynamicMetadata { * Values assigned to a field before its modifiers could lock them, by field * name. Reads return them; `unlock` and `lock` assign them again. */ - floating: Record = {}; + floating: Record = {}; /** * The arguments each modifier of a field was unlocked or locked with, by * field name, then by modifier name. @@ -204,7 +213,7 @@ export class ModifiersDynamicMetadata { ); } - get(field: PropertyKey, internal: InternalType) { + get(field: PropertyKey, internal: InternalRecord) { if (Object.hasOwn(this.floating, field)) { return floatingByKey(this)[field]; } @@ -239,7 +248,7 @@ export class ModifiersDynamicMetadata { ); } - set(field: PropertyKey, internal: InternalType, value: unknown) { + set(field: PropertyKey, internal: InternalRecord, value: unknown) { if (!this.canSet(field)) { floatingByKey(this)[field] = value; return; @@ -249,7 +258,7 @@ export class ModifiersDynamicMetadata { const prevlist: { id: string; modifier: Modifier; - metaRef: { _ref: any }; + metaRef: ModifiedField["metaRef"]; prev: unknown; }[] = []; let prev = dataByKey(internal)[field]; @@ -282,21 +291,25 @@ export class ModifiersDynamicMetadata { } } +/** The names of the modified fields of a table instance. */ +function fieldNamesOf(object: T) { + const metaTable = getMetadata(object.constructor, ModifiersStaticMetadata); + return Object.keys(metaTable.fields) as Array; +} + /** * Builds a table instance from unlocked plain data: each key is assigned, so * modifiers lock the fields they can. Then runs the `fromPlainData` event. */ -export function fromPlainData( - rawObject: Record, - TableClass: T, -): InstanceType { - const shallowCopy: typeof rawObject = {}; - Object.setPrototypeOf(shallowCopy, prototypeOf(TableClass)); - for (const [key, val] of Object.entries(rawObject)) { - shallowCopy[key] = val; - } - triggerEvent(shallowCopy, "fromPlainData", rawObject); - return shallowCopy as InstanceType; +export function fromPlainData( + rawObject: PlainData, + TableClass: Constructible, +): T { + const instance = {}; + Object.setPrototypeOf(instance, prototypeOf(TableClass)); + Object.assign(instance, Object.fromEntries(Object.entries(rawObject))); + triggerEvent(instance, "fromPlainData", rawObject); + return instance as T; } /** @@ -305,13 +318,11 @@ export function fromPlainData( */ export function toPlainData( object: T, -): Record { - const metaTable = getMetadata(object.constructor, ModifiersStaticMetadata); +): PlainRow { const result = { ...object }; if ("_internal" in result) delete result._internal; - for (const field of Object.keys(metaTable.fields)) { - const key = field; - result[key] = object[key]; + for (const field of fieldNamesOf(object)) { + result[field] = object[field]; } triggerEvent(object, "toPlainData", result); return result; @@ -335,7 +346,9 @@ export function fromDatabase( * Converts a table instance to the locked record to store, `_internal` * included and floating values left out. Then runs the `toDatabase` event. */ -export function toDatabase(object: { constructor: any }): Record { +export function toDatabase( + object: T, +): T & InternalHolder { const result = { ...object }; triggerEvent(object, "toDatabase", result); return result; @@ -433,16 +446,14 @@ export function attachModifier< } } -function fieldsModifiedBy( - metaTable: ModifiersStaticMetadata, +function fieldsModifiedBy( + object: T, modifierClass: Constructible, fields?: Array, ): Array { - return (fields ?? (Object.keys(metaTable.fields) as Array)).filter( - (field) => - fieldsByKey(metaTable)[field]?.find( - ({ id }) => id === modifierClass.name, - ), + const metaTable = getMetadata(object.constructor, ModifiersStaticMetadata); + return (fields ?? fieldNamesOf(object)).filter((field) => + fieldsByKey(metaTable)[field]?.find(({ id }) => id === modifierClass.name), ); } @@ -450,8 +461,7 @@ export function getModifiedFields( object: T, Modifier: Constructible, ): (keyof T)[] { - const metaTable = getMetadata(object.constructor, ModifiersStaticMetadata); - return fieldsModifiedBy(metaTable, Modifier); + return fieldsModifiedBy(object, Modifier); } export function unlock< @@ -463,21 +473,21 @@ export function unlock< fields: Array | undefined, ...args: ExtractModifierArgs> ) { - const metaTable = getMetadata(object.constructor, ModifiersStaticMetadata); - const usedFields = fieldsModifiedBy(metaTable, Modifier, fields); + const usedFields = fieldsModifiedBy(object, Modifier, fields); const meta = getMetadata(object, ModifiersDynamicMetadata); for (const field of usedFields) { unlockArgsOf(meta, field)[Modifier.name] = args; if (field in meta.floating) { - object[field] = floatingOf(meta, field); + Object.assign(object, { [field]: floatingOf(meta, field) }); } } } type ModifierWithProxyArgs = { modifier: Constructible>; - args: DatabaseDev.ValueProxyOrValue[]; + args: unknown[]; }; + export function unlockrequest( table: Constructible, object: DatabaseDev.ValueProxy, @@ -488,18 +498,21 @@ export function unlockrequest( modifiers.map(({ modifier, args: modArgs }) => [modifier.name, modArgs]), ); - const stored = object; + const stored = object.cast<{ _internal: InternalRecord }>(); const meta = stored.key("_internal").key("meta"); const metaTable = getMetadata(table, ModifiersStaticMetadata); - return ( - fieldsByKey(metaTable)[field]?.reduce((data, { id, modifier }) => { - if (isWriteOnly(modifier) || !isTwoWay(modifier)) { - return data; - } - data = modifier.unlockrequest(data, meta.key(id), ...args[id]); - return data; - }, stored.key("_internal").key("data").key(field)) || stored.key(field) + const unlocked = fieldsByKey(metaTable)[field]?.reduce< + DatabaseDev.ValueProxy + >( + (data, { id, modifier }) => + isWriteOnly(modifier) || !isTwoWay(modifier) + ? data + : modifier.unlockrequest(data, meta.key(id), ...args[id]), + stored.key("_internal").key("data").key(field), ); + return (unlocked ?? object.cast().key(field)).cast< + T[K] + >(); } export function lock< @@ -511,8 +524,7 @@ export function lock< fields: Array | undefined, ...args: ExtractModifierArgs> | [] ) { - const metaTable = getMetadata(object.constructor, ModifiersStaticMetadata); - const usedFields = fieldsModifiedBy(metaTable, Modifier, fields); + const usedFields = fieldsModifiedBy(object, Modifier, fields); const meta = getMetadata(object, ModifiersDynamicMetadata); for (const field of usedFields) { const val = object[field]; diff --git a/src/modifiers/internal/records.ts b/src/modifiers/internal/records.ts index e8668e2..293fcfa 100644 --- a/src/modifiers/internal/records.ts +++ b/src/modifiers/internal/records.ts @@ -9,7 +9,19 @@ import type { type ModifiedFieldList = ModifiersStaticMetadata["fields"][string]; type ModifiedField = ModifiedFieldList[number]; -type InternalRecord = Parameters[1]; + +/** What a modifier keeps for a field of a stored row, beside the locked value. */ +export type ModifierState = { [key: PropertyKey]: unknown }; + +/** + * Where a row holds its modified fields: the locked values by field name, and + * each modifier's state by modifier name. What the values hold is known to the + * modifiers that wrote them, not to the row. + */ +export interface InternalRecord { + meta: Record; + data: { [field: PropertyKey]: unknown }; +} export interface InternalHolder { constructor: any; diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts index d6f53f2..fb8c512 100644 --- a/src/tests/integration.test.ts +++ b/src/tests/integration.test.ts @@ -207,6 +207,11 @@ describe("Integration - real database operations", () => { ) .run(); expect(frenchTitles).to.deep.equal(["Only English"]); + const ids = await LocalizedContent.table(schemaInstance) + .filter((row) => row.key("_id").eq("content-untranslated")) + .map((row) => unlockrequest(LocalizedContent, row, "_id", [])) + .run(); + expect(ids).to.deep.equal(["content-untranslated"]); }); it("manages encrypted sensitive data", async () => { @RegisterTable("sensitive_data", "integration-encrypt-schema") diff --git a/src/tests/row_forms.test.ts b/src/tests/row_forms.test.ts new file mode 100644 index 0000000..6cdde9c --- /dev/null +++ b/src/tests/row_forms.test.ts @@ -0,0 +1,59 @@ +import { expect } from "chai"; + +import { BasicDataModel } from "../model"; +import { LocalizationModifier, Localized } from "../modifiers/localization"; +import { fromDatabase, toDatabase, toPlainData } from "../modifiers/common"; +import { Field, Table } from "../table"; + +class Article extends Table.with(LocalizationModifier) { + @Localized({ fallbackLocale: "en" }) + declare title: string; + + @Field("number") + declare views: number; +} + +const ArticleModel = BasicDataModel(Article, "articles"); + +describe("Row forms", () => { + it("reads a localized field given per locale", () => { + const article = ArticleModel.fromPlainData({ + title: { en: "Hello", fr: "Salut" }, + }).localize("*"); + + expect(article.localize("fr").title).to.equal("Salut"); + expect(article.localize("en").title).to.equal("Hello"); + }); + + it("locks a value given in the plain form once its locale is known", () => { + const article = ArticleModel.fromPlainData({ + title: "Bonjour", + views: 2, + }).localize("fr"); + + expect( + ArticleModel.toDatabase(article)._internal?.data.title, + ).to.deep.equal({ fr: "Bonjour" }); + }); + + it("leaves the locked fields out of the plain form", () => { + const article = ArticleModel.fromPlainData({ title: "Hi" }).localize("en"); + + const plain = toPlainData(article); + + expect(plain).to.deep.equal({ title: "Hi" }); + }); + + it("gives back a stored row as an instance of its table", () => { + const written = ArticleModel.fromPlainData({ + title: "Hi", + views: 3, + }).localize("en"); + + const read = fromDatabase(toDatabase(written), Article); + + expect(read).to.be.instanceOf(Article); + expect(read.views).to.equal(3); + expect(read.localize("en").title).to.equal("Hi"); + }); +}); From aabd36b4060d35f6dc71e853fde3006e640e3ccd Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sun, 1 Nov 2026 03:00:30 +0100 Subject: [PATCH 12/45] chore(ratchet): lower ceilings to measured counts (#69) --- guardrails.ratchet.json | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/guardrails.ratchet.json b/guardrails.ratchet.json index 22cb134..24f9314 100644 --- a/guardrails.ratchet.json +++ b/guardrails.ratchet.json @@ -1,15 +1,13 @@ { "pack": "0.2.17", "rules": { - "anti-slop/no-known-value-widening": 2, "anti-slop/no-unknown-parameters": 8, "anti-slop/no-unknown-returns": 1, - "anti-slop/no-unsafe-dictionary-type": 14, - "anti-slop/require-safety-comment-for-type-assertion": 7, - "typescript/no-unnecessary-type-parameters": 5, - "typescript/no-unsafe-argument": 5, - "typescript/no-unsafe-assignment": 10, - "typescript/no-unsafe-return": 11 + "anti-slop/no-unsafe-dictionary-type": 11, + "anti-slop/require-safety-comment-for-type-assertion": 5, + "typescript/no-unnecessary-type-parameters": 4, + "typescript/no-unsafe-assignment": 2, + "typescript/no-unsafe-return": 3 }, "oversizeFiles": {} } From e79a62bb5227aa1f92db46d31508d3d1ae92b99a Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Sun, 1 Nov 2026 09:27:15 +0100 Subject: [PATCH 13/45] refactor(database-decorators): read the unlockrequest key as text for typed query operands (#70) * build(deps): pin interface-database to the query value tarball * feat(database-decorators)!: take a text field in unlockrequest `unlockrequest` read the proxy of a field by `keyof T`, which includes numbers and symbols. A query operand is a name or a number held in a query value, never a symbol, so the field is a text key. BREAKING CHANGE: `unlockrequest` takes `K extends keyof T & string`. * chore(ratchet): leave the ceilings to the ceiling pass * build(deps): pin interface-database to the structural operand tarball * build(deps): pin interface-database to the IsQueryValue tarball * build(deps): pin interface-database to the type-only tarball * fix(database-decorators): keep unlockrequest's published key constraint Narrow the key to text inside unlockrequest instead of tightening K, so interface-data-api's Validation.UnlockRequest keeps compiling. --- .campaign/tgz/SHA256SUMS | 2 +- .../antelopejs-interface-database-6bef278.tgz | Bin 117250 -> 0 bytes .../antelopejs-interface-database-724ff72.tgz | Bin 0 -> 120608 bytes README.md | 2 +- package.json | 4 ++-- pnpm-lock.yaml | 12 ++++++------ pnpm-workspace.yaml | 2 +- src/modifiers/common.ts | 5 +++-- src/tests/integration.test.ts | 13 +++++++++++-- 9 files changed, 25 insertions(+), 15 deletions(-) delete mode 100644 .campaign/tgz/antelopejs-interface-database-6bef278.tgz create mode 100644 .campaign/tgz/antelopejs-interface-database-724ff72.tgz diff --git a/.campaign/tgz/SHA256SUMS b/.campaign/tgz/SHA256SUMS index 5d86e53..c6cc116 100644 --- a/.campaign/tgz/SHA256SUMS +++ b/.campaign/tgz/SHA256SUMS @@ -1 +1 @@ -b622e0659f1b0cd8a1a6455061b85e6e7e5a2a6fdbbab1add9cc576b65e7bda5 antelopejs-interface-database-6bef278.tgz +ccdc833729f50825d3567047a22bf9c68f47a38ca3527117756fbd80f380fabd antelopejs-interface-database-724ff72.tgz diff --git a/.campaign/tgz/antelopejs-interface-database-6bef278.tgz b/.campaign/tgz/antelopejs-interface-database-6bef278.tgz deleted file mode 100644 index 560d7eefbdd96cfc0d28745ce6ed25e44e48de12..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 117250 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 diff --git a/.campaign/tgz/antelopejs-interface-database-724ff72.tgz b/.campaign/tgz/antelopejs-interface-database-724ff72.tgz new file mode 100644 index 0000000000000000000000000000000000000000..48dfe0ad0ab34a265f5db31017344a4534d30c7f GIT binary patch literal 120608 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⪙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

kg5`R!_HN5 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 N{U4)9LVf^73;;^l2nqlI literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 5bfdd31..5e9d1c3 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ The package declares these `@antelopejs/*` peer dependencies: | -------------------------------- | ----------------- | -------------------------------------------------------------- | | `@antelopejs/interface-api` | `>=0.0.15 <1.0.0` | `0.0.15` | | `@antelopejs/interface-core` | `>=0.2.4 <1.0.0` | `0.2.4` | -| `@antelopejs/interface-database` | `>=0.1.11 <1.0.0` | `file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz` | +| `@antelopejs/interface-database` | `>=0.1.11 <1.0.0` | `file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz` | diff --git a/package.json b/package.json index c0de0b2..8d4f76f 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ "@antelopejs/core": "^1.16.0", "@antelopejs/interface-api": "^0.0.15", "@antelopejs/interface-core": "^0.2.4", - "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz", + "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz", "@antelopejs/tooling-configs": "^0.2.17", "@types/chai": "^4.3.20", "@types/mocha": "^10.0.10", @@ -175,7 +175,7 @@ "packageManager": "pnpm@10.33.0", "pnpm": { "overrides": { - "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz" + "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz" } }, "antelopeJs": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04bd286..58481a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,15 +5,15 @@ settings: excludeLinksFromLockfile: false overrides: - '@antelopejs/interface-database': file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz + '@antelopejs/interface-database': file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz importers: .: dependencies: '@antelopejs/interface-database': - specifier: file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz - version: file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz(@antelopejs/interface-core@0.2.4) + specifier: file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz + version: file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@0.2.4) randomstring: specifier: ^1.3.1 version: 1.3.1 @@ -100,8 +100,8 @@ packages: '@antelopejs/interface-core@0.2.4': resolution: {integrity: sha512-2qF6/KwahCQvvNuC/piUvPWD4wq2Npmco941HMg2/bMBoJKbhMH16WcXXRvPYznNSkU0U+6vqK6KE+2N5NojSA==} - '@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz': - resolution: {integrity: sha512-iFR2hP69eGn4xlxYiOXOhbuvirLgXQ25LJk0/sP84VJ4xPgxfrwX6YEfrVbhvpUjWLlnWXly+om5qKgU1CQZ6Q==, tarball: file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz} + '@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz': + resolution: {integrity: sha512-rn+0+nTMBje0Wo4Ysg5/HGm/gqreLSyMkuXrsKF+2kAxrNx+5WEzmJHqy3CwJ1SOZr65lb3Yhb/M3CrmXCVYqQ==, tarball: file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz} version: 0.3.0 peerDependencies: '@antelopejs/interface-core': '>=0.2.4 <1.0.0' @@ -2812,7 +2812,7 @@ snapshots: dependencies: reflect-metadata: 0.2.2 - '@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz(@antelopejs/interface-core@0.2.4)': + '@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@0.2.4)': dependencies: '@antelopejs/interface-core': 0.2.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4d6f254..68c37ad 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,2 @@ overrides: - "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-6bef278.tgz" + "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz" diff --git a/src/modifiers/common.ts b/src/modifiers/common.ts index 17d047c..7fc51b2 100644 --- a/src/modifiers/common.ts +++ b/src/modifiers/common.ts @@ -501,6 +501,7 @@ export function unlockrequest( const stored = object.cast<{ _internal: InternalRecord }>(); const meta = stored.key("_internal").key("meta"); const metaTable = getMetadata(table, ModifiersStaticMetadata); + const name = String(field); const unlocked = fieldsByKey(metaTable)[field]?.reduce< DatabaseDev.ValueProxy >( @@ -508,9 +509,9 @@ export function unlockrequest( isWriteOnly(modifier) || !isTwoWay(modifier) ? data : modifier.unlockrequest(data, meta.key(id), ...args[id]), - stored.key("_internal").key("data").key(field), + stored.key("_internal").key("data").key(name), ); - return (unlocked ?? object.cast().key(field)).cast< + return (unlocked ?? object.cast().key(name)).cast< T[K] >(); } diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts index fb8c512..a1c566d 100644 --- a/src/tests/integration.test.ts +++ b/src/tests/integration.test.ts @@ -1,5 +1,6 @@ import { expect } from "chai"; -import { Schema } from "@antelopejs/interface-database"; +import { Schema, type ValueProxy } from "@antelopejs/interface-database"; +import type { Constructible } from "../common"; import { RegisterTable } from "../schema"; import { RegisterSchema } from "../database"; import { BasicDataModel, GetModel } from "../model"; @@ -11,6 +12,14 @@ import { unlockrequest } from "../modifiers/common"; import { asFieldType, numberCodec } from "./codec_helpers"; +function unlockAny( + table: Constructible, + row: ValueProxy, + field: K, +) { + return unlockrequest(table, row, field, []); +} + function getDatabase(schemaId: string) { const instance = Schema.get(schemaId)?.instance(); if (!instance) throw new Error(`Database not found: ${schemaId}`); @@ -209,7 +218,7 @@ describe("Integration - real database operations", () => { expect(frenchTitles).to.deep.equal(["Only English"]); const ids = await LocalizedContent.table(schemaInstance) .filter((row) => row.key("_id").eq("content-untranslated")) - .map((row) => unlockrequest(LocalizedContent, row, "_id", [])) + .map((row) => unlockAny(LocalizedContent, row, "_id")) .run(); expect(ids).to.deep.equal(["content-untranslated"]); }); From 0fb36e4b6b53a7b94996860486623cf2f96ce090 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Mon, 2 Nov 2026 01:10:57 +0100 Subject: [PATCH 14/45] feat(database-decorators): let attachModifier and unlockrequest take what a decorator holds (#71) * refactor(database-decorators): build on interface-core 0.4's typed decorator targets The table modifier decorators get a DecoratorTarget, whose constructor is a Function, so they attach through an internal attachModifierTo that takes the class as a Function and the field as a property key; the public attachModifier keeps its typed signature and delegates to it. RegisterTable's handler receives a Class

through MakeClassDecorator's class parameter. unlockrequest takes any Constructible table, since it only reads the table's modifier metadata, so a caller holding a Class no longer has to claim the row type. Building a modifier's field entry moves to internal/records. Pins interface-core to the 6a8b526 tarball (0.4.0). * feat(database-decorators): let attachModifier take the class a decorator receives attachModifier takes the table class as a Function and the field as a property key, what it reads (the class's metadata, name and prototype), so a property decorator can pass its DecoratorTarget's constructor and key as they come, as the docs show. The internal attachModifierTo this branch added is gone: the table modifiers call attachModifier again, and the package gains no runtime export. --- .campaign/tgz/SHA256SUMS | 1 + .../tgz/antelopejs-interface-core-6a8b526.tgz | Bin 0 -> 89260 bytes .campaign/tgz/ranges.tsv | 3 ++ README.md | 2 +- package.json | 5 +-- pnpm-lock.yaml | 29 +++++++++-------- pnpm-workspace.yaml | 1 + src/modifiers/common.ts | 30 ++++-------------- src/modifiers/internal/records.ts | 26 ++++++++++++++- 9 files changed, 56 insertions(+), 41 deletions(-) create mode 100644 .campaign/tgz/antelopejs-interface-core-6a8b526.tgz diff --git a/.campaign/tgz/SHA256SUMS b/.campaign/tgz/SHA256SUMS index c6cc116..80acf27 100644 --- a/.campaign/tgz/SHA256SUMS +++ b/.campaign/tgz/SHA256SUMS @@ -1 +1,2 @@ +30391718e65481815db5b5bb10e7eabb1feac13165005ee46e54b592f3676c69 antelopejs-interface-core-6a8b526.tgz ccdc833729f50825d3567047a22bf9c68f47a38ca3527117756fbd80f380fabd antelopejs-interface-database-724ff72.tgz diff --git a/.campaign/tgz/antelopejs-interface-core-6a8b526.tgz b/.campaign/tgz/antelopejs-interface-core-6a8b526.tgz new file mode 100644 index 0000000000000000000000000000000000000000..fd9f0fa8f0846c980241ea75a15f4d45554191f9 GIT binary patch literal 89260 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

rM&B8{f}B333jWi7d`-BfuAl#V`_Y33U+uql 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 Lv@ooS0F(y+B+YGD literal 0 HcmV?d00001 diff --git a/.campaign/tgz/ranges.tsv b/.campaign/tgz/ranges.tsv index 41af30c..6a4c29c 100644 --- a/.campaign/tgz/ranges.tsv +++ b/.campaign/tgz/ranges.tsv @@ -1,3 +1,6 @@ @antelopejs/interface-database package.json devDependencies ^0.1.11 @antelopejs/interface-database package.json pnpm.overrides @antelopejs/interface-database pnpm-workspace.yaml overrides +@antelopejs/interface-core package.json devDependencies ^0.2.4 +@antelopejs/interface-core package.json pnpm.overrides +@antelopejs/interface-core pnpm-workspace.yaml overrides diff --git a/README.md b/README.md index 5e9d1c3..b5b9458 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ The package declares these `@antelopejs/*` peer dependencies: | Package | Supported | Tested with | | -------------------------------- | ----------------- | -------------------------------------------------------------- | | `@antelopejs/interface-api` | `>=0.0.15 <1.0.0` | `0.0.15` | -| `@antelopejs/interface-core` | `>=0.2.4 <1.0.0` | `0.2.4` | +| `@antelopejs/interface-core` | `>=0.2.4 <1.0.0` | `file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz` | | `@antelopejs/interface-database` | `>=0.1.11 <1.0.0` | `file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz` | diff --git a/package.json b/package.json index 8d4f76f..390fb92 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "devDependencies": { "@antelopejs/core": "^1.16.0", "@antelopejs/interface-api": "^0.0.15", - "@antelopejs/interface-core": "^0.2.4", + "@antelopejs/interface-core": "file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz", "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz", "@antelopejs/tooling-configs": "^0.2.17", "@types/chai": "^4.3.20", @@ -175,7 +175,8 @@ "packageManager": "pnpm@10.33.0", "pnpm": { "overrides": { - "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz" + "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz", + "@antelopejs/interface-core": "file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz" } }, "antelopeJs": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58481a9..12425c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,14 +6,18 @@ settings: overrides: '@antelopejs/interface-database': file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz + '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz importers: .: dependencies: + '@antelopejs/interface-core': + specifier: file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz + version: file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz '@antelopejs/interface-database': specifier: file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz - version: file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@0.2.4) + version: file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz) randomstring: specifier: ^1.3.1 version: 1.3.1 @@ -26,10 +30,7 @@ importers: version: 1.16.0 '@antelopejs/interface-api': specifier: ^0.0.15 - version: 0.0.15(@antelopejs/interface-core@0.2.4) - '@antelopejs/interface-core': - specifier: ^0.2.4 - version: 0.2.4 + version: 0.0.15(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz) '@antelopejs/tooling-configs': specifier: ^0.2.17 version: 0.2.17(eslint-plugin-perfectionist@5.11.0(eslint@10.10.0(jiti@2.7.0))(typescript@5.9.3))(knip@6.34.0)(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001)) @@ -94,11 +95,13 @@ packages: '@antelopejs/interface-api@0.0.15': resolution: {integrity: sha512-BUtZXJcLoVaTwM+q4KpefVuoR235HUpQqd5nqAfx7S6CA3SED3EPnFFvCLzaNZXq3Vq6ErzqJqvir0TaI/kpXA==} + version: 0.0.15 peerDependencies: '@antelopejs/interface-core': '>=0.2.4 <1.0.0' - '@antelopejs/interface-core@0.2.4': - resolution: {integrity: sha512-2qF6/KwahCQvvNuC/piUvPWD4wq2Npmco941HMg2/bMBoJKbhMH16WcXXRvPYznNSkU0U+6vqK6KE+2N5NojSA==} + '@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz': + resolution: {integrity: sha512-ur2KLvtD8h4SCZ0VSoQDc1pg+E9Bk/py3EW3qHHdoo3i6RvKWrrDKsX2khVdqQESuF2QMc1sJ6B+7ISjx5TvpQ==, tarball: file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz} + version: 0.4.0 '@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz': resolution: {integrity: sha512-rn+0+nTMBje0Wo4Ysg5/HGm/gqreLSyMkuXrsKF+2kAxrNx+5WEzmJHqy3CwJ1SOZr65lb3Yhb/M3CrmXCVYqQ==, tarball: file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz} @@ -2791,7 +2794,7 @@ snapshots: '@antelopejs/core@1.16.0': dependencies: - '@antelopejs/interface-core': 0.2.4 + '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz '@clack/prompts': 1.8.1 '@types/proper-lockfile': 4.1.4 commander: 13.1.0 @@ -2804,17 +2807,17 @@ snapshots: semver: 7.7.4 typescript: 5.9.3 - '@antelopejs/interface-api@0.0.15(@antelopejs/interface-core@0.2.4)': + '@antelopejs/interface-api@0.0.15(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz)': dependencies: - '@antelopejs/interface-core': 0.2.4 + '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz - '@antelopejs/interface-core@0.2.4': + '@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz': dependencies: reflect-metadata: 0.2.2 - '@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@0.2.4)': + '@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz)': dependencies: - '@antelopejs/interface-core': 0.2.4 + '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz '@antelopejs/tooling-configs@0.2.17(eslint-plugin-perfectionist@5.11.0(eslint@10.10.0(jiti@2.7.0))(typescript@5.9.3))(knip@6.34.0)(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 68c37ad..e1c8fb2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ overrides: "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz" + "@antelopejs/interface-core": "file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz" diff --git a/src/modifiers/common.ts b/src/modifiers/common.ts index 7fc51b2..c2ce57e 100644 --- a/src/modifiers/common.ts +++ b/src/modifiers/common.ts @@ -27,6 +27,7 @@ import { type InternalHolder, type InternalRecord, type ModifierState, + newModifiedField, unlockArgsOf, withRef, } from "./internal/records"; @@ -359,32 +360,13 @@ type ExtractModifierOptions = type ExtractModifierArgs = T extends OneWayModifier ? Args : []; -export function attachModifier< - T extends Constructible, - M extends Constructible, ->( - TableClass: T, +export function attachModifier>( + TableClass: Function, Modifier: M, - field: keyof InstanceType, + field: PropertyKey, options: ExtractModifierOptions>, ) { - const entry: ModifiedField = { - id: Modifier.name, - modifier: new Modifier(), - metaRef: { _ref: {} }, - }; - Object.assign(entry.modifier, { - meta: new Proxy(entry.metaRef, { - get: (self, p) => { - if (!self._ref) return undefined; - return self._ref[p]; - }, - set: (self, p, v) => { - if (!self._ref) self._ref = {}; - return Reflect.set(self._ref, p, v, self._ref); - }, - }), - }); + const entry = newModifiedField(Modifier); Object.assign(entry.modifier, { options }); const metaTable = getMetadata(TableClass, ModifiersStaticMetadata); @@ -489,7 +471,7 @@ type ModifierWithProxyArgs = { }; export function unlockrequest( - table: Constructible, + table: Constructible, object: DatabaseDev.ValueProxy, field: K, modifiers: ModifierWithProxyArgs[], diff --git a/src/modifiers/internal/records.ts b/src/modifiers/internal/records.ts index 293fcfa..700c6b0 100644 --- a/src/modifiers/internal/records.ts +++ b/src/modifiers/internal/records.ts @@ -1,5 +1,5 @@ /** @internal @packageDocumentation */ -import type { DatumStaticMetadata } from "../../common"; +import type { Constructible, DatumStaticMetadata } from "../../common"; import type { Modifier, ModifierEvent, @@ -121,6 +121,30 @@ export function dataByKey(internal: { data: Record }): { return internal.data; } +/** A field entry for a new `Modifier`, whose `meta` reads and writes the record its `metaRef` points at. */ +export function newModifiedField( + Modifier: Constructible, +): ModifiedField { + const entry: ModifiedField = { + id: Modifier.name, + modifier: new Modifier(), + metaRef: { _ref: {} }, + }; + Object.assign(entry.modifier, { + meta: new Proxy(entry.metaRef, { + get: (self, p) => { + if (!self._ref) return undefined; + return self._ref[p]; + }, + set: (self, p, v) => { + if (!self._ref) self._ref = {}; + return Reflect.set(self._ref, p, v, self._ref); + }, + }), + }); + return entry; +} + export function withRef( internal: InternalRecord, id: ModifiedField["id"], From 9a1d103a0191400babe4c8e3075567fde05aa82a Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Mon, 2 Nov 2026 12:56:16 +0100 Subject: [PATCH 15/45] chore(ratchet): delete the guardrail ratchet (#72) * chore(ratchet): delete the guardrail ratchet * build(sync): drop the ratchet step --- AGENTS.md | 7 +------ guardrails.ratchet.json | 13 ------------- guardrails.suppressions.json | 11 +++++++++-- lefthook.yml | 3 --- package.json | 2 -- src/common.ts | 2 ++ src/model.ts | 2 ++ src/modifiers/common.ts | 19 +++++++++++++++++++ src/modifiers/encryption.ts | 1 + src/modifiers/hash.ts | 1 + src/modifiers/internal/records.ts | 2 ++ src/modifiers/localization.ts | 4 ++++ src/table.ts | 3 +++ 13 files changed, 44 insertions(+), 26 deletions(-) delete mode 100644 guardrails.ratchet.json diff --git a/AGENTS.md b/AGENTS.md index ec80c8a..a803ecc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,7 @@ The release commit writes `CHANGELOG.md`. Upgrade notes for that version follow ## Guardrails -Finish every change with `pnpm run checks`. It runs typecheck, lint, format, knip, tests, size, ratchet, suppressions, and sync, and prints only what failed. The change is done when it prints no failure. +Finish every change with `pnpm run checks`. It runs typecheck, lint, format, knip, tests, size, suppressions, and sync, and prints only what failed. The change is done when it prints no failure. The tools enforce most rules, listed in the table at the end. The sections below hold the rules no tool checks. @@ -69,10 +69,6 @@ Verify a boundary against the imports a developer or agent would plausibly write `guardrails.suppressions.json` caps the suppressions in authored code: lint directives per rule they name (`*` for none), `@ts-expect-error`, `@ts-ignore` and `SAFETY:` comments, and under `config` each rule a lint config (`oxlint.config.*`, `.oxlintrc.json`, `eslint.config.*`) switches off or to a warning, keyed by rule and `files` glob anchored at the config's directory, so widening or moving a scope is a new ceiling of 0, while narrowing one takes over the room it gives up. Switching a rule off in a config is a suppression too: fix the code its rule reports instead. A suppression over its ceiling fails: fix the code, or remove another suppression of the same kind. A rule that keeps needing suppressions should be changed in the preset. Never raise or remove a number in that file: the check fails a ceiling higher than on the pull request base, and only the operator approves a raise. A directive disabling `anti-slop/require-safety-comment-for-type-assertion` is burn-down debt: remove the assertion, or, where it holds, replace the directive with `SAFETY: `, which takes over its room without a raise. After you remove suppressions, run `pnpm run suppressions:update` and commit the lowered file. -### Debt - -`guardrails.ratchet.json` records the lint findings and oversize files this repository already had. Its numbers only go down. After you remove debt, or after a pack upgrade, run `pnpm run ratchet:update` and commit the file: it lowers the numbers and adds only the rules the new pack release enabled, at today's count. Never edit the numbers by hand; CI fails one added or raised against the base. A rule listed there is off for `oxlint`. The ratchet counts it instead, and the pre-commit hook rejects it on every line you add. - ### What enforces what | Rule | Enforced by | @@ -114,6 +110,5 @@ Verify a boundary against the imports a developer or agent would plausibly write | Entry-only rules exempt no tracked outsider | `antelopejs-sync --check` | | A DMS layer's `build/backend-boundary.ts` is the pack's plugin | `antelopejs-sync --check` | | A test names the wrong implementation it catches | review; no tool | -| Existing debt only shrinks | `antelopejs-ratchet` | diff --git a/guardrails.ratchet.json b/guardrails.ratchet.json deleted file mode 100644 index 24f9314..0000000 --- a/guardrails.ratchet.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "pack": "0.2.17", - "rules": { - "anti-slop/no-unknown-parameters": 8, - "anti-slop/no-unknown-returns": 1, - "anti-slop/no-unsafe-dictionary-type": 11, - "anti-slop/require-safety-comment-for-type-assertion": 5, - "typescript/no-unnecessary-type-parameters": 4, - "typescript/no-unsafe-assignment": 2, - "typescript/no-unsafe-return": 3 - }, - "oversizeFiles": {} -} diff --git a/guardrails.suppressions.json b/guardrails.suppressions.json index 47b5e35..399ae6d 100644 --- a/guardrails.suppressions.json +++ b/guardrails.suppressions.json @@ -1,7 +1,14 @@ { - "rules": {}, + "rules": { + "anti-slop/no-unknown-parameters": 8, + "anti-slop/no-unknown-returns": 1, + "anti-slop/no-unsafe-dictionary-type": 11, + "typescript/no-unnecessary-type-parameters": 4, + "typescript/no-unsafe-assignment": 2, + "typescript/no-unsafe-return": 3 + }, "tsExpectError": 0, "tsIgnore": 0, - "safety": 4, + "safety": 9, "config": {} } diff --git a/lefthook.yml b/lefthook.yml index ff792b5..779c267 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -8,9 +8,6 @@ pre-commit: run: pnpm exec oxlint --no-error-on-unmatched-pattern {staged_files} antelopejs-size: run: pnpm exec antelopejs-check-size --staged {staged_files} - antelopejs-ratchet: - glob: "*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue}" - run: pnpm exec antelopejs-ratchet --staged {staged_files} antelopejs-suppressions: glob: "*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue}" run: pnpm exec antelopejs-suppressions --staged diff --git a/package.json b/package.json index 390fb92..7336c71 100644 --- a/package.json +++ b/package.json @@ -131,9 +131,7 @@ "prepare": "if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && command -v lefthook >/dev/null 2>&1; then lefthook install; fi", "checks": "antelopejs-checks", "size": "antelopejs-check-size", - "ratchet": "antelopejs-ratchet", "sync": "antelopejs-sync", - "ratchet:update": "antelopejs-ratchet --update", "sync:check": "antelopejs-sync --check", "typecheck": "tsc --noEmit", "suppressions": "antelopejs-suppressions", diff --git a/src/common.ts b/src/common.ts index 5645b23..2cdd7a1 100644 --- a/src/common.ts +++ b/src/common.ts @@ -143,11 +143,13 @@ export class DatumStaticMetadata { } } +// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- exported; replacing TTarget with `object` clears this row and adds 6 (1 no-object-parameters, 5 no-unsafe-argument for the any-typed constructor callers pass) export function getMetadata( target: TTarget, metadataClass: MetadataClass, inherit: boolean = true, ): TMetadata { + // SAFETY: getMetadata alone defines metadata under metadataClass.key, always a TMetadata (new metadataClass, its inherit or its upgrade), so the read is a TMetadata or undefined; reflect-metadata types it any. const existingMetadata = Reflect.getOwnMetadata(metadataClass.key, target) as | TMetadata | undefined; diff --git a/src/model.ts b/src/model.ts index d870661..b95d322 100644 --- a/src/model.ts +++ b/src/model.ts @@ -155,6 +155,7 @@ export function BasicDataModel( * Pass `{ partial: true }` to skip fields not present on `obj` (patch semantics). */ public static validate( + // oxlint-disable-next-line anti-slop/no-unknown-parameters -- validate is the parser: it narrows obj with isRecord and decodes every field with its codec before reading it obj: unknown, options?: ValidateInputOptions, ): ValidationResult { @@ -179,6 +180,7 @@ export function BasicDataModel( public readonly table: DatabaseDev.Table; constructor(public readonly database: DatabaseDev.SchemaInstance) { + // oxlint-disable-next-line typescript/no-unsafe-assignment -- database is a SchemaInstance, so its table is a Table; a schema instance covers every table of its schema, so no type parameter of it states that this class's table holds this class's rows this.table = database.table(name); } diff --git a/src/modifiers/common.ts b/src/modifiers/common.ts index c2ce57e..638798b 100644 --- a/src/modifiers/common.ts +++ b/src/modifiers/common.ts @@ -37,7 +37,9 @@ export const MixinSymbol = Symbol(); export type MixinType = T extends { [MixinSymbol]: infer A } ? A : never; export class Modifier< + // oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Meta is read only by subclasses, through this.meta; removing it changes the published Modifier Meta extends object = object, + // oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Options is read only by subclasses, through this.options; removing it changes the published Modifier Options extends object = object, > { protected meta!: Meta; @@ -52,6 +54,7 @@ export class OneWayModifier< > extends Modifier { public lock( _locked_value: LockedType | undefined, + // oxlint-disable-next-line anti-slop/no-unknown-parameters -- the published override point of every modifier; a modified field's value has its table class's type, which the shared modifier chain cannot name _value: unknown, ..._args: Args ): LockedType { @@ -59,6 +62,7 @@ export class OneWayModifier< } public test( locked_value: LockedType, + // oxlint-disable-next-line anti-slop/no-unknown-parameters -- a modified field's value has its table class's type, which the shared modifier chain cannot name value: unknown, ...args: Args ): boolean { @@ -76,6 +80,7 @@ export class TwoWayModifier< Meta extends object = object, Options extends object = object, > extends OneWayModifier { + // oxlint-disable-next-line anti-slop/no-unknown-returns -- the unlocked value has its table class's type, which the shared modifier chain cannot name public unlock(_locked_value: LockedType, ..._args: Args): unknown { throw new Error("Unimplemented"); } @@ -92,17 +97,22 @@ export class ContainerModifier< Meta extends object = object, Options extends object = object, > extends TwoWayModifier< + // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- the container holds the field's values by key, typed by the table class, not by the modifier Record, [key: string], Meta, Options > { public override lock( + // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- the container holds the field's values by key, typed by the table class, not by the modifier locked_value: Record | undefined, + // oxlint-disable-next-line anti-slop/no-unknown-parameters -- a modified field's value has its table class's type, which the shared modifier chain cannot name value: unknown, key: string, ) { if (key === "*") { + // SAFETY: the "*" key addresses the whole container, which the table class declares as the record of values per key; a non-record value is stored as given. + // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- the container holds the field's values by key, typed by the table class, not by the modifier return value as Record; } return locked_value @@ -112,6 +122,7 @@ export class ContainerModifier< } : { [key]: value }; } + // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- the container holds the field's values by key, typed by the table class, not by the modifier public override unlock(locked_value: Record, key: string) { if (key === "*") { return locked_value; @@ -119,6 +130,7 @@ export class ContainerModifier< return Object.hasOwn(locked_value, key) ? locked_value[key] : undefined; } public override unlockrequest( + // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- the container holds the field's values by key, typed by the table class, not by the modifier data: DatabaseDev.ValueProxy>, _meta: DatabaseDev.ValueProxy, key: DatabaseDev.ValueProxyOrValue, @@ -182,6 +194,7 @@ export class ModifiersDynamicMetadata { * Values assigned to a field before its modifiers could lock them, by field * name. Reads return them; `unlock` and `lock` assign them again. */ + // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- the values assigned before modifiers could lock them, by field name, have their table class's types floating: Record = {}; /** * The arguments each modifier of a field was unlocked or locked with, by @@ -249,6 +262,7 @@ export class ModifiersDynamicMetadata { ); } + // oxlint-disable-next-line anti-slop/no-unknown-parameters -- a modified field's value has its table class's type, which the shared modifier chain cannot name set(field: PropertyKey, internal: InternalRecord, value: unknown) { if (!this.canSet(field)) { floatingByKey(this)[field] = value; @@ -295,6 +309,7 @@ export class ModifiersDynamicMetadata { /** The names of the modified fields of a table instance. */ function fieldNamesOf(object: T) { const metaTable = getMetadata(object.constructor, ModifiersStaticMetadata); + // SAFETY: the names attachModifier recorded are keys of the table class of object, so keys of T; Object.keys types them string. return Object.keys(metaTable.fields) as Array; } @@ -310,6 +325,7 @@ export function fromPlainData( Object.setPrototypeOf(instance, prototypeOf(TableClass)); Object.assign(instance, Object.fromEntries(Object.entries(rawObject))); triggerEvent(instance, "fromPlainData", rawObject); + // SAFETY: instance gets the prototype of TableClass and the fields of rawObject, so it is a T, as fromPlainData returns; setPrototypeOf types no result. return instance as T; } @@ -337,9 +353,11 @@ export function fromDatabase( rawObject: any, TableClass: T, ): InstanceType { + // oxlint-disable-next-line typescript/no-unsafe-assignment -- rawObject is a stored row, typed any by the published fromDatabase signature; typing it T breaks consumers const shallowCopy = { ...rawObject }; Object.setPrototypeOf(shallowCopy, prototypeOf(TableClass)); triggerEvent(shallowCopy, "fromDatabase", rawObject); + // oxlint-disable-next-line typescript/no-unsafe-return -- shallowCopy is a copy of the any-typed rawObject return shallowCopy; } @@ -544,6 +562,7 @@ export function testValue( return val === value; } +// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- exported; removing the parameter changes the published signature export function triggerEvent( object: T, event: string, diff --git a/src/modifiers/encryption.ts b/src/modifiers/encryption.ts index 4dc0568..b569dc7 100644 --- a/src/modifiers/encryption.ts +++ b/src/modifiers/encryption.ts @@ -92,6 +92,7 @@ export class EncryptionModifier extends TwoWayModifier< public override lock( _locked_value: LockedType | undefined, + // oxlint-disable-next-line anti-slop/no-unknown-parameters -- overrides OneWayModifier.lock; a modified field's value has its table class's type, which the shared modifier chain cannot name value: unknown, ): LockedType { const iv = randomBytes(this.options.ivSize || 16); diff --git a/src/modifiers/hash.ts b/src/modifiers/hash.ts index 40e0ec4..33ac32b 100644 --- a/src/modifiers/hash.ts +++ b/src/modifiers/hash.ts @@ -29,6 +29,7 @@ export class HashModifier extends OneWayModifier< > { public readonly autolock = true; + // oxlint-disable-next-line anti-slop/no-unknown-parameters -- overrides OneWayModifier.lock; a modified field's value has its table class's type, which the shared modifier chain cannot name public override lock(_locked_value: string | undefined, value: unknown) { if (value === undefined) { return value; diff --git a/src/modifiers/internal/records.ts b/src/modifiers/internal/records.ts index 700c6b0..eba279e 100644 --- a/src/modifiers/internal/records.ts +++ b/src/modifiers/internal/records.ts @@ -11,6 +11,7 @@ type ModifiedFieldList = ModifiersStaticMetadata["fields"][string]; type ModifiedField = ModifiedFieldList[number]; /** What a modifier keeps for a field of a stored row, beside the locked value. */ +// oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- what a modifier keeps for a field is known only to that modifier, so the shared record cannot name it export type ModifierState = { [key: PropertyKey]: unknown }; /** @@ -20,6 +21,7 @@ export type ModifierState = { [key: PropertyKey]: unknown }; */ export interface InternalRecord { meta: Record; + // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- the locked value of a field is known only to the modifier that locked it, so the shared record cannot name it data: { [field: PropertyKey]: unknown }; } diff --git a/src/modifiers/localization.ts b/src/modifiers/localization.ts index de324b1..c672436 100644 --- a/src/modifiers/localization.ts +++ b/src/modifiers/localization.ts @@ -66,7 +66,9 @@ export class LocalizationModifier extends ContainerModifier< * it in queries too (`unlockrequest`). */ public override lock( + // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- the container modifier leaves the values by locale untyped locked_value: Record | undefined, + // oxlint-disable-next-line anti-slop/no-unknown-parameters -- overrides ContainerModifier.lock; a modified field's value has its table class's type, which the shared modifier chain cannot name value: unknown, key: string, ) { @@ -78,6 +80,7 @@ export class LocalizationModifier extends ContainerModifier< return withoutEmptyTexts(super.lock(locked_value, value, key)); } + // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- the container modifier leaves the values by locale untyped public override unlock(locked_value: Record, key: string) { const unlocked = super.unlock(locked_value, key); if (unlocked !== undefined && unlocked !== EMPTY_TEXT) { @@ -89,6 +92,7 @@ export class LocalizationModifier extends ContainerModifier< } public override unlockrequest( + // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- the container modifier leaves the values by locale untyped data: DatabaseDev.ValueProxy>, _meta: DatabaseDev.ValueProxy>, key: DatabaseDev.ValueProxyOrValue, diff --git a/src/table.ts b/src/table.ts index 5762e8d..89445f4 100644 --- a/src/table.ts +++ b/src/table.ts @@ -104,6 +104,7 @@ export class Table { `${this.name} has no table name: decorate it with @RegisterTable`, ); if (isSchemaInstance(target)) { + // oxlint-disable-next-line typescript/no-unsafe-return -- target is a SchemaInstance, so its table is a Table; a schema instance covers every table of its schema, so no type parameter of it states that this class's table holds this class's rows return target.table(tableName); } const schema = boundSchema(metadata); @@ -111,6 +112,7 @@ export class Table { schema, `${this.name} is not bound to a schema: call RegisterSchema("${metadata.schemaName}") in the module that declares it`, ); + // oxlint-disable-next-line typescript/no-unsafe-return -- the schema instance is a SchemaInstance, so its table is a Table; a schema instance covers every table of its schema, so no type parameter of it states that this class's table holds this class's rows return schema.instance(target).table(tableName); } @@ -168,6 +170,7 @@ export class Table { | ExtractTableMeta> | ExtractTableMeta> >; + // SAFETY: c subclasses this and gets every mixin member the base lacks, so it has the instance type Combined computes from the mixin classes; the Object.defineProperty calls give TypeScript no way to derive it. return c as Combined; } } From 7a0cbf59861e2d2eaa0fd4dcf8fd760f126ce1b9 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Mon, 2 Nov 2026 15:53:39 +0100 Subject: [PATCH 16/45] build(deps): pin core 1.20.0 and interface-core 0.4.0 to the W7 tarballs (#75) --- .campaign/tgz/SHA256SUMS | 3 +- .campaign/tgz/antelopejs-core-184eb90.tgz | Bin 0 -> 337658 bytes ... => antelopejs-interface-core-f7df541.tgz} | Bin .campaign/tgz/ranges.tsv | 3 ++ README.md | 2 +- package.json | 7 +-- pnpm-lock.yaml | 40 +++++++++--------- pnpm-workspace.yaml | 3 +- 8 files changed, 33 insertions(+), 25 deletions(-) create mode 100644 .campaign/tgz/antelopejs-core-184eb90.tgz rename .campaign/tgz/{antelopejs-interface-core-6a8b526.tgz => antelopejs-interface-core-f7df541.tgz} (100%) diff --git a/.campaign/tgz/SHA256SUMS b/.campaign/tgz/SHA256SUMS index 80acf27..1fe63a3 100644 --- a/.campaign/tgz/SHA256SUMS +++ b/.campaign/tgz/SHA256SUMS @@ -1,2 +1,3 @@ -30391718e65481815db5b5bb10e7eabb1feac13165005ee46e54b592f3676c69 antelopejs-interface-core-6a8b526.tgz +0a51c9c2da58c2586bfa5095b1da92a7b74130acf1e64820bfd5f98eaa4200f9 antelopejs-core-184eb90.tgz +30391718e65481815db5b5bb10e7eabb1feac13165005ee46e54b592f3676c69 antelopejs-interface-core-f7df541.tgz ccdc833729f50825d3567047a22bf9c68f47a38ca3527117756fbd80f380fabd antelopejs-interface-database-724ff72.tgz diff --git a/.campaign/tgz/antelopejs-core-184eb90.tgz b/.campaign/tgz/antelopejs-core-184eb90.tgz new file mode 100644 index 0000000000000000000000000000000000000000..de90b081ce1923b71baf6e9be2cc04259aeba340 GIT binary patch literal 337658 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%0A?1+caWY!=nm$GwA%8RK$gWj?L~ zAf{msU_k*|LfNhAEcPA45)D(R5X^gC?Ql^3-lDh)msU)9Q(4zzY5uJT7_hix^R*E) z%v503qJjpnyfDHEmV%Y3xGYE9!;E*=3RY^+IBk(& zCJHh-jTsAHvorw)xVD&lI9*^Az%o{ZX(XXYv|p2Vu#_v=}+~ zK?i_Z)w@v|aHp0UW9P%TyyA(`xbIMCU5UqH$djhip%ZsLFl8_$OjH;~^qJ0I7Nwhw#inFOo? zPItO3>4w_!?O!osRIy^1!dCP-(5WLR*u`+@yMnkkcuDLR&DirO#F2Mlm5vvHXY5SMeU?oV=<%3{iyLA-q<9%{sunXcl}1Au;N4H8%~b&wY(Z&6Ke`zC6v1|A`=p z-~o@3|Lx72l^^Z=Z*QOMzaHoJee%DJv#sosf&XHnWhtJohHVwUIeWPOJZasGaw`*D z?rV^jGi5Gb+xL%q2fwW!fVQK%^=Wr~TbK$7*#g|$C@H{yem>gQrb41YZ}({DL+5}7 z=eG2H3TPhau8Om&RQHTAGepz@KaE_qR*vGEtnEcv#LJ8TnKG~vBrg!0k77nBAtAo) zj)!M_6muFpm^QXD=m6SE-HjxOA9kYxKJ@lJe(ZJ*gCG1Y+f(zfPW+SF7~~}j`+34z zfjKj{!uv9&lyE0;EO7cNQSro`Sgdbbg;7P>0Ea+$ zzofySROC&=ObagPJw3CatdcZu2aQx9q#@O?LRhDQ$lRn^lHZiSvf?bR(&4%cPGb?YJLx155Y{&dcT(UDYSxflJNv+29b){i&sA<%#zCX@D%I$35#-g9bkz z+F5mL6G{#{2jC0w@_+v$`QiCXrD)ySJ^EX3e|!D#}_^-x4>d+Z<9MK zbf8GXxqi^uJ?sIvAGbQ&;OVjQ-f_|d@i8KlWPtHz8&%2d&SlQ#BrUqPS-%-7beYPC z=sX>OoG1jQ=#~>D1bY#DakkhZN1+q8m}<+)O@N-_4teCRr}nz zaz9PNQEI4u$||mW0&w_F6GEQT6xISCkdZ*5Wb*R)-6%~aFQMJ3N^Ly{;8Oy_o~LY( zz?p+iRXdFA2;ZVXTvl7CEo?L!&ESV%g?LI;Jm!#$T0K#tbu>lJIA=2KWuLZSFIuQ%$ECb8J1wS zy~(l^LgX7t4g)-=F4t~kl!+E!xaJqwXmI!=pGW`_pNi!0c7)oI(~UR-7auqburTnr zfJJkD9z4GjiKFG<69_dMb5`pGuoy3zUjH3`wrn{8?yv~|jJOm4VD;aQrg?(k^7&c_ z+v0gvjieeg4y~nEcAmhn$aC1!es+gv1fPq?;4%*LdGYKFeb?zDd_nFwLf#vn*VcSD zq7nHL$nk<@MB+uW0po3vCa~CQMg#h~kU@-Bry1IT*sXi#!oH+UT@R^BH!Eynvxwa9 zWUwmDX*opB?fh7y7yeCWhyjm%CiSI>WOjx|DQ1%1NEg!>(-oCq?0C91k;$GTNvao z0QVm9m>nR15DrJMf=#=YfBj5+!cX=TCkggs)+T5Yi{q=rr{^!Gh*u#*2?pH5D=qQwK1&Co3FS+N!I5-4C%ebRb$Tr< zP>c-|(9H8Vq=*nD|M*Em^4GuWj))0>stZx1Y-1Dnzt90qA9MMHQN_qaI;;=v6&!_)S#{9}%;)aFaKmO<2S3kbCa_m4r3AFp{u7 zeuMZkyeTls9uX*frpZ#l5!P=eAU-z!WTsRDgfTDz0kZcW1!AP^FNcEt*`6*cOOG|{Lq(LSW496u8 zixLN;yiB<*ekz7&_}+RljOl?4hyryWsBIl2Ju|+{!V8x1Hjt`l5ce4v-@g36UtY9= z#{bi3qQtP#YBXIaJZDbi(y(&)@cu>a9wkl{yI34+Xh?IPK_6{Xm777XzCrpgIdiQz z6(bT$ahbf#WFyh7rmcP2OD?Huw1V<7FRFIX7&UCaUrZQO*MK<<$f8Ya*moLUi)p!y z?23(oofoXUT*6aKTAbxMn0YolF&ON~394M7Wm+5>v3`;COfnNCMeDnVo$bB-&i{15 zw)KBH8;7(;|0Xfglya0}hi=xTV|i~y+v`WW8=rff-Cws3_IANZz4z<-!PfeR?am1_ zQWYg9)Y<*jNZ+d0!GE>zG_LAk8v#4SO1KBDjIeMD3V!VzeAw%DNQby!8yLAj^mJ^m zBcBva5A3t+H(ssK3Ol21sKq564dQXuzf|>`A+fK3g=|q9fR-tn)7X=B$VU&dWG(zq zJ9}Br`0UL<;CpjTQ8^JF>#zWj@8*M!N7Q8%xIIGTE74W~M$Fqj9D!jrhD7Oh4tpD) zA*9TvwgXeG<~9~n7u#MTT!M}uF|=_#dev{!FV=aq2XbSURlb zGHSK1HnXOA)I5*6>z|BSS`a%gi#I2lIU~`9HV(~OQXN2 zt%c9p{Cm(8B|o#evjw1nsNgMqvgEquwtBtC!p}Yyv#sK77W7FR@tuD4`9I1_h`IF= zFa0H%ljR=bfLZY$-o0_{e_sFi#Q#6W@B7&Q3j$CXc_KB$c{*>2nL6s1EWgP}_WBaj zM>*e}5%jqmyw&@8;FVe4Hgh2r)HmExXb0{l*{_Er>uPtsu3_>24mZ%h-QiwYujp zND%XM(MD+3Q%(P$r(Yms*0Dj-K<3xL%HFCS;`ieLt19sJjQY267Wy_?a{_TcFo#W^ zu@=PAAH^l4X^`Ok1nlh31eOyoqmh(~;#ZBUPidAIJ77=iW_*myam{A$dT@16c) zw<^H_cB#szvn$O@WK+nF$Ux8MWc%Vr^0vpkqs=0$;R?rglo|;`SZl`W#{k9LXgTRomLYh zE9UD;bE!^0zAU(Q9ownnnP=KwA2_?QU>z4ZV?Hk*AL(U>mIR`Ll@+51s>LW0A$ENf zt~(pTuuqeVVP;9^xpBbK5Rq(yH#5Ly-TFQ2JF6y;I>%G%5+c4`wa^~5;@^6zv^<0O zWQl}EwvknPDEgPIf7KnO=&ohl!1y^l-OlqX8L!GiGjM8Q_QRi~LkpWK$^s^*Ca_@S zoTkr=+s>*jTFLwn_v=?LC9OO1%>x7Cobo-Th4b@Z>?|K7 zApy$_OQEj_!8ipTQI~5fV<9;oQHMe$m#UTfd08tzJ?fLw)o#&6EUWG8cF*9W3ci}3 zyEKGjfoM%MFvM@h!idzmsv|zW11lHu&z787!kSrvGuQu1Y(^#b0X@Y7O+aSIe_p?P zW5@q|`{Uau|F1{+{n70|#GUs~K``&fV6z!G--!4rj}u)6awp@%Vjg9WVzD6~iO%k? zy^mYlT;vi7ehJgx?Ec#8>~8FBZtZ^J@|fy;>AzGdcZb;h#GCdy2M2ox+F@*Wud{oo zYVzox$w!`-;TmY5(lqubjPVXQlw=oBi3G+n>xdZD_PsPE#_PpP2Ec&lKQ>N#W# z2VJfmKx(FIQU7s*Qw)Ll+b79e=5&42zFx9}maz_VDUG-bcpWb7$HuQzGv6FXBQ&wX zRPckruG4&6LV$GOLg?g)aVn-4;HT~cxOI1Ta4UY*gJa3heKUwi1UeKV562WG;AZ#( zRBlpYnG7(8I@HOon{wP{)83aJlQHfu z3no2@qW`2VxfKv93f7UY2h;0*u&cR#K; z^xwPJPxil$^7GMuMwV<3{b!WVksN;U?DYT#I4#sATHnO9uXzzd=Tz{*n*m6T3syBC zhc5rJ7Cajg@-t}j&)n<*MB&hae^9c|vjHRb0s`a9Tz|nTlB~WN+^Ll3$5$usgNaJ4 z!T;Y+)4)$%M*C}yKM7TGm)f*I!FiBqG~Yh9Onwi)NQHM-8vGPo;jG^WDR^%2({Y@N=UMHSrhT)80tB8Orxid0B6M+20j=&zXGEbw z+JwiP{o}FITw;jixPI8_ZEhV%N8ASvU;rKQG;AX2t~cU7B&6V58Q!Cz4BLHP!X50Ym9%I3q-8Hz z4Z4s=z6*f&{!L5`THghq^GfV2_)}Q@0wiBDVWaRyfSc(h>+q#L7}Q8sW#;_Xim6sokB_diyyfQQF4t;U5d z;MMgm)z%Re6zb%4A!fFf_+zQc{)1I@*i`aSHMs7>BoA#ExtNiRx4Nna@C!+O@)(+X z6c+?S%ja<-P>q{HA5l4?ADw}c-w;4c0%(f>sUxM{<0e*15%mWwE~uha;uaLJ>W-Oq zVXKE7D=qpUIZ-6;WTy8C$mL|Vjtf@t^qYyyQsKzoM5{=qZ(WOom}@P9ca<+OW|sAc z+~j%LOnD>*b4f2)^_wiTN5Mg?$SyMW1ER7YXL2Wq))0Zg?ntJnRd85+PT3i}znO<3 zN)sd4t|R#tuN;basmTjIepPY%G0o8Gg-c`YXY9N*1+=oBCoF~lR$bntcFUV}=Q>Xl z;Fo}RILZdB1djGM?tkJo#d`H7CpuC~O*rH_Fb#w_V(_LTd-qKRbA)VV=XuzWY!UZQ z6y}dfOHKd?OYAXGTZAPk${5pw1^?p)#5lY}8gB+)krk75++ zy`?XZxTxq2V#M7pvh}lkTpeZQcqDRIVenoE?F}s%iO6GwHhYG}QNpf)r44no8zAn# zepPONgvK{63&j6#kdod^9%peD4ck0cN zALb(uv9d!Z5@$BZU!!v>hkg;ofweCB<@RHIc0!!dAmVY_7kZp|Ghx8y^6-2i zWwh8XWL)E>+TFv>K2K(W$Gi6Ew)BMLb|jzN-O&2W$$Rg{(U%#|9X;bXiG({M3I?*T z(iZOEXctnQ_4fC+w>JK6C9e>OH=a7(Lov>T*?SjkN3%X_E(DDk?=&wf(HRDBpM6P4 z5vtEptq#zNjml=|tXzwXUwgngxd+-nS4N2~=CM66F9~M|{M&2RM~w?078m3;R8Qcl zwB~S)h39?y$Q47oZh9{gYjADv=drAoh)PC?re%xAlh5ii)Nc;G0>)g z#2wBPHK4KYYyRICK(XUr!J3EE67`x($&Hg5eDJ_;#Xl{0pMB1axqBl71Z#m8 zCkLVuOV*`aukEiW%YEppvpcuZYMws%Z1^qaXVAo0!!oZ{W)}KZ(SmVhkJL1JvIMcs zh~EVGdNvw__&4Ze7;!vgdpDUtFbC1>Sy@%MbehPe*Vv$fS8iV)j7MBD2_8n^iGphZ zh8uFu4d*_io7N!2J^jx|jw>T?X%{}j$U<31{C?fj4f_usOIfN-zDepg{+nU{yR!1i zj{o-N-Rn0`_8*V&`!4?9o2<&maM_E0=zrTdjnT5M zGekF`LCj+t(?3aGHa`7*-W?alm>-n!T0Dx+(m@K@c`Vbkm$XcqnQ^h7Q&)U#%$Nz@ zXeE&<%Y?OD^k4uJ8TC;5Ok=?EIHAP5Les=)HX2uL8uu>ENcdMy^|Y<9cru8E_%G;6 zKE(YimL(Fxdnh6PyFZBgS4dx#po9>;O9(_;8xES&nJ#Xq#MNXUWH%PJXJyh zQNYK-E{?^gNc?$Xy)L>3o_>uq$7bh)_`;5mvjM(+g8>)Bgl7VE5>g9LERAF%IfBt+ zl_UP8im`v0r+sEm8ovri#UFO6WISu4Fyepd%`k}1*gzHXzerN$G^?(evx%2&#W05B4Dl!{UB? z{=YMBhT}n%ejfzUuK#^lD(6EJK4M9mhMb-ghuBthTZZsllr{~?KazwzKu|v8SnbD| zX!%=OUFPFTJbpDXH+YMTv83FBM<2(kF_`jqT(h|5_4@T|@IeGm59#F`#r-}TRV5OR zrh08$GFjl=N`MCcQq=p$5-5psE-=)EQf@jIz`@tAM(5!(&+PyDS2cSDa9Zx6EmG55 zeWB`d&qiEc)y`f}0MngpaavXz9|A-s%e0KVD=@2HV9np{4N<|S83xOuEDXc4_w zJ>eTnR?em$$($_Ol*BQgBHbT71>yc<{#^M#8=kQw5s|;Y+%Dxe;JkF1j_$7tm=*u& z)oV-t`wGjy@m#&XC;9)k*Z=n85m>A5-DC^`l6*F$f`@FT^+gT^ZkkWQa7@doMM?C)JI#4dH8I=yu#kTgULoo>hv zlXWMX0126e;0G|vIA;wsh9ddTCU+=l#2TjoPR!Hq{E zhw7Kx2>&(>fOMAakJxWB#ENEmX7NqPrNv;=%yz_x39I7%rDa$yW6S{5e2%Ta0Nz5| zd0up&6N)-w7BCkQ*CG*R$N=1mVC&h$VO#RqCTv*=92 zEL3CLIYOpx#Y`z(XH+YH0!EQ8+Rp6a_EvikYTRAqTxE)4&=SZ(9$Hw8u9@t5T6_KC zqF@(sMI(>FGa%f+AZ#>ZCb-PG0XTozGQ>ZZ)w8sZ~E!MNbrHJw2>rS-KgNYZjNh;Pv;RerUy z22lJSZESS9U2mC_LywegtnY4gwzp}%F;Z#?a%ejYzf3u%|Yp zzWP>YAyOz{6@(2Z8f|lDg9w@r^#S5|$HgsC^mFqRf2tpy-)GkO^^H+ukYkuN1=Oxd zhpqPX#tojD2Dq>84X%r?`vUvQgL?_;EGX(0_WrQc_2@y~>w6vYb$n(~-GU|j5B-Vv=U=l%m*9eEbz$NMjedu!eo&>wwQ@Vv-!Zua{_ z;U-$069(tyxjzg}SM$PPy^HsUz)r*JZko6Ezn}G!eqR9JQo3MXh6V51x(p?%o^EN5I|}#CKkDc(JW0Y3%#MVD_`v z=#un*=;*Sac}F)GlKT&CbhF6V-(cK@#-zO3{fDK!cn-yX-TP3OJm!4?{4a}FofkQj z@xA}phTXD@od0!i+I%JR>P7DIY)|B;PuJcwCYzBgv1$m8C8;IfjM%!?F9 z0`q{eoxTAXXu6zu_{Vh_Hpcz^MPl=s~mt`#LR^Z|B% zL4+fuyf=ieT!E(#)cdpjTF82D7+(`f?YujRihLh5uK*{9z88Qv|>a~qt3RbXfxjc} z9gQ{78A!D(7@U@=wr~b&&{)&L4)yKgA?fV|5CV7!wa3ra2v&Extug`xaM`j%qy~XN z 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 Q3+JAF;*%CFMM% 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%0DXOz%EJ*pmD%&i) zj0#ID1*_5v=uiM=9;xTBBfQnUgaD%MI~;jvsb1b#RSxszh;##=`iHEfI#Q+=uZ{=} z(A!Ynk9M#mx{X_r?^zM4zPZZ7AYc32@nynjszb&o9e33!@H6dZSmXNw^Z^8HT8`Hp zu(59JLu@5GrIZ6?@ruJBTBHX>NMPjOf?+d*D-*O0B8I>Ku0T=0Ziqt62@)6%(j$o_ z1iKQP&LvU^M-KRaYe23Jh#`#w!!S||!A0|+|K0~}2t>=!0YJ$WE#79dEyR3IA*5Jx zO2o+|6E6avsZ|KOv_flFQ+|Mcrg~jy*#xySAt3MSQM#*Qj6EuUf%YPxA}bttj3IFV z?OT{!`It!SPn=vP!R_j@_kT^r6n9C6s!M$_R^EqEw8CHZ^+1J!DJ3Q&PBdg!Ka4Um zQB5c841f{cD`!XmN#|H{w^-gq5=1Pgf9pj8LYc-G)9*!~J<)nb1Y|`rx+}Vy3O038 zW`S3L)(r-OJX_+s_oIiAw%%tV0}X2RHS)d$*!MS`@&lRc5glR8K{#Qt{Z1)lK;*oX&*rP~|flR*-Gr<8yae3BhvVgL^Usevh%t?22 z7nEg#h!mC;4)s+g3R`qx@H36RkPl56EmSv)fKAb~*GTGEVRPH6g@S!U-nHyrBKPpp=jGi(^{bI$4N5`BS|FE~!g`A|k=&{H z^#t}KI%8iad5;~XF@(HGI1q^OBmsnOj%cTN^yvgyDe@%@EmLTAJDOp@S62K*T3=47;ndcp@ly1o(ON$S?w-0cu-wKKu&-*U##6$VX3U=#`Tk zhNC&X6l;9s=yDALi7it?1*ve8>m!3Wi!_23HV2Kz%$PFnOk0E z6>XCb?0_x{981abRl**iTXSCcl1SZyOtP^Ix*MSXLq85e)Cm_chBiF=J!E+s=a%7U zB#4aTre0dQPzYC!O1#chNo2qx%y$ra22g_7Cc2+N8cS1fp#8&W7TweNkvM!DcfWCBZ5 zkQCPdxD<2z3CM5IA&p3ZEtRp&4eF-zc_!&X_e%AJ;=@*uhl_+pI{6D-e;QZ;r&2+?g#JM z&rDtw<${o*cesLGg(~8`_hlbInmNWxtOl72P(oq`9ttUa$?59m(bD_q{>+d{-{V zuBWek!getPi=~)G{FN+o_&iQ}$z*i^DvSdGAlnACW$aLdqlQuu@c{H3gO{S2c(Ncv?6ab^;3}5^YnIYNHITtDm}1RfRo}dXuL53z0V|XddaH; zUJ?|U0ui7T-Ccmri40ebP=zt@9%X7c0pO|ifJ@B3j@U9VjFa(hCB0z`ZdMX?`p3{L)y-3oN-pvm(MJsYG;P zzPe1=)Ix^eO~@Ta3IitlT7+I;WJOc>MTdZlrEz~i$XM(a=m;G!Ng#vv4|Yjarob@> zQqdGBS(i&6EccZ+P9MVs(g1Q{V$eJN-}|I&t27|$(=!S9M~=w+E|E4?&7-ve;90xc zJ{^$sh*U5k6kfThKvfRzNa%_(^dt3;9(%H)2aVKbzl)R0Omd}|(SqXnmF~4*Y6-uP zm~Sl(0QxThs+0khy0>PqDf*%4N?Pb=5}Xu<@KJw5x|oH5_nG{6Loz7(#*2LkG4y#* z%oTXq?~=DEAU}r~ZXwN|gaWG38cXOWlmM20&|ZPQ-$9rGXA~SttWU!2NG6v5bYaW2 zuf+F&)~Br^IhA7N^v_cWcMWq!E*yOHoDS@L6_W~MmU&@tqUln*4oBDEJSbtv(9d4R z3d$wHv#T4c(SJ>he8kHkl`vsrg^Y5E)k*hNS>NR?1j~g0&4>e$A+az3Rwt`nVdTUB zgJDL52Q40)tw_$hA+=jMDv8kPn+fbT+{#~M4r_F zMsZ2&iUi%n%O6xCtc_n;v8Q!q#p}rFFDU*!Eh&qRt6!_ zt&D}6IbS3H_VKS0<#_e!FMDTiU+=v-eD;~0SBq4HDw!6!Q=KabrpMp-&Oj??$8$8ax6H3t2Bb ziVX0ow5alKkHsj-L!WTT`h|9ZoUZiyO6>>;Jpiyi-8E-LH%+WidSPRx%FHODTZ8{` zA!FwT;Z#9!mr9V5Y8v2&n7+U-(Llwby6R%t!G9K|i@RJQylaqMkbZ2#Xv+<7<% zxF-MK?ZaN?2_Y@i6H zuHif2`OE$2orKM%nB2nCQv;_i(q<|~GsPC!eN|!0fyPcL2b@T8e3R==X}Z;{1F>K# z8+8~DqiF#5v*wA&ICG$$WdR4!UMMl94Kz1q)Jd%?1-P7_VPDamx`fq_2n`6YVEKw= ziOnwpN6DrVgviCtN=QA5x(>#OS^`dz9q{7_#(?;Jre@UX{M~itgz8G9z^=tEsQbXg z`9*3?cMZE(su+^O#ZtO(7CD#Hd>Q$vLluDNrwVni2#J9}=7Hv@iep{2CKhm(-=jG zEANUm;yy4dXsl#7USC#;j=;*(O92+TomDBN#|jYH6`8U(kuJK*6syNPSyF(OoB%xn zwQM46)Gaa;X-XYJqm~&%&|xLL^;V2T8e}vA!nD$* ziIGZ6xqXQ}!3%q^noeWT{DkpMBAr=K%@W-rnvlvkLZ`+~ZOVjDdE~#pP9ou5+)l(f z@>_*e<4D%`9};qYxvMcrj8Fx-1eLX6)hlJ)C~PWr+6~xRUoJt$auH2HNjgG22^iB5 zCGXjeA1CS9kHC=&h1shly$pjuP!@WHx8o15S+|mKvMU%0`+smFgp3BIOrjwH7iH#N>O|o? z;Sn8^G)6&`6={s5n$Tyag~_N+t#-BOfiT1P23}u>3!q8>Xx{i}GuP#ilA|jwhep&_ zR*1QD817|k2BiqX_8evzJ>r56ou(ty$Hb<4QWbK>vDoe)>;v`yP+stBYu?6cIcTi# zR>;jnOqfTB!LZUUT~*lcD9M+XI8`XDV(z^ZIk4c~k7du}r=2k9v#EC3`|%m_>d!jM zyGzg^3O>2hvfvto`NR5XL+9FgX@hY4XxHVSZPd~AKFZnYd*GD=QxIIP0%9co`I%=z z?h$kB6?-HrZIat1T8Sww13G)ihC7e8HXkkTdgv<7K{izzM@}#4K4t08&0uN&kf7mfE?TfR>5+3m*$a89Q#!&M0Cqn4kyeg^vh_Ff2V`J}t>;w3w_ z-h|*EiXIXr(5)ZZa=DIru8R?Q;8kt0~EA~oY+&A41>pRTN6XFyQKP$y2N2%dAE58)?} z`_Pc0=z?J1I^SDCqv497GNJCk=D8Gla1=*zlXg3?^9EiOu@Df%a+&1U(&IHV{Q`c- z$g!UJLf92j(L~5q-ijXw=mM!yPnu~VwY+c~=^l>ZmX&bK|P2UrNCE9HC}1`t!$D_acYJe zEN5nv$_7?^3e2b+(G5u>VkavykHfs|Rz$c#yD)jT$O&XZB3%DUIj$1mV`K3lha71a ze)i7$c}5|!FV>G%v_ z)vkf&2v3%aaYkXOddf*Zi4t0L9d(Rr2TV+?N22`u#T(qrr!weRN7t*?p_hH4JX)-b z<%s(t;MCH^E-jr(ZHg9D4;Nk}f)VC$CB1I9P$QOo4kYGWD04xW4vo7@T6zxMbO0$H zS#1oCNmW`xIwKGdfSxLxq%Iq05@JJ)`%+y?epVQ_53{z9LH!uD-{si-z8RJPs#3yw z0Hyts=MCD~RAD^^U8!sXai9ezfCL_ZjN~=Y0||o1n&1Q7;(kDvhBGOift845bkk8jCk2soU6`#GTyenh4cg70+x3Ed%R3)7tqzwu;2 z;&=qmlj*HVn@Ereu!`~|LGnn{NG)p)Biul8#XOwhDAr|>KTJ){@RG-RF6&$Xi)3Kq zQEo~RT_GEy8;(|1Jg||=zGc%e=O~Hy{w<&}8_L1Y9sFP`2DeXgHe>+DizQ zLZnLtA_ca(GEg~gSo<{2dYSK|$pIYXVPar2cUjrSlSU5ugaZL;lO>GlNTxaB^ZZJ; zHb|!0-r~@mF56Lbbi-8Oy;*ebR^)x)jh6-r?2BOZ6?kt&;2*hzwYIBtmk5&4IAgc* zuvc-=W+FYyt15DpFnBZN|5OJ@pL;Z!N#2#|GVbLXO}Pgu2sejd7vg5gd`Ol`JT+QC znuLg3M!z~v_iI7~fg+N;ea2#(q-fy-txyK>J&5iRbDnPleM#n+1|}`ikcF21ILJbO z@s7hR*Ns$#kt87_R|n4?!7<5A&G`qHXLbn!q7ByABhGo0#G=poF{PO9778V;6!%eWXvVytQSk`4k ze94-iEGPnu;Jp$#1GWd0p~5hlz%CCUA2Nh+NuwVpU|CX@2}@Cn9v5|*+yX=nAhtEY zAeGPy8N;OkR2-xw|yj``h{3qt_RQXD{~l58s~epPU|^4`e`fDiB?m zX+#flbwCHurLtpkKo;}3RAL-yrvJ@o@BHn-(b-@bXxcR6!0;Ht#1S*>$_G{jPAs{^ z$aAu~^a3Dg5pNdKQUD5QM~03dHYo)j!)_teSFUm3b!c-T9_%XIn`7`aj+1E${B%4S zr^7*a2@Jw8J)E>LkntGd4X8d?Nf2tQnkUKX($Z1Ro)FRrQ=`Q|2YG}mE6Q#Mr(bz? z(=G=8T@>dz-AVxg&>2WiZ_Ua~$+E~wgomsW5tDcLqj>N@a5=DL%?Q1Ju$INiK-1QFQ5zuo=6OR!( z;Z-Qo40FW~viBiG%B~HDXqHFD47~pl!rkLs^Tat}3}TQBr_!iFT4PFlEseeq(L;oB z@AN2>mqHlB0bcXtB)%PE&Uf%dz$V82C>(MGMOW!EGw;9upa0kUzqi&Ae-K;?;y`>L zx0k*?0SuLw$uJv_HL5?XuCbzh0MY^46jV~zOFvCTr2GzG%&;XvP2bb=Ta!n3F46xP zi7kx32}fe-|BJYQfH^PGs@M2v@bUBR zw?JV3;AJNIKP%zq(m?;$H%jz>eS2qTLI3Abl>dm#*Dk3&?*nx6a1zT`CkJni58s}j zygA!HeEaI;;P70hN9r4jc~)eiv)q?$Eza-PFXs7&yS&IJyT-xYy&&{QaRNX$*WnCMbBBTp8(BEmjRQ2z}HZq8#bqPKt&O zLVujZ!8w@msTD?3=v#egTXl&ep)j^f!7X*hA{}XLoa)MJjDM4l(ZbZ%yVqrZe&gYC zG4iJN&j`NqOu%_!cIYT*>&kW6;hA}5}mo2w%brQyL z!RgM>=uv^cbQ;EUXz)!;xEaAdgk=?MZ)wk#>(pd}@ntxgCe!SIFHR*q8NFx8$Tb4` zi85lBOLSyg0l$O=rUk_(z<75!%xNrsQQIb!u)X=uOw&U`ZX0EwXZA$xx3 zs!Wi;1|Ge7%3i{GK8nF^{m~R8Dn@B<3hs^uV_NwQEf0UdJaEMXxSkca4cmLT+A~UP z^rMmD#QWqunuIa_pPRyev*3*XW`!jFTjAx3;E_WXvw3cwEGb4=Tb2?n_00~p-^~(( z@PM&*6hHq?+_skbD!O);qe|=clZ1DqzM#j9d-3ea;uU`o9tsx=4tOwKVwiB}EQUMzV)gHhEtZXOCh!lccWG)N}9u3x!3vPU!A|FR!5vjC0u z{|#IJyScu#$p1T+V%q=pvr|6O$~Ls>;tY#eVy;Z;`pmRr#kWcw<)XdZt`BxXy-Dp75`QIt||7>h*E&PAxQk>_XRVp){b~03% zT%VMfWmE+-Y%GxNx_?#Mt!$>6LF2G&F;yp5ohs{Jy*2dqET%xJPClKMr;-KcBW zmnjFcS$n`0D64tB`BL|jWL6l!*MkBJu4C|AVAU(1e+p<=Q|OI%zV=mVI{)D8W8yzQ z+1gpie{(73`G4Vu(KHnosUK$%Aoz8?H=#xctdp;aYrI=bm+xu#yzjjUX)oT0^fJi| z7jAf*b6fC3srfp|C!L~G37WoT^PRCD#(l42xMSZWVem}(96#^doz*;#@zw8&=VksnkV&;Ju!|FgccwXpxrrI_cx&Iwp~3gf${v^)3%dF&OXSY4y5NA98?*JOl< zAmETygY;-|*RK@tX52_Ek-%0u5ca4XkF@{MaW;b>6GqCwj{rs8&%PK5!6p!kR z!m_H`i$Xus>+<1&Z9tqvl%uf5<_(n{8EMdL6K)GPZNuDN*=0WYJmpnx02uiH0)x$+ z(S5F;BHCWh`}T%YA%H5&Sapib1}h*8L5~fj{$DwILsQf@Hui4CXgN&Vd`L{E`GmGs zzq}7^zl7&~OXj%h-?jBi zz7EEy?2T5l9c8t;=fzj)pfO=CQc&Wf^5i^+#fKf-;b@RkAM&o`BF&zbu8C*AtvlS@ zOVy4t`$H0Q8kSR8jvU45Cl-W8k)?OxLuN#*QN)#A0E zU7s;%CNJND+X^{~2LM5(l@b-mlT5fN2I8(vO{Rs-nq#45ehhH5j8`g!4fgS=5cgeMAe(ibr&fh-@Cfj zK_@WdQY{UhFV&in83urAvWZ99q@0!HdDwLOblseO`#S`t{*3x0+R=0C)?Lu{yO&1# z&$-XOApPHbyuMSS|J$1j`ah3yNBYn2Lk33oCy2p?PwQ`iuJ%k5QuwhGhp6Vhp!Z*~ z^vyNB#_Upn0_(o6)-z{oE9y@tG+& z<0B~^KdNDWGubu0XB|ne^@q#-ig8p|KK3VaiHh-sBCEP6miw;Buk$i2aqlPT#dVsz zuV&)g()mhZ#ZTj-E7EXe=YYX?2VtdeZTj}&iiopmDzq(=9Rk!`_0@Zy#!xV0BZ_&a z#_Bjo!P0#|tZP=#`d2LT{W=>Zqr>mv9AqLle zwhuj#jSOir`mR~aaNK=k7TUBCh#b7$w0W&*X1VWJhuZ!hhc_S;j#7~|RNUCpFe`S3 zIES1(NnBLv+BI!z54Au;eKP(LeSycOB?HuubF_yY%IluNPzI&;r|JR`Y z{%N-VY?S_2lUrfCK;1Y?FdjbHP#cV$D%eiWV(Vx-CG zwP5tnI)9ERtEm+qNO!X{CJ?(}{sxbpm(B9TfJluoe)qhuM#fQrs9MHXpKH2gsjlYU zv8UX00pHgw2Kwuir-Ov)N}CDD^7dEWCvx4ezt%vkIiQ1bGkq-PrK){3 zA$2b}J7c`TD5?@owXdBM^G@p;20x{m!M z6QAE&9f;YLrLlJ`nq_4m#EZYf<}V zx2zkbF;gKo0b9G>ZK^k`9f+EyzxJ_iU7Y;?y*)n6(xm^}d}7;wx3(7gzxfn*{Qq4t zzqSp(Uo~JR+TsE*?j{Fd45E`MR~t?5m^D5vx@6Kg=ePfBQO#IpNXEFlXG> z&M3^afbd2Nimt!x4Q@fDbTJn>`o`p;A3ZhK`&p|gY7vxK)p%9y=xQw8e_iTGYhOC^ z!Mf+c^j0pV@}=o0?be5yh=$f%G*UiD&cPSV(3=c$uV|1Bqp%fH&Pe3E^NJdPg(&`N zm}%`Zn$_aLY?EfTFF!1veoNQuAHJ03KceXTzn%!fJh`VDu*v@aq;&u9Jl=S`xc}!- z^vid}^E2<_b|TI(sR3WUc=O_eN4u1Qh>t3=T&u8Hh)VSp{fvXR9W1Ua7Ydp? zhl0t9wUop2gPM^15(6*613(IMarOH8SRMuFeuh8jcwG9gzzahU7s*NQRhsBO?T{}^ z|FvwOgunr@CG;!_8J6!Z1o=@eAO}sfF-k3379tr1>?NzN@Ze)q-pPLTl}=@bB>= zX{P`A^_PzS+T1Al|8HzA@PFr0X64YGdhv_%xBDlrUmU%B`{L*rgE!@6uu9u#HLv_u z>eLTk{{kP%^qduMAMd?+z5g@3d~|m5`qkm3*fsME`I6^uFoen&bbsO8D

kIwQT#Ca} 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�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 KU*3BF)*k?wX-`rB literal 0 HcmV?d00001 diff --git a/.campaign/tgz/antelopejs-interface-core-6a8b526.tgz b/.campaign/tgz/antelopejs-interface-core-f7df541.tgz similarity index 100% rename from .campaign/tgz/antelopejs-interface-core-6a8b526.tgz rename to .campaign/tgz/antelopejs-interface-core-f7df541.tgz diff --git a/.campaign/tgz/ranges.tsv b/.campaign/tgz/ranges.tsv index 6a4c29c..1e410e8 100644 --- a/.campaign/tgz/ranges.tsv +++ b/.campaign/tgz/ranges.tsv @@ -4,3 +4,6 @@ @antelopejs/interface-core package.json devDependencies ^0.2.4 @antelopejs/interface-core package.json pnpm.overrides @antelopejs/interface-core pnpm-workspace.yaml overrides +@antelopejs/core package.json devDependencies ^1.16.0 +@antelopejs/core package.json pnpm.overrides +@antelopejs/core pnpm-workspace.yaml overrides diff --git a/README.md b/README.md index b5b9458..c5b7f32 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ The package declares these `@antelopejs/*` peer dependencies: | Package | Supported | Tested with | | -------------------------------- | ----------------- | -------------------------------------------------------------- | | `@antelopejs/interface-api` | `>=0.0.15 <1.0.0` | `0.0.15` | -| `@antelopejs/interface-core` | `>=0.2.4 <1.0.0` | `file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz` | +| `@antelopejs/interface-core` | `>=0.2.4 <1.0.0` | `file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz` | | `@antelopejs/interface-database` | `>=0.1.11 <1.0.0` | `file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz` | diff --git a/package.json b/package.json index 7336c71..0f679c7 100644 --- a/package.json +++ b/package.json @@ -142,9 +142,9 @@ "reflect-metadata": "^0.2.2" }, "devDependencies": { - "@antelopejs/core": "^1.16.0", + "@antelopejs/core": "file:.campaign/tgz/antelopejs-core-184eb90.tgz", "@antelopejs/interface-api": "^0.0.15", - "@antelopejs/interface-core": "file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz", + "@antelopejs/interface-core": "file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz", "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz", "@antelopejs/tooling-configs": "^0.2.17", "@types/chai": "^4.3.20", @@ -174,7 +174,8 @@ "pnpm": { "overrides": { "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz", - "@antelopejs/interface-core": "file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz" + "@antelopejs/interface-core": "file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz", + "@antelopejs/core": "file:.campaign/tgz/antelopejs-core-184eb90.tgz" } }, "antelopeJs": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12425c4..2bf65d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,18 +6,19 @@ settings: overrides: '@antelopejs/interface-database': file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz - '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz + '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz + '@antelopejs/core': file:.campaign/tgz/antelopejs-core-184eb90.tgz importers: .: dependencies: '@antelopejs/interface-core': - specifier: file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz - version: file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz + specifier: file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz + version: file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz '@antelopejs/interface-database': specifier: file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz - version: file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz) + version: file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) randomstring: specifier: ^1.3.1 version: 1.3.1 @@ -26,11 +27,11 @@ importers: version: 0.2.2 devDependencies: '@antelopejs/core': - specifier: ^1.16.0 - version: 1.16.0 + specifier: file:.campaign/tgz/antelopejs-core-184eb90.tgz + version: file:.campaign/tgz/antelopejs-core-184eb90.tgz '@antelopejs/interface-api': specifier: ^0.0.15 - version: 0.0.15(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz) + version: 0.0.15(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz) '@antelopejs/tooling-configs': specifier: ^0.2.17 version: 0.2.17(eslint-plugin-perfectionist@5.11.0(eslint@10.10.0(jiti@2.7.0))(typescript@5.9.3))(knip@6.34.0)(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001)) @@ -88,9 +89,10 @@ importers: packages: - '@antelopejs/core@1.16.0': - resolution: {integrity: sha512-dj1R8xPgLzkTksfenPNh3kX1fffRnWktielk1yad/4/1wTEMhkPSx1WQ/4QI98d95L6Th0EMGTV/U4o84p/2yw==} - engines: {node: '>=20.19'} + '@antelopejs/core@file:.campaign/tgz/antelopejs-core-184eb90.tgz': + resolution: {integrity: sha512-CsiXLalRIIxX5YHjMUVDlz0+zBGSeNjSNPdBTgHwVsWpT1bIeG8izJmbCgpi5tIs8rvxkdlEZ6eKa7yH1GK8CA==, tarball: file:.campaign/tgz/antelopejs-core-184eb90.tgz} + version: 1.20.0 + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true '@antelopejs/interface-api@0.0.15': @@ -99,8 +101,8 @@ packages: peerDependencies: '@antelopejs/interface-core': '>=0.2.4 <1.0.0' - '@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz': - resolution: {integrity: sha512-ur2KLvtD8h4SCZ0VSoQDc1pg+E9Bk/py3EW3qHHdoo3i6RvKWrrDKsX2khVdqQESuF2QMc1sJ6B+7ISjx5TvpQ==, tarball: file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz} + '@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz': + resolution: {integrity: sha512-ur2KLvtD8h4SCZ0VSoQDc1pg+E9Bk/py3EW3qHHdoo3i6RvKWrrDKsX2khVdqQESuF2QMc1sJ6B+7ISjx5TvpQ==, tarball: file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz} version: 0.4.0 '@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz': @@ -2792,9 +2794,9 @@ packages: snapshots: - '@antelopejs/core@1.16.0': + '@antelopejs/core@file:.campaign/tgz/antelopejs-core-184eb90.tgz': dependencies: - '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz + '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz '@clack/prompts': 1.8.1 '@types/proper-lockfile': 4.1.4 commander: 13.1.0 @@ -2807,17 +2809,17 @@ snapshots: semver: 7.7.4 typescript: 5.9.3 - '@antelopejs/interface-api@0.0.15(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz)': + '@antelopejs/interface-api@0.0.15(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz)': dependencies: - '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz + '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz - '@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz': + '@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz': dependencies: reflect-metadata: 0.2.2 - '@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz)': + '@antelopejs/interface-database@file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz(@antelopejs/interface-core@file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz)': dependencies: - '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz + '@antelopejs/interface-core': file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz '@antelopejs/tooling-configs@0.2.17(eslint-plugin-perfectionist@5.11.0(eslint@10.10.0(jiti@2.7.0))(typescript@5.9.3))(knip@6.34.0)(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e1c8fb2..fea5f6a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ overrides: "@antelopejs/interface-database": "file:.campaign/tgz/antelopejs-interface-database-724ff72.tgz" - "@antelopejs/interface-core": "file:.campaign/tgz/antelopejs-interface-core-6a8b526.tgz" + "@antelopejs/interface-core": "file:.campaign/tgz/antelopejs-interface-core-f7df541.tgz" + "@antelopejs/core": "file:.campaign/tgz/antelopejs-core-184eb90.tgz" From 4a959c79d12e6b9d3c894b643c151a5008f2e8da Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 3 Nov 2026 01:12:44 +0100 Subject: [PATCH 17/45] chore: merge main into campaign/integration (#77) * docs(readme): use one Discord invite (#68) * docs(github): send questions and ideas to Discord (#76) * fix(fixtures): let two processes register a fixture on an empty table (#74) * test(fixtures): cover two processes registering a fixture at once * fix(fixtures): let two processes register a fixture on an empty table * docs(fixtures): keep to what the fixture insert checks * Merge commit from fork * fix(model): keep modifier data on a partial update of another field A modifier event creates an empty `_internal` on the instance. The update writes that key as one value, so it erased what the row stored there. * fix(hash): hash values with scrypt by default `@Hashed()` now stores `$scrypt$ln=15,r=8,p=1$$`. Rows in the earlier digest format still verify, and `isHashOutdated` flags them so that a caller can write them again after a successful test. `testHashAsync` runs the test on the thread pool. An explicit `algorithm` keeps its digest format. * fix(hash): raise the default scrypt cost to p=3 N=2^15, r=8, p=3 is the minimum OWASP lists for that N. It takes about 150 ms and the same 32 MiB as p=1. * test(hash): give suites that spend scrypt runs a longer limit At p=3 a test with several runs passes mocha's 2 s limit on a busy machine. * fix(hash): refuse a stored hash outside the cost bounds A stored hash names its own cost, so a tampered row could ask for an expensive run. Salt and key must take 16 to 64 bytes, the memory 128 MiB at most and p 16 at most. A row outside that, or with an N that r cannot hold, verifies nothing and no longer throws. * chore(release): v0.1.12 * docs(hash): mark the stored-hash upgrade snippet as a fragment --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/config.yml | 4 +- CHANGELOG.md | 25 ++ README.md | 2 +- docs/3.table-modifiers.md | 51 ++- package.json | 2 +- skills/database-decorators-interface/SKILL.md | 6 +- src/database.ts | 28 +- src/model.ts | 28 +- src/modifiers/hash.ts | 282 +++++++++++++- src/modifiers/internal/records.ts | 5 + src/tests/database.test.ts | 37 ++ src/tests/hash_upgrade.test.ts | 140 +++++++ src/tests/helpers/hash-rows.ts | 40 ++ src/tests/modifiers/hash.test.ts | 359 ++++++++++++++++++ src/tests/partial_update.test.ts | 42 ++ 15 files changed, 1012 insertions(+), 39 deletions(-) create mode 100644 src/tests/hash_upgrade.test.ts create mode 100644 src/tests/helpers/hash-rows.ts create mode 100644 src/tests/partial_update.test.ts diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 9e41a21..9ff80ed 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,8 +4,8 @@ contact_links: url: https://antelopejs.com/ about: Read the documentation before opening a usage issue. - name: Questions and ideas - url: https://github.com/AntelopeJS/antelopejs/discussions - about: Ask for help or discuss an idea with the community. + url: https://discord.gg/g4pFt6eXqu + about: Ask for help or share an idea in #support on the AntelopeJS Discord. - name: Security vulnerability url: https://github.com/AntelopeJS/.github/blob/main/SECURITY.md about: Read the private reporting instructions. Do not open a public issue. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e87a4e..69303ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## v0.1.12 + +[compare changes](https://github.com/AntelopeJS/interface-database-decorators/compare/v0.1.11...v0.1.12) + +### 🩹 Fixes + +- **fixtures:** Let two processes register a fixture on an empty table ([#74](https://github.com/AntelopeJS/interface-database-decorators/pull/74)) + +### 💅 Refactors + +- Clear the residual ratchet findings ([#46](https://github.com/AntelopeJS/interface-database-decorators/pull/46)) + +### 📖 Documentation + +- **readme:** Use one Discord invite ([#68](https://github.com/AntelopeJS/interface-database-decorators/pull/68)) +- **github:** Send questions and ideas to Discord ([#76](https://github.com/AntelopeJS/interface-database-decorators/pull/76)) + +### 📦 Build + +- **deps:** Bump @antelopejs/tooling-configs to 0.2.17 ([#49](https://github.com/AntelopeJS/interface-database-decorators/pull/49)) + +### ❤️ Contributors + +- Antony Rizzitelli ([@Upd4ting](http://github.com/Upd4ting)) + ## v0.1.11 [compare changes](https://github.com/AntelopeJS/interface-database-decorators/compare/v0.1.10...v0.1.11) diff --git a/README.md b/README.md index c5b7f32..ca792a6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

diff --git a/docs/3.table-modifiers.md b/docs/3.table-modifiers.md index 436588b..dbdaa54 100644 --- a/docs/3.table-modifiers.md +++ b/docs/3.table-modifiers.md @@ -59,7 +59,7 @@ class UserCredentials extends Table.with(EncryptionModifier) { ### HashModifier -The `HashModifier` stores a salted one-way hash of field values. Unlike encryption, hashing is irreversible -- you cannot retrieve the original value. Its default SHA-256 hash is fast, not a password-hardening function; use a dedicated password hashing library for production credentials. +The `HashModifier` stores a salted one-way hash of field values. Unlike encryption, hashing is irreversible -- you cannot retrieve the original value. By default it stores a scrypt hash, a memory-hard function fit for passwords. ```typescript import { @@ -73,39 +73,60 @@ class User extends Table.with(HashModifier) { @Field("string") declare email: string; - @Hashed({ algorithm: "sha256" }) + @Hashed() @Field("string") declare password: string; - verifyPassword(plainPassword: string): boolean { - return this.testHash("password", plainPassword) === true; + verifyPassword(plainPassword: string): Promise { + return this.testHashAsync("password", plainPassword); } } ``` #### How It Works -- A random salt is generated and stored in the field's metadata on first write. -- The value is hashed with the salt using the specified algorithm. -- The `testHash` method (added by the mixin) hashes a candidate value with the stored salt and compares the result. +- Each write generates a random salt and stores `$scrypt$ln=15,r=8,p=3$$` in the field. The string holds the algorithm, the cost, the salt and the hash, so the cost can change later. +- Assigning the stored hash back to the field, or calling `lock` on a loaded row, keeps the hash as it is. +- `testHash` and `testHashAsync` read the cost and the salt from the stored string, hash the candidate and compare in constant time. +- A test spends one scrypt run whatever the row holds, so the time of a refusal does not tell whether a hash exists. +- A write is synchronous, because a property setter cannot wait. Assigning a value blocks the event loop for one scrypt run, about 150 ms and 32 MiB. #### Options -| Option | Type | Default | Description | -| ----------- | -------- | ---------- | ------------------ | -| `algorithm` | `string` | `"sha256"` | Hashing algorithm. | - -`LocalizationModifier.fallbackLocaleOf(TableClass, field)` returns a field's `fallbackLocale`, or `undefined` when the field is not localized or declares none. Generated forms use it to require a value in that locale. +| Option | Type | Default | Description | +| ----------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `algorithm` | `string` | none | Digest to store instead of scrypt, any name `createHash` accepts. A digest is fast to guess: use it only for non-secret fingerprints, never for credentials. Rows stay in the bare digest format. | -#### Mixin Method +#### Mixin Methods -The `HashModifier` mixin adds a `testHash` method to table instances: +The `HashModifier` mixin adds these methods to table instances: ```typescript fragment testHash(field: string, value: unknown): boolean +testHashAsync(field: string, value: unknown): Promise +isHashOutdated(field: string): boolean +``` + +- `testHashAsync` returns whether `value` matches the stored hash. It runs scrypt on the thread pool, so use it on a server. +- `testHash` returns the same result and blocks the event loop while scrypt runs. +- `isHashOutdated` returns whether the stored hash differs from what the field writes today. It is always `false` on a field with an `algorithm`, and when nothing is stored. + +#### Upgrade Stored Hashes + +A row written before scrypt became the default holds a bare salted SHA-256. It still verifies, and `isHashOutdated` returns `true` for it. The same applies to a hash of an earlier cost. To upgrade a row, assign the value after a successful test, because that is the only moment the plaintext is known: + +```typescript fragment +if (await user.testHashAsync("password", password)) { + if (user.isHashOutdated("password")) { + user.password = password; + await userModel.update(user); + } +} ``` -Returns `true` if `value`, when hashed, matches the stored hash for the given field. +To move a credential off `@Hashed({ algorithm: "sha256" })`, remove the option. Its rows keep verifying and are flagged for upgrade. A row written with another digest verifies only while the field declares that digest. + +`LocalizationModifier.fallbackLocaleOf(TableClass, field)` returns a field's `fallbackLocale`, or `undefined` when the field is not localized or declares none. Generated forms use it to require a value in that locale. ### LocalizationModifier diff --git a/package.json b/package.json index 0f679c7..29621ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@antelopejs/interface-database-decorators", - "version": "0.2.0", + "version": "0.1.12", "description": "AntelopeJS decorators for database tables and typed data models, with encrypted, hashed and localized fields", "keywords": [ "antelopejs", diff --git a/skills/database-decorators-interface/SKILL.md b/skills/database-decorators-interface/SKILL.md index 451d648..28ccc14 100644 --- a/skills/database-decorators-interface/SKILL.md +++ b/skills/database-decorators-interface/SKILL.md @@ -110,7 +110,7 @@ class Chef extends Table.with( EncryptionModifier, LocalizationModifier, ) { - @Hashed() declare passphrase: string; // one-way; test via instance.testHash("passphrase", value) + @Hashed() declare passphrase: string; // one-way scrypt; test via await instance.testHashAsync("passphrase", value) @Encrypted({ secretKey }) declare supplierCode: string; // transparent encrypt/decrypt (autolock + autounlock) @Localized() declare motto: string; // read AND write via instance.localize("en", ["motto"]) @CreationTime() declare hiredAt: Date; // set on insert, stripped from updates @@ -124,7 +124,9 @@ class Chef extends Table.with( is loaded once before decorators run (e.g. `import "@antelopejs/interface-database-decorators"`). - `Hashed`, `Encrypted`, and `Localized` require the matching modifier class in `Table.with(...)`; `CreationTime`/`UpdateTime` are event-only and need no mixin. - Modifier ordering matters: `attachModifier` throws if you stack a modifier after a one-way modifier (e.g. anything after `@Hashed` on the same field). -- Hashed fields cannot be read back or queried directly — only equality-tested with `testHash`. +- Hashed fields cannot be read back or queried directly — only equality-tested with `testHashAsync`, or `testHash`, which blocks the event loop for one scrypt run. +- A row can hold an older hash that still verifies. After a successful test, when `instance.isHashOutdated(field)` is true, assign the tested value and save the row. +- `@Hashed({ algorithm })` stores a fast digest instead of scrypt: use it only for non-secret fingerprints, never for credentials. - Localized fields must be written through a localized instance (`instance.localize(locale).field = ...`); values assigned before `localize()` are held in floating state and silently dropped on insert/update. - A localized value `""` is stored as no value: reads and queries in that locale get the `fallbackLocale` value. diff --git a/src/database.ts b/src/database.ts index 44c4c5a..93cbfa4 100644 --- a/src/database.ts +++ b/src/database.ts @@ -98,14 +98,32 @@ async function insertFixtureData( return; } const fixtureData = await generator(tableClass); - const rows = toFixtureRows(fixtureData, tableClass); + const rows = toFixtureRows(fixtureData, tableClass).map((row, position) => + withStableId(row, tableName, position), + ); if (rows.length === 0) { return; } - await schema - .instance() - .table(tableName) - .insert(rows.length === 1 ? rows[0] : rows); + const table = schema.instance().table(tableName); + try { + await table.insert(rows.length === 1 ? rows[0] : rows); + } catch (error) { + if ((await table.count()) === 0) throw error; + } +} + +/** + * Keys a fixture row that declares no id by its table and position, so two + * processes starting on an empty table write one row between them. + */ +function withStableId( + row: StoredRow
npm License -Discord +Discord Docs

, + tableName: string, + position: number, +): StoredRow
{ + return row._id === undefined + ? { ...row, _id: `${tableName}:fixture:${position}` } + : row; } function toFixtureRows( diff --git a/src/model.ts b/src/model.ts index b95d322..3f26f32 100644 --- a/src/model.ts +++ b/src/model.ts @@ -20,6 +20,10 @@ import { } from "./modifiers/common"; import { isRecord } from "./modifiers/internal/kinds"; import { boundSchema } from "./internal/bound-schema"; +import { + type InternalRecord, + isEmptyInternal, +} from "./modifiers/internal/records"; import { type Constructible, DatumStaticMetadata, @@ -272,13 +276,19 @@ export function BasicDataModel( if (parsed.id !== undefined) { const instance = Model.fromPlainData(parsed.data); triggerEvent(instance, "update"); - return this.table.get(parsed.id).update(toDatabase(instance)).run(); + return this.table + .get(parsed.id) + .update(withoutEmptyInternal(toDatabase(instance))) + .run(); } const instance = Model.fromPlainData(parsed.data); triggerEvent(instance, "update"); const id = parsed.data[primaryKey]; assert(id !== undefined, "Missing primary key in object update."); - return this.table.get(id).update(toDatabase(instance)).run(); + return this.table + .get(id) + .update(withoutEmptyInternal(toDatabase(instance))) + .run(); } /** @@ -319,6 +329,20 @@ type ParsedUpdate = options?: ValidateOptions; }; +/** + * Leaves out an `_internal` with nothing in it. The update writes that key as + * one value, and a modifier event creates it empty: writing it would erase what + * the row stores there. + */ +function withoutEmptyInternal( + record: R, +) { + if (record._internal && isEmptyInternal(record._internal)) { + delete record._internal; + } + return record; +} + function makeValidationError(errors: FieldError[]): Error { const summary = errors.map((e) => `${e.field} (${e.message})`).join(", "); return Object.assign(new Error(`validation failed: ${summary}`), { errors }); diff --git a/src/modifiers/hash.ts b/src/modifiers/hash.ts index 33ac32b..f23cce8 100644 --- a/src/modifiers/hash.ts +++ b/src/modifiers/hash.ts @@ -1,23 +1,202 @@ import { generate } from "randomstring"; -import { createHash } from "node:crypto"; +import { + createHash, + randomBytes, + scrypt, + type ScryptOptions, + scryptSync, + timingSafeEqual, +} from "node:crypto"; import { MakePropertyDecorator } from "@antelopejs/interface-core/decorators"; +import { getMetadata } from "../common"; import { attachModifier, MixinSymbol, + ModifiersStaticMetadata, OneWayModifier, testValue, } from "./common"; +import { + dataByKey, + fieldsByKey, + getInternal, + withRef, +} from "./internal/records"; +import { isString } from "./internal/kinds"; type Options = { - /** Hashing algorithm (for supported algorithms refer to {@link createHash}) */ + /** + * Digest to store instead of the default scrypt hash (for supported names + * refer to {@link createHash}). A digest is fast to guess: set this only for + * non-secret fingerprints, never for credentials. + */ algorithm?: string; }; +type HashInput = Parameters["test"]>[1]; + +type ScryptCost = { ln: number; r: number; p: number }; + +type ScryptArgs = [ + password: string, + salt: Buffer, + keylen: number, + options: ScryptOptions, +]; + +/** One test of a candidate: the KDF run it spends and what decides the result. */ +type HashCheck = { + run?: ScryptArgs; + verdict: (derived: Buffer | undefined) => boolean; +}; + +const SCRYPT_COST: ScryptCost = { ln: 15, r: 8, p: 3 }; +const SALT_BYTES = 16; +const KEY_BYTES = 32; +const DECOY_SALT = Buffer.alloc(SALT_BYTES); +const LEGACY_DIGEST = "sha256"; + +/** Most scrypt memory (128 * N * r bytes) a stored hash may ask for. */ +const STORED_MAX_MEMORY = 128 * 1024 * 1024; +const STORED_MAX_P = 16; + +const CURRENT_PREFIX = `$scrypt$ln=${SCRYPT_COST.ln},r=${SCRYPT_COST.r},p=${SCRYPT_COST.p}$`; + +/** Salt and key take 22 to 86 characters, so 16 to 64 bytes. */ +const STORED_SCRYPT = + /^\$scrypt\$ln=([1-9]\d?),r=([1-9]\d?),p=([1-9]\d?)\$([A-Za-z0-9+/]{22,86})\$([A-Za-z0-9+/]{22,86})$/; + +function unpaddedBase64(bytes: Buffer) { + return bytes.toString("base64").replace(/=+$/, ""); +} + +function scryptArgs( + password: string, + salt: Buffer, + keylen: number, + { ln, r, p }: ScryptCost, +): ScryptArgs { + return [ + password, + salt, + keylen, + { N: 2 ** ln, r, p, maxmem: 2 * STORED_MAX_MEMORY }, + ]; +} + +function scryptAsync(...[password, salt, keylen, options]: ScryptArgs) { + return new Promise((resolve, reject) => { + scrypt(password, salt, keylen, options, (error, key) => { + if (error) reject(error); + else resolve(key); + }); + }); +} + +function parseScrypt(stored: string) { + const match = STORED_SCRYPT.exec(stored); + if (!match) return undefined; + const cost = { + ln: Number(match[1]), + r: Number(match[2]), + p: Number(match[3]), + }; + if ( + 128 * cost.r * 2 ** cost.ln > STORED_MAX_MEMORY || + cost.p > STORED_MAX_P || + cost.ln >= 16 * cost.r + ) { + return undefined; + } + return { + cost, + salt: Buffer.from(match[4], "base64"), + key: Buffer.from(match[5], "base64"), + }; +} + +function digestOf(algorithm: string, salt: string, candidate: string) { + return createHash(algorithm).update(salt).update(candidate).digest("base64"); +} + +function digestMatches( + stored: string, + salt: string | undefined, + algorithm: string, + candidate: string | undefined, +) { + if (!salt || candidate === undefined) return false; + const expected = Buffer.from(stored); + const actual = Buffer.from(digestOf(algorithm, salt, candidate)); + return expected.length === actual.length && timingSafeEqual(expected, actual); +} + +/** + * Plans the test of a candidate from what the row holds. A field that uses + * scrypt spends one scrypt run in every case, so the time of a refusal does not + * tell a missing or an older hash from a current one. + */ +function planCheck( + row: { stored: string; salt: string | undefined }, + algorithm: string | undefined, + candidate: string | undefined, +): HashCheck { + const hash = parseScrypt(row.stored); + if (hash && candidate !== undefined) { + return { + run: scryptArgs(candidate, hash.salt, hash.key.length, hash.cost), + verdict: (derived) => + derived !== undefined && timingSafeEqual(derived, hash.key), + }; + } + const matches = digestMatches( + row.stored, + row.salt, + algorithm ?? LEGACY_DIGEST, + candidate, + ); + return { + run: algorithm + ? undefined + : scryptArgs(candidate ?? "", DECOY_SALT, KEY_BYTES, SCRYPT_COST), + verdict: () => matches, + }; +} + +/** A row stores a string or nothing. Anything else cannot match a candidate. */ +function storedText(stored: string | undefined) { + return String(stored ?? ""); +} + +function withHashedField( + row: { constructor: any }, + field: PropertyKey, + use: (modifier: HashModifier, stored: string) => R, +) { + const metaTable = getMetadata(row.constructor, ModifiersStaticMetadata); + const entry = fieldsByKey(metaTable)[field]?.find( + ({ modifier }) => modifier instanceof HashModifier, + ); + if (!entry || !(entry.modifier instanceof HashModifier)) { + throw new Error(`Field ${String(field)} is not hashed`); + } + const internal = getInternal(row); + const stored: unknown = dataByKey(internal)[field]; + return withRef( + internal, + entry.id, + entry.modifier, + entry.metaRef, + (modifier) => use(modifier, isString(stored) ? stored : ""), + ); +} + /** * Hash modifier. enables the use of {@link Hashed} on table fields. * - * Hashed fields only contain a hash of the inserted value. + * Hashed fields only contain a hash of the inserted value, by default a scrypt + * hash in the self-describing form `$scrypt$ln=15,r=8,p=3$$`. * Because of this, you lose the ability to query the information directly. * You can test equality using {@link testHash} */ @@ -29,23 +208,71 @@ export class HashModifier extends OneWayModifier< > { public readonly autolock = true; + /** + * Hashes a new value. Assigning the stored hash back keeps it as it is. + * Blocks the event loop for one scrypt run: a setter and the conversions + * behind `insert` and `update` are synchronous. + */ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- overrides OneWayModifier.lock; a modified field's value has its table class's type, which the shared modifier chain cannot name - public override lock(_locked_value: string | undefined, value: unknown) { + public override lock(locked_value: string | undefined, value: unknown) { if (value === undefined) { return value; } - if (!this.meta.salt) { - this.meta.salt = generate(); + if (value === locked_value) { + return locked_value; } - return createHash(this.options.algorithm || "sha256") - .update(this.meta.salt) - .update(JSON.stringify(value)) - .digest("base64"); + const candidate = JSON.stringify(value); + const { algorithm } = this.options; + if (algorithm) { + if (!this.meta.salt) { + this.meta.salt = generate(); + } + return digestOf(algorithm, this.meta.salt, candidate); + } + const salt = randomBytes(SALT_BYTES); + const key = scryptSync( + ...scryptArgs(candidate, salt, KEY_BYTES, SCRYPT_COST), + ); + return `${CURRENT_PREFIX}${unpaddedBase64(salt)}$${unpaddedBase64(key)}`; + } + + public override test(locked_value: string | undefined, value: HashInput) { + const check = this.plan(storedText(locked_value), value); + return check.verdict(check.run && scryptSync(...check.run)); + } + + /** {@link test} on the thread pool, without blocking the event loop. */ + public async testAsync(locked_value: string, value: HashInput) { + const check = this.plan(locked_value, value); + return check.verdict(check.run && (await scryptAsync(...check.run))); + } + + /** + * @returns Whether a stored hash differs from what this field writes today. A + * field with an explicit `algorithm` never reports one: hashing again would + * give the same digest, or weaken a scrypt hash. + */ + public isOutdated(locked_value: string) { + return ( + !this.options.algorithm && + locked_value !== "" && + !locked_value.startsWith(CURRENT_PREFIX) + ); + } + + /** Reads the salt now: `this.meta` is bound to a row only synchronously. */ + private plan(locked_value: string, value: HashInput) { + return planCheck( + { stored: locked_value, salt: this.meta.salt }, + this.options.algorithm, + value === undefined ? undefined : JSON.stringify(value), + ); } [MixinSymbol] = class { /** - * Test a value against the stored hash. + * Test a value against the stored hash. Blocks the event loop for one + * scrypt run: on a server, prefer {@link testHashAsync}. * * @param field - Name of the field to test against. * @param value - Value to hash and test. @@ -59,6 +286,39 @@ export class HashModifier extends OneWayModifier< ) { return testValue(this, field, value); } + + /** + * Test a value against the stored hash on the thread pool. + * + * @param field - Name of the field to test against. + * @param value - Value to hash and test. + * + * @returns Whether the new value is identical to the original or not. + */ + async testHashAsync( + this: This, + field: F, + value: This[F], + ) { + return withHashedField(this, field, (modifier, stored) => + modifier.testAsync(stored, value), + ); + } + + /** + * Tell whether a stored hash needs writing again: it uses an older format + * or cost. After a successful test, assign the tested value to the field + * and save the row. + * + * @param field - Name of the field to inspect. + * + * @returns Whether the stored hash is outdated. False when nothing is stored. + */ + isHashOutdated(this: This, field: keyof This) { + return withHashedField(this, field, (modifier, stored) => + modifier.isOutdated(stored), + ); + } }; } diff --git a/src/modifiers/internal/records.ts b/src/modifiers/internal/records.ts index eba279e..fddc5cb 100644 --- a/src/modifiers/internal/records.ts +++ b/src/modifiers/internal/records.ts @@ -41,6 +41,11 @@ export function getInternal(object: InternalHolder): InternalRecord { return object._internal; } +/** Whether a row's modifier state holds no locked value and no modifier state. */ +export function isEmptyInternal({ meta, data }: InternalRecord): boolean { + return Object.keys(meta).length === 0 && Object.keys(data).length === 0; +} + /** * Throws when a modifier cannot be attached to the field named `field`: the * two names the table's own structures live under. diff --git a/src/tests/database.test.ts b/src/tests/database.test.ts index 3e10171..9c08a34 100644 --- a/src/tests/database.test.ts +++ b/src/tests/database.test.ts @@ -257,3 +257,40 @@ function getIndexDefinitions(schemaId: string, tableName: string) { if (!schema) throw new Error("Schema not found"); return schema.definition[tableName].indexes; } + +describe("Database - fixture race", () => { + it("writes one row when two processes register an id-less fixture at once", async () => { + @Fixture(() => [{ name: "Only row" }]) + @RegisterTable("fixture_race_no_id", "test-fixture-race-no-id") + class _TestTable extends Table { + declare name: string; + } + + await Promise.all([ + RegisterSchema("test-fixture-race-no-id"), + RegisterSchema("test-fixture-race-no-id"), + ]); + + const schema = Schema.get("test-fixture-race-no-id"); + if (!schema) throw new Error("Schema not found"); + expect(await schema.instance().table("fixture_race_no_id")).to.have.length( + 1, + ); + }); + it("does not fail the process that loses a fixture insert", async () => { + @Fixture(() => [{ _id: "only-row", name: "Only row" }]) + @RegisterTable("fixture_race_id", "test-fixture-race-id") + class _TestTable extends Table { + declare name: string; + } + + await Promise.all([ + RegisterSchema("test-fixture-race-id"), + RegisterSchema("test-fixture-race-id"), + ]); + + const schema = Schema.get("test-fixture-race-id"); + if (!schema) throw new Error("Schema not found"); + expect(await schema.instance().table("fixture_race_id")).to.have.length(1); + }); +}); diff --git a/src/tests/hash_upgrade.test.ts b/src/tests/hash_upgrade.test.ts new file mode 100644 index 0000000..793588c --- /dev/null +++ b/src/tests/hash_upgrade.test.ts @@ -0,0 +1,140 @@ +import { expect } from "chai"; +import { Schema } from "@antelopejs/interface-database"; +import { RegisterSchema } from "../database"; +import { BasicDataModel } from "../model"; +import { UpdateTime } from "../modifiers/autodate"; +import { lock } from "../modifiers/common"; +import { Hashed, HashModifier } from "../modifiers/hash"; +import { RegisterTable } from "../schema"; +import { Field, Table } from "../table"; +import { PASSWORD, ROW_FROM_0_1_11, scryptSuite } from "./helpers/hash-rows"; + +const SCHEMA_ID = "hash-upgrade-schema"; +const WRONG_PASSWORD = "not the password"; + +@RegisterTable("hash_upgrade_accounts", SCHEMA_ID) +class Account extends Table.with(HashModifier) { + declare _id: string; + + @Field("string") + declare name: string; + + @UpdateTime() + @Field("date") + declare updatedAt: Date; + + @Hashed() + @Field("string") + declare password: string; +} + +const AccountModel = BasicDataModel(Account, "hash_upgrade_accounts"); + +scryptSuite("Integration - upgrade of stored hashes", () => { + let model: InstanceType; + + before(async () => { + await RegisterSchema(SCHEMA_ID); + const instance = Schema.get(SCHEMA_ID)?.instance(); + if (!instance) throw new Error("Hash upgrade schema not found"); + model = new AccountModel(instance); + }); + + async function storedRowFrom0_1_11() { + const _id = `from-0.1.11-${Math.random()}`; + await model.table + .insert(structuredClone({ ...ROW_FROM_0_1_11, _id })) + .run(); + return _id; + } + + async function storedCurrentRow() { + const [id] = await model.insert({ name: "Current", password: PASSWORD }); + return id; + } + + async function reload(id: string) { + const account = await model.get(id); + if (!account) throw new Error(`Row ${id} not found`); + return account; + } + + async function expectVerifies(account: Account) { + expect(account.testHash("password", PASSWORD)).to.equal(true); + expect(await account.testHashAsync("password", PASSWORD)).to.equal(true); + expect(account.testHash("password", WRONG_PASSWORD)).to.equal(false); + expect(await account.testHashAsync("password", WRONG_PASSWORD)).to.equal( + false, + ); + } + + it("verifies, flags, re-hashes and saves a row written by 0.1.11", async () => { + const id = await storedRowFrom0_1_11(); + const before = await reload(id); + await expectVerifies(before); + expect(before.isHashOutdated("password")).to.equal(true); + + before.password = PASSWORD; + await model.update(before); + + const after = await reload(id); + expect(after.password).to.match(/^\$scrypt\$ln=15,r=8,p=3\$/); + await expectVerifies(after); + expect(after.isHashOutdated("password")).to.equal(false); + }); + + it("re-hashes through a partial update of the hashed field", async () => { + const id = await storedRowFrom0_1_11(); + + await model.update(id, { password: PASSWORD }); + + const after = await reload(id); + await expectVerifies(after); + expect(after.isHashOutdated("password")).to.equal(false); + expect(after.name).to.equal(ROW_FROM_0_1_11.name); + }); + + for (const [kind, store] of [ + ["from 0.1.11", storedRowFrom0_1_11], + ["current", storedCurrentRow], + ] as const) { + it(`a partial update of another field keeps the hash of a ${kind} row`, async () => { + const id = await store(); + const stored = (await reload(id)).password; + + await model.update(id, { name: "Renamed" }); + + const after = await reload(id); + expect(after.name).to.equal("Renamed"); + expect(after.password).to.equal(stored); + await expectVerifies(after); + }); + + it(`a whole-row update of another field keeps the hash of a ${kind} row`, async () => { + const id = await store(); + const account = await reload(id); + const stored = account.password; + + account.name = "Renamed"; + await model.update(account); + + const after = await reload(id); + expect(after.name).to.equal("Renamed"); + expect(after.password).to.equal(stored); + await expectVerifies(after); + }); + + it(`lock() then a whole-row update keeps the hash of a ${kind} row`, async () => { + const id = await store(); + const account = await reload(id); + const stored = account.password; + + lock(account, HashModifier, undefined); + await model.update(account); + + const after = await reload(id); + expect(after.password).to.equal(stored); + await expectVerifies(after); + }); + } +}); diff --git a/src/tests/helpers/hash-rows.ts b/src/tests/helpers/hash-rows.ts new file mode 100644 index 0000000..d44a80e --- /dev/null +++ b/src/tests/helpers/hash-rows.ts @@ -0,0 +1,40 @@ +export const PASSWORD = "correct horse battery staple"; + +const SCRYPT_SUITE_TIMEOUT_MS = 30_000; + +/** + * `describe` for tests that spend several scrypt runs. One run takes hundreds of + * milliseconds on a busy machine, which passes mocha's 2 s limit in a few runs. + */ +export function scryptSuite(title: string, body: () => void) { + describe(title, function () { + this.timeout(SCRYPT_SUITE_TIMEOUT_MS); + body(); + }); +} + +/** A row as `@Hashed()` stored it in the published 0.1.11: a bare salted SHA-256. */ +export const ROW_FROM_0_1_11 = { + _id: "row-from-0.1.11", + email: "legacy@example.test", + name: "Legacy", + _internal: { + meta: { HashModifier: { salt: "BMNyCHwpLNt2tkqkyB5rwOj1q2wfDIo6" } }, + data: { password: "nPamXXaKC8N+8W7xqxTSo8r+pS9jXMkjJu2XMZtUR+A=" }, + }, +}; + +/** + * A row as the published 0.1.11 stored `@Hashed({ algorithm: "sha256" })` on + * `password` and `@Hashed({ algorithm: "md5" })` on `checksum`, set to `{ a: 1 }`. + */ +export const FINGERPRINT_ROW_FROM_0_1_11 = { + _id: "fingerprint-from-0.1.11", + _internal: { + meta: { HashModifier: { salt: "lGSUI2bFDjUE9tTMDmgi4epfFNLsU0xY" } }, + data: { + password: "UsoNNNsm8q4tIZffzzw9QX58b98ksdLC6JN+a1FN93g=", + checksum: "NbsXtgSCNhwxQV/eBQ+LKA==", + }, + }, +}; diff --git a/src/tests/modifiers/hash.test.ts b/src/tests/modifiers/hash.test.ts index 619b5ce..1977fc6 100644 --- a/src/tests/modifiers/hash.test.ts +++ b/src/tests/modifiers/hash.test.ts @@ -1,6 +1,95 @@ +import { createHash, scryptSync } from "node:crypto"; import { expect } from "chai"; import { Field, Table } from "../../table"; +import { fromDatabase, lock } from "../../modifiers/common"; import { Hashed, HashModifier } from "../../modifiers/hash"; +import { + FINGERPRINT_ROW_FROM_0_1_11, + PASSWORD, + ROW_FROM_0_1_11, + scryptSuite, +} from "../helpers/hash-rows"; + +const WRONG_PASSWORD = "not the password"; +const SALT = "c2FsdHNhbHRzYWx0c2FsdA"; +const KEY = "a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2U"; + +class Account extends Table.with(HashModifier) { + declare _id: string; + + @Hashed() + @Field("string") + declare password: string; + + @Field("string") + declare email: string; +} + +class Fingerprint extends Table.with(HashModifier) { + @Hashed({ algorithm: "sha256" }) + @Field("string") + declare password: string; + + @Hashed({ algorithm: "md5" }) + @Field("any") + declare checksum: { a: number }; +} + +type StoredValue = string | number | null | undefined; + +type StoredRow = { + _internal: { + meta: { HashModifier?: { salt: string } }; + data: { password?: StoredValue; checksum?: string }; + }; +}; + +function accountFrom(row: StoredRow) { + return fromDatabase(structuredClone(row), Account); +} + +function accountStoring(stored: StoredValue) { + return accountFrom({ _internal: { meta: {}, data: { password: stored } } }); +} + +function newAccount(password = PASSWORD) { + const account = new Account(); + account.password = password; + return account; +} + +function scryptRow(ln: number, password = PASSWORD) { + const salt = Buffer.alloc(16, 7); + const key = scryptSync(JSON.stringify(password), salt, 32, { + N: 2 ** ln, + r: 8, + p: 1, + maxmem: 2 ** 30, + }); + const encoded = [salt, key].map((part) => + part.toString("base64").replace(/=+$/, ""), + ); + return `$scrypt$ln=${ln},r=8,p=1$${encoded[0]}$${encoded[1]}`; +} + +async function expectVerifies(account: Account, password = PASSWORD) { + expect(account.testHash("password", password)).to.equal(true); + expect(await account.testHashAsync("password", password)).to.equal(true); +} + +async function expectRefuses(account: Account, password = WRONG_PASSWORD) { + expect(account.testHash("password", password)).to.equal(false); + expect(await account.testHashAsync("password", password)).to.equal(false); +} + +function fastestMs(work: () => void) { + const runs = [0, 1, 2].map(() => { + const started = performance.now(); + work(); + return performance.now() - started; + }); + return Math.min(...runs); +} describe("Modifiers - hash", () => { it("hashes string values", async () => { @@ -110,6 +199,276 @@ describe("Modifiers - hash", () => { }); }); +scryptSuite("Modifiers - hash, default scrypt format", () => { + it("stores a string that describes the hash it holds, at N >= 2^15", () => { + const stored = newAccount().password; + + const match = /^\$scrypt\$ln=(\d+),r=(\d+),p=(\d+)\$([^$]+)\$([^$]+)$/.exec( + stored, + ); + if (!match) throw new Error(`Unexpected stored form: ${stored}`); + const [ln, r, p] = match.slice(1, 4).map(Number); + expect(ln).to.be.at.least(15); + const key = scryptSync( + JSON.stringify(PASSWORD), + Buffer.from(match[4], "base64"), + Buffer.from(match[5], "base64").length, + { N: 2 ** ln, r, p, maxmem: 2 ** 30 }, + ); + expect(key.toString("base64").replace(/=+$/, "")).to.equal(match[5]); + }); + + it("gives each write its own salt", () => { + expect(newAccount().password).to.not.equal(newAccount().password); + }); + + it("tests a value against the stored hash, sync and async", async () => { + const account = newAccount(); + await expectVerifies(account); + await expectRefuses(account); + }); + + it("does not flag a hash it just wrote", () => { + expect(newAccount().isHashOutdated("password")).to.equal(false); + }); + + it("does not flag a field with nothing stored", () => { + expect(new Account().isHashOutdated("password")).to.equal(false); + }); + + it("throws when asked about a field that is not hashed", async () => { + const account = newAccount(); + expect(() => account.isHashOutdated("email")).to.throw("email"); + let failure: unknown; + await account.testHashAsync("email", PASSWORD).catch((error: unknown) => { + failure = error; + }); + expect(failure).to.be.instanceOf(Error); + }); +}); + +scryptSuite("Modifiers - hash, upgrade of stored hashes", () => { + it("verifies a row written by 0.1.11 and flags it", async () => { + const account = accountFrom(ROW_FROM_0_1_11); + await expectVerifies(account); + await expectRefuses(account); + expect(account.isHashOutdated("password")).to.equal(true); + }); + + it("after a re-hash, verifies and no longer flags that row", async () => { + const account = accountFrom(ROW_FROM_0_1_11); + account.password = PASSWORD; + expect(account.password).to.match(/^\$scrypt\$/); + await expectVerifies(account); + await expectRefuses(account); + expect(account.isHashOutdated("password")).to.equal(false); + }); + + it("verifies a hash of a lower cost by its own parameters and flags it", async () => { + const account = accountStoring(scryptRow(14)); + await expectVerifies(account); + await expectRefuses(account); + expect(account.isHashOutdated("password")).to.equal(true); + }); + + it("keeps the salt of each row while two async tests overlap", async () => { + const first = accountFrom(ROW_FROM_0_1_11); + const second = accountFrom({ + _internal: { + meta: { HashModifier: { salt: "another-salt" } }, + data: { + password: createHash("sha256") + .update("another-salt") + .update(JSON.stringify(WRONG_PASSWORD)) + .digest("base64"), + }, + }, + }); + const results = await Promise.all([ + first.testHashAsync("password", PASSWORD), + second.testHashAsync("password", WRONG_PASSWORD), + first.testHashAsync("password", WRONG_PASSWORD), + second.testHashAsync("password", PASSWORD), + ]); + expect(results).to.deep.equal([true, true, false, false]); + }); +}); + +scryptSuite("Modifiers - hash, assigning the stored hash back", () => { + const rows = { + "from 0.1.11": () => accountFrom(ROW_FROM_0_1_11), + current: () => newAccount(), + }; + for (const [name, build] of Object.entries(rows)) { + it(`lock() on a ${name} row keeps the hash`, async () => { + const account = build(); + const stored = account.password; + lock(account, HashModifier, undefined); + lock(account, HashModifier, ["password"]); + expect(account.password).to.equal(stored); + await expectVerifies(account); + }); + + it(`assigning the hash of a ${name} row keeps it`, async () => { + const account = build(); + const stored = account.password; + account.password = stored; + expect(account.password).to.equal(stored); + await expectVerifies(account); + }); + } +}); + +scryptSuite("Modifiers - hash, explicit algorithm", () => { + function fingerprintFrom(row: StoredRow) { + return fromDatabase(structuredClone(row), Fingerprint); + } + + it("still verifies rows written by 0.1.11 and never flags them", async () => { + const row = fingerprintFrom(FINGERPRINT_ROW_FROM_0_1_11); + expect(row.testHash("password", PASSWORD)).to.equal(true); + expect(await row.testHashAsync("password", PASSWORD)).to.equal(true); + expect(row.testHash("password", WRONG_PASSWORD)).to.equal(false); + expect(row.testHash("checksum", { a: 1 })).to.equal(true); + expect(row.testHash("checksum", { a: 2 })).to.equal(false); + expect(row.isHashOutdated("password")).to.equal(false); + expect(row.isHashOutdated("checksum")).to.equal(false); + }); + + it("still writes the same salted digest as 0.1.11", () => { + const row = fingerprintFrom(FINGERPRINT_ROW_FROM_0_1_11); + row.password = "another value"; + expect(row.password).to.equal( + createHash("sha256") + .update(FINGERPRINT_ROW_FROM_0_1_11._internal.meta.HashModifier.salt) + .update(JSON.stringify("another value")) + .digest("base64"), + ); + }); + + it("verifies a scrypt hash and does not flag it, so it is not weakened", async () => { + const row = fromDatabase( + { _internal: { meta: {}, data: { password: scryptRow(15) } } }, + Fingerprint, + ); + expect(await row.testHashAsync("password", PASSWORD)).to.equal(true); + expect(row.testHash("password", WRONG_PASSWORD)).to.equal(false); + expect(row.isHashOutdated("password")).to.equal(false); + }); +}); + +scryptSuite("Modifiers - hash, values that cannot verify", () => { + const unusable: [string, StoredValue][] = [ + ["no value", undefined], + ["a null value", null], + ["a number", 42], + ["a digest with no salt", "nPamXXaKC8N+8W7xqxTSo8r+pS9jXMkjJu2XMZtUR+A="], + ["a bare prefix", "$scrypt$"], + ["a key too short to compare", `$scrypt$ln=15,r=8,p=1$${SALT}$A`], + ["an empty key", `$scrypt$ln=15,r=8,p=1$${SALT}$`], + ["a salt too short", `$scrypt$ln=15,r=8,p=1$AAAA$${KEY}`], + ["a cost over the memory limit", `$scrypt$ln=30,r=8,p=1$${SALT}$${KEY}`], + ["a zero p", `$scrypt$ln=15,r=8,p=0$${SALT}$${KEY}`], + ["a zero r", `$scrypt$ln=15,r=0,p=1$${SALT}$${KEY}`], + ["a zero ln", `$scrypt$ln=0,r=8,p=1$${SALT}$${KEY}`], + ["an N that r cannot hold", `$scrypt$ln=16,r=1,p=1$${SALT}$${KEY}`], + ]; + for (const [name, stored] of unusable) { + it(`refuses a row storing ${name} without throwing`, async () => { + await expectRefuses(accountStoring(stored), PASSWORD); + }); + } + + it("refuses an undefined candidate on a row with nothing stored", async () => { + await expectRefuses(new Account(), new Account().email); + }); + + it("refuses an undefined candidate on a row with a hash", async () => { + await expectRefuses(newAccount(), new Account().email); + }); +}); + +scryptSuite("Modifiers - hash, limits of a stored string", () => { + function rowFor({ ln = 1, r = 1, p = 1, saltBytes = 16, keyBytes = 32 }) { + const salt = Buffer.alloc(saltBytes, 7); + const key = scryptSync(JSON.stringify(PASSWORD), salt, keyBytes, { + N: 2 ** ln, + r, + p, + maxmem: 2 ** 30, + }); + const [encodedSalt, encodedKey] = [salt, key].map((part) => + part.toString("base64").replace(/=+$/, ""), + ); + return `$scrypt$ln=${ln},r=${r},p=${p}$${encodedSalt}$${encodedKey}`; + } + + const overLimit = [ + { name: "a p of 17", cost: { p: 17 } }, + { name: "an r of 100", cost: { r: 100 } }, + { name: "memory above 128 MiB", cost: { ln: 17, r: 9 } }, + { name: "a salt of 65 bytes", cost: { saltBytes: 65 } }, + { name: "a salt of 15 bytes", cost: { saltBytes: 15 } }, + { name: "a key of 65 bytes", cost: { keyBytes: 65 } }, + { name: "a key of 15 bytes", cost: { keyBytes: 15 } }, + ]; + for (const { name, cost } of overLimit) { + it(`refuses the right password on a row asking for ${name}`, async () => { + const account = accountStoring(rowFor(cost)); + await expectRefuses(account, PASSWORD); + expect(account.isHashOutdated("password")).to.equal(true); + }); + } + + const atLimit = [ + { name: "a p of 16", cost: { p: 16 } }, + { name: "128 MiB of memory", cost: { ln: 17, r: 8 } }, + { name: "64-byte salt and key", cost: { saltBytes: 64, keyBytes: 64 } }, + ]; + for (const { name, cost } of atLimit) { + it(`verifies the right password on a row asking for ${name}`, () => { + const account = accountStoring(rowFor(cost)); + expect(account.testHash("password", PASSWORD)).to.equal(true); + }); + } +}); + +scryptSuite("Modifiers - hash, cost of a test", () => { + it("testHashAsync lets the event loop turn while it runs", async () => { + const account = newAccount(); + let turns = 0; + const timer = setInterval(() => { + turns += 1; + }, 1); + await account.testHashAsync("password", PASSWORD); + clearInterval(timer); + expect(turns).to.be.greaterThan(0); + }); + + it("a refusal costs a scrypt run whatever the row holds", () => { + const current = newAccount(); + const legacy = accountFrom(ROW_FROM_0_1_11); + const empty = new Account(); + const full = fastestMs(() => current.testHash("password", WRONG_PASSWORD)); + + for (const account of [legacy, empty]) { + const refused = fastestMs(() => + account.testHash("password", WRONG_PASSWORD), + ); + expect(refused).to.be.greaterThan(full / 4); + } + }); + + it("a field with an explicit digest does not pay for scrypt", () => { + const row = new Fingerprint(); + row.password = PASSWORD; + const digest = fastestMs(() => row.testHash("password", WRONG_PASSWORD)); + const current = newAccount(); + const full = fastestMs(() => current.testHash("password", WRONG_PASSWORD)); + expect(digest).to.be.lessThan(full / 4); + }); +}); + interface HashOptions { algorithm?: string; } diff --git a/src/tests/partial_update.test.ts b/src/tests/partial_update.test.ts new file mode 100644 index 0000000..c3aa436 --- /dev/null +++ b/src/tests/partial_update.test.ts @@ -0,0 +1,42 @@ +import { expect } from "chai"; +import { Schema } from "@antelopejs/interface-database"; +import { RegisterSchema } from "../database"; +import { BasicDataModel } from "../model"; +import { UpdateTime } from "../modifiers/autodate"; +import { Encrypted, EncryptionModifier } from "../modifiers/encryption"; +import { RegisterTable } from "../schema"; +import { Field, Table } from "../table"; + +const SCHEMA_ID = "partial-update-schema"; + +@RegisterTable("partial_update_notes", SCHEMA_ID) +class Note extends Table.with(EncryptionModifier) { + @Field("string") + declare title: string; + + @UpdateTime() + @Field("date") + declare updatedAt: Date; + + @Encrypted({ secretKey: "0123456789abcdef0123456789abcdef" }) + @Field("string") + declare secret: string; +} + +const NoteModel = BasicDataModel(Note, "partial_update_notes"); + +describe("Integration - partial update", () => { + it("keeps modifier data on an update of another field", async () => { + await RegisterSchema(SCHEMA_ID); + const instance = Schema.get(SCHEMA_ID)?.instance(); + if (!instance) throw new Error("Partial update schema not found"); + const model = new NoteModel(instance); + const [id] = await model.insert({ title: "First", secret: "kept" }); + + await model.update(id, { title: "Second" }); + + const note = await model.get(id); + expect(note?.title).to.equal("Second"); + expect(note?.secret).to.equal("kept"); + }); +}); From eaf455d8c6eb14ccb6cdfbd8df4356e78987b20e Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 3 Nov 2026 04:44:38 +0100 Subject: [PATCH 18/45] docs: pass the database-decorators docs to the docs standard (#78) * docs: pass the database-decorators docs to the docs standard * docs: turn on the docs standard * docs: say when fromDatabase returns undefined and when a digest test runs scrypt --- README.md | 12 +- docs/1.introduction.md | 44 +++---- docs/2.table-definitions.md | 80 +++++++------ docs/3.table-modifiers.md | 207 +++++++++++++++++---------------- docs/4.data-models.md | 89 +++++++------- docs/5.parameter-decoration.md | 40 ++++--- package.json | 5 +- 7 files changed, 256 insertions(+), 221 deletions(-) diff --git a/README.md b/README.md index ca792a6..4c1cdba 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -# @antelopejs/interface-database-decorators +# `@antelopejs/interface-database-decorators`
-npm -License -Discord -Docs +npm +License +Discord +Docs
-Database tables and data models as TypeScript classes, on top of [`@antelopejs/interface-database`](https://github.com/AntelopeJS/interface-database): declare fields and indexes with decorators, then read and write rows through a typed model. +Database tables and data models as TypeScript classes, on top of [`@antelopejs/interface-database`](https://github.com/AntelopeJS/interface-database). Declare fields and indexes with decorators, then read and write rows through a typed model. ## What it offers diff --git a/docs/1.introduction.md b/docs/1.introduction.md index f424f05..1e69013 100644 --- a/docs/1.introduction.md +++ b/docs/1.introduction.md @@ -1,21 +1,25 @@ -# Define tables and work with models +--- +title: Define tables and work with models +description: Describe a table as a TypeScript class, register its schema, and save and read a first row through a typed model. +mode: how-to +--- ## Overview -Use `@antelopejs/interface-database-decorators` to describe tables with TypeScript classes and work with converted records through models. It builds on AQL (`@antelopejs/interface-database`) and adds field modifiers and API model injection. +`@antelopejs/interface-database-decorators` describes tables as TypeScript classes and reads and writes converted rows through models. It builds on AQL (`@antelopejs/interface-database`) and adds field modifiers and model injection into API controllers. ## From a table class to a stored record 1. **Describe the table.** Extend `Table`, declare fields and indexes, and register the class with `@RegisterTable("users", "myapp")`. See [table definitions](./2.table-definitions.md). -2. **Bind the schema.** Import the table declarations, then call `await RegisterSchema("myapp")` from their declaring module. This requires a configured database provider and can provision tables and insert fixtures; it is not a read-only metadata operation. -3. **Create the model.** Use `BasicDataModel(User)` and a `SchemaInstance`, or `GetModel` after the table has been bound. See [data models](./4.data-models.md). -4. **Write and read through the model.** Model operations apply conversion and modifier events. Use `model.table` when you need to compose an AQL query yourself. +2. **Bind the schema.** Import the table declarations, then call `await RegisterSchema("myapp")` from their declaring module. The call needs a configured database module. It provisions the tables and inserts fixtures, so it writes to the database. +3. **Create the model.** Use `BasicDataModel(User)` with a `SchemaInstance`, or call `GetModel` once the table is bound. See [data models](./4.data-models.md). +4. **Write and read through the model.** Model operations apply the conversions and the modifier events. To compose an AQL query yourself, use `model.table`. -Keep schema IDs and instance IDs distinct: `myapp` identifies the table definitions; an instance ID selects the database instance used by a model. A table class without a schema binding cannot be resolved by `GetModel`. +Keep schema IDs and instance IDs apart. `myapp` names the table definitions, and an instance ID selects the database instance a model uses. `GetModel` throws for a table class that `RegisterSchema` has not bound, unless a schema of the class's schema name already exists. ### Save and retrieve a note -In a module with a configured database provider, import this declaration before calling `tryNote()` during initialization. Use a disposable development database: `RegisterSchema` provisions the schema, and `insert` writes data. +In a module with a configured database module, import this declaration, then call `tryNote()` during initialization. Use a disposable development database, because `RegisterSchema` provisions the schema and `insert` writes a row. ```ts docs-check import { @@ -45,7 +49,7 @@ export async function tryNote() { } ``` -The model uses the default instance of the schema bound to `Note`. Keep the inserted ID when extending this exercise with `update` or `delete`; each successful run inserts a new note. In an application, register the schema once during initialization rather than on each request. +The model uses the default instance of the schema bound to `Note`. Each run inserts a new note, so keep the inserted ID if you extend this exercise with `update` or `delete`. In an application, register the schema once during initialization, not on each request. ## Choose the layer for your task @@ -54,23 +58,23 @@ The model uses the default instance of the schema bound to `Note`. Keep the inse | Describe storage fields and indexes | [Table definitions](./2.table-definitions.md) | Declaring a class is separate from registering its schema. | | Encrypt, hash, or localize fields | [Table modifiers](./3.table-modifiers.md) | Conversion events determine when transformations run. | | Read or write converted records | [Data models](./4.data-models.md) | Reads return converted records, not chainable queries. | -| Select a model in an HTTP request | [Parameter decoration](./5.parameter-decoration.md) | The instance ID selects a binding; it does not authorize access. | +| Select a model in an HTTP request | [Parameter decoration](./5.parameter-decoration.md) | The instance ID selects a binding. It does not authorize access. | -Start without modifiers until the schema and model work together. Add validation explicitly: string-token `@Field` declarations describe storage, while codec validation requires codecs and the model's validation options. +Start without modifiers until the schema and the model work together. Validation is opt-in. A string-token `@Field` describes storage only, and codec validation needs io-ts codecs and the model's `validate` option. ## Dependencies -This package depends on the following: +The package depends on these packages: -- [**@antelopejs/interface-database**](https://github.com/AntelopeJS/interface-database) - The underlying database query interface (AQL) -- [**@antelopejs/interface-api**](https://github.com/AntelopeJS/interface-api) - API controller integration for parameter decoration -- [**@antelopejs/interface-core**](https://github.com/AntelopeJS/interface-core) - Core decorator utilities -- [**reflect-metadata**](https://www.npmjs.com/package/reflect-metadata) - Metadata reflection for TypeScript decorators -- [**randomstring**](https://www.npmjs.com/package/randomstring) - Random string generation for hash salts +- [`@antelopejs/interface-database`](https://github.com/AntelopeJS/interface-database) is the database query interface (AQL). +- [`@antelopejs/interface-api`](https://github.com/AntelopeJS/interface-api) provides the API controllers that `@Model` injects into. +- [`@antelopejs/interface-core`](https://github.com/AntelopeJS/interface-core) provides the decorator factories. +- [`reflect-metadata`](https://www.npmjs.com/package/reflect-metadata) stores the decorator metadata. +- [`randomstring`](https://www.npmjs.com/package/randomstring) generates the salt of a `@Hashed` field that sets an `algorithm`. ## Documentation -- [Table Definitions](./2.table-definitions.md) - Define database tables with the `Table` class, `Index` decorator, and `Fixture` decorator -- [Table Modifiers](./3.table-modifiers.md) - Apply encryption, hashing, and localization modifiers to table fields, or create custom modifiers -- [Data Models](./4.data-models.md) - Create data models with `BasicDataModel` for CRUD operations and data conversion -- [Parameter Decoration](./5.parameter-decoration.md) - Inject model instances into API controller methods with the `Model` decorator +- [Table definitions](./2.table-definitions.md) covers the `Table` class and the `Field`, `Index`, `Relation`, `RegisterTable`, and `Fixture` decorators. +- [Table modifiers](./3.table-modifiers.md) covers encryption, hashing, and localization of fields, and custom modifiers. +- [Data models](./4.data-models.md) covers `BasicDataModel`, its CRUD methods, and the data conversions. +- [Parameter decoration](./5.parameter-decoration.md) covers the `Model` decorator, which injects a model into API controller methods. diff --git a/docs/2.table-definitions.md b/docs/2.table-definitions.md index 6efcdfc..992c847 100644 --- a/docs/2.table-definitions.md +++ b/docs/2.table-definitions.md @@ -1,10 +1,14 @@ -# Table Definitions +--- +title: Table definitions +description: Reference for the Table class and the Field, Index, Relation, RegisterTable, and Fixture decorators, RegisterSchema, and Table.table. +mode: reference +--- -The Database Decorators package provides a class-based approach to defining database tables. You extend the `Table` base class, declare each field with TypeScript's `declare` keyword and the `@Field` decorator, and use further decorators to configure indexes, relations, and initial data. +A table is a class that extends `Table`. Each field is a property declared with TypeScript's `declare` keyword and the `@Field` decorator. Further decorators add indexes, relations, and initial data. -## The Table Class +## The `Table` class -The `Table` class serves as the base class for all table definitions. Every table automatically includes an `_id` field as the primary key. +`Table` is the base class of every table definition. It declares `_id: string`, the primary key of every table. ```typescript import { Table, Field } from "@antelopejs/interface-database-decorators"; @@ -18,9 +22,9 @@ class User extends Table { } ``` -### Extend with Modifiers +### Extend with modifiers -The `Table.with()` static method incorporates modifier mixins into the table class, adding capabilities like encryption, hashing, or localization: +The static `Table.with()` method returns a subclass of the table class that holds the mixin methods of the modifiers it receives, such as encryption, hashing, or localization: ```typescript import { @@ -45,11 +49,11 @@ class SensitiveData extends Table.with(EncryptionModifier) { } ``` -Provide a high-entropy key through your secret manager; do not use a default key. See [Table Modifiers](./3.table-modifiers.md#encryptionmodifier) for key format and date precision constraints. +Provide a high-entropy key through your secret manager, and never a default key. See [Table modifiers](./3.table-modifiers.md#encryptionmodifier) for the key format and the date precision limit. -## The Index Decorator +## The `Index` decorator -The `Index` decorator marks a field as a database index. Indexed fields enable efficient lookups through `getAll()` and `between()` on the underlying AQL table. +The `Index` decorator marks a field as a database index. An indexed field serves lookups through `getAll()` and `between()` on the AQL table, and through the model's `getBy()`. `@Index` on a symbol-keyed property throws, because a database index names a field. ```typescript import { Table, Field, Index } from "@antelopejs/interface-database-decorators"; @@ -72,11 +76,11 @@ class User extends Table { | `crossInstance` | `boolean` | Also keep the index fast for queries that run across all instances. | | `unique` | `boolean` | Refuse two rows of the instance with the same key. | -### Cross-Instance Indexes +### Cross-instance indexes -Every index is scoped to the schema instance, so queries on a single instance always use it. Set `crossInstance: true` when the index is also queried across all instances (queries on the `CROSS_INSTANCE` instance), so the database driver maintains an additional index for those queries. +Every index is scoped to the schema instance. Set `crossInstance: true` when queries across all instances (on the `CROSS_INSTANCE` instance) also use the index. The database driver then maintains an additional index for those queries. -The option never changes query results. Without it, cross-instance queries that name the index still return the same rows but run slower, and the driver logs a warning once per index. Leave it unset for indexes that are only queried within one instance, since each extra index costs storage and write time. +The option never changes query results. Without it, a cross-instance query that names the index returns the same rows but runs slower, and the `@antelopejs/mongodb` and `@antelopejs/rethinkdb` drivers log a warning once per index. Leave it unset for an index that only queries within one instance use, because each extra index costs storage and write time. ```typescript import { Field, Index, Table } from "@antelopejs/interface-database-decorators"; @@ -90,9 +94,9 @@ class User extends Table { For a compound index, setting `crossInstance: true` on any field of the group applies it to the whole group. -### Unique Indexes +### Unique indexes -Set `unique: true` to refuse a second row with the same key in one instance. The write that would store it throws `DuplicateKeyError` from `@antelopejs/interface-database`, naming the index and its fields; test it with `IsDuplicateKeyError`. Rows of different instances may share a key. +Set `unique: true` to refuse a second row with the same key in one instance. The write that would store it throws `DuplicateKeyError` from `@antelopejs/interface-database`, which names the index and its fields. Test for it with `IsDuplicateKeyError`. Rows of different instances may share a key. ```typescript import { Field, Index, Table } from "@antelopejs/interface-database-decorators"; @@ -104,9 +108,9 @@ class Template extends Table { } ``` -For a compound index, setting `unique: true` on any field of the group makes the whole group's key unique. Not every database module can enforce unique secondary indexes: one that cannot refuses the schema rather than ignoring the option. +For a compound index, setting `unique: true` on any field of the group makes the whole group's key unique. Not every database module can enforce a unique secondary index. A module that cannot, such as `@antelopejs/rethinkdb`, refuses the schema instead of ignoring the option. -### Compound Indexes +### Compound indexes Assign multiple fields to the same index group to create a compound index: @@ -138,9 +142,9 @@ class UserActivity extends Table { } ``` -## The Field Decorator +## The `Field` decorator -`@Field(type)` declares the field type for a property. The type is forwarded verbatim into the `TableDefinition.fields` map handed to the adapter. It accepts the full `FieldType` union -- primitive tokens (`"string"`, `"number"`, ...), arrays, nested records, or an io-ts codec. +`@Field(type)` declares the type of a property. The type goes unchanged into the `TableDefinition.fields` map that the database module receives. It accepts every member of the `FieldType` union, for example a string token such as `"string"` or `"number"`, an array, a nested record, or an io-ts codec. ```typescript fragment import * as t from "io-ts"; @@ -165,11 +169,11 @@ class Order extends Table { } ``` -Properties without `@Field` are omitted from `fields`. `@Field` stacks freely with `@Index`. +Properties without `@Field` are left out of `fields`. `@Field` and `@Index` each write their own metadata, so they combine in either order. -## The Relation Decorator +## The `Relation` decorator -The `Relation` decorator declares a link from a field to another table. It is **declarative metadata only**: database implementations do not enforce it (no foreign key constraint, no referential validation). Introspection tooling consumes it to expose the links between tables — for example to render schema diagrams or navigate related records. +The `Relation` decorator declares a link from a field to another table. It is metadata only. Database modules do not enforce it, with no foreign key constraint and no check that the target exists. Introspection tools read it to show the links between tables, for example in a schema diagram or to navigate to related records. ```typescript fragment import { @@ -199,11 +203,11 @@ class Comment extends Table { | `toField` | `string` | Target field name. Defaults to the target table's primary key. | | `many` | `boolean` | The decorated field holds multiple target keys (many targets per source record). | -`@Relation` stacks freely with `@Field` and `@Index`. +`@Relation` writes its own metadata, so it combines with `@Field` and `@Index` in any order. -## The RegisterTable Decorator +## The `RegisterTable` decorator -The `RegisterTable` class decorator associates a table class with a specific table name and schema. This registration is used by `RegisterSchema` to build the schema definition automatically. +The `RegisterTable` class decorator gives a table class its table name and its schema. `RegisterSchema` builds the schema definition from these registrations. ```typescript import { @@ -226,9 +230,9 @@ class User extends Table { The first argument is the table name in the database, and the second is the schema name. -## The Fixture Decorator +## The `Fixture` decorator -The `Fixture` decorator defines default data to insert when a table is first created. It receives a generator function that produces initial records. +The `Fixture` decorator defines the rows to insert into an empty table. It receives a generator function that returns them. ```typescript import { @@ -250,11 +254,13 @@ class UserRole extends Table { The generator function: -- Receives the table class as its parameter. -- Can return a single object, an array of objects, or a `Promise` resolving to either. -- Runs only when the table is empty, preventing duplicate inserts on subsequent starts. +- Receives the table class registered into the schema, which is the decorated class or a subclass of it. +- Can return a single object, an array of objects, or a `Promise` of either. +- Runs when `RegisterSchema` finds the table empty, so a later start inserts nothing again. -### Async Generators +Only a table registered with `@RegisterTable` gets its fixture, from `RegisterSchema`. Each row goes through the same conversion as a model insert, so modifiers and the `insert` event apply. A row without an `_id` gets `
:fixture:`, so two processes that start on the same empty table write one row between them. + +### Async generators ```typescript fragment import { @@ -278,7 +284,7 @@ class SystemConfig extends Table { ## Inheritance -Tables support standard class inheritance. Define a base table with common fields and extend it for specific use cases: +A table class can extend another table class. The subclass starts with a copy of its parent's table and schema names, fields, indexes, and fixture, so decorators on the subclass do not change the parent: ```typescript import { @@ -322,11 +328,11 @@ class Post extends BaseEntity { } ``` -## Register a Schema +## Register a schema -The `RegisterSchema` function builds a `SchemaDefinition` from all tables registered for the given schema id (via `@RegisterTable`), constructs the `Schema` (which the adapter provisions), binds each table class to that `Schema` (see [Open a Table from its Class](#open-a-table-from-its-class)), then inserts any fixture data for tables that are empty. +The `RegisterSchema` function builds a `SchemaDefinition` from every table that `@RegisterTable` registered for the schema ID. It constructs the `Schema`, which the database module provisions, and binds each table class to it (see [Open a table from its class](#open-a-table-from-its-class)). Then it inserts the fixtures of the tables that are empty. -Call it in the module that declares the tables: the binding then follows that module's database, whichever module later uses the classes. +Call it in the module that declares the tables. The binding then follows that module's database, whichever module later uses the classes. ```typescript import { RegisterSchema } from "@antelopejs/interface-database-decorators"; @@ -340,9 +346,9 @@ await RegisterSchema("myapp"); | ---------- | -------- | ------------------------------------------------------------------ | | `schemaId` | `string` | Schema id matching the `@RegisterTable(_, schemaId)` declarations. | -`RegisterSchema` returns `Promise`. +`RegisterSchema` returns `Promise`. It throws `No tables registered for schema ''` when no class registers into the schema. -## Open a Table from its Class +## Open a table from its class `Table.table()` returns the AQL table a class describes, in the schema `RegisterSchema` bound it to: @@ -365,4 +371,4 @@ User.table("tenant-1"); // instance "tenant-1" (CROSS_INSTANCE is accepted too) User.table(db); // db.table("users") on a SchemaInstance you already hold ``` -When given a `SchemaInstance`, `table()` takes the table from it as is: pass an instance of the class's own schema. A class that no `RegisterSchema` call bound throws, naming the class. +Given a `SchemaInstance`, `table()` takes the table from it as is, so pass an instance of the class's own schema. A class without `@RegisterTable` throws ` has no table name`. A class that no `RegisterSchema` call bound, and whose schema name has no schema, throws ` is not bound to a schema`. diff --git a/docs/3.table-modifiers.md b/docs/3.table-modifiers.md index dbdaa54..d6e3aeb 100644 --- a/docs/3.table-modifiers.md +++ b/docs/3.table-modifiers.md @@ -1,14 +1,18 @@ -# Table Modifiers +--- +title: Table modifiers +description: Reference for the encryption, hashing, localization, and auto-date modifiers, the scrypt hash format, and the classes and events for writing a custom modifier. +mode: reference +--- -Table modifiers transform field values as they are stored in and retrieved from the database. They enable transparent encryption, hashing, localization, and custom data transformations. Add modifiers to a table class using the `Table.with()` method. +A table modifier transforms a field's value on its way into a row and on its way out. The built-in modifiers encrypt, hash, or localize a field, or set a timestamp. Add a modifier's mixin to a table class with `Table.with()`, then mark each field with the modifier's decorator. -## Built-in Modifiers +## Built-in modifiers -### EncryptionModifier +### `EncryptionModifier` -The `EncryptionModifier` encrypts field values before storing them and decrypts them on retrieval. Both operations happen transparently. +The `EncryptionModifier` encrypts a field's value when the value is assigned and decrypts it when the field is read. -The default `aes-256-gcm` algorithm requires a key of exactly 32 bytes. `secretKey` is passed to Node's cipher as a UTF-8 string, without hex or base64 decoding. Supply a high-entropy, 32-byte UTF-8 value from a secret manager, keep it stable for existing ciphertext, and fail startup if it is missing or invalid; a 64-character hex encoding is not a 32-byte key for this API. +The default `aes-256-gcm` algorithm needs a key of exactly 32 bytes. `secretKey` goes to Node's cipher as a UTF-8 string, without hex or base64 decoding. Supply a high-entropy, 32-byte UTF-8 value from a secret manager, keep it stable while ciphertext exists, and fail startup if it is missing or invalid. A 64-character hex encoding is 64 bytes for this API, not a 32-byte key. ```ts docs-check import { @@ -41,13 +45,13 @@ class UserCredentials extends Table.with(EncryptionModifier) { } ``` -#### How It Works +#### How it works -- Data is encrypted using the specified algorithm before database insertion. -- A random initialization vector (IV) is generated for each write. -- For authenticated encryption algorithms (e.g., `aes-256-gcm`), an auth tag is stored alongside the ciphertext. -- On retrieval, the data is automatically decrypted. -- Date objects are restored as `Date` instances, but the encoder uses `toUTCString()`, which drops milliseconds. Encrypted dates round-trip only to whole-second precision. +- An assignment encodes the value as JSON and encrypts it with the configured algorithm. The row stores `[ciphertext, iv, authTag]`, each in base64. +- Each write generates a random initialization vector (IV). +- An authenticated algorithm, such as `aes-256-gcm`, also stores its auth tag. Other algorithms store no tag. +- A read decrypts the value. +- A `Date` comes back as a `Date` instance. The encoder writes it with `toUTCString()`, which drops milliseconds, so an encrypted date keeps whole-second precision only. #### Options @@ -57,9 +61,9 @@ class UserCredentials extends Table.with(EncryptionModifier) { | `algorithm` | `string` | `"aes-256-gcm"` | Encryption algorithm. | | `ivSize` | `number` | `16` | Initialization vector size in bytes. | -### HashModifier +### `HashModifier` -The `HashModifier` stores a salted one-way hash of field values. Unlike encryption, hashing is irreversible -- you cannot retrieve the original value. By default it stores a scrypt hash, a memory-hard function fit for passwords. +The `HashModifier` stores a salted one-way hash of a field's value. A hash cannot be reversed, so the original value cannot be read back. By default the field stores a scrypt hash, a memory-hard function made for passwords. ```typescript import { @@ -83,37 +87,41 @@ class User extends Table.with(HashModifier) { } ``` -#### How It Works +#### How it works -- Each write generates a random salt and stores `$scrypt$ln=15,r=8,p=3$$` in the field. The string holds the algorithm, the cost, the salt and the hash, so the cost can change later. -- Assigning the stored hash back to the field, or calling `lock` on a loaded row, keeps the hash as it is. -- `testHash` and `testHashAsync` read the cost and the salt from the stored string, hash the candidate and compare in constant time. -- A test spends one scrypt run whatever the row holds, so the time of a refusal does not tell whether a hash exists. -- A write is synchronous, because a property setter cannot wait. Assigning a value blocks the event loop for one scrypt run, about 150 ms and 32 MiB. +- The field hashes the value encoded as JSON, so `"secret"` is hashed as the text `"\"secret\""`. +- Each write draws a random 16-byte salt and stores `$scrypt$ln=15,r=8,p=3$$`. The salt and the 32-byte key are base64 without padding. `ln` is the base-2 logarithm of the scrypt cost N, so N is 32768. The string holds the parameters, the salt, and the key, so the cost can change later. +- A write is synchronous, because a property setter cannot wait, and so are the conversions behind a model's `insert` and `update`. Each assignment blocks the event loop for one scrypt run, which uses 32 MiB (128 × N × r bytes). +- Assigning the stored hash back to the field, or calling `lock` on a loaded row, keeps the hash as it is. Assigning `undefined` stores nothing. +- `testHash` and `testHashAsync` read `ln`, `r`, `p`, the salt, and the key from the stored string. They derive a key of the stored key's length from the candidate and compare the two with `crypto.timingSafeEqual`, in constant time. +- A stored scrypt string verifies only within these limits: `ln`, `r`, and `p` from 1 to 99, at most 128 MiB of memory, `p` at most 16, `ln` below 16 × `r`, and a salt and a key of 16 to 64 bytes each. A string outside them refuses every candidate. +- On a field without an `algorithm`, a test spends one scrypt run whatever the row holds, a hash in another format or nothing included. The time of a refusal does not tell whether the row holds a current hash. #### Options -| Option | Type | Default | Description | -| ----------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `algorithm` | `string` | none | Digest to store instead of scrypt, any name `createHash` accepts. A digest is fast to guess: use it only for non-secret fingerprints, never for credentials. Rows stay in the bare digest format. | +| Option | Type | Default | Description | +| ----------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `algorithm` | `string` | none | Digest to store instead of scrypt, any name `createHash` accepts. A digest is fast to guess, so use it only for non-secret fingerprints, never for credentials. See the next list. | -#### Mixin Methods +A field with an `algorithm` stores the base64 digest of the row's salt followed by the JSON-encoded value. The salt is a random string that the first such write creates. The row keeps it in `_internal.meta.HashModifier.salt` and reuses it for every later write to its digest fields. A test of a digest field that holds a digest spends no scrypt run. The field still verifies a scrypt string it holds, with one scrypt run, and `isHashOutdated` never flags it, so a scrypt hash is never flagged for replacement by a weaker digest. + +#### Mixin methods The `HashModifier` mixin adds these methods to table instances: ```typescript fragment -testHash(field: string, value: unknown): boolean -testHashAsync(field: string, value: unknown): Promise -isHashOutdated(field: string): boolean +testHash(this: This, field: F, value: This[F]): unknown +testHashAsync(this: This, field: F, value: This[F]): Promise +isHashOutdated(this: This, field: keyof This): boolean ``` -- `testHashAsync` returns whether `value` matches the stored hash. It runs scrypt on the thread pool, so use it on a server. -- `testHash` returns the same result and blocks the event loop while scrypt runs. -- `isHashOutdated` returns whether the stored hash differs from what the field writes today. It is always `false` on a field with an `algorithm`, and when nothing is stored. +- `testHashAsync` resolves whether `value` matches the stored hash. It runs scrypt on the thread pool, so use it on a server. It rejects with `Field is not hashed` for a field without `@Hashed`. +- `testHash` gives the same result and blocks the event loop while scrypt runs. Its declared return type is `unknown`. On a hashed field, the value is a boolean. +- `isHashOutdated` returns whether the stored hash differs from what the field writes today, which is any stored string that does not start with `$scrypt$ln=15,r=8,p=3$`. It returns `false` when nothing is stored, and always on a field with an `algorithm`. It throws `Field is not hashed` for a field without `@Hashed`. -#### Upgrade Stored Hashes +#### Upgrade stored hashes -A row written before scrypt became the default holds a bare salted SHA-256. It still verifies, and `isHashOutdated` returns `true` for it. The same applies to a hash of an earlier cost. To upgrade a row, assign the value after a successful test, because that is the only moment the plaintext is known: +A row whose hash has another format still verifies. On a field without an `algorithm`, a string that is not a scrypt hash is tested as a salted SHA-256 digest, made with the salt in `_internal.meta.HashModifier.salt`, and `isHashOutdated` returns `true` for it. A scrypt hash of another cost verifies with its own parameters and is flagged too. To rewrite a flagged hash, assign the value after a successful test, because only then is the plaintext known: ```typescript fragment if (await user.testHashAsync("password", password)) { @@ -124,13 +132,13 @@ if (await user.testHashAsync("password", password)) { } ``` -To move a credential off `@Hashed({ algorithm: "sha256" })`, remove the option. Its rows keep verifying and are flagged for upgrade. A row written with another digest verifies only while the field declares that digest. +`userModel.update(user._id, { password })` rewrites the hash too. An update that leaves the hashed field out, by ID or with the whole row, keeps the stored hash. -`LocalizationModifier.fallbackLocaleOf(TableClass, field)` returns a field's `fallbackLocale`, or `undefined` when the field is not localized or declares none. Generated forms use it to require a value in that locale. +To move a credential off `@Hashed({ algorithm: "sha256" })`, remove the option. Its rows keep verifying, and `isHashOutdated` flags them. A row written with another digest verifies only while the field declares that digest. -### LocalizationModifier +### `LocalizationModifier` -The `LocalizationModifier` stores multiple language versions of a field value. Access a specific locale by calling `localize()` on the table instance. +The `LocalizationModifier` stores one value of a field per locale. To read or write a locale, call `localize()` on the table instance. ```typescript import { @@ -154,6 +162,8 @@ class Product extends Table.with(LocalizationModifier) { } ``` +`LocalizationModifier.fallbackLocaleOf(TableClass, field)` returns a field's `fallbackLocale`, or `undefined` when the field is not localized or declares none. Generated forms use it to require a value in that locale. + #### Usage ```typescript fragment @@ -170,30 +180,29 @@ product.localize("es").name = "Auriculares Premium"; const spanish = product.localize("es"); console.log(spanish.name); // "Auriculares Premium" -// Fallback to default locale when translation is missing +// Fall back to the default locale when a translation is missing const german = product.localize("de"); console.log(german.name); // "Premium Headphones" (fallback) ``` -#### Wildcard Key +#### Wildcard key -The wildcard key `"*"` retrieves or sets all translations at once at runtime. `localize()` does not change the property's TypeScript type, so a field declared as `string` needs an explicit view for bulk access: +The wildcard key `"*"` reads or writes all translations at once. `localize()` does not change the property's TypeScript type, so a field declared as `string` holds a record of texts by locale under `"*"`. Read that record as `unknown` and check it before use. Write it with `Object.assign`, which calls the field's setter: ```typescript fragment // Get all translations -const translations = product.localize("*") as unknown as { - name: Record; -}; -const allNames = translations.name; +const allNames: unknown = product.localize("*").name; // { en: "Premium Headphones", fr: "Casque Premium", es: "Auriculares Premium" } // Set all translations -translations.name = { - en: "Premium Headphones", - fr: "Casque Premium", - es: "Auriculares Premium", - de: "Premium-Kopfhoerer", -}; +Object.assign(product.localize("*"), { + name: { + en: "Premium Headphones", + fr: "Casque Premium", + es: "Auriculares Premium", + de: "Premium-Kopfhoerer", + }, +}); ``` #### Options @@ -202,23 +211,23 @@ translations.name = { | ---------------- | -------- | --------------------------------------------------------------------- | | `fallbackLocale` | `string` | Locale to use when the requested locale has no value or an empty one. | -#### Mixin Method +#### Mixin method The `LocalizationModifier` mixin adds a `localize` method: ```typescript fragment -localize(locale: string, fields?: Array): this +localize(this: This, locale: string, fields?: Array): This ``` -`localize()` changes the active locale and returns the same object, not a locale-specific copy. +`localize()` changes the active locale and returns the same object, not a copy for that locale. - **References share state.** `spanish`, `german`, and `product` above refer to one object. A later `localize()` call changes what all three references read. -- **Fields can be restricted.** When you supply `fields`, only those fields are unlocked for the selected locale. -- **Copy values before switching.** Save the values you need before changing locales, and do not share one instance between concurrent locale-specific operations. +- **Fields can be restricted.** When you supply `fields`, only those fields switch to the selected locale. +- **Copy values before switching.** Save the values you need before you change locales, and do not share one instance between concurrent operations on different locales. -### AutoDateModifier +### `AutoDateModifier` -The `AutoDateModifier` automatically populates timestamp fields during insert and update operations. Use the `CreationTime` and `UpdateTime` decorators to mark fields. +The `AutoDateModifier` sets timestamp fields on insert and update. Mark the fields with the `CreationTime` and `UpdateTime` decorators. ```typescript import { @@ -238,15 +247,15 @@ class Article extends Table { } ``` -#### How It Works +#### How it works -- On insert, both `CreationTime` and `UpdateTime` fields are set to the current date. -- On update, `UpdateTime` fields are refreshed while `CreationTime` fields are removed from the payload so the original creation date is preserved. -- This is an event-only modifier: it does not transform stored values and does not require adding a mixin through `Table.with()`. +- On insert, both `CreationTime` and `UpdateTime` fields get the current date. +- On update, `UpdateTime` fields get the current date, and `CreationTime` fields are removed from the payload, so the stored creation date stays. +- The modifier only handles events. It does not transform stored values and has no mixin, so the table class needs no `Table.with()`. -## Combine Multiple Modifiers +## Combine multiple modifiers -Pass multiple modifier classes to `Table.with()`: +Pass several modifier classes to `Table.with()`: ```typescript import { @@ -287,11 +296,11 @@ class UserProfile extends Table.with( } ``` -## Create Custom Modifiers +## Create custom modifiers -### OneWayModifier +### `OneWayModifier` -A `OneWayModifier` transforms data in one direction (when writing to the database). Use it for irreversible transformations like hashing. +A `OneWayModifier` transforms data in one direction, when a value is written. Use it for a transformation that cannot be reversed, such as hashing. ```typescript import { @@ -332,18 +341,18 @@ const PrefixHashed = MakePropertyDecorator( ); ``` -The generic parameters for `OneWayModifier` are: +The generic parameters of `OneWayModifier` are: -| Parameter | Description | -| ------------ | ---------------------------------------------------- | -| `LockedType` | The type stored in the database after transformation | -| `Args` | Additional arguments for `lock` and `test` | -| `Meta` | Per-field metadata stored alongside the value | -| `Options` | Configuration passed via the decorator | +| Parameter | Description | +| ------------ | -------------------------------------------------------------------------------------------------------- | +| `LockedType` | The type stored in the database after the transformation. | +| `Args` | Additional arguments for `lock` and `test`. Defaults to `[]`. | +| `Meta` | The modifier's state for a row, kept in `_internal.meta.` and shared by its fields. | +| `Options` | The configuration that the decorator passes to `attachModifier`. | -### TwoWayModifier +### `TwoWayModifier` -A `TwoWayModifier` extends `OneWayModifier` with an `unlock` method for reversible transformations like encryption: +A `TwoWayModifier` extends `OneWayModifier` with an `unlock` method, for a transformation that can be reversed, such as encryption: ```ts docs-check import { @@ -355,7 +364,7 @@ import { MakePropertyDecorator } from "@antelopejs/interface-core/decorators"; import type { ValueProxy } from "@antelopejs/interface-database"; class Base64Modifier extends TwoWayModifier< - string | undefined, + unknown, [], {}, { prefix?: string } @@ -363,23 +372,23 @@ class Base64Modifier extends TwoWayModifier< public readonly autolock = true; public readonly autounlock = true; - public override lock(_locked: string | undefined, value: unknown) { + public override lock(_locked: unknown, value: unknown) { if (value === undefined) return undefined; const prefix = this.options.prefix || "b64-"; return `${prefix}${Buffer.from(JSON.stringify(value)).toString("base64")}`; } - public override unlock(locked: string | undefined): unknown { - if (locked === undefined) return undefined; + public override unlock(locked: unknown): unknown { + if (typeof locked !== "string") return undefined; const prefix = this.options.prefix || "b64-"; const encoded = locked.substring(prefix.length); return JSON.parse(Buffer.from(encoded, "base64").toString("utf8")); } public override unlockrequest( - data: ValueProxy, + data: ValueProxy, ): ValueProxy { - return data.cast(); + return data; } [MixinSymbol] = class {}; @@ -397,11 +406,11 @@ const Base64Encoded = MakePropertyDecorator( ); ``` -Base64 is encoding, not encryption. This example's `unlockrequest()` leaves the encoded value unchanged in database expressions; only instance reads decode it. +Base64 is an encoding, not encryption. The example declares its `LockedType` as `unknown`, so `unlockrequest()` can return the stored value's proxy as it is. Database expressions therefore see the encoded value, and only instance reads decode it. `unlock` reads a stored value that is not a string as `undefined`. -### ContainerModifier +### `ContainerModifier` -For modifiers that store multiple values in a key-value structure (like `LocalizationModifier`), extend `ContainerModifier`. It provides built-in support for the wildcard key `"*"` for bulk operations: +A modifier that stores several values under keys, as `LocalizationModifier` does, extends `ContainerModifier`. Its `lock`, `unlock`, and `unlockrequest` take the key as their argument. `lock` and `unlock` treat the wildcard key `"*"` as the whole record: ```typescript import { ContainerModifier } from "@antelopejs/interface-database-decorators"; @@ -412,18 +421,18 @@ class MyContainerModifier extends ContainerModifier<{}, { fallback?: string }> { } ``` -## Modifier Events +## Modifier events -Modifiers can hook into the data lifecycle by implementing event handler methods. These methods are called during data conversion and database operations: +A modifier hooks into a row's lifecycle through methods named after events. The engine calls each one with the modifier as `this`, then the table instance, the field name, and the event's arguments: -| Event | Triggered When | -| --------------- | ------------------------------------------ | -| `fromPlainData` | After converting plain data to an instance | -| `toPlainData` | After converting an instance to plain data | -| `fromDatabase` | After reading from the database | -| `toDatabase` | Before writing to the database | -| `insert` | Before inserting a new record | -| `update` | Before updating an existing record | +| Event | Triggered when | +| --------------- | --------------------------------------------------------------------- | +| `fromPlainData` | After plain data is converted to an instance. | +| `toPlainData` | After an instance is converted to plain data. | +| `fromDatabase` | After a stored record is converted to an instance, as reads do. | +| `toDatabase` | When an instance is converted to the record to store, before a write. | +| `insert` | Before a model or a fixture inserts a new record. | +| `update` | Before a model updates an existing record. | ```typescript import { @@ -432,15 +441,15 @@ import { } from "@antelopejs/interface-database-decorators"; class TimestampModifier extends Modifier { - public insert(instance: any, field: string) { - if (field === "createdAt" && !instance[field]) { - instance[field] = new Date(); + public insert(object: Partial>, field: F) { + if (field === "createdAt" && !object[field]) { + object[field] = new Date(); } } - public update(instance: any, field: string) { + public update(object: Partial>, field: F) { if (field === "updatedAt") { - instance[field] = new Date(); + object[field] = new Date(); } } @@ -448,4 +457,4 @@ class TimestampModifier extends Modifier { } ``` -Event-only modifiers (without `lock`/`unlock`) are valid and useful for side effects like logging, validation, or auto-populating fields. +A modifier without `lock` and `unlock` only handles events. `attachModifier` then defines no accessor on the field, which keeps its plain value. Such a modifier fits side effects such as logging, validation, or filling in fields. diff --git a/docs/4.data-models.md b/docs/4.data-models.md index 3276ce0..fbe59a3 100644 --- a/docs/4.data-models.md +++ b/docs/4.data-models.md @@ -1,10 +1,14 @@ -# Data Models +--- +title: Data models +description: Reference for BasicDataModel, its CRUD methods and return values, the data conversions, validation, and GetModel. +mode: reference +--- -Data models provide a high-level interface for interacting with database tables. They handle conversion between database records and TypeScript class instances, and expose methods for common CRUD operations. +A data model reads and writes one table. It converts between stored records and instances of the table class, and has methods for the common CRUD operations. -## BasicDataModel +## `BasicDataModel` -The `BasicDataModel` function creates a model class for a specific table. It wraps the underlying AQL table with methods that automatically apply modifier transformations. +The `BasicDataModel` function creates a model class for a table. The class wraps the AQL table with methods that apply the modifier transformations. ```typescript fragment import { BasicDataModel } from "@antelopejs/interface-database-decorators"; @@ -15,27 +19,28 @@ const UserModel = BasicDataModel(User, "users"); ### Parameters -| Parameter | Type | Description | -| ----------- | ------------------ | --------------------------------------------------------------------- | -| `dataType` | `Constructible` | The Table class representing your table | -| `tableName` | `string` | The table name in the database (optional if `@RegisterTable` is used) | +| Parameter | Type | Description | +| ----------- | ------------------ | ----------------------------------------------------------------------- | +| `dataType` | `Constructible` | The table class. | +| `tableName` | `string` | The table name in the database. Optional when `@RegisterTable` sets it. | -When the table class has been decorated with `@RegisterTable`, the `tableName` parameter can be omitted. +When the table class has `@RegisterTable`, you can omit `tableName`. Without either, `BasicDataModel` throws `tableName must be provided or set via @RegisterTable`. -## Instantiate a Model +## Instantiate a model -A model instance requires a `SchemaInstance` from the database interface: +A model instance needs a `SchemaInstance` from the database interface: ```typescript fragment import { Schema } from "@antelopejs/interface-database"; -const schema = Schema.get("myapp")!; +const schema = Schema.get("myapp"); +if (!schema) throw new Error("Schema myapp is not registered"); const db = schema.instance(); const userModel = new UserModel(db); ``` -## CRUD Operations +## Read and write rows For a model created with `BasicDataModel(User)`, the return values are: @@ -47,7 +52,7 @@ For a model created with `BasicDataModel(User)`, the return values are: | `update(...)` | `Promise` | Number of modified documents. | | `delete(id)` | `Promise` | Number of deleted documents. | -Use `model.table` to compose AQL queries before execution. The static `fromDatabase()` conversion can return `undefined`; `toDatabase()` returns a plain `Record`, not a saved record or a write result. +Use `model.table` to compose an AQL query before you run it. The static `fromDatabase()` conversion returns `undefined` when the record is `null` or `undefined`, as `table.get` gives for a missing ID. The static `toDatabase()` returns a `StoredRow`, the record to store, not a saved record or a write result. ### Read @@ -88,7 +93,7 @@ await userModel.update("user-123", { name: "Updated Name" }); await userModel.update({ _id: "user-123", name: "Updated Name" }); ``` -> **Important:** When calling `update` without a separate ID argument, the object must include the primary key field. Its type requires `_id`. +When you call `update` without a separate ID argument, the object must include the primary key. Its type requires `_id`, and a call without it throws `Missing primary key in object update.` ### Delete @@ -96,9 +101,9 @@ await userModel.update({ _id: "user-123", name: "Updated Name" }); await userModel.delete("user-123"); ``` -## Data Conversion +## Data conversion -Data models include static methods for converting between different data representations: +A model class has static methods that convert between the forms of a row: | Starting value | Conversion | Result | Persists data? | | ----------------------- | --------------- | ------------------------------------------------- | -------------- | @@ -106,13 +111,13 @@ Data models include static methods for converting between different data represe | Raw database record | `fromDatabase` | Table instance with read modifier events applied | No | | Table instance | `toDatabase` | Plain storage representation | No | -Use `insert` or `update` to perform a write; conversion alone does not save anything. Reads through `get`, `getBy`, and `getAll` apply `fromDatabase` for you. A custom query through `model.table` returns AQL results, so apply conversion explicitly when you need model instances. +Use `insert` or `update` to write. A conversion alone saves nothing. Reads through `get`, `getBy`, and `getAll` apply `fromDatabase` for you. A custom query through `model.table` returns AQL results, so apply conversion explicitly when you need model instances. -![Model writes convert input, trigger insert or update events, convert to storage, then execute AQL. Model reads convert fetched records; direct model.table queries bypass model conversion.](./assets/model-conversion.svg){width="480"} +![Model writes convert input, trigger insert or update events, convert to storage, then execute AQL. Model reads convert fetched records. Direct model.table queries bypass model conversion.](./assets/model-conversion.svg "illustration"){width="480"} ### `fromPlainData(obj)` -Convert a plain JavaScript object into a Table class instance. Modifier transformations (like encryption and hashing) are applied during this conversion. +Converts a plain JavaScript object into an instance of the table class. Each key is assigned, so modifiers such as encryption and hashing transform the values during this conversion. An instance of the table class is returned as it is. ```typescript fragment const user = UserModel.fromPlainData({ @@ -122,11 +127,11 @@ const user = UserModel.fromPlainData({ }); ``` -The object is a `PlainData`: every field is optional and may be partial, and a field under a container modifier may hold its value per key, as a localized field holds one text per locale. `toPlainData` gives the reverse form, a `PlainRow`. +The object is a `PlainData`. Every field is optional and may be partial, and a field under a container modifier may hold its value per key, as a localized field holds one text per locale. `toPlainData` gives the reverse form, a `PlainRow`. ### `fromDatabase(obj)` -Convert a raw database record into a Table class instance. This attaches the class prototype and triggers the `fromDatabase` modifier event. +Converts a stored record into an instance of the table class. It copies the record, sets the class prototype on the copy, and triggers the `fromDatabase` modifier event. It returns `undefined` when the record is `null` or `undefined`, as `table.get` gives for a missing ID. An empty object still gives an instance. ```typescript fragment const user = UserModel.fromDatabase(rawDbRecord); @@ -134,7 +139,7 @@ const user = UserModel.fromDatabase(rawDbRecord); ### `toDatabase(obj)` -Convert a Table class instance into a plain object suitable for database storage. Modifier events like `toDatabase` are triggered. The result is a `StoredRow`: the set fields, with no prototype, and its modified fields locked in `_internal` instead of at the top level. +Converts an instance of the table class, or plain data, into the record to store, and triggers the `toDatabase` modifier event. The result is a `StoredRow`. It holds the fields that are set, has no prototype, and keeps its modified fields locked in `_internal` instead of at the top level. ```typescript fragment const dbReady = UserModel.toDatabase(userInstance); @@ -142,29 +147,29 @@ const dbReady = UserModel.toDatabase(userInstance); ## Validation -`Model.validate(obj)` runs every io-ts codec attached via `@Field` against the matching property of `obj`. String-token fields and properties without `@Field` are skipped. +`Model.validate(obj)` runs every io-ts codec that `@Field` attached against the matching property of `obj`. It skips string-token fields and properties without `@Field`. A failure's `message` is the first message the codec reports, or `invalid ` when the codec reports none, as io-ts's built-in codecs do. ```typescript fragment const result = UserModel.validate({ name: "Alice", age: 30 }); if (!result.ok) { - console.error(result.errors); // [{ field: "age", message: "not number" }, ...] + console.error(result.errors); // [{ field: "age", message: "invalid age" }] } ``` -Pass `{ validate: true }` to `insert` or `update` to gate the write on validation. On failure the call throws an `Error` whose `errors` property holds the per-field details; on success the original object passes through unchanged. +Pass `{ validate: true }` to `insert` or `update` to validate before the write. On failure the call throws an `Error` with the message `validation failed: (), ...` and an `errors` property that holds the details per field. On success the write goes on with the object unchanged. ```typescript fragment await userModel.insert({ name: "Alice", age: 30 }, { validate: true }); await userModel.update("user-123", { name: "Bob" }, { validate: true }); ``` -`insert` validates every annotated codec field. `update` is partial: only fields present on the patch object are checked, so omitting fields you don't intend to modify is fine. Use `Model.validate(obj, { partial: true })` for the same patch semantics outside of `update`. +`insert` validates every codec field of each row. `update` validates only the fields present on the patch object, so you can leave out the fields you do not change. For the same patch check outside `update`, use `Model.validate(obj, { partial: true })`. -## Extend BasicDataModel +## Extend `BasicDataModel` -Extend the generated model class to add custom query methods: +Extend the generated model class to add custom query methods. -The `register` method below checks for an existing email to provide early feedback. It does not guarantee uniqueness under concurrent writes; enforce that constraint in the database where supported and handle conflicts. +The `register` method below checks for an existing email to report a duplicate early. It does not guarantee uniqueness under concurrent writes. Enforce that constraint in the database with a unique index where the database module supports it, and handle the conflict. `findActive` converts each row with `fromDatabase`, which returns `undefined` only when the record is `null` or `undefined`, and the filter drops a missing row. ```typescript fragment const UserModelBase = BasicDataModel(User, "users"); @@ -173,7 +178,11 @@ class CustomUserModel extends UserModelBase { async findActive() { return this.table .filter((u) => u.key("status").eq("active")) - .then((rows) => rows.map(CustomUserModel.fromDatabase) as User[]); + .then((rows) => + rows + .map((row) => CustomUserModel.fromDatabase(row)) + .filter((user) => user !== undefined), + ); } async register(email: string, name: string) { @@ -186,9 +195,9 @@ class CustomUserModel extends UserModelBase { } ``` -## GetModel +## `GetModel` -The `GetModel` function retrieves or creates a cached model instance for a given schema: +The `GetModel` function returns the cached model instance for a model class and an instance ID, and creates it on the first call: ```typescript fragment import { GetModel } from "@antelopejs/interface-database-decorators"; @@ -196,14 +205,14 @@ import { GetModel } from "@antelopejs/interface-database-decorators"; const userModel = GetModel(UserModel, "instance-id"); ``` -Model instances are cached by class and instance ID, so repeated calls with the same arguments return the same instance. A model built by `BasicDataModel` uses the schema its table class was bound to by `RegisterSchema`, so it stays on the declaring module's database even when another module calls `GetModel`; an unbound table class throws. A custom model class without a `tableClass` is resolved from its `schemaName` with `Schema.get`. +The cache key is the class and the instance ID, so repeated calls with the same arguments return the same instance. Without an instance ID, the model uses the default instance. A model built by `BasicDataModel` uses the schema that `RegisterSchema` bound its table class to, so it stays on the declaring module's database even when another module calls `GetModel`. An unbound table class whose schema name has no schema throws ` is not bound to a schema`. A custom model class without a `tableClass` gets its schema from its `schemaName` through `Schema.get`. -## Modifier Integration +## Modifier integration -Data models automatically apply all modifier transformations: +Data models apply every modifier transformation: -- **On insert** - `fromPlainData` conversion is applied, the `insert` event fires, and the result is converted with `toDatabase` before writing. -- **On update** - The same conversion pipeline applies with the `update` event. -- **On read** - Results from `get`, `getBy`, and `getAll` are converted with `fromDatabase`. +- On insert, the model converts the data with `fromPlainData`, fires the `insert` event, and converts the result with `toDatabase` before the write. +- On update, the model runs the same conversions with the `update` event. It leaves out an empty `_internal`, so a patch keeps what the row stores there. +- On read, the model converts the results of `get`, `getBy`, and `getAll` with `fromDatabase`. -This means encrypted fields are encrypted before storage and decrypted on retrieval, hashed fields are hashed on write, and localized fields follow their configured locale behavior -- all transparently. +As a result, encrypted fields are encrypted before storage and decrypted on read, hashed fields are hashed on write, and localized fields follow their locale settings. diff --git a/docs/5.parameter-decoration.md b/docs/5.parameter-decoration.md index 110d8c2..b16c668 100644 --- a/docs/5.parameter-decoration.md +++ b/docs/5.parameter-decoration.md @@ -1,14 +1,18 @@ -# Parameter Decoration +--- +title: Parameter decoration +description: Inject a data model into an API controller method or property with the Model decorator, with a fixed or a per-request instance ID. +mode: how-to +--- -The Database Decorators package provides the `Model` decorator for injecting data model instances into API controller methods. This decorator integrates with the `@antelopejs/interface-api` package's parameter provider system. +The `Model` decorator injects a data model instance into an API controller method. It registers a parameter provider of `@antelopejs/interface-api` through `SetParameterProvider`. -## The Model Decorator +## The `Model` decorator -The `Model` decorator injects a cached model instance. It can be used as either a parameter decorator or a property decorator. +The `Model` decorator injects the cached model instance that `GetModel` returns. It works as a parameter decorator and as a property decorator. -### Static Instance ID +### Pass a fixed instance -Pass a fixed instance ID to bind the model to a specific schema instance: +To bind the model to one schema instance, pass a fixed instance ID: ```typescript fragment import { Controller, Get } from "@antelopejs/interface-api"; @@ -25,11 +29,11 @@ class UsersController extends Controller("/users") { } ``` -### Dynamic Instance ID +### Pick the instance per request -Pass a callback function to determine the instance ID at request time. The callback receives the request context: +To pick the instance ID for each request, pass a callback. It receives the request context and returns an instance ID, or `undefined` for the default instance. -Authorize access to the requested tenant before reading or writing through the injected model. The callback selects an instance; a URL parameter alone is not proof that the caller belongs to that tenant. +Authorize access to the requested tenant before you read or write through the injected model. The callback only selects an instance, and a URL parameter alone does not prove that the caller belongs to that tenant. ```typescript fragment import { Controller, Get, Parameter } from "@antelopejs/interface-api"; @@ -48,9 +52,9 @@ class MultitenantController extends Controller("/tenants") { } ``` -## Property Decoration +## Property decoration -The `Model` decorator also works as a property decorator. The model is initialized when the controller handles a request: +The `Model` decorator also works as a property decorator. The property gets its model when the controller handles a request. Outside a request handler, the property is unset, although the `!` declaration types it as always present: ```typescript fragment import { Controller, Get, Parameter } from "@antelopejs/interface-api"; @@ -73,9 +77,9 @@ class UsersController extends Controller("/users") { } ``` -## Model Caching +## Model caching -The `Model` decorator uses `GetModel` internally, which caches model instances by class and instance ID. Multiple decorators referencing the same model class and instance ID share a single instance: +The `Model` decorator calls `GetModel`, which caches model instances by class and instance ID. Decorators that name the same model class and instance ID share one instance: ```typescript fragment import { GetModel } from "@antelopejs/interface-database-decorators"; @@ -88,11 +92,11 @@ const model2 = GetModel(UserModel, "main"); const model3 = GetModel(UserModel, "other"); ``` -## Combine with API Decorators +## Combine with API decorators -Model decorators compose naturally with other API decorators for building complete CRUD controllers: +`@Model` combines with the other API decorators in a CRUD controller. -This example demonstrates injection, not a complete public API policy. Before exposing writes, reject malformed or non-object JSON, allow only permitted fields, validate their values, and authorize the operation. `JSON.parse` and `@Model` do not provide these checks; string-token `@Field` declarations do not validate input either. +This example shows injection, not a complete public API policy. Before you expose writes, reject malformed or non-object JSON, allow only permitted fields, validate their values, and authorize the operation. `JSON.parse` and `@Model` do none of these checks, and string-token `@Field` declarations do not validate input either. ```typescript fragment import { @@ -150,9 +154,9 @@ class UsersController extends Controller("/users") { } ``` -## Custom Parameter Providers +## Custom parameter providers -For more complex injection logic, create custom parameter providers using the core API: +For other injection logic, write your own decorator that registers a parameter provider: ```typescript import { SetParameterProvider } from "@antelopejs/interface-api"; diff --git a/package.json b/package.json index 29621ee..8b1d78f 100644 --- a/package.json +++ b/package.json @@ -183,6 +183,9 @@ "test": "src/antelope.test.ts", "skills": [ "./skills" - ] + ], + "docs": { + "standard": true + } } } From 98dac2d455f2e6a57c83ec81e44773e1c64e962a Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 3 Nov 2026 15:36:59 +0100 Subject: [PATCH 19/45] feat(skills): add the verify-interface verification skill --- .agents/skills/verify-interface/SKILL.md | 137 ++++++++++++ .../verify-interface/features/README.md | 36 +++ .../verify-interface/features/autodate.md | 30 +++ .../features/custom-modifiers.md | 38 ++++ .../verify-interface/features/declarations.md | 33 +++ .../verify-interface/features/encryption.md | 34 +++ .../verify-interface/features/hashing.md | 38 ++++ .../verify-interface/features/localization.md | 34 +++ .../verify-interface/features/models.md | 41 ++++ .../features/parameter-decoration.md | 36 +++ .../verify-interface/features/registration.md | 40 ++++ .../skills/verify-interface/features/smoke.md | 35 +++ .../verify-interface/features/tables.md | 42 ++++ .agents/skills/verify-interface/package.json | 4 + .agents/skills/verify-interface/runner.ts | 141 ++++++++++++ .../verify-interface/scenarios/custom.ts | 115 ++++++++++ .../scenarios/declarations.ts | 78 +++++++ .../verify-interface/scenarios/decoration.ts | 98 +++++++++ .../verify-interface/scenarios/encryption.ts | 102 +++++++++ .../verify-interface/scenarios/hashing.ts | 147 +++++++++++++ .../scenarios/localization.ts | 123 +++++++++++ .../verify-interface/scenarios/models.ts | 205 ++++++++++++++++++ .../scenarios/registration.ts | 152 +++++++++++++ .../verify-interface/scenarios/tables.ts | 147 +++++++++++++ .../seeded-breaks/autodate.patch | 13 ++ .../seeded-breaks/custom-modifiers.patch | 13 ++ .../seeded-breaks/declarations.patch | 12 + .../seeded-breaks/encryption.patch | 13 ++ .../seeded-breaks/hashing.patch | 13 ++ .../seeded-breaks/localization.patch | 13 ++ .../seeded-breaks/models.patch | 13 ++ .../seeded-breaks/parameter-decoration.patch | 13 ++ .../seeded-breaks/registration.patch | 13 ++ .../seeded-breaks/smoke.patch | 13 ++ .../seeded-breaks/tables.patch | 13 ++ .../seeded-doors/contract.patch | 11 + .../verify-interface/seeded-doors/data.patch | 13 ++ .../seeded-doors/format.patch | 13 ++ .../seeded-doors/publish.patch | 13 ++ .agents/skills/verify-interface/tsconfig.json | 14 ++ .../skills/verify-interface/verify.config.ts | 53 +++++ .claude/skills/verify-interface | 1 + AGENTS.md | 2 + 43 files changed, 2148 insertions(+) create mode 100644 .agents/skills/verify-interface/SKILL.md create mode 100644 .agents/skills/verify-interface/features/README.md create mode 100644 .agents/skills/verify-interface/features/autodate.md create mode 100644 .agents/skills/verify-interface/features/custom-modifiers.md create mode 100644 .agents/skills/verify-interface/features/declarations.md create mode 100644 .agents/skills/verify-interface/features/encryption.md create mode 100644 .agents/skills/verify-interface/features/hashing.md create mode 100644 .agents/skills/verify-interface/features/localization.md create mode 100644 .agents/skills/verify-interface/features/models.md create mode 100644 .agents/skills/verify-interface/features/parameter-decoration.md create mode 100644 .agents/skills/verify-interface/features/registration.md create mode 100644 .agents/skills/verify-interface/features/smoke.md create mode 100644 .agents/skills/verify-interface/features/tables.md create mode 100644 .agents/skills/verify-interface/package.json create mode 100644 .agents/skills/verify-interface/runner.ts create mode 100644 .agents/skills/verify-interface/scenarios/custom.ts create mode 100644 .agents/skills/verify-interface/scenarios/declarations.ts create mode 100644 .agents/skills/verify-interface/scenarios/decoration.ts create mode 100644 .agents/skills/verify-interface/scenarios/encryption.ts create mode 100644 .agents/skills/verify-interface/scenarios/hashing.ts create mode 100644 .agents/skills/verify-interface/scenarios/localization.ts create mode 100644 .agents/skills/verify-interface/scenarios/models.ts create mode 100644 .agents/skills/verify-interface/scenarios/registration.ts create mode 100644 .agents/skills/verify-interface/scenarios/tables.ts create mode 100644 .agents/skills/verify-interface/seeded-breaks/autodate.patch create mode 100644 .agents/skills/verify-interface/seeded-breaks/custom-modifiers.patch create mode 100644 .agents/skills/verify-interface/seeded-breaks/declarations.patch create mode 100644 .agents/skills/verify-interface/seeded-breaks/encryption.patch create mode 100644 .agents/skills/verify-interface/seeded-breaks/hashing.patch create mode 100644 .agents/skills/verify-interface/seeded-breaks/localization.patch create mode 100644 .agents/skills/verify-interface/seeded-breaks/models.patch create mode 100644 .agents/skills/verify-interface/seeded-breaks/parameter-decoration.patch create mode 100644 .agents/skills/verify-interface/seeded-breaks/registration.patch create mode 100644 .agents/skills/verify-interface/seeded-breaks/smoke.patch create mode 100644 .agents/skills/verify-interface/seeded-breaks/tables.patch create mode 100644 .agents/skills/verify-interface/seeded-doors/contract.patch create mode 100644 .agents/skills/verify-interface/seeded-doors/data.patch create mode 100644 .agents/skills/verify-interface/seeded-doors/format.patch create mode 100644 .agents/skills/verify-interface/seeded-doors/publish.patch create mode 100644 .agents/skills/verify-interface/tsconfig.json create mode 100644 .agents/skills/verify-interface/verify.config.ts create mode 120000 .claude/skills/verify-interface diff --git a/.agents/skills/verify-interface/SKILL.md b/.agents/skills/verify-interface/SKILL.md new file mode 100644 index 0000000..70db01a --- /dev/null +++ b/.agents/skills/verify-interface/SKILL.md @@ -0,0 +1,137 @@ +--- +name: verify-interface +description: "Drive this AntelopeJS interface the way a module author does: pack it, load it in `ajs project dev` next to a stub provider wired through ImplementInterface and a consumer, call every declared function through core, and check the minor-bump rule against the published version. Use to prove a change to the interface works before merge, to verify a PR, or to check that a seeded break is caught." +--- + +# Verify interface + +The user of an interface is a module author: one module implements it, others import it and call its functions through core. Every deterministic step is an `antelopejs-verify` command: you choose which journeys to run and judge what they show. Never replace a command with a shell recipe. When a journey you need does not exist, add it to `verify.config.ts`, name it in its feature file, then run it. + +`antelopejs-sync` writes this file, `package.json` and `tsconfig.json` here, and keeps them in sync. Everything else in `.agents/skills/verify-interface/` is the repository's own layer, which sync never touches. + +## Launch + +```bash +pnpm exec antelopejs-verify new --base origin/main --title "" +``` + +`new` prints the run folder; pass it as `` below. `new` runs the setup (`pnpm run build` by default), then packs the interface with `pnpm pack` and extracts it, so journeys load what npm would publish, `files` and `exports` included. Every `services` entry of `verify.config.ts` then starts in its own Docker container on a free loopback port, as for a module. Every journey gets its own `HOME` and scratch folder, `CI=1`, `AJS_NO_UPDATE_CHECK=1`, `NO_COLOR=1` and a free port from the kernel, so two runs never share an instance. Run it from the repository root after installing dependencies. + +## Doctor + +```bash +pnpm exec antelopejs-verify doctor +``` + +Doctor checks that HEAD has not moved since `new`, `ajs` comes from the repository's `@antelopejs/core`, the build is newer than `src/`, what `new` prepared is there, and no process the run recorded is still alive. Do not drive a run that fails doctor. + +## Drive + +```bash +pnpm exec antelopejs-verify plan --base origin/main +pnpm exec antelopejs-verify journey [journey...] +``` + +`plan` maps the diff to features and lists unmapped surfaces. `journey` runs a feature's journeys, or the ones named. The pack ships these journeys for this kind; a feature file names them next to the repository's own: + +- `every-function`: a fixture project loads the packed interface with a stub provider and a consumer module. The consumer walks the loaded interface, and every function the `.d.ts` files declare is called through core: + - an async proxy answers the stub's echo, and the stub's call log shows the same arguments (the second view); + - a registering proxy registers and unregisters, and the stub logs both; + - an event proxy delivers the emitted arguments to a registered handler; + - a plain function runs the `interface.calls` hook of `verify.config.ts` and must return its `expect`; one only providers call is listed in `interface.providerSide` with why. + A declared function with none of these fails the journey. +- `minor-bump`: reads the declared functions of the packed head and of the published version (`npm pack @`). An added, changed or removed function needs a release of at least the next minor. The release is the head version when it is already bumped, otherwise what changelogen makes of the change title (`new --title`) or the commit subjects since the base. + +## Evidence + +Each step writes `/evidence////` with `command.txt`, `stdout.txt`, `stderr.txt`, `exit-code.txt`, `pid.txt` for background processes, `tree.txt`, `http.json` for every request, and `checks.json`. `result.json` holds a journey's result and `feature.json` the merged results of the feature. Read the evidence before you judge a PASS: a check proves only what it asserts. + +## Cleanup + +```bash +pnpm exec antelopejs-verify clean +``` + +`clean` stops what the run started, by recorded PID and container name, and removes everything but `evidence/` and `run.json`. Run it after every run, failed ones included. + +## The repository's own layer + +- `verify.config.ts` default-exports a `VerifyConfig` from `@antelopejs/tooling-configs/verify`: + - `doors`: one entry per door kind (`data`, `format`, `contract`, `outbound`, `exposure`, `publish`), each `null` when it cannot apply or `{ globs, funnels, containments, why }`. `containments` names the `feature/journey` journeys that contain a Data or Format door. Each must fail with that kind's seeded door applied and pass without it: write the journey so one of its checks reads the state the door corrupts. + - `journeys`: the repository's journeys by id, each `{ title, entryPoint, run(context) }`, using the check helpers the same import exports. + - `env`, `setup` (commands replacing the build), `userFacing` (globs replacing the kind's default) and `formal` (formal checks: `name`, `covers`, `command`, `violation`, and optionally `behaviour` and `baseCommand`). +- `services`: image, port, readiness command and a `reset` command run before every journey, as for a module. `new` starts them in Docker on free ports; a journey reads them with `context.service(name)` and `context.native(...)`. +- `interface.provide(api, services)` runs in the stub provider after the generic stubs attach, for example to hand a client pointed at `services` to `internal.SetClient`. +- `interface.calls[":"]` calls a plain exported function from the consumer and states the value it must return. +- `interface.providerSide[":"]` names a function only providers call, with the reason. +- `features/.md`: frontmatter lists `covers:` (source globs), `docs:` (pages a verdict judges) and `journeys:`, then the sections Sub-features, How to get to it (user POV), Driving it with antelopejs-verify, and Gotchas. `features/smoke.md` covers nothing and runs on every verdict; here it runs `every-function`. +- `seeded-breaks/.patch`: one minimal product bug per feature that its journeys must catch. +- `seeded-doors/.patch`: one change per door kind that applies. A Data or Format door's patch must break what its containment journeys check. + +```bash +pnpm exec antelopejs-verify map +``` + +`map` fails on a user-facing file no feature covers, a feature without journeys, docs or seeded break, an unknown journey, an applicable door kind without its seeded door, and a Data or Format door whose `containments` name no journey a feature registers. `containments` (below) then runs those journeys against the seeded doors. + +## Verdict rules + +1. **Run every feature whose `covers:` globs the diff touches, plus `smoke`.** `plan` prints that list as `run`. +2. **An unmapped surface fails the verdict.** A changed user-facing file no feature covers needs a feature entry, its journeys and its seeded break in the same PR. +3. **Every registered journey of a feature in `run` must have run and passed.** Run whole features. A journey that could not reach the product is NOT VERIFIED, and so is the verdict, whatever else passed: never PASS and never PASS+NOTES. +4. **Docs.** Judge every `docs:` page of a touched feature against what the journeys showed. A stale page fails the verdict. +5. **Formal checks.** `pnpm exec antelopejs-verify formal ` runs every formal check of `verify.config.ts` that covers the diff, or whose own `files` the diff changes, and records it under `evidence/formal//`. For each one it decides, without judgement, whether the diff changed what the covered code does. `emit` (the default) compares the structure of the covered files' JavaScript on the base and on HEAD: node kinds, names and literal values, leaving out types, comments, formatting, parentheses and `export` keywords. Each use of an imported binding stands for its origin (the resolved module and the imported name), so an import retargeted to another module or alias counts as changed; a function declaration refers to the top-level variables and classes of its own file by file and name, so moving it next to a different variable of the same name counts as changed. Exports are keyed by the origin they export, so a re-export or an `export { f as run }` retargeted to another binding counts as changed. Each file must keep its top-level statements and its side-effect imports in order; function declarations, which are hoisted, may move between covered files with their imports. So a type-only edit, a reformat or a moved function declaration counts as unchanged, while reordering statements or moving any other statement counts as changed. `emit` judges behaviour, not the public surface: exports are the api-diff's job. `probe` runs the check's `behaviour.command` with a worktree of each commit and compares what it prints. Then: + - a covered check that did not run, or exits non-zero (a tool error, or a check that no longer matches its record), fails the verdict; + - recorded findings fail the verdict only when the covered behaviour changed; when it did not, the verdict reports them as known findings; + - when the covered behaviour changed, or the diff changed the check's own `files`, `formal` also runs the check on the base (`baseCommand` when set). A rule the base broke counts as fixed only when HEAD reports it passing (`passing`); a rule the base broke that HEAD does not report at all fails the verdict. A change that leaves no violated rule passes. + + The decision never reads the check's output, so a model whose rule names stay the same still fails on a changed behaviour. It is conservative: a local rename or an extracted helper counts as changed, and code outside `covers` is invisible to it. `--all` also runs the checks that do not cover the diff, without gating. +6. **One-way doors.** The section comes right after the result line and is mandatory. `pnpm exec antelopejs-verify doors` prints the `One-way-door:` line of the diff. Every file the path rules flag needs a door row or a false positive with its reason. A Data or Format row names a `containments` journey that passed in this run, and that journey is proven by `pnpm exec antelopejs-verify containments`: it fails with the kind's seeded door applied. A containment that passes with its door contains nothing, so a Data or Format door without one that fails with its door is not contained. +7. **Result.** FAIL if any rule fails. NOT VERIFIED when any journey of a required feature is NOT VERIFIED, for example a SaaS module run without its sandbox keys, however many other journeys passed. PASS+NOTES when everything required ran and passed but the judgement lists something no journey reached or something the owner should look at. PASS otherwise. + +Write your judgement to `/evidence/judgement.json`: + +```json +{ + "doors": { + "none": "why the diff opens no door", + "rows": [], + "falsePositives": [{ "kind": "publish", "location": "package.json", "reason": "only devDependencies changed" }] + }, + "docs": [{ "page": "README.md", "matches": true, "note": "" }], + "notVerified": ["What no journey reached, and why"], + "attention": ["What the owner should look at that the journeys cannot judge"], + "verifier": "the fresh thread that judged this run, not the author" +} +``` + +A door row is `{ "kind", "location", "irreversible", "containment", "containmentJourney" }`. Then render and publish: + +```bash +pnpm exec antelopejs-verify verdict --pr +pnpm exec antelopejs-verify publish --pr +``` + +`verdict` writes `evidence/verdict.md` and exits 1 unless the result is PASS or PASS+NOTES. `publish` commits the evidence to the orphan branch `verify-evidence` under `pr-//` from a temporary worktree, posts the verdict as a PR comment, sets commit status `verify` on the head SHA (`success` for PASS and PASS+NOTES, `error` for NOT VERIFIED, `failure` for FAIL) and reads it back. It prints the error GitHub answers when the token cannot write statuses. A new push voids the status. Post a verdict only from a thread that did not write the change. + +## Trust + +The skill is not Trusted yet. It becomes Trusted when it catches every seeded break, flags every seeded door, fails every Data and Format containment with its door, passes a shadow run of 10 consecutive agent PRs, and its last maintain pass is newer than the last change to the repository or its `@antelopejs` dependencies and at most 30 days old. + +## Seeded breaks and doors + +```bash +pnpm exec antelopejs-verify seeded +pnpm exec antelopejs-verify seeded-doors +pnpm exec antelopejs-verify containments +``` + +`seeded` runs each feature's journeys in a scratch worktree of HEAD without the patch, as the control, and with `seeded-breaks/.patch` applied. It prints `CAUGHT` only when a journey that passes in the control fails one of its checks with the patch. Journeys failing in the control and failures without a failing check do not count. `seeded-doors` prints `FLAGGED` when the path rules flag each patch with its own kind. `containments` runs every Data and Format containment journey in a scratch worktree of HEAD without the door, as the control, and with `seeded-doors/.patch` applied. It prints `CONTAINED` only when the journey passes in the control and fails one of its checks with the door. `MISSED` means it passes with its door, `UNPROVEN` that it fails in the control or ends without a failing check, and `NO-DOOR` that the kind has no seeded door; the command exits 1 on anything but `CONTAINED`. + +## Gotchas + +- A function the consumer awaits forever, such as a getter no provider resolved, fails after 15 seconds with a `Timeout` value. +- `minor-bump` needs the npm registry. Without it the journey is not verified, never passed. +- A containment journey of a Data or Format door must check what that door's seeded patch changes, for example the arguments the stub provider logged. `containments` reports one that still passes with its door as `MISSED`. +- Runs live under the system temp folder (`TMPDIR`), and the shared caches under `~/.cache/antelopejs-verify`. +- A repository that outgrows this skill runs `antelopejs-sync --verify custom` and keeps its own `.agents/skills/verify-/`. diff --git a/.agents/skills/verify-interface/features/README.md b/.agents/skills/verify-interface/features/README.md new file mode 100644 index 0000000..21f92ec --- /dev/null +++ b/.agents/skills/verify-interface/features/README.md @@ -0,0 +1,36 @@ +# Feature map + +One file per feature. `covers:` lists the source globs it owns, `docs:` the pages a verdict judges, `journeys:` the journeys it runs, and `seeded-breaks/.patch` the break its journeys must catch. `pnpm exec antelopejs-verify map` checks that every user-facing file is covered. + +Every journey runs a scenario inside `ajs module test` with `@antelopejs/mongodb` against a MongoDB container, then reads the database back with `mongosh`. The scenarios live in `scenarios/`, the runner in `runner.ts`. + +| Feature | What the user does | Journeys | +|---|---|---| +| [autodate](autodate.md) | Creation and update timestamps are set on write. | autodate | +| [custom-modifiers](custom-modifiers.md) | A developer writes a one-way, two-way, container or events-only modifier. | one-way-modifier, two-way-modifier, container-modifier, modifier-events | +| [declarations](declarations.md) | The packed type declarations match the return types the docs list. | model-signatures, modifier-signatures | +| [encryption](encryption.md) | A field is stored encrypted and read back decrypted. | encryption-at-rest, encryption-dates, encryption-keys | +| [hashing](hashing.md) | A field is stored hashed and tested against a candidate. | hash-storage, hash-verify, hash-limits, hash-upgrade, hash-digest | +| [localization](localization.md) | A field keeps one text per locale with a fallback. | localize, localize-wildcard, localized-storage | +| [models](models.md) | A developer reads and writes rows through a typed model. | crud, conversion, validation, get-model, model-factory, extend-model | +| [parameter-decoration](parameter-decoration.md) | A controller gets a model injected per route, instance or property. | fixed-instance, per-request-instance, property-decoration, crud-controller | +| [registration](registration.md) | A module registers its schema, fills fixtures and opens tables from their classes. | register-schema, fixtures, fixture-race, table-access | +| [smoke](smoke.md) | The fixed journeys every verdict runs. | register-schema, crud, minor-bump | +| [tables](tables.md) | A developer describes a table with Field, Index and Relation, and subclasses it. | index-options, group-flags, field-types, relation-metadata, inheritance, table-with | + +## Not driven + +| Documented behaviour | Why no journey drives it | +|---|---| +| A model "stays on the declaring module's database even when another module calls `GetModel`" (docs/4, GetModel) | Needs two modules with two databases behind one provider; one container serves one database here. | +| `testHash` and `testHashAsync` spend one scrypt run whatever the row holds, and the time of a refusal does not tell the row's format (docs/3, HashModifier) | A timing claim; a journey comparing wall-clock times would be flaky on a shared machine. | +| A digest field's test spends no scrypt run (docs/3) | Same: a timing claim. | +| A stored scrypt string with `ln` of at least 16 × `r` refuses every candidate (docs/3) | Node's own scrypt cannot compute such a hash, so no correct candidate exists to refuse; the boundary just below it is driven. | +| Encrypted fields keep whole-second dates only (docs/3) | Driven (`encryption-dates`). | +| `Fixture` generator for a class that does not extend the decorated one throws "does not extend" | Not reachable through the public API; the generator is wrapped by the decorator. | +| Queries that read a modified field through `model.table` (`unlockrequest`, locale fallback in AQL) | The docs only say `model.table` returns AQL results; they describe no query behaviour of the modifiers. | +| `@Model` on a method of a controller outside a request | Docs state the property is unset outside a request (driven in `property-decoration`); no other claim. | +| Updating one of several modified fields keeps the others | Documented only for a hashed field left out of the patch (driven in `hash-upgrade`). Updating one modified field beside another erases the other: bug, see the issue draft `interface-database-decorators-partial-update-drops-modified-fields`; not pinned. | +| `toDatabase` "has no prototype" (docs/4) | The result is a plain object with `Object.prototype`; the journey checks it is not an instance of the table class. The wording is loose, noted in the hand-back. | +| Database-driver behaviours: a driver that cannot enforce a unique secondary index refuses the schema (docs/2) | Belongs to the database module (rethinkdb); this repository only passes `unique` to the provider (driven through MongoDB in `index-options`). | +| `CROSS_INSTANCE` indexes and `between()` on a real driver beyond the schema definition | The cross-instance index is checked in the definition and in MongoDB's index list; query speed is not observable. | diff --git a/.agents/skills/verify-interface/features/autodate.md b/.agents/skills/verify-interface/features/autodate.md new file mode 100644 index 0000000..fd5ef8a --- /dev/null +++ b/.agents/skills/verify-interface/features/autodate.md @@ -0,0 +1,30 @@ +--- +covers: + - src/modifiers/autodate.ts +docs: + - docs/3.table-modifiers.md +journeys: + - autodate +--- + +# Auto dates + +`CreationTime` and `UpdateTime`. + +## Sub-features + +- `autodate`: insert, update and the stored dates. + +## How to get to it (user POV) + +- A table marks `Date` fields `@CreationTime()` and `@UpdateTime()`; no mixin. + +## Driving it with antelopejs-verify + +```bash +pnpm exec antelopejs-verify journey "$RUN" autodate +``` + +## Gotchas + +- The update path waits 60 ms so the refreshed time differs. diff --git a/.agents/skills/verify-interface/features/custom-modifiers.md b/.agents/skills/verify-interface/features/custom-modifiers.md new file mode 100644 index 0000000..322b7b6 --- /dev/null +++ b/.agents/skills/verify-interface/features/custom-modifiers.md @@ -0,0 +1,38 @@ +--- +covers: + - src/modifiers/common.ts + - src/modifiers/index.ts + - src/modifiers/internal/** +docs: + - docs/3.table-modifiers.md +journeys: + - one-way-modifier + - two-way-modifier + - container-modifier + - modifier-events +--- + +# Custom modifiers + +`Modifier`, `OneWayModifier`, `TwoWayModifier`, `ContainerModifier`, `attachModifier` and the modifier events. + +## Sub-features + +- `one-way-modifier`: transform on write; a second one-way modifier on a field is refused. +- `two-way-modifier`: transform on write, unlock on read. +- `container-modifier`: values by key and the wildcard. +- `modifier-events`: event order for insert, update and reads; events-only modifiers add no accessor. + +## How to get to it (user POV) + +- A developer extends a modifier class and registers it with `attachModifier`. + +## Driving it with antelopejs-verify + +```bash +pnpm exec antelopejs-verify journey "$RUN" custom-modifiers +``` + +## Gotchas + +- Scenarios define the modifiers in plain JavaScript with the same method names as the docs. diff --git a/.agents/skills/verify-interface/features/declarations.md b/.agents/skills/verify-interface/features/declarations.md new file mode 100644 index 0000000..1f7c2d6 --- /dev/null +++ b/.agents/skills/verify-interface/features/declarations.md @@ -0,0 +1,33 @@ +--- +covers: + - src/index.ts + - src/common.ts +docs: + - docs/4.data-models.md +journeys: + - model-signatures + - modifier-signatures +--- + +# Declarations + +The packed `.d.ts` files. + +## Sub-features + +- `model-signatures`: the return types of the model methods and `RegisterSchema`. +- `modifier-signatures`: the hash and localization mixin methods. + +## How to get to it (user POV) + +- A consumer compiles against `dist/index.d.ts` in strict mode. + +## Driving it with antelopejs-verify + +```bash +pnpm exec antelopejs-verify journey "$RUN" declarations +``` + +## Gotchas + +- Needs no database. diff --git a/.agents/skills/verify-interface/features/encryption.md b/.agents/skills/verify-interface/features/encryption.md new file mode 100644 index 0000000..a579d33 --- /dev/null +++ b/.agents/skills/verify-interface/features/encryption.md @@ -0,0 +1,34 @@ +--- +covers: + - src/modifiers/encryption.ts +docs: + - docs/3.table-modifiers.md +journeys: + - encryption-at-rest + - encryption-dates + - encryption-keys +--- + +# Encryption + +`EncryptionModifier` and `@Encrypted`. + +## Sub-features + +- `encryption-at-rest`: base64 ciphertext, iv and auth tag; only the key opens it. +- `encryption-dates`: a `Date` returns with whole-second precision. +- `encryption-keys`: the key is a UTF-8 string, so a hex key is refused. + +## How to get to it (user POV) + +- A table extends `Table.with(EncryptionModifier)` and marks fields `@Encrypted`. + +## Driving it with antelopejs-verify + +```bash +pnpm exec antelopejs-verify journey "$RUN" encryption +``` + +## Gotchas + +- The scenario decrypts the stored triple with Node's crypto alone. diff --git a/.agents/skills/verify-interface/features/hashing.md b/.agents/skills/verify-interface/features/hashing.md new file mode 100644 index 0000000..73a2062 --- /dev/null +++ b/.agents/skills/verify-interface/features/hashing.md @@ -0,0 +1,38 @@ +--- +covers: + - src/modifiers/hash.ts +docs: + - docs/3.table-modifiers.md +journeys: + - hash-storage + - hash-verify + - hash-limits + - hash-upgrade + - hash-digest +--- + +# Hashing + +`HashModifier` and `@Hashed`. + +## Sub-features + +- `hash-storage`: the scrypt format and parameters, checked with an independent scrypt. +- `hash-verify`: `testHash`, `testHashAsync`, `isHashOutdated`. +- `hash-limits`: the limits a stored scrypt string must stay inside. +- `hash-upgrade`: older formats verify, are flagged and are rewritten. +- `hash-digest`: `algorithm` digests with the row's shared salt. + +## How to get to it (user POV) + +- A table extends `Table.with(HashModifier)` and marks fields `@Hashed`. + +## Driving it with antelopejs-verify + +```bash +pnpm exec antelopejs-verify journey "$RUN" hashing +``` + +## Gotchas + +- Each scrypt run takes about 32 MiB and blocks the loop; the journeys take several seconds. diff --git a/.agents/skills/verify-interface/features/localization.md b/.agents/skills/verify-interface/features/localization.md new file mode 100644 index 0000000..d135be2 --- /dev/null +++ b/.agents/skills/verify-interface/features/localization.md @@ -0,0 +1,34 @@ +--- +covers: + - src/modifiers/localization.ts +docs: + - docs/3.table-modifiers.md +journeys: + - localize + - localize-wildcard + - localized-storage +--- + +# Localization + +`LocalizationModifier` and `@Localized`. + +## Sub-features + +- `localize`: switching locale, fallback, restricting fields. +- `localize-wildcard`: the `"*"` key. +- `localized-storage`: one value per locale in MongoDB. + +## How to get to it (user POV) + +- A table extends `Table.with(LocalizationModifier)` and marks fields `@Localized`. + +## Driving it with antelopejs-verify + +```bash +pnpm exec antelopejs-verify journey "$RUN" localization +``` + +## Gotchas + +- An empty text counts as no text, so the fallback answers for it. diff --git a/.agents/skills/verify-interface/features/models.md b/.agents/skills/verify-interface/features/models.md new file mode 100644 index 0000000..8275ae4 --- /dev/null +++ b/.agents/skills/verify-interface/features/models.md @@ -0,0 +1,41 @@ +--- +covers: + - src/model.ts +docs: + - docs/4.data-models.md + - docs/1.introduction.md +journeys: + - crud + - conversion + - validation + - get-model + - model-factory + - extend-model +--- + +# Models + +`BasicDataModel`, its CRUD methods, conversions, validation and `GetModel`. + +## Sub-features + +- `crud`: ids, counts and read-backs. +- `conversion`: the three static conversions save nothing; reads convert. +- `validation`: codecs, opt-in `validate`, partial patches. +- `get-model`: one cached model per class and instance id; instances stay apart. +- `model-factory`: the table name comes from the argument or `RegisterTable`. +- `extend-model`: a model subclass adds query methods. + +## How to get to it (user POV) + +- A module builds a model class from a table class and gets it with `GetModel`. + +## Driving it with antelopejs-verify + +```bash +pnpm exec antelopejs-verify journey "$RUN" models +``` + +## Gotchas + +- `update` with a patch of one modified field erases the other modified fields of the row (bug, issue draft; not pinned). diff --git a/.agents/skills/verify-interface/features/parameter-decoration.md b/.agents/skills/verify-interface/features/parameter-decoration.md new file mode 100644 index 0000000..4f6cb30 --- /dev/null +++ b/.agents/skills/verify-interface/features/parameter-decoration.md @@ -0,0 +1,36 @@ +--- +covers: + - src/model.ts +docs: + - docs/5.parameter-decoration.md +journeys: + - fixed-instance + - per-request-instance + - property-decoration + - crud-controller +--- + +# Parameter decoration + +`@Model` on a controller parameter or property. + +## Sub-features + +- `fixed-instance`: an instance id. +- `per-request-instance`: a callback picks the instance, or `undefined` for the default. +- `property-decoration`: a property is set while a request is handled. +- `crud-controller`: `@Model` beside `@Parameter` and `@RawBody` in a CRUD controller. + +## How to get to it (user POV) + +- A controller method declares `@Model(UserModel, "main")` on a parameter. + +## Driving it with antelopejs-verify + +```bash +pnpm exec antelopejs-verify journey "$RUN" parameter-decoration +``` + +## Gotchas + +- The journeys also load `@antelopejs/api` on a free port picked per run. diff --git a/.agents/skills/verify-interface/features/registration.md b/.agents/skills/verify-interface/features/registration.md new file mode 100644 index 0000000..7fd6ff0 --- /dev/null +++ b/.agents/skills/verify-interface/features/registration.md @@ -0,0 +1,40 @@ +--- +covers: + - src/database.ts + - src/schema.ts + - src/internal/bound-schema.ts + - src/table.ts +docs: + - docs/2.table-definitions.md + - docs/1.introduction.md +journeys: + - register-schema + - fixtures + - fixture-race + - table-access +--- + +# Registration + +`RegisterSchema`, `Fixture` and `Table.table()`. + +## Sub-features + +- `register-schema`: provisioning, binding and the unknown-schema error. +- `fixtures`: fixtures fill an empty table once, with generated ids and the insert conversion. +- `fixture-race`: two starts write each row once; a subclass gets its parent's fixture. +- `table-access`: `Table.table` for the default, a named, the cross-instance and a held instance, and its two errors. + +## How to get to it (user POV) + +- A module calls `await RegisterSchema(id)` during initialization. + +## Driving it with antelopejs-verify + +```bash +pnpm exec antelopejs-verify journey "$RUN" registration +``` + +## Gotchas + +- A stored fixture row without an id is keyed `
:fixture:`. diff --git a/.agents/skills/verify-interface/features/smoke.md b/.agents/skills/verify-interface/features/smoke.md new file mode 100644 index 0000000..5af8102 --- /dev/null +++ b/.agents/skills/verify-interface/features/smoke.md @@ -0,0 +1,35 @@ +--- +covers: +docs: + - README.md +journeys: + - register-schema + - crud + - minor-bump +--- + +# Smoke + +The fixed journeys every verdict runs, whatever the diff touches: provision a schema from decorated tables, write and read rows through a model in a live MongoDB, and compare the declared functions with the published version. + +## Sub-features + +- `register-schema`: as in [registration](registration.md). +- `crud`: as in [models](models.md). +- `minor-bump`: an added, changed or removed function releases as at least a new minor. + +## How to get to it (user POV) + +- A module that imports `@antelopejs/interface-database-decorators`, declares a table and calls `RegisterSchema`, in a project with a database module. + +## Driving it with antelopejs-verify + +```bash +pnpm exec antelopejs-verify journey "$RUN" smoke +``` + +## Gotchas + +- The smoke feature covers no glob, so it never marks a file as mapped. +- `minor-bump` needs the npm registry; without it the journey is not verified. +- `every-function` is not part of this feature: the package exports decorators, classes and plain functions, and no interface proxy a stub provider could answer. The scenarios below it drive the real functions against MongoDB instead. diff --git a/.agents/skills/verify-interface/features/tables.md b/.agents/skills/verify-interface/features/tables.md new file mode 100644 index 0000000..7fd3ca0 --- /dev/null +++ b/.agents/skills/verify-interface/features/tables.md @@ -0,0 +1,42 @@ +--- +covers: + - src/table.ts + - src/relation.ts + - src/common.ts +docs: + - docs/2.table-definitions.md +journeys: + - index-options + - group-flags + - field-types + - relation-metadata + - inheritance + - table-with +--- + +# Tables + +A table class with `Field`, `Index`, `Relation`, subclasses and `Table.with`. + +## Sub-features + +- `index-options`: plain, compound, cross-instance and unique indexes reach the provider and MongoDB. +- `group-flags`: `unique` and `crossInstance` on one field apply to the whole group. +- `field-types`: every field type goes through unchanged, in either decorator order. +- `relation-metadata`: `Relation` records links and enforces none. +- `inheritance`: a subclass copies its parent and never writes into it. +- `table-with`: mixin methods reach the subclass only. + +## How to get to it (user POV) + +- A module declares `Table` subclasses and registers them. + +## Driving it with antelopejs-verify + +```bash +pnpm exec antelopejs-verify journey "$RUN" tables +``` + +## Gotchas + +- MongoDB creates indexes after the schema call returns, so the index read-back waits 2.5 seconds. diff --git a/.agents/skills/verify-interface/package.json b/.agents/skills/verify-interface/package.json new file mode 100644 index 0000000..e986b24 --- /dev/null +++ b/.agents/skills/verify-interface/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "type": "module" +} diff --git a/.agents/skills/verify-interface/runner.ts b/.agents/skills/verify-interface/runner.ts new file mode 100644 index 0000000..58752a3 --- /dev/null +++ b/.agents/skills/verify-interface/runner.ts @@ -0,0 +1,141 @@ +import { mkdirSync, symlinkSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { createServer } from "node:net"; + +import { equal } from "@antelopejs/tooling-configs/verify"; +import type { Check, Journey, JourneyContext } from "@antelopejs/tooling-configs/verify"; + +/** The Mongo database every scenario uses, dropped before each journey by the service's `reset`. */ +export const DATABASE = "verify"; + +/** One native read-back: a `mongosh` expression evaluated against the verify database, and the JSON it must print. */ +export interface Stored { + name: string; + expression: string; + expected: string; +} + +/** What a scenario must print, and what the database itself must hold afterwards. */ +export interface Scenario { + title: string; + /** Statements run inside a mocha test after the prelude; `return` the observations the docs promise. */ + body: string; + /** The JSON the body returns. */ + printed: string; + stored?: Stored[]; + /** Also loads `@antelopejs/api` on a free port, exposed to the body as `process.env.VERIFY_API_PORT`. */ + api?: boolean; +} + +/** The helpers every scenario shares: the packed decorators, the database interface, and small helpers. */ +const PRELUDE = ` +const assert = require("node:assert/strict"); +const crypto = require("node:crypto"); +const dist = process.env.VERIFY_REPO + "/dist/index.js"; +const d = require(dist); +const iface = require(require.resolve("@antelopejs/interface-database", { paths: [dist] })); +const decorate = (cls, props) => { for (const [key, list] of Object.entries(props)) for (const apply of list) apply(cls.prototype, key); return cls; }; +const caught = (fn) => { try { fn(); return null; } catch (error) { return error.message; } }; +const rejection = async (promise) => { try { await promise; return null; } catch (error) { return { name: error.name, message: error.message }; } }; +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +const rawTable = (schemaId, table, instance) => iface.Schema.get(schemaId).instance(instance).table(table); +`; + +const TAIL = ` +describe("scenario", function () { + it("runs", async function () { + this.timeout(120000); + const out = await main(); + console.log("RESULT " + JSON.stringify(out, (key, value) => (value === undefined ? null : value))); + }); +}); +`; + +const TEST_CONFIG = ` +const { defineConfig } = require(require.resolve("@antelopejs/interface-core/config", { paths: [process.env.VERIFY_REPO] })); +const modules = { + local: { source: { type: "local", path: "." } }, + mongodb: { source: { type: "package", package: "@antelopejs/mongodb", version: "1.5.0" }, config: { url: process.env.VERIFY_MONGO_URL, database: "${DATABASE}" } }, +}; +if (process.env.VERIFY_API_PORT) { + modules.api = { + source: { type: "package", package: "@antelopejs/api", version: "1.3.0" }, + config: { servers: [{ protocol: "http", host: "127.0.0.1", port: Number(process.env.VERIFY_API_PORT) }], publicBaseUrl: "http://127.0.0.1:" + process.env.VERIFY_API_PORT }, + }; +} +module.exports = defineConfig({ name: "verify-scenario", cacheFolder: process.env.VERIFY_MODULE_CACHE, modules, test: { folder: "." } }); +`; + +const MODULE_MANIFEST = JSON.stringify({ + name: "verify-scenario", + version: "1.0.0", + main: "index.js", + antelopeJs: { test: "./antelope.test.js", standalone: true }, +}); + +const MODULE_ENTRY = "exports.construct = () => {};\nexports.start = () => {};\nexports.stop = () => {};\nexports.destroy = () => {};\n"; + +/** Asks the kernel for a free loopback port. */ +function freePort(): Promise { + return new Promise((resolve, reject) => { + const server = createServer(); + server.on("error", reject); + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + const port = address instanceof Object && "port" in address ? address.port : 0; + server.close(() => resolve(port)); + }); + }); +} + +/** Reads `mongosh` output as one compact JSON line. */ +function compact(text: string): string { + return text.trim().split("\n").at(-1) ?? ""; +} + +/** Runs a scenario in a real `ajs module test` with the packed decorators and a MongoDB module wired to the Docker service, then reads the database back through `mongosh`: the second view. */ +export function scenario({ title, body, printed, stored = [], api = false }: Scenario): Journey { + return { + title, + entryPoint: "a module test run by `ajs module test` with the decorators, @antelopejs/mongodb and a MongoDB container", + async run(context: JourneyContext) { + const mongo = context.service("mongo"); + const folder = join(context.work, "module"); + mkdirSync(folder, { recursive: true }); + symlinkSync(join(context.repo, "node_modules"), join(folder, "node_modules"), "dir"); + context.edit("write the module manifest", join(folder, "package.json"), `${MODULE_MANIFEST}\n`); + context.edit("write the module entry", join(folder, "index.js"), MODULE_ENTRY); + context.edit("write the test configuration", join(folder, "antelope.test.js"), TEST_CONFIG); + context.edit("write the scenario", join(folder, "scenario.test.js"), `${PRELUDE}\nasync function main() {\n${body}\n}\n${TAIL}`); + const cache = join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "antelopejs-verify", "ids-modules"); + const apiPort = api ? String(await freePort()) : ""; + const env = { + VERIFY_REPO: context.repo, + VERIFY_MODULE_CACHE: cache, + VERIFY_MONGO_URL: `mongodb://${mongo.host}:${mongo.port}`, + VERIFY_API_PORT: apiPort, + }; + const ran = await context.command("run the scenario", join(context.repo, "node_modules", ".bin", "ajs"), ["module", "test", folder, "-f", join(folder, "scenario.test.js")], { + cwd: folder, + env, + timeoutMs: 300_000, + }); + const line = ran.stdout.split("\n").find((entry) => entry.startsWith("RESULT ")); + context.expect(ran.folder, [ + { name: "the scenario ended cleanly", pass: ran.exitCode === 0, detail: `${ran.stdout.slice(-600)}${ran.stderr.slice(-300)}` }, + equal("the scenario printed what the docs promise", line?.slice("RESULT ".length) ?? "", printed), + ]); + for (const read of stored) { + const result = await context.native(read.name, "mongo", [ + "mongosh", + "--quiet", + "--eval", + `print(JSON.stringify(${read.expression.replaceAll("$DB", `db.getSiblingDB("${DATABASE}")`)}))`, + ]); + const checks: Check[] = [equal(`MongoDB holds ${read.name}`, compact(result.stdout), read.expected)]; + context.expect(result.folder, checks); + } + }, + }; +} diff --git a/.agents/skills/verify-interface/scenarios/custom.ts b/.agents/skills/verify-interface/scenarios/custom.ts new file mode 100644 index 0000000..b2f357c --- /dev/null +++ b/.agents/skills/verify-interface/scenarios/custom.ts @@ -0,0 +1,115 @@ +import type { Journey } from "@antelopejs/tooling-configs/verify"; + +import { scenario } from "../runner.ts"; + +const PRELUDE = ` + const { MakePropertyDecorator } = require(require.resolve("@antelopejs/interface-core/decorators", { paths: [dist] })); +`; + +const ONE_WAY = `${PRELUDE} + class PrefixHashModifier extends d.OneWayModifier { + autolock = true; + lock(_locked, value) { if (value === undefined) return undefined; return crypto.createHash(this.options.algorithm || "sha256").update(JSON.stringify(value)).digest("hex"); } + [d.MixinSymbol] = class {}; + } + const PrefixHashed = MakePropertyDecorator((target, propertyKey, options) => { d.attachModifier(target.constructor, PrefixHashModifier, propertyKey, options || {}); }); + class Doc extends d.Table {} + decorate(Doc, { title: [d.Field("string")], digest: [PrefixHashed(), d.Field("string")], strong: [PrefixHashed({ algorithm: "sha512" }), d.Field("string")] }); + d.RegisterTable("docs", "oneway")(Doc); + await d.RegisterSchema("oneway"); + const model = d.GetModel(d.BasicDataModel(Doc)); + const [id] = await model.insert({ title: "t", digest: "value", strong: "value" }); + const raw = await rawTable("oneway", "docs").get(id); + return { + stored: [raw._internal.data.digest === crypto.createHash("sha256").update(JSON.stringify("value")).digest("hex"), raw._internal.data.strong === crypto.createHash("sha512").update(JSON.stringify("value")).digest("hex")], + secondOneWay: caught(() => { class Twice extends d.Table {} decorate(Twice, { f: [PrefixHashed(), PrefixHashed()] }); }), + }; +`; + +const TWO_WAY = `${PRELUDE} + class Base64Modifier extends d.TwoWayModifier { + autolock = true; + autounlock = true; + lock(_locked, value) { if (value === undefined) return undefined; return (this.options.prefix || "b64-") + Buffer.from(JSON.stringify(value)).toString("base64"); } + unlock(locked) { if (typeof locked !== "string") return undefined; const prefix = this.options.prefix || "b64-"; return JSON.parse(Buffer.from(locked.substring(prefix.length), "base64").toString("utf8")); } + unlockrequest(data) { return data; } + [d.MixinSymbol] = class {}; + } + const Base64Encoded = MakePropertyDecorator((target, propertyKey, options) => { d.attachModifier(target.constructor, Base64Modifier, propertyKey, options || {}); }); + class Doc extends d.Table {} + decorate(Doc, { title: [d.Field("string")], body: [Base64Encoded(), d.Field("any")], tagged: [Base64Encoded({ prefix: "x:" }), d.Field("any")] }); + d.RegisterTable("docs", "twoway")(Doc); + await d.RegisterSchema("twoway"); + const model = d.GetModel(d.BasicDataModel(Doc)); + const [id] = await model.insert({ title: "t", body: { a: [1, 2] }, tagged: "plain" }); + await rawTable("twoway", "docs").insert({ _id: "odd", title: "odd", _internal: { meta: {}, data: { body: 42 } } }).run(); + const read = await model.get(id); + const odd = await model.get("odd"); + const raw = await rawTable("twoway", "docs").get(id); + return { read: [read.body, read.tagged], storedBody: raw._internal.data.body, storedTagged: raw._internal.data.tagged, notAString: odd.body ?? "undefined" }; +`; + +const CONTAINER = `${PRELUDE} + class PickModifier extends d.ContainerModifier { + [d.MixinSymbol] = class { pick(key) { d.unlock(this, PickModifier, undefined, key); return this; } }; + } + const Picked = MakePropertyDecorator((target, propertyKey, options) => { d.attachModifier(target.constructor, PickModifier, propertyKey, options || {}); }); + class Doc extends d.Table.with(PickModifier) {} + decorate(Doc, { values: [Picked(), d.Field("any")] }); + const doc = new Doc(); + doc.pick("a").values = 1; + doc.pick("b").values = 2; + const both = doc.pick("*").values; + doc.pick("a").values = 3; + const afterOverwrite = doc.pick("*").values; + Object.assign(doc.pick("*"), { values: { z: 26 } }); + const replaced = doc.pick("*").values; + return { both, afterOverwrite, replaced, byKey: [doc.pick("z").values, doc.pick("missing").values ?? "undefined", doc.pick("constructor").values ?? "undefined"] }; +`; + +const EVENTS = `${PRELUDE} + const log = []; + class Spy extends d.Modifier { + fromPlainData(object, field) { log.push(["fromPlainData", field, this instanceof Spy, object.constructor.name]); } + toPlainData(object, field) { log.push(["toPlainData", field]); } + fromDatabase(object, field) { log.push(["fromDatabase", field]); } + toDatabase(object, field) { log.push(["toDatabase", field]); } + insert(object, field) { log.push(["insert", field]); } + update(object, field) { log.push(["update", field]); } + [d.MixinSymbol] = class {}; + } + const Spied = MakePropertyDecorator((target, propertyKey, options) => { d.attachModifier(target.constructor, Spy, propertyKey, options || {}); }); + class Doc extends d.Table {} + decorate(Doc, { note: [Spied(), d.Field("string")] }); + d.RegisterTable("docs", "events")(Doc); + await d.RegisterSchema("events"); + const model = d.GetModel(d.BasicDataModel(Doc)); + const phase = (_name, entries) => entries.map((entry) => entry[0]); + const mark = () => log.splice(0); + const [id] = await model.insert({ note: "n" }); + const onInsert = phase("insert", mark()); + await model.update(id, { note: "m" }); + const onUpdate = phase("update", mark()); + await model.get(id); + const onRead = phase("read", mark()); + await model.getAll(); + const onReadAll = phase("readAll", mark()); + const instance = await model.get(id); + mark(); + d.toPlainData(instance); + const onPlain = phase("plain", mark()); + const plainAccessor = Object.getOwnPropertyDescriptor(Doc.prototype, "note"); + return { onInsert, onUpdate, onRead, onReadAll, onPlain, eventsOnlyKeepPlainValue: plainAccessor === undefined }; +`; + +export const CUSTOM_JOURNEYS = { + "one-way-modifier": scenario({ title: "a custom OneWayModifier transforms a value when it is written", body: ONE_WAY, printed: "{\"stored\":[true,true],\"secondOneWay\":\"Attempted adding Modifier PrefixHashModifier on Twice.f but it already has a One-Way Modifier\"}" }), + "two-way-modifier": scenario({ + title: "a custom TwoWayModifier stores a transformed value and unlocks it on read", + body: TWO_WAY, + printed: "{\"read\":[{\"a\":[1,2]},\"plain\"],\"storedBody\":\"b64-eyJhIjpbMSwyXX0=\",\"storedTagged\":\"x:InBsYWluIg==\",\"notAString\":\"undefined\"}", + stored: [{ name: "the stored encodings", expression: "$DB.getCollection('twoway__docs').find({ title: 't' }).toArray().map((r) => [r._internal.data.body, r._internal.data.tagged])", expected: "[[\"b64-eyJhIjpbMSwyXX0=\",\"x:InBsYWluIg==\"]]" }], + }), + "container-modifier": scenario({ title: "a ContainerModifier keeps values by key and treats the wildcard key as the whole record", body: CONTAINER, printed: "{\"both\":{\"a\":1,\"b\":2},\"afterOverwrite\":{\"a\":3,\"b\":2},\"replaced\":{\"z\":26},\"byKey\":[26,\"undefined\",\"undefined\"]}" }), + "modifier-events": scenario({ title: "a modifier's event methods run for inserts, updates and conversions, and an events-only modifier leaves the field plain", body: EVENTS, printed: "{\"onInsert\":[\"fromPlainData\",\"insert\",\"toDatabase\"],\"onUpdate\":[\"fromPlainData\",\"update\",\"toDatabase\"],\"onRead\":[\"fromDatabase\"],\"onReadAll\":[\"fromDatabase\"],\"onPlain\":[\"toPlainData\"],\"eventsOnlyKeepPlainValue\":true}" }), +} satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/declarations.ts b/.agents/skills/verify-interface/scenarios/declarations.ts new file mode 100644 index 0000000..1ada07e --- /dev/null +++ b/.agents/skills/verify-interface/scenarios/declarations.ts @@ -0,0 +1,78 @@ +import type { Journey } from "@antelopejs/tooling-configs/verify"; + +/** The probe header: `Assert>` fails to compile unless A and B are the same type. */ +const PROBE_HEADER = ` +type Identical = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; +type Assert = T; +`; + +const MODEL_SIGNATURES = ` +class User extends Api.Table { declare name: string; } +const Model = Api.BasicDataModel(User, "users"); +type Instance = InstanceType; +type _Get = Assert, PromiseLike>>; +type _GetBy = Assert, PromiseLike>>; +type _GetAll = Assert, PromiseLike>>; +type _Insert = Assert, Promise>>; +type _Delete = Assert, Promise>>; +type _Update = Assert>, number>>; +type _FromDatabase = Assert, User | undefined>>; +type _ToDatabase = Assert, Api.StoredRow>>; +type _Validate = Assert, Api.ValidationResult>>; +type _Register = Assert Promise>>; +type _PrimaryKey = Assert>; +type _Table = Assert[0], Api.TableTarget | undefined>>; +`; + +const MODIFIER_SIGNATURES = ` +class Hashed extends Api.Table.with(Api.HashModifier) { declare password: string; } +class Localized extends Api.Table.with(Api.LocalizationModifier) { declare name: string; } +declare const hashed: Hashed; +const tested = hashed.testHash("password", "x"); +const testedAsync = hashed.testHashAsync("password", "x"); +const outdated = hashed.isHashOutdated("password"); +type _TestHash = Assert>; +type _TestHashAsync = Assert>>; +type _Outdated = Assert>; +declare const localized: Localized; +const switched = localized.localize("en"); +type _Localize = Assert>; +type _Fallback = Assert, string | undefined>>; +`; + +function declared(title: string, probe: string): Journey { + return { + title, + entryPoint: "a TypeScript consumer compiling against the packed declarations", + async run(context) { + const file = `${context.work}/probe.ts`; + context.edit("write the signature probe", file, `import * as Api from "${context.repo}/dist/index";\n${PROBE_HEADER}\n${probe}`); + const compiled = await context.command("compile the signature probe", "node", [ + `${context.repo}/node_modules/typescript/bin/tsc`, + "--noEmit", + "--strict", + "--skipLibCheck", + "--experimentalDecorators", + "--target", + "es2022", + "--module", + "commonjs", + "--moduleResolution", + "node", + "--typeRoots", + `${context.repo}/node_modules/@types`, + "--types", + "node", + file, + ]); + context.expect(compiled.folder, [ + { name: "every export has the documented signature", pass: compiled.exitCode === 0, detail: compiled.stdout.slice(0, 600) }, + ]); + }, + }; +} + +export const DECLARATION_JOURNEYS = { + "model-signatures": declared("the model methods and RegisterSchema declare the return types the docs table lists", MODEL_SIGNATURES), + "modifier-signatures": declared("the hash and localization mixins declare the return types the docs describe", MODIFIER_SIGNATURES), +} satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/decoration.ts b/.agents/skills/verify-interface/scenarios/decoration.ts new file mode 100644 index 0000000..143a18e --- /dev/null +++ b/.agents/skills/verify-interface/scenarios/decoration.ts @@ -0,0 +1,98 @@ +import type { Journey } from "@antelopejs/tooling-configs/verify"; + +import { scenario } from "../runner.ts"; + +/** The helpers the HTTP scenarios share: a controller class whose routes are decorated by hand, and a fetch that answers the status and body. */ +const HTTP = ` + const api = require(require.resolve("@antelopejs/interface-api", { paths: [dist] })); + const base = "http://127.0.0.1:" + process.env.VERIFY_API_PORT; + const route = (Controller, method, verb, path, handler, params) => { + Controller.prototype[method] = handler; + params.forEach((apply, index) => apply(Controller.prototype, method, index)); + api[verb](path)(Controller.prototype, method, Object.getOwnPropertyDescriptor(Controller.prototype, method)); + }; + const call = async (path, init) => { const response = await fetch(base + path, init); const text = await response.text(); return { status: response.status, body: text }; }; + class Note extends d.Table {} + decorate(Note, { title: [d.Field("string")] }); + d.RegisterTable("notes", "http")(Note); + await d.RegisterSchema("http"); + const NoteModel = d.BasicDataModel(Note); +`; + +const FIXED = `${HTTP} + const seen = []; + class Fixed extends api.Controller("/fixed") {} + route(Fixed, "list", "Get", "/", async (model) => { seen.push(model); return (await model.getAll()).map((row) => row.title); }, [d.Model(NoteModel, "main")]); + await d.GetModel(NoteModel, "main").insert({ title: "in main" }); + await d.GetModel(NoteModel).insert({ title: "in default" }); + const first = await call("/fixed"); + const second = await call("/fixed"); + return { first, second, sameModelEveryRequest: seen.length === 2 && seen[0] === seen[1], isTheCachedModel: seen[0] === d.GetModel(NoteModel, "main") }; +`; + +const PER_REQUEST = `${HTTP} + class Tenants extends api.Controller("/tenants") {} + route(Tenants, "list", "Get", "/:tenantId/notes", async (tenantId, model) => ({ tenantId, titles: (await model.getAll()).map((row) => row.title) }), [api.Parameter("tenantId"), d.Model(NoteModel, (ctx) => "tenant-" + ctx.routeParameters.tenantId)]); + class Defaulted extends api.Controller("/defaulted") {} + route(Defaulted, "list", "Get", "/", async (model) => (await model.getAll()).map((row) => row.title), [d.Model(NoteModel, () => undefined)]); + await d.GetModel(NoteModel, "tenant-a").insert({ title: "alpha" }); + await d.GetModel(NoteModel, "tenant-b").insert({ title: "beta" }); + await d.GetModel(NoteModel).insert({ title: "default instance" }); + return { a: await call("/tenants/a/notes"), b: await call("/tenants/b/notes"), none: await call("/tenants/c/notes"), defaulted: await call("/defaulted") }; +`; + +const PROPERTY = `${HTTP} + class Props extends api.Controller("/props") {} + d.Model(NoteModel, "main")(Props.prototype, "notes"); + route(Props, "list", "Get", "/", async function () { return (await this.notes.getAll()).map((row) => row.title); }, []); + route(Props, "one", "Get", "/:id", async function (id) { const row = await this.notes.get(id); return row ? row.title : "missing"; }, [api.Parameter("id")]); + route(Props, "same", "Get", "/same/model", async function () { return this.notes === d.GetModel(NoteModel, "main"); }, []); + const [id] = await d.GetModel(NoteModel, "main").insert({ title: "stored" }); + return { list: await call("/props"), one: await call("/props/" + id), missing: await call("/props/none"), sameAsGetModel: await call("/props/same/model"), outsideARequest: new Props().notes === undefined }; +`; + +const CRUD_CONTROLLER = `${HTTP} + class Notes extends api.Controller("/notes") {} + route(Notes, "list", "Get", "/", async (model) => (await model.getAll()).map((row) => row.title), [d.Model(NoteModel, "main")]); + route(Notes, "get", "Get", "/:id", async (id, model) => { const row = await model.get(id); return row ? row.title : "missing"; }, [api.Parameter("id"), d.Model(NoteModel, "main")]); + route(Notes, "create", "Post", "/", async (body, model) => model.insert(JSON.parse(body.toString())), [api.RawBody(), d.Model(NoteModel, "main")]); + route(Notes, "update", "Put", "/:id", async (id, body, model) => model.update(id, JSON.parse(body.toString())), [api.Parameter("id"), api.RawBody(), d.Model(NoteModel, "main")]); + route(Notes, "remove", "Delete", "/:id", async (id, model) => model.delete(id), [api.Parameter("id"), d.Model(NoteModel, "main")]); + const created = await call("/notes", { method: "POST", body: JSON.stringify({ title: "first" }) }); + const id = JSON.parse(created.body)[0]; + const updated = await call("/notes/" + id, { method: "PUT", body: JSON.stringify({ title: "renamed" }) }); + const read = await call("/notes/" + id); + const removed = await call("/notes/" + id, { method: "DELETE" }); + const gone = await call("/notes/" + id); + return { created: [created.status, typeof id], updated, read, removed, gone }; +`; + +export const DECORATION_JOURNEYS = { + "fixed-instance": scenario({ + title: "@Model with an instance id injects the cached model of that instance into a controller method", + body: FIXED, + printed: "{\"first\":{\"status\":200,\"body\":\"[\\\"in main\\\"]\"},\"second\":{\"status\":200,\"body\":\"[\\\"in main\\\"]\"},\"sameModelEveryRequest\":true,\"isTheCachedModel\":true}", + api: true, + stored: [{ name: "the notes by instance", expression: "$DB.getCollection('http__notes').find().sort({ title: 1 }).toArray().map((r) => [r.title, r._instance])", expected: "[[\"in default\",null],[\"in main\",\"main\"]]" }], + }), + "per-request-instance": scenario({ + title: "@Model with a callback picks the instance from each request, or the default instance", + body: PER_REQUEST, + printed: "{\"a\":{\"status\":200,\"body\":\"{\\\"tenantId\\\":\\\"a\\\",\\\"titles\\\":[\\\"alpha\\\"]}\"},\"b\":{\"status\":200,\"body\":\"{\\\"tenantId\\\":\\\"b\\\",\\\"titles\\\":[\\\"beta\\\"]}\"},\"none\":{\"status\":200,\"body\":\"{\\\"tenantId\\\":\\\"c\\\",\\\"titles\\\":[]}\"},\"defaulted\":{\"status\":200,\"body\":\"[\\\"default instance\\\"]\"}}", + api: true, + stored: [{ name: "the notes by instance", expression: "$DB.getCollection('http__notes').find().sort({ title: 1 }).toArray().map((r) => [r.title, r._instance])", expected: "[[\"alpha\",\"tenant-a\"],[\"beta\",\"tenant-b\"],[\"default instance\",null]]" }], + }), + "property-decoration": scenario({ + title: "@Model on a property gives the controller its model while a request is handled", + body: PROPERTY, + printed: "{\"list\":{\"status\":200,\"body\":\"[\\\"stored\\\"]\"},\"one\":{\"status\":200,\"body\":\"stored\"},\"missing\":{\"status\":200,\"body\":\"missing\"},\"sameAsGetModel\":{\"status\":200,\"body\":\"true\"},\"outsideARequest\":true}", + api: true, + }), + "crud-controller": scenario({ + title: "@Model combines with the API decorators in a CRUD controller", + body: CRUD_CONTROLLER, + printed: "{\"created\":[200,\"string\"],\"updated\":{\"status\":200,\"body\":\"1\"},\"read\":{\"status\":200,\"body\":\"renamed\"},\"removed\":{\"status\":200,\"body\":\"1\"},\"gone\":{\"status\":200,\"body\":\"missing\"}}", + api: true, + stored: [{ name: "the notes left", expression: "$DB.getCollection('http__notes').find().toArray().length", expected: "0" }], + }), +} satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/encryption.ts b/.agents/skills/verify-interface/scenarios/encryption.ts new file mode 100644 index 0000000..e7ec28c --- /dev/null +++ b/.agents/skills/verify-interface/scenarios/encryption.ts @@ -0,0 +1,102 @@ +import type { Journey } from "@antelopejs/tooling-configs/verify"; + +import { scenario } from "../runner.ts"; + +const KEY = "12345678901234567890123456789012"; + +/** Decrypts a stored `[ciphertext, iv, authTag]` triple with Node's crypto alone: the scenario's second view of the cipher. */ +const DECRYPT = ` + const decrypt = (triple, algorithm = "aes-256-gcm", key = "${KEY}") => { + const decipher = crypto.createDecipheriv(algorithm, key, Buffer.from(triple[1], "base64")); + if (triple[2]) decipher.setAuthTag(Buffer.from(triple[2], "base64")); + return Buffer.concat([decipher.update(Buffer.from(triple[0], "base64")), decipher.final()]).toString("utf8"); + }; +`; + +const AT_REST = `${DECRYPT} + class Vault extends d.Table.with(d.EncryptionModifier) {} + decorate(Vault, { + owner: [d.Field("string")], + card: [d.Encrypted({ secretKey: "${KEY}", algorithm: "aes-256-gcm", ivSize: 16 }), d.Field("string")], + personal: [d.Encrypted({ secretKey: "${KEY}" }), d.Field("any")], + legacy: [d.Encrypted({ secretKey: "${KEY}", algorithm: "aes-256-cbc" }), d.Field("string")], + short: [d.Encrypted({ secretKey: "${KEY}", ivSize: 12 }), d.Field("string")], + }); + d.RegisterTable("vaults", "enc")(Vault); + await d.RegisterSchema("enc"); + const model = d.GetModel(d.BasicDataModel(Vault)); + const personal = { ssn: "078-05-1120", birthDate: "1990-01-01" }; + const [id] = await model.insert({ owner: "Ann", card: "4242 4242 4242 4242", personal, legacy: "old cipher", short: "twelve" }); + const [again] = await model.insert({ owner: "Bea", card: "4242 4242 4242 4242" }); + const read = await model.get(id); + const raw = await rawTable("enc", "vaults").get(id); + const rawAgain = await rawTable("enc", "vaults").get(again); + const data = raw._internal.data; + const shape = (triple) => [Array.isArray(triple), triple.length, typeof triple[2]]; + return { + read: [read.card, read.personal, read.legacy, read.short], + topLevelHoldsNoCiphertext: raw.card === undefined && raw.personal === undefined, + gcmShape: shape(data.card), + cbcShape: [data.legacy.length, data.legacy[2] ?? null], + ivBytes: [Buffer.from(data.card[1], "base64").length, Buffer.from(data.short[1], "base64").length], + plaintextAbsent: !JSON.stringify(raw).includes("4242") && !JSON.stringify(raw).includes("078-05-1120"), + independentDecrypt: [decrypt(data.card), decrypt(data.personal), decrypt(data.legacy, "aes-256-cbc")], + sameValueTwice: [data.card[1] !== rawAgain._internal.data.card[1], data.card[0] !== rawAgain._internal.data.card[0]], + }; +`; + +const DATES = `${DECRYPT} + class Event extends d.Table.with(d.EncryptionModifier) {} + decorate(Event, { when: [d.Encrypted({ secretKey: "${KEY}" }), d.Field("any")], plainObject: [d.Encrypted({ secretKey: "${KEY}" }), d.Field("any")] }); + d.RegisterTable("events", "encdate")(Event); + await d.RegisterSchema("encdate"); + const model = d.GetModel(d.BasicDataModel(Event)); + const moment = new Date("2026-01-02T03:04:05.678Z"); + const [id] = await model.insert({ when: moment, plainObject: { type: "Date", value: "not a date", extra: 1 } }); + const read = await model.get(id); + const raw = await rawTable("encdate", "events").get(id); + return { + isDate: read.when instanceof Date, + iso: read.when.toISOString(), + storedText: JSON.parse(decrypt(raw._internal.data.when)), + lookalikeKept: read.plainObject, + }; +`; + +const KEYS = ` + class Strict extends d.Table.with(d.EncryptionModifier) {} + const goodKey = "${KEY}"; + const hexKey = Buffer.from(goodKey).toString("hex"); + class Hex extends d.Table.with(d.EncryptionModifier) {} + decorate(Strict, { value: [d.Encrypted({ secretKey: goodKey }), d.Field("string")] }); + decorate(Hex, { value: [d.Encrypted({ secretKey: hexKey }), d.Field("string")] }); + d.RegisterTable("strict", "enckey")(Strict); + d.RegisterTable("hex", "enckey")(Hex); + await d.RegisterSchema("enckey"); + const strict = d.GetModel(d.BasicDataModel(Strict)); + const hex = d.GetModel(d.BasicDataModel(Hex)); + const [id] = await strict.insert({ value: "kept" }); + return { + hexKeyBytes: Buffer.byteLength(hexKey), + hexRefused: await rejection(hex.insert({ value: "refused" })), + goodStored: (await strict.get(id)).value, + hexStored: (await hex.getAll()).length, + }; +`; + +export const ENCRYPTION_JOURNEYS = { + "encryption-at-rest": scenario({ + title: "an encrypted field is stored as base64 ciphertext, iv and auth tag that only the key opens", + body: AT_REST, + printed: "{\"read\":[\"4242 4242 4242 4242\",{\"ssn\":\"078-05-1120\",\"birthDate\":\"1990-01-01\"},\"old cipher\",\"twelve\"],\"topLevelHoldsNoCiphertext\":true,\"gcmShape\":[true,3,\"string\"],\"cbcShape\":[3,null],\"ivBytes\":[16,12],\"plaintextAbsent\":true,\"independentDecrypt\":[\"\\\"4242 4242 4242 4242\\\"\",\"{\\\"ssn\\\":\\\"078-05-1120\\\",\\\"birthDate\\\":\\\"1990-01-01\\\"}\",\"\\\"old cipher\\\"\"],\"sameValueTwice\":[true,true]}", + stored: [ + { + name: "the stored triples", + expression: "$DB.getCollection('enc__vaults').find({ owner: 'Ann' }).toArray().map((r) => [Object.keys(r._internal.data).sort(), r._internal.data.card.length, r._internal.data.legacy.length, r.card === undefined])", + expected: "[[[\"card\",\"legacy\",\"personal\",\"short\"],3,3,true]]", + }, + ], + }), + "encryption-dates": scenario({ title: "an encrypted Date comes back as a Date with whole-second precision", body: DATES, printed: "{\"isDate\":true,\"iso\":\"2026-01-02T03:04:05.000Z\",\"storedText\":{\"value\":\"Fri, 02 Jan 2026 03:04:05 GMT\",\"type\":\"Date\"},\"lookalikeKept\":{\"type\":\"Date\",\"value\":\"not a date\",\"extra\":1}}" }), + "encryption-keys": scenario({ title: "the key goes to the cipher as a UTF-8 string, so a hex-encoded key is refused", body: KEYS, printed: "{\"hexKeyBytes\":64,\"hexRefused\":{\"name\":\"RangeError\",\"message\":\"Invalid key length\"},\"goodStored\":\"kept\",\"hexStored\":0}" }), +} satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/hashing.ts b/.agents/skills/verify-interface/scenarios/hashing.ts new file mode 100644 index 0000000..b9f9e20 --- /dev/null +++ b/.agents/skills/verify-interface/scenarios/hashing.ts @@ -0,0 +1,147 @@ +import type { Journey } from "@antelopejs/tooling-configs/verify"; + +import { scenario } from "../runner.ts"; + +/** Independent scrypt, digest and parsing helpers, so the scenario checks the stored hash without the library. */ +const HASH_HELPERS = ` + const b64 = (buffer) => buffer.toString("base64").replace(/=+$/, ""); + const scryptOf = (value, salt, keylen, ln, r, p) => crypto.scryptSync(JSON.stringify(value), salt, keylen, { N: 2 ** ln, r, p, maxmem: 1024 * 1024 * 1024 }); + const scryptString = (value, ln, r, p, saltBytes = 16, keyBytes = 32) => { const salt = Buffer.alloc(saltBytes, 7); return "$scrypt$ln=" + ln + ",r=" + r + ",p=" + p + "$" + b64(salt) + "$" + b64(scryptOf(value, salt, keyBytes, ln, r, p)); }; + const digest = (algorithm, salt, value) => crypto.createHash(algorithm).update(salt).update(JSON.stringify(value)).digest("base64"); + class Account extends d.Table.with(d.HashModifier) {} + decorate(Account, { name: [d.Field("string")], password: [d.Hashed(), d.Field("string")], pin: [d.Hashed({ algorithm: "sha256" }), d.Field("string")], token: [d.Hashed({ algorithm: "sha256" }), d.Field("string")] }); + d.RegisterTable("accounts", "hash")(Account); + await d.RegisterSchema("hash"); + const Model = d.BasicDataModel(Account); + const accounts = d.GetModel(Model); + const rawRow = async (id) => rawTable("hash", "accounts").get(id); +`; + +const STORAGE = `${HASH_HELPERS} + const [id] = await accounts.insert({ name: "Ann", password: "secret" }); + const [second] = await accounts.insert({ name: "Bea", password: "secret" }); + const raw = await rawRow(id); + const stored = raw._internal.data.password; + const match = /^\\$scrypt\\$ln=15,r=8,p=3\\$([A-Za-z0-9+/]+)\\$([A-Za-z0-9+/]+)$/.exec(stored); + const salt = Buffer.from(match[1], "base64"); + const key = Buffer.from(match[2], "base64"); + const expected = scryptOf("secret", salt, 32, 15, 8, 3); + const loaded = await accounts.get(id); + d.lock(loaded, d.HashModifier, ["password"]); + const kept = d.toDatabase(loaded)._internal.data.password; + loaded.password = stored; + const undef = Model.fromPlainData({ _id: "y", password: undefined }); + return { + format: [Boolean(match), salt.length, key.length, match[1].includes("="), match[2].includes("=")], + hashesTheJsonText: Buffer.compare(key, expected) === 0, + plaintextAbsent: !JSON.stringify(raw).includes("secret"), + freshSaltEachWrite: raw._internal.data.password !== (await rawRow(second))._internal.data.password, + lockKeepsTheHash: kept === stored, + assigningTheStoredHashKeepsIt: d.toDatabase(loaded)._internal.data.password === stored, + undefinedStoresNothing: d.toDatabase(undef)._internal === undefined || d.toDatabase(undef)._internal.data.password === undefined, + }; +`; + +const VERIFY = `${HASH_HELPERS} + const [id] = await accounts.insert({ name: "Ann", password: "secret" }); + const row = await accounts.get(id); + const asyncRight = await row.testHashAsync("password", "secret"); + const asyncWrong = await row.testHashAsync("password", "Secret"); + const syncRight = row.testHash("password", "secret"); + const syncWrong = row.testHash("password", "nope"); + const notHashedAsync = await rejection(row.testHashAsync("name", "Ann")); + let notHashedOutdated = null; + try { row.isHashOutdated("name"); } catch (error) { notHashedOutdated = error.message; } + const [bare] = await accounts.insert({ name: "Nobody" }); + const bareRow = await accounts.get(bare); + return { + asyncRight, asyncWrong, syncRight: [typeof syncRight, syncRight], syncWrong, + fresh: row.isHashOutdated("password"), + notHashedAsync: notHashedAsync && notHashedAsync.message, + notHashedOutdated, + nothingStored: [bareRow.isHashOutdated("password"), await bareRow.testHashAsync("password", "x")], + }; +`; + +const LIMITS = `${HASH_HELPERS} + const seed = async (id, password, extra = {}) => rawTable("hash", "accounts").insert({ _id: id, name: id, _internal: { meta: {}, data: { password } }, ...extra }).run(); + const cases = { + cheap: [10, 8, 1], boundaryMemory: [17, 8, 1], tooMuchMemory: [18, 8, 1], + parallelBoundary: [8, 8, 16], parallelTooMany: [8, 8, 17], costBelow16TimesR: [15, 1, 1], + }; + const verdicts = {}; + for (const [name, [ln, r, p]] of Object.entries(cases)) { + await seed(name, scryptString("pw", ln, r, p)); + const row = await accounts.get(name); + verdicts[name] = [await row.testHashAsync("password", "pw"), await row.testHashAsync("password", "other")]; + } + const sizes = { shortSalt: scryptString("pw", 8, 8, 1, 8, 32), longKey: scryptString("pw", 8, 8, 1, 16, 64), tooLongKey: scryptString("pw", 8, 8, 1, 16, 80), shortKey: scryptString("pw", 8, 8, 1, 16, 8), threeDigitP: scryptString("pw", 4, 1, 100) }; + for (const [name, stored] of Object.entries(sizes)) { + await seed(name, stored); + verdicts[name] = await (await accounts.get(name)).testHashAsync("password", "pw"); + } + return verdicts; +`; + +const UPGRADE = `${HASH_HELPERS} + const seed = async (id, data, meta = {}) => rawTable("hash", "accounts").insert({ _id: id, name: id, _internal: { meta, data } }).run(); + await seed("legacy", { password: digest("sha256", "old-salt", "hunter2") }, { HashModifier: { salt: "old-salt" } }); + await seed("lowcost", { password: scryptString("hunter2", 10, 8, 1) }); + const legacy = await accounts.get("legacy"); + const lowcost = await accounts.get("lowcost"); + const before = [await legacy.testHashAsync("password", "hunter2"), legacy.isHashOutdated("password"), await lowcost.testHashAsync("password", "hunter2"), lowcost.isHashOutdated("password")]; + const wrongLegacy = await legacy.testHashAsync("password", "wrong"); + if (await legacy.testHashAsync("password", "hunter2")) { if (legacy.isHashOutdated("password")) { legacy.password = "hunter2"; await accounts.update(legacy); } } + const rewritten = await rawRow("legacy"); + const afterRewrite = [rewritten._internal.data.password.startsWith("$scrypt$ln=15,r=8,p=3$"), (await accounts.get("legacy")).isHashOutdated("password"), await (await accounts.get("legacy")).testHashAsync("password", "hunter2")]; + const lowBefore = (await rawRow("lowcost"))._internal.data.password; + await accounts.update("lowcost", { name: "renamed" }); + const keptByNameUpdate = (await rawRow("lowcost"))._internal.data.password === lowBefore; + await accounts.update({ _id: "lowcost", name: "renamed again" }); + const keptByObjectUpdate = (await rawRow("lowcost"))._internal.data.password === lowBefore; + await accounts.update("lowcost", { password: "hunter2" }); + const viaUpdate = (await rawRow("lowcost"))._internal.data.password.startsWith("$scrypt$ln=15,r=8,p=3$"); + return { before, wrongLegacy, afterRewrite, keptByNameUpdate, keptByObjectUpdate, viaUpdate }; +`; + +const DIGEST = `${HASH_HELPERS} + const [id] = await accounts.insert({ name: "Ann", pin: "1234", token: "abc" }); + const raw = await rawRow(id); + const salt = raw._internal.meta.HashModifier.salt; + const row = await accounts.get(id); + const [other] = await accounts.insert({ name: "Bea", pin: "1234" }); + const otherSalt = (await rawRow(other))._internal.meta.HashModifier.salt; + const again = await accounts.get(id); + again.pin = "9999"; + const rewritten = d.toDatabase(again); + const holdsScrypt = Model.fromDatabase({ _id: "h", name: "h", _internal: { meta: { HashModifier: { salt } }, data: { pin: scryptString("1234", 10, 8, 1) } } }); + const sha512 = Model.fromDatabase({ _id: "s", name: "s", _internal: { meta: { HashModifier: { salt } }, data: { pin: crypto.createHash("sha512").update(salt).update(JSON.stringify("1234")).digest("base64") } } }); + return { + storedDigest: raw._internal.data.pin === digest("sha256", salt, "1234"), + sharedSalt: raw._internal.data.token === digest("sha256", salt, "abc"), + saltIsText: typeof salt === "string" && salt.length > 0, + freshSaltPerRow: salt !== otherSalt, + saltReusedOnLaterWrite: [rewritten._internal.meta.HashModifier.salt === salt, rewritten._internal.data.pin === digest("sha256", salt, "9999")], + testDigest: [row.testHash("pin", "1234"), row.testHash("pin", "0000"), await row.testHashAsync("pin", "1234")], + digestNeverOutdated: row.isHashOutdated("pin"), + scryptOnDigestField: [holdsScrypt.testHash("pin", "1234"), holdsScrypt.isHashOutdated("pin")], + otherDigestRefused: sha512.testHash("pin", "1234"), + }; +`; + +export const HASH_JOURNEYS = { + "hash-storage": scenario({ + title: "a hashed field stores a salted scrypt hash of the JSON-encoded value", + body: STORAGE, + printed: "{\"format\":[true,16,32,false,false],\"hashesTheJsonText\":true,\"plaintextAbsent\":true,\"freshSaltEachWrite\":true,\"lockKeepsTheHash\":true,\"assigningTheStoredHashKeepsIt\":true,\"undefinedStoresNothing\":true}", + stored: [{ name: "the stored hash", expression: "$DB.getCollection('hash__accounts').find({ name: 'Ann' }).toArray().map((r) => [Object.keys(r._internal.data), /^\\$scrypt\\$ln=15,r=8,p=3\\$[A-Za-z0-9+/]{22}\\$[A-Za-z0-9+/]{43}$/.test(r._internal.data.password)])", expected: "[[[\"password\"],true]]" }], + }), + "hash-verify": scenario({ title: "testHash, testHashAsync and isHashOutdated answer for a hashed field and refuse another", body: VERIFY, printed: "{\"asyncRight\":true,\"asyncWrong\":false,\"syncRight\":[\"boolean\",true],\"syncWrong\":false,\"fresh\":false,\"notHashedAsync\":\"Field name is not hashed\",\"notHashedOutdated\":\"Field name is not hashed\",\"nothingStored\":[false,false]}" }), + "hash-limits": scenario({ title: "a stored scrypt string verifies only inside the documented limits", body: LIMITS, printed: "{\"cheap\":[true,false],\"boundaryMemory\":[true,false],\"tooMuchMemory\":[false,false],\"parallelBoundary\":[true,false],\"parallelTooMany\":[false,false],\"costBelow16TimesR\":[true,false],\"shortSalt\":false,\"longKey\":true,\"tooLongKey\":false,\"shortKey\":false,\"threeDigitP\":false}" }), + "hash-upgrade": scenario({ + title: "older hash formats still verify, are flagged, and are rewritten by assigning the tested value", + body: UPGRADE, + printed: "{\"before\":[true,true,true,true],\"wrongLegacy\":false,\"afterRewrite\":[true,false,true],\"keptByNameUpdate\":true,\"keptByObjectUpdate\":true,\"viaUpdate\":true}", + }), + "hash-digest": scenario({ title: "a field with an algorithm stores a salted digest with the row's shared salt", body: DIGEST, printed: "{\"storedDigest\":true,\"sharedSalt\":true,\"saltIsText\":true,\"freshSaltPerRow\":true,\"saltReusedOnLaterWrite\":[true,true],\"testDigest\":[true,false,true],\"digestNeverOutdated\":false,\"scryptOnDigestField\":[true,false],\"otherDigestRefused\":false}" }), +} satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/localization.ts b/.agents/skills/verify-interface/scenarios/localization.ts new file mode 100644 index 0000000..9787c40 --- /dev/null +++ b/.agents/skills/verify-interface/scenarios/localization.ts @@ -0,0 +1,123 @@ +import type { Journey } from "@antelopejs/tooling-configs/verify"; + +import { scenario } from "../runner.ts"; + +const PRODUCT = ` + class Product extends d.Table.with(d.LocalizationModifier) {} + decorate(Product, { + price: [d.Field("number")], + name: [d.Localized({ fallbackLocale: "en" }), d.Field("string")], + description: [d.Localized({ fallbackLocale: "en" }), d.Field("string")], + bare: [d.Localized(), d.Field("string")], + }); + d.RegisterTable("products", "loc")(Product); + await d.RegisterSchema("loc"); +`; + +const LOCALIZE = `${PRODUCT} + const product = new Product(); + product._id = "prod-123"; + product.price = 99.99; + product.localize("en").name = "Premium Headphones"; + product.localize("fr").name = "Casque Premium"; + product.localize("es").name = "Auriculares Premium"; + const spanish = product.localize("es"); + const spanishNow = spanish.name; + const german = product.localize("de"); + const germanNow = german.name; + const sameObject = [spanish === product, german === product, spanish === german]; + const readNow = spanish.name; + product.localize("en").description = "Loud"; + product.localize("fr").description = ""; + const frenchEmpty = product.localize("fr").description; + product.localize("fr").bare = "sans repli"; + const noFallback = [product.localize("fr").bare, product.localize("de").bare ?? "undefined"]; + const restricted = product.localize("en").localize("fr", ["name"]); + const restrictedReads = [restricted.name, restricted.description]; + return { + spanish: spanishNow, + germanFallsBack: germanNow, + sameObject, + readNow, + frenchEmpty, + noFallback, + restrictedReads, + fallbackLocaleOf: [d.LocalizationModifier.fallbackLocaleOf(Product, "name"), d.LocalizationModifier.fallbackLocaleOf(Product, "bare") ?? "undefined", d.LocalizationModifier.fallbackLocaleOf(Product, "price") ?? "undefined", d.LocalizationModifier.fallbackLocaleOf(Product, "missing") ?? "undefined"], + }; +`; + +const WILDCARD = `${PRODUCT} + const product = new Product(); + product.localize("en").name = "Premium Headphones"; + product.localize("fr").name = "Casque Premium"; + const all = product.localize("*").name; + Object.assign(product.localize("*"), { name: { en: "Premium Headphones", fr: "Casque Premium", de: "Premium-Kopfhoerer", es: "" } }); + const replaced = product.localize("*").name; + const german = product.localize("de").name; + const model = d.GetModel(d.BasicDataModel(Product)); + const written = new Product(); + written.localize("en").name = "Hello"; + written.localize("fr").name = "Bonjour"; + const [id] = await model.insert(written); + const loaded = await model.get(id); + return { all, replaced, german, stored: [loaded.localize("fr").name, loaded.localize("it").name, loaded.localize("*").name] }; +`; + +const STORED = `${PRODUCT} + const model = d.GetModel(d.BasicDataModel(Product)); + const first = new Product(); + first.price = 5; + first.localize("en").name = "Hello"; + first.localize("fr").name = "Bonjour"; + first.localize("de").name = ""; + const [id] = await model.insert(first); + const alone = new Product(); + alone.price = 6; + alone.localize("fr").name = "Seulement"; + const [second] = await model.insert(alone); + const only = await model.get(second); + const raw = await rawTable("loc", "products").get(id); + return { rawKeys: Object.keys(raw._internal.data).sort(), raw: raw._internal.data.name, onlyFrench: [only.localize("fr").name, only.localize("en").name ?? "undefined"] }; +`; + +const AUTO = ` + class Article extends d.Table {} + decorate(Article, { title: [d.Field("string")], creationDate: [d.CreationTime(), d.Field("date")], updateDate: [d.UpdateTime(), d.Field("date")] }); + d.RegisterTable("articles", "auto")(Article); + await d.RegisterSchema("auto"); + const model = d.GetModel(d.BasicDataModel(Article)); + const before = Date.now(); + const [id] = await model.insert({ title: "One", creationDate: new Date(0), updateDate: new Date(0) }); + const inserted = await rawTable("auto", "articles").get(id); + await sleep(60); + await model.update(id, { title: "Two", creationDate: new Date(0) }); + const updated = await rawTable("auto", "articles").get(id); + const after = Date.now(); + const stamp = (value) => new Date(value).getTime(); + return { + insertSetsBoth: [stamp(inserted.creationDate) >= before, stamp(inserted.updateDate) >= before, stamp(inserted.creationDate) <= after], + creationKeptOnUpdate: stamp(updated.creationDate) === stamp(inserted.creationDate), + updateRefreshed: stamp(updated.updateDate) >= stamp(inserted.updateDate) + 50, + noMixinNeeded: Object.getPrototypeOf(Article.prototype) === d.Table.prototype, + }; +`; + +export const LOCALIZATION_JOURNEYS = { + localize: scenario({ title: "localize switches the locale of an instance and falls back when a translation is missing or empty", body: LOCALIZE, printed: "{\"spanish\":\"Auriculares Premium\",\"germanFallsBack\":\"Premium Headphones\",\"sameObject\":[true,true,true],\"readNow\":\"Premium Headphones\",\"frenchEmpty\":\"Loud\",\"noFallback\":[\"sans repli\",\"undefined\"],\"restrictedReads\":[\"Casque Premium\",\"Loud\"],\"fallbackLocaleOf\":[\"en\",\"undefined\",\"undefined\",\"undefined\"]}" }), + "localize-wildcard": scenario({ title: "the wildcard key reads and writes every translation at once", body: WILDCARD, printed: "{\"all\":{\"en\":\"Premium Headphones\",\"fr\":\"Casque Premium\"},\"replaced\":{\"en\":\"Premium Headphones\",\"fr\":\"Casque Premium\",\"de\":\"Premium-Kopfhoerer\"},\"german\":\"Premium-Kopfhoerer\",\"stored\":[\"Bonjour\",\"Hello\",{\"en\":\"Hello\",\"fr\":\"Bonjour\"}]}" }), + "localized-storage": scenario({ + title: "a localized field is stored as one value per locale", + body: STORED, + printed: "{\"rawKeys\":[\"name\"],\"raw\":{\"en\":\"Hello\",\"fr\":\"Bonjour\"},\"onlyFrench\":[\"Seulement\",\"undefined\"]}", + stored: [{ name: "the stored translations", expression: "$DB.getCollection('loc__products').find().sort({ price: 1 }).toArray().map((r) => r._internal.data.name)", expected: "[{\"en\":\"Hello\",\"fr\":\"Bonjour\"},{\"fr\":\"Seulement\"}]" }], + }), +} satisfies { [id: string]: Journey }; + +export const AUTODATE_JOURNEYS = { + autodate: scenario({ + title: "CreationTime is set on insert and kept on update, UpdateTime is set on insert and refreshed on update", + body: AUTO, + printed: "{\"insertSetsBoth\":[true,true,true],\"creationKeptOnUpdate\":true,\"updateRefreshed\":true,\"noMixinNeeded\":true}", + stored: [{ name: "the article dates", expression: "$DB.getCollection('auto__articles').find().toArray().map((r) => [r.title, r.creationDate instanceof Date, r.updateDate > r.creationDate])", expected: "[[\"Two\",true,true]]" }], + }), +} satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/models.ts b/.agents/skills/verify-interface/scenarios/models.ts new file mode 100644 index 0000000..bdd6e3d --- /dev/null +++ b/.agents/skills/verify-interface/scenarios/models.ts @@ -0,0 +1,205 @@ +import type { Journey } from "@antelopejs/tooling-configs/verify"; + +import { scenario } from "../runner.ts"; + +const CRUD = ` + class User extends d.Table {} + decorate(User, { email: [d.Field("string"), d.Index()], name: [d.Field("string")], role: [d.Field("string"), d.Index()] }); + d.RegisterTable("users", "crud")(User); + await d.RegisterSchema("crud"); + const users = d.GetModel(d.BasicDataModel(User)); + const single = await users.insert({ email: "alice@example.com", name: "Alice", role: "admin" }); + const batch = await users.insert([{ email: "bob@example.com", name: "Bob", role: "user" }, { email: "carol@example.com", name: "Carol", role: "user" }]); + const got = await users.get(single[0]); + const missing = await users.get("no-such-id"); + const admins = (await users.getBy("role", "admin")).map((row) => row.name); + const byEmail = (await users.getBy("email", "bob@example.com")).map((row) => row.name); + const everyone = (await users.getAll()).map((row) => row.name).sort(); + const updatedById = await users.update(single[0], { name: "Alice Liddell" }); + const updatedByObject = await users.update({ _id: batch[0], name: "Robert" }); + const updatedMissing = await users.update("no-such-id", { name: "Nobody" }); + const noKey = await rejection(users.update({ name: "No key" })); + const deleted = await users.delete(batch[1]); + const deletedMissing = await users.delete("no-such-id"); + return { + insertedIds: [single.length, batch.length, [...single, ...batch].every((id) => typeof id === "string")], + got: [got instanceof User, got._id === single[0], got.name], + missing: missing ?? "undefined", + admins, byEmail, everyone, + updatedById, updatedByObject, updatedMissing, noKey, + deleted, deletedMissing, + left: (await users.getAll()).map((row) => row.name).sort(), + }; +`; + +const KEY = "12345678901234567890123456789012"; + +const CONVERSION = ` + class Account extends d.Table.with(d.EncryptionModifier) {} + decorate(Account, { name: [d.Field("string")], secret: [d.Encrypted({ secretKey: "${KEY}" }), d.Field("string")] }); + d.RegisterTable("accounts", "cnv")(Account); + await d.RegisterSchema("cnv"); + const Model = d.BasicDataModel(Account); + const fromPlain = Model.fromPlainData({ _id: "a1", name: "Ann", secret: "s3cret" }); + const sameInstance = Model.fromPlainData(fromPlain) === fromPlain; + const stored = Model.toDatabase(fromPlain); + const fromPlainDirect = Model.toDatabase({ _id: "a2", name: "Bea" }); + const record = { _id: "a3", name: "Cy" }; + const copy = Model.fromDatabase(record); + const empty = Model.fromDatabase({}); + const afterConversions = await rawTable("cnv", "accounts").count(); + const model = d.GetModel(Model); + const [id] = await model.insert({ name: "Dee", secret: "hidden" }); + const read = await model.get(id); + const direct = await model.table.get(id); + const viaGetAll = await model.getAll(); + return { + fromPlain: [fromPlain instanceof Account, fromPlain.secret, fromPlain.name], + sameInstance, + storedShape: [stored instanceof Account, Object.keys(stored).sort(), stored._internal.data.secret.length, stored.secret === undefined], + plainDataShape: [fromPlainDirect instanceof Account, Object.keys(fromPlainDirect).sort()], + nullIsUndefined: [Model.fromDatabase(null) === undefined, Model.fromDatabase(undefined) === undefined], + emptyObjectIsAnInstance: empty instanceof Account, + copyNotSame: [copy !== record, copy instanceof Account, record instanceof Account, copy.name], + afterConversions, + read: [read instanceof Account, read.secret], + direct: [direct instanceof Account, direct.name, direct._internal !== undefined], + getAllConverted: viaGetAll.every((row) => row instanceof Account), + }; +`; + +const STR = `{ _tag: "codec", name: "string", is: (u) => typeof u === "string", encode: (a) => a, decode: (u) => (typeof u === "string" ? { _tag: "Right", right: u } : { _tag: "Left", left: [{ message: "expected a string" }, { message: "second message" }] }) }`; +const NUM = `{ _tag: "codec", name: "number", is: (u) => typeof u === "number", encode: (a) => a, decode: (u) => (typeof u === "number" ? { _tag: "Right", right: u } : { _tag: "Left", left: [] }) }`; + +const VALIDATION = ` + class Person extends d.Table {} + decorate(Person, { name: [d.Field(${STR})], age: [d.Field(${NUM})], nick: [d.Field("string")], extra: [] }); + d.RegisterTable("people", "val")(Person); + await d.RegisterSchema("val"); + const Model = d.BasicDataModel(Person); + const people = d.GetModel(Model); + const errorOf = async (promise) => { try { await promise; return null; } catch (error) { return { message: error.message, errors: error.errors }; } }; + const valid = Model.validate({ name: "Alice", age: 30 }); + const invalid = Model.validate({ name: 42, age: "old" }); + const notAnObject = Model.validate(null); + const partialOk = Model.validate({ name: "x" }, { partial: true }); + const partialEmpty = Model.validate({}, { partial: true }); + const partialBad = Model.validate({ age: "old" }, { partial: true }); + const tokenSkipped = Model.validate({ name: "x", age: 1, nick: 99, extra: {} }); + const optIn = await people.insert({ name: 42, age: "anything" }); + const refused = await errorOf(people.insert({ name: 42, age: "old" }, { validate: true })); + const batchRefused = await errorOf(people.insert([{ name: "ok", age: 1 }, { name: 1, age: 2 }, { name: "fine", age: "x" }], { validate: true })); + const accepted = await people.insert({ name: "Alice", age: 30 }, { validate: true }); + const patched = await people.update(accepted[0], { name: "Bob" }, { validate: true }); + const patchRefused = await errorOf(people.update(accepted[0], { age: "old" }, { validate: true })); + const objectPatchRefused = await errorOf(people.update({ _id: accepted[0], name: 7 }, { validate: true })); + const unvalidatedPatch = await people.update(accepted[0], { age: "old" }); + return { valid, invalid, notAnObject, partialOk, partialEmpty, partialBad, tokenSkipped, optIn: optIn.length, refused, batchRefused, patched, patchRefused, objectPatchRefused, unvalidatedPatch }; +`; + +const GET_MODEL = ` + class Item extends d.Table {} + decorate(Item, { name: [d.Field("string")] }); + d.RegisterTable("items", "gm")(Item); + await d.RegisterSchema("gm"); + const Model = d.BasicDataModel(Item); + const first = d.GetModel(Model); + const again = d.GetModel(Model); + const tenant = d.GetModel(Model, "t1"); + const tenantAgain = d.GetModel(Model, "t1"); + const other = d.GetModel(Model, "t2"); + const everywhere = d.GetModel(Model, iface.CROSS_INSTANCE); + await first.insert({ name: "default" }); + await tenant.insert({ name: "tenant one" }); + await other.insert({ name: "tenant two" }); + const OtherModel = d.BasicDataModel(Item); + class Custom { constructor(database) { this.database = database; this.table = database.table("things"); } } + Custom.schemaName = "manual"; + new iface.Schema("manual", { things: { fields: {}, indexes: {} } }); + const custom = d.GetModel(Custom); + class Lost { constructor(database) { this.database = database; } } + Lost.schemaName = "absent-schema"; + return { + cache: [first === again, tenant === tenantAgain, first !== tenant, tenant !== other, everywhere !== first, first instanceof Model, d.GetModel(OtherModel) !== first], + defaultSees: (await first.getAll()).map((row) => row.name), + tenantSees: (await tenant.getAll()).map((row) => row.name), + otherSees: (await other.getAll()).map((row) => row.name), + everywhereSees: (await everywhere.getAll()).map((row) => row.name).sort(), + customSchema: [custom instanceof Custom, d.GetModel(Custom) === custom], + lost: caught(() => d.GetModel(Lost)), + statics: [Model.schemaName, Model.tableClass === Item], + }; +`; + +const FACTORY = ` + class Plain extends d.Table {} + decorate(Plain, { name: [d.Field("string")] }); + const missingName = caught(() => d.BasicDataModel(Plain)); + new iface.Schema("fac", { explicit: { fields: { name: "string" }, indexes: {} } }); + const Explicit = d.BasicDataModel(Plain, "explicit"); + const model = new Explicit(iface.Schema.get("fac").instance()); + const [id] = await model.insert({ name: "named by argument" }); + class Registered extends d.Table {} + d.RegisterTable("registered", "fac")(Registered); + const Derived = d.BasicDataModel(Registered); + const Overridden = d.BasicDataModel(Registered, "explicit"); + return { missingName, explicit: (await model.get(id)).name, schemaOfPlain: Explicit.schemaName ?? "undefined", schemaOfRegistered: Derived.schemaName, overriddenTable: await new Overridden(iface.Schema.get("fac").instance()).getAll().then((rows) => rows.length) }; +`; + +const EXTEND = ` + class User extends d.Table {} + decorate(User, { email: [d.Field("string"), d.Index()], name: [d.Field("string")], status: [d.Field("string"), d.Index()] }); + d.RegisterTable("users", "ext")(User); + await d.RegisterSchema("ext"); + const UserModelBase = d.BasicDataModel(User); + class CustomUserModel extends UserModelBase { + async findActive() { + return this.table.filter((u) => u.key("status").eq("active")).then((rows) => rows.map((row) => CustomUserModel.fromDatabase(row)).filter((user) => user !== undefined)); + } + async register(email, name) { + const existing = await this.getBy("email", email); + if (existing.length > 0) throw new Error("Email already in use"); + return this.insert({ email, name, status: "active" }); + } + } + const model = d.GetModel(CustomUserModel); + await model.register("a@example.com", "Ada"); + const duplicate = await rejection(model.register("a@example.com", "Again")); + await model.insert({ email: "z@example.com", name: "Zed", status: "suspended" }); + const active = await model.findActive(); + return { isModel: model instanceof CustomUserModel, duplicate, active: active.map((user) => [user instanceof User, user.name]) }; +`; + +export const MODEL_JOURNEYS = { + crud: scenario({ + title: "a model inserts, reads, updates and deletes rows and answers ids and counts", + body: CRUD, + printed: "{\"insertedIds\":[1,2,true],\"got\":[true,true,\"Alice\"],\"missing\":\"undefined\",\"admins\":[\"Alice\"],\"byEmail\":[\"Bob\"],\"everyone\":[\"Alice\",\"Bob\",\"Carol\"],\"updatedById\":1,\"updatedByObject\":1,\"updatedMissing\":0,\"noKey\":{\"name\":\"AssertionError\",\"message\":\"Missing primary key in object update.\"},\"deleted\":1,\"deletedMissing\":0,\"left\":[\"Alice Liddell\",\"Robert\"]}", + stored: [{ name: "the rows left", expression: "$DB.getCollection('crud__users').find().sort({ email: 1 }).toArray().map((r) => [r.email, r.name, r.role])", expected: "[[\"alice@example.com\",\"Alice Liddell\",\"admin\"],[\"bob@example.com\",\"Robert\",\"user\"]]" }], + }), + conversion: scenario({ + title: "fromPlainData, fromDatabase and toDatabase convert without saving, and reads convert their rows", + body: CONVERSION, + printed: "{\"fromPlain\":[true,\"s3cret\",\"Ann\"],\"sameInstance\":true,\"storedShape\":[false,[\"_id\",\"_internal\",\"name\"],3,true],\"plainDataShape\":[false,[\"_id\",\"name\"]],\"nullIsUndefined\":[true,true],\"emptyObjectIsAnInstance\":true,\"copyNotSame\":[true,true,false,\"Cy\"],\"afterConversions\":0,\"read\":[true,\"hidden\"],\"direct\":[false,\"Dee\",true],\"getAllConverted\":true}", + stored: [{ name: "the one saved account", expression: "$DB.getCollection('cnv__accounts').find().toArray().map((r) => [r.name, Object.keys(r._internal.data), r._internal.data.secret.length])", expected: "[[\"Dee\",[\"secret\"],3]]" }], + }), + validation: scenario({ + title: "validate checks the io-ts codecs, and insert and update refuse invalid input only when asked", + body: VALIDATION, + printed: "{\"valid\":{\"ok\":true},\"invalid\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"notAnObject\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"partialOk\":{\"ok\":true},\"partialEmpty\":{\"ok\":true},\"partialBad\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"tokenSkipped\":{\"ok\":true},\"optIn\":1,\"refused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"batchRefused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"patched\":1,\"patchRefused\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"objectPatchRefused\":{\"message\":\"validation failed: name (expected a string)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"}]},\"unvalidatedPatch\":1}", + stored: [{ name: "the people", expression: "$DB.getCollection('val__people').find().sort({ name: 1 }).toArray().map((r) => [r.name, r.age])", expected: "[[42,\"anything\"],[\"Bob\",\"old\"]]" }], + }), + "get-model": scenario({ + title: "GetModel caches one model per class and instance id, and keeps each instance's rows apart", + body: GET_MODEL, + printed: "{\"cache\":[true,true,true,true,true,true,true],\"defaultSees\":[\"default\"],\"tenantSees\":[\"tenant one\"],\"otherSees\":[\"tenant two\"],\"everywhereSees\":[\"default\",\"tenant one\",\"tenant two\"],\"customSchema\":[true,true],\"lost\":\"Schema not found for 'absent-schema'\",\"statics\":[\"gm\",true]}", + stored: [{ name: "the rows by instance", expression: "$DB.getCollection('gm__items').find().sort({ name: 1 }).toArray().map((r) => [r.name, r._instance])", expected: "[[\"default\",null],[\"tenant one\",\"t1\"],[\"tenant two\",\"t2\"]]" }], + }), + "model-factory": scenario({ title: "BasicDataModel takes its table name from the argument or from RegisterTable", body: FACTORY, printed: "{\"missingName\":\"tableName must be provided or set via @RegisterTable\",\"explicit\":\"named by argument\",\"schemaOfPlain\":\"undefined\",\"schemaOfRegistered\":\"fac\",\"overriddenTable\":1}" }), + "extend-model": scenario({ + title: "a subclass of a model adds query methods that use the model's table and conversions", + body: EXTEND, + printed: "{\"isModel\":true,\"duplicate\":{\"name\":\"Error\",\"message\":\"Email already in use\"},\"active\":[[true,\"Ada\"]]}", + stored: [{ name: "the users", expression: "$DB.getCollection('ext__users').find().sort({ email: 1 }).toArray().map((r) => [r.email, r.status])", expected: "[[\"a@example.com\",\"active\"],[\"z@example.com\",\"suspended\"]]" }], + }), +} satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/registration.ts b/.agents/skills/verify-interface/scenarios/registration.ts new file mode 100644 index 0000000..537f9a4 --- /dev/null +++ b/.agents/skills/verify-interface/scenarios/registration.ts @@ -0,0 +1,152 @@ +import type { Journey } from "@antelopejs/tooling-configs/verify"; + +import { scenario } from "../runner.ts"; + +const REGISTER = ` + class Item extends d.Table {} + decorate(Item, { name: [d.Field("string"), d.Index()] }); + d.RegisterTable("items", "reg")(Item); + class Box extends d.Table {} + decorate(Box, { label: [d.Field("string")] }); + d.RegisterTable("boxes", "reg")(Box); + class Early extends d.Table {} + d.RegisterTable("early", "preexisting")(Early); + const noTables = await rejection(d.RegisterSchema("nothing-registered")); + const beforeBinding = caught(() => d.GetModel(d.BasicDataModel(Item))); + const returned = await d.RegisterSchema("reg"); + const schema = iface.Schema.get("reg"); + const afterBinding = d.GetModel(d.BasicDataModel(Item)) !== undefined; + const again = await d.RegisterSchema("reg"); + new iface.Schema("preexisting", { early: { fields: {}, indexes: {} } }); + const viaSchemaName = d.GetModel(d.BasicDataModel(Early)) !== undefined; + await d.GetModel(d.BasicDataModel(Item)).insert({ name: "one" }); + await d.GetModel(d.BasicDataModel(Box)).insert({ label: "red" }); + return { + noTables: noTables && noTables.message, + beforeBinding, + returned: returned ?? "undefined", + again: again ?? "undefined", + tables: Object.keys(schema.definition), + afterBinding, + viaSchemaName, + tableNameIsNotASchema: iface.Schema.get("items") === undefined, + }; +`; + +const FIXTURES = ` + const roles = [{ _id: "admin", name: "Administrator" }, { _id: "user", name: "Standard User" }]; + class Role extends d.Table {} + decorate(Role, { name: [d.Field("string")] }); + d.Fixture(() => roles)(Role); + d.RegisterTable("roles", "fx")(Role); + class Setting extends d.Table {} + decorate(Setting, { key: [d.Field("string")], value: [d.Field("string")] }); + d.Fixture(async () => ({ key: "theme", value: "dark" }))(Setting); + d.RegisterTable("settings", "fx")(Setting); + class Plan extends d.Table {} + decorate(Plan, { label: [d.Field("string")] }); + d.Fixture(async () => [{ label: "free" }, { label: "pro" }, { label: "team" }])(Plan); + d.RegisterTable("plans", "fx")(Plan); + class Nothing extends d.Table {} + d.Fixture(() => undefined)(Nothing); + d.RegisterTable("nothing", "fx")(Nothing); + class Loose extends d.Table {} + decorate(Loose, { name: [d.Field("string")] }); + d.Fixture(() => [{ _id: "loose", name: "never inserted" }])(Loose); + class Stamped extends d.Table.with(d.HashModifier) {} + decorate(Stamped, { secret: [d.Hashed(), d.Field("string")], created: [d.CreationTime(), d.Field("date")] }); + d.Fixture(() => [{ _id: "s1", secret: "hunter2" }])(Stamped); + d.RegisterTable("stamped", "fx")(Stamped); + await d.RegisterSchema("fx"); + const count = (name) => rawTable("fx", name).count(); + const first = { roles: await count("roles"), settings: await count("settings"), plans: await count("plans"), nothing: await count("nothing") }; + await rawTable("fx", "roles").get("user").delete(); + await d.RegisterSchema("fx"); + const afterSecondStart = await count("roles"); + const ids = (await rawTable("fx", "plans")).map((row) => row._id).sort(); + const stamped = await rawTable("fx", "stamped").get("s1"); + return { + first, + afterSecondStart, + plans: ids, + unregisteredClassProvisioned: Object.keys(iface.Schema.get("fx").definition).includes("loose"), + hashedAtInsert: stamped._internal.data.secret.startsWith("$scrypt$ln=15,r=8,p=3$") && !stamped._internal.data.secret.includes("hunter2"), + insertEventFired: stamped.created !== undefined, + }; +`; + +const FIXTURE_RACE = ` + class Seed extends d.Table {} + decorate(Seed, { name: [d.Field("string")] }); + d.Fixture(() => [{ name: "a" }, { name: "b" }])(Seed); + d.RegisterTable("seeds", "race")(Seed); + class Child extends Seed {} + d.RegisterTable("kids", "race")(Child); + let seen = null; + class Spy extends d.Table {} + decorate(Spy, { name: [d.Field("string")] }); + d.Fixture((cls) => { seen = cls.name; return [{ name: "spy" }]; })(Spy); + class SpyChild extends Spy {} + d.RegisterTable("spies", "race")(SpyChild); + await Promise.all([d.RegisterSchema("race"), d.RegisterSchema("race")]); + const rows = (await rawTable("race", "seeds")).map((row) => row._id).sort(); + const kids = (await rawTable("race", "kids")).map((row) => row._id).sort(); + return { rows, kids, generatorReceived: seen }; +`; + +const TABLE_ACCESS = ` + class Item extends d.Table {} + decorate(Item, { name: [d.Field("string")] }); + d.RegisterTable("items", "acc")(Item); + class Nameless extends d.Table {} + class Ghost extends d.Table {} + d.RegisterTable("ghosts", "never-registered")(Ghost); + const unbound = caught(() => Ghost.table()); + const nameless = caught(() => Nameless.table()); + const beforeRegister = caught(() => Item.table()); + await d.RegisterSchema("acc"); + await Item.table().insert({ name: "default" }).run(); + await Item.table("tenant-1").insert({ name: "tenant" }).run(); + await Item.table(iface.CROSS_INSTANCE).count(); + const held = iface.Schema.get("acc").instance("held"); + await Item.table(held).insert({ name: "held" }).run(); + return { + unbound, + nameless, + beforeRegister, + defaultCount: await Item.table().count(), + tenantCount: await Item.table("tenant-1").count(), + crossCount: await Item.table(iface.CROSS_INSTANCE).count(), + heldCount: await Item.table(held).count(), + }; +`; + +export const REGISTRATION_JOURNEYS = { + "register-schema": scenario({ + title: "RegisterSchema provisions the registered tables, binds the classes and refuses an unknown schema", + body: REGISTER, + printed: "{\"noTables\":\"No tables registered for schema 'nothing-registered'\",\"beforeBinding\":\"Item is not bound to a schema: call RegisterSchema(\\\"reg\\\") in the module that declares it\",\"returned\":\"undefined\",\"again\":\"undefined\",\"tables\":[\"items\",\"boxes\"],\"afterBinding\":true,\"viaSchemaName\":true,\"tableNameIsNotASchema\":true}", + stored: [{ name: "the collections of the schema", expression: "$DB.getCollectionNames().filter((n) => n.startsWith('reg__')).sort()", expected: "[\"reg__boxes\",\"reg__items\"]" }], + }), + fixtures: scenario({ + title: "fixtures fill an empty table once, with generated ids and the same conversion as an insert", + body: FIXTURES, + printed: "{\"first\":{\"roles\":2,\"settings\":1,\"plans\":3,\"nothing\":0},\"afterSecondStart\":1,\"plans\":[\"plans:fixture:0\",\"plans:fixture:1\",\"plans:fixture:2\"],\"unregisteredClassProvisioned\":false,\"hashedAtInsert\":true,\"insertEventFired\":true}", + stored: [ + { name: "the plan rows", expression: "$DB.getCollection('fx__plans').find({}, { name: 0 }).sort({ _id: 1 }).toArray().map((r) => [r._id, r.label])", expected: "[[\"plans:fixture:0\",\"free\"],[\"plans:fixture:1\",\"pro\"],[\"plans:fixture:2\",\"team\"]]" }, + { name: "the stamped row", expression: "$DB.getCollection('fx__stamped').find().toArray().map((r) => [r._id, r._internal.data.secret.startsWith('$scrypt$ln=15,r=8,p=3$'), r._internal.data.secret.includes('hunter2'), r.created !== undefined])", expected: "[[\"s1\",true,false,true]]" }, + ], + }), + "fixture-race": scenario({ + title: "two starts on one empty table write each fixture row once, and a subclass gets its parent's fixture", + body: FIXTURE_RACE, + printed: "{\"rows\":[\"seeds:fixture:0\",\"seeds:fixture:1\"],\"kids\":[\"kids:fixture:0\",\"kids:fixture:1\"],\"generatorReceived\":\"SpyChild\"}", + stored: [{ name: "the seed rows", expression: "$DB.getCollection('race__seeds').find().sort({ _id: 1 }).toArray().map((r) => [r._id, r.name])", expected: "[[\"seeds:fixture:0\",\"a\"],[\"seeds:fixture:1\",\"b\"]]" }], + }), + "table-access": scenario({ + title: "Table.table opens the bound table for the default instance, an instance id, all instances or a held instance", + body: TABLE_ACCESS, + printed: "{\"unbound\":\"Ghost is not bound to a schema: call RegisterSchema(\\\"never-registered\\\") in the module that declares it\",\"nameless\":\"Nameless has no table name: decorate it with @RegisterTable\",\"beforeRegister\":\"Item is not bound to a schema: call RegisterSchema(\\\"acc\\\") in the module that declares it\",\"defaultCount\":1,\"tenantCount\":1,\"crossCount\":3,\"heldCount\":1}", + stored: [{ name: "the rows by instance", expression: "$DB.getCollection('acc__items').find().sort({ name: 1 }).toArray().map((r) => [r.name, r._instance])", expected: "[[\"default\",null],[\"held\",\"held\"],[\"tenant\",\"tenant-1\"]]" }], + }), +} satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/tables.ts b/.agents/skills/verify-interface/scenarios/tables.ts new file mode 100644 index 0000000..7f3c285 --- /dev/null +++ b/.agents/skills/verify-interface/scenarios/tables.ts @@ -0,0 +1,147 @@ +import type { Journey } from "@antelopejs/tooling-configs/verify"; + +import { scenario } from "../runner.ts"; + +const INDEX_OPTIONS = ` + class User extends d.Table {} + decorate(User, { + email: [d.Field("string"), d.Index({ unique: true })], + first: [d.Field("string"), d.Index({ group: "name", crossInstance: true })], + last: [d.Field("string"), d.Index({ group: "name" })], + age: [d.Index(), d.Field("number")], + note: [], + }); + d.RegisterTable("users", "idx")(User); + await d.RegisterSchema("idx"); + const definition = iface.Schema.get("idx").definition; + const Model = d.BasicDataModel(User); + const model = d.GetModel(Model); + const other = d.GetModel(Model, "tenant-2"); + await model.insert({ email: "a@x", first: "Ada", last: "Lovelace", age: 36 }); + const duplicate = await rejection(model.insert({ email: "a@x", first: "Bob", last: "Brown", age: 40 })); + const otherInstance = await other.insert({ email: "a@x", first: "Ada", last: "Lovelace", age: 36 }); + const byGroup = (await model.getBy("name", "Ada", "Lovelace")).map((row) => row.email); + const bySingle = (await model.getBy("age", 36)).map((row) => row.first); + const between = (await rawTable("idx", "users").between("age", 30, 38)).map((row) => row.first); + const symbolKey = caught(() => d.Index()({}, Symbol("k"))); + await sleep(2500); + return { definition: definition.users, duplicate, otherInstanceStored: otherInstance.length, byGroup, bySingle, between, symbolKey }; +`; + +const GROUP_FLAGS = ` + class Pair extends d.Table {} + decorate(Pair, { + a: [d.Field("string"), d.Index({ group: "ab" })], + b: [d.Field("string"), d.Index({ group: "ab", unique: true })], + c: [d.Field("string"), d.Index({ group: "cd", crossInstance: true })], + e: [d.Field("string"), d.Index({ group: "cd" })], + }); + d.RegisterTable("pairs", "grp")(Pair); + await d.RegisterSchema("grp"); + const model = d.GetModel(d.BasicDataModel(Pair)); + await model.insert({ a: "1", b: "x", c: "p", e: "q" }); + const sameKey = await rejection(model.insert({ a: "1", b: "x", c: "r", e: "s" })); + const otherSecond = await model.insert({ a: "1", b: "y", c: "p", e: "q" }); + return { indexes: iface.Schema.get("grp").definition.pairs.indexes, sameKey, otherSecond: otherSecond.length }; +`; + +const FIELD_TYPES = ` + const codec = { _tag: "codec", name: "Positive", decode: (u) => (typeof u === "number" && u > 0 ? { _tag: "Right", right: u } : { _tag: "Left", left: [{ message: "not positive" }] }), encode: (a) => a, is: (u) => typeof u === "number" }; + class Order extends d.Table {} + decorate(Order, { + id: [d.Field("string")], + tags: [d.Field(["string"])], + address: [d.Field({ street: "string", geo: { lat: "number", lng: "number" } })], + total: [d.Field(codec), d.Index()], + lateIndex: [d.Index(), d.Field("number")], + plain: [], + }); + d.RegisterTable("orders", "fld")(Order); + await d.RegisterSchema("fld"); + const fields = iface.Schema.get("fld").definition.orders.fields; + return { + names: Object.keys(fields), + id: fields.id, + tags: fields.tags, + address: fields.address, + codecIsTheSameObject: fields.total === codec, + lateIndexType: fields.lateIndex, + indexes: Object.keys(iface.Schema.get("fld").definition.orders.indexes), + }; +`; + +const RELATION = ` + class Author extends d.Table {} + decorate(Author, { name: [d.Field("string")] }); + class Post extends d.Table {} + decorate(Post, { + authorId: [d.Index(), d.Field("string"), d.Relation({ to: () => Author })], + tagIds: [d.Relation({ to: () => Author, toField: "name", many: true }), d.Field(["string"])], + title: [d.Field("string")], + }); + d.RegisterTable("authors", "rel")(Author); + d.RegisterTable("posts", "rel")(Post); + await d.RegisterSchema("rel"); + const metadata = d.getMetadata(Post, d.RelationStaticMetadata).relations; + const posts = d.GetModel(d.BasicDataModel(Post)); + const ids = await posts.insert({ authorId: "nobody", tagIds: ["ghost"], title: "Dangling" }); + const stored = await posts.get(ids[0]); + const definition = iface.Schema.get("rel").definition.posts; + class Child extends Post {} + decorate(Child, { editorId: [d.Field("string"), d.Relation({ to: () => Author })] }); + return { + relationFields: Object.keys(metadata), + authorTarget: metadata.authorId.to() === Author, + authorToField: metadata.authorId.toField ?? null, + tagMany: metadata.tagIds.many, + tagToField: metadata.tagIds.toField, + dangling: [stored.authorId, stored.tagIds], + fieldsKept: Object.keys(definition.fields), + childRelations: Object.keys(d.getMetadata(Child, d.RelationStaticMetadata).relations), + parentRelations: Object.keys(d.getMetadata(Post, d.RelationStaticMetadata).relations), + }; +`; + +const INHERITANCE = ` + class Base extends d.Table {} + decorate(Base, { createdAt: [d.Field("date")] }); + d.RegisterTable("people", "inh")(Base); + class Person extends Base {} + decorate(Person, { email: [d.Field("string"), d.Index({ unique: true })] }); + class Employee extends Person {} + decorate(Employee, { badge: [d.Field("string"), d.Index()] }); + class Contractor extends Base {} + decorate(Contractor, { rate: [d.Field("number")] }); + const meta = (cls) => { const m = d.getMetadata(cls, d.DatumStaticMetadata); return { table: m.tableName ?? null, schema: m.schemaName ?? null, fields: Object.keys(m.fields), indexes: Object.keys(m.indexes), unique: [...m.uniqueIndexes] }; }; + return { base: meta(Base), person: meta(Person), employee: meta(Employee), contractor: meta(Contractor) }; +`; + +const WITH = ` + class Plain extends d.Table {} + class Hashing extends d.Table.with(d.HashModifier) {} + class Both extends d.Table.with(d.HashModifier, d.LocalizationModifier) {} + const has = (cls, name) => typeof new cls()[name] === "function"; + const sub = d.Table.with(d.HashModifier); + return { + plainHasTestHash: has(Plain, "testHash"), + hashingHas: [has(Hashing, "testHash"), has(Hashing, "testHashAsync"), has(Hashing, "isHashOutdated"), has(Hashing, "localize")], + bothHas: [has(Both, "testHash"), has(Both, "localize")], + isTableSubclass: new Hashing() instanceof d.Table, + tableItselfUntouched: typeof d.Table.prototype.testHash, + subIsASubclass: sub.prototype instanceof d.Table, + }; +`; + +export const TABLE_JOURNEYS = { + "index-options": scenario({ + title: "Field and Index reach the database as fields and plain, compound, cross-instance and unique indexes", + body: INDEX_OPTIONS, + printed: "{\"definition\":{\"fields\":{\"email\":\"string\",\"first\":\"string\",\"last\":\"string\",\"age\":\"number\"},\"indexes\":{\"email\":{\"fields\":null,\"unique\":true},\"name\":{\"fields\":[\"first\",\"last\"],\"crossInstance\":true},\"age\":{\"fields\":null}}},\"duplicate\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"email\\\" (email)\"},\"otherInstanceStored\":1,\"byGroup\":[\"a@x\"],\"bySingle\":[\"Ada\"],\"between\":[\"Ada\"],\"symbolKey\":\"@Index on Symbol(k): a database index names a field, never a symbol\"}", + stored: [{ name: "the indexes of the users collection", expression: "$DB.getCollection('idx__users').getIndexes().map((i) => [i.name, i.unique === true]).sort()", expected: "[[\"_id_\",false],[\"age__i\",false],[\"email__i\",true],[\"name\",false],[\"name__i\",false]]" }], + }), + "group-flags": scenario({ title: "unique and crossInstance on any field of a group apply to the whole group", body: GROUP_FLAGS, printed: "{\"indexes\":{\"ab\":{\"fields\":[\"a\",\"b\"],\"unique\":true},\"cd\":{\"fields\":[\"c\",\"e\"],\"crossInstance\":true}},\"sameKey\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"ab\\\" (a, b)\"},\"otherSecond\":1}" }), + "field-types": scenario({ title: "Field hands every type to the database unchanged and combines with Index in either order", body: FIELD_TYPES, printed: "{\"names\":[\"id\",\"tags\",\"address\",\"total\",\"lateIndex\"],\"id\":\"string\",\"tags\":[\"string\"],\"address\":{\"street\":\"string\",\"geo\":{\"lat\":\"number\",\"lng\":\"number\"}},\"codecIsTheSameObject\":true,\"lateIndexType\":\"number\",\"indexes\":[\"total\",\"lateIndex\"]}" }), + "relation-metadata": scenario({ title: "Relation records links without enforcing them", body: RELATION, printed: "{\"relationFields\":[\"authorId\",\"tagIds\"],\"authorTarget\":true,\"authorToField\":null,\"tagMany\":true,\"tagToField\":\"name\",\"dangling\":[\"nobody\",[\"ghost\"]],\"fieldsKept\":[\"authorId\",\"tagIds\",\"title\"],\"childRelations\":[\"authorId\",\"tagIds\",\"editorId\"],\"parentRelations\":[\"authorId\",\"tagIds\"]}" }), + inheritance: scenario({ title: "a table subclass starts with a copy of its parent and never changes it", body: INHERITANCE, printed: "{\"base\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\"],\"indexes\":[],\"unique\":[]},\"person\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\"],\"indexes\":[\"email\"],\"unique\":[\"email\"]},\"employee\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\",\"badge\"],\"indexes\":[\"email\",\"badge\"],\"unique\":[\"email\"]},\"contractor\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"rate\"],\"indexes\":[],\"unique\":[]}}" }), + "table-with": scenario({ title: "Table.with adds the mixin methods of the modifiers to a subclass", body: WITH, printed: "{\"plainHasTestHash\":false,\"hashingHas\":[true,true,true,false],\"bothHas\":[true,true],\"isTableSubclass\":true,\"tableItselfUntouched\":\"undefined\",\"subIsASubclass\":true}" }), +} satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/seeded-breaks/autodate.patch b/.agents/skills/verify-interface/seeded-breaks/autodate.patch new file mode 100644 index 0000000..e08ed4e --- /dev/null +++ b/.agents/skills/verify-interface/seeded-breaks/autodate.patch @@ -0,0 +1,13 @@ +diff --git a/src/modifiers/autodate.ts b/src/modifiers/autodate.ts +index 3be1f82..e6a5c58 100644 +--- a/src/modifiers/autodate.ts ++++ b/src/modifiers/autodate.ts +@@ -25,7 +25,7 @@ export class AutoDateModifier extends Modifier { + if (this.options.type === "updated") { + object[field] = new Date(); + } else { +- delete object[field]; ++ object[field] = new Date(); + } + } + } diff --git a/.agents/skills/verify-interface/seeded-breaks/custom-modifiers.patch b/.agents/skills/verify-interface/seeded-breaks/custom-modifiers.patch new file mode 100644 index 0000000..5b3e4e9 --- /dev/null +++ b/.agents/skills/verify-interface/seeded-breaks/custom-modifiers.patch @@ -0,0 +1,13 @@ +diff --git a/src/modifiers/common.ts b/src/modifiers/common.ts +index 638798b..7eb97df 100644 +--- a/src/modifiers/common.ts ++++ b/src/modifiers/common.ts +@@ -127,7 +127,7 @@ export class ContainerModifier< + if (key === "*") { + return locked_value; + } +- return Object.hasOwn(locked_value, key) ? locked_value[key] : undefined; ++ return locked_value[key]; + } + public override unlockrequest( + // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- the container holds the field's values by key, typed by the table class, not by the modifier diff --git a/.agents/skills/verify-interface/seeded-breaks/declarations.patch b/.agents/skills/verify-interface/seeded-breaks/declarations.patch new file mode 100644 index 0000000..b36f9ae --- /dev/null +++ b/.agents/skills/verify-interface/seeded-breaks/declarations.patch @@ -0,0 +1,12 @@ +diff --git a/src/index.ts b/src/index.ts +index ef4a9de..cff4396 100644 +--- a/src/index.ts ++++ b/src/index.ts +@@ -1,7 +1,6 @@ + import "reflect-metadata"; + + export * from "./common"; +-export * from "./database"; + export * from "./model"; + export * from "./modifiers"; + export * from "./relation"; diff --git a/.agents/skills/verify-interface/seeded-breaks/encryption.patch b/.agents/skills/verify-interface/seeded-breaks/encryption.patch new file mode 100644 index 0000000..b74593f --- /dev/null +++ b/.agents/skills/verify-interface/seeded-breaks/encryption.patch @@ -0,0 +1,13 @@ +diff --git a/src/modifiers/encryption.ts b/src/modifiers/encryption.ts +index b569dc7..4e86667 100644 +--- a/src/modifiers/encryption.ts ++++ b/src/modifiers/encryption.ts +@@ -95,7 +95,7 @@ export class EncryptionModifier extends TwoWayModifier< + // oxlint-disable-next-line anti-slop/no-unknown-parameters -- overrides OneWayModifier.lock; a modified field's value has its table class's type, which the shared modifier chain cannot name + value: unknown, + ): LockedType { +- const iv = randomBytes(this.options.ivSize || 16); ++ const iv = randomBytes(16); + const cipher = createCipheriv( + this.options.algorithm || "aes-256-gcm", + this.options.secretKey, diff --git a/.agents/skills/verify-interface/seeded-breaks/hashing.patch b/.agents/skills/verify-interface/seeded-breaks/hashing.patch new file mode 100644 index 0000000..5cacd2a --- /dev/null +++ b/.agents/skills/verify-interface/seeded-breaks/hashing.patch @@ -0,0 +1,13 @@ +diff --git a/src/modifiers/hash.ts b/src/modifiers/hash.ts +index f23cce8..05ca063 100644 +--- a/src/modifiers/hash.ts ++++ b/src/modifiers/hash.ts +@@ -52,7 +52,7 @@ type HashCheck = { + }; + + const SCRYPT_COST: ScryptCost = { ln: 15, r: 8, p: 3 }; +-const SALT_BYTES = 16; ++const SALT_BYTES = 8; + const KEY_BYTES = 32; + const DECOY_SALT = Buffer.alloc(SALT_BYTES); + const LEGACY_DIGEST = "sha256"; diff --git a/.agents/skills/verify-interface/seeded-breaks/localization.patch b/.agents/skills/verify-interface/seeded-breaks/localization.patch new file mode 100644 index 0000000..d389de0 --- /dev/null +++ b/.agents/skills/verify-interface/seeded-breaks/localization.patch @@ -0,0 +1,13 @@ +diff --git a/src/modifiers/localization.ts b/src/modifiers/localization.ts +index c672436..539a822 100644 +--- a/src/modifiers/localization.ts ++++ b/src/modifiers/localization.ts +@@ -83,7 +83,7 @@ export class LocalizationModifier extends ContainerModifier< + // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- the container modifier leaves the values by locale untyped + public override unlock(locked_value: Record, key: string) { + const unlocked = super.unlock(locked_value, key); +- if (unlocked !== undefined && unlocked !== EMPTY_TEXT) { ++ if (unlocked !== undefined) { + return unlocked; + } + if (this.options.fallbackLocale) { diff --git a/.agents/skills/verify-interface/seeded-breaks/models.patch b/.agents/skills/verify-interface/seeded-breaks/models.patch new file mode 100644 index 0000000..f0fbf8e --- /dev/null +++ b/.agents/skills/verify-interface/seeded-breaks/models.patch @@ -0,0 +1,13 @@ +diff --git a/src/model.ts b/src/model.ts +index 3f26f32..8d1e42c 100644 +--- a/src/model.ts ++++ b/src/model.ts +@@ -140,7 +140,7 @@ export function BasicDataModel( + * @returns Table class instance + */ + public static fromDatabase(obj: any) { +- return obj ? fromDatabase(obj, dataType) : undefined; ++ return obj && Object.keys(obj).length > 0 ? fromDatabase(obj, dataType) : undefined; + } + + /** diff --git a/.agents/skills/verify-interface/seeded-breaks/parameter-decoration.patch b/.agents/skills/verify-interface/seeded-breaks/parameter-decoration.patch new file mode 100644 index 0000000..971f48c --- /dev/null +++ b/.agents/skills/verify-interface/seeded-breaks/parameter-decoration.patch @@ -0,0 +1,13 @@ +diff --git a/src/model.ts b/src/model.ts +index 3f26f32..41aa96d 100644 +--- a/src/model.ts ++++ b/src/model.ts +@@ -419,7 +419,7 @@ export const Model = MakeParameterAndPropertyDecorator( + ) => { + if (isInstanceIdProvider(instanceIdOrCallback)) { + SetParameterProvider(target, key, index, (ctx: RequestContext) => { +- return GetModel(cl, instanceIdOrCallback(ctx)); ++ return GetModel(cl); + }); + } else { + SetParameterProvider(target, key, index, () => diff --git a/.agents/skills/verify-interface/seeded-breaks/registration.patch b/.agents/skills/verify-interface/seeded-breaks/registration.patch new file mode 100644 index 0000000..fa9ec65 --- /dev/null +++ b/.agents/skills/verify-interface/seeded-breaks/registration.patch @@ -0,0 +1,13 @@ +diff --git a/src/database.ts b/src/database.ts +index 93cbfa4..b8e814a 100644 +--- a/src/database.ts ++++ b/src/database.ts +@@ -94,7 +94,7 @@ async function insertFixtureData( + return; + } + const count = await schema.instance().table(tableName).count(); +- if (count > 0) { ++ if (count > 1) { + return; + } + const fixtureData = await generator(tableClass); diff --git a/.agents/skills/verify-interface/seeded-breaks/smoke.patch b/.agents/skills/verify-interface/seeded-breaks/smoke.patch new file mode 100644 index 0000000..2be8a13 --- /dev/null +++ b/.agents/skills/verify-interface/seeded-breaks/smoke.patch @@ -0,0 +1,13 @@ +diff --git a/src/model.ts b/src/model.ts +index 3f26f32..2b6395b 100644 +--- a/src/model.ts ++++ b/src/model.ts +@@ -298,7 +298,7 @@ export function BasicDataModel( + * @returns Delete result + */ + public delete(id: string) { +- return this.table.get(id).delete().run(); ++ return this.table.get(id).run(); + } + }; + return Model; diff --git a/.agents/skills/verify-interface/seeded-breaks/tables.patch b/.agents/skills/verify-interface/seeded-breaks/tables.patch new file mode 100644 index 0000000..1707dd6 --- /dev/null +++ b/.agents/skills/verify-interface/seeded-breaks/tables.patch @@ -0,0 +1,13 @@ +diff --git a/src/table.ts b/src/table.ts +index 89445f4..78cf036 100644 +--- a/src/table.ts ++++ b/src/table.ts +@@ -220,7 +220,7 @@ export const Index = MakePropertyDecorator( + const key = propertyKey; + metadata.addIndex( + key, +- options?.group || key, ++ key, + options?.crossInstance, + options?.unique, + ); diff --git a/.agents/skills/verify-interface/seeded-doors/contract.patch b/.agents/skills/verify-interface/seeded-doors/contract.patch new file mode 100644 index 0000000..4e68584 --- /dev/null +++ b/.agents/skills/verify-interface/seeded-doors/contract.patch @@ -0,0 +1,11 @@ +diff --git a/src/index.ts b/src/index.ts +index ef4a9de..48fc1c8 100644 +--- a/src/index.ts ++++ b/src/index.ts +@@ -4,6 +4,5 @@ export * from "./common"; + export * from "./database"; + export * from "./model"; + export * from "./modifiers"; +-export * from "./relation"; + export * from "./schema"; + export * from "./table"; diff --git a/.agents/skills/verify-interface/seeded-doors/data.patch b/.agents/skills/verify-interface/seeded-doors/data.patch new file mode 100644 index 0000000..2074bb7 --- /dev/null +++ b/.agents/skills/verify-interface/seeded-doors/data.patch @@ -0,0 +1,13 @@ +diff --git a/src/model.ts b/src/model.ts +index 3f26f32..fac3b3a 100644 +--- a/src/model.ts ++++ b/src/model.ts +@@ -298,7 +298,7 @@ export function BasicDataModel( + * @returns Delete result + */ + public delete(id: string) { +- return this.table.get(id).delete().run(); ++ return this.table.delete().run(); + } + }; + return Model; diff --git a/.agents/skills/verify-interface/seeded-doors/format.patch b/.agents/skills/verify-interface/seeded-doors/format.patch new file mode 100644 index 0000000..97cec9b --- /dev/null +++ b/.agents/skills/verify-interface/seeded-doors/format.patch @@ -0,0 +1,13 @@ +diff --git a/src/modifiers/hash.ts b/src/modifiers/hash.ts +index f23cce8..4e4b4e2 100644 +--- a/src/modifiers/hash.ts ++++ b/src/modifiers/hash.ts +@@ -51,7 +51,7 @@ type HashCheck = { + verdict: (derived: Buffer | undefined) => boolean; + }; + +-const SCRYPT_COST: ScryptCost = { ln: 15, r: 8, p: 3 }; ++const SCRYPT_COST: ScryptCost = { ln: 14, r: 8, p: 3 }; + const SALT_BYTES = 16; + const KEY_BYTES = 32; + const DECOY_SALT = Buffer.alloc(SALT_BYTES); diff --git a/.agents/skills/verify-interface/seeded-doors/publish.patch b/.agents/skills/verify-interface/seeded-doors/publish.patch new file mode 100644 index 0000000..35501f5 --- /dev/null +++ b/.agents/skills/verify-interface/seeded-doors/publish.patch @@ -0,0 +1,13 @@ +diff --git a/.release-it.json b/.release-it.json +index a7604b6..90251b6 100644 +--- a/.release-it.json ++++ b/.release-it.json +@@ -18,7 +18,7 @@ + } + }, + "npm": { +- "publish": true, ++ "publish": false, + "skipChecks": true + } + } diff --git a/.agents/skills/verify-interface/tsconfig.json b/.agents/skills/verify-interface/tsconfig.json new file mode 100644 index 0000000..8d4c934 --- /dev/null +++ b/.agents/skills/verify-interface/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "rootDir": "../../..", + "module": "nodenext", + "moduleResolution": "nodenext", + "allowImportingTsExtensions": true, + "composite": false, + "noEmit": true + }, + "include": [ + "**/*.ts" + ] +} diff --git a/.agents/skills/verify-interface/verify.config.ts b/.agents/skills/verify-interface/verify.config.ts new file mode 100644 index 0000000..72fc9dc --- /dev/null +++ b/.agents/skills/verify-interface/verify.config.ts @@ -0,0 +1,53 @@ +import type { VerifyConfig } from "@antelopejs/tooling-configs/verify"; + +import { CUSTOM_JOURNEYS } from "./scenarios/custom.ts"; +import { DECLARATION_JOURNEYS } from "./scenarios/declarations.ts"; +import { DECORATION_JOURNEYS } from "./scenarios/decoration.ts"; +import { ENCRYPTION_JOURNEYS } from "./scenarios/encryption.ts"; +import { HASH_JOURNEYS } from "./scenarios/hashing.ts"; +import { AUTODATE_JOURNEYS, LOCALIZATION_JOURNEYS } from "./scenarios/localization.ts"; +import { MODEL_JOURNEYS } from "./scenarios/models.ts"; +import { REGISTRATION_JOURNEYS } from "./scenarios/registration.ts"; +import { TABLE_JOURNEYS } from "./scenarios/tables.ts"; + +const config: VerifyConfig = { + services: { + mongo: { + image: "mongo:7", + port: 27017, + ready: ["mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"], + reset: ["mongosh", "--quiet", "--eval", 'db.getSiblingDB("verify").dropDatabase()'], + }, + }, + doors: { + data: { + globs: ["src/model.ts", "src/database.ts"], + funnels: [], + containments: ["models/crud"], + why: "model.insert, update and delete and RegisterSchema's fixtures write and remove rows in the consumer's database", + }, + format: { + globs: ["src/modifiers/encryption.ts", "src/modifiers/hash.ts", "src/modifiers/localization.ts", "src/modifiers/internal/records.ts"], + funnels: [], + containments: ["encryption/encryption-at-rest", "hashing/hash-storage"], + why: "the encrypted [ciphertext, iv, authTag] triple, the $scrypt$ln=15,r=8,p=3$salt$key string, the digest and salt and the per-locale record are what rows persist as", + }, + contract: { + globs: ["src/index.ts", "package.json"], + funnels: [], + containments: [], + why: "the exports and subpaths are what every module declaring tables imports", + }, + outbound: null, + exposure: null, + publish: { + globs: ["package.json", ".release-it.json", "changelogen.config.json", ".github/workflows/release.yml"], + funnels: [], + containments: [], + why: "the package name and release workflow claim @antelopejs/interface-database-decorators on npm", + }, + }, + journeys: { ...TABLE_JOURNEYS, ...REGISTRATION_JOURNEYS, ...MODEL_JOURNEYS, ...DECORATION_JOURNEYS, ...ENCRYPTION_JOURNEYS, ...HASH_JOURNEYS, ...LOCALIZATION_JOURNEYS, ...AUTODATE_JOURNEYS, ...CUSTOM_JOURNEYS, ...DECLARATION_JOURNEYS }, +}; + +export default config; diff --git a/.claude/skills/verify-interface b/.claude/skills/verify-interface new file mode 120000 index 0000000..c7df9cd --- /dev/null +++ b/.claude/skills/verify-interface @@ -0,0 +1 @@ +../../.agents/skills/verify-interface \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index a803ecc..a7e021d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,6 +33,8 @@ The release commit writes `CHANGELOG.md`. Upgrade notes for that version follow Finish every change with `pnpm run checks`. It runs typecheck, lint, format, knip, tests, size, suppressions, and sync, and prints only what failed. The change is done when it prints no failure. +Prove a change works with the verification skill in [`.agents/skills/verify-interface/SKILL.md`](.agents/skills/verify-interface/SKILL.md) before you call it done. + The tools enforce most rules, listed in the table at the end. The sections below hold the rules no tool checks. ### Comments From de09f9029ce08424e7159de49b61f3e37cb587ae Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 3 Nov 2026 15:45:05 +0100 Subject: [PATCH 20/45] fix(skills): drop minor-bump from smoke and contain the encryption format door --- .agents/skills/verify-interface/features/README.md | 3 ++- .agents/skills/verify-interface/features/smoke.md | 6 ++---- .../verify-interface/seeded-doors/format.patch | 13 +++++++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.agents/skills/verify-interface/features/README.md b/.agents/skills/verify-interface/features/README.md index 21f92ec..cfb164f 100644 --- a/.agents/skills/verify-interface/features/README.md +++ b/.agents/skills/verify-interface/features/README.md @@ -15,13 +15,14 @@ Every journey runs a scenario inside `ajs module test` with `@antelopejs/mongodb | [models](models.md) | A developer reads and writes rows through a typed model. | crud, conversion, validation, get-model, model-factory, extend-model | | [parameter-decoration](parameter-decoration.md) | A controller gets a model injected per route, instance or property. | fixed-instance, per-request-instance, property-decoration, crud-controller | | [registration](registration.md) | A module registers its schema, fills fixtures and opens tables from their classes. | register-schema, fixtures, fixture-race, table-access | -| [smoke](smoke.md) | The fixed journeys every verdict runs. | register-schema, crud, minor-bump | +| [smoke](smoke.md) | The fixed journeys every verdict runs. | register-schema, crud | | [tables](tables.md) | A developer describes a table with Field, Index and Relation, and subclasses it. | index-options, group-flags, field-types, relation-metadata, inheritance, table-with | ## Not driven | Documented behaviour | Why no journey drives it | |---|---| +| The minor-bump rule of the interface kind | Fails at the current head, not at a change: `campaign/integration` exports more than the published 0.1.12 and `package.json` is still 0.1.12. Add it to `smoke` in the PR that bumps the version. | | A model "stays on the declaring module's database even when another module calls `GetModel`" (docs/4, GetModel) | Needs two modules with two databases behind one provider; one container serves one database here. | | `testHash` and `testHashAsync` spend one scrypt run whatever the row holds, and the time of a refusal does not tell the row's format (docs/3, HashModifier) | A timing claim; a journey comparing wall-clock times would be flaky on a shared machine. | | A digest field's test spends no scrypt run (docs/3) | Same: a timing claim. | diff --git a/.agents/skills/verify-interface/features/smoke.md b/.agents/skills/verify-interface/features/smoke.md index 5af8102..edec47f 100644 --- a/.agents/skills/verify-interface/features/smoke.md +++ b/.agents/skills/verify-interface/features/smoke.md @@ -5,18 +5,16 @@ docs: journeys: - register-schema - crud - - minor-bump --- # Smoke -The fixed journeys every verdict runs, whatever the diff touches: provision a schema from decorated tables, write and read rows through a model in a live MongoDB, and compare the declared functions with the published version. +The fixed journeys every verdict runs, whatever the diff touches: provision a schema from decorated tables, write and read rows through a model in a live MongoDB,. ## Sub-features - `register-schema`: as in [registration](registration.md). - `crud`: as in [models](models.md). -- `minor-bump`: an added, changed or removed function releases as at least a new minor. ## How to get to it (user POV) @@ -31,5 +29,5 @@ pnpm exec antelopejs-verify journey "$RUN" smoke ## Gotchas - The smoke feature covers no glob, so it never marks a file as mapped. -- `minor-bump` needs the npm registry; without it the journey is not verified. +- The pack's `minor-bump` journey is not in this feature. It fails at the current head: `campaign/integration` already adds and changes exports over the published 0.1.12 while `package.json` still says 0.1.12, so the next release must be at least a minor. Add `minor-bump` to `smoke` in the PR that bumps the version. - `every-function` is not part of this feature: the package exports decorators, classes and plain functions, and no interface proxy a stub provider could answer. The scenarios below it drive the real functions against MongoDB instead. diff --git a/.agents/skills/verify-interface/seeded-doors/format.patch b/.agents/skills/verify-interface/seeded-doors/format.patch index 97cec9b..fc16397 100644 --- a/.agents/skills/verify-interface/seeded-doors/format.patch +++ b/.agents/skills/verify-interface/seeded-doors/format.patch @@ -11,3 +11,16 @@ index f23cce8..4e4b4e2 100644 const SALT_BYTES = 16; const KEY_BYTES = 32; const DECOY_SALT = Buffer.alloc(SALT_BYTES); +diff --git a/src/modifiers/encryption.ts b/src/modifiers/encryption.ts +index b569dc7..cf8f055 100644 +--- a/src/modifiers/encryption.ts ++++ b/src/modifiers/encryption.ts +@@ -113,7 +113,7 @@ export class EncryptionModifier extends TwoWayModifier< + : undefined; + } catch {} + +- return [encrypted, iv.toString("base64"), authTag]; ++ return [encrypted, iv.toString("hex"), authTag]; + } + + readonly autounlock = true; From dcb2a3638b59dd6b7e4bed413c32a402d7c7a80d Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 3 Nov 2026 16:11:12 +0100 Subject: [PATCH 21/45] fix(skills): make the declarations, localization and registration breaks bite --- .../scenarios/declarations.ts | 1 + .../seeded-breaks/declarations.patch | 24 +++++++++---------- .../seeded-breaks/localization.patch | 20 ++++++++-------- .../seeded-breaks/registration.patch | 20 ++++++++-------- 4 files changed, 33 insertions(+), 32 deletions(-) diff --git a/.agents/skills/verify-interface/scenarios/declarations.ts b/.agents/skills/verify-interface/scenarios/declarations.ts index 1ada07e..8d98e63 100644 --- a/.agents/skills/verify-interface/scenarios/declarations.ts +++ b/.agents/skills/verify-interface/scenarios/declarations.ts @@ -20,6 +20,7 @@ type _FromDatabase = Assert, Use type _ToDatabase = Assert, Api.StoredRow>>; type _Validate = Assert, Api.ValidationResult>>; type _Register = Assert Promise>>; +type _Fixture = Assert, { a: 1 } | { a: 1 }[] | undefined>>; type _PrimaryKey = Assert>; type _Table = Assert[0], Api.TableTarget | undefined>>; `; diff --git a/.agents/skills/verify-interface/seeded-breaks/declarations.patch b/.agents/skills/verify-interface/seeded-breaks/declarations.patch index b36f9ae..df1f15b 100644 --- a/.agents/skills/verify-interface/seeded-breaks/declarations.patch +++ b/.agents/skills/verify-interface/seeded-breaks/declarations.patch @@ -1,12 +1,12 @@ -diff --git a/src/index.ts b/src/index.ts -index ef4a9de..cff4396 100644 ---- a/src/index.ts -+++ b/src/index.ts -@@ -1,7 +1,6 @@ - import "reflect-metadata"; - - export * from "./common"; --export * from "./database"; - export * from "./model"; - export * from "./modifiers"; - export * from "./relation"; +diff --git a/src/common.ts b/src/common.ts +index 2cdd7a1..d097554 100644 +--- a/src/common.ts ++++ b/src/common.ts +@@ -40,7 +40,6 @@ export type PlainData = { + */ + export type DatumGeneratorOutput = + | Row +- | Row[] + | undefined; + /** + * A fixture generator of a table class, called with the class registered into diff --git a/.agents/skills/verify-interface/seeded-breaks/localization.patch b/.agents/skills/verify-interface/seeded-breaks/localization.patch index d389de0..7e772c6 100644 --- a/.agents/skills/verify-interface/seeded-breaks/localization.patch +++ b/.agents/skills/verify-interface/seeded-breaks/localization.patch @@ -1,13 +1,13 @@ diff --git a/src/modifiers/localization.ts b/src/modifiers/localization.ts -index c672436..539a822 100644 +index c672436..b4d4ec0 100644 --- a/src/modifiers/localization.ts +++ b/src/modifiers/localization.ts -@@ -83,7 +83,7 @@ export class LocalizationModifier extends ContainerModifier< - // oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- the container modifier leaves the values by locale untyped - public override unlock(locked_value: Record, key: string) { - const unlocked = super.unlock(locked_value, key); -- if (unlocked !== undefined && unlocked !== EMPTY_TEXT) { -+ if (unlocked !== undefined) { - return unlocked; - } - if (this.options.fallbackLocale) { +@@ -24,7 +24,7 @@ const EMPTY_TEXT = ""; + + function withoutEmptyTexts(values: Record): Record { + return Object.fromEntries( +- Object.entries(values).filter(([, value]) => value !== EMPTY_TEXT), ++ Object.entries(values).filter(([, value]) => value !== undefined), + ); + } + diff --git a/.agents/skills/verify-interface/seeded-breaks/registration.patch b/.agents/skills/verify-interface/seeded-breaks/registration.patch index fa9ec65..ca13947 100644 --- a/.agents/skills/verify-interface/seeded-breaks/registration.patch +++ b/.agents/skills/verify-interface/seeded-breaks/registration.patch @@ -1,13 +1,13 @@ diff --git a/src/database.ts b/src/database.ts -index 93cbfa4..b8e814a 100644 +index 93cbfa4..5201029 100644 --- a/src/database.ts +++ b/src/database.ts -@@ -94,7 +94,7 @@ async function insertFixtureData( - return; - } - const count = await schema.instance().table(tableName).count(); -- if (count > 0) { -+ if (count > 1) { - return; - } - const fixtureData = await generator(tableClass); +@@ -122,7 +122,7 @@ function withStableId( + position: number, + ): StoredRow
{ + return row._id === undefined +- ? { ...row, _id: `${tableName}:fixture:${position}` } ++ ? { ...row, _id: `${tableName}:fixture:${position + 1}` } + : row; + } + From a4bd004442d0672accb99f397026addb3e990c66 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 3 Nov 2026 17:54:38 +0100 Subject: [PATCH 22/45] fix(skills): tell a restricted localize from an unrestricted one --- .agents/skills/verify-interface/scenarios/localization.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.agents/skills/verify-interface/scenarios/localization.ts b/.agents/skills/verify-interface/scenarios/localization.ts index 9787c40..52ef2e7 100644 --- a/.agents/skills/verify-interface/scenarios/localization.ts +++ b/.agents/skills/verify-interface/scenarios/localization.ts @@ -9,6 +9,7 @@ const PRODUCT = ` name: [d.Localized({ fallbackLocale: "en" }), d.Field("string")], description: [d.Localized({ fallbackLocale: "en" }), d.Field("string")], bare: [d.Localized(), d.Field("string")], + summary: [d.Localized({ fallbackLocale: "en" }), d.Field("string")], }); d.RegisterTable("products", "loc")(Product); await d.RegisterSchema("loc"); @@ -32,8 +33,10 @@ const LOCALIZE = `${PRODUCT} const frenchEmpty = product.localize("fr").description; product.localize("fr").bare = "sans repli"; const noFallback = [product.localize("fr").bare, product.localize("de").bare ?? "undefined"]; + product.localize("en").summary = "short"; + product.localize("fr").summary = "court"; const restricted = product.localize("en").localize("fr", ["name"]); - const restrictedReads = [restricted.name, restricted.description]; + const restrictedReads = [restricted.name, restricted.summary]; return { spanish: spanishNow, germanFallsBack: germanNow, @@ -103,7 +106,7 @@ const AUTO = ` `; export const LOCALIZATION_JOURNEYS = { - localize: scenario({ title: "localize switches the locale of an instance and falls back when a translation is missing or empty", body: LOCALIZE, printed: "{\"spanish\":\"Auriculares Premium\",\"germanFallsBack\":\"Premium Headphones\",\"sameObject\":[true,true,true],\"readNow\":\"Premium Headphones\",\"frenchEmpty\":\"Loud\",\"noFallback\":[\"sans repli\",\"undefined\"],\"restrictedReads\":[\"Casque Premium\",\"Loud\"],\"fallbackLocaleOf\":[\"en\",\"undefined\",\"undefined\",\"undefined\"]}" }), + localize: scenario({ title: "localize switches the locale of an instance and falls back when a translation is missing or empty", body: LOCALIZE, printed: "{\"spanish\":\"Auriculares Premium\",\"germanFallsBack\":\"Premium Headphones\",\"sameObject\":[true,true,true],\"readNow\":\"Premium Headphones\",\"frenchEmpty\":\"Loud\",\"noFallback\":[\"sans repli\",\"undefined\"],\"restrictedReads\":[\"Casque Premium\",\"short\"],\"fallbackLocaleOf\":[\"en\",\"undefined\",\"undefined\",\"undefined\"]}" }), "localize-wildcard": scenario({ title: "the wildcard key reads and writes every translation at once", body: WILDCARD, printed: "{\"all\":{\"en\":\"Premium Headphones\",\"fr\":\"Casque Premium\"},\"replaced\":{\"en\":\"Premium Headphones\",\"fr\":\"Casque Premium\",\"de\":\"Premium-Kopfhoerer\"},\"german\":\"Premium-Kopfhoerer\",\"stored\":[\"Bonjour\",\"Hello\",{\"en\":\"Hello\",\"fr\":\"Bonjour\"}]}" }), "localized-storage": scenario({ title: "a localized field is stored as one value per locale", From 5ce47802aaa054359600b345f1bcc8e695453a2b Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 3 Nov 2026 18:07:38 +0100 Subject: [PATCH 23/45] fix(skills): flag the publish door through publishConfig --- .../seeded-doors/publish.patch | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.agents/skills/verify-interface/seeded-doors/publish.patch b/.agents/skills/verify-interface/seeded-doors/publish.patch index 35501f5..93a7031 100644 --- a/.agents/skills/verify-interface/seeded-doors/publish.patch +++ b/.agents/skills/verify-interface/seeded-doors/publish.patch @@ -1,13 +1,13 @@ -diff --git a/.release-it.json b/.release-it.json -index a7604b6..90251b6 100644 ---- a/.release-it.json -+++ b/.release-it.json -@@ -18,7 +18,7 @@ - } +diff --git a/package.json b/package.json +index 8b1d78f..50284a7 100644 +--- a/package.json ++++ b/package.json +@@ -115,7 +115,7 @@ + "./package.json": "./package.json" }, - "npm": { -- "publish": true, -+ "publish": false, - "skipChecks": true - } - } + "publishConfig": { +- "access": "public", ++ "access": "restricted", + "provenance": true + }, + "scripts": { From b5ffaad5b50f9d30d567380d0b059bd5cb1044de Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 3 Nov 2026 19:42:00 +0100 Subject: [PATCH 24/45] feat(skills): cover the hash bounds, object updates, mixed fixtures and the update finding --- .../verify-interface/features/autodate.md | 2 + .../verify-interface/features/hashing.md | 4 ++ .../verify-interface/features/models.md | 4 ++ .../features/parameter-decoration.md | 2 + .../verify-interface/features/registration.md | 4 ++ .../verify-interface/features/tables.md | 2 + .../verify-interface/scenarios/custom.ts | 18 ++--- .../verify-interface/scenarios/decoration.ts | 12 ++++ .../verify-interface/scenarios/hashing.ts | 55 +++++++++++++++- .../scenarios/localization.ts | 20 ++++++ .../verify-interface/scenarios/models.ts | 66 ++++++++++++++++++- .../scenarios/registration.ts | 49 +++++++++++++- .../verify-interface/scenarios/tables.ts | 35 +++++++++- .../skills/verify-interface/verify.config.ts | 2 +- 14 files changed, 257 insertions(+), 18 deletions(-) diff --git a/.agents/skills/verify-interface/features/autodate.md b/.agents/skills/verify-interface/features/autodate.md index fd5ef8a..5fdac08 100644 --- a/.agents/skills/verify-interface/features/autodate.md +++ b/.agents/skills/verify-interface/features/autodate.md @@ -5,6 +5,7 @@ docs: - docs/3.table-modifiers.md journeys: - autodate + - autodate-object-update --- # Auto dates @@ -13,6 +14,7 @@ journeys: ## Sub-features +- `autodate-object-update`: the update form that carries the primary key. - `autodate`: insert, update and the stored dates. ## How to get to it (user POV) diff --git a/.agents/skills/verify-interface/features/hashing.md b/.agents/skills/verify-interface/features/hashing.md index 73a2062..5abea15 100644 --- a/.agents/skills/verify-interface/features/hashing.md +++ b/.agents/skills/verify-interface/features/hashing.md @@ -8,6 +8,8 @@ journeys: - hash-verify - hash-limits - hash-upgrade + - hash-bounds + - hash-outdated - hash-digest --- @@ -19,6 +21,8 @@ journeys: - `hash-storage`: the scrypt format and parameters, checked with an independent scrypt. - `hash-verify`: `testHash`, `testHashAsync`, `isHashOutdated`. +- `hash-bounds`: both edges of the `ln`, `r`, `p`, salt and key ranges. +- `hash-outdated`: `isHashOutdated` for a changed `p`, `r` and `ln`. - `hash-limits`: the limits a stored scrypt string must stay inside. - `hash-upgrade`: older formats verify, are flagged and are rewritten. - `hash-digest`: `algorithm` digests with the row's shared salt. diff --git a/.agents/skills/verify-interface/features/models.md b/.agents/skills/verify-interface/features/models.md index 8275ae4..3d7a2e5 100644 --- a/.agents/skills/verify-interface/features/models.md +++ b/.agents/skills/verify-interface/features/models.md @@ -9,6 +9,8 @@ journeys: - conversion - validation - get-model + - compound-get-by + - partial-update-keeps-modified-fields - model-factory - extend-model --- @@ -23,6 +25,8 @@ journeys: - `conversion`: the three static conversions save nothing; reads convert. - `validation`: codecs, opt-in `validate`, partial patches. - `get-model`: one cached model per class and instance id; instances stay apart. +- `compound-get-by`: a three-field compound index. +- `partial-update-keeps-modified-fields`: known finding #80, fails until fixed. - `model-factory`: the table name comes from the argument or `RegisterTable`. - `extend-model`: a model subclass adds query methods. diff --git a/.agents/skills/verify-interface/features/parameter-decoration.md b/.agents/skills/verify-interface/features/parameter-decoration.md index 4f6cb30..9e5719b 100644 --- a/.agents/skills/verify-interface/features/parameter-decoration.md +++ b/.agents/skills/verify-interface/features/parameter-decoration.md @@ -6,6 +6,7 @@ docs: journeys: - fixed-instance - per-request-instance + - custom-parameter-provider - property-decoration - crud-controller --- @@ -18,6 +19,7 @@ journeys: - `fixed-instance`: an instance id. - `per-request-instance`: a callback picks the instance, or `undefined` for the default. +- `custom-parameter-provider`: the docs' custom decorator example. - `property-decoration`: a property is set while a request is handled. - `crud-controller`: `@Model` beside `@Parameter` and `@RawBody` in a CRUD controller. diff --git a/.agents/skills/verify-interface/features/registration.md b/.agents/skills/verify-interface/features/registration.md index 7fd6ff0..e94a55a 100644 --- a/.agents/skills/verify-interface/features/registration.md +++ b/.agents/skills/verify-interface/features/registration.md @@ -12,6 +12,8 @@ journeys: - fixtures - fixture-race - table-access + - fixture-ids-mixed + - table-foreign-instance --- # Registration @@ -23,6 +25,8 @@ journeys: - `register-schema`: provisioning, binding and the unknown-schema error. - `fixtures`: fixtures fill an empty table once, with generated ids and the insert conversion. - `fixture-race`: two starts write each row once; a subclass gets its parent's fixture. +- `fixture-ids-mixed`: ids kept and positions for the rest. +- `table-foreign-instance`: a held instance of another schema is used as is. - `table-access`: `Table.table` for the default, a named, the cross-instance and a held instance, and its two errors. ## How to get to it (user POV) diff --git a/.agents/skills/verify-interface/features/tables.md b/.agents/skills/verify-interface/features/tables.md index 7fd3ca0..9df2de7 100644 --- a/.agents/skills/verify-interface/features/tables.md +++ b/.agents/skills/verify-interface/features/tables.md @@ -12,6 +12,7 @@ journeys: - relation-metadata - inheritance - table-with + - combine-modifiers --- # Tables @@ -25,6 +26,7 @@ A table class with `Field`, `Index`, `Relation`, subclasses and `Table.with`. - `field-types`: every field type goes through unchanged, in either decorator order. - `relation-metadata`: `Relation` records links and enforces none. - `inheritance`: a subclass copies its parent and never writes into it. +- `combine-modifiers`: three modifiers on one table. - `table-with`: mixin methods reach the subclass only. ## How to get to it (user POV) diff --git a/.agents/skills/verify-interface/scenarios/custom.ts b/.agents/skills/verify-interface/scenarios/custom.ts index b2f357c..df10ad8 100644 --- a/.agents/skills/verify-interface/scenarios/custom.ts +++ b/.agents/skills/verify-interface/scenarios/custom.ts @@ -20,7 +20,9 @@ const ONE_WAY = `${PRELUDE} const model = d.GetModel(d.BasicDataModel(Doc)); const [id] = await model.insert({ title: "t", digest: "value", strong: "value" }); const raw = await rawTable("oneway", "docs").get(id); + const loaded = await model.get(id); return { + tested: [d.testValue(loaded, "digest", "value"), d.testValue(loaded, "digest", "other"), d.testValue(loaded, "strong", "value")], stored: [raw._internal.data.digest === crypto.createHash("sha256").update(JSON.stringify("value")).digest("hex"), raw._internal.data.strong === crypto.createHash("sha512").update(JSON.stringify("value")).digest("hex")], secondOneWay: caught(() => { class Twice extends d.Table {} decorate(Twice, { f: [PrefixHashed(), PrefixHashed()] }); }), }; @@ -71,11 +73,11 @@ const EVENTS = `${PRELUDE} const log = []; class Spy extends d.Modifier { fromPlainData(object, field) { log.push(["fromPlainData", field, this instanceof Spy, object.constructor.name]); } - toPlainData(object, field) { log.push(["toPlainData", field]); } - fromDatabase(object, field) { log.push(["fromDatabase", field]); } - toDatabase(object, field) { log.push(["toDatabase", field]); } - insert(object, field) { log.push(["insert", field]); } - update(object, field) { log.push(["update", field]); } + toPlainData(object, field) { log.push(["toPlainData", field, this instanceof Spy, object.constructor.name]); } + fromDatabase(object, field) { log.push(["fromDatabase", field, this instanceof Spy, object.constructor.name]); } + toDatabase(object, field) { log.push(["toDatabase", field, this instanceof Spy, object.constructor.name]); } + insert(object, field) { log.push(["insert", field, this instanceof Spy, object.constructor.name]); } + update(object, field) { log.push(["update", field, this instanceof Spy, object.constructor.name]); } [d.MixinSymbol] = class {}; } const Spied = MakePropertyDecorator((target, propertyKey, options) => { d.attachModifier(target.constructor, Spy, propertyKey, options || {}); }); @@ -84,7 +86,7 @@ const EVENTS = `${PRELUDE} d.RegisterTable("docs", "events")(Doc); await d.RegisterSchema("events"); const model = d.GetModel(d.BasicDataModel(Doc)); - const phase = (_name, entries) => entries.map((entry) => entry[0]); + const phase = (_name, entries) => entries.map((entry) => entry.join("/")); const mark = () => log.splice(0); const [id] = await model.insert({ note: "n" }); const onInsert = phase("insert", mark()); @@ -103,7 +105,7 @@ const EVENTS = `${PRELUDE} `; export const CUSTOM_JOURNEYS = { - "one-way-modifier": scenario({ title: "a custom OneWayModifier transforms a value when it is written", body: ONE_WAY, printed: "{\"stored\":[true,true],\"secondOneWay\":\"Attempted adding Modifier PrefixHashModifier on Twice.f but it already has a One-Way Modifier\"}" }), + "one-way-modifier": scenario({ title: "a custom OneWayModifier transforms a value when it is written", body: ONE_WAY, printed: "{\"tested\":[true,false,true],\"stored\":[true,true],\"secondOneWay\":\"Attempted adding Modifier PrefixHashModifier on Twice.f but it already has a One-Way Modifier\"}" }), "two-way-modifier": scenario({ title: "a custom TwoWayModifier stores a transformed value and unlocks it on read", body: TWO_WAY, @@ -111,5 +113,5 @@ export const CUSTOM_JOURNEYS = { stored: [{ name: "the stored encodings", expression: "$DB.getCollection('twoway__docs').find({ title: 't' }).toArray().map((r) => [r._internal.data.body, r._internal.data.tagged])", expected: "[[\"b64-eyJhIjpbMSwyXX0=\",\"x:InBsYWluIg==\"]]" }], }), "container-modifier": scenario({ title: "a ContainerModifier keeps values by key and treats the wildcard key as the whole record", body: CONTAINER, printed: "{\"both\":{\"a\":1,\"b\":2},\"afterOverwrite\":{\"a\":3,\"b\":2},\"replaced\":{\"z\":26},\"byKey\":[26,\"undefined\",\"undefined\"]}" }), - "modifier-events": scenario({ title: "a modifier's event methods run for inserts, updates and conversions, and an events-only modifier leaves the field plain", body: EVENTS, printed: "{\"onInsert\":[\"fromPlainData\",\"insert\",\"toDatabase\"],\"onUpdate\":[\"fromPlainData\",\"update\",\"toDatabase\"],\"onRead\":[\"fromDatabase\"],\"onReadAll\":[\"fromDatabase\"],\"onPlain\":[\"toPlainData\"],\"eventsOnlyKeepPlainValue\":true}" }), + "modifier-events": scenario({ title: "a modifier's event methods run for inserts, updates and conversions, and an events-only modifier leaves the field plain", body: EVENTS, printed: "{\"onInsert\":[\"fromPlainData/note/true/Doc\",\"insert/note/true/Doc\",\"toDatabase/note/true/Doc\"],\"onUpdate\":[\"fromPlainData/note/true/Doc\",\"update/note/true/Doc\",\"toDatabase/note/true/Doc\"],\"onRead\":[\"fromDatabase/note/true/Doc\"],\"onReadAll\":[\"fromDatabase/note/true/Doc\"],\"onPlain\":[\"toPlainData/note/true/Doc\"],\"eventsOnlyKeepPlainValue\":true}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/decoration.ts b/.agents/skills/verify-interface/scenarios/decoration.ts index 143a18e..6269fdc 100644 --- a/.agents/skills/verify-interface/scenarios/decoration.ts +++ b/.agents/skills/verify-interface/scenarios/decoration.ts @@ -67,6 +67,17 @@ const CRUD_CONTROLLER = `${HTTP} return { created: [created.status, typeof id], updated, read, removed, gone }; `; +const CUSTOM_PROVIDER = `${HTTP} + const { MakeParameterAndPropertyDecorator } = require(require.resolve("@antelopejs/interface-core/decorators", { paths: [dist] })); + const CustomModel = MakeParameterAndPropertyDecorator((target, key, index, modelClass, resolver) => { + api.SetParameterProvider(target, key, index, (ctx) => d.GetModel(modelClass, resolver(ctx))); + }); + class Custom extends api.Controller("/custom") {} + route(Custom, "list", "Get", "/:tenant", async (model) => (await model.getAll()).map((row) => row.title), [CustomModel(NoteModel, (ctx) => "c-" + ctx.routeParameters.tenant)]); + await d.GetModel(NoteModel, "c-x").insert({ title: "custom x" }); + return { x: await call("/custom/x"), y: await call("/custom/y") }; +`; + export const DECORATION_JOURNEYS = { "fixed-instance": scenario({ title: "@Model with an instance id injects the cached model of that instance into a controller method", @@ -82,6 +93,7 @@ export const DECORATION_JOURNEYS = { api: true, stored: [{ name: "the notes by instance", expression: "$DB.getCollection('http__notes').find().sort({ title: 1 }).toArray().map((r) => [r.title, r._instance])", expected: "[[\"alpha\",\"tenant-a\"],[\"beta\",\"tenant-b\"],[\"default instance\",null]]" }], }), + "custom-parameter-provider": scenario({ title: "a custom decorator can register its own parameter provider that resolves a model with GetModel", body: CUSTOM_PROVIDER, printed: "{\"x\":{\"status\":200,\"body\":\"[\\\"custom x\\\"]\"},\"y\":{\"status\":200,\"body\":\"[]\"}}", api: true }), "property-decoration": scenario({ title: "@Model on a property gives the controller its model while a request is handled", body: PROPERTY, diff --git a/.agents/skills/verify-interface/scenarios/hashing.ts b/.agents/skills/verify-interface/scenarios/hashing.ts index b9f9e20..f06ac2b 100644 --- a/.agents/skills/verify-interface/scenarios/hashing.ts +++ b/.agents/skills/verify-interface/scenarios/hashing.ts @@ -4,9 +4,10 @@ import { scenario } from "../runner.ts"; /** Independent scrypt, digest and parsing helpers, so the scenario checks the stored hash without the library. */ const HASH_HELPERS = ` + const CURRENT = "$" + "scrypt" + "$ln=15,r=8,p=3$"; const b64 = (buffer) => buffer.toString("base64").replace(/=+$/, ""); const scryptOf = (value, salt, keylen, ln, r, p) => crypto.scryptSync(JSON.stringify(value), salt, keylen, { N: 2 ** ln, r, p, maxmem: 1024 * 1024 * 1024 }); - const scryptString = (value, ln, r, p, saltBytes = 16, keyBytes = 32) => { const salt = Buffer.alloc(saltBytes, 7); return "$scrypt$ln=" + ln + ",r=" + r + ",p=" + p + "$" + b64(salt) + "$" + b64(scryptOf(value, salt, keyBytes, ln, r, p)); }; + const scryptString = (value, ln, r, p, saltBytes = 16, keyBytes = 32) => { const salt = Buffer.alloc(saltBytes, 7); return "$" + "scrypt" + "$ln=" + ln + ",r=" + r + ",p=" + p + "$" + b64(salt) + "$" + b64(scryptOf(value, salt, keyBytes, ln, r, p)); }; const digest = (algorithm, salt, value) => crypto.createHash(algorithm).update(salt).update(JSON.stringify(value)).digest("base64"); class Account extends d.Table.with(d.HashModifier) {} decorate(Account, { name: [d.Field("string")], password: [d.Hashed(), d.Field("string")], pin: [d.Hashed({ algorithm: "sha256" }), d.Field("string")], token: [d.Hashed({ algorithm: "sha256" }), d.Field("string")] }); @@ -93,14 +94,14 @@ const UPGRADE = `${HASH_HELPERS} const wrongLegacy = await legacy.testHashAsync("password", "wrong"); if (await legacy.testHashAsync("password", "hunter2")) { if (legacy.isHashOutdated("password")) { legacy.password = "hunter2"; await accounts.update(legacy); } } const rewritten = await rawRow("legacy"); - const afterRewrite = [rewritten._internal.data.password.startsWith("$scrypt$ln=15,r=8,p=3$"), (await accounts.get("legacy")).isHashOutdated("password"), await (await accounts.get("legacy")).testHashAsync("password", "hunter2")]; + const afterRewrite = [rewritten._internal.data.password.startsWith(CURRENT), (await accounts.get("legacy")).isHashOutdated("password"), await (await accounts.get("legacy")).testHashAsync("password", "hunter2")]; const lowBefore = (await rawRow("lowcost"))._internal.data.password; await accounts.update("lowcost", { name: "renamed" }); const keptByNameUpdate = (await rawRow("lowcost"))._internal.data.password === lowBefore; await accounts.update({ _id: "lowcost", name: "renamed again" }); const keptByObjectUpdate = (await rawRow("lowcost"))._internal.data.password === lowBefore; await accounts.update("lowcost", { password: "hunter2" }); - const viaUpdate = (await rawRow("lowcost"))._internal.data.password.startsWith("$scrypt$ln=15,r=8,p=3$"); + const viaUpdate = (await rawRow("lowcost"))._internal.data.password.startsWith(CURRENT); return { before, wrongLegacy, afterRewrite, keptByNameUpdate, keptByObjectUpdate, viaUpdate }; `; @@ -129,6 +130,52 @@ const DIGEST = `${HASH_HELPERS} }; `; + +const BOUNDS = `${HASH_HELPERS} + const seed = async (id, password) => rawTable("hash", "accounts").insert({ _id: id, name: id, _internal: { meta: {}, data: { password } } }).run(); + const attempt = async (id, stored) => { + await seed(id, stored); + const row = await accounts.get(id); + try { return [await row.testHashAsync("password", "pw"), await row.testHashAsync("password", "other")]; } catch (error) { return "threw " + error.name; } + }; + const fake = (ln, r, p) => "$" + "scrypt" + "$ln=" + ln + ",r=" + r + ",p=" + p + "$" + b64(Buffer.alloc(16, 1)) + "$" + b64(Buffer.alloc(32, 2)); + return { + r1: await attempt("r1", scryptString("pw", 4, 1, 1)), + r10: await attempt("r10", scryptString("pw", 4, 10, 1)), + r99: await attempt("r99", scryptString("pw", 4, 99, 1)), + r100: await attempt("r100", fake(4, 100, 1)), + ln1: await attempt("ln1", scryptString("pw", 1, 8, 1)), + ln2: await attempt("ln2", scryptString("pw", 2, 8, 1)), + ln99: await attempt("ln99", fake(99, 8, 1)), + ln100: await attempt("ln100", fake(100, 8, 1)), + p1: await attempt("p1", scryptString("pw", 4, 8, 1)), + p16: await attempt("p16", scryptString("pw", 4, 8, 16)), + salt16: await attempt("salt16", scryptString("pw", 4, 8, 1, 16, 32)), + salt64: await attempt("salt64", scryptString("pw", 4, 8, 1, 64, 32)), + salt80: await attempt("salt80", scryptString("pw", 4, 8, 1, 80, 32)), + salt15: await attempt("salt15", scryptString("pw", 4, 8, 1, 15, 32)), + key16: await attempt("key16", scryptString("pw", 4, 8, 1, 16, 16)), + key64: await attempt("key64", scryptString("pw", 4, 8, 1, 16, 64)), + key15: await attempt("key15", scryptString("pw", 4, 8, 1, 16, 15)), + key65: await attempt("key65", scryptString("pw", 4, 8, 1, 16, 65)), + }; +`; + +const OUTDATED = `${HASH_HELPERS} + const seed = async (id, password) => rawTable("hash", "accounts").insert({ _id: id, name: id, _internal: { meta: {}, data: { password } } }).run(); + const probe = async (id, stored) => { await seed(id, stored); const row = await accounts.get(id); return [await row.testHashAsync("password", "pw"), row.isHashOutdated("password")]; }; + const [fresh] = await accounts.insert({ name: "fresh", password: "pw" }); + const freshRow = await accounts.get(fresh); + return { + fresh: [await freshRow.testHashAsync("password", "pw"), freshRow.isHashOutdated("password")], + sameCost: await probe("same", scryptString("pw", 15, 8, 3)), + lowerP: await probe("p1", scryptString("pw", 15, 8, 1)), + higherR: await probe("r9", scryptString("pw", 15, 9, 3)), + lowerLn: await probe("ln14", scryptString("pw", 14, 8, 3)), + notScrypt: await probe("junk", "plain text"), + }; +`; + export const HASH_JOURNEYS = { "hash-storage": scenario({ title: "a hashed field stores a salted scrypt hash of the JSON-encoded value", @@ -143,5 +190,7 @@ export const HASH_JOURNEYS = { body: UPGRADE, printed: "{\"before\":[true,true,true,true],\"wrongLegacy\":false,\"afterRewrite\":[true,false,true],\"keptByNameUpdate\":true,\"keptByObjectUpdate\":true,\"viaUpdate\":true}", }), + "hash-bounds": scenario({ title: "a stored scrypt string verifies at each edge of the documented ranges and is refused just outside", body: BOUNDS, printed: "{\"r1\":[true,false],\"r10\":[true,false],\"r99\":[true,false],\"r100\":[false,false],\"ln1\":[true,false],\"ln2\":[true,false],\"ln99\":[false,false],\"ln100\":[false,false],\"p1\":[true,false],\"p16\":[true,false],\"salt16\":[true,false],\"salt64\":[true,false],\"salt80\":[false,false],\"salt15\":[false,false],\"key16\":[true,false],\"key64\":[true,false],\"key15\":[false,false],\"key65\":[false,false]}" }), + "hash-outdated": scenario({ title: "isHashOutdated flags any scrypt string whose ln, r or p differs from the current cost", body: OUTDATED, printed: "{\"fresh\":[true,false],\"sameCost\":[true,false],\"lowerP\":[true,true],\"higherR\":[true,true],\"lowerLn\":[true,true],\"notScrypt\":[false,true]}" }), "hash-digest": scenario({ title: "a field with an algorithm stores a salted digest with the row's shared salt", body: DIGEST, printed: "{\"storedDigest\":true,\"sharedSalt\":true,\"saltIsText\":true,\"freshSaltPerRow\":true,\"saltReusedOnLaterWrite\":[true,true],\"testDigest\":[true,false,true],\"digestNeverOutdated\":false,\"scryptOnDigestField\":[true,false],\"otherDigestRefused\":false}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/localization.ts b/.agents/skills/verify-interface/scenarios/localization.ts index 52ef2e7..51e60f4 100644 --- a/.agents/skills/verify-interface/scenarios/localization.ts +++ b/.agents/skills/verify-interface/scenarios/localization.ts @@ -105,6 +105,25 @@ const AUTO = ` }; `; +const AUTO_OBJECT = ` + class Article extends d.Table {} + decorate(Article, { title: [d.Field("string")], creationDate: [d.CreationTime(), d.Field("date")], updateDate: [d.UpdateTime(), d.Field("date")] }); + d.RegisterTable("articles", "autoobj")(Article); + await d.RegisterSchema("autoobj"); + const model = d.GetModel(d.BasicDataModel(Article)); + const [id] = await model.insert({ title: "One" }); + const inserted = await rawTable("autoobj", "articles").get(id); + await sleep(60); + await model.update({ _id: id, title: "Two", creationDate: new Date(0) }); + const updated = await rawTable("autoobj", "articles").get(id); + const stamp = (value) => new Date(value).getTime(); + return { + title: updated.title, + updateRefreshed: stamp(updated.updateDate) >= stamp(inserted.updateDate) + 50, + creationKept: stamp(updated.creationDate) === stamp(inserted.creationDate), + }; +`; + export const LOCALIZATION_JOURNEYS = { localize: scenario({ title: "localize switches the locale of an instance and falls back when a translation is missing or empty", body: LOCALIZE, printed: "{\"spanish\":\"Auriculares Premium\",\"germanFallsBack\":\"Premium Headphones\",\"sameObject\":[true,true,true],\"readNow\":\"Premium Headphones\",\"frenchEmpty\":\"Loud\",\"noFallback\":[\"sans repli\",\"undefined\"],\"restrictedReads\":[\"Casque Premium\",\"short\"],\"fallbackLocaleOf\":[\"en\",\"undefined\",\"undefined\",\"undefined\"]}" }), "localize-wildcard": scenario({ title: "the wildcard key reads and writes every translation at once", body: WILDCARD, printed: "{\"all\":{\"en\":\"Premium Headphones\",\"fr\":\"Casque Premium\"},\"replaced\":{\"en\":\"Premium Headphones\",\"fr\":\"Casque Premium\",\"de\":\"Premium-Kopfhoerer\"},\"german\":\"Premium-Kopfhoerer\",\"stored\":[\"Bonjour\",\"Hello\",{\"en\":\"Hello\",\"fr\":\"Bonjour\"}]}" }), @@ -117,6 +136,7 @@ export const LOCALIZATION_JOURNEYS = { } satisfies { [id: string]: Journey }; export const AUTODATE_JOURNEYS = { + "autodate-object-update": scenario({ title: "update with the primary key inside the object refreshes UpdateTime and keeps CreationTime", body: AUTO_OBJECT, printed: "{\"title\":\"Two\",\"updateRefreshed\":true,\"creationKept\":true}" }), autodate: scenario({ title: "CreationTime is set on insert and kept on update, UpdateTime is set on insert and refreshed on update", body: AUTO, diff --git a/.agents/skills/verify-interface/scenarios/models.ts b/.agents/skills/verify-interface/scenarios/models.ts index bdd6e3d..d9c8214 100644 --- a/.agents/skills/verify-interface/scenarios/models.ts +++ b/.agents/skills/verify-interface/scenarios/models.ts @@ -94,7 +94,12 @@ const VALIDATION = ` const patchRefused = await errorOf(people.update(accepted[0], { age: "old" }, { validate: true })); const objectPatchRefused = await errorOf(people.update({ _id: accepted[0], name: 7 }, { validate: true })); const unvalidatedPatch = await people.update(accepted[0], { age: "old" }); - return { valid, invalid, notAnObject, partialOk, partialEmpty, partialBad, tokenSkipped, optIn: optIn.length, refused, batchRefused, patched, patchRefused, objectPatchRefused, unvalidatedPatch }; + const hole = { _tag: "codec", name: "hole", is: () => false, encode: (a) => a, decode: () => ({ _tag: "Left", left: [{}, { message: "after a hole" }, { message: "last" }] }) }; + const silent = { _tag: "codec", name: "silent", is: () => false, encode: (a) => a, decode: () => ({ _tag: "Left", left: [{}, { message: "" }] }) }; + class Odd extends d.Table {} + decorate(Odd, { a: [d.Field(hole)], b: [d.Field(silent)] }); + const oddMessages = d.BasicDataModel(Odd, "odd").validate({}); + return { valid, invalid, notAnObject, partialOk, partialEmpty, partialBad, tokenSkipped, optIn: optIn.length, refused, batchRefused, patched, patchRefused, objectPatchRefused, unvalidatedPatch, oddMessages }; `; const GET_MODEL = ` @@ -170,6 +175,57 @@ const EXTEND = ` return { isModel: model instanceof CustomUserModel, duplicate, active: active.map((user) => [user instanceof User, user.name]) }; `; +const COMPOUND = ` + class Event extends d.Table {} + decorate(Event, { + kind: [d.Field("string"), d.Index({ group: "triple" })], + day: [d.Field("string"), d.Index({ group: "triple" })], + room: [d.Field("string"), d.Index({ group: "triple" })], + title: [d.Field("string")], + }); + d.RegisterTable("events", "tri")(Event); + await d.RegisterSchema("tri"); + const model = d.GetModel(d.BasicDataModel(Event)); + await model.insert([ + { kind: "talk", day: "mon", room: "A", title: "one" }, + { kind: "talk", day: "mon", room: "B", title: "two" }, + { kind: "talk", day: "tue", room: "A", title: "three" }, + { kind: "demo", day: "mon", room: "A", title: "four" }, + ]); + await sleep(2500); + const titles = async (...keys) => (await model.getBy("triple", ...keys)).map((row) => row.title).sort(); + return { exact: await titles("talk", "mon", "A"), other: await titles("talk", "mon", "B"), none: await titles("talk", "tue", "B"), order: await titles("mon", "talk", "A") }; +`; + +const PARTIAL = ` + const secretKey = "${KEY}"; + class UserProfile extends d.Table.with(d.EncryptionModifier, d.HashModifier, d.LocalizationModifier) {} + decorate(UserProfile, { + privateInfo: [d.Encrypted({ secretKey }), d.Field("any")], + password: [d.Hashed(), d.Field("string")], + bio: [d.Localized({ fallbackLocale: "en" }), d.Field("string")], + }); + d.RegisterTable("profiles", "partial")(UserProfile); + await d.RegisterSchema("partial"); + const model = d.GetModel(d.BasicDataModel(UserProfile)); + const profile = new UserProfile(); + profile.privateInfo = { address: "1 Main St", phoneNumber: "555" }; + profile.password = "old-password"; + profile.localize("en").bio = "Hello"; + const [id] = await model.insert(profile); + const before = Object.keys((await rawTable("partial", "profiles").get(id))._internal.data).sort(); + await model.update(id, { password: "new-password" }); + const row = await model.get(id); + const after = Object.keys((await rawTable("partial", "profiles").get(id))._internal.data).sort(); + return { + before, + after, + passwordReplaced: [await row.testHashAsync("password", "new-password"), await row.testHashAsync("password", "old-password")], + privateInfo: row.privateInfo ?? "lost", + bio: row.localize("en").bio ?? "lost", + }; +`; + export const MODEL_JOURNEYS = { crud: scenario({ title: "a model inserts, reads, updates and deletes rows and answers ids and counts", @@ -186,7 +242,7 @@ export const MODEL_JOURNEYS = { validation: scenario({ title: "validate checks the io-ts codecs, and insert and update refuse invalid input only when asked", body: VALIDATION, - printed: "{\"valid\":{\"ok\":true},\"invalid\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"notAnObject\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"partialOk\":{\"ok\":true},\"partialEmpty\":{\"ok\":true},\"partialBad\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"tokenSkipped\":{\"ok\":true},\"optIn\":1,\"refused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"batchRefused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"patched\":1,\"patchRefused\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"objectPatchRefused\":{\"message\":\"validation failed: name (expected a string)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"}]},\"unvalidatedPatch\":1}", + printed: "{\"valid\":{\"ok\":true},\"invalid\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"notAnObject\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"partialOk\":{\"ok\":true},\"partialEmpty\":{\"ok\":true},\"partialBad\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"tokenSkipped\":{\"ok\":true},\"optIn\":1,\"refused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"batchRefused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"patched\":1,\"patchRefused\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"objectPatchRefused\":{\"message\":\"validation failed: name (expected a string)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"}]},\"unvalidatedPatch\":1,\"oddMessages\":{\"ok\":false,\"errors\":[{\"field\":\"a\",\"message\":\"after a hole\"},{\"field\":\"b\",\"message\":\"invalid b\"}]}}", stored: [{ name: "the people", expression: "$DB.getCollection('val__people').find().sort({ name: 1 }).toArray().map((r) => [r.name, r.age])", expected: "[[42,\"anything\"],[\"Bob\",\"old\"]]" }], }), "get-model": scenario({ @@ -195,6 +251,12 @@ export const MODEL_JOURNEYS = { printed: "{\"cache\":[true,true,true,true,true,true,true],\"defaultSees\":[\"default\"],\"tenantSees\":[\"tenant one\"],\"otherSees\":[\"tenant two\"],\"everywhereSees\":[\"default\",\"tenant one\",\"tenant two\"],\"customSchema\":[true,true],\"lost\":\"Schema not found for 'absent-schema'\",\"statics\":[\"gm\",true]}", stored: [{ name: "the rows by instance", expression: "$DB.getCollection('gm__items').find().sort({ name: 1 }).toArray().map((r) => [r.name, r._instance])", expected: "[[\"default\",null],[\"tenant one\",\"t1\"],[\"tenant two\",\"t2\"]]" }], }), + "compound-get-by": scenario({ title: "getBy matches a compound index of three fields by its keys in field order", body: COMPOUND, printed: "{\"exact\":[\"one\"],\"other\":[\"two\"],\"none\":[],\"order\":[]}" }), + "partial-update-keeps-modified-fields": scenario({ + title: "KNOWN FINDING AntelopeJS/interface-database-decorators#80, fails until fixed: update with one field keeps the row's other encrypted, hashed and localized fields", + body: PARTIAL, + printed: "{\"before\":[\"bio\",\"password\",\"privateInfo\"],\"after\":[\"bio\",\"password\",\"privateInfo\"],\"passwordReplaced\":[true,false],\"privateInfo\":{\"address\":\"1 Main St\",\"phoneNumber\":\"555\"},\"bio\":\"Hello\"}", + }), "model-factory": scenario({ title: "BasicDataModel takes its table name from the argument or from RegisterTable", body: FACTORY, printed: "{\"missingName\":\"tableName must be provided or set via @RegisterTable\",\"explicit\":\"named by argument\",\"schemaOfPlain\":\"undefined\",\"schemaOfRegistered\":\"fac\",\"overriddenTable\":1}" }), "extend-model": scenario({ title: "a subclass of a model adds query methods that use the model's table and conversions", diff --git a/.agents/skills/verify-interface/scenarios/registration.ts b/.agents/skills/verify-interface/scenarios/registration.ts index 537f9a4..2e239f8 100644 --- a/.agents/skills/verify-interface/scenarios/registration.ts +++ b/.agents/skills/verify-interface/scenarios/registration.ts @@ -70,7 +70,7 @@ const FIXTURES = ` afterSecondStart, plans: ids, unregisteredClassProvisioned: Object.keys(iface.Schema.get("fx").definition).includes("loose"), - hashedAtInsert: stamped._internal.data.secret.startsWith("$scrypt$ln=15,r=8,p=3$") && !stamped._internal.data.secret.includes("hunter2"), + hashedAtInsert: stamped._internal.data.secret.startsWith("$" + "scrypt" + "$ln=15,r=8,p=3$") && !stamped._internal.data.secret.includes("hunter2"), insertEventFired: stamped.created !== undefined, }; `; @@ -121,6 +121,36 @@ const TABLE_ACCESS = ` }; `; +const FIXTURE_MIXED = ` + class Mixed extends d.Table {} + decorate(Mixed, { name: [d.Field("string")] }); + d.Fixture(() => [{ _id: "keep", name: "a" }, { name: "b" }, { name: "c" }])(Mixed); + d.RegisterTable("mixed", "mix")(Mixed); + class Single extends d.Table {} + decorate(Single, { name: [d.Field("string")] }); + d.Fixture(() => ({ name: "only" }))(Single); + d.RegisterTable("single", "mix")(Single); + class Last extends d.Table {} + decorate(Last, { name: [d.Field("string")] }); + d.Fixture(() => [{ name: "x" }, { _id: "named", name: "y" }])(Last); + d.RegisterTable("last", "mix")(Last); + await d.RegisterSchema("mix"); + const ids = async (table) => (await rawTable("mix", table)).map((row) => row._id).sort(); + return { mixed: await ids("mixed"), last: await ids("last"), single: await ids("single") }; +`; + +const FOREIGN = ` + class Item extends d.Table {} + decorate(Item, { name: [d.Field("string")] }); + d.RegisterTable("items", "home")(Item); + await d.RegisterSchema("home"); + new iface.Schema("away", { items: { fields: { name: "string" }, indexes: {} } }); + const away = iface.Schema.get("away").instance("away-1"); + await Item.table(away).insert({ _id: "foreign", name: "stored away" }).run(); + await Item.table().insert({ _id: "local", name: "stored home" }).run(); + return { homeCount: await Item.table().count(), awayCount: await Item.table(away).count() }; +`; + export const REGISTRATION_JOURNEYS = { "register-schema": scenario({ title: "RegisterSchema provisions the registered tables, binds the classes and refuses an unknown schema", @@ -134,7 +164,7 @@ export const REGISTRATION_JOURNEYS = { printed: "{\"first\":{\"roles\":2,\"settings\":1,\"plans\":3,\"nothing\":0},\"afterSecondStart\":1,\"plans\":[\"plans:fixture:0\",\"plans:fixture:1\",\"plans:fixture:2\"],\"unregisteredClassProvisioned\":false,\"hashedAtInsert\":true,\"insertEventFired\":true}", stored: [ { name: "the plan rows", expression: "$DB.getCollection('fx__plans').find({}, { name: 0 }).sort({ _id: 1 }).toArray().map((r) => [r._id, r.label])", expected: "[[\"plans:fixture:0\",\"free\"],[\"plans:fixture:1\",\"pro\"],[\"plans:fixture:2\",\"team\"]]" }, - { name: "the stamped row", expression: "$DB.getCollection('fx__stamped').find().toArray().map((r) => [r._id, r._internal.data.secret.startsWith('$scrypt$ln=15,r=8,p=3$'), r._internal.data.secret.includes('hunter2'), r.created !== undefined])", expected: "[[\"s1\",true,false,true]]" }, + { name: "the stamped row", expression: "$DB.getCollection('fx__stamped').find().toArray().map((r) => [r._id, r._internal.data.secret.startsWith('$' + 'scrypt' + '$ln=15,r=8,p=3$'), r._internal.data.secret.includes('hunter2'), r.created !== undefined])", expected: "[[\"s1\",true,false,true]]" }, ], }), "fixture-race": scenario({ @@ -143,6 +173,21 @@ export const REGISTRATION_JOURNEYS = { printed: "{\"rows\":[\"seeds:fixture:0\",\"seeds:fixture:1\"],\"kids\":[\"kids:fixture:0\",\"kids:fixture:1\"],\"generatorReceived\":\"SpyChild\"}", stored: [{ name: "the seed rows", expression: "$DB.getCollection('race__seeds').find().sort({ _id: 1 }).toArray().map((r) => [r._id, r.name])", expected: "[[\"seeds:fixture:0\",\"a\"],[\"seeds:fixture:1\",\"b\"]]" }], }), + "fixture-ids-mixed": scenario({ + title: "a fixture mixing rows with and without an id keeps the ids it has and numbers the others by position", + body: FIXTURE_MIXED, + printed: "{\"mixed\":[\"keep\",\"mixed:fixture:1\",\"mixed:fixture:2\"],\"last\":[\"last:fixture:0\",\"named\"],\"single\":[\"single:fixture:0\"]}", + stored: [{ name: "the mixed ids", expression: "$DB.getCollection('mix__mixed').find().sort({ _id: 1 }).toArray().map((r) => r._id)", expected: "[\"keep\",\"mixed:fixture:1\",\"mixed:fixture:2\"]" }], + }), + "table-foreign-instance": scenario({ + title: "Table.table with a schema instance takes the table from that instance as is, even of another schema", + body: FOREIGN, + printed: "{\"homeCount\":1,\"awayCount\":1}", + stored: [ + { name: "the home rows", expression: "$DB.getCollection('home__items').find().toArray().map((r) => r._id)", expected: "[\"local\"]" }, + { name: "the away rows", expression: "$DB.getCollection('away__items').find().toArray().map((r) => [r._id, r._instance])", expected: "[[\"foreign\",\"away-1\"]]" }, + ], + }), "table-access": scenario({ title: "Table.table opens the bound table for the default instance, an instance id, all instances or a held instance", body: TABLE_ACCESS, diff --git a/.agents/skills/verify-interface/scenarios/tables.ts b/.agents/skills/verify-interface/scenarios/tables.ts index 7f3c285..0c3f18a 100644 --- a/.agents/skills/verify-interface/scenarios/tables.ts +++ b/.agents/skills/verify-interface/scenarios/tables.ts @@ -19,13 +19,15 @@ const INDEX_OPTIONS = ` const other = d.GetModel(Model, "tenant-2"); await model.insert({ email: "a@x", first: "Ada", last: "Lovelace", age: 36 }); const duplicate = await rejection(model.insert({ email: "a@x", first: "Bob", last: "Brown", age: 40 })); + let recognised = null; + try { await model.insert({ email: "a@x", first: "Cy", last: "Dee", age: 41 }); } catch (error) { recognised = [iface.IsDuplicateKeyError(error), iface.IsDuplicateKeyError(new Error("other"))]; } const otherInstance = await other.insert({ email: "a@x", first: "Ada", last: "Lovelace", age: 36 }); const byGroup = (await model.getBy("name", "Ada", "Lovelace")).map((row) => row.email); const bySingle = (await model.getBy("age", 36)).map((row) => row.first); const between = (await rawTable("idx", "users").between("age", 30, 38)).map((row) => row.first); const symbolKey = caught(() => d.Index()({}, Symbol("k"))); await sleep(2500); - return { definition: definition.users, duplicate, otherInstanceStored: otherInstance.length, byGroup, bySingle, between, symbolKey }; + return { definition: definition.users, duplicate, recognised, otherInstanceStored: otherInstance.length, byGroup, bySingle, between, symbolKey }; `; const GROUP_FLAGS = ` @@ -132,16 +134,45 @@ const WITH = ` }; `; +const COMBINE = ` + const secretKey = "12345678901234567890123456789012"; + class UserProfile extends d.Table.with(d.EncryptionModifier, d.HashModifier, d.LocalizationModifier) {} + decorate(UserProfile, { + privateInfo: [d.Encrypted({ secretKey }), d.Field("any")], + password: [d.Hashed(), d.Field("string")], + bio: [d.Localized({ fallbackLocale: "en" }), d.Field("string")], + }); + d.RegisterTable("profiles", "combo")(UserProfile); + await d.RegisterSchema("combo"); + const model = d.GetModel(d.BasicDataModel(UserProfile)); + const profile = new UserProfile(); + profile.privateInfo = { address: "1 Main St", phoneNumber: "555" }; + profile.password = "pw"; + profile.localize("en").bio = "Hello"; + profile.localize("fr").bio = "Bonjour"; + const [id] = await model.insert(profile); + const row = await model.get(id); + const raw = await rawTable("combo", "profiles").get(id); + return { + stored: Object.keys(raw._internal.data).sort(), + privateInfo: row.privateInfo, + password: [await row.testHashAsync("password", "pw"), await row.testHashAsync("password", "no")], + bio: [row.localize("fr").bio, row.localize("de").bio], + mixins: [typeof row.testHash, typeof row.localize], + }; +`; + export const TABLE_JOURNEYS = { "index-options": scenario({ title: "Field and Index reach the database as fields and plain, compound, cross-instance and unique indexes", body: INDEX_OPTIONS, - printed: "{\"definition\":{\"fields\":{\"email\":\"string\",\"first\":\"string\",\"last\":\"string\",\"age\":\"number\"},\"indexes\":{\"email\":{\"fields\":null,\"unique\":true},\"name\":{\"fields\":[\"first\",\"last\"],\"crossInstance\":true},\"age\":{\"fields\":null}}},\"duplicate\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"email\\\" (email)\"},\"otherInstanceStored\":1,\"byGroup\":[\"a@x\"],\"bySingle\":[\"Ada\"],\"between\":[\"Ada\"],\"symbolKey\":\"@Index on Symbol(k): a database index names a field, never a symbol\"}", + printed: "{\"definition\":{\"fields\":{\"email\":\"string\",\"first\":\"string\",\"last\":\"string\",\"age\":\"number\"},\"indexes\":{\"email\":{\"fields\":null,\"unique\":true},\"name\":{\"fields\":[\"first\",\"last\"],\"crossInstance\":true},\"age\":{\"fields\":null}}},\"duplicate\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"email\\\" (email)\"},\"recognised\":[true,false],\"otherInstanceStored\":1,\"byGroup\":[\"a@x\"],\"bySingle\":[\"Ada\"],\"between\":[\"Ada\"],\"symbolKey\":\"@Index on Symbol(k): a database index names a field, never a symbol\"}", stored: [{ name: "the indexes of the users collection", expression: "$DB.getCollection('idx__users').getIndexes().map((i) => [i.name, i.unique === true]).sort()", expected: "[[\"_id_\",false],[\"age__i\",false],[\"email__i\",true],[\"name\",false],[\"name__i\",false]]" }], }), "group-flags": scenario({ title: "unique and crossInstance on any field of a group apply to the whole group", body: GROUP_FLAGS, printed: "{\"indexes\":{\"ab\":{\"fields\":[\"a\",\"b\"],\"unique\":true},\"cd\":{\"fields\":[\"c\",\"e\"],\"crossInstance\":true}},\"sameKey\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"ab\\\" (a, b)\"},\"otherSecond\":1}" }), "field-types": scenario({ title: "Field hands every type to the database unchanged and combines with Index in either order", body: FIELD_TYPES, printed: "{\"names\":[\"id\",\"tags\",\"address\",\"total\",\"lateIndex\"],\"id\":\"string\",\"tags\":[\"string\"],\"address\":{\"street\":\"string\",\"geo\":{\"lat\":\"number\",\"lng\":\"number\"}},\"codecIsTheSameObject\":true,\"lateIndexType\":\"number\",\"indexes\":[\"total\",\"lateIndex\"]}" }), "relation-metadata": scenario({ title: "Relation records links without enforcing them", body: RELATION, printed: "{\"relationFields\":[\"authorId\",\"tagIds\"],\"authorTarget\":true,\"authorToField\":null,\"tagMany\":true,\"tagToField\":\"name\",\"dangling\":[\"nobody\",[\"ghost\"]],\"fieldsKept\":[\"authorId\",\"tagIds\",\"title\"],\"childRelations\":[\"authorId\",\"tagIds\",\"editorId\"],\"parentRelations\":[\"authorId\",\"tagIds\"]}" }), inheritance: scenario({ title: "a table subclass starts with a copy of its parent and never changes it", body: INHERITANCE, printed: "{\"base\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\"],\"indexes\":[],\"unique\":[]},\"person\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\"],\"indexes\":[\"email\"],\"unique\":[\"email\"]},\"employee\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\",\"badge\"],\"indexes\":[\"email\",\"badge\"],\"unique\":[\"email\"]},\"contractor\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"rate\"],\"indexes\":[],\"unique\":[]}}" }), + "combine-modifiers": scenario({ title: "Table.with combines the encryption, hashing and localization modifiers on one table", body: COMBINE, printed: "{\"stored\":[\"bio\",\"password\",\"privateInfo\"],\"privateInfo\":{\"address\":\"1 Main St\",\"phoneNumber\":\"555\"},\"password\":[true,false],\"bio\":[\"Bonjour\",\"Hello\"],\"mixins\":[\"function\",\"function\"]}" }), "table-with": scenario({ title: "Table.with adds the mixin methods of the modifiers to a subclass", body: WITH, printed: "{\"plainHasTestHash\":false,\"hashingHas\":[true,true,true,false],\"bothHas\":[true,true],\"isTableSubclass\":true,\"tableItselfUntouched\":\"undefined\",\"subIsASubclass\":true}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/verify.config.ts b/.agents/skills/verify-interface/verify.config.ts index 72fc9dc..ddd0f87 100644 --- a/.agents/skills/verify-interface/verify.config.ts +++ b/.agents/skills/verify-interface/verify.config.ts @@ -30,7 +30,7 @@ const config: VerifyConfig = { globs: ["src/modifiers/encryption.ts", "src/modifiers/hash.ts", "src/modifiers/localization.ts", "src/modifiers/internal/records.ts"], funnels: [], containments: ["encryption/encryption-at-rest", "hashing/hash-storage"], - why: "the encrypted [ciphertext, iv, authTag] triple, the $scrypt$ln=15,r=8,p=3$salt$key string, the digest and salt and the per-locale record are what rows persist as", + why: "the encrypted [ciphertext, iv, authTag] triple, the scrypt string with its ln, r and p cost, salt and key, the digest and salt and the per-locale record are what rows persist as", }, contract: { globs: ["src/index.ts", "package.json"], From 77d908d50962079065af1d6fd21f552834e9dffc Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 3 Nov 2026 19:59:39 +0100 Subject: [PATCH 25/45] fix(skills): run the object-update journey for the models feature too --- .agents/skills/verify-interface/features/README.md | 6 +++++- .agents/skills/verify-interface/features/models.md | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.agents/skills/verify-interface/features/README.md b/.agents/skills/verify-interface/features/README.md index cfb164f..8f2e870 100644 --- a/.agents/skills/verify-interface/features/README.md +++ b/.agents/skills/verify-interface/features/README.md @@ -31,7 +31,11 @@ Every journey runs a scenario inside `ajs module test` with `@antelopejs/mongodb | `Fixture` generator for a class that does not extend the decorated one throws "does not extend" | Not reachable through the public API; the generator is wrapped by the decorator. | | Queries that read a modified field through `model.table` (`unlockrequest`, locale fallback in AQL) | The docs only say `model.table` returns AQL results; they describe no query behaviour of the modifiers. | | `@Model` on a method of a controller outside a request | Docs state the property is unset outside a request (driven in `property-decoration`); no other claim. | -| Updating one of several modified fields keeps the others | Documented only for a hashed field left out of the patch (driven in `hash-upgrade`). Updating one modified field beside another erases the other: bug, see the issue draft `interface-database-decorators-partial-update-drops-modified-fields`; not pinned. | +| Updating one modified field beside others keeps them (docs/3 salt reuse, docs/4 `_internal`) | Driven by `models/partial-update-keeps-modified-fields`, which FAILS on purpose: known finding AntelopeJS/interface-database-decorators#80. The verdict stays FAIL for `models` until #80 is fixed. | | `toDatabase` "has no prototype" (docs/4) | The result is a plain object with `Object.prototype`; the journey checks it is not an instance of the table class. The wording is loose, noted in the hand-back. | | Database-driver behaviours: a driver that cannot enforce a unique secondary index refuses the schema (docs/2) | Belongs to the database module (rethinkdb); this repository only passes `unique` to the provider (driven through MongoDB in `index-options`). | | `CROSS_INSTANCE` indexes and `between()` on a real driver beyond the schema definition | The cross-instance index is checked in the definition and in MongoDB's index list; query speed is not observable. | +| Without `crossInstance`, the `@antelopejs/mongodb` and `@antelopejs/rethinkdb` drivers log a warning once per index (docs/2) | A log line of the database module, not of this package; the index definition that triggers it is driven. | +| Call `RegisterSchema` in the declaring module so the binding follows that module's database (docs/2) | Needs two modules on two databases; one container serves one database here. | +| `unlockrequest` shows database expressions the encoded value of a custom two-way modifier (docs/3) | Needs an AQL expression over a modified field; the docs describe no result a journey could compare beyond the read-back it drives. | +| The first codec message wording (docs/4 Validation) | Driven in `validation`: the first entry that has a message is used and `invalid ` otherwise. An entry without a message is read as reporting none, which matches the sentence. | diff --git a/.agents/skills/verify-interface/features/models.md b/.agents/skills/verify-interface/features/models.md index 3d7a2e5..4d83ed5 100644 --- a/.agents/skills/verify-interface/features/models.md +++ b/.agents/skills/verify-interface/features/models.md @@ -10,6 +10,7 @@ journeys: - validation - get-model - compound-get-by + - autodate-object-update - partial-update-keeps-modified-fields - model-factory - extend-model @@ -25,6 +26,7 @@ journeys: - `conversion`: the three static conversions save nothing; reads convert. - `validation`: codecs, opt-in `validate`, partial patches. - `get-model`: one cached model per class and instance id; instances stay apart. +- `autodate-object-update`: the update form that carries the primary key, as in [autodate](autodate.md). - `compound-get-by`: a three-field compound index. - `partial-update-keeps-modified-fields`: known finding #80, fails until fixed. - `model-factory`: the table name comes from the argument or `RegisterTable`. From 149f62011dbdba3355211c5aaacf52a1f0f27c60 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 3 Nov 2026 21:14:17 +0100 Subject: [PATCH 26/45] feat(skills): pin the formulas, defaults and every-field quantifiers of the docs --- .../verify-interface/features/hashing.md | 2 ++ .../verify-interface/scenarios/encryption.ts | 9 ++++-- .../verify-interface/scenarios/hashing.ts | 28 +++++++++++++++++-- .../scenarios/localization.ts | 5 ++-- .../verify-interface/scenarios/models.ts | 6 ++-- .../scenarios/registration.ts | 11 ++++---- .../verify-interface/scenarios/tables.ts | 23 ++++++++++++--- 7 files changed, 66 insertions(+), 18 deletions(-) diff --git a/.agents/skills/verify-interface/features/hashing.md b/.agents/skills/verify-interface/features/hashing.md index 5abea15..95ca98f 100644 --- a/.agents/skills/verify-interface/features/hashing.md +++ b/.agents/skills/verify-interface/features/hashing.md @@ -9,6 +9,7 @@ journeys: - hash-limits - hash-upgrade - hash-bounds + - hash-memory - hash-outdated - hash-digest --- @@ -22,6 +23,7 @@ journeys: - `hash-storage`: the scrypt format and parameters, checked with an independent scrypt. - `hash-verify`: `testHash`, `testHashAsync`, `isHashOutdated`. - `hash-bounds`: both edges of the `ln`, `r`, `p`, salt and key ranges. +- `hash-memory`: the 128 MiB limit at, under and over the line. - `hash-outdated`: `isHashOutdated` for a changed `p`, `r` and `ln`. - `hash-limits`: the limits a stored scrypt string must stay inside. - `hash-upgrade`: older formats verify, are flagged and are rewritten. diff --git a/.agents/skills/verify-interface/scenarios/encryption.ts b/.agents/skills/verify-interface/scenarios/encryption.ts index e7ec28c..04513a8 100644 --- a/.agents/skills/verify-interface/scenarios/encryption.ts +++ b/.agents/skills/verify-interface/scenarios/encryption.ts @@ -38,7 +38,7 @@ const AT_REST = `${DECRYPT} topLevelHoldsNoCiphertext: raw.card === undefined && raw.personal === undefined, gcmShape: shape(data.card), cbcShape: [data.legacy.length, data.legacy[2] ?? null], - ivBytes: [Buffer.from(data.card[1], "base64").length, Buffer.from(data.short[1], "base64").length], + ivBytes: [Buffer.from(data.card[1], "base64").length, Buffer.from(data.short[1], "base64").length, Buffer.from(data.personal[1], "base64").length, Buffer.from(data.legacy[1], "base64").length], plaintextAbsent: !JSON.stringify(raw).includes("4242") && !JSON.stringify(raw).includes("078-05-1120"), independentDecrypt: [decrypt(data.card), decrypt(data.personal), decrypt(data.legacy, "aes-256-cbc")], sameValueTwice: [data.card[1] !== rawAgain._internal.data.card[1], data.card[0] !== rawAgain._internal.data.card[0]], @@ -52,6 +52,8 @@ const DATES = `${DECRYPT} await d.RegisterSchema("encdate"); const model = d.GetModel(d.BasicDataModel(Event)); const moment = new Date("2026-01-02T03:04:05.678Z"); + const [nestedId] = await model.insert({ when: { inside: { at: moment }, list: [moment, "text", [moment]] }, plainObject: 1 }); + const nested = (await model.get(nestedId)).when; const [id] = await model.insert({ when: moment, plainObject: { type: "Date", value: "not a date", extra: 1 } }); const read = await model.get(id); const raw = await rawTable("encdate", "events").get(id); @@ -60,6 +62,7 @@ const DATES = `${DECRYPT} iso: read.when.toISOString(), storedText: JSON.parse(decrypt(raw._internal.data.when)), lookalikeKept: read.plainObject, + nested: [nested.inside.at instanceof Date, nested.inside.at.toISOString(), nested.list[0] instanceof Date, nested.list[1], nested.list[2][0] instanceof Date, nested.list[2][0].toISOString()], }; `; @@ -88,7 +91,7 @@ export const ENCRYPTION_JOURNEYS = { "encryption-at-rest": scenario({ title: "an encrypted field is stored as base64 ciphertext, iv and auth tag that only the key opens", body: AT_REST, - printed: "{\"read\":[\"4242 4242 4242 4242\",{\"ssn\":\"078-05-1120\",\"birthDate\":\"1990-01-01\"},\"old cipher\",\"twelve\"],\"topLevelHoldsNoCiphertext\":true,\"gcmShape\":[true,3,\"string\"],\"cbcShape\":[3,null],\"ivBytes\":[16,12],\"plaintextAbsent\":true,\"independentDecrypt\":[\"\\\"4242 4242 4242 4242\\\"\",\"{\\\"ssn\\\":\\\"078-05-1120\\\",\\\"birthDate\\\":\\\"1990-01-01\\\"}\",\"\\\"old cipher\\\"\"],\"sameValueTwice\":[true,true]}", + printed: "{\"read\":[\"4242 4242 4242 4242\",{\"ssn\":\"078-05-1120\",\"birthDate\":\"1990-01-01\"},\"old cipher\",\"twelve\"],\"topLevelHoldsNoCiphertext\":true,\"gcmShape\":[true,3,\"string\"],\"cbcShape\":[3,null],\"ivBytes\":[16,12,16,16],\"plaintextAbsent\":true,\"independentDecrypt\":[\"\\\"4242 4242 4242 4242\\\"\",\"{\\\"ssn\\\":\\\"078-05-1120\\\",\\\"birthDate\\\":\\\"1990-01-01\\\"}\",\"\\\"old cipher\\\"\"],\"sameValueTwice\":[true,true]}", stored: [ { name: "the stored triples", @@ -97,6 +100,6 @@ export const ENCRYPTION_JOURNEYS = { }, ], }), - "encryption-dates": scenario({ title: "an encrypted Date comes back as a Date with whole-second precision", body: DATES, printed: "{\"isDate\":true,\"iso\":\"2026-01-02T03:04:05.000Z\",\"storedText\":{\"value\":\"Fri, 02 Jan 2026 03:04:05 GMT\",\"type\":\"Date\"},\"lookalikeKept\":{\"type\":\"Date\",\"value\":\"not a date\",\"extra\":1}}" }), + "encryption-dates": scenario({ title: "an encrypted Date comes back as a Date with whole-second precision", body: DATES, printed: "{\"isDate\":true,\"iso\":\"2026-01-02T03:04:05.000Z\",\"storedText\":{\"value\":\"Fri, 02 Jan 2026 03:04:05 GMT\",\"type\":\"Date\"},\"lookalikeKept\":{\"type\":\"Date\",\"value\":\"not a date\",\"extra\":1},\"nested\":[true,\"2026-01-02T03:04:05.000Z\",true,\"text\",true,\"2026-01-02T03:04:05.000Z\"]}" }), "encryption-keys": scenario({ title: "the key goes to the cipher as a UTF-8 string, so a hex-encoded key is refused", body: KEYS, printed: "{\"hexKeyBytes\":64,\"hexRefused\":{\"name\":\"RangeError\",\"message\":\"Invalid key length\"},\"goodStored\":\"kept\",\"hexStored\":0}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/hashing.ts b/.agents/skills/verify-interface/scenarios/hashing.ts index f06ac2b..32e9b83 100644 --- a/.agents/skills/verify-interface/scenarios/hashing.ts +++ b/.agents/skills/verify-interface/scenarios/hashing.ts @@ -143,7 +143,13 @@ const BOUNDS = `${HASH_HELPERS} r1: await attempt("r1", scryptString("pw", 4, 1, 1)), r10: await attempt("r10", scryptString("pw", 4, 10, 1)), r99: await attempt("r99", scryptString("pw", 4, 99, 1)), - r100: await attempt("r100", fake(4, 100, 1)), + r100: await attempt("r100", scryptString("pw", 4, 100, 1)), + r0: await attempt("r0", fake(4, 0, 1)), + ln0: await attempt("ln0", fake(0, 8, 1)), + p0: await attempt("p0", fake(4, 8, 0)), + p99: await attempt("p99", scryptString("pw", 4, 8, 99)), + ln16r1: await attempt("ln16r1", fake(16, 1, 1)), + salt65: await attempt("salt65", scryptString("pw", 4, 8, 1, 65, 32)), ln1: await attempt("ln1", scryptString("pw", 1, 8, 1)), ln2: await attempt("ln2", scryptString("pw", 2, 8, 1)), ln99: await attempt("ln99", fake(99, 8, 1)), @@ -161,6 +167,23 @@ const BOUNDS = `${HASH_HELPERS} }; `; +const MEMORY = `${HASH_HELPERS} + const seed = async (id, password) => rawTable("hash", "accounts").insert({ _id: id, name: id, _internal: { meta: {}, data: { password } } }).run(); + const attempt = async (id, stored) => { + await seed(id, stored); + const row = await accounts.get(id); + try { return [await row.testHashAsync("password", "pw"), await row.testHashAsync("password", "other")]; } catch (error) { return "threw " + error.name; } + }; + return { + exactR64: await attempt("a", scryptString("pw", 14, 64, 1)), + overR65: await attempt("b", scryptString("pw", 14, 65, 1)), + exactR2: await attempt("c", scryptString("pw", 19, 2, 1)), + underR3: await attempt("d", scryptString("pw", 18, 3, 1)), + overR5: await attempt("e", scryptString("pw", 18, 5, 1)), + overR9: await attempt("f", scryptString("pw", 17, 9, 1)), + }; +`; + const OUTDATED = `${HASH_HELPERS} const seed = async (id, password) => rawTable("hash", "accounts").insert({ _id: id, name: id, _internal: { meta: {}, data: { password } } }).run(); const probe = async (id, stored) => { await seed(id, stored); const row = await accounts.get(id); return [await row.testHashAsync("password", "pw"), row.isHashOutdated("password")]; }; @@ -190,7 +213,8 @@ export const HASH_JOURNEYS = { body: UPGRADE, printed: "{\"before\":[true,true,true,true],\"wrongLegacy\":false,\"afterRewrite\":[true,false,true],\"keptByNameUpdate\":true,\"keptByObjectUpdate\":true,\"viaUpdate\":true}", }), - "hash-bounds": scenario({ title: "a stored scrypt string verifies at each edge of the documented ranges and is refused just outside", body: BOUNDS, printed: "{\"r1\":[true,false],\"r10\":[true,false],\"r99\":[true,false],\"r100\":[false,false],\"ln1\":[true,false],\"ln2\":[true,false],\"ln99\":[false,false],\"ln100\":[false,false],\"p1\":[true,false],\"p16\":[true,false],\"salt16\":[true,false],\"salt64\":[true,false],\"salt80\":[false,false],\"salt15\":[false,false],\"key16\":[true,false],\"key64\":[true,false],\"key15\":[false,false],\"key65\":[false,false]}" }), + "hash-bounds": scenario({ title: "a stored scrypt string verifies at each edge of the documented ranges and is refused just outside", body: BOUNDS, printed: "{\"r1\":[true,false],\"r10\":[true,false],\"r99\":[true,false],\"r100\":[false,false],\"r0\":[false,false],\"ln0\":[false,false],\"p0\":[false,false],\"p99\":[false,false],\"ln16r1\":[false,false],\"salt65\":[false,false],\"ln1\":[true,false],\"ln2\":[true,false],\"ln99\":[false,false],\"ln100\":[false,false],\"p1\":[true,false],\"p16\":[true,false],\"salt16\":[true,false],\"salt64\":[true,false],\"salt80\":[false,false],\"salt15\":[false,false],\"key16\":[true,false],\"key64\":[true,false],\"key15\":[false,false],\"key65\":[false,false]}" }), + "hash-memory": scenario({ title: "a stored scrypt string verifies up to 128 MiB of memory (128 x N x r) and is refused above it", body: MEMORY, printed: "{\"exactR64\":[true,false],\"overR65\":[false,false],\"exactR2\":[true,false],\"underR3\":[true,false],\"overR5\":[false,false],\"overR9\":[false,false]}" }), "hash-outdated": scenario({ title: "isHashOutdated flags any scrypt string whose ln, r or p differs from the current cost", body: OUTDATED, printed: "{\"fresh\":[true,false],\"sameCost\":[true,false],\"lowerP\":[true,true],\"higherR\":[true,true],\"lowerLn\":[true,true],\"notScrypt\":[false,true]}" }), "hash-digest": scenario({ title: "a field with an algorithm stores a salted digest with the row's shared salt", body: DIGEST, printed: "{\"storedDigest\":true,\"sharedSalt\":true,\"saltIsText\":true,\"freshSaltPerRow\":true,\"saltReusedOnLaterWrite\":[true,true],\"testDigest\":[true,false,true],\"digestNeverOutdated\":false,\"scryptOnDigestField\":[true,false],\"otherDigestRefused\":false}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/localization.ts b/.agents/skills/verify-interface/scenarios/localization.ts index 51e60f4..3ff14e5 100644 --- a/.agents/skills/verify-interface/scenarios/localization.ts +++ b/.agents/skills/verify-interface/scenarios/localization.ts @@ -85,7 +85,7 @@ const STORED = `${PRODUCT} const AUTO = ` class Article extends d.Table {} - decorate(Article, { title: [d.Field("string")], creationDate: [d.CreationTime(), d.Field("date")], updateDate: [d.UpdateTime(), d.Field("date")] }); + decorate(Article, { title: [d.Field("string")], creationDate: [d.CreationTime(), d.Field("date")], updateDate: [d.UpdateTime(), d.Field("date")], born: [d.CreationTime(), d.Field("date")], touched: [d.UpdateTime(), d.Field("date")] }); d.RegisterTable("articles", "auto")(Article); await d.RegisterSchema("auto"); const model = d.GetModel(d.BasicDataModel(Article)); @@ -101,6 +101,7 @@ const AUTO = ` insertSetsBoth: [stamp(inserted.creationDate) >= before, stamp(inserted.updateDate) >= before, stamp(inserted.creationDate) <= after], creationKeptOnUpdate: stamp(updated.creationDate) === stamp(inserted.creationDate), updateRefreshed: stamp(updated.updateDate) >= stamp(inserted.updateDate) + 50, + secondFieldsToo: [stamp(inserted.born) >= before, stamp(inserted.touched) >= before, stamp(updated.born) === stamp(inserted.born), stamp(updated.touched) >= stamp(inserted.touched) + 50], noMixinNeeded: Object.getPrototypeOf(Article.prototype) === d.Table.prototype, }; `; @@ -140,7 +141,7 @@ export const AUTODATE_JOURNEYS = { autodate: scenario({ title: "CreationTime is set on insert and kept on update, UpdateTime is set on insert and refreshed on update", body: AUTO, - printed: "{\"insertSetsBoth\":[true,true,true],\"creationKeptOnUpdate\":true,\"updateRefreshed\":true,\"noMixinNeeded\":true}", + printed: "{\"insertSetsBoth\":[true,true,true],\"creationKeptOnUpdate\":true,\"updateRefreshed\":true,\"secondFieldsToo\":[true,true,true,true],\"noMixinNeeded\":true}", stored: [{ name: "the article dates", expression: "$DB.getCollection('auto__articles').find().toArray().map((r) => [r.title, r.creationDate instanceof Date, r.updateDate > r.creationDate])", expected: "[[\"Two\",true,true]]" }], }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/models.ts b/.agents/skills/verify-interface/scenarios/models.ts index d9c8214..07562ed 100644 --- a/.agents/skills/verify-interface/scenarios/models.ts +++ b/.agents/skills/verify-interface/scenarios/models.ts @@ -94,12 +94,14 @@ const VALIDATION = ` const patchRefused = await errorOf(people.update(accepted[0], { age: "old" }, { validate: true })); const objectPatchRefused = await errorOf(people.update({ _id: accepted[0], name: 7 }, { validate: true })); const unvalidatedPatch = await people.update(accepted[0], { age: "old" }); + const presentUndefined = Model.validate({ age: undefined }, { partial: true }); + const absentAge = Model.validate({ name: "x" }, { partial: true }); const hole = { _tag: "codec", name: "hole", is: () => false, encode: (a) => a, decode: () => ({ _tag: "Left", left: [{}, { message: "after a hole" }, { message: "last" }] }) }; const silent = { _tag: "codec", name: "silent", is: () => false, encode: (a) => a, decode: () => ({ _tag: "Left", left: [{}, { message: "" }] }) }; class Odd extends d.Table {} decorate(Odd, { a: [d.Field(hole)], b: [d.Field(silent)] }); const oddMessages = d.BasicDataModel(Odd, "odd").validate({}); - return { valid, invalid, notAnObject, partialOk, partialEmpty, partialBad, tokenSkipped, optIn: optIn.length, refused, batchRefused, patched, patchRefused, objectPatchRefused, unvalidatedPatch, oddMessages }; + return { valid, invalid, notAnObject, partialOk, partialEmpty, partialBad, tokenSkipped, optIn: optIn.length, refused, batchRefused, patched, patchRefused, objectPatchRefused, unvalidatedPatch, oddMessages, presentUndefined, absentAge }; `; const GET_MODEL = ` @@ -242,7 +244,7 @@ export const MODEL_JOURNEYS = { validation: scenario({ title: "validate checks the io-ts codecs, and insert and update refuse invalid input only when asked", body: VALIDATION, - printed: "{\"valid\":{\"ok\":true},\"invalid\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"notAnObject\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"partialOk\":{\"ok\":true},\"partialEmpty\":{\"ok\":true},\"partialBad\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"tokenSkipped\":{\"ok\":true},\"optIn\":1,\"refused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"batchRefused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"patched\":1,\"patchRefused\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"objectPatchRefused\":{\"message\":\"validation failed: name (expected a string)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"}]},\"unvalidatedPatch\":1,\"oddMessages\":{\"ok\":false,\"errors\":[{\"field\":\"a\",\"message\":\"after a hole\"},{\"field\":\"b\",\"message\":\"invalid b\"}]}}", + printed: "{\"valid\":{\"ok\":true},\"invalid\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"notAnObject\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"partialOk\":{\"ok\":true},\"partialEmpty\":{\"ok\":true},\"partialBad\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"tokenSkipped\":{\"ok\":true},\"optIn\":1,\"refused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"batchRefused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"patched\":1,\"patchRefused\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"objectPatchRefused\":{\"message\":\"validation failed: name (expected a string)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"}]},\"unvalidatedPatch\":1,\"oddMessages\":{\"ok\":false,\"errors\":[{\"field\":\"a\",\"message\":\"after a hole\"},{\"field\":\"b\",\"message\":\"invalid b\"}]},\"presentUndefined\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"absentAge\":{\"ok\":true}}", stored: [{ name: "the people", expression: "$DB.getCollection('val__people').find().sort({ name: 1 }).toArray().map((r) => [r.name, r.age])", expected: "[[42,\"anything\"],[\"Bob\",\"old\"]]" }], }), "get-model": scenario({ diff --git a/.agents/skills/verify-interface/scenarios/registration.ts b/.agents/skills/verify-interface/scenarios/registration.ts index 2e239f8..c5c9ec9 100644 --- a/.agents/skills/verify-interface/scenarios/registration.ts +++ b/.agents/skills/verify-interface/scenarios/registration.ts @@ -55,7 +55,7 @@ const FIXTURES = ` d.Fixture(() => [{ _id: "loose", name: "never inserted" }])(Loose); class Stamped extends d.Table.with(d.HashModifier) {} decorate(Stamped, { secret: [d.Hashed(), d.Field("string")], created: [d.CreationTime(), d.Field("date")] }); - d.Fixture(() => [{ _id: "s1", secret: "hunter2" }])(Stamped); + d.Fixture(() => [{ _id: "s1", secret: "hunter2" }, { _id: "s2", secret: "hunter3" }])(Stamped); d.RegisterTable("stamped", "fx")(Stamped); await d.RegisterSchema("fx"); const count = (name) => rawTable("fx", name).count(); @@ -64,13 +64,14 @@ const FIXTURES = ` await d.RegisterSchema("fx"); const afterSecondStart = await count("roles"); const ids = (await rawTable("fx", "plans")).map((row) => row._id).sort(); - const stamped = await rawTable("fx", "stamped").get("s1"); + const stampedRows = [await rawTable("fx", "stamped").get("s1"), await rawTable("fx", "stamped").get("s2")]; + const stamped = stampedRows[0]; return { first, afterSecondStart, plans: ids, unregisteredClassProvisioned: Object.keys(iface.Schema.get("fx").definition).includes("loose"), - hashedAtInsert: stamped._internal.data.secret.startsWith("$" + "scrypt" + "$ln=15,r=8,p=3$") && !stamped._internal.data.secret.includes("hunter2"), + hashedAtInsert: stampedRows.map((row) => row._internal.data.secret.startsWith("$" + "scrypt" + "$ln=15,r=8,p=3$") && !row._internal.data.secret.includes("hunter")), insertEventFired: stamped.created !== undefined, }; `; @@ -161,10 +162,10 @@ export const REGISTRATION_JOURNEYS = { fixtures: scenario({ title: "fixtures fill an empty table once, with generated ids and the same conversion as an insert", body: FIXTURES, - printed: "{\"first\":{\"roles\":2,\"settings\":1,\"plans\":3,\"nothing\":0},\"afterSecondStart\":1,\"plans\":[\"plans:fixture:0\",\"plans:fixture:1\",\"plans:fixture:2\"],\"unregisteredClassProvisioned\":false,\"hashedAtInsert\":true,\"insertEventFired\":true}", + printed: "{\"first\":{\"roles\":2,\"settings\":1,\"plans\":3,\"nothing\":0},\"afterSecondStart\":1,\"plans\":[\"plans:fixture:0\",\"plans:fixture:1\",\"plans:fixture:2\"],\"unregisteredClassProvisioned\":false,\"hashedAtInsert\":[true,true],\"insertEventFired\":true}", stored: [ { name: "the plan rows", expression: "$DB.getCollection('fx__plans').find({}, { name: 0 }).sort({ _id: 1 }).toArray().map((r) => [r._id, r.label])", expected: "[[\"plans:fixture:0\",\"free\"],[\"plans:fixture:1\",\"pro\"],[\"plans:fixture:2\",\"team\"]]" }, - { name: "the stamped row", expression: "$DB.getCollection('fx__stamped').find().toArray().map((r) => [r._id, r._internal.data.secret.startsWith('$' + 'scrypt' + '$ln=15,r=8,p=3$'), r._internal.data.secret.includes('hunter2'), r.created !== undefined])", expected: "[[\"s1\",true,false,true]]" }, + { name: "the stamped row", expression: "$DB.getCollection('fx__stamped').find().sort({ _id: 1 }).toArray().map((r) => [r._id, r._internal.data.secret.startsWith('$' + 'scrypt' + '$ln=15,r=8,p=3$'), r._internal.data.secret.includes('hunter'), r.created !== undefined])", expected: "[[\"plans:fixture:0\",\"free\"],[\"plans:fixture:1\",\"pro\"],[\"plans:fixture:2\",\"team\"]]" }, ], }), "fixture-race": scenario({ diff --git a/.agents/skills/verify-interface/scenarios/tables.ts b/.agents/skills/verify-interface/scenarios/tables.ts index 0c3f18a..9984236 100644 --- a/.agents/skills/verify-interface/scenarios/tables.ts +++ b/.agents/skills/verify-interface/scenarios/tables.ts @@ -37,6 +37,13 @@ const GROUP_FLAGS = ` b: [d.Field("string"), d.Index({ group: "ab", unique: true })], c: [d.Field("string"), d.Index({ group: "cd", crossInstance: true })], e: [d.Field("string"), d.Index({ group: "cd" })], + f: [d.Field("string"), d.Index({ group: "ef" })], + g: [d.Field("string"), d.Index({ group: "ef", crossInstance: true })], + h: [d.Field("string"), d.Index({ group: "ef" })], + i: [d.Field("string"), d.Index({ group: "ij", unique: true })], + j: [d.Field("string"), d.Index({ group: "ij" })], + k: [d.Field("string"), d.Index({ group: "plain" })], + l: [d.Field("string"), d.Index({ group: "plain" })], }); d.RegisterTable("pairs", "grp")(Pair); await d.RegisterSchema("grp"); @@ -105,6 +112,9 @@ const RELATION = ` `; const INHERITANCE = ` + class Cross extends d.Table {} + decorate(Cross, { a: [d.Field("string"), d.Index({ group: "ab" })], b: [d.Field("string"), d.Index({ group: "ab", crossInstance: true, unique: true })], c: [d.Field("string"), d.Index({ group: "c" })] }); + d.RegisterTable("crosses", "inh")(Cross); class Base extends d.Table {} decorate(Base, { createdAt: [d.Field("date")] }); d.RegisterTable("people", "inh")(Base); @@ -112,10 +122,15 @@ const INHERITANCE = ` decorate(Person, { email: [d.Field("string"), d.Index({ unique: true })] }); class Employee extends Person {} decorate(Employee, { badge: [d.Field("string"), d.Index()] }); + class Sub extends Cross {} + decorate(Sub, { extra: [d.Field("string")] }); + d.RegisterTable("subs", "inh")(Sub); class Contractor extends Base {} decorate(Contractor, { rate: [d.Field("number")] }); - const meta = (cls) => { const m = d.getMetadata(cls, d.DatumStaticMetadata); return { table: m.tableName ?? null, schema: m.schemaName ?? null, fields: Object.keys(m.fields), indexes: Object.keys(m.indexes), unique: [...m.uniqueIndexes] }; }; - return { base: meta(Base), person: meta(Person), employee: meta(Employee), contractor: meta(Contractor) }; + const meta = (cls) => { const m = d.getMetadata(cls, d.DatumStaticMetadata); return { table: m.tableName ?? null, schema: m.schemaName ?? null, fields: Object.keys(m.fields), indexes: Object.keys(m.indexes), unique: [...m.uniqueIndexes], cross: [...m.crossInstanceIndexes] }; }; + await d.RegisterSchema("inh"); + const built = iface.Schema.get("inh").definition; + return { built: [built.subs.indexes.ab, built.crosses.indexes.ab], base: meta(Base), person: meta(Person), employee: meta(Employee), contractor: meta(Contractor), cross: meta(Cross), sub: meta(Sub) }; `; const WITH = ` @@ -169,10 +184,10 @@ export const TABLE_JOURNEYS = { printed: "{\"definition\":{\"fields\":{\"email\":\"string\",\"first\":\"string\",\"last\":\"string\",\"age\":\"number\"},\"indexes\":{\"email\":{\"fields\":null,\"unique\":true},\"name\":{\"fields\":[\"first\",\"last\"],\"crossInstance\":true},\"age\":{\"fields\":null}}},\"duplicate\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"email\\\" (email)\"},\"recognised\":[true,false],\"otherInstanceStored\":1,\"byGroup\":[\"a@x\"],\"bySingle\":[\"Ada\"],\"between\":[\"Ada\"],\"symbolKey\":\"@Index on Symbol(k): a database index names a field, never a symbol\"}", stored: [{ name: "the indexes of the users collection", expression: "$DB.getCollection('idx__users').getIndexes().map((i) => [i.name, i.unique === true]).sort()", expected: "[[\"_id_\",false],[\"age__i\",false],[\"email__i\",true],[\"name\",false],[\"name__i\",false]]" }], }), - "group-flags": scenario({ title: "unique and crossInstance on any field of a group apply to the whole group", body: GROUP_FLAGS, printed: "{\"indexes\":{\"ab\":{\"fields\":[\"a\",\"b\"],\"unique\":true},\"cd\":{\"fields\":[\"c\",\"e\"],\"crossInstance\":true}},\"sameKey\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"ab\\\" (a, b)\"},\"otherSecond\":1}" }), + "group-flags": scenario({ title: "unique and crossInstance on any field of a group apply to the whole group", body: GROUP_FLAGS, printed: "{\"indexes\":{\"ab\":{\"fields\":[\"a\",\"b\"],\"unique\":true},\"cd\":{\"fields\":[\"c\",\"e\"],\"crossInstance\":true},\"ef\":{\"fields\":[\"f\",\"g\",\"h\"],\"crossInstance\":true},\"ij\":{\"fields\":[\"i\",\"j\"],\"unique\":true},\"plain\":{\"fields\":[\"k\",\"l\"]}},\"sameKey\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"ab\\\" (a, b)\"},\"otherSecond\":1}" }), "field-types": scenario({ title: "Field hands every type to the database unchanged and combines with Index in either order", body: FIELD_TYPES, printed: "{\"names\":[\"id\",\"tags\",\"address\",\"total\",\"lateIndex\"],\"id\":\"string\",\"tags\":[\"string\"],\"address\":{\"street\":\"string\",\"geo\":{\"lat\":\"number\",\"lng\":\"number\"}},\"codecIsTheSameObject\":true,\"lateIndexType\":\"number\",\"indexes\":[\"total\",\"lateIndex\"]}" }), "relation-metadata": scenario({ title: "Relation records links without enforcing them", body: RELATION, printed: "{\"relationFields\":[\"authorId\",\"tagIds\"],\"authorTarget\":true,\"authorToField\":null,\"tagMany\":true,\"tagToField\":\"name\",\"dangling\":[\"nobody\",[\"ghost\"]],\"fieldsKept\":[\"authorId\",\"tagIds\",\"title\"],\"childRelations\":[\"authorId\",\"tagIds\",\"editorId\"],\"parentRelations\":[\"authorId\",\"tagIds\"]}" }), - inheritance: scenario({ title: "a table subclass starts with a copy of its parent and never changes it", body: INHERITANCE, printed: "{\"base\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\"],\"indexes\":[],\"unique\":[]},\"person\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\"],\"indexes\":[\"email\"],\"unique\":[\"email\"]},\"employee\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\",\"badge\"],\"indexes\":[\"email\",\"badge\"],\"unique\":[\"email\"]},\"contractor\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"rate\"],\"indexes\":[],\"unique\":[]}}" }), + inheritance: scenario({ title: "a table subclass starts with a copy of its parent and never changes it", body: INHERITANCE, printed: "{\"built\":[{\"fields\":[\"a\",\"b\"],\"crossInstance\":true,\"unique\":true},{\"fields\":[\"a\",\"b\"],\"crossInstance\":true,\"unique\":true}],\"base\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\"],\"indexes\":[],\"unique\":[],\"cross\":[]},\"person\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\"],\"indexes\":[\"email\"],\"unique\":[\"email\"],\"cross\":[]},\"employee\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\",\"badge\"],\"indexes\":[\"email\",\"badge\"],\"unique\":[\"email\"],\"cross\":[]},\"contractor\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"rate\"],\"indexes\":[],\"unique\":[],\"cross\":[]},\"cross\":{\"table\":\"crosses\",\"schema\":\"inh\",\"fields\":[\"a\",\"b\",\"c\"],\"indexes\":[\"ab\",\"c\"],\"unique\":[\"ab\"],\"cross\":[\"ab\"]},\"sub\":{\"table\":\"subs\",\"schema\":\"inh\",\"fields\":[\"a\",\"b\",\"c\",\"extra\"],\"indexes\":[\"ab\",\"c\"],\"unique\":[\"ab\"],\"cross\":[\"ab\"]}}" }), "combine-modifiers": scenario({ title: "Table.with combines the encryption, hashing and localization modifiers on one table", body: COMBINE, printed: "{\"stored\":[\"bio\",\"password\",\"privateInfo\"],\"privateInfo\":{\"address\":\"1 Main St\",\"phoneNumber\":\"555\"},\"password\":[true,false],\"bio\":[\"Bonjour\",\"Hello\"],\"mixins\":[\"function\",\"function\"]}" }), "table-with": scenario({ title: "Table.with adds the mixin methods of the modifiers to a subclass", body: WITH, printed: "{\"plainHasTestHash\":false,\"hashingHas\":[true,true,true,false],\"bothHas\":[true,true],\"isTableSubclass\":true,\"tableItselfUntouched\":\"undefined\",\"subIsASubclass\":true}" }), } satisfies { [id: string]: Journey }; From b6796b1377c0e08007b3c4286428f32b76955926 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 3 Nov 2026 21:55:53 +0100 Subject: [PATCH 27/45] fix(skills): correct the stamped fixture read-back --- .agents/skills/verify-interface/scenarios/registration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/skills/verify-interface/scenarios/registration.ts b/.agents/skills/verify-interface/scenarios/registration.ts index c5c9ec9..3070821 100644 --- a/.agents/skills/verify-interface/scenarios/registration.ts +++ b/.agents/skills/verify-interface/scenarios/registration.ts @@ -165,7 +165,7 @@ export const REGISTRATION_JOURNEYS = { printed: "{\"first\":{\"roles\":2,\"settings\":1,\"plans\":3,\"nothing\":0},\"afterSecondStart\":1,\"plans\":[\"plans:fixture:0\",\"plans:fixture:1\",\"plans:fixture:2\"],\"unregisteredClassProvisioned\":false,\"hashedAtInsert\":[true,true],\"insertEventFired\":true}", stored: [ { name: "the plan rows", expression: "$DB.getCollection('fx__plans').find({}, { name: 0 }).sort({ _id: 1 }).toArray().map((r) => [r._id, r.label])", expected: "[[\"plans:fixture:0\",\"free\"],[\"plans:fixture:1\",\"pro\"],[\"plans:fixture:2\",\"team\"]]" }, - { name: "the stamped row", expression: "$DB.getCollection('fx__stamped').find().sort({ _id: 1 }).toArray().map((r) => [r._id, r._internal.data.secret.startsWith('$' + 'scrypt' + '$ln=15,r=8,p=3$'), r._internal.data.secret.includes('hunter'), r.created !== undefined])", expected: "[[\"plans:fixture:0\",\"free\"],[\"plans:fixture:1\",\"pro\"],[\"plans:fixture:2\",\"team\"]]" }, + { name: "the stamped row", expression: "$DB.getCollection('fx__stamped').find().sort({ _id: 1 }).toArray().map((r) => [r._id, r._internal.data.secret.startsWith('$' + 'scrypt' + '$ln=15,r=8,p=3$'), r._internal.data.secret.includes('hunter'), r.created !== undefined])", expected: "[[\"s1\",true,false,true],[\"s2\",true,false,true]]" }, ], }), "fixture-race": scenario({ From 941564316d24e915134ceffa5068df5609f6e6b5 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 3 Nov 2026 23:32:07 +0100 Subject: [PATCH 28/45] feat(skills): pin method options and the serialisation helpers --- .../features/custom-modifiers.md | 2 + .../verify-interface/features/models.md | 2 + .../verify-interface/features/registration.md | 2 + .../verify-interface/scenarios/custom.ts | 42 ++++++++++++++++++ .../verify-interface/scenarios/hashing.ts | 12 +++-- .../scenarios/localization.ts | 9 ++-- .../verify-interface/scenarios/models.ts | 44 ++++++++++++++++++- .../scenarios/registration.ts | 19 ++++++++ 8 files changed, 124 insertions(+), 8 deletions(-) diff --git a/.agents/skills/verify-interface/features/custom-modifiers.md b/.agents/skills/verify-interface/features/custom-modifiers.md index 322b7b6..8505454 100644 --- a/.agents/skills/verify-interface/features/custom-modifiers.md +++ b/.agents/skills/verify-interface/features/custom-modifiers.md @@ -9,6 +9,7 @@ journeys: - one-way-modifier - two-way-modifier - container-modifier + - modifier-helpers - modifier-events --- @@ -20,6 +21,7 @@ journeys: - `one-way-modifier`: transform on write; a second one-way modifier on a field is refused. - `two-way-modifier`: transform on write, unlock on read. +- `modifier-helpers`: the exported helpers of the modifier machinery. - `container-modifier`: values by key and the wildcard. - `modifier-events`: event order for insert, update and reads; events-only modifiers add no accessor. diff --git a/.agents/skills/verify-interface/features/models.md b/.agents/skills/verify-interface/features/models.md index 4d83ed5..4c1ecfa 100644 --- a/.agents/skills/verify-interface/features/models.md +++ b/.agents/skills/verify-interface/features/models.md @@ -9,6 +9,7 @@ journeys: - conversion - validation - get-model + - plain-data - compound-get-by - autodate-object-update - partial-update-keeps-modified-fields @@ -27,6 +28,7 @@ journeys: - `validation`: codecs, opt-in `validate`, partial patches. - `get-model`: one cached model per class and instance id; instances stay apart. - `autodate-object-update`: the update form that carries the primary key, as in [autodate](autodate.md). +- `plain-data`: `toPlainData` output. - `compound-get-by`: a three-field compound index. - `partial-update-keeps-modified-fields`: known finding #80, fails until fixed. - `model-factory`: the table name comes from the argument or `RegisterTable`. diff --git a/.agents/skills/verify-interface/features/registration.md b/.agents/skills/verify-interface/features/registration.md index e94a55a..626cf6b 100644 --- a/.agents/skills/verify-interface/features/registration.md +++ b/.agents/skills/verify-interface/features/registration.md @@ -12,6 +12,7 @@ journeys: - fixtures - fixture-race - table-access + - fixture-not-empty - fixture-ids-mixed - table-foreign-instance --- @@ -25,6 +26,7 @@ journeys: - `register-schema`: provisioning, binding and the unknown-schema error. - `fixtures`: fixtures fill an empty table once, with generated ids and the insert conversion. - `fixture-race`: two starts write each row once; a subclass gets its parent's fixture. +- `fixture-not-empty`: fixtures only fill an empty table. - `fixture-ids-mixed`: ids kept and positions for the rest. - `table-foreign-instance`: a held instance of another schema is used as is. - `table-access`: `Table.table` for the default, a named, the cross-instance and a held instance, and its two errors. diff --git a/.agents/skills/verify-interface/scenarios/custom.ts b/.agents/skills/verify-interface/scenarios/custom.ts index df10ad8..3b4624e 100644 --- a/.agents/skills/verify-interface/scenarios/custom.ts +++ b/.agents/skills/verify-interface/scenarios/custom.ts @@ -104,6 +104,47 @@ const EVENTS = `${PRELUDE} return { onInsert, onUpdate, onRead, onReadAll, onPlain, eventsOnlyKeepPlainValue: plainAccessor === undefined }; `; +const HELPERS = ` + class Profile extends d.Table.with(d.HashModifier, d.LocalizationModifier) {} + decorate(Profile, { + password: [d.Hashed(), d.Field("string")], + title: [d.Localized({ fallbackLocale: "en" }), d.Field("string")], + summary: [d.Localized(), d.Field("string")], + plain: [d.Field("string")], + }); + d.RegisterTable("profiles", "helpers")(Profile); + class Other extends d.Table {} + d.RegisterTable("others", "helpers")(Other); + const row = new Profile(); + const names = { hashed: d.getModifiedFields(row, d.HashModifier), localized: d.getModifiedFields(row, d.LocalizationModifier), none: d.getModifiedFields(row, d.EncryptionModifier) }; + row.title = "floating"; + const floatingRead = row.title; + const floatingStored = Object.keys(d.toDatabase(row)).sort(); + d.unlock(row, d.LocalizationModifier, ["title"], "fr"); + const afterUnlock = [row.title, row.localize("fr").title, row.localize("en").title ?? "undefined"]; + const stored = d.toDatabase(row); + const lockedKeys = Object.keys(stored._internal.data).sort(); + row.localize("fr").summary = "résumé"; + d.unlock(row, d.LocalizationModifier, ["title"], "en"); + const restricted = [row.title ?? "undefined", row.summary ?? "undefined"]; + const tables = d.getTablesForSchema("helpers"); + const parentMeta = d.getMetadata(Profile, d.DatumStaticMetadata); + class Child extends Profile {} + const own = d.getMetadata(Child, d.DatumStaticMetadata, false); + class Child2 extends Profile {} + const inherited = d.getMetadata(Child2, d.DatumStaticMetadata); + return { + names, + floatingRead, + floatingStored, + afterUnlock, + lockedKeys, + restricted, + tables: [Object.keys(tables).sort(), tables.profiles === Profile, d.getTablesForSchema("nothing-here") ?? "undefined"], + metadata: [parentMeta.tableName, own.tableName ?? "undefined", inherited.tableName, own === inherited, inherited === parentMeta, d.getMetadata(Profile, d.DatumStaticMetadata) === parentMeta], + }; +`; + export const CUSTOM_JOURNEYS = { "one-way-modifier": scenario({ title: "a custom OneWayModifier transforms a value when it is written", body: ONE_WAY, printed: "{\"tested\":[true,false,true],\"stored\":[true,true],\"secondOneWay\":\"Attempted adding Modifier PrefixHashModifier on Twice.f but it already has a One-Way Modifier\"}" }), "two-way-modifier": scenario({ @@ -112,6 +153,7 @@ export const CUSTOM_JOURNEYS = { printed: "{\"read\":[{\"a\":[1,2]},\"plain\"],\"storedBody\":\"b64-eyJhIjpbMSwyXX0=\",\"storedTagged\":\"x:InBsYWluIg==\",\"notAString\":\"undefined\"}", stored: [{ name: "the stored encodings", expression: "$DB.getCollection('twoway__docs').find({ title: 't' }).toArray().map((r) => [r._internal.data.body, r._internal.data.tagged])", expected: "[[\"b64-eyJhIjpbMSwyXX0=\",\"x:InBsYWluIg==\"]]" }], }), + "modifier-helpers": scenario({ title: "getModifiedFields, unlock, lock, floating values, getMetadata and getTablesForSchema do what their TSDoc says", body: HELPERS, printed: "{\"names\":{\"hashed\":[\"password\"],\"localized\":[\"title\",\"summary\"],\"none\":[]},\"floatingRead\":\"floating\",\"floatingStored\":[\"_internal\"],\"afterUnlock\":[\"floating\",\"floating\",\"undefined\"],\"lockedKeys\":[\"title\"],\"restricted\":[\"undefined\",\"r\u00e9sum\u00e9\"],\"tables\":[[\"others\",\"profiles\"],true,\"undefined\"],\"metadata\":[\"profiles\",\"undefined\",\"profiles\",false,false,true]}" }), "container-modifier": scenario({ title: "a ContainerModifier keeps values by key and treats the wildcard key as the whole record", body: CONTAINER, printed: "{\"both\":{\"a\":1,\"b\":2},\"afterOverwrite\":{\"a\":3,\"b\":2},\"replaced\":{\"z\":26},\"byKey\":[26,\"undefined\",\"undefined\"]}" }), "modifier-events": scenario({ title: "a modifier's event methods run for inserts, updates and conversions, and an events-only modifier leaves the field plain", body: EVENTS, printed: "{\"onInsert\":[\"fromPlainData/note/true/Doc\",\"insert/note/true/Doc\",\"toDatabase/note/true/Doc\"],\"onUpdate\":[\"fromPlainData/note/true/Doc\",\"update/note/true/Doc\",\"toDatabase/note/true/Doc\"],\"onRead\":[\"fromDatabase/note/true/Doc\"],\"onReadAll\":[\"fromDatabase/note/true/Doc\"],\"onPlain\":[\"toPlainData/note/true/Doc\"],\"eventsOnlyKeepPlainValue\":true}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/hashing.ts b/.agents/skills/verify-interface/scenarios/hashing.ts index 32e9b83..1259231 100644 --- a/.agents/skills/verify-interface/scenarios/hashing.ts +++ b/.agents/skills/verify-interface/scenarios/hashing.ts @@ -10,7 +10,7 @@ const HASH_HELPERS = ` const scryptString = (value, ln, r, p, saltBytes = 16, keyBytes = 32) => { const salt = Buffer.alloc(saltBytes, 7); return "$" + "scrypt" + "$ln=" + ln + ",r=" + r + ",p=" + p + "$" + b64(salt) + "$" + b64(scryptOf(value, salt, keyBytes, ln, r, p)); }; const digest = (algorithm, salt, value) => crypto.createHash(algorithm).update(salt).update(JSON.stringify(value)).digest("base64"); class Account extends d.Table.with(d.HashModifier) {} - decorate(Account, { name: [d.Field("string")], password: [d.Hashed(), d.Field("string")], pin: [d.Hashed({ algorithm: "sha256" }), d.Field("string")], token: [d.Hashed({ algorithm: "sha256" }), d.Field("string")] }); + decorate(Account, { name: [d.Field("string")], password: [d.Hashed(), d.Field("string")], pin: [d.Hashed({ algorithm: "sha256" }), d.Field("string")], token: [d.Hashed({ algorithm: "sha256" }), d.Field("string")], long: [d.Hashed({ algorithm: "sha512" }), d.Field("string")], legacy: [d.Hashed({ algorithm: "md5" }), d.Field("string")] }); d.RegisterTable("accounts", "hash")(Account); await d.RegisterSchema("hash"); const Model = d.BasicDataModel(Account); @@ -106,9 +106,14 @@ const UPGRADE = `${HASH_HELPERS} `; const DIGEST = `${HASH_HELPERS} - const [id] = await accounts.insert({ name: "Ann", pin: "1234", token: "abc" }); + const [id] = await accounts.insert({ name: "Ann", pin: "1234", token: "abc", long: "abc", legacy: "abc" }); const raw = await rawRow(id); const salt = raw._internal.meta.HashModifier.salt; + const algorithms = [raw._internal.data.long === digest("sha512", salt, "abc"), raw._internal.data.legacy === digest("md5", salt, "abc"), raw._internal.data.long === digest("sha256", salt, "abc"), raw._internal.data.long.length, raw._internal.data.legacy.length]; + const checked = await accounts.get(id); + const algorithmTests = [checked.testHash("long", "abc"), checked.testHash("legacy", "abc"), checked.testHash("long", "abd")]; + await rawTable("hash", "accounts").insert({ _id: "wrong-digest", name: "w", _internal: { meta: { HashModifier: { salt } }, data: { long: digest("sha256", salt, "abc") } } }).run(); + const wrongDigest = (await accounts.get("wrong-digest")).testHash("long", "abc"); const row = await accounts.get(id); const [other] = await accounts.insert({ name: "Bea", pin: "1234" }); const otherSalt = (await rawRow(other))._internal.meta.HashModifier.salt; @@ -118,6 +123,7 @@ const DIGEST = `${HASH_HELPERS} const holdsScrypt = Model.fromDatabase({ _id: "h", name: "h", _internal: { meta: { HashModifier: { salt } }, data: { pin: scryptString("1234", 10, 8, 1) } } }); const sha512 = Model.fromDatabase({ _id: "s", name: "s", _internal: { meta: { HashModifier: { salt } }, data: { pin: crypto.createHash("sha512").update(salt).update(JSON.stringify("1234")).digest("base64") } } }); return { + algorithms, algorithmTests, wrongDigest, storedDigest: raw._internal.data.pin === digest("sha256", salt, "1234"), sharedSalt: raw._internal.data.token === digest("sha256", salt, "abc"), saltIsText: typeof salt === "string" && salt.length > 0, @@ -216,5 +222,5 @@ export const HASH_JOURNEYS = { "hash-bounds": scenario({ title: "a stored scrypt string verifies at each edge of the documented ranges and is refused just outside", body: BOUNDS, printed: "{\"r1\":[true,false],\"r10\":[true,false],\"r99\":[true,false],\"r100\":[false,false],\"r0\":[false,false],\"ln0\":[false,false],\"p0\":[false,false],\"p99\":[false,false],\"ln16r1\":[false,false],\"salt65\":[false,false],\"ln1\":[true,false],\"ln2\":[true,false],\"ln99\":[false,false],\"ln100\":[false,false],\"p1\":[true,false],\"p16\":[true,false],\"salt16\":[true,false],\"salt64\":[true,false],\"salt80\":[false,false],\"salt15\":[false,false],\"key16\":[true,false],\"key64\":[true,false],\"key15\":[false,false],\"key65\":[false,false]}" }), "hash-memory": scenario({ title: "a stored scrypt string verifies up to 128 MiB of memory (128 x N x r) and is refused above it", body: MEMORY, printed: "{\"exactR64\":[true,false],\"overR65\":[false,false],\"exactR2\":[true,false],\"underR3\":[true,false],\"overR5\":[false,false],\"overR9\":[false,false]}" }), "hash-outdated": scenario({ title: "isHashOutdated flags any scrypt string whose ln, r or p differs from the current cost", body: OUTDATED, printed: "{\"fresh\":[true,false],\"sameCost\":[true,false],\"lowerP\":[true,true],\"higherR\":[true,true],\"lowerLn\":[true,true],\"notScrypt\":[false,true]}" }), - "hash-digest": scenario({ title: "a field with an algorithm stores a salted digest with the row's shared salt", body: DIGEST, printed: "{\"storedDigest\":true,\"sharedSalt\":true,\"saltIsText\":true,\"freshSaltPerRow\":true,\"saltReusedOnLaterWrite\":[true,true],\"testDigest\":[true,false,true],\"digestNeverOutdated\":false,\"scryptOnDigestField\":[true,false],\"otherDigestRefused\":false}" }), + "hash-digest": scenario({ title: "a field with an algorithm stores a salted digest with the row's shared salt", body: DIGEST, printed: "{\"algorithms\":[true,true,false,88,24],\"algorithmTests\":[true,true,false],\"wrongDigest\":false,\"storedDigest\":true,\"sharedSalt\":true,\"saltIsText\":true,\"freshSaltPerRow\":true,\"saltReusedOnLaterWrite\":[true,true],\"testDigest\":[true,false,true],\"digestNeverOutdated\":false,\"scryptOnDigestField\":[true,false],\"otherDigestRefused\":false}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/localization.ts b/.agents/skills/verify-interface/scenarios/localization.ts index 3ff14e5..b8e8740 100644 --- a/.agents/skills/verify-interface/scenarios/localization.ts +++ b/.agents/skills/verify-interface/scenarios/localization.ts @@ -80,7 +80,10 @@ const STORED = `${PRODUCT} const [second] = await model.insert(alone); const only = await model.get(second); const raw = await rawTable("loc", "products").get(id); - return { rawKeys: Object.keys(raw._internal.data).sort(), raw: raw._internal.data.name, onlyFrench: [only.localize("fr").name, only.localize("en").name ?? "undefined"] }; + await rawTable("loc", "products").insert({ _id: "raw-empty", price: 7, _internal: { meta: {}, data: { name: { en: "Hello", fr: "" }, bare: { fr: "" } } } }).run(); + const rawRow = await model.get("raw-empty"); + const fromRaw = [rawRow.localize("fr").name, rawRow.localize("fr").bare ?? "undefined", rawRow.localize("en").name]; + return { fromRaw, rawKeys: Object.keys(raw._internal.data).sort(), raw: raw._internal.data.name, onlyFrench: [only.localize("fr").name, only.localize("en").name ?? "undefined"] }; `; const AUTO = ` @@ -131,8 +134,8 @@ export const LOCALIZATION_JOURNEYS = { "localized-storage": scenario({ title: "a localized field is stored as one value per locale", body: STORED, - printed: "{\"rawKeys\":[\"name\"],\"raw\":{\"en\":\"Hello\",\"fr\":\"Bonjour\"},\"onlyFrench\":[\"Seulement\",\"undefined\"]}", - stored: [{ name: "the stored translations", expression: "$DB.getCollection('loc__products').find().sort({ price: 1 }).toArray().map((r) => r._internal.data.name)", expected: "[{\"en\":\"Hello\",\"fr\":\"Bonjour\"},{\"fr\":\"Seulement\"}]" }], + printed: "{\"fromRaw\":[\"Hello\",\"undefined\",\"Hello\"],\"rawKeys\":[\"name\"],\"raw\":{\"en\":\"Hello\",\"fr\":\"Bonjour\"},\"onlyFrench\":[\"Seulement\",\"undefined\"]}", + stored: [{ name: "the stored translations", expression: "$DB.getCollection('loc__products').find().sort({ price: 1 }).toArray().map((r) => r._internal.data.name)", expected: "[{\"en\":\"Hello\",\"fr\":\"Bonjour\"},{\"fr\":\"Seulement\"},{\"en\":\"Hello\",\"fr\":\"\"}]" }], }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/models.ts b/.agents/skills/verify-interface/scenarios/models.ts index 07562ed..19ec3aa 100644 --- a/.agents/skills/verify-interface/scenarios/models.ts +++ b/.agents/skills/verify-interface/scenarios/models.ts @@ -96,12 +96,15 @@ const VALIDATION = ` const unvalidatedPatch = await people.update(accepted[0], { age: "old" }); const presentUndefined = Model.validate({ age: undefined }, { partial: true }); const absentAge = Model.validate({ name: "x" }, { partial: true }); + const missingRequired = await errorOf(people.insert({ name: "Alice" }, { validate: true })); + const missingInArray = await errorOf(people.insert([{ name: "Ok", age: 1 }, { name: "No age" }], { validate: true })); + const missingStored = (await people.getAll()).map((row) => row.name).sort(); const hole = { _tag: "codec", name: "hole", is: () => false, encode: (a) => a, decode: () => ({ _tag: "Left", left: [{}, { message: "after a hole" }, { message: "last" }] }) }; const silent = { _tag: "codec", name: "silent", is: () => false, encode: (a) => a, decode: () => ({ _tag: "Left", left: [{}, { message: "" }] }) }; class Odd extends d.Table {} decorate(Odd, { a: [d.Field(hole)], b: [d.Field(silent)] }); const oddMessages = d.BasicDataModel(Odd, "odd").validate({}); - return { valid, invalid, notAnObject, partialOk, partialEmpty, partialBad, tokenSkipped, optIn: optIn.length, refused, batchRefused, patched, patchRefused, objectPatchRefused, unvalidatedPatch, oddMessages, presentUndefined, absentAge }; + return { valid, invalid, notAnObject, partialOk, partialEmpty, partialBad, tokenSkipped, optIn: optIn.length, refused, batchRefused, patched, patchRefused, objectPatchRefused, unvalidatedPatch, oddMessages, missingRequired, missingInArray, missingStored, presentUndefined, absentAge }; `; const GET_MODEL = ` @@ -228,6 +231,42 @@ const PARTIAL = ` }; `; +const PLAIN_DATA = ` + class Account extends d.Table.with(d.EncryptionModifier, d.HashModifier, d.LocalizationModifier) {} + decorate(Account, { + name: [d.Field("string")], + secret: [d.Encrypted({ secretKey: "${KEY}" }), d.Field("any")], + password: [d.Hashed(), d.Field("string")], + title: [d.Localized({ fallbackLocale: "en" }), d.Field("string")], + }); + d.RegisterTable("accounts", "plain")(Account); + await d.RegisterSchema("plain"); + const model = d.GetModel(d.BasicDataModel(Account)); + const account = new Account(); + account._id = "a1"; + account.name = "Ann"; + account.secret = { token: "t" }; + account.password = "pw"; + account.localize("en").title = "Hello"; + const [id] = await model.insert(account); + const loaded = await model.get(id); + const plain = d.toPlainData(loaded); + const unlocked = d.toPlainData(loaded.localize("en")); + const events = []; + return { + keys: Object.keys(plain).filter((key) => key !== "_instance").sort(), + noInternal: !("_internal" in plain), + name: plain.name, + secret: plain.secret, + passwordIsHash: typeof plain.password === "string" && plain.password !== "pw" && plain.password.length > 20, + lockedTitle: plain.title ?? "undefined", + unlockedTitle: unlocked.title, + json: JSON.parse(JSON.stringify(loaded)).secret, + stillLocked: Object.keys(loaded._internal.data).sort(), + notAnInstance: !(plain instanceof Account), + }; +`; + export const MODEL_JOURNEYS = { crud: scenario({ title: "a model inserts, reads, updates and deletes rows and answers ids and counts", @@ -244,7 +283,7 @@ export const MODEL_JOURNEYS = { validation: scenario({ title: "validate checks the io-ts codecs, and insert and update refuse invalid input only when asked", body: VALIDATION, - printed: "{\"valid\":{\"ok\":true},\"invalid\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"notAnObject\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"partialOk\":{\"ok\":true},\"partialEmpty\":{\"ok\":true},\"partialBad\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"tokenSkipped\":{\"ok\":true},\"optIn\":1,\"refused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"batchRefused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"patched\":1,\"patchRefused\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"objectPatchRefused\":{\"message\":\"validation failed: name (expected a string)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"}]},\"unvalidatedPatch\":1,\"oddMessages\":{\"ok\":false,\"errors\":[{\"field\":\"a\",\"message\":\"after a hole\"},{\"field\":\"b\",\"message\":\"invalid b\"}]},\"presentUndefined\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"absentAge\":{\"ok\":true}}", + printed: "{\"valid\":{\"ok\":true},\"invalid\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"notAnObject\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"partialOk\":{\"ok\":true},\"partialEmpty\":{\"ok\":true},\"partialBad\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"tokenSkipped\":{\"ok\":true},\"optIn\":1,\"refused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"batchRefused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"patched\":1,\"patchRefused\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"objectPatchRefused\":{\"message\":\"validation failed: name (expected a string)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"}]},\"unvalidatedPatch\":1,\"oddMessages\":{\"ok\":false,\"errors\":[{\"field\":\"a\",\"message\":\"after a hole\"},{\"field\":\"b\",\"message\":\"invalid b\"}]},\"missingRequired\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"missingInArray\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"missingStored\":[42,\"Bob\"],\"presentUndefined\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"absentAge\":{\"ok\":true}}", stored: [{ name: "the people", expression: "$DB.getCollection('val__people').find().sort({ name: 1 }).toArray().map((r) => [r.name, r.age])", expected: "[[42,\"anything\"],[\"Bob\",\"old\"]]" }], }), "get-model": scenario({ @@ -253,6 +292,7 @@ export const MODEL_JOURNEYS = { printed: "{\"cache\":[true,true,true,true,true,true,true],\"defaultSees\":[\"default\"],\"tenantSees\":[\"tenant one\"],\"otherSees\":[\"tenant two\"],\"everywhereSees\":[\"default\",\"tenant one\",\"tenant two\"],\"customSchema\":[true,true],\"lost\":\"Schema not found for 'absent-schema'\",\"statics\":[\"gm\",true]}", stored: [{ name: "the rows by instance", expression: "$DB.getCollection('gm__items').find().sort({ name: 1 }).toArray().map((r) => [r.name, r._instance])", expected: "[[\"default\",null],[\"tenant one\",\"t1\"],[\"tenant two\",\"t2\"]]" }], }), + "plain-data": scenario({ title: "toPlainData gives unlocked values without _internal, and leaves a still-locked field undefined", body: PLAIN_DATA, printed: "{\"keys\":[\"_id\",\"name\",\"password\",\"secret\",\"title\"],\"noInternal\":true,\"name\":\"Ann\",\"secret\":{\"token\":\"t\"},\"passwordIsHash\":true,\"lockedTitle\":\"undefined\",\"unlockedTitle\":\"Hello\",\"json\":{\"token\":\"t\"},\"stillLocked\":[\"password\",\"secret\",\"title\"],\"notAnInstance\":true}" }), "compound-get-by": scenario({ title: "getBy matches a compound index of three fields by its keys in field order", body: COMPOUND, printed: "{\"exact\":[\"one\"],\"other\":[\"two\"],\"none\":[],\"order\":[]}" }), "partial-update-keeps-modified-fields": scenario({ title: "KNOWN FINDING AntelopeJS/interface-database-decorators#80, fails until fixed: update with one field keeps the row's other encrypted, hashed and localized fields", diff --git a/.agents/skills/verify-interface/scenarios/registration.ts b/.agents/skills/verify-interface/scenarios/registration.ts index 3070821..af497d4 100644 --- a/.agents/skills/verify-interface/scenarios/registration.ts +++ b/.agents/skills/verify-interface/scenarios/registration.ts @@ -152,6 +152,24 @@ const FOREIGN = ` return { homeCount: await Item.table().count(), awayCount: await Item.table(away).count() }; `; +const FIXTURE_NOT_EMPTY = ` + class Plain extends d.Table {} + decorate(Plain, { name: [d.Field("string")] }); + d.RegisterTable("shared", "notempty")(Plain); + await d.RegisterSchema("notempty"); + await d.GetModel(d.BasicDataModel(Plain)).insert({ _id: "unrelated", name: "already here" }); + class Seeded extends d.Table {} + decorate(Seeded, { name: [d.Field("string")] }); + d.Fixture(() => [{ _id: "fixture-a", name: "a" }, { _id: "fixture-b", name: "b" }])(Seeded); + d.RegisterTable("shared", "notempty")(Seeded); + await d.RegisterSchema("notempty"); + const ids = (await rawTable("notempty", "shared")).map((row) => row._id).sort(); + await rawTable("notempty", "shared").get("unrelated").delete().run(); + await d.RegisterSchema("notempty"); + const afterEmptied = (await rawTable("notempty", "shared")).map((row) => row._id).sort(); + return { whileNotEmpty: ids, afterEmptied }; +`; + export const REGISTRATION_JOURNEYS = { "register-schema": scenario({ title: "RegisterSchema provisions the registered tables, binds the classes and refuses an unknown schema", @@ -174,6 +192,7 @@ export const REGISTRATION_JOURNEYS = { printed: "{\"rows\":[\"seeds:fixture:0\",\"seeds:fixture:1\"],\"kids\":[\"kids:fixture:0\",\"kids:fixture:1\"],\"generatorReceived\":\"SpyChild\"}", stored: [{ name: "the seed rows", expression: "$DB.getCollection('race__seeds').find().sort({ _id: 1 }).toArray().map((r) => [r._id, r.name])", expected: "[[\"seeds:fixture:0\",\"a\"],[\"seeds:fixture:1\",\"b\"]]" }], }), + "fixture-not-empty": scenario({ title: "a table that already holds a row gets no fixture rows, and an emptied table gets them", body: FIXTURE_NOT_EMPTY, printed: "{\"whileNotEmpty\":[\"unrelated\"],\"afterEmptied\":[\"fixture-a\",\"fixture-b\"]}" }), "fixture-ids-mixed": scenario({ title: "a fixture mixing rows with and without an id keeps the ids it has and numbers the others by position", body: FIXTURE_MIXED, From da0a091a31191126229e6336378f8d9f6c9ec4bd Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Wed, 4 Nov 2026 00:49:11 +0100 Subject: [PATCH 29/45] feat(skills): cover sync and async hash tests, subclass modifier lists, event arguments, localized queries and with() --- .../verify-interface/features/README.md | 2 + .../verify-interface/features/hashing.md | 2 + .../verify-interface/features/localization.md | 2 + .../verify-interface/features/tables.md | 6 ++ .agents/skills/verify-interface/runner.ts | 16 ++-- .../verify-interface/scenarios/custom.ts | 18 ++-- .../verify-interface/scenarios/hashing.ts | 24 ++++++ .../scenarios/localization.ts | 23 +++++ .../verify-interface/scenarios/tables.ts | 83 +++++++++++++++++-- 9 files changed, 155 insertions(+), 21 deletions(-) diff --git a/.agents/skills/verify-interface/features/README.md b/.agents/skills/verify-interface/features/README.md index 8f2e870..b885a97 100644 --- a/.agents/skills/verify-interface/features/README.md +++ b/.agents/skills/verify-interface/features/README.md @@ -39,3 +39,5 @@ Every journey runs a scenario inside `ajs module test` with `@antelopejs/mongodb | Call `RegisterSchema` in the declaring module so the binding follows that module's database (docs/2) | Needs two modules on two databases; one container serves one database here. | | `unlockrequest` shows database expressions the encoded value of a custom two-way modifier (docs/3) | Needs an AQL expression over a modified field; the docs describe no result a journey could compare beyond the read-back it drives. | | The first codec message wording (docs/4 Validation) | Driven in `validation`: the first entry that has a message is used and `invalid ` otherwise. An entry without a message is read as reporting none, which matches the sentence. | +| `X.with(...)` keeps the fields and indexes decorated on `X` (docs/2 Inheritance, applied to the class `with` returns) | Driven by `tables/table-with-keeps-fields`, which FAILS on purpose: known finding AntelopeJS/interface-database-decorators#81. The `tables` verdict stays FAIL until #81 is fixed. | +| A raw stored empty text in the query path (`unlockrequest`) | The docs promise the fallback for texts the modifier wrote (an empty text is stored as no text); a hand-written `""` in the database is not described. Driven for the written case in `localized-query`. | diff --git a/.agents/skills/verify-interface/features/hashing.md b/.agents/skills/verify-interface/features/hashing.md index 95ca98f..0bea867 100644 --- a/.agents/skills/verify-interface/features/hashing.md +++ b/.agents/skills/verify-interface/features/hashing.md @@ -10,6 +10,7 @@ journeys: - hash-upgrade - hash-bounds - hash-memory + - hash-sync-async - hash-outdated - hash-digest --- @@ -24,6 +25,7 @@ journeys: - `hash-verify`: `testHash`, `testHashAsync`, `isHashOutdated`. - `hash-bounds`: both edges of the `ln`, `r`, `p`, salt and key ranges. - `hash-memory`: the 128 MiB limit at, under and over the line. +- `hash-sync-async`: `testHash` equals `testHashAsync` on current, legacy, low-cost, junk, empty and digest rows. - `hash-outdated`: `isHashOutdated` for a changed `p`, `r` and `ln`. - `hash-limits`: the limits a stored scrypt string must stay inside. - `hash-upgrade`: older formats verify, are flagged and are rewritten. diff --git a/.agents/skills/verify-interface/features/localization.md b/.agents/skills/verify-interface/features/localization.md index d135be2..67f5893 100644 --- a/.agents/skills/verify-interface/features/localization.md +++ b/.agents/skills/verify-interface/features/localization.md @@ -5,6 +5,7 @@ docs: - docs/3.table-modifiers.md journeys: - localize + - localized-query - localize-wildcard - localized-storage --- @@ -16,6 +17,7 @@ journeys: ## Sub-features - `localize`: switching locale, fallback, restricting fields. +- `localized-query`: `unlockrequest` with a fallback locale. - `localize-wildcard`: the `"*"` key. - `localized-storage`: one value per locale in MongoDB. diff --git a/.agents/skills/verify-interface/features/tables.md b/.agents/skills/verify-interface/features/tables.md index 9df2de7..f3678c9 100644 --- a/.agents/skills/verify-interface/features/tables.md +++ b/.agents/skills/verify-interface/features/tables.md @@ -12,6 +12,9 @@ journeys: - relation-metadata - inheritance - table-with + - table-with-subclass + - table-with-keeps-fields + - subclass-modifiers - combine-modifiers --- @@ -27,6 +30,9 @@ A table class with `Field`, `Index`, `Relation`, subclasses and `Table.with`. - `relation-metadata`: `Relation` records links and enforces none. - `inheritance`: a subclass copies its parent and never writes into it. - `combine-modifiers`: three modifiers on one table. +- `table-with-subclass`: the result of `with` extends the class it was called on. +- `table-with-keeps-fields`: known finding #81, fails until fixed. +- `subclass-modifiers`: modifier and event lists are copied per subclass. - `table-with`: mixin methods reach the subclass only. ## How to get to it (user POV) diff --git a/.agents/skills/verify-interface/runner.ts b/.agents/skills/verify-interface/runner.ts index 58752a3..37a1319 100644 --- a/.agents/skills/verify-interface/runner.ts +++ b/.agents/skills/verify-interface/runner.ts @@ -38,6 +38,10 @@ const iface = require(require.resolve("@antelopejs/interface-database", { paths: const decorate = (cls, props) => { for (const [key, list] of Object.entries(props)) for (const apply of list) apply(cls.prototype, key); return cls; }; const caught = (fn) => { try { fn(); return null; } catch (error) { return error.message; } }; const rejection = async (promise) => { try { await promise; return null; } catch (error) { return { name: error.name, message: error.message }; } }; +const mongoDriver = () => require(require.resolve("mongodb", { paths: [process.env.VERIFY_MODULE_CACHE + "/@antelopejs/mongodb"] })); +const indexNames = async (collection) => { const client = new (mongoDriver().MongoClient)(process.env.VERIFY_MONGO_URL); try { await client.connect(); return (await client.db("${DATABASE}").collection(collection).indexes()).map((index) => index.name).sort(); } finally { await client.close(); } }; +const untilIndexed = async (collection, names) => { for (let attempt = 0; attempt < 120; attempt += 1) { const have = await indexNames(collection); if (names.every((name) => have.includes(name))) return have; await new Promise((resolve) => setTimeout(resolve, 500)); } return null; }; +const refusedDuplicate = async (model, make) => { for (let attempt = 0; attempt < 60; attempt += 1) { try { const [id] = await model.insert(make(attempt)); await model.delete(id); } catch (error) { return error; } await new Promise((resolve) => setTimeout(resolve, 500)); } return null; }; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const rawTable = (schemaId, table, instance) => iface.Schema.get(schemaId).instance(instance).table(table); `; @@ -127,12 +131,12 @@ export function scenario({ title, body, printed, stored = [], api = false }: Sce equal("the scenario printed what the docs promise", line?.slice("RESULT ".length) ?? "", printed), ]); for (const read of stored) { - const result = await context.native(read.name, "mongo", [ - "mongosh", - "--quiet", - "--eval", - `print(JSON.stringify(${read.expression.replaceAll("$DB", `db.getSiblingDB("${DATABASE}")`)}))`, - ]); + const expression = `print(JSON.stringify(${read.expression.replaceAll("$DB", `db.getSiblingDB("${DATABASE}")`)}))`; + let result = await context.native(read.name, "mongo", ["mongosh", "--quiet", "--eval", expression]); + for (let again = 0; again < 20 && compact(result.stdout) !== read.expected; again += 1) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + result = await context.native(read.name, "mongo", ["mongosh", "--quiet", "--eval", expression]); + } const checks: Check[] = [equal(`MongoDB holds ${read.name}`, compact(result.stdout), read.expected)]; context.expect(result.folder, checks); } diff --git a/.agents/skills/verify-interface/scenarios/custom.ts b/.agents/skills/verify-interface/scenarios/custom.ts index 3b4624e..f8b2c02 100644 --- a/.agents/skills/verify-interface/scenarios/custom.ts +++ b/.agents/skills/verify-interface/scenarios/custom.ts @@ -71,13 +71,14 @@ const CONTAINER = `${PRELUDE} const EVENTS = `${PRELUDE} const log = []; + const shape = (args) => args.length + ":" + args.map((value) => (value !== null && typeof value === "object" ? Object.keys(value).filter((key) => key !== "_instance").sort().join("+") : String(value))).join(";"); class Spy extends d.Modifier { - fromPlainData(object, field) { log.push(["fromPlainData", field, this instanceof Spy, object.constructor.name]); } - toPlainData(object, field) { log.push(["toPlainData", field, this instanceof Spy, object.constructor.name]); } - fromDatabase(object, field) { log.push(["fromDatabase", field, this instanceof Spy, object.constructor.name]); } - toDatabase(object, field) { log.push(["toDatabase", field, this instanceof Spy, object.constructor.name]); } - insert(object, field) { log.push(["insert", field, this instanceof Spy, object.constructor.name]); } - update(object, field) { log.push(["update", field, this instanceof Spy, object.constructor.name]); } + fromPlainData(object, field, ...args) { log.push(["fromPlainData", field, this instanceof Spy, object.constructor.name, shape(args)]); } + toPlainData(object, field, ...args) { log.push(["toPlainData", field, this instanceof Spy, object.constructor.name, shape(args)]); } + fromDatabase(object, field, ...args) { log.push(["fromDatabase", field, this instanceof Spy, object.constructor.name, shape(args)]); } + toDatabase(object, field, ...args) { log.push(["toDatabase", field, this instanceof Spy, object.constructor.name, shape(args)]); args[0].handled = "by-handler"; } + insert(object, field, ...args) { log.push(["insert", field, this instanceof Spy, object.constructor.name, shape(args)]); } + update(object, field, ...args) { log.push(["update", field, this instanceof Spy, object.constructor.name, shape(args)]); } [d.MixinSymbol] = class {}; } const Spied = MakePropertyDecorator((target, propertyKey, options) => { d.attachModifier(target.constructor, Spy, propertyKey, options || {}); }); @@ -100,8 +101,9 @@ const EVENTS = `${PRELUDE} mark(); d.toPlainData(instance); const onPlain = phase("plain", mark()); + const storedByHandler = (await rawTable("events", "docs").get(id)).handled ?? "undefined"; const plainAccessor = Object.getOwnPropertyDescriptor(Doc.prototype, "note"); - return { onInsert, onUpdate, onRead, onReadAll, onPlain, eventsOnlyKeepPlainValue: plainAccessor === undefined }; + return { onInsert, onUpdate, onRead, onReadAll, onPlain, storedByHandler, eventsOnlyKeepPlainValue: plainAccessor === undefined }; `; const HELPERS = ` @@ -155,5 +157,5 @@ export const CUSTOM_JOURNEYS = { }), "modifier-helpers": scenario({ title: "getModifiedFields, unlock, lock, floating values, getMetadata and getTablesForSchema do what their TSDoc says", body: HELPERS, printed: "{\"names\":{\"hashed\":[\"password\"],\"localized\":[\"title\",\"summary\"],\"none\":[]},\"floatingRead\":\"floating\",\"floatingStored\":[\"_internal\"],\"afterUnlock\":[\"floating\",\"floating\",\"undefined\"],\"lockedKeys\":[\"title\"],\"restricted\":[\"undefined\",\"r\u00e9sum\u00e9\"],\"tables\":[[\"others\",\"profiles\"],true,\"undefined\"],\"metadata\":[\"profiles\",\"undefined\",\"profiles\",false,false,true]}" }), "container-modifier": scenario({ title: "a ContainerModifier keeps values by key and treats the wildcard key as the whole record", body: CONTAINER, printed: "{\"both\":{\"a\":1,\"b\":2},\"afterOverwrite\":{\"a\":3,\"b\":2},\"replaced\":{\"z\":26},\"byKey\":[26,\"undefined\",\"undefined\"]}" }), - "modifier-events": scenario({ title: "a modifier's event methods run for inserts, updates and conversions, and an events-only modifier leaves the field plain", body: EVENTS, printed: "{\"onInsert\":[\"fromPlainData/note/true/Doc\",\"insert/note/true/Doc\",\"toDatabase/note/true/Doc\"],\"onUpdate\":[\"fromPlainData/note/true/Doc\",\"update/note/true/Doc\",\"toDatabase/note/true/Doc\"],\"onRead\":[\"fromDatabase/note/true/Doc\"],\"onReadAll\":[\"fromDatabase/note/true/Doc\"],\"onPlain\":[\"toPlainData/note/true/Doc\"],\"eventsOnlyKeepPlainValue\":true}" }), + "modifier-events": scenario({ title: "a modifier's event methods run for inserts, updates and conversions, and an events-only modifier leaves the field plain", body: EVENTS, printed: "{\"onInsert\":[\"fromPlainData/note/true/Doc/1:note\",\"insert/note/true/Doc/0:\",\"toDatabase/note/true/Doc/1:_internal+note\"],\"onUpdate\":[\"fromPlainData/note/true/Doc/1:note\",\"update/note/true/Doc/0:\",\"toDatabase/note/true/Doc/1:_internal+note\"],\"onRead\":[\"fromDatabase/note/true/Doc/1:_id+_internal+handled+note\"],\"onReadAll\":[\"fromDatabase/note/true/Doc/1:_id+_internal+handled+note\"],\"onPlain\":[\"toPlainData/note/true/Doc/1:_id+handled+note\"],\"storedByHandler\":\"by-handler\",\"eventsOnlyKeepPlainValue\":true}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/hashing.ts b/.agents/skills/verify-interface/scenarios/hashing.ts index 1259231..f1197e8 100644 --- a/.agents/skills/verify-interface/scenarios/hashing.ts +++ b/.agents/skills/verify-interface/scenarios/hashing.ts @@ -190,6 +190,29 @@ const MEMORY = `${HASH_HELPERS} }; `; +const SYNC_ASYNC = `${HASH_HELPERS} + const seedRow = async (id, data, meta = {}) => rawTable("hash", "accounts").insert({ _id: id, name: id, _internal: { meta, data } }).run(); + await seedRow("legacy", { password: digest("sha256", "old-salt", "hunter2") }, { HashModifier: { salt: "old-salt" } }); + await seedRow("lowcost", { password: scryptString("hunter2", 10, 8, 1) }); + await seedRow("junk", { password: "plain text" }); + await seedRow("empty", {}); + await seedRow("digest", { pin: digest("sha256", "pin-salt", "hunter2") }, { HashModifier: { salt: "pin-salt" } }); + await seedRow("scryptOnDigest", { pin: scryptString("hunter2", 10, 8, 1) }); + const [fresh] = await accounts.insert({ name: "fresh", password: "hunter2", pin: "hunter2" }); + const matrix = {}; + for (const [id, field] of [[fresh, "password"], [fresh, "pin"], ["legacy", "password"], ["lowcost", "password"], ["junk", "password"], ["empty", "password"], ["digest", "pin"], ["scryptOnDigest", "pin"]]) { + const row = await accounts.get(id); + const answers = []; + for (const candidate of ["hunter2", "wrong", ""]) { + const sync = row.testHash(field, candidate); + const asynchronous = await row.testHashAsync(field, candidate); + answers.push([sync, asynchronous, sync === asynchronous]); + } + matrix[(id === fresh ? "fresh" : id) + ":" + field] = answers; + } + return matrix; +`; + const OUTDATED = `${HASH_HELPERS} const seed = async (id, password) => rawTable("hash", "accounts").insert({ _id: id, name: id, _internal: { meta: {}, data: { password } } }).run(); const probe = async (id, stored) => { await seed(id, stored); const row = await accounts.get(id); return [await row.testHashAsync("password", "pw"), row.isHashOutdated("password")]; }; @@ -221,6 +244,7 @@ export const HASH_JOURNEYS = { }), "hash-bounds": scenario({ title: "a stored scrypt string verifies at each edge of the documented ranges and is refused just outside", body: BOUNDS, printed: "{\"r1\":[true,false],\"r10\":[true,false],\"r99\":[true,false],\"r100\":[false,false],\"r0\":[false,false],\"ln0\":[false,false],\"p0\":[false,false],\"p99\":[false,false],\"ln16r1\":[false,false],\"salt65\":[false,false],\"ln1\":[true,false],\"ln2\":[true,false],\"ln99\":[false,false],\"ln100\":[false,false],\"p1\":[true,false],\"p16\":[true,false],\"salt16\":[true,false],\"salt64\":[true,false],\"salt80\":[false,false],\"salt15\":[false,false],\"key16\":[true,false],\"key64\":[true,false],\"key15\":[false,false],\"key65\":[false,false]}" }), "hash-memory": scenario({ title: "a stored scrypt string verifies up to 128 MiB of memory (128 x N x r) and is refused above it", body: MEMORY, printed: "{\"exactR64\":[true,false],\"overR65\":[false,false],\"exactR2\":[true,false],\"underR3\":[true,false],\"overR5\":[false,false],\"overR9\":[false,false]}" }), + "hash-sync-async": scenario({ title: "testHash answers exactly what testHashAsync answers, for every kind of stored value", body: SYNC_ASYNC, printed: "{\"fresh:password\":[[true,true,true],[false,false,true],[false,false,true]],\"fresh:pin\":[[true,true,true],[false,false,true],[false,false,true]],\"legacy:password\":[[true,true,true],[false,false,true],[false,false,true]],\"lowcost:password\":[[true,true,true],[false,false,true],[false,false,true]],\"junk:password\":[[false,false,true],[false,false,true],[false,false,true]],\"empty:password\":[[false,false,true],[false,false,true],[false,false,true]],\"digest:pin\":[[true,true,true],[false,false,true],[false,false,true]],\"scryptOnDigest:pin\":[[true,true,true],[false,false,true],[false,false,true]]}" }), "hash-outdated": scenario({ title: "isHashOutdated flags any scrypt string whose ln, r or p differs from the current cost", body: OUTDATED, printed: "{\"fresh\":[true,false],\"sameCost\":[true,false],\"lowerP\":[true,true],\"higherR\":[true,true],\"lowerLn\":[true,true],\"notScrypt\":[false,true]}" }), "hash-digest": scenario({ title: "a field with an algorithm stores a salted digest with the row's shared salt", body: DIGEST, printed: "{\"algorithms\":[true,true,false,88,24],\"algorithmTests\":[true,true,false],\"wrongDigest\":false,\"storedDigest\":true,\"sharedSalt\":true,\"saltIsText\":true,\"freshSaltPerRow\":true,\"saltReusedOnLaterWrite\":[true,true],\"testDigest\":[true,false,true],\"digestNeverOutdated\":false,\"scryptOnDigestField\":[true,false],\"otherDigestRefused\":false}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/localization.ts b/.agents/skills/verify-interface/scenarios/localization.ts index b8e8740..e48d932 100644 --- a/.agents/skills/verify-interface/scenarios/localization.ts +++ b/.agents/skills/verify-interface/scenarios/localization.ts @@ -109,6 +109,28 @@ const AUTO = ` }; `; +const QUERY = `${PRODUCT} + const seed = (id, name) => rawTable("loc", "products").insert({ _id: id, price: 1, _internal: { meta: {}, data: { name } } }).run(); + await seed("only-en", { en: "Hello" }); + await seed("has-de", { en: "Hi", de: "Hallo" }); + const written = new Product(); + written._id = "empty-de"; + written.localize("en").name = "Hello"; + written.localize("de").name = ""; + await d.GetModel(d.BasicDataModel(Product)).insert(written); + await seed("none", { fr: "Salut" }); + const table = rawTable("loc", "products"); + const askedFor = async (locale, text) => (await table.filter((row) => d.unlockrequest(Product, row, "name", [{ modifier: d.LocalizationModifier, args: [locale] }]).eq(text))).map((row) => row._id).sort(); + const noFallback = async (text) => (await table.filter((row) => d.unlockrequest(Product, row, "bare", [{ modifier: d.LocalizationModifier, args: ["de"] }]).eq(text))).map((row) => row._id).sort(); + return { + germanHello: await askedFor("de", "Hello"), + germanHallo: await askedFor("de", "Hallo"), + englishHello: await askedFor("en", "Hello"), + frenchSalut: await askedFor("fr", "Salut"), + frenchHello: await askedFor("fr", "Hello"), + }; +`; + const AUTO_OBJECT = ` class Article extends d.Table {} decorate(Article, { title: [d.Field("string")], creationDate: [d.CreationTime(), d.Field("date")], updateDate: [d.UpdateTime(), d.Field("date")] }); @@ -130,6 +152,7 @@ const AUTO_OBJECT = ` export const LOCALIZATION_JOURNEYS = { localize: scenario({ title: "localize switches the locale of an instance and falls back when a translation is missing or empty", body: LOCALIZE, printed: "{\"spanish\":\"Auriculares Premium\",\"germanFallsBack\":\"Premium Headphones\",\"sameObject\":[true,true,true],\"readNow\":\"Premium Headphones\",\"frenchEmpty\":\"Loud\",\"noFallback\":[\"sans repli\",\"undefined\"],\"restrictedReads\":[\"Casque Premium\",\"short\"],\"fallbackLocaleOf\":[\"en\",\"undefined\",\"undefined\",\"undefined\"]}" }), + "localized-query": scenario({ title: "a query through unlockrequest answers the requested locale and falls back to the fallback locale, an empty text included", body: QUERY, printed: "{\"germanHello\":[\"empty-de\",\"only-en\"],\"germanHallo\":[\"has-de\"],\"englishHello\":[\"empty-de\",\"only-en\"],\"frenchSalut\":[\"none\"],\"frenchHello\":[\"empty-de\",\"only-en\"]}" }), "localize-wildcard": scenario({ title: "the wildcard key reads and writes every translation at once", body: WILDCARD, printed: "{\"all\":{\"en\":\"Premium Headphones\",\"fr\":\"Casque Premium\"},\"replaced\":{\"en\":\"Premium Headphones\",\"fr\":\"Casque Premium\",\"de\":\"Premium-Kopfhoerer\"},\"german\":\"Premium-Kopfhoerer\",\"stored\":[\"Bonjour\",\"Hello\",{\"en\":\"Hello\",\"fr\":\"Bonjour\"}]}" }), "localized-storage": scenario({ title: "a localized field is stored as one value per locale", diff --git a/.agents/skills/verify-interface/scenarios/tables.ts b/.agents/skills/verify-interface/scenarios/tables.ts index 9984236..ba9f3d6 100644 --- a/.agents/skills/verify-interface/scenarios/tables.ts +++ b/.agents/skills/verify-interface/scenarios/tables.ts @@ -18,16 +18,16 @@ const INDEX_OPTIONS = ` const model = d.GetModel(Model); const other = d.GetModel(Model, "tenant-2"); await model.insert({ email: "a@x", first: "Ada", last: "Lovelace", age: 36 }); - const duplicate = await rejection(model.insert({ email: "a@x", first: "Bob", last: "Brown", age: 40 })); - let recognised = null; - try { await model.insert({ email: "a@x", first: "Cy", last: "Dee", age: 41 }); } catch (error) { recognised = [iface.IsDuplicateKeyError(error), iface.IsDuplicateKeyError(new Error("other"))]; } + const refused = await refusedDuplicate(model, (attempt) => ({ email: "a@x", first: "Bob", last: "Brown", age: 40 + attempt })); + const duplicate = refused && { name: refused.name, message: refused.message }; + const recognised = [iface.IsDuplicateKeyError(refused), iface.IsDuplicateKeyError(new Error("other"))]; const otherInstance = await other.insert({ email: "a@x", first: "Ada", last: "Lovelace", age: 36 }); const byGroup = (await model.getBy("name", "Ada", "Lovelace")).map((row) => row.email); const bySingle = (await model.getBy("age", 36)).map((row) => row.first); const between = (await rawTable("idx", "users").between("age", 30, 38)).map((row) => row.first); const symbolKey = caught(() => d.Index()({}, Symbol("k"))); - await sleep(2500); - return { definition: definition.users, duplicate, recognised, otherInstanceStored: otherInstance.length, byGroup, bySingle, between, symbolKey }; + const built = await untilIndexed("idx__users", ["email__i", "name__i", "age__i", "name"]); + return { indexed: built !== null, definition: definition.users, duplicate, recognised, otherInstanceStored: otherInstance.length, byGroup, bySingle, between, symbolKey }; `; const GROUP_FLAGS = ` @@ -49,7 +49,8 @@ const GROUP_FLAGS = ` await d.RegisterSchema("grp"); const model = d.GetModel(d.BasicDataModel(Pair)); await model.insert({ a: "1", b: "x", c: "p", e: "q" }); - const sameKey = await rejection(model.insert({ a: "1", b: "x", c: "r", e: "s" })); + const refusedKey = await refusedDuplicate(model, (attempt) => ({ a: "1", b: "x", c: "r" + attempt, e: "s" })); + const sameKey = refusedKey && { name: refusedKey.name, message: refusedKey.message }; const otherSecond = await model.insert({ a: "1", b: "y", c: "p", e: "q" }); return { indexes: iface.Schema.get("grp").definition.pairs.indexes, sameKey, otherSecond: otherSecond.length }; `; @@ -133,6 +134,67 @@ const INHERITANCE = ` return { built: [built.subs.indexes.ab, built.crosses.indexes.ab], base: meta(Base), person: meta(Person), employee: meta(Employee), contractor: meta(Contractor), cross: meta(Cross), sub: meta(Sub) }; `; +const WITH_SUBCLASS = ` + class User extends d.Table { + greet() { return "hi " + this.name; } + } + decorate(User, { name: [d.Field("string")] }); + class Admin extends User.with(d.HashModifier) {} + decorate(Admin, { password: [d.Hashed(), d.Field("string")] }); + d.RegisterTable("admins", "withsub")(Admin); + await d.RegisterSchema("withsub"); + const admin = new Admin(); + admin.name = "Root"; + const model = d.GetModel(d.BasicDataModel(Admin)); + const [id] = await model.insert({ name: "Root", password: "pw" }); + const row = await model.get(id); + return { + isUser: [admin instanceof User, admin instanceof d.Table, Object.getPrototypeOf(User.with(d.HashModifier)) === User], + methods: [admin.greet(), typeof admin.testHash, typeof admin.testHashAsync, typeof admin.isHashOutdated, typeof User.prototype.testHash], + stored: [row.greet(), await row.testHashAsync("password", "pw")], + }; +`; + +const WITH_FIELDS = ` + class User extends d.Table {} + decorate(User, { name: [d.Field("string")], email: [d.Field("string"), d.Index()] }); + class Admin extends User.with(d.HashModifier) {} + decorate(Admin, { password: [d.Hashed(), d.Field("string")] }); + d.RegisterTable("admins", "withfields")(Admin); + await d.RegisterSchema("withfields"); + const definition = iface.Schema.get("withfields").definition.admins; + return { fields: Object.keys(d.getMetadata(Admin, d.DatumStaticMetadata).fields).sort(), built: Object.keys(definition.fields).sort(), indexes: Object.keys(definition.indexes) }; +`; + +const SUBCLASS_MODIFIERS = ` + class Parent extends d.Table.with(d.HashModifier) {} + decorate(Parent, { name: [d.Field("string")], born: [d.CreationTime(), d.Field("date")] }); + d.RegisterTable("parents", "submod")(Parent); + class Child extends Parent {} + decorate(Child, { extra: [d.CreationTime(), d.Field("date")], secret: [d.Hashed(), d.Field("string")] }); + d.RegisterTable("children", "submod")(Child); + await d.RegisterSchema("submod"); + const parentRow = new Parent(); + const childRow = new Child(); + const parents = d.GetModel(d.BasicDataModel(Parent)); + const children = d.GetModel(d.BasicDataModel(Child)); + const [parentId] = await parents.insert({ name: "p" }); + const [childId] = await children.insert({ name: "c", secret: "s" }); + const parentStored = await rawTable("submod", "parents").get(parentId); + const childStored = await rawTable("submod", "children").get(childId); + const meta = d.getMetadata(Parent, d.DatumStaticMetadata); + class Fixtured extends Parent {} + d.Fixture(() => [{ name: "only the child" }])(Fixtured); + return { + parentModified: [d.getModifiedFields(parentRow, d.HashModifier), d.getModifiedFields(parentRow, d.AutoDateModifier)], + childModified: [d.getModifiedFields(childRow, d.HashModifier), d.getModifiedFields(childRow, d.AutoDateModifier)], + parentStored: [Object.keys(parentStored).filter((key) => ["born", "extra"].includes(key)).sort(), parentStored._internal === undefined || Object.keys(parentStored._internal.data ?? {})], + childStored: [Object.keys(childStored).filter((key) => ["born", "extra"].includes(key)).sort(), Object.keys(childStored._internal.data)], + parentFixture: meta.generator === undefined, + childSeesParentFields: Object.keys(d.getMetadata(Child, d.DatumStaticMetadata).fields), + }; +`; + const WITH = ` class Plain extends d.Table {} class Hashing extends d.Table.with(d.HashModifier) {} @@ -181,7 +243,7 @@ export const TABLE_JOURNEYS = { "index-options": scenario({ title: "Field and Index reach the database as fields and plain, compound, cross-instance and unique indexes", body: INDEX_OPTIONS, - printed: "{\"definition\":{\"fields\":{\"email\":\"string\",\"first\":\"string\",\"last\":\"string\",\"age\":\"number\"},\"indexes\":{\"email\":{\"fields\":null,\"unique\":true},\"name\":{\"fields\":[\"first\",\"last\"],\"crossInstance\":true},\"age\":{\"fields\":null}}},\"duplicate\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"email\\\" (email)\"},\"recognised\":[true,false],\"otherInstanceStored\":1,\"byGroup\":[\"a@x\"],\"bySingle\":[\"Ada\"],\"between\":[\"Ada\"],\"symbolKey\":\"@Index on Symbol(k): a database index names a field, never a symbol\"}", + printed: "{\"indexed\":true,\"definition\":{\"fields\":{\"email\":\"string\",\"first\":\"string\",\"last\":\"string\",\"age\":\"number\"},\"indexes\":{\"email\":{\"fields\":null,\"unique\":true},\"name\":{\"fields\":[\"first\",\"last\"],\"crossInstance\":true},\"age\":{\"fields\":null}}},\"duplicate\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"email\\\" (email)\"},\"recognised\":[true,false],\"otherInstanceStored\":1,\"byGroup\":[\"a@x\"],\"bySingle\":[\"Ada\"],\"between\":[\"Ada\"],\"symbolKey\":\"@Index on Symbol(k): a database index names a field, never a symbol\"}", stored: [{ name: "the indexes of the users collection", expression: "$DB.getCollection('idx__users').getIndexes().map((i) => [i.name, i.unique === true]).sort()", expected: "[[\"_id_\",false],[\"age__i\",false],[\"email__i\",true],[\"name\",false],[\"name__i\",false]]" }], }), "group-flags": scenario({ title: "unique and crossInstance on any field of a group apply to the whole group", body: GROUP_FLAGS, printed: "{\"indexes\":{\"ab\":{\"fields\":[\"a\",\"b\"],\"unique\":true},\"cd\":{\"fields\":[\"c\",\"e\"],\"crossInstance\":true},\"ef\":{\"fields\":[\"f\",\"g\",\"h\"],\"crossInstance\":true},\"ij\":{\"fields\":[\"i\",\"j\"],\"unique\":true},\"plain\":{\"fields\":[\"k\",\"l\"]}},\"sameKey\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"ab\\\" (a, b)\"},\"otherSecond\":1}" }), @@ -189,5 +251,12 @@ export const TABLE_JOURNEYS = { "relation-metadata": scenario({ title: "Relation records links without enforcing them", body: RELATION, printed: "{\"relationFields\":[\"authorId\",\"tagIds\"],\"authorTarget\":true,\"authorToField\":null,\"tagMany\":true,\"tagToField\":\"name\",\"dangling\":[\"nobody\",[\"ghost\"]],\"fieldsKept\":[\"authorId\",\"tagIds\",\"title\"],\"childRelations\":[\"authorId\",\"tagIds\",\"editorId\"],\"parentRelations\":[\"authorId\",\"tagIds\"]}" }), inheritance: scenario({ title: "a table subclass starts with a copy of its parent and never changes it", body: INHERITANCE, printed: "{\"built\":[{\"fields\":[\"a\",\"b\"],\"crossInstance\":true,\"unique\":true},{\"fields\":[\"a\",\"b\"],\"crossInstance\":true,\"unique\":true}],\"base\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\"],\"indexes\":[],\"unique\":[],\"cross\":[]},\"person\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\"],\"indexes\":[\"email\"],\"unique\":[\"email\"],\"cross\":[]},\"employee\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\",\"badge\"],\"indexes\":[\"email\",\"badge\"],\"unique\":[\"email\"],\"cross\":[]},\"contractor\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"rate\"],\"indexes\":[],\"unique\":[],\"cross\":[]},\"cross\":{\"table\":\"crosses\",\"schema\":\"inh\",\"fields\":[\"a\",\"b\",\"c\"],\"indexes\":[\"ab\",\"c\"],\"unique\":[\"ab\"],\"cross\":[\"ab\"]},\"sub\":{\"table\":\"subs\",\"schema\":\"inh\",\"fields\":[\"a\",\"b\",\"c\",\"extra\"],\"indexes\":[\"ab\",\"c\"],\"unique\":[\"ab\"],\"cross\":[\"ab\"]}}" }), "combine-modifiers": scenario({ title: "Table.with combines the encryption, hashing and localization modifiers on one table", body: COMBINE, printed: "{\"stored\":[\"bio\",\"password\",\"privateInfo\"],\"privateInfo\":{\"address\":\"1 Main St\",\"phoneNumber\":\"555\"},\"password\":[true,false],\"bio\":[\"Bonjour\",\"Hello\"],\"mixins\":[\"function\",\"function\"]}" }), + "table-with-subclass": scenario({ title: "X.with(...) returns a subclass of X that keeps X's fields and methods and gains the mixin's", body: WITH_SUBCLASS, printed: "{\"isUser\":[true,true,true],\"methods\":[\"hi Root\",\"function\",\"function\",\"function\",\"undefined\"],\"stored\":[\"hi Root\",true]}" }), + "table-with-keeps-fields": scenario({ + title: "KNOWN FINDING AntelopeJS/interface-database-decorators#81, fails until fixed: the subclass X.with(...) returns keeps the fields and indexes decorated on X", + body: WITH_FIELDS, + printed: "{\"fields\":[\"email\",\"name\",\"password\"],\"built\":[\"email\",\"name\",\"password\"],\"indexes\":[\"email\"]}", + }), + "subclass-modifiers": scenario({ title: "a subclass's modifier and event lists never reach its parent", body: SUBCLASS_MODIFIERS, printed: "{\"parentModified\":[[],[]],\"childModified\":[[\"secret\"],[]],\"parentStored\":[[\"born\"],[]],\"childStored\":[[\"born\",\"extra\"],[\"secret\"]],\"parentFixture\":true,\"childSeesParentFields\":[\"name\",\"born\",\"extra\",\"secret\"]}" }), "table-with": scenario({ title: "Table.with adds the mixin methods of the modifiers to a subclass", body: WITH, printed: "{\"plainHasTestHash\":false,\"hashingHas\":[true,true,true,false],\"bothHas\":[true,true],\"isTableSubclass\":true,\"tableItselfUntouched\":\"undefined\",\"subIsASubclass\":true}" }), } satisfies { [id: string]: Journey }; From 4326e871dd4b42d6d237dbcb98d2efec7741d9c4 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Wed, 4 Nov 2026 01:39:45 +0100 Subject: [PATCH 30/45] feat(skills): pin metadata classes, the aead auth tag, container queries and the no-fallback query --- .../features/custom-modifiers.md | 6 ++ .../verify-interface/scenarios/custom.ts | 74 ++++++++++++++++++- .../verify-interface/scenarios/encryption.ts | 14 ++-- .../scenarios/localization.ts | 13 ++-- 4 files changed, 96 insertions(+), 11 deletions(-) diff --git a/.agents/skills/verify-interface/features/custom-modifiers.md b/.agents/skills/verify-interface/features/custom-modifiers.md index 8505454..ba6e499 100644 --- a/.agents/skills/verify-interface/features/custom-modifiers.md +++ b/.agents/skills/verify-interface/features/custom-modifiers.md @@ -9,6 +9,9 @@ journeys: - one-way-modifier - two-way-modifier - container-modifier + - container-query + - metadata-class + - legacy-metadata - modifier-helpers - modifier-events --- @@ -22,6 +25,9 @@ journeys: - `one-way-modifier`: transform on write; a second one-way modifier on a field is refused. - `two-way-modifier`: transform on write, unlock on read. - `modifier-helpers`: the exported helpers of the modifier machinery. +- `container-query`: the key used in a query. +- `metadata-class`: `inherit` and `upgrade` of a published `MetadataClass`. +- `legacy-metadata`: metadata stored by an older copy of the package. - `container-modifier`: values by key and the wildcard. - `modifier-events`: event order for insert, update and reads; events-only modifiers add no accessor. diff --git a/.agents/skills/verify-interface/scenarios/custom.ts b/.agents/skills/verify-interface/scenarios/custom.ts index f8b2c02..8de4cfe 100644 --- a/.agents/skills/verify-interface/scenarios/custom.ts +++ b/.agents/skills/verify-interface/scenarios/custom.ts @@ -69,6 +69,70 @@ const CONTAINER = `${PRELUDE} return { both, afterOverwrite, replaced, byKey: [doc.pick("z").values, doc.pick("missing").values ?? "undefined", doc.pick("constructor").values ?? "undefined"] }; `; +const CONTAINER_QUERY = `${PRELUDE} + class PickModifier extends d.ContainerModifier { + [d.MixinSymbol] = class {}; + } + const Picked = MakePropertyDecorator((target, propertyKey, options) => { d.attachModifier(target.constructor, PickModifier, propertyKey, options || {}); }); + class Doc extends d.Table.with(PickModifier) {} + decorate(Doc, { values: [Picked(), d.Field("any")] }); + d.RegisterTable("docs", "ctquery")(Doc); + await d.RegisterSchema("ctquery"); + const seed = (id, values) => rawTable("ctquery", "docs").insert({ _id: id, _internal: { meta: {}, data: { values } } }).run(); + await seed("one", { a: 1, b: 2 }); + await seed("two", { a: 2, b: 1 }); + await seed("only-b", { b: 1 }); + const table = rawTable("ctquery", "docs"); + const where = async (key, value) => (await table.filter((row) => d.unlockrequest(Doc, row, "values", [{ modifier: PickModifier, args: [key] }]).eq(value))).map((row) => row._id).sort(); + return { a1: await where("a", 1), a2: await where("a", 2), b1: await where("b", 1), b2: await where("b", 2), c1: await where("c", 1) }; +`; + +const METADATA = `${PRELUDE} + const share = { key: Symbol("shared") }; + class Shared { constructor() { this.list = []; } } + Shared.key = share.key; + class Copied { constructor() { this.list = []; } static inherit(parent) { const copy = new Copied(); copy.list = [...parent.list]; return copy; } } + Copied.key = Symbol("copied"); + class A {} + class B extends A {} + d.getMetadata(A, Shared).list.push("x"); + d.getMetadata(A, Copied).list.push("x"); + const sharedB = d.getMetadata(B, Shared); + const copiedB = d.getMetadata(B, Copied); + copiedB.list.push("y"); + const upgraded = { upgrade: (stored) => (stored.version ? stored : Object.assign(new Copied(), stored, { version: 2 })) }; + class Aged { constructor() { this.list = []; } static upgrade(stored) { return Object.hasOwn(stored, "version") ? stored : Object.assign(new Aged(), stored, { version: 2 }); } } + Aged.key = Symbol("aged"); + class Old {} + Reflect.defineMetadata(Aged.key, { list: ["legacy"] }, Old); + const first = d.getMetadata(Old, Aged); + const second = d.getMetadata(Old, Aged); + return { + shared: [sharedB === d.getMetadata(A, Shared), sharedB.list], + copied: [copiedB === d.getMetadata(A, Copied), copiedB.list, d.getMetadata(A, Copied).list], + upgrade: [first instanceof Aged, first.version, first.list, first === second], + }; +`; + +const LEGACY_METADATA = ` + class Old {} + Reflect.defineMetadata(d.DatumStaticMetadata.key, { schemaName: "legacy", tableName: "olds", indexes: {}, crossInstanceIndexes: new Set(), fields: {}, primary: "_id" }, Old); + const upgraded = d.getMetadata(Old, d.DatumStaticMetadata); + const again = d.getMetadata(Old, d.DatumStaticMetadata); + d.Field("string")(Old.prototype, "code"); + d.Index({ unique: true })(Old.prototype, "code"); + d.RegisterTable("olds", "legacy")(Old); + await d.RegisterSchema("legacy"); + const model = d.GetModel(d.BasicDataModel(Old)); + await model.insert({ code: "A" }); + const refused = await refusedDuplicate(model, (attempt) => ({ code: "A" })); + return { + upgraded: [upgraded instanceof d.DatumStaticMetadata, upgraded.uniqueIndexes instanceof Set, upgraded === again, upgraded.tableName], + definition: iface.Schema.get("legacy").definition.olds.indexes, + uniqueEnforced: refused && refused.name, + }; +`; + const EVENTS = `${PRELUDE} const log = []; const shape = (args) => args.length + ":" + args.map((value) => (value !== null && typeof value === "object" ? Object.keys(value).filter((key) => key !== "_instance").sort().join("+") : String(value))).join(";"); @@ -130,6 +194,10 @@ const HELPERS = ` d.unlock(row, d.LocalizationModifier, ["title"], "en"); const restricted = [row.title ?? "undefined", row.summary ?? "undefined"]; const tables = d.getTablesForSchema("helpers"); + const bound = d.getMetadata(Profile, d.DatumStaticMetadata); + class Early extends Profile {} + const earlyBefore = d.getMetadata(Early, d.DatumStaticMetadata); + const copiedArrays = [earlyBefore.indexes !== bound.indexes, earlyBefore.fields !== bound.fields, earlyBefore.primary, earlyBefore.schema === undefined]; const parentMeta = d.getMetadata(Profile, d.DatumStaticMetadata); class Child extends Profile {} const own = d.getMetadata(Child, d.DatumStaticMetadata, false); @@ -142,6 +210,7 @@ const HELPERS = ` afterUnlock, lockedKeys, restricted, + copiedArrays, tables: [Object.keys(tables).sort(), tables.profiles === Profile, d.getTablesForSchema("nothing-here") ?? "undefined"], metadata: [parentMeta.tableName, own.tableName ?? "undefined", inherited.tableName, own === inherited, inherited === parentMeta, d.getMetadata(Profile, d.DatumStaticMetadata) === parentMeta], }; @@ -155,7 +224,10 @@ export const CUSTOM_JOURNEYS = { printed: "{\"read\":[{\"a\":[1,2]},\"plain\"],\"storedBody\":\"b64-eyJhIjpbMSwyXX0=\",\"storedTagged\":\"x:InBsYWluIg==\",\"notAString\":\"undefined\"}", stored: [{ name: "the stored encodings", expression: "$DB.getCollection('twoway__docs').find({ title: 't' }).toArray().map((r) => [r._internal.data.body, r._internal.data.tagged])", expected: "[[\"b64-eyJhIjpbMSwyXX0=\",\"x:InBsYWluIg==\"]]" }], }), - "modifier-helpers": scenario({ title: "getModifiedFields, unlock, lock, floating values, getMetadata and getTablesForSchema do what their TSDoc says", body: HELPERS, printed: "{\"names\":{\"hashed\":[\"password\"],\"localized\":[\"title\",\"summary\"],\"none\":[]},\"floatingRead\":\"floating\",\"floatingStored\":[\"_internal\"],\"afterUnlock\":[\"floating\",\"floating\",\"undefined\"],\"lockedKeys\":[\"title\"],\"restricted\":[\"undefined\",\"r\u00e9sum\u00e9\"],\"tables\":[[\"others\",\"profiles\"],true,\"undefined\"],\"metadata\":[\"profiles\",\"undefined\",\"profiles\",false,false,true]}" }), + "modifier-helpers": scenario({ title: "getModifiedFields, unlock, lock, floating values, getMetadata and getTablesForSchema do what their TSDoc says", body: HELPERS, printed: "{\"names\":{\"hashed\":[\"password\"],\"localized\":[\"title\",\"summary\"],\"none\":[]},\"floatingRead\":\"floating\",\"floatingStored\":[\"_internal\"],\"afterUnlock\":[\"floating\",\"floating\",\"undefined\"],\"lockedKeys\":[\"title\"],\"restricted\":[\"undefined\",\"r\u00e9sum\u00e9\"],\"copiedArrays\":[true,true,\"_id\",true],\"tables\":[[\"others\",\"profiles\"],true,\"undefined\"],\"metadata\":[\"profiles\",\"undefined\",\"profiles\",false,false,true]}" }), + "container-query": scenario({ title: "ContainerModifier.unlockrequest uses its key, so a query reads one entry of the stored record", body: CONTAINER_QUERY, printed: "{\"a1\":[\"one\"],\"a2\":[\"two\"],\"b1\":[\"only-b\",\"two\"],\"b2\":[\"one\"],\"c1\":[]}" }), + "metadata-class": scenario({ title: "a metadata class without inherit is shared with the subclass, one with inherit is copied, and upgrade runs on stored metadata", body: METADATA, printed: "{\"shared\":[true,[\"x\"]],\"copied\":[false,[\"x\",\"y\"],[\"x\"]],\"upgrade\":[true,2,[\"legacy\"],true]}" }), + "legacy-metadata": scenario({ title: "metadata stored without uniqueIndexes is upgraded and unique indexes still register", body: LEGACY_METADATA, printed: "{\"upgraded\":[true,true,true,\"olds\"],\"definition\":{\"code\":{\"fields\":null,\"unique\":true}},\"uniqueEnforced\":\"DuplicateKeyError\"}" }), "container-modifier": scenario({ title: "a ContainerModifier keeps values by key and treats the wildcard key as the whole record", body: CONTAINER, printed: "{\"both\":{\"a\":1,\"b\":2},\"afterOverwrite\":{\"a\":3,\"b\":2},\"replaced\":{\"z\":26},\"byKey\":[26,\"undefined\",\"undefined\"]}" }), "modifier-events": scenario({ title: "a modifier's event methods run for inserts, updates and conversions, and an events-only modifier leaves the field plain", body: EVENTS, printed: "{\"onInsert\":[\"fromPlainData/note/true/Doc/1:note\",\"insert/note/true/Doc/0:\",\"toDatabase/note/true/Doc/1:_internal+note\"],\"onUpdate\":[\"fromPlainData/note/true/Doc/1:note\",\"update/note/true/Doc/0:\",\"toDatabase/note/true/Doc/1:_internal+note\"],\"onRead\":[\"fromDatabase/note/true/Doc/1:_id+_internal+handled+note\"],\"onReadAll\":[\"fromDatabase/note/true/Doc/1:_id+_internal+handled+note\"],\"onPlain\":[\"toPlainData/note/true/Doc/1:_id+handled+note\"],\"storedByHandler\":\"by-handler\",\"eventsOnlyKeepPlainValue\":true}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/encryption.ts b/.agents/skills/verify-interface/scenarios/encryption.ts index 04513a8..a28ebe0 100644 --- a/.agents/skills/verify-interface/scenarios/encryption.ts +++ b/.agents/skills/verify-interface/scenarios/encryption.ts @@ -7,7 +7,7 @@ const KEY = "12345678901234567890123456789012"; /** Decrypts a stored `[ciphertext, iv, authTag]` triple with Node's crypto alone: the scenario's second view of the cipher. */ const DECRYPT = ` const decrypt = (triple, algorithm = "aes-256-gcm", key = "${KEY}") => { - const decipher = crypto.createDecipheriv(algorithm, key, Buffer.from(triple[1], "base64")); + const decipher = crypto.createDecipheriv(algorithm, key, Buffer.from(triple[1], "base64"), triple[2] ? { authTagLength: 16 } : undefined); if (triple[2]) decipher.setAuthTag(Buffer.from(triple[2], "base64")); return Buffer.concat([decipher.update(Buffer.from(triple[0], "base64")), decipher.final()]).toString("utf8"); }; @@ -21,12 +21,13 @@ const AT_REST = `${DECRYPT} personal: [d.Encrypted({ secretKey: "${KEY}" }), d.Field("any")], legacy: [d.Encrypted({ secretKey: "${KEY}", algorithm: "aes-256-cbc" }), d.Field("string")], short: [d.Encrypted({ secretKey: "${KEY}", ivSize: 12 }), d.Field("string")], + aead: [d.Encrypted({ secretKey: "${KEY}", algorithm: "chacha20-poly1305", ivSize: 12 }), d.Field("string")], }); d.RegisterTable("vaults", "enc")(Vault); await d.RegisterSchema("enc"); const model = d.GetModel(d.BasicDataModel(Vault)); const personal = { ssn: "078-05-1120", birthDate: "1990-01-01" }; - const [id] = await model.insert({ owner: "Ann", card: "4242 4242 4242 4242", personal, legacy: "old cipher", short: "twelve" }); + const [id] = await model.insert({ owner: "Ann", card: "4242 4242 4242 4242", personal, legacy: "old cipher", short: "twelve", aead: "hello" }); const [again] = await model.insert({ owner: "Bea", card: "4242 4242 4242 4242" }); const read = await model.get(id); const raw = await rawTable("enc", "vaults").get(id); @@ -34,11 +35,14 @@ const AT_REST = `${DECRYPT} const data = raw._internal.data; const shape = (triple) => [Array.isArray(triple), triple.length, typeof triple[2]]; return { - read: [read.card, read.personal, read.legacy, read.short], + read: [read.card, read.personal, read.legacy, read.short, read.aead], topLevelHoldsNoCiphertext: raw.card === undefined && raw.personal === undefined, gcmShape: shape(data.card), cbcShape: [data.legacy.length, data.legacy[2] ?? null], ivBytes: [Buffer.from(data.card[1], "base64").length, Buffer.from(data.short[1], "base64").length, Buffer.from(data.personal[1], "base64").length, Buffer.from(data.legacy[1], "base64").length], + aeadShape: [data.aead.length, typeof data.aead[2], Buffer.from(data.aead[2], "base64").length, Buffer.from(data.aead[1], "base64").length], + aeadDecrypt: decrypt(data.aead, "chacha20-poly1305"), + aeadTagChecked: (() => { const bad = [...data.aead]; bad[2] = Buffer.alloc(16).toString("base64"); try { decrypt(bad, "chacha20-poly1305"); return "decrypted"; } catch { return "refused"; } })(), plaintextAbsent: !JSON.stringify(raw).includes("4242") && !JSON.stringify(raw).includes("078-05-1120"), independentDecrypt: [decrypt(data.card), decrypt(data.personal), decrypt(data.legacy, "aes-256-cbc")], sameValueTwice: [data.card[1] !== rawAgain._internal.data.card[1], data.card[0] !== rawAgain._internal.data.card[0]], @@ -91,12 +95,12 @@ export const ENCRYPTION_JOURNEYS = { "encryption-at-rest": scenario({ title: "an encrypted field is stored as base64 ciphertext, iv and auth tag that only the key opens", body: AT_REST, - printed: "{\"read\":[\"4242 4242 4242 4242\",{\"ssn\":\"078-05-1120\",\"birthDate\":\"1990-01-01\"},\"old cipher\",\"twelve\"],\"topLevelHoldsNoCiphertext\":true,\"gcmShape\":[true,3,\"string\"],\"cbcShape\":[3,null],\"ivBytes\":[16,12,16,16],\"plaintextAbsent\":true,\"independentDecrypt\":[\"\\\"4242 4242 4242 4242\\\"\",\"{\\\"ssn\\\":\\\"078-05-1120\\\",\\\"birthDate\\\":\\\"1990-01-01\\\"}\",\"\\\"old cipher\\\"\"],\"sameValueTwice\":[true,true]}", + printed: "{\"read\":[\"4242 4242 4242 4242\",{\"ssn\":\"078-05-1120\",\"birthDate\":\"1990-01-01\"},\"old cipher\",\"twelve\",\"hello\"],\"topLevelHoldsNoCiphertext\":true,\"gcmShape\":[true,3,\"string\"],\"cbcShape\":[3,null],\"ivBytes\":[16,12,16,16],\"aeadShape\":[3,\"string\",16,12],\"aeadDecrypt\":\"\\\"hello\\\"\",\"aeadTagChecked\":\"refused\",\"plaintextAbsent\":true,\"independentDecrypt\":[\"\\\"4242 4242 4242 4242\\\"\",\"{\\\"ssn\\\":\\\"078-05-1120\\\",\\\"birthDate\\\":\\\"1990-01-01\\\"}\",\"\\\"old cipher\\\"\"],\"sameValueTwice\":[true,true]}", stored: [ { name: "the stored triples", expression: "$DB.getCollection('enc__vaults').find({ owner: 'Ann' }).toArray().map((r) => [Object.keys(r._internal.data).sort(), r._internal.data.card.length, r._internal.data.legacy.length, r.card === undefined])", - expected: "[[[\"card\",\"legacy\",\"personal\",\"short\"],3,3,true]]", + expected: "[[[\"aead\",\"card\",\"legacy\",\"personal\",\"short\"],3,3,true]]", }, ], }), diff --git a/.agents/skills/verify-interface/scenarios/localization.ts b/.agents/skills/verify-interface/scenarios/localization.ts index e48d932..e446e9f 100644 --- a/.agents/skills/verify-interface/scenarios/localization.ts +++ b/.agents/skills/verify-interface/scenarios/localization.ts @@ -110,9 +110,9 @@ const AUTO = ` `; const QUERY = `${PRODUCT} - const seed = (id, name) => rawTable("loc", "products").insert({ _id: id, price: 1, _internal: { meta: {}, data: { name } } }).run(); - await seed("only-en", { en: "Hello" }); - await seed("has-de", { en: "Hi", de: "Hallo" }); + const seed = (id, name, bare) => rawTable("loc", "products").insert({ _id: id, price: 1, _internal: { meta: {}, data: bare ? { name, bare } : { name } } }).run(); + await seed("only-en", { en: "Hello" }, { en: "Hello" }); + await seed("has-de", { en: "Hi", de: "Hallo" }, { en: "Hi", de: "Hallo" }); const written = new Product(); written._id = "empty-de"; written.localize("en").name = "Hello"; @@ -121,13 +121,16 @@ const QUERY = `${PRODUCT} await seed("none", { fr: "Salut" }); const table = rawTable("loc", "products"); const askedFor = async (locale, text) => (await table.filter((row) => d.unlockrequest(Product, row, "name", [{ modifier: d.LocalizationModifier, args: [locale] }]).eq(text))).map((row) => row._id).sort(); - const noFallback = async (text) => (await table.filter((row) => d.unlockrequest(Product, row, "bare", [{ modifier: d.LocalizationModifier, args: ["de"] }]).eq(text))).map((row) => row._id).sort(); + const noFallback = async (locale, text) => (await table.filter((row) => d.unlockrequest(Product, row, "bare", [{ modifier: d.LocalizationModifier, args: [locale] }]).eq(text))).map((row) => row._id).sort(); return { germanHello: await askedFor("de", "Hello"), germanHallo: await askedFor("de", "Hallo"), englishHello: await askedFor("en", "Hello"), frenchSalut: await askedFor("fr", "Salut"), frenchHello: await askedFor("fr", "Hello"), + noFallbackGerman: await noFallback("de", "Hello"), + noFallbackEnglish: await noFallback("en", "Hello"), + noFallbackOwnText: await noFallback("de", "Hallo"), }; `; @@ -152,7 +155,7 @@ const AUTO_OBJECT = ` export const LOCALIZATION_JOURNEYS = { localize: scenario({ title: "localize switches the locale of an instance and falls back when a translation is missing or empty", body: LOCALIZE, printed: "{\"spanish\":\"Auriculares Premium\",\"germanFallsBack\":\"Premium Headphones\",\"sameObject\":[true,true,true],\"readNow\":\"Premium Headphones\",\"frenchEmpty\":\"Loud\",\"noFallback\":[\"sans repli\",\"undefined\"],\"restrictedReads\":[\"Casque Premium\",\"short\"],\"fallbackLocaleOf\":[\"en\",\"undefined\",\"undefined\",\"undefined\"]}" }), - "localized-query": scenario({ title: "a query through unlockrequest answers the requested locale and falls back to the fallback locale, an empty text included", body: QUERY, printed: "{\"germanHello\":[\"empty-de\",\"only-en\"],\"germanHallo\":[\"has-de\"],\"englishHello\":[\"empty-de\",\"only-en\"],\"frenchSalut\":[\"none\"],\"frenchHello\":[\"empty-de\",\"only-en\"]}" }), + "localized-query": scenario({ title: "a query through unlockrequest answers the requested locale and falls back to the fallback locale, an empty text included", body: QUERY, printed: "{\"germanHello\":[\"empty-de\",\"only-en\"],\"germanHallo\":[\"has-de\"],\"englishHello\":[\"empty-de\",\"only-en\"],\"frenchSalut\":[\"none\"],\"frenchHello\":[\"empty-de\",\"only-en\"],\"noFallbackGerman\":[],\"noFallbackEnglish\":[\"only-en\"],\"noFallbackOwnText\":[\"has-de\"]}" }), "localize-wildcard": scenario({ title: "the wildcard key reads and writes every translation at once", body: WILDCARD, printed: "{\"all\":{\"en\":\"Premium Headphones\",\"fr\":\"Casque Premium\"},\"replaced\":{\"en\":\"Premium Headphones\",\"fr\":\"Casque Premium\",\"de\":\"Premium-Kopfhoerer\"},\"german\":\"Premium-Kopfhoerer\",\"stored\":[\"Bonjour\",\"Hello\",{\"en\":\"Hello\",\"fr\":\"Bonjour\"}]}" }), "localized-storage": scenario({ title: "a localized field is stored as one value per locale", From 12482a12b55468b2ab0f5e0702e2b0cde21b43d1 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Wed, 4 Nov 2026 02:39:08 +0100 Subject: [PATCH 31/45] feat(skills): pin parameter types, wildcard replacement and metadata identity --- .../verify-interface/features/declarations.md | 2 + .../verify-interface/features/localization.md | 2 + .../verify-interface/scenarios/custom.ts | 12 ++- .../scenarios/declarations.ts | 93 +++++++++++++++++++ .../verify-interface/scenarios/hashing.ts | 9 +- .../scenarios/localization.ts | 22 +++++ 6 files changed, 133 insertions(+), 7 deletions(-) diff --git a/.agents/skills/verify-interface/features/declarations.md b/.agents/skills/verify-interface/features/declarations.md index 1f7c2d6..b3a35ac 100644 --- a/.agents/skills/verify-interface/features/declarations.md +++ b/.agents/skills/verify-interface/features/declarations.md @@ -7,6 +7,7 @@ docs: journeys: - model-signatures - modifier-signatures + - rejected-calls --- # Declarations @@ -16,6 +17,7 @@ The packed `.d.ts` files. ## Sub-features - `model-signatures`: the return types of the model methods and `RegisterSchema`. +- `rejected-calls`: every wrong call listed in the journey is refused by tsc. - `modifier-signatures`: the hash and localization mixin methods. ## How to get to it (user POV) diff --git a/.agents/skills/verify-interface/features/localization.md b/.agents/skills/verify-interface/features/localization.md index 67f5893..76f6631 100644 --- a/.agents/skills/verify-interface/features/localization.md +++ b/.agents/skills/verify-interface/features/localization.md @@ -7,6 +7,7 @@ journeys: - localize - localized-query - localize-wildcard + - localize-wildcard-replace - localized-storage --- @@ -18,6 +19,7 @@ journeys: - `localize`: switching locale, fallback, restricting fields. - `localized-query`: `unlockrequest` with a fallback locale. +- `localize-wildcard-replace`: a wildcard write replaces what was stored. - `localize-wildcard`: the `"*"` key. - `localized-storage`: one value per locale in MongoDB. diff --git a/.agents/skills/verify-interface/scenarios/custom.ts b/.agents/skills/verify-interface/scenarios/custom.ts index 8de4cfe..7c0516b 100644 --- a/.agents/skills/verify-interface/scenarios/custom.ts +++ b/.agents/skills/verify-interface/scenarios/custom.ts @@ -107,7 +107,12 @@ const METADATA = `${PRELUDE} Reflect.defineMetadata(Aged.key, { list: ["legacy"] }, Old); const first = d.getMetadata(Old, Aged); const second = d.getMetadata(Old, Aged); + class Current {} + d.RegisterTable("currents", "meta")(Current); + const current = d.getMetadata(Current, d.DatumStaticMetadata); + const currentAgain = d.getMetadata(Current, d.DatumStaticMetadata); return { + identity: [current === currentAgain, d.DatumStaticMetadata.upgrade(current) === current, Aged.upgrade(first) === first], shared: [sharedB === d.getMetadata(A, Shared), sharedB.list], copied: [copiedB === d.getMetadata(A, Copied), copiedB.list, d.getMetadata(A, Copied).list], upgrade: [first instanceof Aged, first.version, first.list, first === second], @@ -194,10 +199,11 @@ const HELPERS = ` d.unlock(row, d.LocalizationModifier, ["title"], "en"); const restricted = [row.title ?? "undefined", row.summary ?? "undefined"]; const tables = d.getTablesForSchema("helpers"); + await d.RegisterSchema("helpers"); const bound = d.getMetadata(Profile, d.DatumStaticMetadata); class Early extends Profile {} const earlyBefore = d.getMetadata(Early, d.DatumStaticMetadata); - const copiedArrays = [earlyBefore.indexes !== bound.indexes, earlyBefore.fields !== bound.fields, earlyBefore.primary, earlyBefore.schema === undefined]; + const copiedArrays = [earlyBefore.indexes !== bound.indexes, earlyBefore.fields !== bound.fields, earlyBefore.primary, bound.schema !== undefined, earlyBefore.schema === undefined]; const parentMeta = d.getMetadata(Profile, d.DatumStaticMetadata); class Child extends Profile {} const own = d.getMetadata(Child, d.DatumStaticMetadata, false); @@ -224,9 +230,9 @@ export const CUSTOM_JOURNEYS = { printed: "{\"read\":[{\"a\":[1,2]},\"plain\"],\"storedBody\":\"b64-eyJhIjpbMSwyXX0=\",\"storedTagged\":\"x:InBsYWluIg==\",\"notAString\":\"undefined\"}", stored: [{ name: "the stored encodings", expression: "$DB.getCollection('twoway__docs').find({ title: 't' }).toArray().map((r) => [r._internal.data.body, r._internal.data.tagged])", expected: "[[\"b64-eyJhIjpbMSwyXX0=\",\"x:InBsYWluIg==\"]]" }], }), - "modifier-helpers": scenario({ title: "getModifiedFields, unlock, lock, floating values, getMetadata and getTablesForSchema do what their TSDoc says", body: HELPERS, printed: "{\"names\":{\"hashed\":[\"password\"],\"localized\":[\"title\",\"summary\"],\"none\":[]},\"floatingRead\":\"floating\",\"floatingStored\":[\"_internal\"],\"afterUnlock\":[\"floating\",\"floating\",\"undefined\"],\"lockedKeys\":[\"title\"],\"restricted\":[\"undefined\",\"r\u00e9sum\u00e9\"],\"copiedArrays\":[true,true,\"_id\",true],\"tables\":[[\"others\",\"profiles\"],true,\"undefined\"],\"metadata\":[\"profiles\",\"undefined\",\"profiles\",false,false,true]}" }), + "modifier-helpers": scenario({ title: "getModifiedFields, unlock, lock, floating values, getMetadata and getTablesForSchema do what their TSDoc says", body: HELPERS, printed: "{\"names\":{\"hashed\":[\"password\"],\"localized\":[\"title\",\"summary\"],\"none\":[]},\"floatingRead\":\"floating\",\"floatingStored\":[\"_internal\"],\"afterUnlock\":[\"floating\",\"floating\",\"undefined\"],\"lockedKeys\":[\"title\"],\"restricted\":[\"undefined\",\"r\u00e9sum\u00e9\"],\"copiedArrays\":[true,true,\"_id\",true,true],\"tables\":[[\"others\",\"profiles\"],true,\"undefined\"],\"metadata\":[\"profiles\",\"undefined\",\"profiles\",false,false,true]}" }), "container-query": scenario({ title: "ContainerModifier.unlockrequest uses its key, so a query reads one entry of the stored record", body: CONTAINER_QUERY, printed: "{\"a1\":[\"one\"],\"a2\":[\"two\"],\"b1\":[\"only-b\",\"two\"],\"b2\":[\"one\"],\"c1\":[]}" }), - "metadata-class": scenario({ title: "a metadata class without inherit is shared with the subclass, one with inherit is copied, and upgrade runs on stored metadata", body: METADATA, printed: "{\"shared\":[true,[\"x\"]],\"copied\":[false,[\"x\",\"y\"],[\"x\"]],\"upgrade\":[true,2,[\"legacy\"],true]}" }), + "metadata-class": scenario({ title: "a metadata class without inherit is shared with the subclass, one with inherit is copied, and upgrade runs on stored metadata", body: METADATA, printed: "{\"identity\":[true,true,true],\"shared\":[true,[\"x\"]],\"copied\":[false,[\"x\",\"y\"],[\"x\"]],\"upgrade\":[true,2,[\"legacy\"],true]}" }), "legacy-metadata": scenario({ title: "metadata stored without uniqueIndexes is upgraded and unique indexes still register", body: LEGACY_METADATA, printed: "{\"upgraded\":[true,true,true,\"olds\"],\"definition\":{\"code\":{\"fields\":null,\"unique\":true}},\"uniqueEnforced\":\"DuplicateKeyError\"}" }), "container-modifier": scenario({ title: "a ContainerModifier keeps values by key and treats the wildcard key as the whole record", body: CONTAINER, printed: "{\"both\":{\"a\":1,\"b\":2},\"afterOverwrite\":{\"a\":3,\"b\":2},\"replaced\":{\"z\":26},\"byKey\":[26,\"undefined\",\"undefined\"]}" }), "modifier-events": scenario({ title: "a modifier's event methods run for inserts, updates and conversions, and an events-only modifier leaves the field plain", body: EVENTS, printed: "{\"onInsert\":[\"fromPlainData/note/true/Doc/1:note\",\"insert/note/true/Doc/0:\",\"toDatabase/note/true/Doc/1:_internal+note\"],\"onUpdate\":[\"fromPlainData/note/true/Doc/1:note\",\"update/note/true/Doc/0:\",\"toDatabase/note/true/Doc/1:_internal+note\"],\"onRead\":[\"fromDatabase/note/true/Doc/1:_id+_internal+handled+note\"],\"onReadAll\":[\"fromDatabase/note/true/Doc/1:_id+_internal+handled+note\"],\"onPlain\":[\"toPlainData/note/true/Doc/1:_id+handled+note\"],\"storedByHandler\":\"by-handler\",\"eventsOnlyKeepPlainValue\":true}" }), diff --git a/.agents/skills/verify-interface/scenarios/declarations.ts b/.agents/skills/verify-interface/scenarios/declarations.ts index 8d98e63..22486d5 100644 --- a/.agents/skills/verify-interface/scenarios/declarations.ts +++ b/.agents/skills/verify-interface/scenarios/declarations.ts @@ -4,6 +4,7 @@ import type { Journey } from "@antelopejs/tooling-configs/verify"; const PROBE_HEADER = ` type Identical = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; type Assert = T; +type CanCall = F extends (...args: Args) => unknown ? true : false; `; const MODEL_SIGNATURES = ` @@ -22,6 +23,21 @@ type _Validate = Assert, Api.Validat type _Register = Assert Promise>>; type _Fixture = Assert, { a: 1 } | { a: 1 }[] | undefined>>; type _PrimaryKey = Assert>; +type _UpdateById = Assert>; +type _UpdateByIdOptions = Assert>; +type _UpdateByObject = Assert>; +type _UpdateByObjectOptions = Assert>; +type _UpdateNoKey = Assert, false>>; +type _UpdateNoData = Assert, false>>; +type _UpdateBadId = Assert, false>>; +type _UpdateBadField = Assert, false>>; +type _UpdateBadOptions = Assert, false>>; +type _GetParams = Assert, [id: string]>>; +type _DeleteParams = Assert, [id: string]>>; +type _InsertOptions = Assert[1], Api.ValidateOptions | undefined>>; +type _ValidateParams = Assert, [obj: unknown, options?: Api.ValidateInputOptions]>>; +type _ValidateOptions = Assert>; +type _GetModelInstance = Assert[1], Parameters[0] extends infer T ? Extract : never>>; type _Table = Assert[0], Api.TableTarget | undefined>>; `; @@ -38,9 +54,85 @@ type _Outdated = Assert>; declare const localized: Localized; const switched = localized.localize("en"); type _Localize = Assert>; +const testedNumber = hashed.testHash("password", "x"); +const accepted = [hashed.testHashAsync("password", "x"), localized.localize("en", ["name"]), localized.localize("fr", [])]; +type _FallbackParams = Assert, [TableClass: Api.Constructible, field: string]>>; type _Fallback = Assert, string | undefined>>; `; +/** Statements that must each fail to compile: the journey passes only when tsc reports an error on every one of them, so a parameter type that gets looser stops being reported. */ +const REJECTED = [ + 'void m.update({ name: "x" });', + 'void m.update("1");', + 'void m.update("1", { name: 5 });', + 'void m.update({ _id: 1, name: "x" });', + 'void m.update("1", { name: "x" }, { validate: "yes" });', + 'void m.insert({ name: 5 });', + 'void m.insert({ name: "x" }, { validate: 1 });', + "void m.get(5);", + "void m.delete(5);", + 'void m.getBy("nope", "x");', + 'void Model.validate({}, { partial: "yes" });', + 'void Model.fromPlainData({ name: 5 });', + "void User.table(5);", + "void Api.RegisterSchema(5);", + "void Api.GetModel(Model, 5);", + 'void hashed.testHash("password", 5);', + 'void hashed.testHash("nope", "x");', + 'void hashed.testHashAsync("password", 5);', + 'void hashed.testHashAsync("nope", "x");', + 'void hashed.isHashOutdated("nope");', + 'void localized.localize("en", ["nope"]);', + "void localized.localize(5);", + 'void Api.LocalizationModifier.fallbackLocaleOf(5, "name");', +]; + +const REJECT_PRELUDE = ` +class User extends Api.Table { declare name: string; } +const Model = Api.BasicDataModel(User, "users"); +declare const m: InstanceType; +class Hashed extends Api.Table.with(Api.HashModifier) { declare password: string; } +class Localized extends Api.Table.with(Api.LocalizationModifier) { declare name: string; } +declare const hashed: Hashed; +declare const localized: Localized; +`; + +function rejected(title: string, statements: string[]): Journey { + return { + title, + entryPoint: "a TypeScript consumer compiling wrong calls against the packed declarations", + async run(context) { + const file = `${context.work}/reject.ts`; + const head = `import * as Api from "${context.repo}/dist/index";\n${PROBE_HEADER}\n${REJECT_PRELUDE}`; + const firstLine = head.split("\n").length + 1; + context.edit("write the rejecting probe", file, `${head}\n${statements.join("\n")}\n`); + const compiled = await context.command("compile the rejecting probe", "node", [ + `${context.repo}/node_modules/typescript/bin/tsc`, + "--noEmit", + "--strict", + "--skipLibCheck", + "--experimentalDecorators", + "--target", + "es2022", + "--module", + "commonjs", + "--moduleResolution", + "node", + "--typeRoots", + `${context.repo}/node_modules/@types`, + "--types", + "node", + file, + ]); + const failing = new Set([...compiled.stdout.matchAll(/reject\.ts\((\d+),/g)].map((match) => Number(match[1]))); + const accepted = statements.filter((_, index) => !failing.has(firstLine + index)); + context.expect(compiled.folder, [ + { name: "tsc refuses every wrong call", pass: accepted.length === 0, detail: accepted.join(" | ") }, + ]); + }, + }; +} + function declared(title: string, probe: string): Journey { return { title, @@ -75,5 +167,6 @@ function declared(title: string, probe: string): Journey { export const DECLARATION_JOURNEYS = { "model-signatures": declared("the model methods and RegisterSchema declare the return types the docs table lists", MODEL_SIGNATURES), + "rejected-calls": rejected("calls with a wrong parameter type or a missing key are refused by the declarations", REJECTED), "modifier-signatures": declared("the hash and localization mixins declare the return types the docs describe", MODIFIER_SIGNATURES), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/hashing.ts b/.agents/skills/verify-interface/scenarios/hashing.ts index f1197e8..e9bd71d 100644 --- a/.agents/skills/verify-interface/scenarios/hashing.ts +++ b/.agents/skills/verify-interface/scenarios/hashing.ts @@ -10,7 +10,7 @@ const HASH_HELPERS = ` const scryptString = (value, ln, r, p, saltBytes = 16, keyBytes = 32) => { const salt = Buffer.alloc(saltBytes, 7); return "$" + "scrypt" + "$ln=" + ln + ",r=" + r + ",p=" + p + "$" + b64(salt) + "$" + b64(scryptOf(value, salt, keyBytes, ln, r, p)); }; const digest = (algorithm, salt, value) => crypto.createHash(algorithm).update(salt).update(JSON.stringify(value)).digest("base64"); class Account extends d.Table.with(d.HashModifier) {} - decorate(Account, { name: [d.Field("string")], password: [d.Hashed(), d.Field("string")], pin: [d.Hashed({ algorithm: "sha256" }), d.Field("string")], token: [d.Hashed({ algorithm: "sha256" }), d.Field("string")], long: [d.Hashed({ algorithm: "sha512" }), d.Field("string")], legacy: [d.Hashed({ algorithm: "md5" }), d.Field("string")] }); + decorate(Account, { name: [d.Field("string")], password: [d.Hashed(), d.Field("string")], pin: [d.Hashed({ algorithm: "sha256" }), d.Field("string")], token: [d.Hashed({ algorithm: "sha256" }), d.Field("string")], long: [d.Hashed({ algorithm: "sha512" }), d.Field("string")], legacy: [d.Hashed({ algorithm: "md5" }), d.Field("string")], one: [d.Hashed({ algorithm: "sha1" }), d.Field("string")], mid: [d.Hashed({ algorithm: "sha384" }), d.Field("string")] }); d.RegisterTable("accounts", "hash")(Account); await d.RegisterSchema("hash"); const Model = d.BasicDataModel(Account); @@ -106,9 +106,10 @@ const UPGRADE = `${HASH_HELPERS} `; const DIGEST = `${HASH_HELPERS} - const [id] = await accounts.insert({ name: "Ann", pin: "1234", token: "abc", long: "abc", legacy: "abc" }); + const [id] = await accounts.insert({ name: "Ann", pin: "1234", token: "abc", long: "abc", legacy: "abc", one: "abc", mid: "abc" }); const raw = await rawRow(id); const salt = raw._internal.meta.HashModifier.salt; + const others = [raw._internal.data.one === digest("sha1", salt, "abc"), raw._internal.data.mid === digest("sha384", salt, "abc")]; const algorithms = [raw._internal.data.long === digest("sha512", salt, "abc"), raw._internal.data.legacy === digest("md5", salt, "abc"), raw._internal.data.long === digest("sha256", salt, "abc"), raw._internal.data.long.length, raw._internal.data.legacy.length]; const checked = await accounts.get(id); const algorithmTests = [checked.testHash("long", "abc"), checked.testHash("legacy", "abc"), checked.testHash("long", "abd")]; @@ -123,7 +124,7 @@ const DIGEST = `${HASH_HELPERS} const holdsScrypt = Model.fromDatabase({ _id: "h", name: "h", _internal: { meta: { HashModifier: { salt } }, data: { pin: scryptString("1234", 10, 8, 1) } } }); const sha512 = Model.fromDatabase({ _id: "s", name: "s", _internal: { meta: { HashModifier: { salt } }, data: { pin: crypto.createHash("sha512").update(salt).update(JSON.stringify("1234")).digest("base64") } } }); return { - algorithms, algorithmTests, wrongDigest, + algorithms, others, algorithmTests, wrongDigest, storedDigest: raw._internal.data.pin === digest("sha256", salt, "1234"), sharedSalt: raw._internal.data.token === digest("sha256", salt, "abc"), saltIsText: typeof salt === "string" && salt.length > 0, @@ -246,5 +247,5 @@ export const HASH_JOURNEYS = { "hash-memory": scenario({ title: "a stored scrypt string verifies up to 128 MiB of memory (128 x N x r) and is refused above it", body: MEMORY, printed: "{\"exactR64\":[true,false],\"overR65\":[false,false],\"exactR2\":[true,false],\"underR3\":[true,false],\"overR5\":[false,false],\"overR9\":[false,false]}" }), "hash-sync-async": scenario({ title: "testHash answers exactly what testHashAsync answers, for every kind of stored value", body: SYNC_ASYNC, printed: "{\"fresh:password\":[[true,true,true],[false,false,true],[false,false,true]],\"fresh:pin\":[[true,true,true],[false,false,true],[false,false,true]],\"legacy:password\":[[true,true,true],[false,false,true],[false,false,true]],\"lowcost:password\":[[true,true,true],[false,false,true],[false,false,true]],\"junk:password\":[[false,false,true],[false,false,true],[false,false,true]],\"empty:password\":[[false,false,true],[false,false,true],[false,false,true]],\"digest:pin\":[[true,true,true],[false,false,true],[false,false,true]],\"scryptOnDigest:pin\":[[true,true,true],[false,false,true],[false,false,true]]}" }), "hash-outdated": scenario({ title: "isHashOutdated flags any scrypt string whose ln, r or p differs from the current cost", body: OUTDATED, printed: "{\"fresh\":[true,false],\"sameCost\":[true,false],\"lowerP\":[true,true],\"higherR\":[true,true],\"lowerLn\":[true,true],\"notScrypt\":[false,true]}" }), - "hash-digest": scenario({ title: "a field with an algorithm stores a salted digest with the row's shared salt", body: DIGEST, printed: "{\"algorithms\":[true,true,false,88,24],\"algorithmTests\":[true,true,false],\"wrongDigest\":false,\"storedDigest\":true,\"sharedSalt\":true,\"saltIsText\":true,\"freshSaltPerRow\":true,\"saltReusedOnLaterWrite\":[true,true],\"testDigest\":[true,false,true],\"digestNeverOutdated\":false,\"scryptOnDigestField\":[true,false],\"otherDigestRefused\":false}" }), + "hash-digest": scenario({ title: "a field with an algorithm stores a salted digest with the row's shared salt", body: DIGEST, printed: "{\"algorithms\":[true,true,false,88,24],\"others\":[true,true],\"algorithmTests\":[true,true,false],\"wrongDigest\":false,\"storedDigest\":true,\"sharedSalt\":true,\"saltIsText\":true,\"freshSaltPerRow\":true,\"saltReusedOnLaterWrite\":[true,true],\"testDigest\":[true,false,true],\"digestNeverOutdated\":false,\"scryptOnDigestField\":[true,false],\"otherDigestRefused\":false}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/localization.ts b/.agents/skills/verify-interface/scenarios/localization.ts index e446e9f..11bb7fd 100644 --- a/.agents/skills/verify-interface/scenarios/localization.ts +++ b/.agents/skills/verify-interface/scenarios/localization.ts @@ -66,6 +66,22 @@ const WILDCARD = `${PRODUCT} return { all, replaced, german, stored: [loaded.localize("fr").name, loaded.localize("it").name, loaded.localize("*").name] }; `; +const WILDCARD_REPLACE = `${PRODUCT} + const model = d.GetModel(d.BasicDataModel(Product)); + const product = new Product(); + product._id = "wild"; + product.price = 3; + product.localize("en").name = "Hello"; + product.localize("fr").name = "Bonjour"; + const before = product.localize("*").name; + Object.assign(product.localize("*"), { name: { de: "Hallo" } }); + const afterWrite = product.localize("*").name; + const fallbacks = [product.localize("fr").name ?? "undefined", product.localize("en").name ?? "undefined", product.localize("de").name]; + await model.insert(product); + const loaded = await model.get("wild"); + return { before, afterWrite, fallbacks, read: loaded.localize("*").name, readFr: loaded.localize("fr").name ?? "undefined" }; +`; + const STORED = `${PRODUCT} const model = d.GetModel(d.BasicDataModel(Product)); const first = new Product(); @@ -156,6 +172,12 @@ const AUTO_OBJECT = ` export const LOCALIZATION_JOURNEYS = { localize: scenario({ title: "localize switches the locale of an instance and falls back when a translation is missing or empty", body: LOCALIZE, printed: "{\"spanish\":\"Auriculares Premium\",\"germanFallsBack\":\"Premium Headphones\",\"sameObject\":[true,true,true],\"readNow\":\"Premium Headphones\",\"frenchEmpty\":\"Loud\",\"noFallback\":[\"sans repli\",\"undefined\"],\"restrictedReads\":[\"Casque Premium\",\"short\"],\"fallbackLocaleOf\":[\"en\",\"undefined\",\"undefined\",\"undefined\"]}" }), "localized-query": scenario({ title: "a query through unlockrequest answers the requested locale and falls back to the fallback locale, an empty text included", body: QUERY, printed: "{\"germanHello\":[\"empty-de\",\"only-en\"],\"germanHallo\":[\"has-de\"],\"englishHello\":[\"empty-de\",\"only-en\"],\"frenchSalut\":[\"none\"],\"frenchHello\":[\"empty-de\",\"only-en\"],\"noFallbackGerman\":[],\"noFallbackEnglish\":[\"only-en\"],\"noFallbackOwnText\":[\"has-de\"]}" }), + "localize-wildcard-replace": scenario({ + title: "a wildcard write replaces every stored translation", + body: WILDCARD_REPLACE, + printed: "{\"before\":{\"en\":\"Hello\",\"fr\":\"Bonjour\"},\"afterWrite\":{\"de\":\"Hallo\"},\"fallbacks\":[\"undefined\",\"undefined\",\"Hallo\"],\"read\":{\"de\":\"Hallo\"},\"readFr\":\"undefined\"}", + stored: [{ name: "the stored translations", expression: "$DB.getCollection('loc__products').find({ _id: 'wild' }).toArray().map((r) => r._internal.data.name)", expected: "[{\"de\":\"Hallo\"}]" }], + }), "localize-wildcard": scenario({ title: "the wildcard key reads and writes every translation at once", body: WILDCARD, printed: "{\"all\":{\"en\":\"Premium Headphones\",\"fr\":\"Casque Premium\"},\"replaced\":{\"en\":\"Premium Headphones\",\"fr\":\"Casque Premium\",\"de\":\"Premium-Kopfhoerer\"},\"german\":\"Premium-Kopfhoerer\",\"stored\":[\"Bonjour\",\"Hello\",{\"en\":\"Hello\",\"fr\":\"Bonjour\"}]}" }), "localized-storage": scenario({ title: "a localized field is stored as one value per locale", From e35ed2a59f59b63c448b46b43196e2ffbfb86c58 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Wed, 4 Nov 2026 03:31:34 +0100 Subject: [PATCH 32/45] feat(skills): refuse wrong option types of the decorators and factories --- .../verify-interface/scenarios/declarations.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.agents/skills/verify-interface/scenarios/declarations.ts b/.agents/skills/verify-interface/scenarios/declarations.ts index 22486d5..63903bc 100644 --- a/.agents/skills/verify-interface/scenarios/declarations.ts +++ b/.agents/skills/verify-interface/scenarios/declarations.ts @@ -85,6 +85,20 @@ const REJECTED = [ 'void localized.localize("en", ["nope"]);', "void localized.localize(5);", 'void Api.LocalizationModifier.fallbackLocaleOf(5, "name");', + 'Api.Index({ unique: "yes" });', + "Api.Index({ group: 5 });", + 'Api.Index({ crossInstance: "yes" });', + "Api.Relation({});", + 'Api.Relation({ to: () => User, many: "yes" });', + 'Api.Relation({ to: () => User, toField: 5 });', + 'Api.RegisterTable("only-a-name");', + "Api.Encrypted({});", + 'Api.Encrypted({ secretKey: "k", ivSize: "16" });', + "Api.Hashed({ algorithm: 5 });", + "Api.Localized({ fallbackLocale: 5 });", + "Api.Fixture(5);", + "void Api.BasicDataModel(5);", + "void Api.GetModel(5);", ]; const REJECT_PRELUDE = ` From 14327aee3c83b0e97454d09e05ea66d48d38b4cd Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Wed, 4 Nov 2026 03:44:53 +0100 Subject: [PATCH 33/45] feat(skills): pin that a late subclass is not bound to its parent's schema --- .agents/skills/verify-interface/scenarios/tables.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.agents/skills/verify-interface/scenarios/tables.ts b/.agents/skills/verify-interface/scenarios/tables.ts index ba9f3d6..ee02090 100644 --- a/.agents/skills/verify-interface/scenarios/tables.ts +++ b/.agents/skills/verify-interface/scenarios/tables.ts @@ -131,7 +131,9 @@ const INHERITANCE = ` const meta = (cls) => { const m = d.getMetadata(cls, d.DatumStaticMetadata); return { table: m.tableName ?? null, schema: m.schemaName ?? null, fields: Object.keys(m.fields), indexes: Object.keys(m.indexes), unique: [...m.uniqueIndexes], cross: [...m.crossInstanceIndexes] }; }; await d.RegisterSchema("inh"); const built = iface.Schema.get("inh").definition; - return { built: [built.subs.indexes.ab, built.crosses.indexes.ab], base: meta(Base), person: meta(Person), employee: meta(Employee), contractor: meta(Contractor), cross: meta(Cross), sub: meta(Sub) }; + class Late extends Cross {} + const late = [d.getMetadata(Late, d.DatumStaticMetadata).schema === undefined, d.getMetadata(Cross, d.DatumStaticMetadata).schema !== undefined, d.getMetadata(Late, d.DatumStaticMetadata).tableName]; + return { late, built: [built.subs.indexes.ab, built.crosses.indexes.ab], base: meta(Base), person: meta(Person), employee: meta(Employee), contractor: meta(Contractor), cross: meta(Cross), sub: meta(Sub) }; `; const WITH_SUBCLASS = ` @@ -249,7 +251,7 @@ export const TABLE_JOURNEYS = { "group-flags": scenario({ title: "unique and crossInstance on any field of a group apply to the whole group", body: GROUP_FLAGS, printed: "{\"indexes\":{\"ab\":{\"fields\":[\"a\",\"b\"],\"unique\":true},\"cd\":{\"fields\":[\"c\",\"e\"],\"crossInstance\":true},\"ef\":{\"fields\":[\"f\",\"g\",\"h\"],\"crossInstance\":true},\"ij\":{\"fields\":[\"i\",\"j\"],\"unique\":true},\"plain\":{\"fields\":[\"k\",\"l\"]}},\"sameKey\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"ab\\\" (a, b)\"},\"otherSecond\":1}" }), "field-types": scenario({ title: "Field hands every type to the database unchanged and combines with Index in either order", body: FIELD_TYPES, printed: "{\"names\":[\"id\",\"tags\",\"address\",\"total\",\"lateIndex\"],\"id\":\"string\",\"tags\":[\"string\"],\"address\":{\"street\":\"string\",\"geo\":{\"lat\":\"number\",\"lng\":\"number\"}},\"codecIsTheSameObject\":true,\"lateIndexType\":\"number\",\"indexes\":[\"total\",\"lateIndex\"]}" }), "relation-metadata": scenario({ title: "Relation records links without enforcing them", body: RELATION, printed: "{\"relationFields\":[\"authorId\",\"tagIds\"],\"authorTarget\":true,\"authorToField\":null,\"tagMany\":true,\"tagToField\":\"name\",\"dangling\":[\"nobody\",[\"ghost\"]],\"fieldsKept\":[\"authorId\",\"tagIds\",\"title\"],\"childRelations\":[\"authorId\",\"tagIds\",\"editorId\"],\"parentRelations\":[\"authorId\",\"tagIds\"]}" }), - inheritance: scenario({ title: "a table subclass starts with a copy of its parent and never changes it", body: INHERITANCE, printed: "{\"built\":[{\"fields\":[\"a\",\"b\"],\"crossInstance\":true,\"unique\":true},{\"fields\":[\"a\",\"b\"],\"crossInstance\":true,\"unique\":true}],\"base\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\"],\"indexes\":[],\"unique\":[],\"cross\":[]},\"person\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\"],\"indexes\":[\"email\"],\"unique\":[\"email\"],\"cross\":[]},\"employee\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\",\"badge\"],\"indexes\":[\"email\",\"badge\"],\"unique\":[\"email\"],\"cross\":[]},\"contractor\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"rate\"],\"indexes\":[],\"unique\":[],\"cross\":[]},\"cross\":{\"table\":\"crosses\",\"schema\":\"inh\",\"fields\":[\"a\",\"b\",\"c\"],\"indexes\":[\"ab\",\"c\"],\"unique\":[\"ab\"],\"cross\":[\"ab\"]},\"sub\":{\"table\":\"subs\",\"schema\":\"inh\",\"fields\":[\"a\",\"b\",\"c\",\"extra\"],\"indexes\":[\"ab\",\"c\"],\"unique\":[\"ab\"],\"cross\":[\"ab\"]}}" }), + inheritance: scenario({ title: "a table subclass starts with a copy of its parent and never changes it", body: INHERITANCE, printed: "{\"late\":[true,true,\"crosses\"],\"built\":[{\"fields\":[\"a\",\"b\"],\"crossInstance\":true,\"unique\":true},{\"fields\":[\"a\",\"b\"],\"crossInstance\":true,\"unique\":true}],\"base\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\"],\"indexes\":[],\"unique\":[],\"cross\":[]},\"person\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\"],\"indexes\":[\"email\"],\"unique\":[\"email\"],\"cross\":[]},\"employee\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\",\"badge\"],\"indexes\":[\"email\",\"badge\"],\"unique\":[\"email\"],\"cross\":[]},\"contractor\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"rate\"],\"indexes\":[],\"unique\":[],\"cross\":[]},\"cross\":{\"table\":\"crosses\",\"schema\":\"inh\",\"fields\":[\"a\",\"b\",\"c\"],\"indexes\":[\"ab\",\"c\"],\"unique\":[\"ab\"],\"cross\":[\"ab\"]},\"sub\":{\"table\":\"subs\",\"schema\":\"inh\",\"fields\":[\"a\",\"b\",\"c\",\"extra\"],\"indexes\":[\"ab\",\"c\"],\"unique\":[\"ab\"],\"cross\":[\"ab\"]}}" }), "combine-modifiers": scenario({ title: "Table.with combines the encryption, hashing and localization modifiers on one table", body: COMBINE, printed: "{\"stored\":[\"bio\",\"password\",\"privateInfo\"],\"privateInfo\":{\"address\":\"1 Main St\",\"phoneNumber\":\"555\"},\"password\":[true,false],\"bio\":[\"Bonjour\",\"Hello\"],\"mixins\":[\"function\",\"function\"]}" }), "table-with-subclass": scenario({ title: "X.with(...) returns a subclass of X that keeps X's fields and methods and gains the mixin's", body: WITH_SUBCLASS, printed: "{\"isUser\":[true,true,true],\"methods\":[\"hi Root\",\"function\",\"function\",\"function\",\"undefined\"],\"stored\":[\"hi Root\",true]}" }), "table-with-keeps-fields": scenario({ From ef6afca1fca1a0f9fd278bee6c7353e35cda98d7 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Wed, 4 Nov 2026 03:47:16 +0100 Subject: [PATCH 34/45] feat(skills): load the packed package, pin the helper return types and every option type --- .../verify-interface/features/declarations.md | 2 + .../verify-interface/scenarios/custom.ts | 4 +- .../scenarios/declarations.ts | 94 +++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/.agents/skills/verify-interface/features/declarations.md b/.agents/skills/verify-interface/features/declarations.md index b3a35ac..d39c1c4 100644 --- a/.agents/skills/verify-interface/features/declarations.md +++ b/.agents/skills/verify-interface/features/declarations.md @@ -8,6 +8,7 @@ journeys: - model-signatures - modifier-signatures - rejected-calls + - packed-subpaths --- # Declarations @@ -17,6 +18,7 @@ The packed `.d.ts` files. ## Sub-features - `model-signatures`: the return types of the model methods and `RegisterSchema`. +- `packed-subpaths`: the packed package, not the checkout, serves every subpath the skill file imports. - `rejected-calls`: every wrong call listed in the journey is refused by tsc. - `modifier-signatures`: the hash and localization mixin methods. diff --git a/.agents/skills/verify-interface/scenarios/custom.ts b/.agents/skills/verify-interface/scenarios/custom.ts index 7c0516b..2cb75ea 100644 --- a/.agents/skills/verify-interface/scenarios/custom.ts +++ b/.agents/skills/verify-interface/scenarios/custom.ts @@ -24,6 +24,8 @@ const ONE_WAY = `${PRELUDE} return { tested: [d.testValue(loaded, "digest", "value"), d.testValue(loaded, "digest", "other"), d.testValue(loaded, "strong", "value")], stored: [raw._internal.data.digest === crypto.createHash("sha256").update(JSON.stringify("value")).digest("hex"), raw._internal.data.strong === crypto.createHash("sha512").update(JSON.stringify("value")).digest("hex")], + afterOneWay: caught(() => { class T extends d.Table.with(d.HashModifier, d.EncryptionModifier) {} decorate(T, { pin: [d.Hashed(), d.Encrypted({ secretKey: "12345678901234567890123456789012" })] }); }), + beforeOneWay: caught(() => { class T extends d.Table.with(d.HashModifier, d.EncryptionModifier) {} decorate(T, { pin: [d.Encrypted({ secretKey: "12345678901234567890123456789012" }), d.Hashed()] }); }), secondOneWay: caught(() => { class Twice extends d.Table {} decorate(Twice, { f: [PrefixHashed(), PrefixHashed()] }); }), }; `; @@ -223,7 +225,7 @@ const HELPERS = ` `; export const CUSTOM_JOURNEYS = { - "one-way-modifier": scenario({ title: "a custom OneWayModifier transforms a value when it is written", body: ONE_WAY, printed: "{\"tested\":[true,false,true],\"stored\":[true,true],\"secondOneWay\":\"Attempted adding Modifier PrefixHashModifier on Twice.f but it already has a One-Way Modifier\"}" }), + "one-way-modifier": scenario({ title: "a custom OneWayModifier transforms a value when it is written", body: ONE_WAY, printed: "{\"tested\":[true,false,true],\"stored\":[true,true],\"afterOneWay\":\"Adding Modifier EncryptionModifier on T.pin after a One-Way Modifier, please review your ordering.\",\"beforeOneWay\":null,\"secondOneWay\":\"Attempted adding Modifier PrefixHashModifier on Twice.f but it already has a One-Way Modifier\"}" }), "two-way-modifier": scenario({ title: "a custom TwoWayModifier stores a transformed value and unlocks it on read", body: TWO_WAY, diff --git a/.agents/skills/verify-interface/scenarios/declarations.ts b/.agents/skills/verify-interface/scenarios/declarations.ts index 63903bc..e48c47c 100644 --- a/.agents/skills/verify-interface/scenarios/declarations.ts +++ b/.agents/skills/verify-interface/scenarios/declarations.ts @@ -1,3 +1,7 @@ +import { existsSync, mkdirSync, symlinkSync } from "node:fs"; +import { dirname, join } from "node:path"; + +import { equal } from "@antelopejs/tooling-configs/verify"; import type { Journey } from "@antelopejs/tooling-configs/verify"; /** The probe header: `Assert>` fails to compile unless A and B are the same type. */ @@ -38,6 +42,28 @@ type _InsertOptions = Assert[1], Api.Va type _ValidateParams = Assert, [obj: unknown, options?: Api.ValidateInputOptions]>>; type _ValidateOptions = Assert>; type _GetModelInstance = Assert[1], Parameters[0] extends infer T ? Extract : never>>; +declare const u: User; +const plain = Api.toPlainData(u); +type _ToPlain = Assert>>; +const modified = Api.getModifiedFields(u, Api.HashModifier); +type _Modified = Assert>; +type _Tables = Assert>[string]>, Api.Table>>; +type _TablesMaybe = Assert ? true : false, true>>; +const meta = Api.getMetadata(User, Api.DatumStaticMetadata); +type _Meta = Assert>; +const built = Api.fromPlainData({ name: "x" }, User); +type _FromPlain = Assert>; +const read = Api.fromDatabase({ name: "x" }, User); +type _FromDb = Assert>; +const stored = Api.toDatabase(u); +type _ToDb = Assert>; +class Options extends Api.Table.with(Api.EncryptionModifier, Api.HashModifier, Api.LocalizationModifier) { + @Api.Index({ group: "g", crossInstance: true, unique: true }) @Api.Field("string") declare a: string; + @Api.Relation({ to: () => User, toField: "name", many: true }) @Api.Field("string") declare b: string; + @Api.Encrypted({ secretKey: "k", algorithm: "aes-256-gcm", ivSize: 16 }) @Api.Field("string") declare c: string; + @Api.Hashed({ algorithm: "sha256" }) @Api.Field("string") declare d: string; + @Api.Localized({ fallbackLocale: "en" }) @Api.Field("string") declare e: string; +} type _Table = Assert[0], Api.TableTarget | undefined>>; `; @@ -99,6 +125,16 @@ const REJECTED = [ "Api.Fixture(5);", "void Api.BasicDataModel(5);", "void Api.GetModel(5);", + 'Api.Index({ crossInstance: 1 });', + 'Api.RegisterTable(5, "schema");', + 'Api.RegisterTable("table", 5);', + "Api.Encrypted({ secretKey: 5 });", + 'Api.Encrypted({ secretKey: "k", algorithm: 5 });', + "Api.Relation({ to: User });", + "Api.Localized({ fallbackLocale: true });", + "Api.Hashed({ algorithm: true });", + "Api.Model(Model, 5);", + 'void Api.getMetadata(User, "key");', ]; const REJECT_PRELUDE = ` @@ -179,8 +215,66 @@ function declared(title: string, probe: string): Journey { }; } +const PACKED_CHECK = ` +const fs = require("node:fs"); +const path = require("node:path"); +const pack = process.argv[2]; +const consumer = process.argv[3]; +const skill = fs.readFileSync(path.join(pack, "skills", "database-decorators-interface", "SKILL.md"), "utf8"); +const name = "@antelopejs/interface-database-decorators"; +const result = { files: ["docs/1.introduction.md", "docs/5.parameter-decoration.md", "skills/database-decorators-interface/SKILL.md", "dist/index.js"].filter((file) => !fs.existsSync(path.join(pack, file))), imports: {} }; +const wanted = new Map(); +for (const match of skill.matchAll(/import \\{([^}]*)\\} from "(@antelopejs\\/interface-database-decorators[^"]*)"/g)) { + const names = match[1].split(",").map((entry) => entry.trim()).filter(Boolean); + wanted.set(match[2], [...(wanted.get(match[2]) ?? []), ...names]); +} +const requireFrom = require("node:module").createRequire(path.join(consumer, "index.js")); +for (const [specifier, names] of wanted) { + try { + const loaded = requireFrom(specifier); + result.imports[specifier] = names.filter((entry) => loaded[entry] === undefined); + } catch (error) { + result.imports[specifier] = "ERR " + error.code; + } +} +console.log("RESULT " + JSON.stringify({ subpaths: [...wanted.keys()].sort(), files: result.files, imports: result.imports })); +`; + +/** The folder `new` packed and extracted: the nearest ancestor of the journey folder that holds `pack/package`. */ +function packedPackage(work: string): string { + let folder = work; + while (!existsSync(join(folder, "pack", "package", "package.json"))) { + const parent = dirname(folder); + if (parent === folder) throw new Error(`no packed package above ${work}`); + folder = parent; + } + return join(folder, "pack", "package"); +} + +const packedSubpaths: Journey = { + title: "the packed package ships its docs and skill, and every subpath its skill file imports from exports the names the skill shows", + entryPoint: "a consumer resolving the packed tarball's exports, as npm would publish them", + async run(context) { + const pack = packedPackage(context.work); + const consumer = join(context.work, "consumer"); + const scope = join(consumer, "node_modules", "@antelopejs"); + mkdirSync(scope, { recursive: true }); + symlinkSync(pack, join(scope, "interface-database-decorators"), "dir"); + if (!existsSync(join(pack, "node_modules"))) symlinkSync(join(context.repo, "node_modules"), join(pack, "node_modules"), "dir"); + const script = join(context.work, "packed-check.cjs"); + context.edit("write the packed check", script, PACKED_CHECK); + const ran = await context.command("load every subpath of the skill's imports", "node", [script, pack, consumer]); + const line = ran.stdout.split("\n").find((entry) => entry.startsWith("RESULT ")); + context.expect(ran.folder, [ + { name: "the check ended cleanly", pass: ran.exitCode === 0, detail: ran.stderr.slice(-300) }, + equal("the packed package carries what the skill imports", line?.slice("RESULT ".length) ?? "", "{\"subpaths\":[\"@antelopejs/interface-database-decorators/common\",\"@antelopejs/interface-database-decorators/database\",\"@antelopejs/interface-database-decorators/model\",\"@antelopejs/interface-database-decorators/modifiers/autodate\",\"@antelopejs/interface-database-decorators/modifiers/common\",\"@antelopejs/interface-database-decorators/modifiers/encryption\",\"@antelopejs/interface-database-decorators/modifiers/hash\",\"@antelopejs/interface-database-decorators/modifiers/localization\",\"@antelopejs/interface-database-decorators/relation\",\"@antelopejs/interface-database-decorators/schema\",\"@antelopejs/interface-database-decorators/table\"],\"files\":[],\"imports\":{\"@antelopejs/interface-database-decorators/table\":[],\"@antelopejs/interface-database-decorators/schema\":[],\"@antelopejs/interface-database-decorators/database\":[],\"@antelopejs/interface-database-decorators/model\":[],\"@antelopejs/interface-database-decorators/relation\":[],\"@antelopejs/interface-database-decorators/common\":[],\"@antelopejs/interface-database-decorators/modifiers/autodate\":[],\"@antelopejs/interface-database-decorators/modifiers/encryption\":[],\"@antelopejs/interface-database-decorators/modifiers/hash\":[],\"@antelopejs/interface-database-decorators/modifiers/localization\":[],\"@antelopejs/interface-database-decorators/modifiers/common\":[]}}"), + ]); + }, +}; + export const DECLARATION_JOURNEYS = { "model-signatures": declared("the model methods and RegisterSchema declare the return types the docs table lists", MODEL_SIGNATURES), + "packed-subpaths": packedSubpaths, "rejected-calls": rejected("calls with a wrong parameter type or a missing key are refused by the declarations", REJECTED), "modifier-signatures": declared("the hash and localization mixins declare the return types the docs describe", MODIFIER_SIGNATURES), } satisfies { [id: string]: Journey }; From a70786b2d83eeed24839a5c4c815c2979d9c6f32 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Wed, 4 Nov 2026 04:10:15 +0100 Subject: [PATCH 35/45] feat(skills): convert on every read path, inject the default instance, fix the unbound-class line --- .../verify-interface/scenarios/decoration.ts | 8 ++++++-- .../verify-interface/scenarios/models.ts | 18 +++++++++++++----- .../verify-interface/scenarios/tables.ts | 11 +++++++---- skills/database-decorators-interface/SKILL.md | 2 +- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/.agents/skills/verify-interface/scenarios/decoration.ts b/.agents/skills/verify-interface/scenarios/decoration.ts index 6269fdc..6b586ae 100644 --- a/.agents/skills/verify-interface/scenarios/decoration.ts +++ b/.agents/skills/verify-interface/scenarios/decoration.ts @@ -23,11 +23,15 @@ const FIXED = `${HTTP} const seen = []; class Fixed extends api.Controller("/fixed") {} route(Fixed, "list", "Get", "/", async (model) => { seen.push(model); return (await model.getAll()).map((row) => row.title); }, [d.Model(NoteModel, "main")]); + const seenDefault = []; + route(Fixed, "plain", "Get", "/plain", async (model) => { seenDefault.push(model); return (await model.getAll()).map((row) => row.title); }, [d.Model(NoteModel)]); await d.GetModel(NoteModel, "main").insert({ title: "in main" }); await d.GetModel(NoteModel).insert({ title: "in default" }); + const plain = await call("/fixed/plain"); + await call("/fixed/plain"); const first = await call("/fixed"); const second = await call("/fixed"); - return { first, second, sameModelEveryRequest: seen.length === 2 && seen[0] === seen[1], isTheCachedModel: seen[0] === d.GetModel(NoteModel, "main") }; + return { plain, defaultIsGetModel: [seenDefault.length, seenDefault[0] === d.GetModel(NoteModel), seenDefault[0] === seenDefault[1], seenDefault[0] === d.GetModel(NoteModel, "main")], first, second, sameModelEveryRequest: seen.length === 2 && seen[0] === seen[1], isTheCachedModel: seen[0] === d.GetModel(NoteModel, "main") }; `; const PER_REQUEST = `${HTTP} @@ -82,7 +86,7 @@ export const DECORATION_JOURNEYS = { "fixed-instance": scenario({ title: "@Model with an instance id injects the cached model of that instance into a controller method", body: FIXED, - printed: "{\"first\":{\"status\":200,\"body\":\"[\\\"in main\\\"]\"},\"second\":{\"status\":200,\"body\":\"[\\\"in main\\\"]\"},\"sameModelEveryRequest\":true,\"isTheCachedModel\":true}", + printed: "{\"plain\":{\"status\":200,\"body\":\"[\\\"in default\\\"]\"},\"defaultIsGetModel\":[2,true,true,false],\"first\":{\"status\":200,\"body\":\"[\\\"in main\\\"]\"},\"second\":{\"status\":200,\"body\":\"[\\\"in main\\\"]\"},\"sameModelEveryRequest\":true,\"isTheCachedModel\":true}", api: true, stored: [{ name: "the notes by instance", expression: "$DB.getCollection('http__notes').find().sort({ title: 1 }).toArray().map((r) => [r.title, r._instance])", expected: "[[\"in default\",null],[\"in main\",\"main\"]]" }], }), diff --git a/.agents/skills/verify-interface/scenarios/models.ts b/.agents/skills/verify-interface/scenarios/models.ts index 19ec3aa..f588b15 100644 --- a/.agents/skills/verify-interface/scenarios/models.ts +++ b/.agents/skills/verify-interface/scenarios/models.ts @@ -15,6 +15,7 @@ const CRUD = ` const admins = (await users.getBy("role", "admin")).map((row) => row.name); const byEmail = (await users.getBy("email", "bob@example.com")).map((row) => row.name); const everyone = (await users.getAll()).map((row) => row.name).sort(); + const instances = [got instanceof User, (await users.getBy("role", "admin")).every((row) => row instanceof User), (await users.getBy("email", "bob@example.com")).every((row) => row instanceof User), (await users.getAll()).every((row) => row instanceof User), (await users.getAll()).length]; const updatedById = await users.update(single[0], { name: "Alice Liddell" }); const updatedByObject = await users.update({ _id: batch[0], name: "Robert" }); const updatedMissing = await users.update("no-such-id", { name: "Nobody" }); @@ -25,6 +26,7 @@ const CRUD = ` insertedIds: [single.length, batch.length, [...single, ...batch].every((id) => typeof id === "string")], got: [got instanceof User, got._id === single[0], got.name], missing: missing ?? "undefined", + instances, admins, byEmail, everyone, updatedById, updatedByObject, updatedMissing, noKey, deleted, deletedMissing, @@ -36,7 +38,7 @@ const KEY = "12345678901234567890123456789012"; const CONVERSION = ` class Account extends d.Table.with(d.EncryptionModifier) {} - decorate(Account, { name: [d.Field("string")], secret: [d.Encrypted({ secretKey: "${KEY}" }), d.Field("string")] }); + decorate(Account, { name: [d.Field("string"), d.Index()], secret: [d.Encrypted({ secretKey: "${KEY}" }), d.Field("string")] }); d.RegisterTable("accounts", "cnv")(Account); await d.RegisterSchema("cnv"); const Model = d.BasicDataModel(Account); @@ -53,6 +55,7 @@ const CONVERSION = ` const read = await model.get(id); const direct = await model.table.get(id); const viaGetAll = await model.getAll(); + const viaGetBy = await model.getBy("name", "Dee"); return { fromPlain: [fromPlain instanceof Account, fromPlain.secret, fromPlain.name], sameInstance, @@ -65,6 +68,7 @@ const CONVERSION = ` read: [read instanceof Account, read.secret], direct: [direct instanceof Account, direct.name, direct._internal !== undefined], getAllConverted: viaGetAll.every((row) => row instanceof Account), + getByConverted: [viaGetBy.length, viaGetBy[0] instanceof Account, viaGetBy[0].secret, viaGetAll[0].secret, read.secret], }; `; @@ -234,7 +238,7 @@ const PARTIAL = ` const PLAIN_DATA = ` class Account extends d.Table.with(d.EncryptionModifier, d.HashModifier, d.LocalizationModifier) {} decorate(Account, { - name: [d.Field("string")], + name: [d.Field("string"), d.Index()], secret: [d.Encrypted({ secretKey: "${KEY}" }), d.Field("any")], password: [d.Hashed(), d.Field("string")], title: [d.Localized({ fallbackLocale: "en" }), d.Field("string")], @@ -250,10 +254,14 @@ const PLAIN_DATA = ` account.localize("en").title = "Hello"; const [id] = await model.insert(account); const loaded = await model.get(id); + const everyPath = [await model.get(id), (await model.getBy("name", "Ann"))[0], (await model.getAll())[0]]; + const pathResults = []; + for (const row of everyPath) pathResults.push([row instanceof Account, row.name, row.secret, await row.testHashAsync("password", "pw"), row.localize("en").title]); const plain = d.toPlainData(loaded); const unlocked = d.toPlainData(loaded.localize("en")); const events = []; return { + pathResults, keys: Object.keys(plain).filter((key) => key !== "_instance").sort(), noInternal: !("_internal" in plain), name: plain.name, @@ -271,13 +279,13 @@ export const MODEL_JOURNEYS = { crud: scenario({ title: "a model inserts, reads, updates and deletes rows and answers ids and counts", body: CRUD, - printed: "{\"insertedIds\":[1,2,true],\"got\":[true,true,\"Alice\"],\"missing\":\"undefined\",\"admins\":[\"Alice\"],\"byEmail\":[\"Bob\"],\"everyone\":[\"Alice\",\"Bob\",\"Carol\"],\"updatedById\":1,\"updatedByObject\":1,\"updatedMissing\":0,\"noKey\":{\"name\":\"AssertionError\",\"message\":\"Missing primary key in object update.\"},\"deleted\":1,\"deletedMissing\":0,\"left\":[\"Alice Liddell\",\"Robert\"]}", + printed: "{\"insertedIds\":[1,2,true],\"got\":[true,true,\"Alice\"],\"missing\":\"undefined\",\"instances\":[true,true,true,true,3],\"admins\":[\"Alice\"],\"byEmail\":[\"Bob\"],\"everyone\":[\"Alice\",\"Bob\",\"Carol\"],\"updatedById\":1,\"updatedByObject\":1,\"updatedMissing\":0,\"noKey\":{\"name\":\"AssertionError\",\"message\":\"Missing primary key in object update.\"},\"deleted\":1,\"deletedMissing\":0,\"left\":[\"Alice Liddell\",\"Robert\"]}", stored: [{ name: "the rows left", expression: "$DB.getCollection('crud__users').find().sort({ email: 1 }).toArray().map((r) => [r.email, r.name, r.role])", expected: "[[\"alice@example.com\",\"Alice Liddell\",\"admin\"],[\"bob@example.com\",\"Robert\",\"user\"]]" }], }), conversion: scenario({ title: "fromPlainData, fromDatabase and toDatabase convert without saving, and reads convert their rows", body: CONVERSION, - printed: "{\"fromPlain\":[true,\"s3cret\",\"Ann\"],\"sameInstance\":true,\"storedShape\":[false,[\"_id\",\"_internal\",\"name\"],3,true],\"plainDataShape\":[false,[\"_id\",\"name\"]],\"nullIsUndefined\":[true,true],\"emptyObjectIsAnInstance\":true,\"copyNotSame\":[true,true,false,\"Cy\"],\"afterConversions\":0,\"read\":[true,\"hidden\"],\"direct\":[false,\"Dee\",true],\"getAllConverted\":true}", + printed: "{\"fromPlain\":[true,\"s3cret\",\"Ann\"],\"sameInstance\":true,\"storedShape\":[false,[\"_id\",\"_internal\",\"name\"],3,true],\"plainDataShape\":[false,[\"_id\",\"name\"]],\"nullIsUndefined\":[true,true],\"emptyObjectIsAnInstance\":true,\"copyNotSame\":[true,true,false,\"Cy\"],\"afterConversions\":0,\"read\":[true,\"hidden\"],\"direct\":[false,\"Dee\",true],\"getAllConverted\":true,\"getByConverted\":[1,true,\"hidden\",\"hidden\",\"hidden\"]}", stored: [{ name: "the one saved account", expression: "$DB.getCollection('cnv__accounts').find().toArray().map((r) => [r.name, Object.keys(r._internal.data), r._internal.data.secret.length])", expected: "[[\"Dee\",[\"secret\"],3]]" }], }), validation: scenario({ @@ -292,7 +300,7 @@ export const MODEL_JOURNEYS = { printed: "{\"cache\":[true,true,true,true,true,true,true],\"defaultSees\":[\"default\"],\"tenantSees\":[\"tenant one\"],\"otherSees\":[\"tenant two\"],\"everywhereSees\":[\"default\",\"tenant one\",\"tenant two\"],\"customSchema\":[true,true],\"lost\":\"Schema not found for 'absent-schema'\",\"statics\":[\"gm\",true]}", stored: [{ name: "the rows by instance", expression: "$DB.getCollection('gm__items').find().sort({ name: 1 }).toArray().map((r) => [r.name, r._instance])", expected: "[[\"default\",null],[\"tenant one\",\"t1\"],[\"tenant two\",\"t2\"]]" }], }), - "plain-data": scenario({ title: "toPlainData gives unlocked values without _internal, and leaves a still-locked field undefined", body: PLAIN_DATA, printed: "{\"keys\":[\"_id\",\"name\",\"password\",\"secret\",\"title\"],\"noInternal\":true,\"name\":\"Ann\",\"secret\":{\"token\":\"t\"},\"passwordIsHash\":true,\"lockedTitle\":\"undefined\",\"unlockedTitle\":\"Hello\",\"json\":{\"token\":\"t\"},\"stillLocked\":[\"password\",\"secret\",\"title\"],\"notAnInstance\":true}" }), + "plain-data": scenario({ title: "toPlainData gives unlocked values without _internal, and leaves a still-locked field undefined", body: PLAIN_DATA, printed: "{\"pathResults\":[[true,\"Ann\",{\"token\":\"t\"},true,\"Hello\"],[true,\"Ann\",{\"token\":\"t\"},true,\"Hello\"],[true,\"Ann\",{\"token\":\"t\"},true,\"Hello\"]],\"keys\":[\"_id\",\"name\",\"password\",\"secret\",\"title\"],\"noInternal\":true,\"name\":\"Ann\",\"secret\":{\"token\":\"t\"},\"passwordIsHash\":true,\"lockedTitle\":\"undefined\",\"unlockedTitle\":\"Hello\",\"json\":{\"token\":\"t\"},\"stillLocked\":[\"password\",\"secret\",\"title\"],\"notAnInstance\":true}" }), "compound-get-by": scenario({ title: "getBy matches a compound index of three fields by its keys in field order", body: COMPOUND, printed: "{\"exact\":[\"one\"],\"other\":[\"two\"],\"none\":[],\"order\":[]}" }), "partial-update-keeps-modified-fields": scenario({ title: "KNOWN FINDING AntelopeJS/interface-database-decorators#80, fails until fixed: update with one field keeps the row's other encrypted, hashed and localized fields", diff --git a/.agents/skills/verify-interface/scenarios/tables.ts b/.agents/skills/verify-interface/scenarios/tables.ts index ee02090..c85b40c 100644 --- a/.agents/skills/verify-interface/scenarios/tables.ts +++ b/.agents/skills/verify-interface/scenarios/tables.ts @@ -22,12 +22,15 @@ const INDEX_OPTIONS = ` const duplicate = refused && { name: refused.name, message: refused.message }; const recognised = [iface.IsDuplicateKeyError(refused), iface.IsDuplicateKeyError(new Error("other"))]; const otherInstance = await other.insert({ email: "a@x", first: "Ada", last: "Lovelace", age: 36 }); - const byGroup = (await model.getBy("name", "Ada", "Lovelace")).map((row) => row.email); - const bySingle = (await model.getBy("age", 36)).map((row) => row.first); + const groupRows = await model.getBy("name", "Ada", "Lovelace"); + const singleRows = await model.getBy("age", 36); + const converted = [groupRows.every((row) => row instanceof User), singleRows.every((row) => row instanceof User), groupRows.length, singleRows.length]; + const byGroup = groupRows.map((row) => row.email); + const bySingle = singleRows.map((row) => row.first); const between = (await rawTable("idx", "users").between("age", 30, 38)).map((row) => row.first); const symbolKey = caught(() => d.Index()({}, Symbol("k"))); const built = await untilIndexed("idx__users", ["email__i", "name__i", "age__i", "name"]); - return { indexed: built !== null, definition: definition.users, duplicate, recognised, otherInstanceStored: otherInstance.length, byGroup, bySingle, between, symbolKey }; + return { converted, indexed: built !== null, definition: definition.users, duplicate, recognised, otherInstanceStored: otherInstance.length, byGroup, bySingle, between, symbolKey }; `; const GROUP_FLAGS = ` @@ -245,7 +248,7 @@ export const TABLE_JOURNEYS = { "index-options": scenario({ title: "Field and Index reach the database as fields and plain, compound, cross-instance and unique indexes", body: INDEX_OPTIONS, - printed: "{\"indexed\":true,\"definition\":{\"fields\":{\"email\":\"string\",\"first\":\"string\",\"last\":\"string\",\"age\":\"number\"},\"indexes\":{\"email\":{\"fields\":null,\"unique\":true},\"name\":{\"fields\":[\"first\",\"last\"],\"crossInstance\":true},\"age\":{\"fields\":null}}},\"duplicate\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"email\\\" (email)\"},\"recognised\":[true,false],\"otherInstanceStored\":1,\"byGroup\":[\"a@x\"],\"bySingle\":[\"Ada\"],\"between\":[\"Ada\"],\"symbolKey\":\"@Index on Symbol(k): a database index names a field, never a symbol\"}", + printed: "{\"converted\":[true,true,1,1],\"indexed\":true,\"definition\":{\"fields\":{\"email\":\"string\",\"first\":\"string\",\"last\":\"string\",\"age\":\"number\"},\"indexes\":{\"email\":{\"fields\":null,\"unique\":true},\"name\":{\"fields\":[\"first\",\"last\"],\"crossInstance\":true},\"age\":{\"fields\":null}}},\"duplicate\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"email\\\" (email)\"},\"recognised\":[true,false],\"otherInstanceStored\":1,\"byGroup\":[\"a@x\"],\"bySingle\":[\"Ada\"],\"between\":[\"Ada\"],\"symbolKey\":\"@Index on Symbol(k): a database index names a field, never a symbol\"}", stored: [{ name: "the indexes of the users collection", expression: "$DB.getCollection('idx__users').getIndexes().map((i) => [i.name, i.unique === true]).sort()", expected: "[[\"_id_\",false],[\"age__i\",false],[\"email__i\",true],[\"name\",false],[\"name__i\",false]]" }], }), "group-flags": scenario({ title: "unique and crossInstance on any field of a group apply to the whole group", body: GROUP_FLAGS, printed: "{\"indexes\":{\"ab\":{\"fields\":[\"a\",\"b\"],\"unique\":true},\"cd\":{\"fields\":[\"c\",\"e\"],\"crossInstance\":true},\"ef\":{\"fields\":[\"f\",\"g\",\"h\"],\"crossInstance\":true},\"ij\":{\"fields\":[\"i\",\"j\"],\"unique\":true},\"plain\":{\"fields\":[\"k\",\"l\"]}},\"sameKey\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"ab\\\" (a, b)\"},\"otherSecond\":1}" }), diff --git a/skills/database-decorators-interface/SKILL.md b/skills/database-decorators-interface/SKILL.md index 28ccc14..77817bb 100644 --- a/skills/database-decorators-interface/SKILL.md +++ b/skills/database-decorators-interface/SKILL.md @@ -144,7 +144,7 @@ class Chef extends Table.with( to the whole group. - `@Fixture` data is only inserted by `RegisterSchema` when the table is empty (count === 0). - `GetModel` caches by model class + instanceId; the same pair always returns the same instance. -- `RegisterSchema` binds each registered table class to the `Schema` it builds; call it in the module that declares the tables. `User.table()`, `User.table(instanceId)` and `User.table(db)` then return the class's AQL table, and `GetModel` builds `BasicDataModel` models on that same schema, even when another module (bound to another database) calls them. An unbound class throws rather than looking a schema up by id. +- `RegisterSchema` binds each registered table class to the `Schema` it builds; call it in the module that declares the tables. `User.table()`, `User.table(instanceId)` and `User.table(db)` then return the class's AQL table, and `GetModel` builds `BasicDataModel` models on that same schema, even when another module (bound to another database) calls them. An unbound class follows the schema of its schema name when one exists, and throws ` is not bound to a schema` when none does. - Serialization of modified instances to plain JSON goes through `toPlainData` (auto-attached as `toJSON`). Deeper reference: this package's `docs/` chapters — Introduction, Table Definitions, Table Modifiers, From fe7ff45be553b9a7f68fd264cf7df6c6cc4fa7a8 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Wed, 4 Nov 2026 07:58:05 +0100 Subject: [PATCH 36/45] feat(skills): check batches, generator calls, mixed tables, trimming codecs and the quick start --- .../verify-interface/features/registration.md | 6 ++ .agents/skills/verify-interface/runner.ts | 6 +- .../verify-interface/scenarios/custom.ts | 7 +- .../scenarios/localization.ts | 5 +- .../verify-interface/scenarios/models.ts | 17 ++++- .../scenarios/registration.ts | 69 +++++++++++++++++++ 6 files changed, 104 insertions(+), 6 deletions(-) diff --git a/.agents/skills/verify-interface/features/registration.md b/.agents/skills/verify-interface/features/registration.md index 626cf6b..d064eff 100644 --- a/.agents/skills/verify-interface/features/registration.md +++ b/.agents/skills/verify-interface/features/registration.md @@ -13,6 +13,9 @@ journeys: - fixture-race - table-access - fixture-not-empty + - fixture-once + - fixture-mixed-tables + - quick-start - fixture-ids-mixed - table-foreign-instance --- @@ -26,6 +29,9 @@ journeys: - `register-schema`: provisioning, binding and the unknown-schema error. - `fixtures`: fixtures fill an empty table once, with generated ids and the insert conversion. - `fixture-race`: two starts write each row once; a subclass gets its parent's fixture. +- `fixture-once`: the generator runs once over several `RegisterSchema` calls. +- `fixture-mixed-tables`: only the empty table of a schema gets its fixture. +- `quick-start`: the docs/1 block, compiled and run as written. - `fixture-not-empty`: fixtures only fill an empty table. - `fixture-ids-mixed`: ids kept and positions for the rest. - `table-foreign-instance`: a held instance of another schema is used as is. diff --git a/.agents/skills/verify-interface/runner.ts b/.agents/skills/verify-interface/runner.ts index 37a1319..96025bf 100644 --- a/.agents/skills/verify-interface/runner.ts +++ b/.agents/skills/verify-interface/runner.ts @@ -26,6 +26,8 @@ export interface Scenario { stored?: Stored[]; /** Also loads `@antelopejs/api` on a free port, exposed to the body as `process.env.VERIFY_API_PORT`. */ api?: boolean; + /** Runs before the test with the module folder; the environment it returns is added to the run. */ + prepare?: (context: JourneyContext, folder: string) => Promise<{ [name: string]: string }>; } /** The helpers every scenario shares: the packed decorators, the database interface, and small helpers. */ @@ -99,7 +101,7 @@ function compact(text: string): string { } /** Runs a scenario in a real `ajs module test` with the packed decorators and a MongoDB module wired to the Docker service, then reads the database back through `mongosh`: the second view. */ -export function scenario({ title, body, printed, stored = [], api = false }: Scenario): Journey { +export function scenario({ title, body, printed, stored = [], api = false, prepare }: Scenario): Journey { return { title, entryPoint: "a module test run by `ajs module test` with the decorators, @antelopejs/mongodb and a MongoDB container", @@ -114,11 +116,13 @@ export function scenario({ title, body, printed, stored = [], api = false }: Sce context.edit("write the scenario", join(folder, "scenario.test.js"), `${PRELUDE}\nasync function main() {\n${body}\n}\n${TAIL}`); const cache = join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "antelopejs-verify", "ids-modules"); const apiPort = api ? String(await freePort()) : ""; + const extra = prepare === undefined ? {} : await prepare(context, folder); const env = { VERIFY_REPO: context.repo, VERIFY_MODULE_CACHE: cache, VERIFY_MONGO_URL: `mongodb://${mongo.host}:${mongo.port}`, VERIFY_API_PORT: apiPort, + ...extra, }; const ran = await context.command("run the scenario", join(context.repo, "node_modules", ".bin", "ajs"), ["module", "test", folder, "-f", join(folder, "scenario.test.js")], { cwd: folder, diff --git a/.agents/skills/verify-interface/scenarios/custom.ts b/.agents/skills/verify-interface/scenarios/custom.ts index 2cb75ea..0c44a69 100644 --- a/.agents/skills/verify-interface/scenarios/custom.ts +++ b/.agents/skills/verify-interface/scenarios/custom.ts @@ -162,6 +162,9 @@ const EVENTS = `${PRELUDE} const mark = () => log.splice(0); const [id] = await model.insert({ note: "n" }); const onInsert = phase("insert", mark()); + const batchIds = await model.insert([{ note: "x" }, { note: "y" }]); + const onBatch = phase("batch", mark()); + const batchStored = (await Promise.all(batchIds.map((batchId) => rawTable("events", "docs").get(batchId)))).map((row) => [row.note, row.handled]); await model.update(id, { note: "m" }); const onUpdate = phase("update", mark()); await model.get(id); @@ -174,7 +177,7 @@ const EVENTS = `${PRELUDE} const onPlain = phase("plain", mark()); const storedByHandler = (await rawTable("events", "docs").get(id)).handled ?? "undefined"; const plainAccessor = Object.getOwnPropertyDescriptor(Doc.prototype, "note"); - return { onInsert, onUpdate, onRead, onReadAll, onPlain, storedByHandler, eventsOnlyKeepPlainValue: plainAccessor === undefined }; + return { onInsert, onBatch, batchStored, onUpdate, onRead, onReadAll, onPlain, storedByHandler, eventsOnlyKeepPlainValue: plainAccessor === undefined }; `; const HELPERS = ` @@ -237,5 +240,5 @@ export const CUSTOM_JOURNEYS = { "metadata-class": scenario({ title: "a metadata class without inherit is shared with the subclass, one with inherit is copied, and upgrade runs on stored metadata", body: METADATA, printed: "{\"identity\":[true,true,true],\"shared\":[true,[\"x\"]],\"copied\":[false,[\"x\",\"y\"],[\"x\"]],\"upgrade\":[true,2,[\"legacy\"],true]}" }), "legacy-metadata": scenario({ title: "metadata stored without uniqueIndexes is upgraded and unique indexes still register", body: LEGACY_METADATA, printed: "{\"upgraded\":[true,true,true,\"olds\"],\"definition\":{\"code\":{\"fields\":null,\"unique\":true}},\"uniqueEnforced\":\"DuplicateKeyError\"}" }), "container-modifier": scenario({ title: "a ContainerModifier keeps values by key and treats the wildcard key as the whole record", body: CONTAINER, printed: "{\"both\":{\"a\":1,\"b\":2},\"afterOverwrite\":{\"a\":3,\"b\":2},\"replaced\":{\"z\":26},\"byKey\":[26,\"undefined\",\"undefined\"]}" }), - "modifier-events": scenario({ title: "a modifier's event methods run for inserts, updates and conversions, and an events-only modifier leaves the field plain", body: EVENTS, printed: "{\"onInsert\":[\"fromPlainData/note/true/Doc/1:note\",\"insert/note/true/Doc/0:\",\"toDatabase/note/true/Doc/1:_internal+note\"],\"onUpdate\":[\"fromPlainData/note/true/Doc/1:note\",\"update/note/true/Doc/0:\",\"toDatabase/note/true/Doc/1:_internal+note\"],\"onRead\":[\"fromDatabase/note/true/Doc/1:_id+_internal+handled+note\"],\"onReadAll\":[\"fromDatabase/note/true/Doc/1:_id+_internal+handled+note\"],\"onPlain\":[\"toPlainData/note/true/Doc/1:_id+handled+note\"],\"storedByHandler\":\"by-handler\",\"eventsOnlyKeepPlainValue\":true}" }), + "modifier-events": scenario({ title: "a modifier's event methods run for inserts, updates and conversions, and an events-only modifier leaves the field plain", body: EVENTS, printed: "{\"onInsert\":[\"fromPlainData/note/true/Doc/1:note\",\"insert/note/true/Doc/0:\",\"toDatabase/note/true/Doc/1:_internal+note\"],\"onBatch\":[\"fromPlainData/note/true/Doc/1:note\",\"insert/note/true/Doc/0:\",\"toDatabase/note/true/Doc/1:_internal+note\",\"fromPlainData/note/true/Doc/1:note\",\"insert/note/true/Doc/0:\",\"toDatabase/note/true/Doc/1:_internal+note\"],\"batchStored\":[[\"x\",\"by-handler\"],[\"y\",\"by-handler\"]],\"onUpdate\":[\"fromPlainData/note/true/Doc/1:note\",\"update/note/true/Doc/0:\",\"toDatabase/note/true/Doc/1:_internal+note\"],\"onRead\":[\"fromDatabase/note/true/Doc/1:_id+_internal+handled+note\"],\"onReadAll\":[\"fromDatabase/note/true/Doc/1:_id+_internal+handled+note\",\"fromDatabase/note/true/Doc/1:_id+_internal+handled+note\",\"fromDatabase/note/true/Doc/1:_id+_internal+handled+note\"],\"onPlain\":[\"toPlainData/note/true/Doc/1:_id+handled+note\"],\"storedByHandler\":\"by-handler\",\"eventsOnlyKeepPlainValue\":true}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/localization.ts b/.agents/skills/verify-interface/scenarios/localization.ts index 11bb7fd..f857a8c 100644 --- a/.agents/skills/verify-interface/scenarios/localization.ts +++ b/.agents/skills/verify-interface/scenarios/localization.ts @@ -114,9 +114,12 @@ const AUTO = ` await sleep(60); await model.update(id, { title: "Two", creationDate: new Date(0) }); const updated = await rawTable("auto", "articles").get(id); + const batch = await model.insert([{ title: "B1", creationDate: new Date(0) }, { title: "B2" }, { title: "B3", updateDate: new Date(0) }]); + const batchRows = await Promise.all(batch.map((batchId) => rawTable("auto", "articles").get(batchId))); const after = Date.now(); const stamp = (value) => new Date(value).getTime(); return { + batchStamped: batchRows.map((row) => [row.title, stamp(row.creationDate) >= before, stamp(row.updateDate) >= before, stamp(row.born) >= before, stamp(row.touched) >= before]), insertSetsBoth: [stamp(inserted.creationDate) >= before, stamp(inserted.updateDate) >= before, stamp(inserted.creationDate) <= after], creationKeptOnUpdate: stamp(updated.creationDate) === stamp(inserted.creationDate), updateRefreshed: stamp(updated.updateDate) >= stamp(inserted.updateDate) + 50, @@ -192,7 +195,7 @@ export const AUTODATE_JOURNEYS = { autodate: scenario({ title: "CreationTime is set on insert and kept on update, UpdateTime is set on insert and refreshed on update", body: AUTO, - printed: "{\"insertSetsBoth\":[true,true,true],\"creationKeptOnUpdate\":true,\"updateRefreshed\":true,\"secondFieldsToo\":[true,true,true,true],\"noMixinNeeded\":true}", + printed: "{\"batchStamped\":[[\"B1\",true,true,true,true],[\"B2\",true,true,true,true],[\"B3\",true,true,true,true]],\"insertSetsBoth\":[true,true,true],\"creationKeptOnUpdate\":true,\"updateRefreshed\":true,\"secondFieldsToo\":[true,true,true,true],\"noMixinNeeded\":true}", stored: [{ name: "the article dates", expression: "$DB.getCollection('auto__articles').find().toArray().map((r) => [r.title, r.creationDate instanceof Date, r.updateDate > r.creationDate])", expected: "[[\"Two\",true,true]]" }], }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/models.ts b/.agents/skills/verify-interface/scenarios/models.ts index f588b15..4e8d4ee 100644 --- a/.agents/skills/verify-interface/scenarios/models.ts +++ b/.agents/skills/verify-interface/scenarios/models.ts @@ -103,12 +103,25 @@ const VALIDATION = ` const missingRequired = await errorOf(people.insert({ name: "Alice" }, { validate: true })); const missingInArray = await errorOf(people.insert([{ name: "Ok", age: 1 }, { name: "No age" }], { validate: true })); const missingStored = (await people.getAll()).map((row) => row.name).sort(); + const trim = { _tag: "codec", name: "trim", is: (u) => typeof u === "string", encode: (a) => a, decode: (u) => (typeof u === "string" ? { _tag: "Right", right: u.trim() } : { _tag: "Left", left: [{ message: "not text" }] }) }; + class Label extends d.Table {} + decorate(Label, { label: [d.Field(trim)] }); + d.RegisterTable("labels", "val")(Label); + await d.RegisterSchema("val"); + const labels = d.GetModel(d.BasicDataModel(Label)); + const [padded] = await labels.insert({ label: " padded " }, { validate: true }); + const [batchA, batchB] = await labels.insert([{ label: " a " }, { label: " b " }], { validate: true }); + const afterInsert = (await rawTable("val", "labels").get(padded)).label; + await labels.update(padded, { label: " again " }, { validate: true }); + const [unvalidated] = await labels.insert({ label: 42 }, { validate: false }); + await labels.update(unvalidated, { label: 43 }, { validate: false }); + const storedLabels = await Promise.all([padded, batchA, batchB, unvalidated].map(async (labelId) => (await rawTable("val", "labels").get(labelId)).label)); const hole = { _tag: "codec", name: "hole", is: () => false, encode: (a) => a, decode: () => ({ _tag: "Left", left: [{}, { message: "after a hole" }, { message: "last" }] }) }; const silent = { _tag: "codec", name: "silent", is: () => false, encode: (a) => a, decode: () => ({ _tag: "Left", left: [{}, { message: "" }] }) }; class Odd extends d.Table {} decorate(Odd, { a: [d.Field(hole)], b: [d.Field(silent)] }); const oddMessages = d.BasicDataModel(Odd, "odd").validate({}); - return { valid, invalid, notAnObject, partialOk, partialEmpty, partialBad, tokenSkipped, optIn: optIn.length, refused, batchRefused, patched, patchRefused, objectPatchRefused, unvalidatedPatch, oddMessages, missingRequired, missingInArray, missingStored, presentUndefined, absentAge }; + return { valid, invalid, notAnObject, partialOk, partialEmpty, partialBad, tokenSkipped, optIn: optIn.length, refused, batchRefused, patched, patchRefused, objectPatchRefused, unvalidatedPatch, oddMessages, missingRequired, missingInArray, missingStored, afterInsert, storedLabels, presentUndefined, absentAge }; `; const GET_MODEL = ` @@ -291,7 +304,7 @@ export const MODEL_JOURNEYS = { validation: scenario({ title: "validate checks the io-ts codecs, and insert and update refuse invalid input only when asked", body: VALIDATION, - printed: "{\"valid\":{\"ok\":true},\"invalid\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"notAnObject\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"partialOk\":{\"ok\":true},\"partialEmpty\":{\"ok\":true},\"partialBad\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"tokenSkipped\":{\"ok\":true},\"optIn\":1,\"refused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"batchRefused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"patched\":1,\"patchRefused\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"objectPatchRefused\":{\"message\":\"validation failed: name (expected a string)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"}]},\"unvalidatedPatch\":1,\"oddMessages\":{\"ok\":false,\"errors\":[{\"field\":\"a\",\"message\":\"after a hole\"},{\"field\":\"b\",\"message\":\"invalid b\"}]},\"missingRequired\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"missingInArray\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"missingStored\":[42,\"Bob\"],\"presentUndefined\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"absentAge\":{\"ok\":true}}", + printed: "{\"valid\":{\"ok\":true},\"invalid\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"notAnObject\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"partialOk\":{\"ok\":true},\"partialEmpty\":{\"ok\":true},\"partialBad\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"tokenSkipped\":{\"ok\":true},\"optIn\":1,\"refused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"batchRefused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"patched\":1,\"patchRefused\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"objectPatchRefused\":{\"message\":\"validation failed: name (expected a string)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"}]},\"unvalidatedPatch\":1,\"oddMessages\":{\"ok\":false,\"errors\":[{\"field\":\"a\",\"message\":\"after a hole\"},{\"field\":\"b\",\"message\":\"invalid b\"}]},\"missingRequired\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"missingInArray\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"missingStored\":[42,\"Bob\"],\"afterInsert\":\" padded \",\"storedLabels\":[\" again \",\" a \",\" b \",43],\"presentUndefined\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"absentAge\":{\"ok\":true}}", stored: [{ name: "the people", expression: "$DB.getCollection('val__people').find().sort({ name: 1 }).toArray().map((r) => [r.name, r.age])", expected: "[[42,\"anything\"],[\"Bob\",\"old\"]]" }], }), "get-model": scenario({ diff --git a/.agents/skills/verify-interface/scenarios/registration.ts b/.agents/skills/verify-interface/scenarios/registration.ts index af497d4..0b7b113 100644 --- a/.agents/skills/verify-interface/scenarios/registration.ts +++ b/.agents/skills/verify-interface/scenarios/registration.ts @@ -1,3 +1,6 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + import type { Journey } from "@antelopejs/tooling-configs/verify"; import { scenario } from "../runner.ts"; @@ -152,6 +155,38 @@ const FOREIGN = ` return { homeCount: await Item.table().count(), awayCount: await Item.table(away).count() }; `; +const FIXTURE_ONCE = ` + let calls = 0; + class Counted extends d.Table {} + decorate(Counted, { name: [d.Field("string")] }); + d.Fixture(() => { calls += 1; return [{ name: "once" }]; })(Counted); + d.RegisterTable("counted", "once")(Counted); + await d.RegisterSchema("once"); + const afterFirst = [calls, (await rawTable("once", "counted")).length]; + await d.RegisterSchema("once"); + await d.RegisterSchema("once"); + return { afterFirst, afterThree: [calls, (await rawTable("once", "counted")).length] }; +`; + +const FIXTURE_MIXED_TABLES = ` + class Filled extends d.Table {} + decorate(Filled, { name: [d.Field("string")] }); + d.RegisterTable("filled", "twotables")(Filled); + await d.RegisterSchema("twotables"); + await d.GetModel(d.BasicDataModel(Filled)).insert({ _id: "mine", name: "mine" }); + class Fixtured extends d.Table {} + decorate(Fixtured, { name: [d.Field("string")] }); + d.Fixture(() => [{ name: "seed" }])(Fixtured); + d.RegisterTable("empties", "twotables")(Fixtured); + class FilledFixtured extends d.Table {} + decorate(FilledFixtured, { name: [d.Field("string")] }); + d.Fixture(() => [{ _id: "fixture-for-filled", name: "must not appear" }])(FilledFixtured); + d.RegisterTable("filled", "twotables")(FilledFixtured); + await d.RegisterSchema("twotables"); + const ids = async (table) => (await rawTable("twotables", table)).map((row) => row._id).sort(); + return { filled: await ids("filled"), empties: await ids("empties") }; +`; + const FIXTURE_NOT_EMPTY = ` class Plain extends d.Table {} decorate(Plain, { name: [d.Field("string")] }); @@ -192,6 +227,40 @@ export const REGISTRATION_JOURNEYS = { printed: "{\"rows\":[\"seeds:fixture:0\",\"seeds:fixture:1\"],\"kids\":[\"kids:fixture:0\",\"kids:fixture:1\"],\"generatorReceived\":\"SpyChild\"}", stored: [{ name: "the seed rows", expression: "$DB.getCollection('race__seeds').find().sort({ _id: 1 }).toArray().map((r) => [r._id, r.name])", expected: "[[\"seeds:fixture:0\",\"a\"],[\"seeds:fixture:1\",\"b\"]]" }], }), + "fixture-once": scenario({ title: "a fixture generator runs once, on the first RegisterSchema that finds its table empty", body: FIXTURE_ONCE, printed: "{\"afterFirst\":[1,1],\"afterThree\":[1,1]}" }), + "fixture-mixed-tables": scenario({ title: "in a schema with a filled table and an empty one, only the empty table gets its fixture", body: FIXTURE_MIXED_TABLES, printed: "{\"filled\":[\"mine\"],\"empties\":[\"empties:fixture:0\"]}" }), + "quick-start": scenario({ + title: "the docs/1 quick start, compiled as written, saves and reads back a note", + body: `const quick = require(process.env.VERIFY_QUICKSTART);\n return { title: await quick.tryNote() };`, + printed: "{\"title\":\"Learn models\"}", + async prepare(context, folder) { + const page = readFileSync(join(context.repo, "docs", "1.introduction.md"), "utf8"); + const block: string | undefined = /```ts docs-check\n([\s\S]*?)```/.exec(page)?.[1]; + if (block === undefined) throw new Error("docs/1 has no docs-check block"); + const source = join(folder, "quick-start.ts"); + context.edit("write the quick start as the docs show it", source, block.replace('"@antelopejs/interface-database-decorators"', JSON.stringify(join(context.repo, "dist", "index")))); + const compiled = await context.command("compile the quick start", "node", [ + join(context.repo, "node_modules", "typescript", "bin", "tsc"), + "--strict", + "--skipLibCheck", + "--experimentalDecorators", + "--emitDecoratorMetadata", + "--target", + "es2022", + "--module", + "commonjs", + "--moduleResolution", + "node", + "--typeRoots", + join(context.repo, "node_modules", "@types"), + "--types", + "node", + source, + ]); + context.expect(compiled.folder, [{ name: "the docs/1 block compiles", pass: compiled.exitCode === 0, detail: compiled.stdout.slice(0, 400) }], { critical: true }); + return { VERIFY_QUICKSTART: join(folder, "quick-start.js") }; + }, + }), "fixture-not-empty": scenario({ title: "a table that already holds a row gets no fixture rows, and an emptied table gets them", body: FIXTURE_NOT_EMPTY, printed: "{\"whileNotEmpty\":[\"unrelated\"],\"afterEmptied\":[\"fixture-a\",\"fixture-b\"]}" }), "fixture-ids-mixed": scenario({ title: "a fixture mixing rows with and without an id keeps the ids it has and numbers the others by position", From 5e437769b1bb312503a314107b0ca641113e410f Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Wed, 4 Nov 2026 12:19:13 +0100 Subject: [PATCH 37/45] fix(skills): read the batch rows back in the autodate journey --- .agents/skills/verify-interface/scenarios/localization.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/skills/verify-interface/scenarios/localization.ts b/.agents/skills/verify-interface/scenarios/localization.ts index f857a8c..091c88e 100644 --- a/.agents/skills/verify-interface/scenarios/localization.ts +++ b/.agents/skills/verify-interface/scenarios/localization.ts @@ -196,6 +196,6 @@ export const AUTODATE_JOURNEYS = { title: "CreationTime is set on insert and kept on update, UpdateTime is set on insert and refreshed on update", body: AUTO, printed: "{\"batchStamped\":[[\"B1\",true,true,true,true],[\"B2\",true,true,true,true],[\"B3\",true,true,true,true]],\"insertSetsBoth\":[true,true,true],\"creationKeptOnUpdate\":true,\"updateRefreshed\":true,\"secondFieldsToo\":[true,true,true,true],\"noMixinNeeded\":true}", - stored: [{ name: "the article dates", expression: "$DB.getCollection('auto__articles').find().toArray().map((r) => [r.title, r.creationDate instanceof Date, r.updateDate > r.creationDate])", expected: "[[\"Two\",true,true]]" }], + stored: [{ name: "the article dates", expression: "$DB.getCollection('auto__articles').find().sort({ title: 1 }).toArray().map((r) => [r.title, r.creationDate instanceof Date, r.updateDate instanceof Date, r.born instanceof Date, r.touched instanceof Date])", expected: "[[\"B1\",true,true,true,true],[\"B2\",true,true,true,true],[\"B3\",true,true,true,true],[\"Two\",true,true,true,true]]" }], }), } satisfies { [id: string]: Journey }; From 1bb742101f45611e94d25ae3b145ce6c8588446e Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Wed, 4 Nov 2026 13:00:20 +0100 Subject: [PATCH 38/45] feat(skills): measure scrypt cost and loop blocking, forward relations and index-only properties --- .../verify-interface/features/hashing.md | 2 ++ .../verify-interface/features/tables.md | 2 ++ .../verify-interface/scenarios/hashing.ts | 27 +++++++++++++++++++ .../verify-interface/scenarios/tables.ts | 14 +++++++++- 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/.agents/skills/verify-interface/features/hashing.md b/.agents/skills/verify-interface/features/hashing.md index 0bea867..db750ba 100644 --- a/.agents/skills/verify-interface/features/hashing.md +++ b/.agents/skills/verify-interface/features/hashing.md @@ -11,6 +11,7 @@ journeys: - hash-bounds - hash-memory - hash-sync-async + - hash-cost - hash-outdated - hash-digest --- @@ -26,6 +27,7 @@ journeys: - `hash-bounds`: both edges of the `ln`, `r`, `p`, salt and key ranges. - `hash-memory`: the 128 MiB limit at, under and over the line. - `hash-sync-async`: `testHash` equals `testHashAsync` on current, legacy, low-cost, junk, empty and digest rows. +- `hash-cost`: one scrypt run per test, none for a digest, async off the loop and sync on it. Ratios and tick counts only, retried up to three times. - `hash-outdated`: `isHashOutdated` for a changed `p`, `r` and `ln`. - `hash-limits`: the limits a stored scrypt string must stay inside. - `hash-upgrade`: older formats verify, are flagged and are rewritten. diff --git a/.agents/skills/verify-interface/features/tables.md b/.agents/skills/verify-interface/features/tables.md index f3678c9..5557565 100644 --- a/.agents/skills/verify-interface/features/tables.md +++ b/.agents/skills/verify-interface/features/tables.md @@ -10,6 +10,7 @@ journeys: - group-flags - field-types - relation-metadata + - forward-relation - inheritance - table-with - table-with-subclass @@ -27,6 +28,7 @@ A table class with `Field`, `Index`, `Relation`, subclasses and `Table.with`. - `index-options`: plain, compound, cross-instance and unique indexes reach the provider and MongoDB. - `group-flags`: `unique` and `crossInstance` on one field apply to the whole group. - `field-types`: every field type goes through unchanged, in either decorator order. +- `forward-relation`: the thunk allows forward references. - `relation-metadata`: `Relation` records links and enforces none. - `inheritance`: a subclass copies its parent and never writes into it. - `combine-modifiers`: three modifiers on one table. diff --git a/.agents/skills/verify-interface/scenarios/hashing.ts b/.agents/skills/verify-interface/scenarios/hashing.ts index e9bd71d..297ad01 100644 --- a/.agents/skills/verify-interface/scenarios/hashing.ts +++ b/.agents/skills/verify-interface/scenarios/hashing.ts @@ -214,6 +214,32 @@ const SYNC_ASYNC = `${HASH_HELPERS} return matrix; `; +const COST = `${HASH_HELPERS} + const seedRow = async (id, data, meta = {}) => rawTable("hash", "accounts").insert({ _id: id, name: id, _internal: { meta, data } }).run(); + await seedRow("junk", { password: "plain text" }); + await seedRow("empty", {}); + await seedRow("digest", { pin: digest("sha256", "pin-salt", "hunter2") }, { HashModifier: { salt: "pin-salt" } }); + const [fresh] = await accounts.insert({ name: "fresh", password: "hunter2" }); + const median = (list) => [...list].sort((a, b) => a - b)[Math.floor(list.length / 2)]; + const timed = async (id, field) => { const row = await accounts.get(id); const runs = []; for (let i = 0; i < 3; i += 1) { const start = process.hrtime.bigint(); await row.testHashAsync(field, "wrong"); runs.push(Number(process.hrtime.bigint() - start) / 1e6); } return median(runs); }; + const measure = async () => { + const hashed = await timed(fresh, "password"); + const empty = await timed("empty", "password"); + const junk = await timed("junk", "password"); + const digest = await timed("digest", "pin"); + return { emptyVsHashed: empty / hashed >= 0.5, junkVsHashed: junk / hashed >= 0.5, digestCheaper: digest / hashed < 0.5 }; + }; + let cost = await measure(); + for (let attempt = 0; attempt < 2 && !Object.values(cost).every(Boolean); attempt += 1) cost = await measure(); + const row = await accounts.get(fresh); + const loopTicks = async (work) => { let ticks = 0; const timer = setInterval(() => { ticks += 1; }, 1); try { await work(); } finally { clearInterval(timer); } return ticks; }; + let asyncTicks = 0; + for (let attempt = 0; attempt < 3 && asyncTicks <= 2; attempt += 1) asyncTicks = await loopTicks(() => row.testHashAsync("password", "hunter2")); + let syncTicks = -1; + { let ticks = 0; const timer = setInterval(() => { ticks += 1; }, 1); row.testHash("password", "hunter2"); syncTicks = ticks; clearInterval(timer); } + return { ...cost, asyncKeepsLoopRunning: asyncTicks > 2, syncBlocksLoop: syncTicks === 0 }; +`; + const OUTDATED = `${HASH_HELPERS} const seed = async (id, password) => rawTable("hash", "accounts").insert({ _id: id, name: id, _internal: { meta: {}, data: { password } } }).run(); const probe = async (id, stored) => { await seed(id, stored); const row = await accounts.get(id); return [await row.testHashAsync("password", "pw"), row.isHashOutdated("password")]; }; @@ -246,6 +272,7 @@ export const HASH_JOURNEYS = { "hash-bounds": scenario({ title: "a stored scrypt string verifies at each edge of the documented ranges and is refused just outside", body: BOUNDS, printed: "{\"r1\":[true,false],\"r10\":[true,false],\"r99\":[true,false],\"r100\":[false,false],\"r0\":[false,false],\"ln0\":[false,false],\"p0\":[false,false],\"p99\":[false,false],\"ln16r1\":[false,false],\"salt65\":[false,false],\"ln1\":[true,false],\"ln2\":[true,false],\"ln99\":[false,false],\"ln100\":[false,false],\"p1\":[true,false],\"p16\":[true,false],\"salt16\":[true,false],\"salt64\":[true,false],\"salt80\":[false,false],\"salt15\":[false,false],\"key16\":[true,false],\"key64\":[true,false],\"key15\":[false,false],\"key65\":[false,false]}" }), "hash-memory": scenario({ title: "a stored scrypt string verifies up to 128 MiB of memory (128 x N x r) and is refused above it", body: MEMORY, printed: "{\"exactR64\":[true,false],\"overR65\":[false,false],\"exactR2\":[true,false],\"underR3\":[true,false],\"overR5\":[false,false],\"overR9\":[false,false]}" }), "hash-sync-async": scenario({ title: "testHash answers exactly what testHashAsync answers, for every kind of stored value", body: SYNC_ASYNC, printed: "{\"fresh:password\":[[true,true,true],[false,false,true],[false,false,true]],\"fresh:pin\":[[true,true,true],[false,false,true],[false,false,true]],\"legacy:password\":[[true,true,true],[false,false,true],[false,false,true]],\"lowcost:password\":[[true,true,true],[false,false,true],[false,false,true]],\"junk:password\":[[false,false,true],[false,false,true],[false,false,true]],\"empty:password\":[[false,false,true],[false,false,true],[false,false,true]],\"digest:pin\":[[true,true,true],[false,false,true],[false,false,true]],\"scryptOnDigest:pin\":[[true,true,true],[false,false,true],[false,false,true]]}" }), + "hash-cost": scenario({ title: "a test spends one scrypt run whatever the row holds, a digest field spends none, testHashAsync leaves the event loop running and testHash blocks it", body: COST, printed: "{\"emptyVsHashed\":true,\"junkVsHashed\":true,\"digestCheaper\":true,\"asyncKeepsLoopRunning\":true,\"syncBlocksLoop\":true}" }), "hash-outdated": scenario({ title: "isHashOutdated flags any scrypt string whose ln, r or p differs from the current cost", body: OUTDATED, printed: "{\"fresh\":[true,false],\"sameCost\":[true,false],\"lowerP\":[true,true],\"higherR\":[true,true],\"lowerLn\":[true,true],\"notScrypt\":[false,true]}" }), "hash-digest": scenario({ title: "a field with an algorithm stores a salted digest with the row's shared salt", body: DIGEST, printed: "{\"algorithms\":[true,true,false,88,24],\"others\":[true,true],\"algorithmTests\":[true,true,false],\"wrongDigest\":false,\"storedDigest\":true,\"sharedSalt\":true,\"saltIsText\":true,\"freshSaltPerRow\":true,\"saltReusedOnLaterWrite\":[true,true],\"testDigest\":[true,false,true],\"digestNeverOutdated\":false,\"scryptOnDigestField\":[true,false],\"otherDigestRefused\":false}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/tables.ts b/.agents/skills/verify-interface/scenarios/tables.ts index c85b40c..55533c0 100644 --- a/.agents/skills/verify-interface/scenarios/tables.ts +++ b/.agents/skills/verify-interface/scenarios/tables.ts @@ -67,6 +67,8 @@ const FIELD_TYPES = ` address: [d.Field({ street: "string", geo: { lat: "number", lng: "number" } })], total: [d.Field(codec), d.Index()], lateIndex: [d.Index(), d.Field("number")], + slugOnly: [d.Index()], + stamped: [d.CreationTime()], plain: [], }); d.RegisterTable("orders", "fld")(Order); @@ -80,9 +82,18 @@ const FIELD_TYPES = ` codecIsTheSameObject: fields.total === codec, lateIndexType: fields.lateIndex, indexes: Object.keys(iface.Schema.get("fld").definition.orders.indexes), + leftOut: ["slugOnly", "stamped", "plain"].map((name) => name in fields), }; `; +const FORWARD_RELATION = ` + class Early extends d.Table {} + decorate(Early, { laterId: [d.Field("string"), d.Relation({ to: () => Later })] }); + class Later extends d.Table {} + const relation = d.getMetadata(Early, d.RelationStaticMetadata).relations.laterId; + return { resolves: relation.to() === Later, toField: relation.toField ?? "undefined" }; +`; + const RELATION = ` class Author extends d.Table {} decorate(Author, { name: [d.Field("string")] }); @@ -252,7 +263,8 @@ export const TABLE_JOURNEYS = { stored: [{ name: "the indexes of the users collection", expression: "$DB.getCollection('idx__users').getIndexes().map((i) => [i.name, i.unique === true]).sort()", expected: "[[\"_id_\",false],[\"age__i\",false],[\"email__i\",true],[\"name\",false],[\"name__i\",false]]" }], }), "group-flags": scenario({ title: "unique and crossInstance on any field of a group apply to the whole group", body: GROUP_FLAGS, printed: "{\"indexes\":{\"ab\":{\"fields\":[\"a\",\"b\"],\"unique\":true},\"cd\":{\"fields\":[\"c\",\"e\"],\"crossInstance\":true},\"ef\":{\"fields\":[\"f\",\"g\",\"h\"],\"crossInstance\":true},\"ij\":{\"fields\":[\"i\",\"j\"],\"unique\":true},\"plain\":{\"fields\":[\"k\",\"l\"]}},\"sameKey\":{\"name\":\"DuplicateKeyError\",\"message\":\"Duplicate key on index \\\"ab\\\" (a, b)\"},\"otherSecond\":1}" }), - "field-types": scenario({ title: "Field hands every type to the database unchanged and combines with Index in either order", body: FIELD_TYPES, printed: "{\"names\":[\"id\",\"tags\",\"address\",\"total\",\"lateIndex\"],\"id\":\"string\",\"tags\":[\"string\"],\"address\":{\"street\":\"string\",\"geo\":{\"lat\":\"number\",\"lng\":\"number\"}},\"codecIsTheSameObject\":true,\"lateIndexType\":\"number\",\"indexes\":[\"total\",\"lateIndex\"]}" }), + "forward-relation": scenario({ title: "a Relation thunk is not called while the decorator runs, so it can name a class declared later", body: FORWARD_RELATION, printed: "{\"resolves\":true,\"toField\":\"undefined\"}" }), + "field-types": scenario({ title: "Field hands every type to the database unchanged and combines with Index in either order", body: FIELD_TYPES, printed: "{\"names\":[\"id\",\"tags\",\"address\",\"total\",\"lateIndex\"],\"id\":\"string\",\"tags\":[\"string\"],\"address\":{\"street\":\"string\",\"geo\":{\"lat\":\"number\",\"lng\":\"number\"}},\"codecIsTheSameObject\":true,\"lateIndexType\":\"number\",\"indexes\":[\"total\",\"lateIndex\",\"slugOnly\"],\"leftOut\":[false,false,false]}" }), "relation-metadata": scenario({ title: "Relation records links without enforcing them", body: RELATION, printed: "{\"relationFields\":[\"authorId\",\"tagIds\"],\"authorTarget\":true,\"authorToField\":null,\"tagMany\":true,\"tagToField\":\"name\",\"dangling\":[\"nobody\",[\"ghost\"]],\"fieldsKept\":[\"authorId\",\"tagIds\",\"title\"],\"childRelations\":[\"authorId\",\"tagIds\",\"editorId\"],\"parentRelations\":[\"authorId\",\"tagIds\"]}" }), inheritance: scenario({ title: "a table subclass starts with a copy of its parent and never changes it", body: INHERITANCE, printed: "{\"late\":[true,true,\"crosses\"],\"built\":[{\"fields\":[\"a\",\"b\"],\"crossInstance\":true,\"unique\":true},{\"fields\":[\"a\",\"b\"],\"crossInstance\":true,\"unique\":true}],\"base\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\"],\"indexes\":[],\"unique\":[],\"cross\":[]},\"person\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\"],\"indexes\":[\"email\"],\"unique\":[\"email\"],\"cross\":[]},\"employee\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"email\",\"badge\"],\"indexes\":[\"email\",\"badge\"],\"unique\":[\"email\"],\"cross\":[]},\"contractor\":{\"table\":\"people\",\"schema\":\"inh\",\"fields\":[\"createdAt\",\"rate\"],\"indexes\":[],\"unique\":[],\"cross\":[]},\"cross\":{\"table\":\"crosses\",\"schema\":\"inh\",\"fields\":[\"a\",\"b\",\"c\"],\"indexes\":[\"ab\",\"c\"],\"unique\":[\"ab\"],\"cross\":[\"ab\"]},\"sub\":{\"table\":\"subs\",\"schema\":\"inh\",\"fields\":[\"a\",\"b\",\"c\",\"extra\"],\"indexes\":[\"ab\",\"c\"],\"unique\":[\"ab\"],\"cross\":[\"ab\"]}}" }), "combine-modifiers": scenario({ title: "Table.with combines the encryption, hashing and localization modifiers on one table", body: COMBINE, printed: "{\"stored\":[\"bio\",\"password\",\"privateInfo\"],\"privateInfo\":{\"address\":\"1 Main St\",\"phoneNumber\":\"555\"},\"password\":[true,false],\"bio\":[\"Bonjour\",\"Hello\"],\"mixins\":[\"function\",\"function\"]}" }), From 1c18cfb892b769af4fb077aa9208230d7de8ece3 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Thu, 5 Nov 2026 06:45:28 +0100 Subject: [PATCH 39/45] chore: sync AGENTS.md with the verify skill --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index a803ecc..a7e021d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,6 +33,8 @@ The release commit writes `CHANGELOG.md`. Upgrade notes for that version follow Finish every change with `pnpm run checks`. It runs typecheck, lint, format, knip, tests, size, suppressions, and sync, and prints only what failed. The change is done when it prints no failure. +Prove a change works with the verification skill in [`.agents/skills/verify-interface/SKILL.md`](.agents/skills/verify-interface/SKILL.md) before you call it done. + The tools enforce most rules, listed in the table at the end. The sections below hold the rules no tool checks. ### Comments From e73eff9abea0de05ce337d989539bd44cb84f825 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Thu, 5 Nov 2026 13:40:39 +0000 Subject: [PATCH 40/45] feat(skills): pin the validation Error, field-less tables, getBy key types, the row aliases and the sync scrypt run --- .../verify-interface/features/README.md | 4 ++-- .../verify-interface/features/hashing.md | 2 +- .../verify-interface/features/models.md | 4 +++- .../verify-interface/features/registration.md | 2 +- .../scenarios/declarations.ts | 6 +++++ .../verify-interface/scenarios/hashing.ts | 15 +++++++----- .../verify-interface/scenarios/models.ts | 23 +++++++++++++++++-- .../scenarios/registration.ts | 11 ++++++--- 8 files changed, 51 insertions(+), 16 deletions(-) diff --git a/.agents/skills/verify-interface/features/README.md b/.agents/skills/verify-interface/features/README.md index b885a97..5398acc 100644 --- a/.agents/skills/verify-interface/features/README.md +++ b/.agents/skills/verify-interface/features/README.md @@ -24,8 +24,8 @@ Every journey runs a scenario inside `ajs module test` with `@antelopejs/mongodb |---|---| | The minor-bump rule of the interface kind | Fails at the current head, not at a change: `campaign/integration` exports more than the published 0.1.12 and `package.json` is still 0.1.12. Add it to `smoke` in the PR that bumps the version. | | A model "stays on the declaring module's database even when another module calls `GetModel`" (docs/4, GetModel) | Needs two modules with two databases behind one provider; one container serves one database here. | -| `testHash` and `testHashAsync` spend one scrypt run whatever the row holds, and the time of a refusal does not tell the row's format (docs/3, HashModifier) | A timing claim; a journey comparing wall-clock times would be flaky on a shared machine. | -| A digest field's test spends no scrypt run (docs/3) | Same: a timing claim. | +| `testHash` and `testHashAsync` spend one scrypt run whatever the row holds, and the time of a refusal does not tell the row's format (docs/3, HashModifier) | Driven (`hashing/hash-cost`) by ratios against one async run, retried up to three times, not by absolute times. | +| A digest field's test spends no scrypt run (docs/3) | Driven (`hashing/hash-cost`), by the same ratios. | | A stored scrypt string with `ln` of at least 16 × `r` refuses every candidate (docs/3) | Node's own scrypt cannot compute such a hash, so no correct candidate exists to refuse; the boundary just below it is driven. | | Encrypted fields keep whole-second dates only (docs/3) | Driven (`encryption-dates`). | | `Fixture` generator for a class that does not extend the decorated one throws "does not extend" | Not reachable through the public API; the generator is wrapped by the decorator. | diff --git a/.agents/skills/verify-interface/features/hashing.md b/.agents/skills/verify-interface/features/hashing.md index db750ba..f013517 100644 --- a/.agents/skills/verify-interface/features/hashing.md +++ b/.agents/skills/verify-interface/features/hashing.md @@ -27,7 +27,7 @@ journeys: - `hash-bounds`: both edges of the `ln`, `r`, `p`, salt and key ranges. - `hash-memory`: the 128 MiB limit at, under and over the line. - `hash-sync-async`: `testHash` equals `testHashAsync` on current, legacy, low-cost, junk, empty and digest rows. -- `hash-cost`: one scrypt run per test, none for a digest, async off the loop and sync on it. Ratios and tick counts only, retried up to three times. +- `hash-cost`: one scrypt run per test, sync and async, whatever the row holds, none for a digest, and async off the loop. The sync call is timed against one async run, so a `testHash` that skipped the run on an empty or junk row fails. Ratios and tick counts only, retried up to three times. - `hash-outdated`: `isHashOutdated` for a changed `p`, `r` and `ln`. - `hash-limits`: the limits a stored scrypt string must stay inside. - `hash-upgrade`: older formats verify, are flagged and are rewritten. diff --git a/.agents/skills/verify-interface/features/models.md b/.agents/skills/verify-interface/features/models.md index 4c1ecfa..426c9cc 100644 --- a/.agents/skills/verify-interface/features/models.md +++ b/.agents/skills/verify-interface/features/models.md @@ -11,6 +11,7 @@ journeys: - get-model - plain-data - compound-get-by + - get-by-keys - autodate-object-update - partial-update-keeps-modified-fields - model-factory @@ -25,11 +26,12 @@ journeys: - `crud`: ids, counts and read-backs. - `conversion`: the three static conversions save nothing; reads convert. -- `validation`: codecs, opt-in `validate`, partial patches. +- `validation`: codecs, opt-in `validate`, partial patches, and a refusal that is an `Error`. - `get-model`: one cached model per class and instance id; instances stay apart. - `autodate-object-update`: the update form that carries the primary key, as in [autodate](autodate.md). - `plain-data`: `toPlainData` output. - `compound-get-by`: a three-field compound index. +- `get-by-keys`: a number, a boolean and a value proxy key, as `Table.getAll` takes them. - `partial-update-keeps-modified-fields`: known finding #80, fails until fixed. - `model-factory`: the table name comes from the argument or `RegisterTable`. - `extend-model`: a model subclass adds query methods. diff --git a/.agents/skills/verify-interface/features/registration.md b/.agents/skills/verify-interface/features/registration.md index d064eff..f5a78e9 100644 --- a/.agents/skills/verify-interface/features/registration.md +++ b/.agents/skills/verify-interface/features/registration.md @@ -26,7 +26,7 @@ journeys: ## Sub-features -- `register-schema`: provisioning, binding and the unknown-schema error. +- `register-schema`: provisioning, a table without any `@Field` included, binding and the unknown-schema error. - `fixtures`: fixtures fill an empty table once, with generated ids and the insert conversion. - `fixture-race`: two starts write each row once; a subclass gets its parent's fixture. - `fixture-once`: the generator runs once over several `RegisterSchema` calls. diff --git a/.agents/skills/verify-interface/scenarios/declarations.ts b/.agents/skills/verify-interface/scenarios/declarations.ts index e48c47c..cf2044c 100644 --- a/.agents/skills/verify-interface/scenarios/declarations.ts +++ b/.agents/skills/verify-interface/scenarios/declarations.ts @@ -24,6 +24,12 @@ type _Update = Assert>, number> type _FromDatabase = Assert, User | undefined>>; type _ToDatabase = Assert, Api.StoredRow>>; type _Validate = Assert, Api.ValidationResult>>; +type _ValidationResultShape = Assert>; +type _PlainRowShape = Assert, { _id?: string; name?: string }>>; +type _StoredRowField = Assert["name"], string | undefined>>; +type _StoredRowId = Assert["_id"], string | undefined>>; +type _StoredRowInternal = Assert ? true : false, true>>; +type _StoredRowLocked = Assert["_internal"]>, "meta" | "data">>; type _Register = Assert Promise>>; type _Fixture = Assert, { a: 1 } | { a: 1 }[] | undefined>>; type _PrimaryKey = Assert>; diff --git a/.agents/skills/verify-interface/scenarios/hashing.ts b/.agents/skills/verify-interface/scenarios/hashing.ts index 297ad01..ef3b1a6 100644 --- a/.agents/skills/verify-interface/scenarios/hashing.ts +++ b/.agents/skills/verify-interface/scenarios/hashing.ts @@ -222,22 +222,25 @@ const COST = `${HASH_HELPERS} const [fresh] = await accounts.insert({ name: "fresh", password: "hunter2" }); const median = (list) => [...list].sort((a, b) => a - b)[Math.floor(list.length / 2)]; const timed = async (id, field) => { const row = await accounts.get(id); const runs = []; for (let i = 0; i < 3; i += 1) { const start = process.hrtime.bigint(); await row.testHashAsync(field, "wrong"); runs.push(Number(process.hrtime.bigint() - start) / 1e6); } return median(runs); }; + const timedSync = async (id, field) => { const row = await accounts.get(id); const runs = []; for (let i = 0; i < 3; i += 1) { const start = process.hrtime.bigint(); const answer = row.testHash(field, "wrong"); runs.push(answer === false ? Number(process.hrtime.bigint() - start) / 1e6 : -1); } return median(runs); }; + const allPass = (value) => Object.values(value).every((entry) => (Array.isArray(entry) ? entry.every(Boolean) : Boolean(entry))); const measure = async () => { const hashed = await timed(fresh, "password"); const empty = await timed("empty", "password"); const junk = await timed("junk", "password"); const digest = await timed("digest", "pin"); - return { emptyVsHashed: empty / hashed >= 0.5, junkVsHashed: junk / hashed >= 0.5, digestCheaper: digest / hashed < 0.5 }; + const syncHashed = await timedSync(fresh, "password"); + const syncEmpty = await timedSync("empty", "password"); + const syncJunk = await timedSync("junk", "password"); + return { emptyVsHashed: empty / hashed >= 0.5, junkVsHashed: junk / hashed >= 0.5, digestCheaper: digest / hashed < 0.5, syncSpendsOneRun: [syncHashed / hashed >= 0.5, syncEmpty / hashed >= 0.5, syncJunk / hashed >= 0.5] }; }; let cost = await measure(); - for (let attempt = 0; attempt < 2 && !Object.values(cost).every(Boolean); attempt += 1) cost = await measure(); + for (let attempt = 0; attempt < 2 && !allPass(cost); attempt += 1) cost = await measure(); const row = await accounts.get(fresh); const loopTicks = async (work) => { let ticks = 0; const timer = setInterval(() => { ticks += 1; }, 1); try { await work(); } finally { clearInterval(timer); } return ticks; }; let asyncTicks = 0; for (let attempt = 0; attempt < 3 && asyncTicks <= 2; attempt += 1) asyncTicks = await loopTicks(() => row.testHashAsync("password", "hunter2")); - let syncTicks = -1; - { let ticks = 0; const timer = setInterval(() => { ticks += 1; }, 1); row.testHash("password", "hunter2"); syncTicks = ticks; clearInterval(timer); } - return { ...cost, asyncKeepsLoopRunning: asyncTicks > 2, syncBlocksLoop: syncTicks === 0 }; + return { ...cost, asyncKeepsLoopRunning: asyncTicks > 2 }; `; const OUTDATED = `${HASH_HELPERS} @@ -272,7 +275,7 @@ export const HASH_JOURNEYS = { "hash-bounds": scenario({ title: "a stored scrypt string verifies at each edge of the documented ranges and is refused just outside", body: BOUNDS, printed: "{\"r1\":[true,false],\"r10\":[true,false],\"r99\":[true,false],\"r100\":[false,false],\"r0\":[false,false],\"ln0\":[false,false],\"p0\":[false,false],\"p99\":[false,false],\"ln16r1\":[false,false],\"salt65\":[false,false],\"ln1\":[true,false],\"ln2\":[true,false],\"ln99\":[false,false],\"ln100\":[false,false],\"p1\":[true,false],\"p16\":[true,false],\"salt16\":[true,false],\"salt64\":[true,false],\"salt80\":[false,false],\"salt15\":[false,false],\"key16\":[true,false],\"key64\":[true,false],\"key15\":[false,false],\"key65\":[false,false]}" }), "hash-memory": scenario({ title: "a stored scrypt string verifies up to 128 MiB of memory (128 x N x r) and is refused above it", body: MEMORY, printed: "{\"exactR64\":[true,false],\"overR65\":[false,false],\"exactR2\":[true,false],\"underR3\":[true,false],\"overR5\":[false,false],\"overR9\":[false,false]}" }), "hash-sync-async": scenario({ title: "testHash answers exactly what testHashAsync answers, for every kind of stored value", body: SYNC_ASYNC, printed: "{\"fresh:password\":[[true,true,true],[false,false,true],[false,false,true]],\"fresh:pin\":[[true,true,true],[false,false,true],[false,false,true]],\"legacy:password\":[[true,true,true],[false,false,true],[false,false,true]],\"lowcost:password\":[[true,true,true],[false,false,true],[false,false,true]],\"junk:password\":[[false,false,true],[false,false,true],[false,false,true]],\"empty:password\":[[false,false,true],[false,false,true],[false,false,true]],\"digest:pin\":[[true,true,true],[false,false,true],[false,false,true]],\"scryptOnDigest:pin\":[[true,true,true],[false,false,true],[false,false,true]]}" }), - "hash-cost": scenario({ title: "a test spends one scrypt run whatever the row holds, a digest field spends none, testHashAsync leaves the event loop running and testHash blocks it", body: COST, printed: "{\"emptyVsHashed\":true,\"junkVsHashed\":true,\"digestCheaper\":true,\"asyncKeepsLoopRunning\":true,\"syncBlocksLoop\":true}" }), + "hash-cost": scenario({ title: "a test spends one scrypt run whatever the row holds, a digest field spends none, testHashAsync leaves the event loop running and testHash spends the scrypt run inside the call", body: COST, printed: "{\"emptyVsHashed\":true,\"junkVsHashed\":true,\"digestCheaper\":true,\"syncSpendsOneRun\":[true,true,true],\"asyncKeepsLoopRunning\":true}" }), "hash-outdated": scenario({ title: "isHashOutdated flags any scrypt string whose ln, r or p differs from the current cost", body: OUTDATED, printed: "{\"fresh\":[true,false],\"sameCost\":[true,false],\"lowerP\":[true,true],\"higherR\":[true,true],\"lowerLn\":[true,true],\"notScrypt\":[false,true]}" }), "hash-digest": scenario({ title: "a field with an algorithm stores a salted digest with the row's shared salt", body: DIGEST, printed: "{\"algorithms\":[true,true,false,88,24],\"others\":[true,true],\"algorithmTests\":[true,true,false],\"wrongDigest\":false,\"storedDigest\":true,\"sharedSalt\":true,\"saltIsText\":true,\"freshSaltPerRow\":true,\"saltReusedOnLaterWrite\":[true,true],\"testDigest\":[true,false,true],\"digestNeverOutdated\":false,\"scryptOnDigestField\":[true,false],\"otherDigestRefused\":false}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/models.ts b/.agents/skills/verify-interface/scenarios/models.ts index 4e8d4ee..35909d6 100644 --- a/.agents/skills/verify-interface/scenarios/models.ts +++ b/.agents/skills/verify-interface/scenarios/models.ts @@ -82,7 +82,7 @@ const VALIDATION = ` await d.RegisterSchema("val"); const Model = d.BasicDataModel(Person); const people = d.GetModel(Model); - const errorOf = async (promise) => { try { await promise; return null; } catch (error) { return { message: error.message, errors: error.errors }; } }; + const errorOf = async (promise) => { try { await promise; return null; } catch (error) { return { isError: error instanceof Error, message: error.message, errors: error.errors }; } }; const valid = Model.validate({ name: "Alice", age: 30 }); const invalid = Model.validate({ name: 42, age: "old" }); const notAnObject = Model.validate(null); @@ -219,6 +219,24 @@ const COMPOUND = ` return { exact: await titles("talk", "mon", "A"), other: await titles("talk", "mon", "B"), none: await titles("talk", "tue", "B"), order: await titles("mon", "talk", "A") }; `; +const GET_BY_KEYS = ` + class Player extends d.Table {} + decorate(Player, { name: [d.Field("string"), d.Index()], rank: [d.Field("number"), d.Index()], active: [d.Field("boolean"), d.Index()] }); + d.RegisterTable("players", "keys")(Player); + await d.RegisterSchema("keys"); + const model = d.GetModel(d.BasicDataModel(Player)); + await model.insert([{ name: "Ann", rank: 2, active: true }, { name: "Bob", rank: 12, active: false }, { name: "Cid", rank: 2, active: false }]); + await untilIndexed("keys__players", ["name", "rank", "active"]); + const names = async (index, ...keys) => (await model.getBy(index, ...keys)).map((row) => [row instanceof Player, row.name]).sort(); + return { + number: await names("rank", 2), + numberIsNotText: await names("rank", "2"), + boolean: await names("active", false), + proxy: await names("name", iface.ValueProxy.constant("Bob")), + numberProxy: await names("rank", iface.ValueProxy.constant(12)), + }; +`; + const PARTIAL = ` const secretKey = "${KEY}"; class UserProfile extends d.Table.with(d.EncryptionModifier, d.HashModifier, d.LocalizationModifier) {} @@ -304,7 +322,7 @@ export const MODEL_JOURNEYS = { validation: scenario({ title: "validate checks the io-ts codecs, and insert and update refuse invalid input only when asked", body: VALIDATION, - printed: "{\"valid\":{\"ok\":true},\"invalid\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"notAnObject\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"partialOk\":{\"ok\":true},\"partialEmpty\":{\"ok\":true},\"partialBad\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"tokenSkipped\":{\"ok\":true},\"optIn\":1,\"refused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"batchRefused\":{\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"patched\":1,\"patchRefused\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"objectPatchRefused\":{\"message\":\"validation failed: name (expected a string)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"}]},\"unvalidatedPatch\":1,\"oddMessages\":{\"ok\":false,\"errors\":[{\"field\":\"a\",\"message\":\"after a hole\"},{\"field\":\"b\",\"message\":\"invalid b\"}]},\"missingRequired\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"missingInArray\":{\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"missingStored\":[42,\"Bob\"],\"afterInsert\":\" padded \",\"storedLabels\":[\" again \",\" a \",\" b \",43],\"presentUndefined\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"absentAge\":{\"ok\":true}}", + printed: "{\"valid\":{\"ok\":true},\"invalid\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"notAnObject\":{\"ok\":false,\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"partialOk\":{\"ok\":true},\"partialEmpty\":{\"ok\":true},\"partialBad\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"tokenSkipped\":{\"ok\":true},\"optIn\":1,\"refused\":{\"isError\":true,\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"batchRefused\":{\"isError\":true,\"message\":\"validation failed: name (expected a string), age (invalid age)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"},{\"field\":\"age\",\"message\":\"invalid age\"}]},\"patched\":1,\"patchRefused\":{\"isError\":true,\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"objectPatchRefused\":{\"isError\":true,\"message\":\"validation failed: name (expected a string)\",\"errors\":[{\"field\":\"name\",\"message\":\"expected a string\"}]},\"unvalidatedPatch\":1,\"oddMessages\":{\"ok\":false,\"errors\":[{\"field\":\"a\",\"message\":\"after a hole\"},{\"field\":\"b\",\"message\":\"invalid b\"}]},\"missingRequired\":{\"isError\":true,\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"missingInArray\":{\"isError\":true,\"message\":\"validation failed: age (invalid age)\",\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"missingStored\":[42,\"Bob\"],\"afterInsert\":\" padded \",\"storedLabels\":[\" again \",\" a \",\" b \",43],\"presentUndefined\":{\"ok\":false,\"errors\":[{\"field\":\"age\",\"message\":\"invalid age\"}]},\"absentAge\":{\"ok\":true}}", stored: [{ name: "the people", expression: "$DB.getCollection('val__people').find().sort({ name: 1 }).toArray().map((r) => [r.name, r.age])", expected: "[[42,\"anything\"],[\"Bob\",\"old\"]]" }], }), "get-model": scenario({ @@ -315,6 +333,7 @@ export const MODEL_JOURNEYS = { }), "plain-data": scenario({ title: "toPlainData gives unlocked values without _internal, and leaves a still-locked field undefined", body: PLAIN_DATA, printed: "{\"pathResults\":[[true,\"Ann\",{\"token\":\"t\"},true,\"Hello\"],[true,\"Ann\",{\"token\":\"t\"},true,\"Hello\"],[true,\"Ann\",{\"token\":\"t\"},true,\"Hello\"]],\"keys\":[\"_id\",\"name\",\"password\",\"secret\",\"title\"],\"noInternal\":true,\"name\":\"Ann\",\"secret\":{\"token\":\"t\"},\"passwordIsHash\":true,\"lockedTitle\":\"undefined\",\"unlockedTitle\":\"Hello\",\"json\":{\"token\":\"t\"},\"stillLocked\":[\"password\",\"secret\",\"title\"],\"notAnInstance\":true}" }), "compound-get-by": scenario({ title: "getBy matches a compound index of three fields by its keys in field order", body: COMPOUND, printed: "{\"exact\":[\"one\"],\"other\":[\"two\"],\"none\":[],\"order\":[]}" }), + "get-by-keys": scenario({ title: "getBy matches a number, a boolean and a value proxy key as the index holds it, and a text key does not match a number", body: GET_BY_KEYS, printed: "{\"number\":[[true,\"Ann\"],[true,\"Cid\"]],\"numberIsNotText\":[],\"boolean\":[[true,\"Bob\"],[true,\"Cid\"]],\"proxy\":[[true,\"Bob\"]],\"numberProxy\":[[true,\"Bob\"]]}" }), "partial-update-keeps-modified-fields": scenario({ title: "KNOWN FINDING AntelopeJS/interface-database-decorators#80, fails until fixed: update with one field keeps the row's other encrypted, hashed and localized fields", body: PARTIAL, diff --git a/.agents/skills/verify-interface/scenarios/registration.ts b/.agents/skills/verify-interface/scenarios/registration.ts index 0b7b113..4af4754 100644 --- a/.agents/skills/verify-interface/scenarios/registration.ts +++ b/.agents/skills/verify-interface/scenarios/registration.ts @@ -12,6 +12,8 @@ const REGISTER = ` class Box extends d.Table {} decorate(Box, { label: [d.Field("string")] }); d.RegisterTable("boxes", "reg")(Box); + class Bare extends d.Table {} + d.RegisterTable("bare", "reg")(Bare); class Early extends d.Table {} d.RegisterTable("early", "preexisting")(Early); const noTables = await rejection(d.RegisterSchema("nothing-registered")); @@ -24,12 +26,15 @@ const REGISTER = ` const viaSchemaName = d.GetModel(d.BasicDataModel(Early)) !== undefined; await d.GetModel(d.BasicDataModel(Item)).insert({ name: "one" }); await d.GetModel(d.BasicDataModel(Box)).insert({ label: "red" }); + const [bareId] = await d.GetModel(d.BasicDataModel(Bare)).insert({}); return { noTables: noTables && noTables.message, beforeBinding, returned: returned ?? "undefined", again: again ?? "undefined", tables: Object.keys(schema.definition), + bareDefinition: schema.definition.bare, + bareRow: (await d.GetModel(d.BasicDataModel(Bare)).get(bareId)) instanceof Bare, afterBinding, viaSchemaName, tableNameIsNotASchema: iface.Schema.get("items") === undefined, @@ -207,10 +212,10 @@ const FIXTURE_NOT_EMPTY = ` export const REGISTRATION_JOURNEYS = { "register-schema": scenario({ - title: "RegisterSchema provisions the registered tables, binds the classes and refuses an unknown schema", + title: "RegisterSchema provisions the registered tables, one without any @Field included, binds the classes and refuses an unknown schema", body: REGISTER, - printed: "{\"noTables\":\"No tables registered for schema 'nothing-registered'\",\"beforeBinding\":\"Item is not bound to a schema: call RegisterSchema(\\\"reg\\\") in the module that declares it\",\"returned\":\"undefined\",\"again\":\"undefined\",\"tables\":[\"items\",\"boxes\"],\"afterBinding\":true,\"viaSchemaName\":true,\"tableNameIsNotASchema\":true}", - stored: [{ name: "the collections of the schema", expression: "$DB.getCollectionNames().filter((n) => n.startsWith('reg__')).sort()", expected: "[\"reg__boxes\",\"reg__items\"]" }], + printed: "{\"noTables\":\"No tables registered for schema 'nothing-registered'\",\"beforeBinding\":\"Item is not bound to a schema: call RegisterSchema(\\\"reg\\\") in the module that declares it\",\"returned\":\"undefined\",\"again\":\"undefined\",\"tables\":[\"items\",\"boxes\",\"bare\"],\"bareDefinition\":{\"fields\":{},\"indexes\":{}},\"bareRow\":true,\"afterBinding\":true,\"viaSchemaName\":true,\"tableNameIsNotASchema\":true}", + stored: [{ name: "the collections of the schema", expression: "$DB.getCollectionNames().filter((n) => n.startsWith('reg__')).sort()", expected: "[\"reg__bare\",\"reg__boxes\",\"reg__items\"]" }], }), fixtures: scenario({ title: "fixtures fill an empty table once, with generated ids and the same conversion as an insert", From 056e6f42e5e19aa9288a6851d6edc3c0847aaa09 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Thu, 5 Nov 2026 14:05:59 +0000 Subject: [PATCH 41/45] fix(skills): load @antelopejs/api 1.6.0 as the repo tests do and give each group-flags row its own unique key --- .agents/skills/verify-interface/runner.ts | 2 +- .agents/skills/verify-interface/scenarios/tables.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.agents/skills/verify-interface/runner.ts b/.agents/skills/verify-interface/runner.ts index 96025bf..f82f370 100644 --- a/.agents/skills/verify-interface/runner.ts +++ b/.agents/skills/verify-interface/runner.ts @@ -66,7 +66,7 @@ const modules = { }; if (process.env.VERIFY_API_PORT) { modules.api = { - source: { type: "package", package: "@antelopejs/api", version: "1.3.0" }, + source: { type: "package", package: "@antelopejs/api", version: "1.6.0" }, config: { servers: [{ protocol: "http", host: "127.0.0.1", port: Number(process.env.VERIFY_API_PORT) }], publicBaseUrl: "http://127.0.0.1:" + process.env.VERIFY_API_PORT }, }; } diff --git a/.agents/skills/verify-interface/scenarios/tables.ts b/.agents/skills/verify-interface/scenarios/tables.ts index 55533c0..971e372 100644 --- a/.agents/skills/verify-interface/scenarios/tables.ts +++ b/.agents/skills/verify-interface/scenarios/tables.ts @@ -51,10 +51,10 @@ const GROUP_FLAGS = ` d.RegisterTable("pairs", "grp")(Pair); await d.RegisterSchema("grp"); const model = d.GetModel(d.BasicDataModel(Pair)); - await model.insert({ a: "1", b: "x", c: "p", e: "q" }); - const refusedKey = await refusedDuplicate(model, (attempt) => ({ a: "1", b: "x", c: "r" + attempt, e: "s" })); + await model.insert({ a: "1", b: "x", c: "p", e: "q", i: "first" }); + const refusedKey = await refusedDuplicate(model, (attempt) => ({ a: "1", b: "x", c: "r" + attempt, e: "s", i: "refused" + attempt })); const sameKey = refusedKey && { name: refusedKey.name, message: refusedKey.message }; - const otherSecond = await model.insert({ a: "1", b: "y", c: "p", e: "q" }); + const otherSecond = await model.insert({ a: "1", b: "y", c: "p", e: "q", i: "other" }); return { indexes: iface.Schema.get("grp").definition.pairs.indexes, sameKey, otherSecond: otherSecond.length }; `; From 3fb85af57a0a242bf0d1a1f316db62b06447f76b Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Thu, 5 Nov 2026 14:54:16 +0000 Subject: [PATCH 42/45] feat(skills): pin fixtures on field-less tables, number and proxy keys of compound getBy and their types, and the sync digest test cost --- .../verify-interface/features/models.md | 2 +- .../verify-interface/features/registration.md | 2 +- .../scenarios/declarations.ts | 7 ++++++- .../verify-interface/scenarios/hashing.ts | 5 +++-- .../verify-interface/scenarios/models.ts | 19 +++++++++++++++++-- .../scenarios/registration.ts | 7 +++++-- 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/.agents/skills/verify-interface/features/models.md b/.agents/skills/verify-interface/features/models.md index 426c9cc..da4bc5c 100644 --- a/.agents/skills/verify-interface/features/models.md +++ b/.agents/skills/verify-interface/features/models.md @@ -30,7 +30,7 @@ journeys: - `get-model`: one cached model per class and instance id; instances stay apart. - `autodate-object-update`: the update form that carries the primary key, as in [autodate](autodate.md). - `plain-data`: `toPlainData` output. -- `compound-get-by`: a three-field compound index. +- `compound-get-by`: a three-field compound index, with text keys and with number, boolean and value proxy keys. - `get-by-keys`: a number, a boolean and a value proxy key, as `Table.getAll` takes them. - `partial-update-keeps-modified-fields`: known finding #80, fails until fixed. - `model-factory`: the table name comes from the argument or `RegisterTable`. diff --git a/.agents/skills/verify-interface/features/registration.md b/.agents/skills/verify-interface/features/registration.md index f5a78e9..68c589b 100644 --- a/.agents/skills/verify-interface/features/registration.md +++ b/.agents/skills/verify-interface/features/registration.md @@ -27,7 +27,7 @@ journeys: ## Sub-features - `register-schema`: provisioning, a table without any `@Field` included, binding and the unknown-schema error. -- `fixtures`: fixtures fill an empty table once, with generated ids and the insert conversion. +- `fixtures`: fixtures fill an empty table once, a table without any `@Field` included, with generated ids and the insert conversion. - `fixture-race`: two starts write each row once; a subclass gets its parent's fixture. - `fixture-once`: the generator runs once over several `RegisterSchema` calls. - `fixture-mixed-tables`: only the empty table of a schema gets its fixture. diff --git a/.agents/skills/verify-interface/scenarios/declarations.ts b/.agents/skills/verify-interface/scenarios/declarations.ts index cf2044c..39f8e7c 100644 --- a/.agents/skills/verify-interface/scenarios/declarations.ts +++ b/.agents/skills/verify-interface/scenarios/declarations.ts @@ -18,6 +18,11 @@ type Instance = InstanceType; type _Get = Assert, PromiseLike>>; type _GetBy = Assert, PromiseLike>>; type _GetAll = Assert, PromiseLike>>; +type _GetByNumberKey = Assert>; +type _GetByBooleanKey = Assert>; +type _GetByCompoundKeys = Assert>; +type _GetByProxyKey = Assert]>>; +type _IndexKeyIsGetAllKey = Assert["getAll"]>[0], unknown[]>>>; type _Insert = Assert, Promise>>; type _Delete = Assert, Promise>>; type _Update = Assert>, number>>; @@ -195,7 +200,7 @@ function declared(title: string, probe: string): Journey { entryPoint: "a TypeScript consumer compiling against the packed declarations", async run(context) { const file = `${context.work}/probe.ts`; - context.edit("write the signature probe", file, `import * as Api from "${context.repo}/dist/index";\n${PROBE_HEADER}\n${probe}`); + context.edit("write the signature probe", file, `import * as Api from "${context.repo}/dist/index";\nimport type * as Db from "${context.repo}/node_modules/@antelopejs/interface-database";\n${PROBE_HEADER}\n${probe}`); const compiled = await context.command("compile the signature probe", "node", [ `${context.repo}/node_modules/typescript/bin/tsc`, "--noEmit", diff --git a/.agents/skills/verify-interface/scenarios/hashing.ts b/.agents/skills/verify-interface/scenarios/hashing.ts index ef3b1a6..b051ee0 100644 --- a/.agents/skills/verify-interface/scenarios/hashing.ts +++ b/.agents/skills/verify-interface/scenarios/hashing.ts @@ -232,7 +232,8 @@ const COST = `${HASH_HELPERS} const syncHashed = await timedSync(fresh, "password"); const syncEmpty = await timedSync("empty", "password"); const syncJunk = await timedSync("junk", "password"); - return { emptyVsHashed: empty / hashed >= 0.5, junkVsHashed: junk / hashed >= 0.5, digestCheaper: digest / hashed < 0.5, syncSpendsOneRun: [syncHashed / hashed >= 0.5, syncEmpty / hashed >= 0.5, syncJunk / hashed >= 0.5] }; + const syncDigest = await timedSync("digest", "pin"); + return { emptyVsHashed: empty / hashed >= 0.5, junkVsHashed: junk / hashed >= 0.5, digestCheaper: digest / hashed < 0.5, syncSpendsOneRun: [syncHashed / hashed >= 0.5, syncEmpty / hashed >= 0.5, syncJunk / hashed >= 0.5], syncDigestCheaper: syncDigest / hashed < 0.5 }; }; let cost = await measure(); for (let attempt = 0; attempt < 2 && !allPass(cost); attempt += 1) cost = await measure(); @@ -275,7 +276,7 @@ export const HASH_JOURNEYS = { "hash-bounds": scenario({ title: "a stored scrypt string verifies at each edge of the documented ranges and is refused just outside", body: BOUNDS, printed: "{\"r1\":[true,false],\"r10\":[true,false],\"r99\":[true,false],\"r100\":[false,false],\"r0\":[false,false],\"ln0\":[false,false],\"p0\":[false,false],\"p99\":[false,false],\"ln16r1\":[false,false],\"salt65\":[false,false],\"ln1\":[true,false],\"ln2\":[true,false],\"ln99\":[false,false],\"ln100\":[false,false],\"p1\":[true,false],\"p16\":[true,false],\"salt16\":[true,false],\"salt64\":[true,false],\"salt80\":[false,false],\"salt15\":[false,false],\"key16\":[true,false],\"key64\":[true,false],\"key15\":[false,false],\"key65\":[false,false]}" }), "hash-memory": scenario({ title: "a stored scrypt string verifies up to 128 MiB of memory (128 x N x r) and is refused above it", body: MEMORY, printed: "{\"exactR64\":[true,false],\"overR65\":[false,false],\"exactR2\":[true,false],\"underR3\":[true,false],\"overR5\":[false,false],\"overR9\":[false,false]}" }), "hash-sync-async": scenario({ title: "testHash answers exactly what testHashAsync answers, for every kind of stored value", body: SYNC_ASYNC, printed: "{\"fresh:password\":[[true,true,true],[false,false,true],[false,false,true]],\"fresh:pin\":[[true,true,true],[false,false,true],[false,false,true]],\"legacy:password\":[[true,true,true],[false,false,true],[false,false,true]],\"lowcost:password\":[[true,true,true],[false,false,true],[false,false,true]],\"junk:password\":[[false,false,true],[false,false,true],[false,false,true]],\"empty:password\":[[false,false,true],[false,false,true],[false,false,true]],\"digest:pin\":[[true,true,true],[false,false,true],[false,false,true]],\"scryptOnDigest:pin\":[[true,true,true],[false,false,true],[false,false,true]]}" }), - "hash-cost": scenario({ title: "a test spends one scrypt run whatever the row holds, a digest field spends none, testHashAsync leaves the event loop running and testHash spends the scrypt run inside the call", body: COST, printed: "{\"emptyVsHashed\":true,\"junkVsHashed\":true,\"digestCheaper\":true,\"syncSpendsOneRun\":[true,true,true],\"asyncKeepsLoopRunning\":true}" }), + "hash-cost": scenario({ title: "a test spends one scrypt run whatever the row holds, a digest field spends none, testHashAsync leaves the event loop running and testHash spends the scrypt run inside the call, none for a digest", body: COST, printed: "{\"emptyVsHashed\":true,\"junkVsHashed\":true,\"digestCheaper\":true,\"syncSpendsOneRun\":[true,true,true],\"syncDigestCheaper\":true,\"asyncKeepsLoopRunning\":true}" }), "hash-outdated": scenario({ title: "isHashOutdated flags any scrypt string whose ln, r or p differs from the current cost", body: OUTDATED, printed: "{\"fresh\":[true,false],\"sameCost\":[true,false],\"lowerP\":[true,true],\"higherR\":[true,true],\"lowerLn\":[true,true],\"notScrypt\":[false,true]}" }), "hash-digest": scenario({ title: "a field with an algorithm stores a salted digest with the row's shared salt", body: DIGEST, printed: "{\"algorithms\":[true,true,false,88,24],\"others\":[true,true],\"algorithmTests\":[true,true,false],\"wrongDigest\":false,\"storedDigest\":true,\"sharedSalt\":true,\"saltIsText\":true,\"freshSaltPerRow\":true,\"saltReusedOnLaterWrite\":[true,true],\"testDigest\":[true,false,true],\"digestNeverOutdated\":false,\"scryptOnDigestField\":[true,false],\"otherDigestRefused\":false}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/models.ts b/.agents/skills/verify-interface/scenarios/models.ts index 35909d6..6d8a6f0 100644 --- a/.agents/skills/verify-interface/scenarios/models.ts +++ b/.agents/skills/verify-interface/scenarios/models.ts @@ -206,7 +206,17 @@ const COMPOUND = ` title: [d.Field("string")], }); d.RegisterTable("events", "tri")(Event); + class Seat extends d.Table {} + decorate(Seat, { + row: [d.Field("number"), d.Index({ group: "spot" })], + col: [d.Field("number"), d.Index({ group: "spot" })], + open: [d.Field("boolean"), d.Index({ group: "spot" })], + label: [d.Field("string")], + }); + d.RegisterTable("seats", "tri")(Seat); await d.RegisterSchema("tri"); + const seats = d.GetModel(d.BasicDataModel(Seat)); + await seats.insert([{ row: 1, col: 2, open: true, label: "1-2" }, { row: 2, col: 1, open: true, label: "2-1" }, { row: 1, col: 2, open: false, label: "1-2 shut" }]); const model = d.GetModel(d.BasicDataModel(Event)); await model.insert([ { kind: "talk", day: "mon", room: "A", title: "one" }, @@ -216,7 +226,12 @@ const COMPOUND = ` ]); await sleep(2500); const titles = async (...keys) => (await model.getBy("triple", ...keys)).map((row) => row.title).sort(); - return { exact: await titles("talk", "mon", "A"), other: await titles("talk", "mon", "B"), none: await titles("talk", "tue", "B"), order: await titles("mon", "talk", "A") }; + const labels = async (...keys) => (await seats.getBy("spot", ...keys)).map((row) => row.label).sort(); + const constant = (value) => iface.ValueProxy.constant(value); + return { + exact: await titles("talk", "mon", "A"), other: await titles("talk", "mon", "B"), none: await titles("talk", "tue", "B"), order: await titles("mon", "talk", "A"), + numbers: await labels(1, 2, true), numbersAsText: await labels("1", "2", true), shut: await labels(1, 2, false), proxies: await labels(constant(2), constant(1), constant(true)), + }; `; const GET_BY_KEYS = ` @@ -332,7 +347,7 @@ export const MODEL_JOURNEYS = { stored: [{ name: "the rows by instance", expression: "$DB.getCollection('gm__items').find().sort({ name: 1 }).toArray().map((r) => [r.name, r._instance])", expected: "[[\"default\",null],[\"tenant one\",\"t1\"],[\"tenant two\",\"t2\"]]" }], }), "plain-data": scenario({ title: "toPlainData gives unlocked values without _internal, and leaves a still-locked field undefined", body: PLAIN_DATA, printed: "{\"pathResults\":[[true,\"Ann\",{\"token\":\"t\"},true,\"Hello\"],[true,\"Ann\",{\"token\":\"t\"},true,\"Hello\"],[true,\"Ann\",{\"token\":\"t\"},true,\"Hello\"]],\"keys\":[\"_id\",\"name\",\"password\",\"secret\",\"title\"],\"noInternal\":true,\"name\":\"Ann\",\"secret\":{\"token\":\"t\"},\"passwordIsHash\":true,\"lockedTitle\":\"undefined\",\"unlockedTitle\":\"Hello\",\"json\":{\"token\":\"t\"},\"stillLocked\":[\"password\",\"secret\",\"title\"],\"notAnInstance\":true}" }), - "compound-get-by": scenario({ title: "getBy matches a compound index of three fields by its keys in field order", body: COMPOUND, printed: "{\"exact\":[\"one\"],\"other\":[\"two\"],\"none\":[],\"order\":[]}" }), + "compound-get-by": scenario({ title: "getBy matches a compound index of three fields by its keys in field order, with text, number, boolean and value proxy keys", body: COMPOUND, printed: "{\"exact\":[\"one\"],\"other\":[\"two\"],\"none\":[],\"order\":[],\"numbers\":[\"1-2\"],\"numbersAsText\":[],\"shut\":[\"1-2 shut\"],\"proxies\":[\"2-1\"]}" }), "get-by-keys": scenario({ title: "getBy matches a number, a boolean and a value proxy key as the index holds it, and a text key does not match a number", body: GET_BY_KEYS, printed: "{\"number\":[[true,\"Ann\"],[true,\"Cid\"]],\"numberIsNotText\":[],\"boolean\":[[true,\"Bob\"],[true,\"Cid\"]],\"proxy\":[[true,\"Bob\"]],\"numberProxy\":[[true,\"Bob\"]]}" }), "partial-update-keeps-modified-fields": scenario({ title: "KNOWN FINDING AntelopeJS/interface-database-decorators#80, fails until fixed: update with one field keeps the row's other encrypted, hashed and localized fields", diff --git a/.agents/skills/verify-interface/scenarios/registration.ts b/.agents/skills/verify-interface/scenarios/registration.ts index 4af4754..804b300 100644 --- a/.agents/skills/verify-interface/scenarios/registration.ts +++ b/.agents/skills/verify-interface/scenarios/registration.ts @@ -58,6 +58,9 @@ const FIXTURES = ` class Nothing extends d.Table {} d.Fixture(() => undefined)(Nothing); d.RegisterTable("nothing", "fx")(Nothing); + class Marker extends d.Table {} + d.Fixture(() => [{ _id: "marker-a" }, { _id: "marker-b" }])(Marker); + d.RegisterTable("markers", "fx")(Marker); class Loose extends d.Table {} decorate(Loose, { name: [d.Field("string")] }); d.Fixture(() => [{ _id: "loose", name: "never inserted" }])(Loose); @@ -67,7 +70,7 @@ const FIXTURES = ` d.RegisterTable("stamped", "fx")(Stamped); await d.RegisterSchema("fx"); const count = (name) => rawTable("fx", name).count(); - const first = { roles: await count("roles"), settings: await count("settings"), plans: await count("plans"), nothing: await count("nothing") }; + const first = { roles: await count("roles"), settings: await count("settings"), plans: await count("plans"), nothing: await count("nothing"), markers: await count("markers") }; await rawTable("fx", "roles").get("user").delete(); await d.RegisterSchema("fx"); const afterSecondStart = await count("roles"); @@ -220,7 +223,7 @@ export const REGISTRATION_JOURNEYS = { fixtures: scenario({ title: "fixtures fill an empty table once, with generated ids and the same conversion as an insert", body: FIXTURES, - printed: "{\"first\":{\"roles\":2,\"settings\":1,\"plans\":3,\"nothing\":0},\"afterSecondStart\":1,\"plans\":[\"plans:fixture:0\",\"plans:fixture:1\",\"plans:fixture:2\"],\"unregisteredClassProvisioned\":false,\"hashedAtInsert\":[true,true],\"insertEventFired\":true}", + printed: "{\"first\":{\"roles\":2,\"settings\":1,\"plans\":3,\"nothing\":0,\"markers\":2},\"afterSecondStart\":1,\"plans\":[\"plans:fixture:0\",\"plans:fixture:1\",\"plans:fixture:2\"],\"unregisteredClassProvisioned\":false,\"hashedAtInsert\":[true,true],\"insertEventFired\":true}", stored: [ { name: "the plan rows", expression: "$DB.getCollection('fx__plans').find({}, { name: 0 }).sort({ _id: 1 }).toArray().map((r) => [r._id, r.label])", expected: "[[\"plans:fixture:0\",\"free\"],[\"plans:fixture:1\",\"pro\"],[\"plans:fixture:2\",\"team\"]]" }, { name: "the stamped row", expression: "$DB.getCollection('fx__stamped').find().sort({ _id: 1 }).toArray().map((r) => [r._id, r._internal.data.secret.startsWith('$' + 'scrypt' + '$ln=15,r=8,p=3$'), r._internal.data.secret.includes('hunter'), r.created !== undefined])", expected: "[[\"s1\",true,false,true],[\"s2\",true,false,true]]" }, From 5194c7d334c048d432191bfe4dd8163667a6147f Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Thu, 5 Nov 2026 15:52:42 +0000 Subject: [PATCH 43/45] feat(skills): drive @Model before RegisterSchema, date marker lookalikes, Relation thunks, custom model classes and localize with no fields --- .../features/parameter-decoration.md | 2 ++ .../scenarios/declarations.ts | 16 ++++++++++ .../verify-interface/scenarios/decoration.ts | 30 +++++++++++++++++++ .../verify-interface/scenarios/encryption.ts | 8 ++++- .../scenarios/localization.ts | 5 +++- 5 files changed, 59 insertions(+), 2 deletions(-) diff --git a/.agents/skills/verify-interface/features/parameter-decoration.md b/.agents/skills/verify-interface/features/parameter-decoration.md index 9e5719b..3cab970 100644 --- a/.agents/skills/verify-interface/features/parameter-decoration.md +++ b/.agents/skills/verify-interface/features/parameter-decoration.md @@ -9,6 +9,7 @@ journeys: - custom-parameter-provider - property-decoration - crud-controller + - decorated-before-binding --- # Parameter decoration @@ -22,6 +23,7 @@ journeys: - `custom-parameter-provider`: the docs' custom decorator example. - `property-decoration`: a property is set while a request is handled. - `crud-controller`: `@Model` beside `@Parameter` and `@RawBody` in a CRUD controller. +- `decorated-before-binding`: a controller decorated before `RegisterSchema` runs, as a module declares it at import. ## How to get to it (user POV) diff --git a/.agents/skills/verify-interface/scenarios/declarations.ts b/.agents/skills/verify-interface/scenarios/declarations.ts index 39f8e7c..a4d8fcd 100644 --- a/.agents/skills/verify-interface/scenarios/declarations.ts +++ b/.agents/skills/verify-interface/scenarios/declarations.ts @@ -76,6 +76,21 @@ class Options extends Api.Table.with(Api.EncryptionModifier, Api.HashModifier, A @Api.Localized({ fallbackLocale: "en" }) @Api.Field("string") declare e: string; } type _Table = Assert[0], Api.TableTarget | undefined>>; +class Custom { + static readonly schemaName = "manual"; + readonly table: Db.Table; + constructor(readonly database: Db.SchemaInstance) { + this.table = database.table("things"); + } + static fromDatabase(obj: any): object | undefined { + return obj ? obj : undefined; + } + static fromPlainData(obj: object): object { + return obj; + } +} +const custom = Api.GetModel(Custom); +type _CustomModel = Assert>; `; const MODIFIER_SIGNATURES = ` @@ -142,6 +157,7 @@ const REJECTED = [ "Api.Encrypted({ secretKey: 5 });", 'Api.Encrypted({ secretKey: "k", algorithm: 5 });', "Api.Relation({ to: User });", + "Api.Relation({ to: () => Date });", "Api.Localized({ fallbackLocale: true });", "Api.Hashed({ algorithm: true });", "Api.Model(Model, 5);", diff --git a/.agents/skills/verify-interface/scenarios/decoration.ts b/.agents/skills/verify-interface/scenarios/decoration.ts index 6b586ae..2822b4a 100644 --- a/.agents/skills/verify-interface/scenarios/decoration.ts +++ b/.agents/skills/verify-interface/scenarios/decoration.ts @@ -82,7 +82,37 @@ const CUSTOM_PROVIDER = `${HTTP} return { x: await call("/custom/x"), y: await call("/custom/y") }; `; +const EARLY = ` + const api = require(require.resolve("@antelopejs/interface-api", { paths: [dist] })); + const base = "http://127.0.0.1:" + process.env.VERIFY_API_PORT; + const route = (Controller, method, verb, path, handler, params) => { + Controller.prototype[method] = handler; + params.forEach((apply, index) => apply(Controller.prototype, method, index)); + api[verb](path)(Controller.prototype, method, Object.getOwnPropertyDescriptor(Controller.prototype, method)); + }; + const call = async (path, init) => { const response = await fetch(base + path, init); const text = await response.text(); return { status: response.status, body: text }; }; + class Note extends d.Table {} + decorate(Note, { title: [d.Field("string")] }); + d.RegisterTable("notes", "early")(Note); + const NoteModel = d.BasicDataModel(Note); + class Early extends api.Controller("/early") {} + const decorated = caught(() => route(Early, "list", "Get", "/", async (model) => (await model.getAll()).map((row) => row.title), [d.Model(NoteModel, "main")])); + class Props extends api.Controller("/early-props") {} + const decoratedProperty = caught(() => d.Model(NoteModel)(Props.prototype, "notes")); + route(Props, "list", "Get", "/", async function () { return (await this.notes.getAll()).map((row) => row.title); }, []); + await d.RegisterSchema("early"); + await d.GetModel(NoteModel, "main").insert({ title: "after binding" }); + await d.GetModel(NoteModel).insert({ title: "default after binding" }); + return { decorated, decoratedProperty, list: await call("/early"), props: await call("/early-props") }; +`; + export const DECORATION_JOURNEYS = { + "decorated-before-binding": scenario({ + title: "@Model on a controller declared before RegisterSchema binds the table resolves the model per request, not when the decorator runs", + body: EARLY, + printed: "{\"decorated\":null,\"decoratedProperty\":null,\"list\":{\"status\":200,\"body\":\"[\\\"after binding\\\"]\"},\"props\":{\"status\":200,\"body\":\"[\\\"default after binding\\\"]\"}}", + api: true, + }), "fixed-instance": scenario({ title: "@Model with an instance id injects the cached model of that instance into a controller method", body: FIXED, diff --git a/.agents/skills/verify-interface/scenarios/encryption.ts b/.agents/skills/verify-interface/scenarios/encryption.ts index a28ebe0..0388603 100644 --- a/.agents/skills/verify-interface/scenarios/encryption.ts +++ b/.agents/skills/verify-interface/scenarios/encryption.ts @@ -61,7 +61,13 @@ const DATES = `${DECRYPT} const [id] = await model.insert({ when: moment, plainObject: { type: "Date", value: "not a date", extra: 1 } }); const read = await model.get(id); const raw = await rawTable("encdate", "events").get(id); + const [taggedId] = await model.insert({ when: moment, plainObject: { type: "Date", value: moment.toUTCString(), extra: 1 } }); + const tagged = (await model.get(taggedId)).plainObject; + const [twoKeyId] = await model.insert({ when: moment, plainObject: { type: "Date", value: "x" } }); + const twoKey = (await model.get(twoKeyId)).plainObject; return { + taggedKept: [tagged instanceof Date, tagged], + twoKeyKept: [twoKey instanceof Date, twoKey], isDate: read.when instanceof Date, iso: read.when.toISOString(), storedText: JSON.parse(decrypt(raw._internal.data.when)), @@ -104,6 +110,6 @@ export const ENCRYPTION_JOURNEYS = { }, ], }), - "encryption-dates": scenario({ title: "an encrypted Date comes back as a Date with whole-second precision", body: DATES, printed: "{\"isDate\":true,\"iso\":\"2026-01-02T03:04:05.000Z\",\"storedText\":{\"value\":\"Fri, 02 Jan 2026 03:04:05 GMT\",\"type\":\"Date\"},\"lookalikeKept\":{\"type\":\"Date\",\"value\":\"not a date\",\"extra\":1},\"nested\":[true,\"2026-01-02T03:04:05.000Z\",true,\"text\",true,\"2026-01-02T03:04:05.000Z\"]}" }), + "encryption-dates": scenario({ title: "an encrypted Date comes back as a Date with whole-second precision", body: DATES, printed: "{\"taggedKept\":[false,{\"type\":\"Date\",\"value\":\"Fri, 02 Jan 2026 03:04:05 GMT\",\"extra\":1}],\"twoKeyKept\":[false,{\"type\":\"Date\",\"value\":\"x\"}],\"isDate\":true,\"iso\":\"2026-01-02T03:04:05.000Z\",\"storedText\":{\"value\":\"Fri, 02 Jan 2026 03:04:05 GMT\",\"type\":\"Date\"},\"lookalikeKept\":{\"type\":\"Date\",\"value\":\"not a date\",\"extra\":1},\"nested\":[true,\"2026-01-02T03:04:05.000Z\",true,\"text\",true,\"2026-01-02T03:04:05.000Z\"]}" }), "encryption-keys": scenario({ title: "the key goes to the cipher as a UTF-8 string, so a hex-encoded key is refused", body: KEYS, printed: "{\"hexKeyBytes\":64,\"hexRefused\":{\"name\":\"RangeError\",\"message\":\"Invalid key length\"},\"goodStored\":\"kept\",\"hexStored\":0}" }), } satisfies { [id: string]: Journey }; diff --git a/.agents/skills/verify-interface/scenarios/localization.ts b/.agents/skills/verify-interface/scenarios/localization.ts index 091c88e..4f2a741 100644 --- a/.agents/skills/verify-interface/scenarios/localization.ts +++ b/.agents/skills/verify-interface/scenarios/localization.ts @@ -37,7 +37,10 @@ const LOCALIZE = `${PRODUCT} product.localize("fr").summary = "court"; const restricted = product.localize("en").localize("fr", ["name"]); const restrictedReads = [restricted.name, restricted.summary]; + const untouched = product.localize("en").localize("fr", []); + const emptyListReads = [untouched.name, untouched.summary]; return { + emptyListReads, spanish: spanishNow, germanFallsBack: germanNow, sameObject, @@ -173,7 +176,7 @@ const AUTO_OBJECT = ` `; export const LOCALIZATION_JOURNEYS = { - localize: scenario({ title: "localize switches the locale of an instance and falls back when a translation is missing or empty", body: LOCALIZE, printed: "{\"spanish\":\"Auriculares Premium\",\"germanFallsBack\":\"Premium Headphones\",\"sameObject\":[true,true,true],\"readNow\":\"Premium Headphones\",\"frenchEmpty\":\"Loud\",\"noFallback\":[\"sans repli\",\"undefined\"],\"restrictedReads\":[\"Casque Premium\",\"short\"],\"fallbackLocaleOf\":[\"en\",\"undefined\",\"undefined\",\"undefined\"]}" }), + localize: scenario({ title: "localize switches the locale of an instance and falls back when a translation is missing or empty", body: LOCALIZE, printed: "{\"emptyListReads\":[\"Premium Headphones\",\"short\"],\"spanish\":\"Auriculares Premium\",\"germanFallsBack\":\"Premium Headphones\",\"sameObject\":[true,true,true],\"readNow\":\"Premium Headphones\",\"frenchEmpty\":\"Loud\",\"noFallback\":[\"sans repli\",\"undefined\"],\"restrictedReads\":[\"Casque Premium\",\"short\"],\"fallbackLocaleOf\":[\"en\",\"undefined\",\"undefined\",\"undefined\"]}" }), "localized-query": scenario({ title: "a query through unlockrequest answers the requested locale and falls back to the fallback locale, an empty text included", body: QUERY, printed: "{\"germanHello\":[\"empty-de\",\"only-en\"],\"germanHallo\":[\"has-de\"],\"englishHello\":[\"empty-de\",\"only-en\"],\"frenchSalut\":[\"none\"],\"frenchHello\":[\"empty-de\",\"only-en\"],\"noFallbackGerman\":[],\"noFallbackEnglish\":[\"only-en\"],\"noFallbackOwnText\":[\"has-de\"]}" }), "localize-wildcard-replace": scenario({ title: "a wildcard write replaces every stored translation", From d11ac9c58b9d70d8409ab8884b4478493d9eee46 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Thu, 5 Nov 2026 15:54:27 +0000 Subject: [PATCH 44/45] docs(skills): say an unbound class uses the schema its schema name has, as docs/4 does --- skills/database-decorators-interface/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/database-decorators-interface/SKILL.md b/skills/database-decorators-interface/SKILL.md index 28ccc14..c11d190 100644 --- a/skills/database-decorators-interface/SKILL.md +++ b/skills/database-decorators-interface/SKILL.md @@ -144,7 +144,7 @@ class Chef extends Table.with( to the whole group. - `@Fixture` data is only inserted by `RegisterSchema` when the table is empty (count === 0). - `GetModel` caches by model class + instanceId; the same pair always returns the same instance. -- `RegisterSchema` binds each registered table class to the `Schema` it builds; call it in the module that declares the tables. `User.table()`, `User.table(instanceId)` and `User.table(db)` then return the class's AQL table, and `GetModel` builds `BasicDataModel` models on that same schema, even when another module (bound to another database) calls them. An unbound class throws rather than looking a schema up by id. +- `RegisterSchema` binds each registered table class to the `Schema` it builds; call it in the module that declares the tables. `User.table()`, `User.table(instanceId)` and `User.table(db)` then return the class's AQL table, and `GetModel` builds `BasicDataModel` models on that same schema, even when another module (bound to another database) calls them. An unbound class whose schema name has a schema uses that schema; one whose schema name has none throws ` is not bound to a schema`. - Serialization of modified instances to plain JSON goes through `toPlainData` (auto-attached as `toJSON`). Deeper reference: this package's `docs/` chapters — Introduction, Table Definitions, Table Modifiers, From d506dd34883012bc2deebba10ece5575250a6daf Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Thu, 5 Nov 2026 16:53:29 +0000 Subject: [PATCH 45/45] feat(skills): compile every complete docs example, pin @Model without an instance, async fixtures, toDatabase of plain data and with() on a decorated class --- .../verify-interface/features/declarations.md | 10 +++- .../scenarios/declarations.ts | 56 ++++++++++++++++++- .../verify-interface/scenarios/models.ts | 4 +- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/.agents/skills/verify-interface/features/declarations.md b/.agents/skills/verify-interface/features/declarations.md index d39c1c4..4c3f0b2 100644 --- a/.agents/skills/verify-interface/features/declarations.md +++ b/.agents/skills/verify-interface/features/declarations.md @@ -2,9 +2,16 @@ covers: - src/index.ts - src/common.ts + - src/model.ts + - src/table.ts + - src/relation.ts + - src/database.ts + - src/schema.ts + - src/modifiers/*.ts docs: - docs/4.data-models.md journeys: + - docs-examples - model-signatures - modifier-signatures - rejected-calls @@ -17,7 +24,8 @@ The packed `.d.ts` files. ## Sub-features -- `model-signatures`: the return types of the model methods and `RegisterSchema`. +- `docs-examples`: every complete TypeScript example of docs/ and README.md (blocks not marked `fragment`) compiles against the packed declarations. +- `model-signatures`: the return types of the model methods and `RegisterSchema`, the row aliases, `getBy` keys, `@Model` without an instance, async fixtures, `toDatabase` of plain data, and `X.with(...)` on a decorated class. - `packed-subpaths`: the packed package, not the checkout, serves every subpath the skill file imports. - `rejected-calls`: every wrong call listed in the journey is refused by tsc. - `modifier-signatures`: the hash and localization mixin methods. diff --git a/.agents/skills/verify-interface/scenarios/declarations.ts b/.agents/skills/verify-interface/scenarios/declarations.ts index a4d8fcd..8823ca2 100644 --- a/.agents/skills/verify-interface/scenarios/declarations.ts +++ b/.agents/skills/verify-interface/scenarios/declarations.ts @@ -1,4 +1,4 @@ -import { existsSync, mkdirSync, symlinkSync } from "node:fs"; +import { existsSync, mkdirSync, readdirSync, readFileSync, symlinkSync } from "node:fs"; import { dirname, join } from "node:path"; import { equal } from "@antelopejs/tooling-configs/verify"; @@ -29,6 +29,16 @@ type _Update = Assert>, number> type _FromDatabase = Assert, User | undefined>>; type _ToDatabase = Assert, Api.StoredRow>>; type _Validate = Assert, Api.ValidationResult>>; +type _ModelWithoutInstance = Assert>; +Api.Fixture(async () => [{ _id: "a" }])(User); +Api.Fixture(async () => ({ _id: "b" }))(User); +class Greeter extends Api.Table { declare name: string; greet(): string { return this.name; } } +const GreeterModel = Api.BasicDataModel(Greeter, "greeters"); +const greeterRow = GreeterModel.toDatabase({ name: "plain data" }); +type _ToDatabasePlain = Assert>>; +class Admin extends Greeter.with(Api.HashModifier) {} +type _WithKeepsFields = Assert>; +type _WithKeepsMethods = Assert, string>>; type _ValidationResultShape = Assert>; type _PlainRowShape = Assert, { _id?: string; name?: string }>>; type _StoredRowField = Assert["name"], string | undefined>>; @@ -299,7 +309,51 @@ const packedSubpaths: Journey = { }, }; +/** Every complete TypeScript example of the docs and README, as a reader copies it; `fragment` blocks are partial by design and skipped. */ +function docExamples(repo: string): { name: string; source: string }[] { + const pages = [...readdirSync(join(repo, "docs")).filter((page) => page.endsWith(".md")).sort().map((page) => join("docs", page)), "README.md"]; + const examples: { name: string; source: string }[] = []; + for (const page of pages) { + const text = readFileSync(join(repo, page), "utf8"); + for (const match of text.matchAll(/^```(?:typescript|ts)([^\n]*)\n([\s\S]*?)^```/gm)) { + if (match[1].includes("fragment")) continue; + const line = text.slice(0, match.index).split("\n").length; + examples.push({ name: `${page.replace(/[/.]/g, "_")}_L${line}`, source: `${match[2]}\nexport {};\n` }); + } + } + return examples; +} + +const docsExamples: Journey = { + title: "every complete TypeScript example in the docs and README compiles against the packed declarations", + entryPoint: "a TypeScript consumer pasting each documented example into a module that depends on the packed package", + async run(context) { + const pack = packedPackage(context.work); + const folder = join(context.work, "examples"); + mkdirSync(folder, { recursive: true }); + symlinkSync(join(context.repo, "node_modules"), join(folder, "node_modules"), "dir"); + const examples = docExamples(context.repo); + for (const example of examples) context.edit(`write ${example.name}`, join(folder, `${example.name}.ts`), example.source); + const tsconfig = { + compilerOptions: { + strict: true, noImplicitAny: false, skipLibCheck: true, experimentalDecorators: true, noEmit: true, + target: "es2022", module: "esnext", moduleResolution: "bundler", types: ["node"], + paths: { "@antelopejs/interface-database-decorators": [`${pack}/dist/index`], "@antelopejs/interface-database-decorators/*": [`${pack}/dist/*`] }, + }, + files: examples.map((example) => `${example.name}.ts`), + }; + context.edit("write the consumer tsconfig", join(folder, "tsconfig.json"), `${JSON.stringify(tsconfig, null, 2)}\n`); + const compiled = await context.command("compile every documented example", "node", [`${context.repo}/node_modules/typescript/bin/tsc`, "-p", folder], { cwd: folder }); + const failing = [...new Set([...compiled.stdout.matchAll(/^([\w.-]+)\.ts\(/gm)].map((match) => match[1]))]; + context.expect(compiled.folder, [ + { name: "the docs hold complete examples to compile", pass: examples.length >= 20, detail: examples.map((example) => example.name).join(" ") }, + { name: "every documented example compiles", pass: compiled.exitCode === 0 && failing.length === 0, detail: `${failing.join(" ")}\n${compiled.stdout.slice(0, 1500)}` }, + ]); + }, +}; + export const DECLARATION_JOURNEYS = { + "docs-examples": docsExamples, "model-signatures": declared("the model methods and RegisterSchema declare the return types the docs table lists", MODEL_SIGNATURES), "packed-subpaths": packedSubpaths, "rejected-calls": rejected("calls with a wrong parameter type or a missing key are refused by the declarations", REJECTED), diff --git a/.agents/skills/verify-interface/scenarios/models.ts b/.agents/skills/verify-interface/scenarios/models.ts index 6d8a6f0..055769f 100644 --- a/.agents/skills/verify-interface/scenarios/models.ts +++ b/.agents/skills/verify-interface/scenarios/models.ts @@ -46,6 +46,7 @@ const CONVERSION = ` const sameInstance = Model.fromPlainData(fromPlain) === fromPlain; const stored = Model.toDatabase(fromPlain); const fromPlainDirect = Model.toDatabase({ _id: "a2", name: "Bea" }); + const plainWithSecret = Model.toDatabase({ _id: "a4", name: "Eve", secret: "plain" }); const record = { _id: "a3", name: "Cy" }; const copy = Model.fromDatabase(record); const empty = Model.fromDatabase({}); @@ -57,6 +58,7 @@ const CONVERSION = ` const viaGetAll = await model.getAll(); const viaGetBy = await model.getBy("name", "Dee"); return { + plainLocked: [plainWithSecret.secret === undefined, Object.keys(plainWithSecret._internal?.data ?? {})], fromPlain: [fromPlain instanceof Account, fromPlain.secret, fromPlain.name], sameInstance, storedShape: [stored instanceof Account, Object.keys(stored).sort(), stored._internal.data.secret.length, stored.secret === undefined], @@ -331,7 +333,7 @@ export const MODEL_JOURNEYS = { conversion: scenario({ title: "fromPlainData, fromDatabase and toDatabase convert without saving, and reads convert their rows", body: CONVERSION, - printed: "{\"fromPlain\":[true,\"s3cret\",\"Ann\"],\"sameInstance\":true,\"storedShape\":[false,[\"_id\",\"_internal\",\"name\"],3,true],\"plainDataShape\":[false,[\"_id\",\"name\"]],\"nullIsUndefined\":[true,true],\"emptyObjectIsAnInstance\":true,\"copyNotSame\":[true,true,false,\"Cy\"],\"afterConversions\":0,\"read\":[true,\"hidden\"],\"direct\":[false,\"Dee\",true],\"getAllConverted\":true,\"getByConverted\":[1,true,\"hidden\",\"hidden\",\"hidden\"]}", + printed: "{\"plainLocked\":[true,[\"secret\"]],\"fromPlain\":[true,\"s3cret\",\"Ann\"],\"sameInstance\":true,\"storedShape\":[false,[\"_id\",\"_internal\",\"name\"],3,true],\"plainDataShape\":[false,[\"_id\",\"name\"]],\"nullIsUndefined\":[true,true],\"emptyObjectIsAnInstance\":true,\"copyNotSame\":[true,true,false,\"Cy\"],\"afterConversions\":0,\"read\":[true,\"hidden\"],\"direct\":[false,\"Dee\",true],\"getAllConverted\":true,\"getByConverted\":[1,true,\"hidden\",\"hidden\",\"hidden\"]}", stored: [{ name: "the one saved account", expression: "$DB.getCollection('cnv__accounts').find().toArray().map((r) => [r.name, Object.keys(r._internal.data), r._internal.data.secret.length])", expected: "[[\"Dee\",[\"secret\"],3]]" }], }), validation: scenario({