Skip to content

Commit 4df747c

Browse files
os-zhuangclaude
andauthored
fix(objectql): drop the 135 as any that dodged registerObject's old parameter type (#5543) (#6786)
`registerObject(schema: ServiceObject, …)` used to name the POST-parse object shape, so an authored literal failed with TS2740 demanding ~9 zod `.default(...)` products the registry never materializes. ADR-0122 phase 2 (#6083) fixed the annotation upstream by making the bare `ServiceObject` alias mean the authored (`z.input`) shape; this removes the workaround it made obsolete — 135 `as any` casts across 46 files in this package, written only to get authored literals past the old type. A blanket `as any` suppresses every error at the call, not one, so the casts were hiding real mistakes too. Removing them surfaced four, now fixed: a `primaryKey: true` that is not a spec Field key at all, a field typed `'longtext'` where the spec spells it `'textarea'`, and two validation-rule fixtures missing the required `name`/`message`. Two casts in engine.ts were load-bearing for an unrelated reason — `registerApp(manifest: any)` widens its map branch to `unknown` — and are replaced by stating the contract once on the entries, which also makes the adjacent `(objDef as any).name = name` a checked assignment. `register-object-authored-shape.pin.ts` pins both halves so a re-flip cannot land quietly: the #5543 literal compiles with no cast, and an unknown key, a wrong field type, a missing `name`, and a bare-string field each still fail. It is a `.pin.ts` rather than a test because this package's tsconfig excludes its tests, which would make the directives phantom checks (#5286 PINS_CHECKED). A companion test registers the same literal for real and asserts the path still fills no defaults and still does not throw. Runtime behaviour is unchanged in both directions. Measured objectql TEST_DEBT: 333 before → 332 after (ledger records 355). Claude-Session: https://claude.ai/code/session_01N4y4NTZYxr2sRRzBb5AQxw Co-authored-by: Claude <noreply@anthropic.com>
1 parent 271cee1 commit 4df747c

49 files changed

Lines changed: 365 additions & 170 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
fix(objectql): drop the 135 `as any` that dodged `registerObject`'s old parameter type, and pin the authored shape (#5543)
6+
7+
**Runtime behaviour is unchanged in both directions.** Nothing in this change
8+
adds, removes, or reorders a single runtime step: `registerObject` still runs no
9+
`parse`, still fills no zod defaults, and still warns rather than throws on a
10+
sparse object. What changes is what the compiler is allowed to see at the call.
11+
12+
#5543 reported that `registerObject(schema: ServiceObject, …)` demanded the
13+
POST-parse object shape, so a perfectly legal authored literal —
14+
15+
```ts
16+
ql.registerObject({ name: 'task', label: 'Task', fields: { title: { type: 'text', label: 'Title' } } })
17+
```
18+
19+
— failed with TS2740 asking for ~9 keys (`searchable`, `required`, `multiple`,
20+
`unique`, …) that are zod `.default(...)` products, only exist after a parse the
21+
registry never runs, and that no author is supposed to write.
22+
23+
The annotation itself is already fixed upstream: ADR-0122 phase 2 (#6083,
24+
`@objectstack/spec` 17.0.0) made the bare alias `ServiceObject` mean the
25+
**authored** (`z.input`) shape, so the existing `ServiceObject` annotation on
26+
both `ObjectQL.registerObject` and `SchemaRegistry.registerObject` now names
27+
exactly what the runtime accepts. No annotation in this package needed to move.
28+
29+
What the flip left behind — and what this change removes — is the workaround it
30+
made obsolete: **135 `as any` casts** across 46 files in `packages/objectql`,
31+
every one of them written only to get an authored literal past the old
32+
parameter type. A blanket `as any` does not suppress one error, it suppresses
33+
all of them, so those casts were also hiding real mistakes. Deleting them
34+
surfaced four, now fixed:
35+
36+
- `save-meta-response-conformance.test.ts` declared `primaryKey: true` on a
37+
field. There is no such Field key in the spec (it exists only on
38+
external-catalog remote columns) — inert metadata nothing ever read.
39+
- the same fixture typed a field `'longtext'`, which is not a field type; the
40+
spec spells it `'textarea'`.
41+
- two validation-rule fixtures in `registry.test.ts` omitted the required
42+
`name` and `message`.
43+
44+
Two casts in `engine.ts` were load-bearing for a different reason — `registerApp`
45+
takes `manifest: any`, so its map branch widens object definitions to `unknown`.
46+
Those are replaced by stating the contract once on the entries
47+
(`as [string, ServiceObject][]`), which also lets the adjacent
48+
`(objDef as any).name = name` become a checked `objDef.name = name`.
49+
50+
New `register-object-authored-shape.pin.ts` pins both halves of the contract so
51+
a re-flip cannot land quietly: the #5543 literal compiles with no cast, and an
52+
unknown key, a wrong field type, a missing `name`, and a bare-string field each
53+
still fail. It is a `.pin.ts` and not a test because this package's `tsconfig`
54+
excludes its tests, which would make a `@ts-expect-error` there a phantom check.
55+
A companion test registers the same literal for real and asserts the register
56+
path still materializes no defaults and still does not throw.

packages/objectql/src/bulk-write-per-row-hooks.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,8 +1013,8 @@ async function boot(hooks: Hook[]): Promise<{ engine: ObjectQL; driver: any }> {
10131013
const driver = makeStubDriver();
10141014
engine.registerDriver(driver, true);
10151015
await engine.init();
1016-
engine.registry.registerObject(taskObject as any);
1017-
engine.registry.registerObject(otherObject as any);
1016+
engine.registry.registerObject(taskObject);
1017+
engine.registry.registerObject(otherObject);
10181018
if (hooks.length > 0) {
10191019
bindHooksToEngine(engine, hooks, { packageId: 'app:test', logger: silentLogger });
10201020
}

packages/objectql/src/engine-ambient-transaction.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ describe('engine ambient transaction (ADR-0034)', () => {
8282
seen = d.seen;
8383
engine.registerDriver(d.driver, true);
8484
await engine.init();
85-
engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } } as any);
85+
engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } });
8686
});
8787

