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();