From 9529942f011a5bbd96a3d581291cb3e5d9b43954 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:05:23 +0800 Subject: [PATCH 1/3] feat(metadata-core): publish assertEngineFindOnePredicate, the #4419 read-side guard Mirrors ObjectQL.requireFindOnePredicate byte-for-byte and is proved against the REAL engine over a shared conformance case-set. --- .../src/engine-findone-predicate.ts | 287 ++++++++++++++++++ packages/metadata-core/src/index.ts | 5 + .../src/engine-findone-predicate.test.ts | 150 +++++++++ .../objectql/src/engine-findone-predicate.ts | 40 +++ packages/objectql/src/index.ts | 18 ++ 5 files changed, 500 insertions(+) create mode 100644 packages/metadata-core/src/engine-findone-predicate.ts create mode 100644 packages/objectql/src/engine-findone-predicate.test.ts create mode 100644 packages/objectql/src/engine-findone-predicate.ts diff --git a/packages/metadata-core/src/engine-findone-predicate.ts b/packages/metadata-core/src/engine-findone-predicate.ts new file mode 100644 index 0000000000..f76a6bea0c --- /dev/null +++ b/packages/metadata-core/src/engine-findone-predicate.ts @@ -0,0 +1,287 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The **one** answer to "does this `findOne` call select a particular record?" + * — the READ-side sibling of {@link ./engine-delete-dispatch.ts} and + * {@link ./engine-update-dispatch.ts}, extracted so that the engine and every + * test double standing in for it read the same predicate rather than two + * hand-written approximations of it (objectstack#11957, from objectstack#11767). + * + * ## The failure mode this exists for, measured rather than argued + * + * `ObjectQL.findOne` REFUSES a call that selects no particular record + * (objectstack#4419): `findOne` applies `limit: 1`, so a query with no `where` + * and no `orderBy` returns an ARBITRARY row — a real, plausible-looking record + * unrelated to what was asked for, which no caller's null-check can catch. + * + * Every in-memory double in this repo instead reads an absent filter as "match + * everything" and answers happily: `null` on an empty table, an arbitrary row + * otherwise. So a production call site that violates #4419 reads as *working* + * under every unit suite and only fails on a real engine. + * + * That is not hypothetical. `AuthManager.isBootstrapCreation` probed the + * bootstrap population with `adapter.findOne({ model: 'user', where: [] })` + * inside a `try/catch`. On a real engine that call throws; the + * `catch { return false; }` read the engine's REFUSAL as the data answer "users + * exist", so the declared first-run bypass never fired and the `invite_only` + * default refused the operator's own dev-admin seed. Two required gates went red + * across every shard — while a 641-line posture × creation-method matrix over + * the in-memory double stayed green, including a case literally named + * "bootstrap: the very first signup is admitted". A double more permissive than + * the engine converts a production defect into a green test. + * + * ## Why a shared module and not four lines inside each fake + * + * The same argument the delete twin's header makes, and the same two halves a + * hand-written copy drops — both of them measured on the #4419 rule itself: + * + * 1. **`where: []` is NOT a predicate.** It is the exact shape #11767 shipped. + * A copyist writing `if (!query?.where && !query?.orderBy) throw` accepts it, + * because an empty array is truthy. The engine lowers a `FilterArray` at + * every entry point and an empty one means "no filter" — this is stated in + * `requireFindOnePredicate`'s own header, which records that `where: []` + * once walked past the guard and returned an arbitrary row: "the #4419 + * defect surviving inside #4419's own guard". + * 2. **`where: {}` is NOT a predicate either, and `filter: {…}` IS one.** The + * engine reads "selects nothing" as absent / `null` / `{}` — the three + * shapes that mean match-every-row — and folds the `filter` alias into + * `where` on every entry point before the guard runs. A copy that tests + * `query.where != null` accepts `{}` (looser) and refuses `{ filter }` + * (stricter). Looser hides bugs, stricter invents them; importing the + * decision is the only spelling that does neither. + * + * ## Why this module lives in `@objectstack/metadata-core` + * + * Byte-for-byte the twins' reason (objectstack#5619), which is why it is not + * re-argued here: `@objectstack/objectql` **depends on** + * `@objectstack/metadata-protocol`, so that package's fake engines cannot + * import from objectql at all — turbo rejects the resulting task graph + * outright. Sinking the predicate into a package both sides already depend on + * is the only route that pins them. This package's dependencies are + * `{ @objectstack/spec, zod }`, so nothing here adds an edge. + * `@objectstack/objectql` re-exports every symbol below from + * `src/engine-findone-predicate.ts`, exactly as it does for the twins. + * + * ## The contract, normatively + * + * `findOne(object, query)` is SELECTIVE when any one of these holds, read on the + * CALLER's own spelling (a double is handed the query before the engine folds, + * lowers or expands anything): + * + * - **`where`** — after folding the `filter` alias and lowering a `FilterArray`: + * a non-empty plain object, a non-empty array, or a non-object value + * (the engine's own `typeof where !== 'object'` arm, kept as defence in depth + * for a caller that reaches the engine without lowering). + * - **`orderBy`** — a non-empty ARRAY. "The newest", "the highest priority" is + * a legitimate way to be specific and every driver honours it on this path. + * Arrayness is load-bearing: the engine tests `Array.isArray`, so a + * record-form `orderBy` is not selective there and is not selective here. + * - **`search`** — a non-empty search, because the engine expands it into + * `where` BEFORE the guard and #4419's own refusal names it as one of the + * three ways to select ("Pass 'where' (or a 'search' that resolves to one)"). + * + * Otherwise the call is **`reject`**, and the message is byte-identical to the + * engine's, object name included — see {@link engineFindOnePredicateRefusalMessage}. + * + * ## ⛔ The two residuals, stated because a closed-sounding contract is worse + * + * Both are one-sided by construction — a double is not handed the object's + * schema, so two of the engine's inputs are structurally unavailable to it. + * Neither is a place to "improve" the predicate; changing either without + * changing `engine.ts` re-creates the drift this module removes. + * + * 1. **`search` over an object with NO searchable field.** The engine's + * `expandSearchOnAst` produces no filter there, so `where` stays absent and + * the guard refuses. This predicate accepts it, because resolving searchable + * fields needs the registry. The alternative — refusing every `search` — is + * stricter than the producer on the mainline shape, which invents failures + * a running server does not have. Accepting is the narrower error. + * 2. **A non-empty `where` ARRAY that is not a well-formed filter AST.** The + * engine refuses it too, but with `lowerWhereFilterArray`'s own louder + * message rather than this one. Both refuse; only the wording differs, and + * reproducing that lowering would mean re-implementing `parseFilterAST` + * here — a second copy of a different contract, which is the defect this + * module exists to remove. + * + * A third, deliberate non-claim: the wire-only spellings (`sort`, `select`, + * `skip`, `populate`) are rejected by the engine's own entry-point guard with a + * different message, so `sort` is NOT read as `orderBy` here. This predicate + * answers the #4419 question and only that one, exactly as + * `assertEngineDeleteDispatch` answers the delete dispatch and only that one. + * + * @see ObjectQL.findOne / `requireFindOnePredicate` in `packages/objectql/src/engine.ts` — the producer. + * @see packages/objectql/src/engine-findone-predicate.ts — the re-export shim keeping objectql's public API. + * @see packages/objectql/src/engine-findone-predicate.test.ts — the case-set driven against the REAL engine. + * @see engine-delete-dispatch.ts / engine-update-dispatch.ts — the write-side twins. + */ + +/** + * The message `findOne` throws when a call selects no particular record — + * byte-identical to `ObjectQL.requireFindOnePredicate`'s, object name included. + * + * A function rather than a constant because the engine's message quotes the + * object twice, and a fake that reproduced only the prefix would let a test + * assert on wording the producer never emits. + */ +export function engineFindOnePredicateRefusalMessage(object: string): string { + return ( + `findOne('${object}') selects no particular record: 'where' is absent or empty ` + + `and the query carries no 'orderBy'. findOne applies limit: 1, so this would return an ` + + `ARBITRARY row — a real, plausible-looking record unrelated to what was asked for, which ` + + `no caller's null-check can catch (#4419). Pass 'where' (or a 'search' that resolves to ` + + `one) to select the record; pass 'orderBy' if you mean "the first record in THIS order"; ` + + `or call find('${object}', { limit: 1 }) if any row will genuinely do.` + ); +} + +/** What `ObjectQLEngine.findOne` will do with a given query bag. */ +export type EngineFindOnePredicate = + /** The call names a particular record; `by` says which of the three ways. */ + | { readonly kind: 'selective'; readonly by: 'where' | 'orderBy' | 'search' } + /** It does not — the engine throws {@link engineFindOnePredicateRefusalMessage}. */ + | { readonly kind: 'reject'; readonly message: string }; + +/** The subset of `EngineQueryOptions` the #4419 decision actually reads. */ +export interface EngineFindOneQueryInput { + readonly where?: unknown; + /** The alias the engine folds into `where` on every entry point (#4346). */ + readonly filter?: unknown; + readonly orderBy?: unknown; + readonly search?: unknown; + readonly [k: string]: unknown; +} + +/** + * The caller's effective `where`, with the `filter` alias folded exactly as + * `foldEngineOptionAliases` folds it: a slot is PRESENT when its value is + * `!= null`, and the canonical spelling wins. + * + * An explicit `null` is a withdrawal rather than a value — the same reading + * `foldQueryAliasSlots` gives it — so `{ where: null, filter: { a: 1 } }` folds + * to the filter, not to nothing. + */ +function foldedWhere(query?: EngineFindOneQueryInput | null): unknown { + if (!query) return undefined; + if (query.where != null) return query.where; + if (query.filter != null) return query.filter; + return undefined; +} + +/** + * Is the caller's `where` a predicate, read the way the engine reads it after + * `lowerWhereFilterArray`? + * + * The three arms are the engine's own, in its order: a `null`/absent `where` is + * nothing; a non-object is the driver's to interpret and counts (defence in + * depth — every entry point lowers before the guard today); an array counts + * only when NON-EMPTY, because `[]` is "no filter" and is deleted by the + * lowering. `{}` is the match-every-row shape and does not count. + */ +function whereIsPredicate(where: unknown): boolean { + if (where == null) return false; + if (typeof where !== 'object') return true; + if (Array.isArray(where)) return where.length > 0; + return Object.keys(where as Record).length > 0; +} + +/** Does `search` carry anything the engine could expand into a filter? */ +function searchIsPredicate(search: unknown): boolean { + if (search == null) return false; + if (typeof search === 'string') return search.trim().length > 0; + if (typeof search !== 'object') return true; + return Object.keys(search as Record).length > 0; +} + +/** + * Decide what `ObjectQLEngine.findOne` does with `query`, without doing it. + * + * Pure and side-effect free, so a test double can classify a call and then read + * rows however its fixture stores them — while being bound to the real engine's + * refusal surface for free. + */ +export function resolveEngineFindOnePredicate( + object: string, + query?: EngineFindOneQueryInput | null, +): EngineFindOnePredicate { + if (whereIsPredicate(foldedWhere(query))) return { kind: 'selective', by: 'where' }; + // Search is expanded INTO `where` before the guard runs, so it is checked + // ahead of `orderBy` for the same reason the engine checks the expanded + // `where` first: a resolving search IS a `where` by the time #4419 looks. + if (searchIsPredicate(query?.search)) return { kind: 'selective', by: 'search' }; + const orderBy = query?.orderBy; + if (Array.isArray(orderBy) && orderBy.length > 0) return { kind: 'selective', by: 'orderBy' }; + return { kind: 'reject', message: engineFindOnePredicateRefusalMessage(object) }; +} + +/** + * Throw exactly what `ObjectQLEngine.findOne` throws when a call selects no + * particular record; return the resolved verdict otherwise. + * + * This is the line a fake engine's `findOne` opens with. One call pins the fake + * to the producer's refusal surface, and — unlike a mirrored `if` — it cannot + * drift when the producer's rule changes. + * + * ```ts + * async findOne(object: string, query?: any) { + * assertEngineFindOnePredicate(object, query); // refuses what a real server refuses + * … + * } + * ``` + */ +export function assertEngineFindOnePredicate( + object: string, + query?: EngineFindOneQueryInput | null, +): Exclude { + const verdict = resolveEngineFindOnePredicate(object, query); + if (verdict.kind === 'reject') throw new Error(verdict.message); + return verdict; +} + +/** + * The shared conformance case-set for the #4419 predicate — the same role + * `ENGINE_DELETE_DISPATCH_CASES` plays for the delete dispatch. + * + * Every case names a query shape and the verdict the **real engine** gives it, + * and `packages/objectql/src/engine-findone-predicate.test.ts` drives + * `ObjectQL.findOne` over every row to prove the two agree. A double proved + * against these is proved against the producer — including the shapes that look + * like a predicate and are not. + */ +export interface EngineFindOnePredicateCase { + /** What the shape is, in the words a failure message should use. */ + readonly what: string; + /** The query bag handed to `findOne(object, query)`. */ + readonly query: EngineFindOneQueryInput | undefined; + /** The verdict the engine gives it. */ + readonly expect: EngineFindOnePredicate['kind']; +} + +export const ENGINE_FINDONE_PREDICATE_CASES: readonly EngineFindOnePredicateCase[] = [ + // ── Selective. Each is a shape a running server answers a row for. + { what: 'a scalar equality predicate', query: { where: { id: 'rec_1' } }, expect: 'selective' }, + { what: 'a non-id predicate', query: { where: { status: 'open' } }, expect: 'selective' }, + { what: 'an operator predicate', query: { where: { id: { $in: ['a', 'b'] } } }, expect: 'selective' }, + // The alias the engine folds on every entry point (#4346). Before the fold, + // `findOne({ filter })` matched the first row of the WHOLE table. + { what: "the 'filter' alias alone — folded into 'where' before the guard (#4346)", query: { filter: { status: 'open' } }, expect: 'selective' }, + { what: "an explicit null 'where' beside a real 'filter' — null is a withdrawal, not a value", query: { where: null, filter: { status: 'open' } }, expect: 'selective' }, + // A FilterArray that is a well-formed AST lowers to a condition. + { what: 'a non-empty FilterArray — lowered to a condition before the guard', query: { where: ['status', '=', 'open'] }, expect: 'selective' }, + { what: 'orderBy alone — "the first record in THIS order" is a real answer', query: { orderBy: [{ field: 'title', order: 'desc' }] }, expect: 'selective' }, + { what: 'orderBy beside an empty where', query: { where: {}, orderBy: [{ field: 'title', order: 'desc' }] }, expect: 'selective' }, + { what: 'a search that resolves to a filter', query: { search: 'widget' }, expect: 'selective' }, + // ── The refusals. Every one of these is a shape a double answers happily and + // a running server throws on — which is the whole of #11957. + { what: 'no query at all', query: undefined, expect: 'reject' }, + { what: 'an empty query bag', query: {}, expect: 'reject' }, + { what: "an empty 'where' object — the match-every-row shape (#3896's reading)", query: { where: {} }, expect: 'reject' }, + { what: "an explicitly null 'where'", query: { where: null }, expect: 'reject' }, + // THE #11767 SHAPE. An empty FilterArray is truthy, so every hand-written + // `if (!query?.where)` copy accepts it; the engine's lowering deletes the key + // and the guard refuses. This one row is what the card was filed for. + { what: "an empty FilterArray 'where: []' — truthy, and NOT a predicate (#11767)", query: { where: [] }, expect: 'reject' }, + { what: "a null 'filter' alias — a withdrawal, so nothing folds", query: { filter: null }, expect: 'reject' }, + { what: 'an empty orderBy array', query: { orderBy: [] }, expect: 'reject' }, + { what: 'a projection and a limit but nothing selective', query: { fields: ['id', 'name'], limit: 1 }, expect: 'reject' }, + { what: 'an empty search string', query: { search: ' ' }, expect: 'reject' }, +]; diff --git a/packages/metadata-core/src/index.ts b/packages/metadata-core/src/index.ts index 2874f32fcb..1cafea127e 100644 --- a/packages/metadata-core/src/index.ts +++ b/packages/metadata-core/src/index.ts @@ -29,6 +29,11 @@ export * from './engine-update-dispatch.js'; // [#11009] The refusal both write dispatches share: a by-id call whose // `where` carries keys the by-id path would silently discard. export * from './engine-dispatch-unhonoured-predicate.js'; +// [#11957] The READ-side sibling: `ObjectQL.findOne` REFUSES a call that selects +// no particular record (#4419), and every in-memory double answered it happily — +// which is how #11767 shipped a bootstrap bypass that was permanently inert on +// real deployments under a 641-line all-green matrix. +export * from './engine-findone-predicate.js'; // [#4513] The audit-family GOVERNANCE table (#4447) and its normalizer, sunk // here for the same reason and by the same criterion as the two dispatch diff --git a/packages/objectql/src/engine-findone-predicate.test.ts b/packages/objectql/src/engine-findone-predicate.test.ts new file mode 100644 index 0000000000..a7731a4ad0 --- /dev/null +++ b/packages/objectql/src/engine-findone-predicate.test.ts @@ -0,0 +1,150 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// objectstack#11957 — the shared `findOne` predicate must be the REAL engine's +// answer, not a second opinion that happens to agree today. +// +// A shared predicate that drifted from `ObjectQL.findOne` would be worse than no +// predicate at all: every fake engine pinned to it would be confidently, +// uniformly wrong, and the gate over them would report success (route-ownership +// rule 3 — prefer failing to falling back). So this file does not test the +// predicate against a table of expectations written next to it. It drives the +// **real engine** with a recording driver over `ENGINE_FINDONE_PREDICATE_CASES` +// and asserts the engine's observed behaviour equals the predicate's verdict, +// case by case — the same construction `engine-delete-dispatch.test.ts` uses for +// the write side, and for the same reason. +// +// If someone changes `requireFindOnePredicate` in `engine.ts` without changing +// `engine-findone-predicate.ts`, this goes red here — the one place where both +// halves are in the room together. + +import { describe, it, expect } from 'vitest'; +import { ObjectQL } from './engine.js'; +import { + ENGINE_FINDONE_PREDICATE_CASES, + engineFindOnePredicateRefusalMessage, + resolveEngineFindOnePredicate, + assertEngineFindOnePredicate, +} from './engine-findone-predicate.js'; + +const OBJECT = 'task'; + +/** Records whether the engine ever reached the driver's read path. */ +function makeRecordingDriver() { + const calls: Array<{ fn: 'find' | 'findOne'; ast: unknown }> = []; + const driver: any = { + name: 'recording', + version: '0.0.0', + supports: {}, + async connect() {}, async disconnect() {}, async checkHealth() { return true; }, async execute() { return null; }, + async find(_o: string, ast: unknown) { calls.push({ fn: 'find', ast }); return []; }, + async findOne(_o: string, ast: unknown) { calls.push({ fn: 'findOne', ast }); return null; }, + async create(_o: string, data: Record) { return { id: 'r1', ...data }; }, + async update(_o: string, id: string, data: Record) { return { id, ...data }; }, + async delete() { return true; }, + async deleteMany() { return 0; }, + async count() { return 0; }, + async bulkCreate() { return []; }, async bulkUpdate() { return []; }, async bulkDelete() {}, + async beginTransaction() { return { __trx: true, commit: async () => {}, rollback: async () => {} }; }, + async commit() {}, async rollback() {}, + }; + return { driver, calls }; +} + +async function makeEngine() { + const engine = new ObjectQL(); + const { driver, calls } = makeRecordingDriver(); + engine.registerDriver(driver, true); + await engine.init(); + // `title`/`status` are declared because the filter doors that run BEFORE the + // #4419 guard (`assertFilterIsMaterializable`, `assertOrderByIsMaterializable`) + // judge against the real field map — a case naming an undeclared column would + // die at a different door and tell us nothing about this one. + // `searchableFields` is explicit so the `search` case resolves deterministically + // rather than through the auto-default. + engine.registry.registerObject({ + name: OBJECT, + fields: { title: { type: 'text' }, status: { type: 'text' } }, + searchableFields: ['title', 'status'], + } as any); + return { engine, calls }; +} + +/** What the real engine actually did with this query bag. */ +async function observeEngine(query: unknown): Promise<'selective' | 'reject'> { + const { engine, calls } = await makeEngine(); + try { + await engine.findOne(OBJECT, query as any); + } catch (e) { + const message = (e as Error).message; + // Only the #4419 refusal counts as this predicate's verdict. Anything else + // — a malformed filter array, an unmaterializable column, an unknown option + // — is a DIFFERENT door and must not be laundered into a passing case. + if (message === engineFindOnePredicateRefusalMessage(OBJECT)) return 'reject'; + throw e; + } + if (calls.length !== 1) { + throw new Error(`expected exactly one driver read, saw ${JSON.stringify(calls)}`); + } + return 'selective'; +} + +describe('engine findOne predicate — the shared predicate IS the engine (#11957)', () => { + it('has cases on both sides of the guard (an empty or one-sided set proves nothing)', () => { + const kinds = new Set(ENGINE_FINDONE_PREDICATE_CASES.map((c) => c.expect)); + expect(kinds).toEqual(new Set(['selective', 'reject'])); + expect(ENGINE_FINDONE_PREDICATE_CASES.filter((c) => c.expect === 'reject').length) + .toBeGreaterThan(3); + expect(ENGINE_FINDONE_PREDICATE_CASES.filter((c) => c.expect === 'selective').length) + .toBeGreaterThan(3); + }); + + for (const c of ENGINE_FINDONE_PREDICATE_CASES) { + it(`real engine agrees with the predicate: ${c.what} → ${c.expect}`, async () => { + expect(resolveEngineFindOnePredicate(OBJECT, c.query).kind, 'predicate').toBe(c.expect); + expect(await observeEngine(c.query), 'real ObjectQL.findOne').toBe(c.expect); + }); + } + + it('refuses with the exact message a fake must reproduce, object name included', () => { + expect(() => assertEngineFindOnePredicate(OBJECT, { where: [] })) + .toThrow(engineFindOnePredicateRefusalMessage(OBJECT)); + // The message quotes the object twice — a fake reproducing only the prefix + // would let a test assert on wording the producer never emits. + const message = engineFindOnePredicateRefusalMessage('sys_user'); + expect(message).toContain("findOne('sys_user')"); + expect(message).toContain("find('sys_user', { limit: 1 })"); + }); + + it('returns the verdict (never `reject`) when the call selects a record', () => { + expect(assertEngineFindOnePredicate(OBJECT, { where: { id: 'a' } })) + .toEqual({ kind: 'selective', by: 'where' }); + expect(assertEngineFindOnePredicate(OBJECT, { filter: { status: 'open' } })) + .toEqual({ kind: 'selective', by: 'where' }); + expect(assertEngineFindOnePredicate(OBJECT, { orderBy: [{ field: 'title', order: 'desc' }] })) + .toEqual({ kind: 'selective', by: 'orderBy' }); + expect(assertEngineFindOnePredicate(OBJECT, { search: 'widget' })) + .toEqual({ kind: 'selective', by: 'search' }); + }); + + // The three shapes a hand-mirrored `if (!query?.where && !query?.orderBy)` + // gets wrong, spelled out because they are the whole argument for importing + // the producer's decision instead of copying it. + it('reads `where: []` as NO predicate — the #11767 shape a truthiness copy accepts', () => { + expect(resolveEngineFindOnePredicate(OBJECT, { where: [] }).kind).toBe('reject'); + // …while a non-empty filter array lowers to a real condition. + expect(resolveEngineFindOnePredicate(OBJECT, { where: ['status', '=', 'open'] }).kind) + .toBe('selective'); + }); + + it('reads `where: {}` as NO predicate and the `filter` alias as one', () => { + expect(resolveEngineFindOnePredicate(OBJECT, { where: {} }).kind).toBe('reject'); + expect(resolveEngineFindOnePredicate(OBJECT, { filter: { status: 'open' } }).kind) + .toBe('selective'); + }); + + it('requires orderBy to be a NON-EMPTY ARRAY, as `Array.isArray` does in the engine', () => { + expect(resolveEngineFindOnePredicate(OBJECT, { orderBy: [] }).kind).toBe('reject'); + expect(resolveEngineFindOnePredicate(OBJECT, { orderBy: { title: 'desc' } as any }).kind) + .toBe('reject'); + }); +}); diff --git a/packages/objectql/src/engine-findone-predicate.ts b/packages/objectql/src/engine-findone-predicate.ts new file mode 100644 index 0000000000..7a3b57d9ba --- /dev/null +++ b/packages/objectql/src/engine-findone-predicate.ts @@ -0,0 +1,40 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The `findOne` predicate's objectql-side path — a **re-export of its home** in + * `@objectstack/metadata-core`, the same shape the two write-side twins use + * (`engine-delete-dispatch.ts`, `engine-update-dispatch.ts`). + * + * ## Why the predicate lives one package down, and this file exists at all + * + * `@objectstack/objectql` **depends on** `@objectstack/metadata-protocol`, so + * the fake engines there cannot import from objectql without closing a cycle + * turbo refuses outright. Sinking the predicate into `@objectstack/metadata-core` + * — a package both sides already depend on, and which depends on neither — is + * the only route that pins those doubles without inventing a dependency edge. + * The full reasoning, with the measured cycle, is in the module header at the + * implementation (objectstack#5619 established it for the delete twin). + * + * This file exists so the predicate has objectql's public spelling too: the + * engine's own pinned test doubles and the real-engine conformance test import + * `./engine-findone-predicate.js`, and `index.ts` re-exports the public API + * from here. + * + * @see @objectstack/metadata-core `src/engine-findone-predicate.ts` — the implementation. + * @see engine-findone-predicate.test.ts — the case-set driven against the REAL engine, + * which stays in this package because it needs `ObjectQL`. + * @see ObjectQL.findOne → `requireFindOnePredicate` in `engine.ts` — the producer (#4419). + */ + +export { + engineFindOnePredicateRefusalMessage, + resolveEngineFindOnePredicate, + assertEngineFindOnePredicate, + ENGINE_FINDONE_PREDICATE_CASES, +} from '@objectstack/metadata-core'; + +export type { + EngineFindOnePredicate, + EngineFindOneQueryInput, + EngineFindOnePredicateCase, +} from '@objectstack/metadata-core'; diff --git a/packages/objectql/src/index.ts b/packages/objectql/src/index.ts index 7e8e0569db..eb816ef46b 100644 --- a/packages/objectql/src/index.ts +++ b/packages/objectql/src/index.ts @@ -134,6 +134,24 @@ export type { EngineUpdateDispatchCase, } from './engine-update-dispatch.js'; +// [#11957] The READ-side sibling of the two dispatches above, on exactly the +// same terms. `ObjectQL.findOne` applies `limit: 1`, so a query with no `where` +// and no `orderBy` would return an ARBITRARY row — `requireFindOnePredicate` +// REFUSES it (#4419) and every in-memory double answered it happily, which is +// how #11767 shipped a first-run bypass that was permanently inert on real +// deployments while a 641-line unit matrix stayed green. +export { + resolveEngineFindOnePredicate, + assertEngineFindOnePredicate, + engineFindOnePredicateRefusalMessage, + ENGINE_FINDONE_PREDICATE_CASES, +} from './engine-findone-predicate.js'; +export type { + EngineFindOnePredicate, + EngineFindOneQueryInput, + EngineFindOnePredicateCase, +} from './engine-findone-predicate.js'; + // Export in-memory aggregation fallback (used by engine.aggregate when the // driver lacks native groupBy/aggregations support; also useful for tests). export { applyInMemoryAggregation, bucketDateValue } from './in-memory-aggregation.js'; From cc3697f8e82e096dc29301796e1bd260e643e272 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:32:19 +0800 Subject: [PATCH 2/3] test(objectql): pin all 25 findOne doubles in the package to the shared #4419 predicate Adds the changeset for the new public API and types the conformance test's deliberately off-contract query bags as `as unknown as EngineQueryOptions`. --- .changeset/engine-findone-predicate-guard.md | 40 +++++++++++++++++++ .../src/engine-findone-predicate.test.ts | 11 ++++- .../src/layered-overlay-integration.test.ts | 7 ++++ ...ject-primary-designation-roundtrip.test.ts | 7 ++++ ...-object-search-companion-roundtrip.test.ts | 7 ++++ ...meta-object-tenant-index-roundtrip.test.ts | 7 ++++ .../src/package-disable-enforcement.test.ts | 7 ++++ .../protocol-boot-hydration-scoped.test.ts | 7 ++++ ...otocol-boot-object-package-binding.test.ts | 7 ++++ .../src/protocol-commit-history.test.ts | 14 ++++++- ...otocol-delete-object-registry-heal.test.ts | 7 ++++ .../protocol-meta-effective-schema.test.ts | 7 ++++ ...rotocol-meta-type-canonicalization.test.ts | 7 ++++ .../src/protocol-object-overlay-layer.test.ts | 7 ++++ ...protocol-org-overlay-registry-gate.test.ts | 11 ++++- .../src/protocol-packaged-object-base.test.ts | 7 ++++ .../src/protocol-publish-rollback.test.ts | 7 ++++ .../src/protocol-registry-shadow.test.ts | 12 +++++- .../src/protocol-save-meta-repo-path.test.ts | 7 ++++ .../protocol-view-identity-overlay.test.ts | 7 ++++ ...rotocol-writepath-object-ownership.test.ts | 7 ++++ ...enant-index-author-declared-column.test.ts | 12 +++++- .../registry-tenant-index-declaration.test.ts | 12 +++++- ...registry-tenant-index-follows-wall.test.ts | 12 +++++- .../src/sys-metadata-repository.test.ts | 7 ++++ 25 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 .changeset/engine-findone-predicate-guard.md diff --git a/.changeset/engine-findone-predicate-guard.md b/.changeset/engine-findone-predicate-guard.md new file mode 100644 index 0000000000..bf712f1cb2 --- /dev/null +++ b/.changeset/engine-findone-predicate-guard.md @@ -0,0 +1,40 @@ +--- +"@objectstack/metadata-core": minor +"@objectstack/objectql": minor +--- + +feat(metadata-core,objectql): publish `assertEngineFindOnePredicate` — the read-side member of the engine-double contract family (#11957) + +`ObjectQL.findOne` applies `limit: 1`, so a query naming no particular record +would return an ARBITRARY row. `requireFindOnePredicate` (#4419) REFUSES that +call. Every in-memory test double in the repo instead read an absent filter as +"match everything" and answered happily, so a production call site that violates +#4419 read as *working* under every unit suite and only failed on a real engine. + +That is measured, not hypothetical. `AuthManager.isBootstrapCreation` probed the +bootstrap population with `findOne({ where: [] })` inside a `try/catch`; on a +real engine that throws, the `catch` read the refusal as "users exist", and the +declared first-run bypass became permanently inert on real deployments — while a +641-line unit matrix over the double stayed green, including a case named +"bootstrap: the very first signup is admitted" (#11767). + +New public API, mirroring the two write-side dispatch predicates +(`assertEngineDeleteDispatch`, `assertEngineUpdateDispatch`) exactly — the +implementation lives in `@objectstack/metadata-core` so that packages +`@objectstack/objectql` itself depends on can reach it, and `@objectstack/objectql` +re-exports every symbol: + +- `assertEngineFindOnePredicate(object, query)` — the line a fake engine's + `findOne` opens with; throws the engine's own message, object name included. +- `resolveEngineFindOnePredicate(object, query)` — the same decision without the + throw, for a double that wants to classify. +- `engineFindOnePredicateRefusalMessage(object)` — the refusal text, so an + assertion pins the producer's wording rather than a paraphrase. +- `ENGINE_FINDONE_PREDICATE_CASES` — the shared conformance case-set, driven + against the REAL engine by + `packages/objectql/src/engine-findone-predicate.test.ts`, so the predicate + cannot drift from `engine.ts` unnoticed. + +Nothing is removed and no existing behaviour changes: the engine's own guard is +untouched, and this publishes the decision it already makes so a double can +import it instead of re-deriving it. diff --git a/packages/objectql/src/engine-findone-predicate.test.ts b/packages/objectql/src/engine-findone-predicate.test.ts index a7731a4ad0..e0f4cf0b9e 100644 --- a/packages/objectql/src/engine-findone-predicate.test.ts +++ b/packages/objectql/src/engine-findone-predicate.test.ts @@ -18,6 +18,7 @@ // halves are in the room together. import { describe, it, expect } from 'vitest'; +import type { EngineQueryOptions } from '@objectstack/spec/data'; import { ObjectQL } from './engine.js'; import { ENGINE_FINDONE_PREDICATE_CASES, @@ -73,7 +74,11 @@ async function makeEngine() { async function observeEngine(query: unknown): Promise<'selective' | 'reject'> { const { engine, calls } = await makeEngine(); try { - await engine.findOne(OBJECT, query as any); + // `as unknown as EngineQueryOptions`, never a bare `as any`: the case-set + // deliberately carries OFF-CONTRACT bags (`where: []`, an `orderBy` record) + // because those are the shapes the guard exists to refuse, and this spelling + // names the contract being bypassed instead of erasing it (#4674/#4918). + await engine.findOne(OBJECT, query as unknown as EngineQueryOptions); } catch (e) { const message = (e as Error).message; // Only the #4419 refusal counts as this predicate's verdict. Anything else @@ -144,7 +149,9 @@ describe('engine findOne predicate — the shared predicate IS the engine (#1195 it('requires orderBy to be a NON-EMPTY ARRAY, as `Array.isArray` does in the engine', () => { expect(resolveEngineFindOnePredicate(OBJECT, { orderBy: [] }).kind).toBe('reject'); - expect(resolveEngineFindOnePredicate(OBJECT, { orderBy: { title: 'desc' } as any }).kind) + // `EngineFindOneQueryInput.orderBy` is `unknown`, so the record form needs no + // assertion at all — the predicate's own input type admits it and answers. + expect(resolveEngineFindOnePredicate(OBJECT, { orderBy: { title: 'desc' } }).kind) .toBe('reject'); }); }); diff --git a/packages/objectql/src/layered-overlay-integration.test.ts b/packages/objectql/src/layered-overlay-integration.test.ts index 2c0496953a..761ce15daf 100644 --- a/packages/objectql/src/layered-overlay-integration.test.ts +++ b/packages/objectql/src/layered-overlay-integration.test.ts @@ -21,6 +21,7 @@ import type { MetaRef } from '@objectstack/metadata-core'; import { SysMetadataRepository } from '@objectstack/metadata-protocol'; import { assertEngineDeleteDispatch } from './engine-delete-dispatch.js'; import { assertEngineUpdateDispatch } from './engine-update-dispatch.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; interface Row { id: string; @@ -69,6 +70,12 @@ function makeFakeEngine() { ); }, async findOne(table: string, opts: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, opts); if (table === 'sys_metadata_history') { return historyRows.find((h) => matchesHistory(h, opts.where)) ?? null; } diff --git a/packages/objectql/src/meta-object-primary-designation-roundtrip.test.ts b/packages/objectql/src/meta-object-primary-designation-roundtrip.test.ts index 53406ea673..fee77785db 100644 --- a/packages/objectql/src/meta-object-primary-designation-roundtrip.test.ts +++ b/packages/objectql/src/meta-object-primary-designation-roundtrip.test.ts @@ -61,6 +61,7 @@ import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objects // against what the platform would derive rather than against a transcription. import { provisionPrimary } from '@objectstack/spec/data'; import { SchemaRegistry } from './registry.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; interface Row { id: string; @@ -120,6 +121,12 @@ function makeHost() { const engine: any = { registry, async findOne(_t: string, o: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(_t, o); return findRow(o.where)?.row ?? null; }, async find(_t: string, o: { where: Record }) { diff --git a/packages/objectql/src/meta-object-search-companion-roundtrip.test.ts b/packages/objectql/src/meta-object-search-companion-roundtrip.test.ts index 0f5ea15b89..1f75e82a41 100644 --- a/packages/objectql/src/meta-object-search-companion-roundtrip.test.ts +++ b/packages/objectql/src/meta-object-search-companion-roundtrip.test.ts @@ -40,6 +40,7 @@ import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protoco import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { SchemaRegistry } from './registry.js'; import { SEARCH_COMPANION_FIELD } from './search-companion.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; interface Row { id: string; @@ -88,6 +89,12 @@ function makeHost() { const engine: any = { registry, async findOne(_t: string, o: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(_t, o); return findRow(o.where)?.row ?? null; }, async find(_t: string, o: { where: Record }) { diff --git a/packages/objectql/src/meta-object-tenant-index-roundtrip.test.ts b/packages/objectql/src/meta-object-tenant-index-roundtrip.test.ts index a8fe09065a..ed5ddbd6ae 100644 --- a/packages/objectql/src/meta-object-tenant-index-roundtrip.test.ts +++ b/packages/objectql/src/meta-object-tenant-index-roundtrip.test.ts @@ -54,6 +54,7 @@ import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protoco // below cannot accept a call ObjectQL refuses. import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { SchemaRegistry } from './registry.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; interface Row { id: string; @@ -111,6 +112,12 @@ function makeHost(multiTenant: boolean) { const engine: any = { registry, async findOne(_t: string, o: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(_t, o); return findRow(o.where)?.row ?? null; }, async find(_t: string, o: { where: Record }) { diff --git a/packages/objectql/src/package-disable-enforcement.test.ts b/packages/objectql/src/package-disable-enforcement.test.ts index d933057855..5048415d73 100644 --- a/packages/objectql/src/package-disable-enforcement.test.ts +++ b/packages/objectql/src/package-disable-enforcement.test.ts @@ -8,6 +8,7 @@ import { SchemaRegistry } from './registry.js'; // refuses — a double looser than the implementation is no test at all. import { assertEngineDeleteDispatch } from './engine-delete-dispatch.js'; import { assertEngineUpdateDispatch } from './engine-update-dispatch.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; /** * #7557 (A) — disabling a package must reach the metadata listing and the data @@ -110,6 +111,12 @@ function makeHarness() { const engine: any = { registry, async findOne(table: string, o: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, o); if (table === 'sys_metadata_history') return historyRows.find((h) => matchesWhere(h, o.where)) ?? null; if (table !== 'sys_metadata') return null; return findRow(o.where)?.row ?? null; diff --git a/packages/objectql/src/protocol-boot-hydration-scoped.test.ts b/packages/objectql/src/protocol-boot-hydration-scoped.test.ts index 59db262964..7b98557c2d 100644 --- a/packages/objectql/src/protocol-boot-hydration-scoped.test.ts +++ b/packages/objectql/src/protocol-boot-hydration-scoped.test.ts @@ -23,6 +23,7 @@ import { describe, it, expect } from 'vitest'; import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; import { SchemaRegistry } from './registry.js'; import { assertEngineUpdateDispatch } from './engine-update-dispatch.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; const PKG_A = 'com.acme.a'; const PKG_B = 'com.acme.b'; @@ -63,6 +64,12 @@ function makeEngine(registry: SchemaRegistry, rows: Row[]) { return rows.filter((r) => matches(r, opts.where)); }, async findOne(_t: string, opts: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(_t, opts); return rows.find((r) => matches(r, opts.where)) ?? null; }, async insert() { return { id: 'x' }; }, diff --git a/packages/objectql/src/protocol-boot-object-package-binding.test.ts b/packages/objectql/src/protocol-boot-object-package-binding.test.ts index b83a1e3e59..11111a577a 100644 --- a/packages/objectql/src/protocol-boot-object-package-binding.test.ts +++ b/packages/objectql/src/protocol-boot-object-package-binding.test.ts @@ -52,6 +52,7 @@ import { SchemaRegistry } from './registry.js'; // double cannot accept a call `ObjectQL.delete` / `ObjectQL.update` refuses. import { assertEngineDeleteDispatch } from './engine-delete-dispatch.js'; import { assertEngineUpdateDispatch } from './engine-update-dispatch.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; /** A Studio authoring workspace id — writable under ADR-0070. */ const APP_PKG = 'app.myapp'; @@ -117,6 +118,12 @@ function makeSession(seed: { rows?: Row[]; history?: HistoryRow[] } = {}) { const engine: any = { registry, async findOne(table: string, opts: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, opts); if (table === 'sys_metadata_history') { return historyRows.find((h) => matches(h as any, opts.where)) ?? null; } diff --git a/packages/objectql/src/protocol-commit-history.test.ts b/packages/objectql/src/protocol-commit-history.test.ts index 147a71c250..3e67cba878 100644 --- a/packages/objectql/src/protocol-commit-history.test.ts +++ b/packages/objectql/src/protocol-commit-history.test.ts @@ -8,6 +8,8 @@ import { SchemaRegistry } from './registry.js'; // refuses. import { assertEngineDeleteDispatch } from './engine-delete-dispatch.js'; import { assertEngineUpdateDispatch } from './engine-update-dispatch.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; +import type { EngineFindOneQueryInput } from './engine-findone-predicate.js'; /** * ADR-0067 — package-scoped commit history & rollback. @@ -42,6 +44,12 @@ function makeFakeEngine(seedCommits: any[] = []) { return []; }), findOne: vi.fn(async (table: string, q: any) => { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, q); if (table === 'sys_metadata_commit') return commits.find((c) => c.id === q.where.id) ?? null; return null; // no active sys_metadata rows by default }), @@ -187,7 +195,10 @@ describe('ADR-0067 — publishPackageDrafts records a commit', () => { const commits: any[] = []; const engine: any = { insert: vi.fn(async (t: string, d: any) => { if (t === 'sys_metadata_commit') commits.push(d); }), - findOne: vi.fn(async () => null), // no active rows → every draft is a CREATE + findOne: vi.fn(async (table: string, query?: EngineFindOneQueryInput) => { + assertEngineFindOnePredicate(table, query); + return null; // no active rows → every draft is a CREATE + }), find: vi.fn(async () => []), }; const protocol = new ObjectStackProtocolImplementation(engine as never); @@ -340,6 +351,7 @@ function makeRealRepoHarness(seedCommits: any[] = [], opts: { controlPlane?: boo const engine: any = { registry, async findOne(table: string, opts: { where: Record }) { + assertEngineFindOnePredicate(table, opts); if (table === 'sys_metadata_commit') return commits.find((c) => matchesWhere(c, opts.where)) ?? null; if (table === 'sys_metadata_history') return historyRows.find((h) => matchesWhere(h, opts.where)) ?? null; if (table !== 'sys_metadata') return null; diff --git a/packages/objectql/src/protocol-delete-object-registry-heal.test.ts b/packages/objectql/src/protocol-delete-object-registry-heal.test.ts index abfac76135..3c247d3db0 100644 --- a/packages/objectql/src/protocol-delete-object-registry-heal.test.ts +++ b/packages/objectql/src/protocol-delete-object-registry-heal.test.ts @@ -8,6 +8,7 @@ import { SchemaRegistry } from './registry.js'; // refuses — a double looser than the implementation is no test at all. import { assertEngineDeleteDispatch } from './engine-delete-dispatch.js'; import { assertEngineUpdateDispatch } from './engine-update-dispatch.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; /** * #6808 — deleting an `object` must stop BOTH registry exits serving it. @@ -86,6 +87,12 @@ function makeHarness(opts: { controlPlane?: boolean } = {}) { const engine: any = { registry, async findOne(table: string, o: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, o); if (table === 'sys_metadata_history') return historyRows.find((h) => matchesWhere(h, o.where)) ?? null; if (table !== 'sys_metadata') return null; return findRow(o.where)?.row ?? null; diff --git a/packages/objectql/src/protocol-meta-effective-schema.test.ts b/packages/objectql/src/protocol-meta-effective-schema.test.ts index 42fe1643da..a56bb518dc 100644 --- a/packages/objectql/src/protocol-meta-effective-schema.test.ts +++ b/packages/objectql/src/protocol-meta-effective-schema.test.ts @@ -46,6 +46,7 @@ import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protoco // below cannot accept a call ObjectQL refuses. import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { SchemaRegistry } from './registry.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; interface Row { id: string; @@ -86,6 +87,12 @@ function makeHost(multiTenant: boolean) { const engine: any = { registry, async findOne(_t: string, opts: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(_t, opts); return findRow(opts.where)?.row ?? null; }, async find(_t: string, opts: { where: Record }) { diff --git a/packages/objectql/src/protocol-meta-type-canonicalization.test.ts b/packages/objectql/src/protocol-meta-type-canonicalization.test.ts index d6ce4fbb88..9a4abd6c00 100644 --- a/packages/objectql/src/protocol-meta-type-canonicalization.test.ts +++ b/packages/objectql/src/protocol-meta-type-canonicalization.test.ts @@ -23,6 +23,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; import { SchemaRegistry } from './registry.js'; import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; /** One env-wide, active overlay row for `rc1_probe`, stored under the canonical type. */ const OVERLAY_ROW = { @@ -61,6 +62,12 @@ describe('#4432 — canonical `/meta` type segment', () => { && (w.organization_id === undefined || r.organization_id === w.organization_id)); }), findOne: vi.fn(async (table: string, opts: any) => { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, opts); const found = await engine.find(table, opts); return found[0] ?? null; }), diff --git a/packages/objectql/src/protocol-object-overlay-layer.test.ts b/packages/objectql/src/protocol-object-overlay-layer.test.ts index caa939bd10..736fa93160 100644 --- a/packages/objectql/src/protocol-object-overlay-layer.test.ts +++ b/packages/objectql/src/protocol-object-overlay-layer.test.ts @@ -8,6 +8,7 @@ import { SchemaRegistry } from './registry.js'; // refuses — a double looser than the implementation is no test at all. import { assertEngineDeleteDispatch } from './engine-delete-dispatch.js'; import { assertEngineUpdateDispatch } from './engine-update-dispatch.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; /** * ADR-0029 D9, end to end — a tenant overlay of an object registers as its own @@ -121,6 +122,12 @@ function makeSession(opts: { controlPlane?: boolean; seed?: any[] } = {}) { const engine: any = { registry, async findOne(table: string, o: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, o); if (table === 'sys_metadata_history') return historyRows.find((h) => matchesWhere(h, o.where)) ?? null; if (table !== 'sys_metadata') return null; return findRow(o.where)?.row ?? null; diff --git a/packages/objectql/src/protocol-org-overlay-registry-gate.test.ts b/packages/objectql/src/protocol-org-overlay-registry-gate.test.ts index 82729a8ed7..fd23a4b18e 100644 --- a/packages/objectql/src/protocol-org-overlay-registry-gate.test.ts +++ b/packages/objectql/src/protocol-org-overlay-registry-gate.test.ts @@ -59,6 +59,7 @@ import { DEFAULT_METADATA_TYPE_REGISTRY } from '@objectstack/spec/kernel'; import { SchemaRegistry } from './registry.js'; import { assertEngineUpdateDispatch } from './engine-update-dispatch.js'; import { assertEngineDeleteDispatch } from './engine-delete-dispatch.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; const ORG_A = 'org_a'; const ORG_B = 'org_b'; @@ -82,7 +83,15 @@ function makeEngine(registry: SchemaRegistry) { const engine: any = { registry, find: vi.fn(async (_table: string, opts: any) => rows.filter((r) => matches(r, opts?.where ?? {}))), - findOne: vi.fn(async (table: string, opts: any) => (await engine.find(table, opts))[0] ?? null), + findOne: vi.fn(async (table: string, opts: any) => { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, opts); + return (await engine.find(table, opts))[0] ?? null; + }), insert: vi.fn(async (_table: string, data: any) => { const row = { id: data.id ?? `row_${nextId++}`, ...data }; rows.push(row); diff --git a/packages/objectql/src/protocol-packaged-object-base.test.ts b/packages/objectql/src/protocol-packaged-object-base.test.ts index cc1028b967..5f7b1c8dae 100644 --- a/packages/objectql/src/protocol-packaged-object-base.test.ts +++ b/packages/objectql/src/protocol-packaged-object-base.test.ts @@ -27,6 +27,7 @@ import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protoco import { SchemaRegistry } from './registry.js'; import { assertEngineDeleteDispatch } from './engine-delete-dispatch.js'; import { assertEngineUpdateDispatch } from './engine-update-dispatch.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; const PKG = 'app.showcase'; const OBJ = 'showcase_account'; @@ -66,6 +67,12 @@ function makeSession() { const engine: any = { registry, async findOne(table: string, o: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, o); if (table === 'sys_metadata_history') return historyRows.find((h) => matchesWhere(h, o.where)) ?? null; if (table !== 'sys_metadata') return null; return findRow(o.where)?.row ?? null; diff --git a/packages/objectql/src/protocol-publish-rollback.test.ts b/packages/objectql/src/protocol-publish-rollback.test.ts index fd561784d1..28d401b7ef 100644 --- a/packages/objectql/src/protocol-publish-rollback.test.ts +++ b/packages/objectql/src/protocol-publish-rollback.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it, vi } from 'vitest'; import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; import { assertEngineDeleteDispatch } from './engine-delete-dispatch.js'; import { assertEngineUpdateDispatch } from './engine-update-dispatch.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; /** * Protocol-level coverage for the per-item draft / publish / rollback / @@ -76,6 +77,12 @@ function makeStubEngine() { const engine: any = { async findOne(table: string, opts: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, opts); if (table === 'sys_metadata_history') { return historyRows.find((h) => matchesHistory(h, opts.where)) ?? null; } diff --git a/packages/objectql/src/protocol-registry-shadow.test.ts b/packages/objectql/src/protocol-registry-shadow.test.ts index e302e2a0e6..20b1ff6aed 100644 --- a/packages/objectql/src/protocol-registry-shadow.test.ts +++ b/packages/objectql/src/protocol-registry-shadow.test.ts @@ -27,6 +27,8 @@ import { ObjectStackProtocolImplementation, resetEnvWritableMetadataTypes } from import { ObjectQL } from './engine.js'; import { SchemaRegistry } from './registry.js'; import { assertEngineUpdateDispatch } from './engine-update-dispatch.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; +import type { EngineFindOneQueryInput } from './engine-findone-predicate.js'; const PKG = 'com.objectstack.test-pkg'; @@ -311,7 +313,15 @@ describe('registry shadow — scoped-kernel lock enforcement is shadow-immune', const mockEngine: any = { registry, find: async () => [], - findOne: async () => null, + findOne: async (table: string, query?: EngineFindOneQueryInput) => { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, query); + return null; + }, insert: async () => ({ id: 'x' }), update: async (_o: string, data: any, opts?: any) => { // [#5480] Pinned to ObjectQL.update's OWN dispatch predicate — the twin of diff --git a/packages/objectql/src/protocol-save-meta-repo-path.test.ts b/packages/objectql/src/protocol-save-meta-repo-path.test.ts index acdf1e3c82..0571087d04 100644 --- a/packages/objectql/src/protocol-save-meta-repo-path.test.ts +++ b/packages/objectql/src/protocol-save-meta-repo-path.test.ts @@ -5,6 +5,7 @@ import { hashSpec } from '@objectstack/metadata-core'; import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; import { assertEngineDeleteDispatch } from './engine-delete-dispatch.js'; import { assertEngineUpdateDispatch } from './engine-update-dispatch.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; /** * Repository write-path coverage (post PR-10d.6). @@ -47,6 +48,12 @@ function makeStubEngine() { }; const engine: any = { async findOne(_t: string, opts: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(_t, opts); return findRow(opts.where)?.row ?? null; }, async find(_t: string, opts: { where: Record }) { diff --git a/packages/objectql/src/protocol-view-identity-overlay.test.ts b/packages/objectql/src/protocol-view-identity-overlay.test.ts index e8c39eee25..b07bf6bca9 100644 --- a/packages/objectql/src/protocol-view-identity-overlay.test.ts +++ b/packages/objectql/src/protocol-view-identity-overlay.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest'; import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; import { assertEngineDeleteDispatch } from './engine-delete-dispatch.js'; import { assertEngineUpdateDispatch } from './engine-update-dispatch.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; /** * #2555 — a console personalization PUT (grid column sort, inline edit, …) @@ -65,6 +66,12 @@ function makeStubEngine(registryViews: Record = {}) { }; const engine: any = { async findOne(_t: string, opts: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(_t, opts); return findRow(opts.where)?.row ?? null; }, async find(_t: string, opts: { where: Record }) { diff --git a/packages/objectql/src/protocol-writepath-object-ownership.test.ts b/packages/objectql/src/protocol-writepath-object-ownership.test.ts index aad15acc5d..4ea7b6efd6 100644 --- a/packages/objectql/src/protocol-writepath-object-ownership.test.ts +++ b/packages/objectql/src/protocol-writepath-object-ownership.test.ts @@ -47,6 +47,7 @@ import { SchemaRegistry } from './registry.js'; // double cannot accept a call `ObjectQL.delete` / `ObjectQL.update` refuses. import { assertEngineDeleteDispatch } from './engine-delete-dispatch.js'; import { assertEngineUpdateDispatch } from './engine-update-dispatch.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; /** A Studio authoring workspace id — writable under ADR-0070. */ const APP_PKG = 'app.myapp'; @@ -106,6 +107,12 @@ function makeHarness() { const engine: any = { registry, async findOne(table: string, opts: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, opts); if (table === 'sys_metadata_history') { return historyRows.find((h) => matches(h as any, opts.where)) ?? null; } diff --git a/packages/objectql/src/registry-tenant-index-author-declared-column.test.ts b/packages/objectql/src/registry-tenant-index-author-declared-column.test.ts index 37036ddcfa..5e5d040f1c 100644 --- a/packages/objectql/src/registry-tenant-index-author-declared-column.test.ts +++ b/packages/objectql/src/registry-tenant-index-author-declared-column.test.ts @@ -59,6 +59,8 @@ import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protoco // below cannot accept a call ObjectQL refuses. import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { SchemaRegistry, applySystemFields } from './registry.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; +import type { EngineFindOneQueryInput } from './engine-findone-predicate.js'; /** The platform's own entry — the exact value the seam appends. */ const PLATFORM_TENANT_INDEX = { fields: ['organization_id'] }; @@ -115,7 +117,15 @@ function metaSurface(multiTenant: boolean, object: any) { const engine = { registry, find: async () => [], - findOne: async () => null, + findOne: async (table: string, query?: EngineFindOneQueryInput) => { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, query); + return null; + }, insert: async () => ({ id: 'x' }), update: async (_t: string, data: Record, opts?: Record) => { assertEngineUpdateDispatch(data, opts); diff --git a/packages/objectql/src/registry-tenant-index-declaration.test.ts b/packages/objectql/src/registry-tenant-index-declaration.test.ts index dcc0de9481..4a331a2a42 100644 --- a/packages/objectql/src/registry-tenant-index-declaration.test.ts +++ b/packages/objectql/src/registry-tenant-index-declaration.test.ts @@ -32,6 +32,8 @@ import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protoco // below cannot accept a call ObjectQL refuses. import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { SchemaRegistry, applySystemFields } from './registry.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; +import type { EngineFindOneQueryInput } from './engine-findone-predicate.js'; /** A plain business object — one authored field, nothing else. */ const LEAD = { name: 'lead', label: 'Lead', fields: { first_name: { type: 'text' } } } as any; @@ -47,7 +49,15 @@ function metaSurface(multiTenant: boolean) { const engine = { registry, find: async () => [], - findOne: async () => null, + findOne: async (table: string, query?: EngineFindOneQueryInput) => { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, query); + return null; + }, insert: async () => ({ id: 'x' }), update: async (_t: string, data: Record, opts?: Record) => { assertEngineUpdateDispatch(data, opts); diff --git a/packages/objectql/src/registry-tenant-index-follows-wall.test.ts b/packages/objectql/src/registry-tenant-index-follows-wall.test.ts index 8bf1ccd40f..96cd51af33 100644 --- a/packages/objectql/src/registry-tenant-index-follows-wall.test.ts +++ b/packages/objectql/src/registry-tenant-index-follows-wall.test.ts @@ -58,6 +58,8 @@ import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protoco // below cannot accept a call ObjectQL refuses. import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { SchemaRegistry, applySystemFields } from './registry.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; +import type { EngineFindOneQueryInput } from './engine-findone-predicate.js'; /** The platform's own entry — the exact value the seam appends. */ const PLATFORM_TENANT_INDEX = { fields: ['organization_id'] }; @@ -101,7 +103,15 @@ function metaSurface(multiTenant: boolean, object: any) { const engine = { registry, find: async () => [], - findOne: async () => null, + findOne: async (table: string, query?: EngineFindOneQueryInput) => { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, query); + return null; + }, insert: async () => ({ id: 'x' }), update: async (_t: string, data: Record, opts?: Record) => { assertEngineUpdateDispatch(data, opts); diff --git a/packages/objectql/src/sys-metadata-repository.test.ts b/packages/objectql/src/sys-metadata-repository.test.ts index 6206453d3c..4ae485ce95 100644 --- a/packages/objectql/src/sys-metadata-repository.test.ts +++ b/packages/objectql/src/sys-metadata-repository.test.ts @@ -13,6 +13,7 @@ import { ConflictError, hashSpec } from '@objectstack/metadata-core'; import { SysMetadataRepository } from '@objectstack/metadata-protocol'; import { assertEngineDeleteDispatch } from './engine-delete-dispatch.js'; import { assertEngineUpdateDispatch } from './engine-update-dispatch.js'; +import { assertEngineFindOnePredicate } from './engine-findone-predicate.js'; interface Row { id: string; @@ -97,6 +98,12 @@ function makeFakeEngine() { }); }, async findOne(table: string, opts: { where: Record }) { + // [#11957] Pinned to ObjectQL.findOne's OWN #4419 predicate: `findOne` + // applies limit: 1, so a query naming no record returns an ARBITRARY row + // and the engine REFUSES it. A double that answers it anyway is how + // #11767 shipped a bootstrap bypass that was inert on every real + // deployment while a 641-line unit matrix stayed green. + assertEngineFindOnePredicate(table, opts); if (table === 'sys_metadata_history') { return historyRows.find((h) => matchesHistory(h, opts.where)) ?? null; } From 885a32bc3c1823124810332a141dabfa8d1126e5 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Tue, 25 Aug 2026 17:38:59 +0800 Subject: [PATCH 3/3] fix(objectql): pass the required packageId to registerObject in the findOne conformance test The one-argument spelling is a TS2554 that objectql's own tsconfig hides (it excludes **/*.test.ts), visible only to the shrink-only TEST_DEBT re-measure, where it read as 354 -> 355. --- .../src/engine-findone-predicate.test.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/objectql/src/engine-findone-predicate.test.ts b/packages/objectql/src/engine-findone-predicate.test.ts index e0f4cf0b9e..983a35bc59 100644 --- a/packages/objectql/src/engine-findone-predicate.test.ts +++ b/packages/objectql/src/engine-findone-predicate.test.ts @@ -62,11 +62,19 @@ async function makeEngine() { // die at a different door and tell us nothing about this one. // `searchableFields` is explicit so the `search` case resolves deterministically // rather than through the auto-default. - engine.registry.registerObject({ - name: OBJECT, - fields: { title: { type: 'text' }, status: { type: 'text' } }, - searchableFields: ['title', 'status'], - } as any); + // `packageId` is REQUIRED (`registerObject(schema, packageId, …)`); the + // one-argument spelling some older doubles in this package still use is a + // TS2554 that objectql's own `tsconfig.json` hides, because it excludes + // `**/*.test.ts` — visible only to the TEST_DEBT re-measure, which is a + // shrink-only ratchet. Passing it keeps this file out of that pile. + engine.registry.registerObject( + { + name: OBJECT, + fields: { title: { type: 'text' }, status: { type: 'text' } }, + searchableFields: ['title', 'status'], + }, + 'test-package', + ); return { engine, calls }; }