8888
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,
243243
committedNames = d.committedNames;
244244
engine.registerDriver(d.driver, true);
245245
await engine.init();
246-
engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } } as any);
246+
engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } });
247247
});
248248

249249
/**
@@ -373,7 +373,7 @@ describe('ScopedContext.transaction joins the ambient transaction (ADR-0067 D2,
373373
const oneConn = new ObjectQL();
374374
oneConn.registerDriver(d.driver, true);
375375
await oneConn.init();
376-
oneConn.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } } as any);
376+
oneConn.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } });
377377
(oneConn as any).registerHook(
378378
'afterInsert',
379379
async (ctx: any) => {
@@ -434,7 +434,7 @@ describe('ScopedContext trio joins the ambient transaction (ADR-0067 D2, #6406)'
434434
committedNames = d.committedNames;
435435
engine.registerDriver(d.driver, true);
436436
await engine.init();
437-
engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } } as any, 'test');
437+
engine.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } }, 'test');
438438
});
439439

440440
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)'
574574
const oneConn = new ObjectQL();
575575
oneConn.registerDriver(d.driver, true);
576576
await oneConn.init();
577-
oneConn.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } } as any, 'test');
577+
oneConn.registry.registerObject({ name: 'thing', fields: { name: { type: 'text' } } }, 'test');
578578

579579
await expect(
580580
oneConn.transaction(async () => {

packages/objectql/src/engine-audit-anchor-write.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ describe('[#4447] created_at is engine-owned on an ordinary write', () => {
114114
const { driver } = makeStubDriver();
115115
engine.registerDriver(driver, true);
116116
await engine.init();
117-
engine.registry.registerObject(taskObject as any);
117+
engine.registry.registerObject(taskObject);
118118
});
119119

120120
/** An ordinary authenticated caller — NOT `isSystem`, no `preserveAudit`. */
@@ -260,7 +260,7 @@ describe('[#4447] a declared audit field cannot loosen the platform posture', ()
260260
const { driver } = makeStubDriver();
261261
engine.registerDriver(driver, true);
262262
await engine.init();
263-
engine.registry.registerObject(shadowed as any);
263+
engine.registry.registerObject(shadowed);
264264
});
265265

266266
it('the registry restores the engine-owned governance', () => {
@@ -351,7 +351,7 @@ describe('[#4513] the read-path normalizer answers what this engine enforces', (
351351
// [#4311] Typed call site: `packageId` is required, and the ledger over
352352
// this package's hidden test layer is a shrink-only ratchet — a new test
353353
// pays its own way rather than raising the frozen number.
354-
engine.registry.registerObject(shadowed as any, 'test');
354+
engine.registry.registerObject(shadowed, 'test');
355355
});
356356

357357
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', (
400400
created_at: { label: 'Created At', type: 'datetime' as const, readonly: false },
401401
},
402402
};
403-
engine.registry.registerObject(optedOut as any, 'test');
403+
engine.registry.registerObject(optedOut, 'test');
404404

405405
const enforced: any = engine.registry.getObject('audit_optout')?.fields.created_at;
406406
const reported: any = (applyAuditFieldGovernance(optedOut) as any).fields.created_at;

