From 57cd8d5486d916099dacfb99a12323de792571f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 17:36:02 +0000 Subject: [PATCH] fix(objectql): drop the 135 `as any` that dodged registerObject's old parameter type (#5543) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `registerObject(schema: ServiceObject, …)` used to name the POST-parse object shape, so an authored literal failed with TS2740 demanding ~9 zod `.default(...)` products the registry never materializes. ADR-0122 phase 2 (#6083) fixed the annotation upstream by making the bare `ServiceObject` alias mean the authored (`z.input`) shape; this removes the workaround it made obsolete — 135 `as any` casts across 46 files in this package, written only to get authored literals past the old type. A blanket `as any` suppresses every error at the call, not one, so the casts were hiding real mistakes too. Removing them surfaced four, now fixed: a `primaryKey: true` that is not a spec Field key at all, a field typed `'longtext'` where the spec spells it `'textarea'`, and two validation-rule fixtures missing the required `name`/`message`. Two casts in engine.ts were load-bearing for an unrelated reason — `registerApp(manifest: any)` widens its map branch to `unknown` — and are replaced by stating the contract once on the entries, which also makes the adjacent `(objDef as any).name = name` a checked assignment. `register-object-authored-shape.pin.ts` pins both halves so a re-flip cannot land quietly: the #5543 literal compiles with no cast, and an unknown key, a wrong field type, a missing `name`, and a bare-string field each still fail. It is a `.pin.ts` rather than a test because this package's tsconfig excludes its tests, which would make the directives phantom checks (#5286 PINS_CHECKED). A companion test registers the same literal for real and asserts the path still fills no defaults and still does not throw. Runtime behaviour is unchanged in both directions. Measured objectql TEST_DEBT: 333 before → 332 after (ledger records 355). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N4y4NTZYxr2sRRzBb5AQxw --- .../register-object-authored-shape-casts.md | 56 ++++++++ .../src/bulk-write-per-row-hooks.test.ts | 4 +- .../src/engine-ambient-transaction.test.ts | 10 +- .../src/engine-audit-anchor-write.test.ts | 8 +- .../engine-autonumber-runtime-owned.test.ts | 4 +- .../objectql/src/engine-bulk-contract.test.ts | 2 +- .../src/engine-cascade-delete.test.ts | 2 +- .../engine-dangling-reference-audit.test.ts | 6 +- .../objectql/src/engine-data-events.bench.ts | 2 +- .../objectql/src/engine-data-events.test.ts | 8 +- .../src/engine-default-value-tokens.test.ts | 4 +- .../src/engine-delete-dispatch.test.ts | 2 +- .../objectql/src/engine-filter-alias.test.ts | 2 +- .../src/engine-filter-array-lowering.test.ts | 2 +- .../src/engine-findone-contract.test.ts | 4 +- .../src/engine-lifecycle-datasource.test.ts | 2 +- ...ngine-lookup-referential-integrity.test.ts | 6 +- .../src/engine-transaction-contract.test.ts | 2 +- .../engine-transaction-observability.test.ts | 4 +- .../engine-transaction-same-origin.test.ts | 6 +- .../src/engine-unknown-option.test.ts | 4 +- .../engine-update-prior-read-scope.test.ts | 2 +- .../src/engine-wire-alias-reject.test.ts | 2 +- packages/objectql/src/engine.ts | 21 ++- .../src/hook-condition-bulk-previous.test.ts | 2 +- .../src/hook-condition-fail-loud.test.ts | 2 +- .../src/hook-condition-merged-record.test.ts | 2 +- .../src/hook-condition-previous-scope.test.ts | 6 +- .../src/hook-input-shape-contract.test.ts | 2 +- .../src/protocol-batch-atomic.test.ts | 6 +- .../src/protocol-clone-real-engine.test.ts | 4 +- .../src/protocol-recorded-by-null.test.ts | 6 +- .../objectql/src/protocol-references.test.ts | 4 +- .../src/protocol-registry-shadow.test.ts | 4 +- ...ol-save-meta-repo-path-real-engine.test.ts | 6 +- .../src/protocol-unknown-query-param.test.ts | 2 +- .../src/protocol-unregistered-object.test.ts | 2 +- ...rotocol-writepath-object-ownership.test.ts | 5 +- .../src/query-expression-conformance.test.ts | 10 +- .../src/register-object-authored-shape.pin.ts | 67 +++++++++ .../register-object-authored-shape.test.ts | 58 ++++++++ .../objectql/src/registry-log-level.test.ts | 4 +- .../src/registry-tenancy-posture.test.ts | 2 +- packages/objectql/src/registry.test.ts | 136 +++++++++--------- .../save-meta-response-conformance.test.ts | 12 +- .../schema-sync-durability-log-level.test.ts | 9 +- .../objectql/src/search-companion.test.ts | 7 +- packages/objectql/src/secret-fields.test.ts | 8 +- packages/objectql/src/summary-rollup.test.ts | 4 +- 49 files changed, 365 insertions(+), 170 deletions(-) create mode 100644 .changeset/register-object-authored-shape-casts.md create mode 100644 packages/objectql/src/register-object-authored-shape.pin.ts create mode 100644 packages/objectql/src/register-object-authored-shape.test.ts diff --git a/.changeset/register-object-authored-shape-casts.md b/.changeset/register-object-authored-shape-casts.md new file mode 100644 index 0000000000..55534fa374 --- /dev/null +++ b/.changeset/register-object-authored-shape-casts.md @@ -0,0 +1,56 @@ +--- +"@objectstack/objectql": patch +--- + +fix(objectql): drop the 135 `as any` that dodged `registerObject`'s old parameter type, and pin the authored shape (#5543) + +**Runtime behaviour is unchanged in both directions.** Nothing in this change +adds, removes, or reorders a single runtime step: `registerObject` still runs no +`parse`, still fills no zod defaults, and still warns rather than throws on a +sparse object. What changes is what the compiler is allowed to see at the call. + +#5543 reported that `registerObject(schema: ServiceObject, …)` demanded the +POST-parse object shape, so a perfectly legal authored literal — + +```ts +ql.registerObject({ name: 'task', label: 'Task', fields: { title: { type: 'text', label: 'Title' } } }) +``` + +— failed with TS2740 asking for ~9 keys (`searchable`, `required`, `multiple`, +`unique`, …) that are zod `.default(...)` products, only exist after a parse the +registry never runs, and that no author is supposed to write. + +The annotation itself is already fixed upstream: ADR-0122 phase 2 (#6083, +`@objectstack/spec` 17.0.0) made the bare alias `ServiceObject` mean the +**authored** (`z.input`) shape, so the existing `ServiceObject` annotation on +both `ObjectQL.registerObject` and `SchemaRegistry.registerObject` now names +exactly what the runtime accepts. No annotation in this package needed to move. + +What the flip left behind — and what this change removes — is the workaround it +made obsolete: **135 `as any` casts** across 46 files in `packages/objectql`, +every one of them written only to get an authored literal past the old +parameter type. A blanket `as any` does not suppress one error, it suppresses +all of them, so those casts were also hiding real mistakes. Deleting them +surfaced four, now fixed: + +- `save-meta-response-conformance.test.ts` declared `primaryKey: true` on a + field. There is no such Field key in the spec (it exists only on + external-catalog remote columns) — inert metadata nothing ever read. +- the same fixture typed a field `'longtext'`, which is not a field type; the + spec spells it `'textarea'`. +- two validation-rule fixtures in `registry.test.ts` omitted the required + `name` and `message`. + +Two casts in `engine.ts` were load-bearing for a different reason — `registerApp` +takes `manifest: any`, so its map branch widens object definitions to `unknown`. +Those are replaced by stating the contract once on the entries +(`as [string, ServiceObject][]`), which also lets the adjacent +`(objDef as any).name = name` become a checked `objDef.name = name`. + +New `register-object-authored-shape.pin.ts` pins both halves of the contract so +a re-flip cannot land quietly: the #5543 literal compiles with no cast, and an +unknown key, a wrong field type, a missing `name`, and a bare-string field each +still fail. It is a `.pin.ts` and not a test because this package's `tsconfig` +excludes its tests, which would make a `@ts-expect-error` there a phantom check. +A companion test registers the same literal for real and asserts the register +path still materializes no defaults and still does not throw. diff --git a/packages/objectql/src/bulk-write-per-row-hooks.test.ts b/packages/objectql/src/bulk-write-per-row-hooks.test.ts index 71ad6ae368..ab56a1e1bb 100644 --- a/packages/objectql/src/bulk-write-per-row-hooks.test.ts +++ b/packages/objectql/src/bulk-write-per-row-hooks.test.ts @@ -1013,8 +1013,8 @@ async function boot(hooks: Hook[]): Promise<{ engine: ObjectQL; driver: any }> { const driver = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(taskObject as any); - engine.registry.registerObject(otherObject as any); + engine.registry.registerObject(taskObject); + engine.registry.registerObject(otherObject); if (hooks.length > 0) { bindHooksToEngine(engine, hooks, { packageId: 'app:test', logger: silentLogger }); } diff --git a/packages/objectql/src/engine-ambient-transaction.test.ts b/packages/objectql/src/engine-ambient-transaction.test.ts index 75d419a25e..11a8ded6ad 100644 --- a/packages/objectql/src/engine-ambient-transaction.test.ts +++ b/packages/objectql/src/engine-ambient-transaction.test.ts @@ -82,7 +82,7 @@ describe('engine ambient transaction (ADR-0034)', () => { seen = d.seen; engine.registerDriver(d.driver, true); await engine.init(); - engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } } as any); + engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } }); }); it('threads the active transaction into writes given NO explicit context', async () => { @@ -243,7 +243,7 @@ describe('ScopedContext.transaction joins the ambient transaction (ADR-0067 D2, committedNames = d.committedNames; engine.registerDriver(d.driver, true); await engine.init(); - engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } } as any); + engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } }); }); /** @@ -373,7 +373,7 @@ describe('ScopedContext.transaction joins the ambient transaction (ADR-0067 D2, const oneConn = new ObjectQL(); oneConn.registerDriver(d.driver, true); await oneConn.init(); - oneConn.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } } as any); + oneConn.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } }); (oneConn as any).registerHook( 'afterInsert', async (ctx: any) => { @@ -434,7 +434,7 @@ describe('ScopedContext trio joins the ambient transaction (ADR-0067 D2, #6406)' committedNames = d.committedNames; engine.registerDriver(d.driver, true); await engine.init(); - engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } } as any, 'test'); + engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } }, 'test'); }); const scoped = () => (engine as any).createContext({ userId: 'u1' }) as ScopedContext; @@ -574,7 +574,7 @@ describe('ScopedContext trio joins the ambient transaction (ADR-0067 D2, #6406)' const oneConn = new ObjectQL(); oneConn.registerDriver(d.driver, true); await oneConn.init(); - oneConn.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } } as any, 'test'); + oneConn.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } }, 'test'); await expect( oneConn.transaction(async () => { diff --git a/packages/objectql/src/engine-audit-anchor-write.test.ts b/packages/objectql/src/engine-audit-anchor-write.test.ts index cd9451d02f..a96954f563 100644 --- a/packages/objectql/src/engine-audit-anchor-write.test.ts +++ b/packages/objectql/src/engine-audit-anchor-write.test.ts @@ -114,7 +114,7 @@ describe('[#4447] created_at is engine-owned on an ordinary write', () => { const { driver } = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(taskObject as any); + engine.registry.registerObject(taskObject); }); /** An ordinary authenticated caller — NOT `isSystem`, no `preserveAudit`. */ @@ -260,7 +260,7 @@ describe('[#4447] a declared audit field cannot loosen the platform posture', () const { driver } = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(shadowed as any); + engine.registry.registerObject(shadowed); }); it('the registry restores the engine-owned governance', () => { @@ -351,7 +351,7 @@ describe('[#4513] the read-path normalizer answers what this engine enforces', ( // [#4311] Typed call site: `packageId` is required, and the ledger over // this package's hidden test layer is a shrink-only ratchet — a new test // pays its own way rather than raising the frozen number. - engine.registry.registerObject(shadowed as any, 'test'); + engine.registry.registerObject(shadowed, 'test'); }); it('the normalizer reproduces the registry\'s governance, field for field', () => { @@ -400,7 +400,7 @@ describe('[#4513] the read-path normalizer answers what this engine enforces', ( created_at: { label: 'Created At', type: 'datetime' as const, readonly: false }, }, }; - engine.registry.registerObject(optedOut as any, 'test'); + engine.registry.registerObject(optedOut, 'test'); const enforced: any = engine.registry.getObject('audit_optout')?.fields.created_at; const reported: any = (applyAuditFieldGovernance(optedOut) as any).fields.created_at; diff --git a/packages/objectql/src/engine-autonumber-runtime-owned.test.ts b/packages/objectql/src/engine-autonumber-runtime-owned.test.ts index 0be82815b2..22de28069e 100644 --- a/packages/objectql/src/engine-autonumber-runtime-owned.test.ts +++ b/packages/objectql/src/engine-autonumber-runtime-owned.test.ts @@ -156,7 +156,7 @@ async function makeEngine(opts: { nativeAutonumber?: boolean } = {}) { const rig = makeStubDriver(opts); engine.registerDriver(rig.driver, true); await engine.init(); - engine.registry.registerObject(ACCOUNT as any); + engine.registry.registerObject(ACCOUNT); const protocol = new ObjectStackProtocolImplementation(engine); return { engine, protocol, ...rig }; } @@ -581,7 +581,7 @@ describe('#5628 — a `readonly: true` autonumber keeps the #5503 exemption set' let rig: Awaited>; beforeEach(async () => { rig = await makeEngine(); - rig.engine.registry.registerObject(INVOICE as any, 'test'); + rig.engine.registry.registerObject(INVOICE, 'test'); }); it('still strips an ordinary caller-supplied number and issues the sequence value', async () => { diff --git a/packages/objectql/src/engine-bulk-contract.test.ts b/packages/objectql/src/engine-bulk-contract.test.ts index e978abb615..ec079df19d 100644 --- a/packages/objectql/src/engine-bulk-contract.test.ts +++ b/packages/objectql/src/engine-bulk-contract.test.ts @@ -54,7 +54,7 @@ async function makeEngine(driverOpts?: { bulkCreate?: (object: string, rows: any const d = makeDriver(driverOpts); engine.registerDriver(d.driver, true); await engine.init(); - engine.registry.registerObject({ name: 'task', fields: { title: { type: 'text' } } } as any); + engine.registry.registerObject({ name: 'task', fields: { title: { type: 'text' } } }); return { engine, storeFor: d.storeFor }; } diff --git a/packages/objectql/src/engine-cascade-delete.test.ts b/packages/objectql/src/engine-cascade-delete.test.ts index 858fec53e2..abfc07a25a 100644 --- a/packages/objectql/src/engine-cascade-delete.test.ts +++ b/packages/objectql/src/engine-cascade-delete.test.ts @@ -99,7 +99,7 @@ describe('cascadeDeleteRelations — required FK escalates set_null → restrict const { driver } = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - for (const o of [acct, oppRequired, noteOptional, taskCascade]) engine.registry.registerObject(o as any); + for (const o of [acct, oppRequired, noteOptional, taskCascade]) engine.registry.registerObject(o); }); it('refuses to delete a parent with a REQUIRED-FK child (DELETE_RESTRICTED, 409) and leaves both rows', async () => { diff --git a/packages/objectql/src/engine-dangling-reference-audit.test.ts b/packages/objectql/src/engine-dangling-reference-audit.test.ts index a661a0dd04..cb62382b8a 100644 --- a/packages/objectql/src/engine-dangling-reference-audit.test.ts +++ b/packages/objectql/src/engine-dangling-reference-audit.test.ts @@ -144,9 +144,9 @@ describe('[#4551] the engine reports the dangling rows its own `isSystem` exempt stores = stub.stores; engine.registerDriver(stub.driver, true); await engine.init(); - engine.registry.registerObject(permissionSet as any); - engine.registry.registerObject(binding as any); - engine.registry.registerObject(history as any); + engine.registry.registerObject(permissionSet); + engine.registry.registerObject(binding); + engine.registry.registerObject(history); await engine.insert('aud_permission_set', { id: 'ps_real', name: 'Real' }, { context: { isSystem: true } } as any); }); diff --git a/packages/objectql/src/engine-data-events.bench.ts b/packages/objectql/src/engine-data-events.bench.ts index 89be419ab8..18601fc7b3 100644 --- a/packages/objectql/src/engine-data-events.bench.ts +++ b/packages/objectql/src/engine-data-events.bench.ts @@ -104,7 +104,7 @@ async function makeEngine(withRealtime: boolean): Promise { const engine = new ObjectQL(); engine.registerDriver(makeStubDriver(), true); await engine.init(); - engine.registry.registerObject(task as any, 'bench'); + engine.registry.registerObject(task, 'bench'); if (withRealtime) engine.setRealtimeService(nullRealtime); // Silence the per-write logger so log formatting is not in the measurement. const logger = (engine as any).logger; diff --git a/packages/objectql/src/engine-data-events.test.ts b/packages/objectql/src/engine-data-events.test.ts index 21eca1b1b6..400fee62f0 100644 --- a/packages/objectql/src/engine-data-events.test.ts +++ b/packages/objectql/src/engine-data-events.test.ts @@ -121,7 +121,7 @@ describe('#4626 — engine writes publish true DataEvents', () => { const { driver } = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(task as any); + engine.registry.registerObject(task); engine.setRealtimeService(realtime); warn = vi.spyOn((engine as any).logger, 'warn').mockImplementation(() => undefined); }); @@ -219,7 +219,7 @@ describe('#4626 — engine writes publish true DataEvents', () => { const { driver } = makeStubDriver(); bare.registerDriver(driver, true); await bare.init(); - bare.registry.registerObject(task as any); + bare.registry.registerObject(task); await expect(bare.insert('task', { title: 'no realtime' })).resolves.toBeTruthy(); expect(published).toHaveLength(0); @@ -256,7 +256,7 @@ describe('#4639 — predicate writes publish aggregate BulkDataEvents', () => { const { driver } = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(task as any); + engine.registry.registerObject(task); engine.setRealtimeService(realtime); warn = vi.spyOn((engine as any).logger, 'warn').mockImplementation(() => undefined); }); @@ -358,7 +358,7 @@ describe('#4639 — predicate writes publish aggregate BulkDataEvents', () => { driver.updateMany = async () => ({ acknowledged: true } as any); offContract.registerDriver(driver, true); await offContract.init(); - offContract.registry.registerObject(task as any); + offContract.registry.registerObject(task); offContract.setRealtimeService(realtime); const offWarn = vi.spyOn((offContract as any).logger, 'warn').mockImplementation(() => undefined); await offContract.insert('task', [{ title: 'a', status: 'open' }]); diff --git a/packages/objectql/src/engine-default-value-tokens.test.ts b/packages/objectql/src/engine-default-value-tokens.test.ts index 02349972c7..80185c2f69 100644 --- a/packages/objectql/src/engine-default-value-tokens.test.ts +++ b/packages/objectql/src/engine-default-value-tokens.test.ts @@ -90,7 +90,7 @@ describe('[#4560] the `current_user` defaultValue token is engine-owned', () => engine = new ObjectQL(); engine.registerDriver(makeStubDriver().driver, true); await engine.init(); - engine.registry.registerObject(owned as any); + engine.registry.registerObject(owned); }); it('stamps the acting user id on an authenticated insert', async () => { @@ -147,7 +147,7 @@ describe('[#4597] the `NOW()` defaultValue token is engine-owned too', () => { engine = new ObjectQL(); engine.registerDriver(makeStubDriver().driver, true); await engine.init(); - engine.registry.registerObject(stamped as any); + engine.registry.registerObject(stamped); }); it('resolves on a NON-SQL datasource — the insert succeeds and stores a real instant', async () => { diff --git a/packages/objectql/src/engine-delete-dispatch.test.ts b/packages/objectql/src/engine-delete-dispatch.test.ts index 3887646b6b..75b0621838 100644 --- a/packages/objectql/src/engine-delete-dispatch.test.ts +++ b/packages/objectql/src/engine-delete-dispatch.test.ts @@ -53,7 +53,7 @@ async function makeEngine() { const { driver, calls } = makeRecordingDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject({ name: 'task', fields: { title: { type: 'text' } } } as any); + engine.registry.registerObject({ name: 'task', fields: { title: { type: 'text' } } }); return { engine, calls }; } diff --git a/packages/objectql/src/engine-filter-alias.test.ts b/packages/objectql/src/engine-filter-alias.test.ts index 12289896b4..d0dbcd448e 100644 --- a/packages/objectql/src/engine-filter-alias.test.ts +++ b/packages/objectql/src/engine-filter-alias.test.ts @@ -103,7 +103,7 @@ describe('filter → where folds on every engine method (#4346)', () => { stores = stub.stores; engine.registerDriver(stub.driver, true); await engine.init(); - engine.registry.registerObject(task as any); + engine.registry.registerObject(task); // The issue's repro set: one open row, two done rows. a = await engine.insert('task', { title: 'A', status: 'open' }); b = await engine.insert('task', { title: 'B', status: 'done' }); diff --git a/packages/objectql/src/engine-filter-array-lowering.test.ts b/packages/objectql/src/engine-filter-array-lowering.test.ts index c573f5be20..3b9d373cb1 100644 --- a/packages/objectql/src/engine-filter-array-lowering.test.ts +++ b/packages/objectql/src/engine-filter-array-lowering.test.ts @@ -155,7 +155,7 @@ describe('Door 2 lowers FilterArray to FilterCondition before the driver (#5158) engine = new ObjectQL(); engine.registerDriver(rec.driver, true); await engine.init(); - engine.registry.registerObject(deal as any); + engine.registry.registerObject(deal); await engine.insert('deal', { id: 'd1', stage: 'won', amount: 10, owner_id: 'u1' }); await engine.insert('deal', { id: 'd2', stage: 'lost', amount: 20, owner_id: 'u2' }); await engine.insert('deal', { id: 'd3', stage: 'won', amount: 30, owner_id: 'u1' }); diff --git a/packages/objectql/src/engine-findone-contract.test.ts b/packages/objectql/src/engine-findone-contract.test.ts index f2b9bdf463..c195011eaf 100644 --- a/packages/objectql/src/engine-findone-contract.test.ts +++ b/packages/objectql/src/engine-findone-contract.test.ts @@ -127,8 +127,8 @@ describe('findOne executes what it declares and refuses an empty predicate (#441 reads = mem.reads; engine.registerDriver(mem.driver, true); await engine.init(); - engine.registry.registerObject(account as any); - engine.registry.registerObject(person as any); + engine.registry.registerObject(account); + engine.registry.registerObject(person); // The issue's own repro set. one = await engine.insert('crm_account', { name: 'One', industry: 'Retail', annual_revenue: 100 }); two = await engine.insert('crm_account', { name: 'Two', industry: 'Metals', annual_revenue: 200 }); diff --git a/packages/objectql/src/engine-lifecycle-datasource.test.ts b/packages/objectql/src/engine-lifecycle-datasource.test.ts index 6de85029c0..0af1c7105c 100644 --- a/packages/objectql/src/engine-lifecycle-datasource.test.ts +++ b/packages/objectql/src/engine-lifecycle-datasource.test.ts @@ -82,7 +82,7 @@ describe('lifecycle-class datasource separation (ADR-0057 §3.6)', () => { primary = stubDriver('memory'); engine.registerDriver(primary, true); await engine.init(); - for (const o of OBJECTS) engine.registry.registerObject(o as any); + for (const o of OBJECTS) engine.registry.registerObject(o); }); it('without a telemetry datasource, every object resolves exactly as before', () => { diff --git a/packages/objectql/src/engine-lookup-referential-integrity.test.ts b/packages/objectql/src/engine-lookup-referential-integrity.test.ts index 3c8326e17d..279fd594a0 100644 --- a/packages/objectql/src/engine-lookup-referential-integrity.test.ts +++ b/packages/objectql/src/engine-lookup-referential-integrity.test.ts @@ -154,9 +154,9 @@ describe('[#4441] a lookup id that resolves to nothing is refused', () => { stores = stub.stores; engine.registerDriver(stub.driver, true); await engine.init(); - engine.registry.registerObject(permissionSet as any); - engine.registry.registerObject(binding as any); - engine.registry.registerObject(task as any); + engine.registry.registerObject(permissionSet); + engine.registry.registerObject(binding); + engine.registry.registerObject(task); await engine.insert('ref_permission_set', { id: 'ps_real', name: 'Real' }, { context: { isSystem: true } } as any); }); diff --git a/packages/objectql/src/engine-transaction-contract.test.ts b/packages/objectql/src/engine-transaction-contract.test.ts index 0f27995b6a..3b663f5905 100644 --- a/packages/objectql/src/engine-transaction-contract.test.ts +++ b/packages/objectql/src/engine-transaction-contract.test.ts @@ -124,7 +124,7 @@ async function engineWith(opts: { transactional: boolean }) { const driver = makeDriver('primary', { transactional: opts.transactional }); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } } as any, '__test__'); + engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } }, '__test__'); return { rec, engine, driver }; } diff --git a/packages/objectql/src/engine-transaction-observability.test.ts b/packages/objectql/src/engine-transaction-observability.test.ts index e10c3a9f8f..dc2ea3fa30 100644 --- a/packages/objectql/src/engine-transaction-observability.test.ts +++ b/packages/objectql/src/engine-transaction-observability.test.ts @@ -135,7 +135,7 @@ describe('transaction() degrade with no beginTransaction warns once (#4619)', () const driver = makeDriver('memory', { transactional: false }); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } } as any, '__test__'); + engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } }, '__test__'); return { rec, engine, driver }; } @@ -197,7 +197,7 @@ describe('transaction() degrade with no beginTransaction warns once (#4619)', () const engine = new ObjectQL({ logger: rec.logger } as any); engine.registerDriver(makeDriver('memory'), true); await engine.init(); - engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } } as any, '__test__'); + engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } }, '__test__'); await engine.transaction(async () => { await engine.insert('thing', { name: 'A' }); diff --git a/packages/objectql/src/engine-transaction-same-origin.test.ts b/packages/objectql/src/engine-transaction-same-origin.test.ts index da4b26c605..1e3fd3fc72 100644 --- a/packages/objectql/src/engine-transaction-same-origin.test.ts +++ b/packages/objectql/src/engine-transaction-same-origin.test.ts @@ -159,8 +159,8 @@ async function splitEngine() { await engine.init(); // `ledger` is an ordinary BUSINESS object that a mapping rule routes away. engine.setDatasourceMapping([{ objectPattern: 'ledger', datasource: 'ledger_db' }]); - engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } } as any, '__test__'); - engine.registry.registerObject({ name: 'ledger', fields: { name: { type: 'text' } } } as any, '__test__'); + engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } }, '__test__'); + engine.registry.registerObject({ name: 'ledger', fields: { name: { type: 'text' } } }, '__test__'); // The three append-only system ledgers, routed by lifecycle class alone. engine.registry.registerObject({ name: 'sys_audit_log', @@ -488,7 +488,7 @@ describe('a same-origin (single datasource) transaction is untouched (#5351 regr const primary = makeDriver('primary'); engine.registerDriver(primary, true); await engine.init(); - engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } } as any, '__test__'); + engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } }, '__test__'); // Audit-classed, but with NO telemetry datasource registered it resolves to // the default driver like everything else — ADR-0057 §3.6 is opt-in by the // datasource's existence. The carve-out must not fire on it. diff --git a/packages/objectql/src/engine-unknown-option.test.ts b/packages/objectql/src/engine-unknown-option.test.ts index cb1cdc3269..c76fe54353 100644 --- a/packages/objectql/src/engine-unknown-option.test.ts +++ b/packages/objectql/src/engine-unknown-option.test.ts @@ -101,8 +101,8 @@ describe('unknown engine option keys are rejected (#4371 option 2)', () => { finds = mem.finds; engine.registerDriver(mem.driver, true); await engine.init(); - engine.registry.registerObject(task as any); - engine.registry.registerObject(person as any); + engine.registry.registerObject(task); + engine.registry.registerObject(person); await engine.insert('person', { id: 'p1', name: 'P' }); a = await engine.insert('task', { title: 'A', status: 'open', owner: 'p1' }); await engine.insert('task', { title: 'B', status: 'done', owner: 'p1' }); diff --git a/packages/objectql/src/engine-update-prior-read-scope.test.ts b/packages/objectql/src/engine-update-prior-read-scope.test.ts index d1ec6a6315..86dafbe467 100644 --- a/packages/objectql/src/engine-update-prior-read-scope.test.ts +++ b/packages/objectql/src/engine-update-prior-read-scope.test.ts @@ -179,7 +179,7 @@ async function boot(hooks: Hook[] = [], objects: unknown[] = [taskA, taskB]) { const stub = makeCountingDriver(); engine.registerDriver(stub.driver, true); await engine.init(); - for (const o of objects) engine.registry.registerObject(o as any); + for (const o of objects) engine.registry.registerObject(o); const warn = vi.fn(); if (hooks.length > 0) { bindHooksToEngine(engine, hooks, { diff --git a/packages/objectql/src/engine-wire-alias-reject.test.ts b/packages/objectql/src/engine-wire-alias-reject.test.ts index 33ea89a628..56a3467780 100644 --- a/packages/objectql/src/engine-wire-alias-reject.test.ts +++ b/packages/objectql/src/engine-wire-alias-reject.test.ts @@ -90,7 +90,7 @@ describe('wire-only alias spellings are rejected on direct engine calls (#4371)' finds = mem.finds; engine.registerDriver(mem.driver, true); await engine.init(); - engine.registry.registerObject(task as any); + engine.registry.registerObject(task); await engine.insert('task', { title: 'B', status: 'done' }); await engine.insert('task', { title: 'A', status: 'open' }); await engine.insert('task', { title: 'C', status: 'done' }); diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts index 946bbeae3f..90f6adf578 100644 --- a/packages/objectql/src/engine.ts +++ b/packages/objectql/src/engine.ts @@ -2588,10 +2588,15 @@ export class ObjectQL implements IObjectQLEngine { } } else { this.logger.debug('Registering objects from manifest (Map)', { id, objectCount: Object.keys(manifest.objects).length }); - for (const [name, objDef] of Object.entries(manifest.objects)) { + // `manifest` is `any` (a raw authored manifest), so the map branch + // widens its values to `unknown`. State the contract once, on the + // entries, rather than casting the argument at the call: the map + // values ARE authored `ServiceObject`s — the INPUT shape, which is + // what `registerObject` takes since ADR-0122 phase 2 (#6083). + for (const [name, objDef] of Object.entries(manifest.objects) as [string, ServiceObject][]) { // Ensure name in definition matches key - (objDef as any).name = name; - const fqn = this._registry.registerObject(objDef as any, id, namespace, 'own'); + objDef.name = name; + const fqn = this._registry.registerObject(objDef, id, namespace, 'own'); this.logger.debug('Registered Object', { fqn, from: id }); } } @@ -2614,7 +2619,7 @@ export class ObjectQL implements IObjectQLEngine { indexes: ext.indexes, }; // Register as extension (namespace is undefined since we're targeting by FQN) - this._registry.registerObject(extDef as any, id, undefined, 'extend', priority); + this._registry.registerObject(extDef, id, undefined, 'extend', priority); this.logger.debug('Registered Object Extension', { target: targetFqn, priority, from: id }); } } @@ -2809,11 +2814,13 @@ export class ObjectQL implements IObjectQLEngine { this.logger.debug('Registered Object', { fqn, from: pluginName }); } } else { - const entries = Object.entries(plugin.objects); + // Same contract statement as the manifest map branch above — + // authored `ServiceObject`s (INPUT shape, ADR-0122 phase 2). + const entries = Object.entries(plugin.objects) as [string, ServiceObject][]; this.logger.debug('Registering plugin objects (Map)', { pluginName, count: entries.length }); for (const [name, objDef] of entries) { - (objDef as any).name = name; - const fqn = this._registry.registerObject(objDef as any, ownerId, pluginNamespace, 'own'); + objDef.name = name; + const fqn = this._registry.registerObject(objDef, ownerId, pluginNamespace, 'own'); this.logger.debug('Registered Object', { fqn, from: pluginName }); } } diff --git a/packages/objectql/src/hook-condition-bulk-previous.test.ts b/packages/objectql/src/hook-condition-bulk-previous.test.ts index 76706dfbb9..2ef45c1ced 100644 --- a/packages/objectql/src/hook-condition-bulk-previous.test.ts +++ b/packages/objectql/src/hook-condition-bulk-previous.test.ts @@ -477,7 +477,7 @@ async function bootEngine(hooks: Hook[]): Promise { const engine = new ObjectQL(); engine.registerDriver(makeStubDriver(), true); await engine.init(); - engine.registry.registerObject(taskObject as any); + engine.registry.registerObject(taskObject); bindHooksToEngine(engine, hooks, { packageId: 'app:test', logger: silentLogger }); return engine; } diff --git a/packages/objectql/src/hook-condition-fail-loud.test.ts b/packages/objectql/src/hook-condition-fail-loud.test.ts index 0a5feee794..702e5f09f8 100644 --- a/packages/objectql/src/hook-condition-fail-loud.test.ts +++ b/packages/objectql/src/hook-condition-fail-loud.test.ts @@ -478,7 +478,7 @@ async function bootEngine(hooks: Hook[]): Promise { const engine = new ObjectQL(); engine.registerDriver(makeStubDriver(), true); await engine.init(); - engine.registry.registerObject(taskObject as any); + engine.registry.registerObject(taskObject); bindHooksToEngine(engine, hooks, { packageId: 'app:test', logger: silentLogger }); return engine; } diff --git a/packages/objectql/src/hook-condition-merged-record.test.ts b/packages/objectql/src/hook-condition-merged-record.test.ts index fe162cf9ed..110bb1a6cf 100644 --- a/packages/objectql/src/hook-condition-merged-record.test.ts +++ b/packages/objectql/src/hook-condition-merged-record.test.ts @@ -324,7 +324,7 @@ describe('[#4770] showcase repro — showcase_audit_task_completion over a real const { driver } = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(taskObject as any); + engine.registry.registerObject(taskObject); audited = []; warn = vi.fn(); diff --git a/packages/objectql/src/hook-condition-previous-scope.test.ts b/packages/objectql/src/hook-condition-previous-scope.test.ts index d276c2af30..a55fab76d7 100644 --- a/packages/objectql/src/hook-condition-previous-scope.test.ts +++ b/packages/objectql/src/hook-condition-previous-scope.test.ts @@ -367,7 +367,7 @@ describe('[#4784] transition condition over a real engine', () => { reads = stub.reads; engine.registerDriver(stub.driver, true); await engine.init(); - engine.registry.registerObject(taskObject as any); + engine.registry.registerObject(taskObject); audited = []; warn = vi.fn(); @@ -470,7 +470,7 @@ describe('[#4784] a condition that never mentions `previous` costs zero extra fe const stub = makeStubDriver(); engine.registerDriver(stub.driver, true); await engine.init(); - engine.registry.registerObject(taskObject as any); + engine.registry.registerObject(taskObject); bindHooksToEngine(engine, hooks, { packageId: 'app:pin', logger: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} }, @@ -571,7 +571,7 @@ describe('[#5272] a single-record delete binds `previous` through the real engin const stub = makeStubDriver(); engine.registerDriver(stub.driver, true); await engine.init(); - engine.registry.registerObject(taskObject as any); + engine.registry.registerObject(taskObject); const warn = vi.fn(); bindHooksToEngine(engine, hooks, { packageId: 'app:showcase', diff --git a/packages/objectql/src/hook-input-shape-contract.test.ts b/packages/objectql/src/hook-input-shape-contract.test.ts index 2a11c8b2fe..e861f446ac 100644 --- a/packages/objectql/src/hook-input-shape-contract.test.ts +++ b/packages/objectql/src/hook-input-shape-contract.test.ts @@ -510,6 +510,6 @@ async function boot(): Promise<{ engine: ObjectQL; driver: any }> { const driver = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(taskObject as any); + engine.registry.registerObject(taskObject); return { engine, driver }; } diff --git a/packages/objectql/src/protocol-batch-atomic.test.ts b/packages/objectql/src/protocol-batch-atomic.test.ts index a1d1cc83a3..babef9bc0d 100644 --- a/packages/objectql/src/protocol-batch-atomic.test.ts +++ b/packages/objectql/src/protocol-batch-atomic.test.ts @@ -112,7 +112,7 @@ describe('atomic batchData over the real engine (ADR-0119 D4 / ADR-0034)', () => d = makeSnapshotDriver(); engine.registerDriver(d.driver, true); await engine.init(); - engine.registry.registerObject({ name: 'invoice', fields: { title: { type: 'text' } } } as any); + engine.registry.registerObject({ name: 'invoice', fields: { title: { type: 'text' } } }); protocol = new ObjectStackProtocolImplementation(engine as any); }); @@ -204,7 +204,7 @@ describe('atomic batchData over the real engine (ADR-0119 D4 / ADR-0034)', () => delete plain.driver.beginTransaction; bare.registerDriver(plain.driver, true); await bare.init(); - bare.registry.registerObject({ name: 'invoice', fields: { title: { type: 'text' } } } as any); + bare.registry.registerObject({ name: 'invoice', fields: { title: { type: 'text' } } }); const p = new ObjectStackProtocolImplementation(bare as any); await expect(p.batchData({ @@ -222,7 +222,7 @@ describe('ADR-0119 D1 — transaction is reachable through the contract', () => const d = makeSnapshotDriver(); engine.registerDriver(d.driver, true); await engine.init(); - engine.registry.registerObject({ name: 'invoice', fields: { title: { type: 'text' } } } as any); + engine.registry.registerObject({ name: 'invoice', fields: { title: { type: 'text' } } }); // The point of the pin is the TYPE, not the runtime: before ADR-0119 D1 // this line could not compile — `transaction` was absent from the diff --git a/packages/objectql/src/protocol-clone-real-engine.test.ts b/packages/objectql/src/protocol-clone-real-engine.test.ts index ecc44e8f70..b0893a3394 100644 --- a/packages/objectql/src/protocol-clone-real-engine.test.ts +++ b/packages/objectql/src/protocol-clone-real-engine.test.ts @@ -116,8 +116,8 @@ describe('cloneData — real ObjectQL engine', () => { const { driver } = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(accountObject as any); - engine.registry.registerObject(lockedObject as any); + engine.registry.registerObject(accountObject); + engine.registry.registerObject(lockedObject); protocol = new ObjectStackProtocolImplementation(engine); }); diff --git a/packages/objectql/src/protocol-recorded-by-null.test.ts b/packages/objectql/src/protocol-recorded-by-null.test.ts index 4b43e85587..382e8b79c9 100644 --- a/packages/objectql/src/protocol-recorded-by-null.test.ts +++ b/packages/objectql/src/protocol-recorded-by-null.test.ts @@ -160,9 +160,9 @@ describe('#4556 — protocol write paths store NULL, not the sentinel string', ( const { driver } = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(sysUserObject as any); - engine.registry.registerObject(sysMetadataObject as any); - engine.registry.registerObject(sysMetadataHistoryObject as any); + engine.registry.registerObject(sysUserObject); + engine.registry.registerObject(sysMetadataObject); + engine.registry.registerObject(sysMetadataHistoryObject); await engine.insert('sys_user', { id: 'usr_alice', name: 'Alice' }, { context: { isSystem: true } } as any); protocol = new ObjectStackProtocolImplementation(engine); }); diff --git a/packages/objectql/src/protocol-references.test.ts b/packages/objectql/src/protocol-references.test.ts index 8335212d63..4c5bb4cd84 100644 --- a/packages/objectql/src/protocol-references.test.ts +++ b/packages/objectql/src/protocol-references.test.ts @@ -19,7 +19,7 @@ describe('ObjectStackProtocolImplementation - findReferencesToMeta', () => { registry = new SchemaRegistry({ multiTenant: false }); // Target object — must use registerObject so listItems('object') // surfaces it (objects live in their own contributor map). - registry.registerObject({ name: 'account', label: 'Account', fields: {} } as any, 'pkg'); + registry.registerObject({ name: 'account', label: 'Account', fields: {} }, 'pkg'); // Sibling object whose field points at it. registry.registerObject({ name: 'task', @@ -94,7 +94,7 @@ describe('ObjectStackProtocolImplementation - findReferencesToMeta', () => { }); it('returns empty array when nothing points at the target', async () => { - registry.registerObject({ name: 'orphan', fields: {} } as any, 'pkg'); + registry.registerObject({ name: 'orphan', fields: {} }, 'pkg'); const result = await protocol.findReferencesToMeta({ type: 'object', name: 'orphan' }); expect(result.references).toEqual([]); }); diff --git a/packages/objectql/src/protocol-registry-shadow.test.ts b/packages/objectql/src/protocol-registry-shadow.test.ts index 93aa256957..ae7685d8bd 100644 --- a/packages/objectql/src/protocol-registry-shadow.test.ts +++ b/packages/objectql/src/protocol-registry-shadow.test.ts @@ -174,8 +174,8 @@ describe('registry shadow — control-plane PUT → GET → DELETE keeps the art const { driver } = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(sysMetadataObject as any); - engine.registry.registerObject(sysMetadataHistoryObject as any); + engine.registry.registerObject(sysMetadataObject); + engine.registry.registerObject(sysMetadataHistoryObject); engine.registry.registerItem('app', artifactApp(), 'name', PKG); // No environmentId — single-kernel / control-plane mode, where the // L3 lock gate is bypassed and the GET list hydrates overlay rows diff --git a/packages/objectql/src/protocol-save-meta-repo-path-real-engine.test.ts b/packages/objectql/src/protocol-save-meta-repo-path-real-engine.test.ts index eca218be8f..827bea7e85 100644 --- a/packages/objectql/src/protocol-save-meta-repo-path-real-engine.test.ts +++ b/packages/objectql/src/protocol-save-meta-repo-path-real-engine.test.ts @@ -127,7 +127,7 @@ describe('saveMetaItem — repository write path against real ObjectQL (PR-10d.4 const { driver } = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(sysMetadataObject as any); + engine.registry.registerObject(sysMetadataObject); protocol = new ObjectStackProtocolImplementation(engine); }); @@ -227,8 +227,8 @@ describe('deleteMetaItem — repository write path against real ObjectQL (PR-10d const { driver } = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(sysMetadataObject as any); - engine.registry.registerObject(sysMetadataHistoryObject as any); + engine.registry.registerObject(sysMetadataObject); + engine.registry.registerObject(sysMetadataHistoryObject); protocol = new ObjectStackProtocolImplementation(engine); }); diff --git a/packages/objectql/src/protocol-unknown-query-param.test.ts b/packages/objectql/src/protocol-unknown-query-param.test.ts index 293129582d..170f8f4f3c 100644 --- a/packages/objectql/src/protocol-unknown-query-param.test.ts +++ b/packages/objectql/src/protocol-unknown-query-param.test.ts @@ -124,7 +124,7 @@ describe('#4134 — unknown list query params (real ObjectQL engine)', () => { const { driver, stores } = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(taskObject as any, 'test-package'); + engine.registry.registerObject(taskObject, 'test-package'); protocol = new ObjectStackProtocolImplementation(engine); const rows = new Map>(); diff --git a/packages/objectql/src/protocol-unregistered-object.test.ts b/packages/objectql/src/protocol-unregistered-object.test.ts index f481474532..aa1e27b0a0 100644 --- a/packages/objectql/src/protocol-unregistered-object.test.ts +++ b/packages/objectql/src/protocol-unregistered-object.test.ts @@ -126,7 +126,7 @@ describe('#3770 — data-plane object-existence gate (real ObjectQL engine)', () stores = made.stores; engine.registerDriver(made.driver, true); await engine.init(); - engine.registry.registerObject(registeredObject as any); + engine.registry.registerObject(registeredObject); protocol = new ObjectStackProtocolImplementation(engine); // Case B setup: a physical table exists under the unregistered name, diff --git a/packages/objectql/src/protocol-writepath-object-ownership.test.ts b/packages/objectql/src/protocol-writepath-object-ownership.test.ts index 39520c75c9..6e1d6dac53 100644 --- a/packages/objectql/src/protocol-writepath-object-ownership.test.ts +++ b/packages/objectql/src/protocol-writepath-object-ownership.test.ts @@ -40,6 +40,7 @@ */ import { describe, expect, it } from 'vitest'; +import type { ServiceObject } from '@objectstack/spec/data'; import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; import { SchemaRegistry } from './registry.js'; // [#4550 / #5480] The producer's OWN write-verb dispatch decisions, so this @@ -154,7 +155,7 @@ function makeHarness() { return { registry, protocol, rows, historyRows, synced, engine }; } -function objectBody(name: string, extra?: Record) { +function objectBody(name: string, extra?: Record): ServiceObject { return { name, label: 'Invoice', @@ -301,7 +302,7 @@ describe('#4636 — cloud#970 counter-example: a freshly created app stays edita // Exactly what `applyObjectRegistryMutation` would do if it moved the // ownership key and left the provenance stamp out — the shape measured // as "B-minimal" on this issue. Nothing else about the run differs. - registry.registerObject(objectBody('myapp_invoice') as any, APP_PKG); + registry.registerObject(objectBody('myapp_invoice'), APP_PKG); await expect(protocol.saveMetaItem({ type: 'object', diff --git a/packages/objectql/src/query-expression-conformance.test.ts b/packages/objectql/src/query-expression-conformance.test.ts index 40e200f2fa..7f8488969c 100644 --- a/packages/objectql/src/query-expression-conformance.test.ts +++ b/packages/objectql/src/query-expression-conformance.test.ts @@ -215,8 +215,8 @@ describe('#4226 — sort / select / expand on the list path (real ObjectQL engin stores = made.stores; engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(projectObject as any, 'test-package'); - engine.registry.registerObject(taskObject as any, 'test-package'); + engine.registry.registerObject(projectObject, 'test-package'); + engine.registry.registerObject(taskObject, 'test-package'); protocol = new ObjectStackProtocolImplementation(engine); stores.set('showcase_project', new Map([ @@ -805,9 +805,9 @@ describe('#4254 — searchFields / groupBy / aggregations on the list path (real delete (driver as any).aggregate; engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(projectObject as any, 'test-package'); - engine.registry.registerObject(taskObject as any, 'test-package'); - engine.registry.registerObject(memoObject as any, 'test-package'); + engine.registry.registerObject(projectObject, 'test-package'); + engine.registry.registerObject(taskObject, 'test-package'); + engine.registry.registerObject(memoObject, 'test-package'); protocol = new ObjectStackProtocolImplementation(engine); const tasks = new Map>(); diff --git a/packages/objectql/src/register-object-authored-shape.pin.ts b/packages/objectql/src/register-object-authored-shape.pin.ts new file mode 100644 index 0000000000..0a9142c879 --- /dev/null +++ b/packages/objectql/src/register-object-authored-shape.pin.ts @@ -0,0 +1,67 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #5543 — compile-time pin for the shape `registerObject` accepts. + * + * Both doors (`ObjectQL.registerObject` and `SchemaRegistry.registerObject`) + * are annotated `ServiceObject`. Since ADR-0122 phase 2 (#6083) that bare spec + * alias means the **authored** (`z.input`) shape — defaulted keys optional, + * pre-transform — which is what the registry actually receives: `registerObject` + * runs no `parse`, so nothing on that path materializes a `.default(...)`. + * Before the flip the same annotation named the POST-parse shape, so a legal + * authored literal was rejected with TS2740 demanding ~9 keys an author is not + * supposed to write, and 135 call sites in this package reached for `as any`. + * + * This module pins both halves of that contract so a re-flip cannot land quietly: + * - the authored literal from #5543 compiles with **no** cast, and + * - a genuinely wrong literal (unknown key, wrong field type, missing `name`) + * still fails — the loosening must not admit garbage. + * + * WHY A `.pin.ts` AND NOT A `*.test.ts`: `packages/objectql/tsconfig.json` + * excludes `**\/*.test.ts`, so a `@ts-expect-error` written in a test file here + * is a phantom check — no tsc program the `typecheck` script runs would ever + * evaluate it, and deleting the directive would leave every gate green + * (AGENTS.md, #5286's `PINS_CHECKED`). This file IS in that program. It carries + * no executable pin: the assertions live inside a function nobody calls, so the + * only thing it costs at runtime is the literal below, which the companion + * `register-object-authored-shape.test.ts` registers for real. + */ + +import type { ObjectQL } from './engine.js'; +import type { SchemaRegistry } from './registry.js'; + +/** The exact literal #5543 reported as uncompilable. */ +export const AUTHORED_TASK_OBJECT = { + name: 'task', + label: 'Task', + fields: { title: { type: 'text', label: 'Title' } }, +} as const; + +type EngineArg = Parameters[0]; +type RegistryArg = Parameters[0]; + +/** + * Never called — every line below is a type-level assertion evaluated by + * `tsc --noEmit`. The parameters are taken as arguments rather than read off a + * live engine so the pin needs no instance and no import cycle. + */ +export function __pinRegisterObjectAcceptsAuthoredLiterals( + engineRegister: (schema: EngineArg) => unknown, + registryRegister: (schema: RegistryArg, packageId: string) => unknown, +): void { + // ── POSITIVE: what an author writes, verbatim, with no cast. ────────────── + engineRegister({ name: 'task', label: 'Task', fields: { title: { type: 'text', label: 'Title' } } }); + registryRegister({ name: 'task', label: 'Task', fields: { title: { type: 'text', label: 'Title' } } }, 'p'); + // `label` and every other `.default(...)` key stays optional. + engineRegister({ name: 'sys_thing', fields: {} }); + + // ── NEGATIVE: the loosening must not admit garbage. ─────────────────────── + // @ts-expect-error `primaryKey` is not a spec Field key (it exists only on external-catalog columns) + engineRegister({ name: 'task', fields: { title: { type: 'text', primaryKey: true } } }); + // @ts-expect-error 'longtext' is not a field type — the spec spells it 'textarea' + engineRegister({ name: 'task', fields: { body: { type: 'longtext' } } }); + // @ts-expect-error `name` is required on every object, defaults or not + engineRegister({ label: 'Task', fields: {} }); + // @ts-expect-error a field must be an object, not a bare type string + engineRegister({ name: 'task', fields: { title: 'text' } }); +} diff --git a/packages/objectql/src/register-object-authored-shape.test.ts b/packages/objectql/src/register-object-authored-shape.test.ts new file mode 100644 index 0000000000..13c4f2c780 --- /dev/null +++ b/packages/objectql/src/register-object-authored-shape.test.ts @@ -0,0 +1,58 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #5543 — runtime half of the `registerObject` authored-shape contract. + * + * The compile-time half lives in `register-object-authored-shape.pin.ts` (it has + * to: this file is excluded from every tsc program the `typecheck` script runs, + * so a `@ts-expect-error` written here would never be evaluated). What this file + * adds is the other direction — that the literal which now *compiles* also + * *registers*, unparsed, and comes back out with the authored keys intact and + * without the `.default(...)` products fabricated on the way through. That is + * the runtime fact the INPUT annotation is claiming, and it is why `registerObject` + * must keep its warn-never-throw posture rather than growing a `parse`. + */ + +import { describe, it, expect } from 'vitest'; +import { SchemaRegistry } from './registry'; +import { AUTHORED_TASK_OBJECT } from './register-object-authored-shape.pin'; + +describe('registerObject takes the AUTHORED object shape (#5543)', () => { + it('registers the #5543 literal with no cast and resolves it back', () => { + const registry = new SchemaRegistry({ multiTenant: false }); + registry.logLevel = 'silent'; + + // No `as any` — this call is the regression guard. If the parameter is ever + // re-annotated to the POST-parse shape, this line stops compiling. + const fqn = registry.registerObject(AUTHORED_TASK_OBJECT, 'com.example.app'); + + expect(fqn).toBe('task'); + const resolved = registry.getObject('task'); + expect(resolved?.name).toBe('task'); + expect(resolved?.label).toBe('Task'); + expect(resolved?.fields?.title).toMatchObject({ type: 'text', label: 'Title' }); + }); + + it('does NOT materialize zod defaults on the register path — the annotation tells the truth', () => { + // `registerObject` runs no `ObjectSchemaBase.parse`, so the keys that only + // exist AFTER a parse (`searchable`, `required`, `multiple`, `unique`, …) + // must be absent on the way out. If a future change adds a parse at this + // seam it would also add a throw at boot, which registry.ts's + // warn-never-throw philosophy rules out — this asserts the seam stayed put. + const registry = new SchemaRegistry({ multiTenant: false }); + registry.logLevel = 'silent'; + registry.registerObject({ name: 'plain', fields: { note: { type: 'text' } } }, 'com.example.app'); + + const note = registry.getObject('plain')?.fields?.note as Record | undefined; + expect(note).toBeDefined(); + for (const defaulted of ['searchable', 'required', 'multiple', 'unique']) { + expect(note).not.toHaveProperty(defaulted); + } + }); + + it('never throws on a sparse authored object (warn-never-throw stands)', () => { + const registry = new SchemaRegistry({ multiTenant: false }); + registry.logLevel = 'silent'; + expect(() => registry.registerObject({ name: 'sparse', fields: {} }, 'com.example.app')).not.toThrow(); + }); +}); diff --git a/packages/objectql/src/registry-log-level.test.ts b/packages/objectql/src/registry-log-level.test.ts index 637c84db4e..e0e7d98ab8 100644 --- a/packages/objectql/src/registry-log-level.test.ts +++ b/packages/objectql/src/registry-log-level.test.ts @@ -22,8 +22,8 @@ describe('SchemaRegistry log-level gating (#3420)', () => { afterEach(() => { delete process.env.OS_REGISTRY_LOG; }); const reRegisterSameOwner = (r: SchemaRegistry) => { - r.registerObject({ name: 'sys_thing', fields: {} } as any, 'com.acme.app', 'sys', 'own'); - r.registerObject({ name: 'sys_thing', fields: {} } as any, 'com.acme.app', 'sys', 'own'); + r.registerObject({ name: 'sys_thing', fields: {} }, 'com.acme.app', 'sys', 'own'); + r.registerObject({ name: 'sys_thing', fields: {} }, 'com.acme.app', 'sys', 'own'); }; it('at the default (info) level, re-registering an owned object is silent — no warn, no debug', () => { diff --git a/packages/objectql/src/registry-tenancy-posture.test.ts b/packages/objectql/src/registry-tenancy-posture.test.ts index 8dd1984b3f..517b78fea2 100644 --- a/packages/objectql/src/registry-tenancy-posture.test.ts +++ b/packages/objectql/src/registry-tenancy-posture.test.ts @@ -112,7 +112,7 @@ describe('#5262 — SchemaRegistry keys its multi-tenant default off OS_TENANCY_ // a mode; the posture read is only the DEFAULT when they say nothing. process.env.OS_TENANCY_POSTURE = 'isolated'; const registry = new SchemaRegistry({ multiTenant: false }); - registry.registerObject({ name: 'lead', fields: {} } as any, 'crm', 'crm', 'own'); + registry.registerObject({ name: 'lead', fields: {} }, 'crm', 'crm', 'own'); const stored = (registry as any).objectContributors.get('lead')[0].definition; expect(stored.fields.organization_id.indexed).toBe(false); }); diff --git a/packages/objectql/src/registry.test.ts b/packages/objectql/src/registry.test.ts index a07f1d14d8..b6d5a95a63 100644 --- a/packages/objectql/src/registry.test.ts +++ b/packages/objectql/src/registry.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { SchemaRegistry, applySystemFields, reconcileManagedApiMethods, warnStrippedLegacyApiMethods, warnFunctionalCompleteness, computeFQN, parseFQN } from './registry'; -import { AUDIT_PROVENANCE_FIELDS } from '@objectstack/spec/data'; +import { AUDIT_PROVENANCE_FIELDS, type ServiceObject } from '@objectstack/spec/data'; describe('SchemaRegistry', () => { let registry: SchemaRegistry; @@ -88,8 +88,8 @@ describe('SchemaRegistry', () => { // ========================================== describe('Object Ownership', () => { it('should register owned object using object name as canonical key', () => { - const obj = { name: 'account', fields: { name: { type: 'text' } } }; - const key = registry.registerObject(obj as any, 'com.example.crm', 'crm', 'own'); + const obj: ServiceObject = { name: 'account', fields: { name: { type: 'text' } } }; + const key = registry.registerObject(obj, 'com.example.crm', 'crm', 'own'); expect(key).toBe('account'); const resolved = registry.getObject('account'); @@ -98,30 +98,30 @@ describe('SchemaRegistry', () => { }); it('should register object without namespace', () => { - const obj = { name: 'task', fields: {} }; - const key = registry.registerObject(obj as any, 'com.example.app'); + const obj: ServiceObject = { name: 'task', fields: {} }; + const key = registry.registerObject(obj, 'com.example.app'); expect(key).toBe('task'); expect(registry.getObject('task')).toBeDefined(); }); it('should allow only one owner per object name', () => { - const obj = { name: 'shared', fields: {} }; - registry.registerObject(obj as any, 'com.vendor.a', 'vendor_a', 'own'); + const obj: ServiceObject = { name: 'shared', fields: {} }; + registry.registerObject(obj, 'com.vendor.a', 'vendor_a', 'own'); - const obj2 = { name: 'shared', fields: {} }; + const obj2: ServiceObject = { name: 'shared', fields: {} }; expect(() => { - registry.registerObject(obj2 as any, 'com.vendor.b', undefined, 'own'); + registry.registerObject(obj2, 'com.vendor.b', undefined, 'own'); }).toThrow(/already owned/); }); it('should allow re-registration by same owner', () => { - const obj = { name: 'account', fields: { v1: { type: 'text' } } }; - registry.registerObject(obj as any, 'com.example.crm', 'crm', 'own'); + const obj: ServiceObject = { name: 'account', fields: { v1: { type: 'text' } } }; + registry.registerObject(obj, 'com.example.crm', 'crm', 'own'); - const obj2 = { name: 'account', fields: { v2: { type: 'text' } } }; + const obj2: ServiceObject = { name: 'account', fields: { v2: { type: 'text' } } }; expect(() => { - registry.registerObject(obj2 as any, 'com.example.crm', 'crm', 'own'); + registry.registerObject(obj2, 'com.example.crm', 'crm', 'own'); }).not.toThrow(); const resolved = registry.getObject('account'); @@ -136,8 +136,8 @@ describe('SchemaRegistry', () => { it('designates nameField from an existing title-eligible field when none declared', () => { // No `nameField` declared; `title` (text) is derivable → registry // should DESIGNATE it on the owned object. - const obj = { name: 'ticket', fields: { notes: { type: 'text' }, title: { type: 'text' } } }; - registry.registerObject(obj as any, 'com.example.crm', undefined, 'own'); + const obj: ServiceObject = { name: 'ticket', fields: { notes: { type: 'text' }, title: { type: 'text' } } }; + registry.registerObject(obj, 'com.example.crm', undefined, 'own'); const resolved = registry.getObject('ticket'); expect((resolved as any)?.nameField).toBe('title'); @@ -149,19 +149,19 @@ describe('SchemaRegistry', () => { }); it('prefers a `name`-ish field and respects an explicit pointer', () => { - const named = { name: 'company', fields: { description: { type: 'text' }, company_name: { type: 'text' } } }; - registry.registerObject(named as any, 'com.crm', undefined, 'own'); + const named: ServiceObject = { name: 'company', fields: { description: { type: 'text' }, company_name: { type: 'text' } } }; + registry.registerObject(named, 'com.crm', undefined, 'own'); expect((registry.getObject('company') as any)?.nameField).toBe('company_name'); - const explicit = { name: 'invoice', nameField: 'ref', fields: { ref: { type: 'text' }, memo: { type: 'text' } } }; - registry.registerObject(explicit as any, 'com.fin', undefined, 'own'); + const explicit: ServiceObject = { name: 'invoice', nameField: 'ref', fields: { ref: { type: 'text' }, memo: { type: 'text' } } }; + registry.registerObject(explicit, 'com.fin', undefined, 'own'); expect((registry.getObject('invoice') as any)?.nameField).toBe('ref'); }); it('does NOT add a `name` column to a title-LESS object (no schema migration)', () => { // currency/date/select/lookup → nothing title-eligible. The object // must be left as-is: no `nameField`, no synthesized `name` field. - const obj = { + const obj: ServiceObject = { name: 'sys_ledger_entry', fields: { amount: { type: 'currency' }, @@ -170,7 +170,7 @@ describe('SchemaRegistry', () => { account: { type: 'lookup' }, }, }; - registry.registerObject(obj as any, 'com.objectstack.system', undefined, 'own'); + registry.registerObject(obj, 'com.objectstack.system', undefined, 'own'); const resolved = registry.getObject('sys_ledger_entry'); // Nothing title-eligible (currency/date/select/lookup + injected @@ -185,8 +185,8 @@ describe('SchemaRegistry', () => { }); it('does NOT add a `name` column to a FIELDLESS object', () => { - const obj = { name: 'sys_empty', fields: {} }; - registry.registerObject(obj as any, 'com.objectstack.system', undefined, 'own'); + const obj: ServiceObject = { name: 'sys_empty', fields: {} }; + registry.registerObject(obj, 'com.objectstack.system', undefined, 'own'); const resolved = registry.getObject('sys_empty'); expect((resolved as any)?.nameField).toBeUndefined(); @@ -199,11 +199,11 @@ describe('SchemaRegistry', () => { // ========================================== describe('Object Extension', () => { it('should merge extension fields into owner', () => { - const owner = { name: 'contact', fields: { email: { type: 'text' } } }; - registry.registerObject(owner as any, 'com.base', 'base', 'own'); + const owner: ServiceObject = { name: 'contact', fields: { email: { type: 'text' } } }; + registry.registerObject(owner, 'com.base', 'base', 'own'); - const ext = { name: 'contact', fields: { phone: { type: 'text' } } }; - registry.registerObject(ext as any, 'com.crm', undefined, 'extend', 200); + const ext: ServiceObject = { name: 'contact', fields: { phone: { type: 'text' } } }; + registry.registerObject(ext, 'com.crm', undefined, 'extend', 200); const resolved = registry.getObject('contact'); expect(resolved?.fields).toHaveProperty('email'); @@ -211,33 +211,33 @@ describe('SchemaRegistry', () => { }); it('should apply priority order (higher wins)', () => { - const owner = { name: 'task', label: 'Task', fields: {} }; - registry.registerObject(owner as any, 'com.base', 'base', 'own', 100); + const owner: ServiceObject = { name: 'task', label: 'Task', fields: {} }; + registry.registerObject(owner, 'com.base', 'base', 'own', 100); - const ext1 = { name: 'task', label: 'Extended Task', fields: {} }; - registry.registerObject(ext1 as any, 'com.ext1', undefined, 'extend', 150); + const ext1: ServiceObject = { name: 'task', label: 'Extended Task', fields: {} }; + registry.registerObject(ext1, 'com.ext1', undefined, 'extend', 150); - const ext2 = { name: 'task', label: 'Final Task', fields: {} }; - registry.registerObject(ext2 as any, 'com.ext2', undefined, 'extend', 250); + const ext2: ServiceObject = { name: 'task', label: 'Final Task', fields: {} }; + registry.registerObject(ext2, 'com.ext2', undefined, 'extend', 250); const resolved = registry.getObject('task'); expect(resolved?.label).toBe('Final Task'); }); it('should merge validations additively', () => { - const owner = { name: 'order', fields: {}, validations: [{ type: 'required', field: 'id' }] }; - registry.registerObject(owner as any, 'com.base', 'base', 'own'); + const owner: ServiceObject = { name: 'order', fields: {}, validations: [{ type: 'required', name: 'id_required', message: 'id is required', field: 'id' }] }; + registry.registerObject(owner, 'com.base', 'base', 'own'); - const ext = { name: 'order', fields: {}, validations: [{ type: 'required', field: 'status' }] }; - registry.registerObject(ext as any, 'com.ext', undefined, 'extend'); + const ext: ServiceObject = { name: 'order', fields: {}, validations: [{ type: 'required', name: 'status_required', message: 'status is required', field: 'status' }] }; + registry.registerObject(ext, 'com.ext', undefined, 'extend'); const resolved = registry.getObject('order'); expect(resolved?.validations).toHaveLength(2); }); it('should fail extension without owner', () => { - const ext = { name: 'phantom', fields: {} }; - registry.registerObject(ext as any, 'com.ext', undefined, 'extend'); + const ext: ServiceObject = { name: 'phantom', fields: {} }; + registry.registerObject(ext, 'com.ext', undefined, 'extend'); const resolved = registry.getObject('phantom'); expect(resolved).toBeUndefined(); @@ -249,22 +249,22 @@ describe('SchemaRegistry', () => { // ========================================== describe('Object Resolution', () => { it('should resolve by canonical name', () => { - const obj = { name: 'deal', fields: {} }; - registry.registerObject(obj as any, 'com.crm', 'crm', 'own'); + const obj: ServiceObject = { name: 'deal', fields: {} }; + registry.registerObject(obj, 'com.crm', 'crm', 'own'); expect(registry.resolveObject('deal')).toBeDefined(); }); it('should resolve system objects by their sys_ prefixed name', () => { - const obj = { name: 'sys_user', fields: {} }; - registry.registerObject(obj as any, 'com.objectstack.system', 'sys', 'own'); + const obj: ServiceObject = { name: 'sys_user', fields: {} }; + registry.registerObject(obj, 'com.objectstack.system', 'sys', 'own'); expect(registry.getObject('sys_user')).toBeDefined(); }); it('should cache merged objects', () => { - const obj = { name: 'cached', fields: {} }; - registry.registerObject(obj as any, 'com.test', 'test', 'own'); + const obj: ServiceObject = { name: 'cached', fields: {} }; + registry.registerObject(obj, 'com.test', 'test', 'own'); const first = registry.resolveObject('cached'); const second = registry.resolveObject('cached'); @@ -272,13 +272,13 @@ describe('SchemaRegistry', () => { }); it('should invalidate cache on re-registration', () => { - const obj = { name: 'evolve', fields: { v1: { type: 'text' } } }; - registry.registerObject(obj as any, 'com.test', 'test', 'own'); + const obj: ServiceObject = { name: 'evolve', fields: { v1: { type: 'text' } } }; + registry.registerObject(obj, 'com.test', 'test', 'own'); const first = registry.resolveObject('evolve'); - const obj2 = { name: 'evolve', fields: { v2: { type: 'text' } } }; - registry.registerObject(obj2 as any, 'com.test', 'test', 'own'); + const obj2: ServiceObject = { name: 'evolve', fields: { v2: { type: 'text' } } }; + registry.registerObject(obj2, 'com.test', 'test', 'own'); const second = registry.resolveObject('evolve'); expect(first).not.toBe(second); @@ -291,8 +291,8 @@ describe('SchemaRegistry', () => { // ========================================== describe('getAllObjects', () => { it('should return all merged objects', () => { - registry.registerObject({ name: 'a', fields: {} } as any, 'com.pkg1', 'pkg1', 'own'); - registry.registerObject({ name: 'b', fields: {} } as any, 'com.pkg2', 'pkg2', 'own'); + registry.registerObject({ name: 'a', fields: {} }, 'com.pkg1', 'pkg1', 'own'); + registry.registerObject({ name: 'b', fields: {} }, 'com.pkg2', 'pkg2', 'own'); const all = registry.getAllObjects(); expect(all).toHaveLength(2); @@ -300,8 +300,8 @@ describe('SchemaRegistry', () => { }); it('should filter by packageId', () => { - registry.registerObject({ name: 'a', fields: {} } as any, 'com.pkg1', 'pkg1', 'own'); - registry.registerObject({ name: 'b', fields: {} } as any, 'com.pkg2', 'pkg2', 'own'); + registry.registerObject({ name: 'a', fields: {} }, 'com.pkg1', 'pkg1', 'own'); + registry.registerObject({ name: 'b', fields: {} }, 'com.pkg2', 'pkg2', 'own'); const filtered = registry.getAllObjects('com.pkg1'); expect(filtered).toHaveLength(1); @@ -309,8 +309,8 @@ describe('SchemaRegistry', () => { }); it('should include objects where package is extender', () => { - registry.registerObject({ name: 'base_obj', fields: {} } as any, 'com.owner', 'base', 'own'); - registry.registerObject({ name: 'base_obj', fields: { ext: { type: 'text' } } } as any, 'com.extender', undefined, 'extend'); + registry.registerObject({ name: 'base_obj', fields: {} }, 'com.owner', 'base', 'own'); + registry.registerObject({ name: 'base_obj', fields: { ext: { type: 'text' } } }, 'com.extender', undefined, 'extend'); const filtered = registry.getAllObjects('com.extender'); expect(filtered).toHaveLength(1); @@ -322,7 +322,7 @@ describe('SchemaRegistry', () => { // ========================================== describe('Uninstall', () => { it('should remove owner contribution', () => { - registry.registerObject({ name: 'removable', fields: {} } as any, 'com.pkg', 'pkg', 'own'); + registry.registerObject({ name: 'removable', fields: {} }, 'com.pkg', 'pkg', 'own'); expect(registry.getObject('removable')).toBeDefined(); registry.unregisterObjectsByPackage('com.pkg'); @@ -330,8 +330,8 @@ describe('SchemaRegistry', () => { }); it('should remove extension contribution', () => { - registry.registerObject({ name: 'target', fields: { base: { type: 'text' } } } as any, 'com.owner', 'base', 'own'); - registry.registerObject({ name: 'target', fields: { ext: { type: 'text' } } } as any, 'com.ext', undefined, 'extend'); + registry.registerObject({ name: 'target', fields: { base: { type: 'text' } } }, 'com.owner', 'base', 'own'); + registry.registerObject({ name: 'target', fields: { ext: { type: 'text' } } }, 'com.ext', undefined, 'extend'); registry.unregisterObjectsByPackage('com.ext'); @@ -341,8 +341,8 @@ describe('SchemaRegistry', () => { }); it('should prevent uninstall of owner with active extenders', () => { - registry.registerObject({ name: 'important', fields: {} } as any, 'com.owner', 'base', 'own'); - registry.registerObject({ name: 'important', fields: {} } as any, 'com.ext', undefined, 'extend'); + registry.registerObject({ name: 'important', fields: {} }, 'com.owner', 'base', 'own'); + registry.registerObject({ name: 'important', fields: {} }, 'com.ext', undefined, 'extend'); expect(() => { registry.unregisterObjectsByPackage('com.owner'); @@ -350,8 +350,8 @@ describe('SchemaRegistry', () => { }); it('should allow force uninstall of owner with extenders', () => { - registry.registerObject({ name: 'forced', fields: {} } as any, 'com.owner', 'base', 'own'); - registry.registerObject({ name: 'forced', fields: {} } as any, 'com.ext', undefined, 'extend'); + registry.registerObject({ name: 'forced', fields: {} }, 'com.owner', 'base', 'own'); + registry.registerObject({ name: 'forced', fields: {} }, 'com.ext', undefined, 'extend'); expect(() => { registry.unregisterObjectsByPackage('com.owner', true); @@ -364,9 +364,9 @@ describe('SchemaRegistry', () => { // ========================================== describe('Contributors API', () => { it('should return all contributors for object', () => { - registry.registerObject({ name: 'multi', fields: {} } as any, 'com.owner', 'pkg', 'own', 100); - registry.registerObject({ name: 'multi', fields: {} } as any, 'com.ext1', undefined, 'extend', 200); - registry.registerObject({ name: 'multi', fields: {} } as any, 'com.ext2', undefined, 'extend', 300); + registry.registerObject({ name: 'multi', fields: {} }, 'com.owner', 'pkg', 'own', 100); + registry.registerObject({ name: 'multi', fields: {} }, 'com.ext1', undefined, 'extend', 200); + registry.registerObject({ name: 'multi', fields: {} }, 'com.ext2', undefined, 'extend', 300); const contribs = registry.getObjectContributors('multi'); expect(contribs).toHaveLength(3); @@ -376,7 +376,7 @@ describe('SchemaRegistry', () => { }); it('should return owner contributor', () => { - registry.registerObject({ name: 'owned', fields: {} } as any, 'com.owner', 'pkg', 'own'); + registry.registerObject({ name: 'owned', fields: {} }, 'com.owner', 'pkg', 'own'); const owner = registry.getObjectOwner('owned'); expect(owner).toBeDefined(); @@ -463,7 +463,7 @@ describe('SchemaRegistry', () => { // ========================================== describe('Reset', () => { it('should clear all state', () => { - registry.registerObject({ name: 'obj', fields: {} } as any, 'com.pkg', 'pkg', 'own'); + registry.registerObject({ name: 'obj', fields: {} }, 'com.pkg', 'pkg', 'own'); registry.registerItem('action', { name: 'act' }, 'name'); registry.reset(); @@ -850,7 +850,7 @@ describe('applySystemFields', () => { it('SchemaRegistry({ multiTenant: true }) auto-injects on registerObject', () => { const reg = new SchemaRegistry({ multiTenant: true }); - reg.registerObject({ name: 'lead', fields: { first_name: { type: 'text' } } } as any, 'crm', 'crm', 'own'); + reg.registerObject({ name: 'lead', fields: { first_name: { type: 'text' } } }, 'crm', 'crm', 'own'); const stored = (reg as any).objectContributors.get('lead')[0].definition; expect(stored.fields.organization_id).toBeDefined(); expect(stored.fields.organization_id.reference).toBe('sys_organization'); diff --git a/packages/objectql/src/save-meta-response-conformance.test.ts b/packages/objectql/src/save-meta-response-conformance.test.ts index 9fcc771197..e3d714fb6a 100644 --- a/packages/objectql/src/save-meta-response-conformance.test.ts +++ b/packages/objectql/src/save-meta-response-conformance.test.ts @@ -21,19 +21,23 @@ * non-empty. That is the direction it must never drift back to. */ import { describe, it, expect } from 'vitest'; +import type { ServiceObject } from '@objectstack/spec/data'; import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; import { SaveMetaItemResponseSchema } from '@objectstack/spec/api'; import { ObjectQL } from './engine.js'; -const sysMetadataObject = { +const sysMetadataObject: ServiceObject = { name: 'sys_metadata', label: 'System Metadata', fields: { - id: { name: 'id', label: 'ID', type: 'text' as const, primaryKey: true }, + // `primaryKey` was declared here behind an `as any` and is not a spec + // Field key at all (it exists only on external-catalog remote columns) — + // inert metadata nothing ever read. `id` is the primary key by convention. + id: { name: 'id', label: 'ID', type: 'text' as const }, type: { name: 'type', label: 'Type', type: 'text' as const, required: true }, name: { name: 'name', label: 'Name', type: 'text' as const, required: true }, organization_id: { name: 'organization_id', label: 'Org', type: 'text' as const }, - metadata: { name: 'metadata', label: 'Body', type: 'longtext' as const }, + metadata: { name: 'metadata', label: 'Body', type: 'textarea' as const }, checksum: { name: 'checksum', label: 'Checksum', type: 'text' as const, maxLength: 71 }, state: { name: 'state', label: 'State', type: 'text' as const }, version: { name: 'version', label: 'Version', type: 'number' as const }, @@ -112,7 +116,7 @@ async function makeProtocol() { const { driver } = makeMemoryDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(sysMetadataObject as any, 'test-package'); + engine.registry.registerObject(sysMetadataObject, 'test-package'); return new ObjectStackProtocolImplementation(engine); } diff --git a/packages/objectql/src/schema-sync-durability-log-level.test.ts b/packages/objectql/src/schema-sync-durability-log-level.test.ts index 0497f760ed..507b6edcb4 100644 --- a/packages/objectql/src/schema-sync-durability-log-level.test.ts +++ b/packages/objectql/src/schema-sync-durability-log-level.test.ts @@ -12,6 +12,7 @@ // message owes the reader: the CONSEQUENCE and the FIX. import { describe, it, expect } from 'vitest'; +import type { ServiceObject } from '@objectstack/spec/data'; import { ObjectQL } from './engine.js'; import { ObjectQLPlugin } from './plugin.js'; @@ -97,7 +98,7 @@ describe('ObjectQL.syncSchemas() — DDL failure is an error, not a silent swall const rec = recordingLogger(); const engine = new ObjectQL({ logger: rec.logger } as any); engine.registerDriver(failingDriver('default', 'boom') as any); - engine.registerObject({ name: 'invoice', label: 'Invoice', fields: { id: { type: 'text' } } } as any); + engine.registerObject({ name: 'invoice', label: 'Invoice', fields: { id: { type: 'text' } } }); await engine.syncSchemas(); @@ -120,7 +121,7 @@ describe('ObjectQL.syncSchemas() — DDL failure is an error, not a silent swall return []; }, } as any); - engine.registerObject({ name: 'invoice', label: 'Invoice', fields: { id: { type: 'text' } } } as any); + engine.registerObject({ name: 'invoice', label: 'Invoice', fields: { id: { type: 'text' } } }); await engine.syncSchemas(); @@ -130,12 +131,12 @@ describe('ObjectQL.syncSchemas() — DDL failure is an error, not a silent swall describe('ObjectQLPlugin.syncRegisteredSchemas() — per-object and summary levels (#4632)', () => { /** Drive the private sync pass directly: the level is the unit under test. */ - async function runSync(driver: unknown, objects: Array>) { + async function runSync(driver: unknown, objects: Array) { const rec = recordingLogger(); const plugin = new ObjectQLPlugin(); const engine = new ObjectQL({ logger: rec.logger } as any); engine.registerDriver(driver as any); - for (const obj of objects) engine.registerObject(obj as any); + for (const obj of objects) engine.registerObject(obj); (plugin as any).ql = engine; await (plugin as any).syncRegisteredSchemas({ logger: rec.logger }); return rec; diff --git a/packages/objectql/src/search-companion.test.ts b/packages/objectql/src/search-companion.test.ts index 2dea3d5dde..6d5f23d3f5 100644 --- a/packages/objectql/src/search-companion.test.ts +++ b/packages/objectql/src/search-companion.test.ts @@ -6,6 +6,7 @@ */ import { describe, it, expect } from 'vitest'; +import type { ServiceObject } from '@objectstack/spec/data'; import { SEARCH_COMPANION_FIELD, provisionSearchCompanion, @@ -17,7 +18,7 @@ import { import { expandSearchToFilter, resolveSearchFields } from './search-filter'; import { SchemaRegistry } from './registry'; -const contact = () => ({ +const contact = (): ServiceObject => ({ name: 'crm_contact', fields: { name: { type: 'text', label: 'Name' }, @@ -102,7 +103,7 @@ describe('provisionSearchCompanion', () => { describe('SchemaRegistry integration (compile-time seam)', () => { it('provisions the companion on registered objects when searchCompanion is on', () => { const registry = new SchemaRegistry({ multiTenant: false, searchCompanion: true }); - registry.registerObject(contact() as any, 'test-pkg', 'crm'); + registry.registerObject(contact(), 'test-pkg', 'crm'); const schema = registry.getObject('crm_contact')!; expect(schema.fields![SEARCH_COMPANION_FIELD]).toBeDefined(); expect((schema.fields![SEARCH_COMPANION_FIELD] as any).hidden).toBe(true); @@ -110,7 +111,7 @@ describe('SchemaRegistry integration (compile-time seam)', () => { it('does NOT provision when the flag is off (default) — pure additive', () => { const registry = new SchemaRegistry({ multiTenant: false }); - registry.registerObject(contact() as any, 'test-pkg', 'crm'); + registry.registerObject(contact(), 'test-pkg', 'crm'); expect(registry.getObject('crm_contact')!.fields![SEARCH_COMPANION_FIELD]).toBeUndefined(); }); diff --git a/packages/objectql/src/secret-fields.test.ts b/packages/objectql/src/secret-fields.test.ts index adc09dc38d..9bffff7b25 100644 --- a/packages/objectql/src/secret-fields.test.ts +++ b/packages/objectql/src/secret-fields.test.ts @@ -133,8 +133,8 @@ async function buildEngine(withCrypto: boolean) { const { driver, stores } = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(sysSecretObject as any); - engine.registry.registerObject(dsObject as any); + engine.registry.registerObject(sysSecretObject); + engine.registry.registerObject(dsObject); const crypto = makeFakeCrypto(); if (withCrypto) engine.setCryptoProvider(crypto.provider); return { engine, stores, crypto }; @@ -259,8 +259,8 @@ async function buildPasswordEngine() { const { driver, stores } = makeStubDriver(); engine.registerDriver(driver, true); await engine.init(); - engine.registry.registerObject(deviceObject as any); - engine.registry.registerObject(authUserObject as any); + engine.registry.registerObject(deviceObject); + engine.registry.registerObject(authUserObject); return { engine, stores }; } diff --git a/packages/objectql/src/summary-rollup.test.ts b/packages/objectql/src/summary-rollup.test.ts index 35fef6b073..3321f653f6 100644 --- a/packages/objectql/src/summary-rollup.test.ts +++ b/packages/objectql/src/summary-rollup.test.ts @@ -253,7 +253,7 @@ describe('roll-up summary index — a roll-up registered at RUNTIME still comput await engine.init(); // A write BEFORE the roll-up exists — this is what warmed the stale cache. - engine.registry.registerObject({ name: 'note', fields: { body: { type: 'text' } } } as any); + engine.registry.registerObject({ name: 'note', fields: { body: { type: 'text' } } }); await engine.insert('note', { body: 'warm the summary index' }); // Now publish the parent + child, the way a runtime publish does. @@ -420,7 +420,7 @@ describe('roll-up summary seeding on the PARENT insert (#5749)', () => { ghost_count: { type: 'summary', summaryOperations: { object: 'ghost', field: 'id', function: 'count' } }, }, } as any); - engine.registry.registerObject({ name: 'ghost', fields: { label: { type: 'text' } } } as any); + engine.registry.registerObject({ name: 'ghost', fields: { label: { type: 'text' } } }); const p = await engine.insert('orphan_parent', { name: 'x' }); expect(storeFor('orphan_parent').get(p.id).ghost_count ?? null).toBeNull();