Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .changeset/register-object-authored-shape-casts.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions packages/objectql/src/bulk-write-per-row-hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
Expand Down
10 changes: 5 additions & 5 deletions packages/objectql/src/engine-ambient-transaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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' } } });
});

/**
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 () => {
Expand Down
8 changes: 4 additions & 4 deletions packages/objectql/src/engine-audit-anchor-write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`. */
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions packages/objectql/src/engine-autonumber-runtime-owned.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand Down Expand Up @@ -581,7 +581,7 @@ describe('#5628 — a `readonly: true` autonumber keeps the #5503 exemption set'
let rig: Awaited<ReturnType<typeof makeEngine>>;
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 () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/objectql/src/engine-bulk-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}

Expand Down
2 changes: 1 addition & 1 deletion packages/objectql/src/engine-cascade-delete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/objectql/src/engine-dangling-reference-audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/objectql/src/engine-data-events.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ async function makeEngine(withRealtime: boolean): Promise<ObjectQL> {
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;
Expand Down
8 changes: 4 additions & 4 deletions packages/objectql/src/engine-data-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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' }]);
Expand Down
4 changes: 2 additions & 2 deletions packages/objectql/src/engine-default-value-tokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/objectql/src/engine-delete-dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}

Expand Down
2 changes: 1 addition & 1 deletion packages/objectql/src/engine-filter-alias.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand Down
2 changes: 1 addition & 1 deletion packages/objectql/src/engine-filter-array-lowering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand Down
4 changes: 2 additions & 2 deletions packages/objectql/src/engine-findone-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
2 changes: 1 addition & 1 deletion packages/objectql/src/engine-lifecycle-datasource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/objectql/src/engine-transaction-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}

Expand Down Expand Up @@ -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' });
Expand Down
6 changes: 3 additions & 3 deletions packages/objectql/src/engine-transaction-same-origin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions packages/objectql/src/engine-unknown-option.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand Down
Loading
Loading