packages/objectql/src/engine-autonumber-runtime-owned.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ async function makeEngine(opts: { nativeAutonumber?: boolean } = {}) {
156156
const rig = makeStubDriver(opts);
157157
engine.registerDriver(rig.driver, true);
158158
await engine.init();
159-
engine.registry.registerObject(ACCOUNT as any);
159+
engine.registry.registerObject(ACCOUNT);
160160
const protocol = new ObjectStackProtocolImplementation(engine);
161161
return { engine, protocol, ...rig };
162162
}
@@ -581,7 +581,7 @@ describe('#5628 — a `readonly: true` autonumber keeps the #5503 exemption set'
581581
let rig: Awaited<ReturnType<typeof makeEngine>>;
582582
beforeEach(async () => {
583583
rig = await makeEngine();
584-
rig.engine.registry.registerObject(INVOICE as any, 'test');
584+
rig.engine.registry.registerObject(INVOICE, 'test');
585585
});
586586

587587
it('still strips an ordinary caller-supplied number and issues the sequence value', async () => {

packages/objectql/src/engine-bulk-contract.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ async function makeEngine(driverOpts?: { bulkCreate?: (object: string, rows: any
5454
const d = makeDriver(driverOpts);
5555
engine.registerDriver(d.driver, true);
5656
await engine.init();
57-
engine.registry.registerObject({ name: 'task', fields: { title: { type: 'text' } } } as any);
57+
engine.registry.registerObject({ name: 'task', fields: { title: { type: 'text' } } });
5858
return { engine, storeFor: d.storeFor };
5959
}
6060

packages/objectql/src/engine-cascade-delete.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ describe('cascadeDeleteRelations — required FK escalates set_null → restrict
9999
const { driver } = makeStubDriver();
100100
engine.registerDriver(driver, true);
101101
await engine.init();
102-
for (const o of [acct, oppRequired, noteOptional, taskCascade]) engine.registry.registerObject(o as any);
102+
for (const o of [acct, oppRequired, noteOptional, taskCascade]) engine.registry.registerObject(o);
103103
});
104104

105105
it('refuses to delete a parent with a REQUIRED-FK child (DELETE_RESTRICTED, 409) and leaves both rows', async () => {

packages/objectql/src/engine-dangling-reference-audit.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,9 @@ describe('[#4551] the engine reports the dangling rows its own `isSystem` exempt
144144
stores = stub.stores;
145145
engine.registerDriver(stub.driver, true);
146146
await engine.init();
147-
engine.registry.registerObject(permissionSet as any);
148-
engine.registry.registerObject(binding as any);
149-
engine.registry.registerObject(history as any);
147+
engine.registry.registerObject(permissionSet);
148+
engine.registry.registerObject(binding);
149+
engine.registry.registerObject(history);
150150
await engine.insert('aud_permission_set', { id: 'ps_real', name: 'Real' }, { context: { isSystem: true } } as any);
151151
});
152152

packages/objectql/src/engine-data-events.bench.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ async function makeEngine(withRealtime: boolean): Promise<ObjectQL> {
104104
const engine = new ObjectQL();
105105
engine.registerDriver(makeStubDriver(), true);
106106
await engine.init();
107-
engine.registry.registerObject(task as any, 'bench');
107+
engine.registry.registerObject(task, 'bench');
108108
if (withRealtime) engine.setRealtimeService(nullRealtime);
109109
// Silence the per-write logger so log formatting is not in the measurement.
110110
const logger = (engine as any).logger;

packages/objectql/src/engine-data-events.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ describe('#4626 — engine writes publish true DataEvents', () => {
121121
const { driver } = makeStubDriver();
122122
engine.registerDriver(driver, true);
123123
await engine.init();
124-
engine.registry.registerObject(task as any);
124+
engine.registry.registerObject(task);
125125
engine.setRealtimeService(realtime);
126126
warn = vi.spyOn((engine as any).logger, 'warn').mockImplementation(() => undefined);
127127
});
@@ -219,7 +219,7 @@ describe('#4626 — engine writes publish true DataEvents', () => {
219219
const { driver } = makeStubDriver();
220220
bare.registerDriver(driver, true);
221221
await bare.init();
222-
bare.registry.registerObject(task as any);
222+
bare.registry.registerObject(task);
223223

224224
await expect(bare.insert('task', { title: 'no realtime' })).resolves.toBeTruthy();
225225
expect(published).toHaveLength(0);
@@ -256,7 +256,7 @@ describe('#4639 — predicate writes publish aggregate BulkDataEvents', () => {
256256
const { driver } = makeStubDriver();
257257
engine.registerDriver(driver, true);
258258
await engine.init();
259-
engine.registry.registerObject(task as any);
259+
engine.registry.registerObject(task);
260260
engine.setRealtimeService(realtime);
261261
warn = vi.spyOn((engine as any).logger, 'warn').mockImplementation(() => undefined);
262262
});
@@ -358,7 +358,7 @@ describe('#4639 — predicate writes publish aggregate BulkDataEvents', () => {
358358
driver.updateMany = async () => ({ acknowledged: true } as any);
359359
offContract.registerDriver(driver, true);
360360
await offContract.init();
361-
offContract.registry.registerObject(task as any);
361+
offContract.registry.registerObject(task);
362362
offContract.setRealtimeService(realtime);
363363
const offWarn = vi.spyOn((offContract as any).logger, 'warn').mockImplementation(() => undefined);
364364
await offContract.insert('task', [{ title: 'a', status: 'open' }]);

0 commit comments

Comments
 (0)