Skip to content

Commit bd0afc6

Browse files
committed
fix(engine): ObjectRepository declares the findOne/update shapes it already publishes
`IScopedObjectRepository.findOne` / `.update` declare `Record<string, any> | null` and `Record<string, any> | number | null`, and `IDataEngine` — the call each of these forwards to — declares the same. `ObjectRepository` sat between two narrow declarations and re-widened the value back to `Promise<any>` on the way out, which `implements IScopedObjectRepository` accepts (a wider return always satisfies a narrower one) while every call site reaching a repository through the CLASS kept reading `any`, `ObjectQL.createContext(…).object(n).findOne(…)` included. Census: one consumer, `engine-filter-alias.test.ts`, which read `.status` off a value that can be null. Repaired with the file's own `not.toBeNull()` / `!` idiom. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
1 parent ae19f5e commit bd0afc6

2 files changed

Lines changed: 38 additions & 4 deletions

File tree

packages/objectql/src/engine-filter-alias.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,15 @@ describe('filter → where folds on every engine method (#4346)', () => {
217217
const repo = ctx.object('task');
218218
const viaWhere = await repo.findOne({ where: { status: 'done' } });
219219
const viaFilter = await repo.findOne({ filter: { status: 'done' } });
220-
expect(viaFilter.status).toBe('done');
221-
expect(viaWhere.status).toBe('done');
220+
// [#16786] `repo.findOne` declares `Record<string, any> | null`, so the
221+
// null both spellings could return is asserted away rather than read
222+
// through — the same `expect(row).not.toBeNull()` / `row!` idiom this
223+
// file already uses above. Under the old `Promise<any>` this pair
224+
// agreed vacuously if BOTH lookups came back null.
225+
expect(viaFilter).not.toBeNull();
226+
expect(viaWhere).not.toBeNull();
227+
expect(viaFilter!.status).toBe('done');
228+
expect(viaWhere!.status).toBe('done');
222229
});
223230

224231
it('a cleanup hook calling repo.delete({filter, multi}) no longer empties the object', async () => {

packages/objectql/src/engine.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14707,7 +14707,23 @@ export class ObjectRepository implements IScopedObjectRepository {
1470714707
});
1470814708
}
1470914709

14710-
async findOne(query: any = {}): Promise<any> {
14710+
/**
14711+
* [#16786] Declared `Promise<Record<string, any> | null>`, not `Promise<any>`.
14712+
*
14713+
* `IScopedObjectRepository.findOne` has declared that shape since #16231's
14714+
* ruling A landed (PR #16783), and `IDataEngine.findOne` — the call this
14715+
* method forwards to, one line down — declares it too. This method sat
14716+
* between two narrow declarations and re-widened the value back to `any` on
14717+
* the way out, so `implements IScopedObjectRepository` stayed satisfied (a
14718+
* wider return always satisfies a narrower one) while every call site that
14719+
* reaches a repository through the CLASS rather than the interface kept
14720+
* reading `any` — `ObjectQL.createContext(…).object(n).findOne(…)` among
14721+
* them, which is exported.
14722+
*
14723+
* ⛔ Not a narrowing of the contract: the contract already said this. This
14724+
* is the implementation coming back to the declaration it published.
14725+
*/
14726+
async findOne(query: any = {}): Promise<Record<string, any> | null> {
1471114727
return this.engine.findOne(this.objectName, {
1471214728
...query,
1471314729
context: this.context,
@@ -14725,7 +14741,18 @@ export class ObjectRepository implements IScopedObjectRepository {
1472514741
return this.insert(data);
1472614742
}
1472714743

14728-
async update(data: any, options: any = {}): Promise<any> {
14744+
/**
14745+
* [#16786] Declared `Promise<Record<string, any> | number | null>`, the same
14746+
* re-widening as {@link findOne} and repaired the same way: the record for
14747+
* the single-record form, the affected-row count for the predicate form
14748+
* (`{ where, multi: true }`), `null` when the write matched nothing.
14749+
*
14750+
* ⛔ `updateById` is deliberately NOT touched here. Its `Promise<any>` is
14751+
* what `IScopedObjectRepository.updateById` itself declares, so the class
14752+
* matches its contract and there is no drift to repair on this side; that
14753+
* member is `packages/spec`'s to narrow and stays open on #16786.
14754+
*/
14755+
async update(data: any, options: any = {}): Promise<Record<string, any> | number | null> {
1472914756
return this.engine.update(this.objectName, data, {
1473014757
...options,
1473114758
context: this.context,

0 commit comments

Comments
 (0)