You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Browse filesBrowse the repository at this point in the historyBrowse files
authored
fix(rest): import-runner builds the canonical QueryAST through a typed findData envelope (#16950)
* fix(rest): import-runner builds canonical QueryAST, through a typed envelope
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
* chore(rest): changeset for the canonical QueryAST rewrite in import-runner
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
* fix(plugin-auth): read the canonical QueryAST in the admin-import protocol
`runImport` now sends `where` / `limit`, and it takes an INJECTED
`ImportProtocolLike`. The wire-alias folding the runner's rewrite relied on
lives in `ObjectStackProtocolImplementation`; a hand-written protocol never
passes through it, so `admin-import-users.ts` read `args.query.$filter` and
got `undefined`.
The failure mode is not a missing filter but an unbounded one: `?? {}` turns
the unread key into an empty filter, the upsert duplicate probe stops
discriminating, and an admin import updates the wrong user. Both halves were
red on this branch (`admin-import-users.test.ts:560` and `:592`).
- The adapter reads `args.query.where` / `args.query.limit`, with no `??`
behind either. One dialect, and an absent `query` is a loud TypeError
rather than a silent match-everything.
- The three `import-runner` test doubles read `where` too. Two were red
(`import-runner-selfref.test.ts`, `import-runner-bulk.test.ts`); the third
was GREEN FOR THE WRONG REASON — its degraded `{}` matched the whole store,
so every no-duplicate assertion held without the probe discriminating.
- `import-runner-idempotency.test.ts` gains the assertion that closes that:
every probe must narrow, and the id recheck is pinned to the
`id: { $in: [...] }` over exactly the pre-assigned ids, bounded by `limit`.
`ImportProtocolLike.findData(args: any)` is deliberately untouched — narrowing
a published extension point is a contract decision and has its own card.
Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <pm@objectstack.ai>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`import-runner.ts` builds its three server-side `findData` requests in the CANONICAL QueryAST, and the helper that carried them is typed against the declared contract instead of `any`.
6
+
7
+
`FindDataRequestSchema` declares `query: QuerySchema.optional()`, and `QuerySchema` declares `where` / `limit` / `offset` / `fields` / `orderBy` / `expand` — it declares neither `$filter` nor `$top`. The normalizer's own table calls those two "the wire-only spellings no schema declares". The reference resolver, the duplicate probe and the id recheck each built a literal in that undeclared dialect, and nothing reddened because the helper they went through took `query: any`: the literals were type-checked by nothing at all, so the undeclared keys cost no diagnostic. Reverting one of them to `$filter` now costs `TS2353 … '$filter' does not exist in type 'QueryInput'`; on the pre-change file the identical revert cost zero errors.
8
+
9
+
-**The three literals.**`$filter` → `where`, `$top` → `limit`, plus the `object` the declared query requires. No behaviour change on the two `rest-server.ts` call paths (`POST /data/:object/import` and the async import-job worker), which hand `runImport` the real `ObjectStackProtocolImplementation`: that normalizer folds `$filter` onto `where` and `$top` onto `limit` by the spec's own `RPC_QUERY_ALIAS_SLOTS`, moving the value verbatim, so both dialects reach `engine.find` as the same option bag.
10
+
-**The erasure vehicle.**`findArgsBase` now takes a `FindDataRequest` rather than a bare `any` query, so the request-level `object` is compiled too and the `object: ''` placeholder every caller had to override is gone. This is the durable half: rewriting the literals while leaving the parameter `any` would leave the next author in this file with no diagnostic at all.
11
+
-**The pin.**`rest-server-canonical-query-ast.test.ts` censuses the PACKAGE rather than one file. `import-runner.ts` has no HTTP door — every query in it is server-built — so its census rejects a wire spelling anywhere in the file, not only inside a `query:` slot. That whole-file rule is the one that finds this class: these three literals were arguments to a helper and were never in a `query:` slot to begin with.
12
+
13
+
⚠️ Implementor-visible: `ImportProtocolLike` is exported, its `findData(args: any)` never declared which dialect the runner sends, and the runner now sends the canonical one. An implementation that reads `args.query.$filter` / `args.query.$top` directly — rather than through the protocol normalizer — receives `undefined` and must be updated to read `where` / `limit`.
`runAdminImportUsers`'s hand-written `ImportProtocolLike` reads the CANONICAL QueryAST (`where` / `limit`) — the payload `@objectstack/rest`'s import runner sends as of this same release — instead of the wire-only `$filter` / `$top`.
6
+
7
+
`POST /api/v1/auth/admin/import-users` reuses the shared import runner but swaps in an identity-specific protocol, because an identity write is `auth.api.createUser` and not an engine insert. That protocol is hand-written, so it never passes through `ObjectStackProtocolImplementation` — the normalizer that folds `$filter` onto `where` and `$top` onto `limit` for a caller arriving off the HTTP door. It has to read the canonical keys itself.
8
+
9
+
-**A mismatch here does not produce a missing filter, it produces an unbounded one.**`const where = args?.query?.$filter ?? {}` turns an unread key into an empty filter, and an empty filter constrains nothing: the upsert duplicate probe stops discriminating, `findExisting` matches rows it was given no key for, and an admin import updates the WRONG user. Both halves are measured in `admin-import-users.test.ts` — the email-match case reported `updated: 2` where one of the two rows was new, and the phone-match case sent a probe carrying no `where` at all.
10
+
-**One dialect, and no default behind it.** The two reads are now `args.query.where` and `args.query.limit`, with no `??`. A default here would not be tolerance for an older caller — this handle is fed by the runner, never off the wire — it is precisely the lenient fallback that converts a spelling mismatch into a silent match-everything. A request that arrives without a `query` now costs a loud `TypeError` instead.
11
+
12
+
⚠️ No published version shipped the mismatch. The runner's rewrite and this adapter land in the same release, and `@objectstack/plugin-auth` depends on `@objectstack/rest` at an exact workspace version, so the two cannot be installed apart. What this entry records is why they move together — and what the same mismatch costs any OTHER hand-written `ImportProtocolLike`, which the `@objectstack/rest` entry calls out for implementors.
0 commit comments