Skip to content

Commit 1dc6029

Browse files
refactor(metadata-protocol): type the six query-options erasures in protocol.ts data reads (#29834)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8HsXKaPvAsTjybiH6QpVnvn)_ Fixes #29764 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:`protocol.ts` 数据读取里六处把引擎查询参数擦成 `any` 的调用点(`findData` 的 find、count、aggregate,`getData`,`cloneData`,`probeRecord`)改为按引擎声明的类型传参(`EngineQueryOptions`、`EngineCountOptions`、`EngineAggregateOptions`);`protocol.ts` 从擦除基线中移除(计数归零)。 - **为什么改**:#25467 席位裁决 A(6594932448):基线按文件登记且只减不增,这四个方法体带着擦除点搬不出类。类型化之后,#25467 才能把剩下四个方法体作为下一刀搬走。 - **风险与回滚**:低。不改行为:find 与 findOne 的参数包按键逐个从原工作包复制,只复制存在的键,引擎收到的键与之前相同;唯一不再带上的是值为 null 的 `groupBy`/`aggregations`,引擎本来就把 null 读作缺省。回滚即整体 revert。 - **先看**:`protocol.ts` 中 `findQuery` 的构造(`findData` 里);`getData` 的 `findOneQuery`;`scripts/query-options-erasure-baseline.json` 删掉的一行。 - **两分钟验证**:`pnpm check:query-options-erasure`,预期 `✓ … holds`,且输出里不再出现 `protocol.ts`。 - **需要你做的**:无,席位落地。 ## What changes `scripts/query-options-erasure-baseline.json` grandfathered six `query-options/no-any-erasure` sites in `packages/metadata-protocol/src/protocol.ts`. Measured on `origin/main` `eb05ea2f` with the rule run over the file and the grandfathering lifted: | line on main | member | engine call | erased expression | declared type now | |---:|---|---|---|---| | 6124 | `findData` | `aggregate` | `{ where, groupBy, aggregations, having, search, searchFields, context } as any` | `EngineAggregateOptions` | | 6220 | `findData` | `find` | `options`, declared `const options: any` | `EngineQueryOptions` | | 6296 | `findData` | `count` | `{ where, search, searchFields, context } as any` | `EngineCountOptions` | | 6371 | `getData` | `findOne` | `queryOptions`, declared `const queryOptions: any` | `EngineQueryOptions` | | 6455 | `cloneData` | `findOne` | `{ where: { id }, ...(ctxOpt as any) } as any` | `EngineQueryOptions` | | 6612 | `probeRecord` | `findOne` | `{ where: { id }, context: { isSystem: true } } as any` | `EngineQueryOptions` | Each type is the one `IDataEngine` declares for that method (`packages/spec/src/contracts/data-engine.ts`). How each site is typed: - **`aggregate` and `count`**: the literal is bound to a typed const (`aggregateQuery`, `countQuery`) and the assertion is gone. - **`cloneData` and `probeRecord`**: the assertions are dropped. The literals already match `findOne`'s parameter. - **`find` in `findData`**: `options` is the caller's raw request bag, normalized in place over about 600 lines, so it keeps its working type. The bag handed to the engine is now a typed `findQuery`, built from the nine keys the engine's find set holds (`context`, `where`, `fields`, `orderBy`, `limit`, `offset`, `search`, `searchFields`, `expand`). Each key is copied only when it is present. By that line every other key has been folded (`top`, `select` and the other aliases), deleted (`object`, `count`, `joins`, `windowFunctions`, `cursor`, `distinct`, `having`) or moved into `where` (the implicit field filters). `groupBy` and `aggregations` can only be null there: a non-empty one takes the aggregate branch, an empty one is refused, and a non-array is refused. The engine already reads a null option as absent (`rejectUnknownEngineOptions` skips it, and nothing on the find path reads either key), so leaving them out changes nothing. - **`findOne` in `getData`**: `queryOptions` stays the working bag, because the two projection helpers it calls (`projectionFieldsValue`, `storedMetadataBodyProjection`) answer `unknown`. The bag handed to the engine is a typed `findOneQuery` built the same way from its four keys (`where`, `context`, `fields`, `expand`). `protocol.ts` leaves the baseline: its count is 0, and its key is removed, as the ratchet asks of a fixed file. `eslint.config.mjs` needs no edit, since the rule's `ignores` are read from the baseline's keys. No body moves: that is #25467's next cut. ## Tests No pin needed an edit, and no new pin was added: the change is in types plus a key-by-key copy that hands the engine the same keys. A reverse check shows the types reach the six bags. With one wrong key planted in each (`direction` in `findQuery` and in `probeRecord`, `limit` in `aggregateQuery`, `orderBy` in `countQuery`, `sort` in `findOneQuery`, `filter` in `cloneData`'s literal), the package typecheck exited 2 with six `TS2353` errors, one per site. The file was restored from `HEAD` and `git diff HEAD` was empty afterwards. ## Acceptance notes - `getData`'s working bag and `findData`'s raw request bag still hold `any` values. What tsc now checks at all six sites is the key set the engine receives, which is the failure the rule exists for. Typing `getData`'s working bag itself would need `projectionFieldsValue` and `storedMetadataBodyProjection` to answer `string[]` when they are given `string[]`. Those are type-only signature changes in `protocol-list-query-params.ts` and `metadata-redaction.ts`, outside this card's surface, and they are noted here, not filed. - The ratchet also reports `plugin-approvals/src/approval-service.ts` falling 10 → 4 on main. That row belongs to another file and is left as it is. ## Verification On `e16c71d2` (the branch merged with `origin/main` `b0ef25f5`; no file of this diff moved on main): - Measure on main `eb05ea2f`, the rule run over `protocol.ts` with the grandfathering lifted: 6 sites (the table above). On this head: 0. - Build of the package's dependency closure plus `@objectstack/objectql`, package typecheck and package test, each through os-verify-lock: `VERDICT command-exit 0`. Test Files 310 passed, 4 skipped (314); Tests 22305 passed, 26 skipped (22331). - `pnpm check:query-options-erasure` exits 0: `✓ query-options-erasure ratchet holds: 38 unswept non-test site(s) in 14 file(s), none new`, baseline key set verified against `b0ef25f`, no files added. - `pnpm lint` exits 0, and `eslint packages/metadata-protocol --no-inline-config` exits 0 with `protocol.ts` now under the rule. - The published `index.d.ts` is byte-identical. Built from main's `protocol.ts` and from this head with the same dependency build: sha256 `3a15d153…f48075` on both sides, ESM equal to CJS. - `node scripts/pm/dispatch-gates.mjs --commands` derived 63 commands. `--ran` reports 63 derived, 61 run, 2 NOT-MEASURED, 0 UNRUN, and each run family exits 0. The two NOT-MEASURED exit 3 on a prerequisite, so their reading is CI's: `check:dual-build-cjs-loads` needs every package built, and `check-plugin-teardown-shape.mjs --self-test` needs an unshallowed clone. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (untouched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: n/a. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is the dispatch template's). --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbYv9aJAo6MEAcF1dRK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3cdb162 commit 1dc6029

3 files changed

Lines changed: 53 additions & 9 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
refactor(metadata-protocol): the data reads in `protocol.ts` hand the engine typed query options instead of `any`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
No behaviour change. `findData` (its `find`, `count` and `aggregate` calls), `getData`, `cloneData` and `probeRecord` now pass the engine an options bag typed as `IDataEngine` declares it (`EngineQueryOptions`, `EngineCountOptions`, `EngineAggregateOptions`), so `tsc` checks every key they send. `findData` builds its `find` bag key by key from the normalized request, copying only the keys that are present, so the engine receives the same keys as before. The one key that no longer rides along is a `groupBy` or `aggregations` set to null on a plain list query, which the engine already read as absent. `protocol.ts` leaves `scripts/query-options-erasure-baseline.json`, since its count of erased sites is now 0. Nothing to migrate.

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ import {
113113
QUERY_TRANSPORT_DOLLAR_ALIASES,
114114
QUERY_TRANSPORT_DOLLAR_PARAMS,
115115
type DroppedFieldsEvent,
116+
type EngineAggregateOptions,
117+
type EngineCountOptions,
118+
type EngineQueryOptions,
116119
type EngineQueryOptionsParsed,
117120
type ValidationAdvisoryEvent,
118121
externalBindingDatasourceRefusal,
@@ -6121,7 +6124,7 @@ export class ObjectStackProtocolImplementation implements
61216124
const hasGroupBy = Array.isArray(options.groupBy) && options.groupBy.length > 0;
61226125
const hasAggregations = Array.isArray(options.aggregations) && options.aggregations.length > 0;
61236126
if (hasGroupBy || hasAggregations) {
6124-
const records = await this.engine.aggregate(request.object, {
6127+
const aggregateQuery: EngineAggregateOptions = {
61256128
where: options.where,
61266129
groupBy: options.groupBy,
61276130
aggregations: options.aggregations,
@@ -6140,7 +6143,8 @@ export class ObjectStackProtocolImplementation implements
61406143
search: options.search,
61416144
searchFields: options.searchFields,
61426145
context: options.context,
6143-
} as any);
6146+
};
6147+
const records = await this.engine.aggregate(request.object, aggregateQuery);
61446148
// Apply limit client-side (EngineAggregateOptions doesn't carry limit).
61456149
// `records` is the full grouped set, so its length IS the real total
61466150
// and `hasMore` follows from whether the slice dropped any groups.
@@ -6209,6 +6213,25 @@ export class ObjectStackProtocolImplementation implements
62096213
// projection named only the body, and taken back off before serving.
62106214
const bodyProjection = storedMetadataBodyProjection(request.object, options.fields);
62116215
if (bodyProjection.addedType) options.fields = bodyProjection.fields;
6216+
// [#29764] The bag `engine.find` receives is typed as the engine declares
6217+
// it. `options` is the caller's normalized request bag, and by this line
6218+
// every key the normalizer does not hand on has been folded, deleted or
6219+
// moved into `where`. What can still stand is the find set below, plus
6220+
// `groupBy` / `aggregations` holding null at most (a non-empty one took
6221+
// the aggregate branch above, an empty one was refused). The engine reads
6222+
// a null option as absent, so leaving those two out changes nothing. Keys
6223+
// are copied only when present, so the engine sees the same bag as before.
6224+
const findQuery: EngineQueryOptions = {
6225+
...(options.context !== undefined ? { context: options.context } : {}),
6226+
...(options.where !== undefined ? { where: options.where } : {}),
6227+
...(options.fields !== undefined ? { fields: options.fields } : {}),
6228+
...(options.orderBy !== undefined ? { orderBy: options.orderBy } : {}),
6229+
...(options.limit !== undefined ? { limit: options.limit } : {}),
6230+
...(options.offset !== undefined ? { offset: options.offset } : {}),
6231+
...(options.search !== undefined ? { search: options.search } : {}),
6232+
...(options.searchFields !== undefined ? { searchFields: options.searchFields } : {}),
6233+
...(options.expand !== undefined ? { expand: options.expand } : {}),
6234+
};
62126235
// [#21207] …and its stored CONTENT HASH (`checksum`, `previous_checksum`)
62136236
// is served in keyed form — never the stored value, which beside the
62146237
// projected body confirms a guess at the withheld material offline —
@@ -6217,7 +6240,7 @@ export class ObjectStackProtocolImplementation implements
62176240
request.object,
62186241
redactStoredMetadataRows(
62196242
request.object,
6220-
await this.engine.find(request.object, options),
6243+
await this.engine.find(request.object, findQuery),
62216244
{ dropType: bodyProjection.addedType },
62226245
),
62236246
this.storedHashDigest(),
@@ -6293,12 +6316,13 @@ export class ObjectStackProtocolImplementation implements
62936316
// TypeScript cannot narrow it back across the try/catch.
62946317
let counted: number;
62956318
try {
6296-
counted = await this.engine.count(request.object, {
6319+
const countQuery: EngineCountOptions = {
62976320
where: options.where,
62986321
search: options.search,
62996322
searchFields: options.searchFields,
63006323
context: options.context,
6301-
} as any);
6324+
};
6325+
counted = await this.engine.count(request.object, countQuery);
63026326
} catch {
63036327
// engine.count() has its own find().length fallback; if it still
63046328
// throws, degrade to a page-local total rather than failing the list.
@@ -6368,7 +6392,18 @@ export class ObjectStackProtocolImplementation implements
63686392
if (Object.keys(served).length > 0) queryOptions.expand = served;
63696393
}
63706394

6371-
const result = await this.engine.findOne(request.object, queryOptions);
6395+
// [#29764] The bag `engine.findOne` receives is typed as the engine
6396+
// declares it. `queryOptions` stays the working bag: the projection
6397+
// helpers above answer `unknown` (they serve the raw list query too), so
6398+
// its `fields` has no declared type until it is handed on here. Keys are
6399+
// copied only when present, so the engine sees the same bag as before.
6400+
const findOneQuery: EngineQueryOptions = {
6401+
where: queryOptions.where,
6402+
...(queryOptions.context !== undefined ? { context: queryOptions.context } : {}),
6403+
...(queryOptions.fields !== undefined ? { fields: queryOptions.fields } : {}),
6404+
...(queryOptions.expand !== undefined ? { expand: queryOptions.expand } : {}),
6405+
};
6406+
const result = await this.engine.findOne(request.object, findOneQuery);
63726407
if (result) {
63736408
return {
63746409
object: request.object,
@@ -6452,7 +6487,7 @@ export class ObjectStackProtocolImplementation implements
64526487

64536488
const source = await this.engine.findOne(
64546489
request.object,
6455-
{ where: { id: request.id }, ...(ctxOpt as any) } as any,
6490+
{ where: { id: request.id }, ...ctxOpt },
64566491
);
64576492
if (!source) throw recordNotFoundError(request.object, request.id);
64586493

@@ -6609,7 +6644,7 @@ export class ObjectStackProtocolImplementation implements
66096644
* token, a caller whose read of the row is refused gets that read refusal.
66106645
*/
66116646
private async probeRecord(object: string, id: string): Promise<any> {
6612-
return this.engine.findOne(object, { where: { id }, context: { isSystem: true } } as any);
6647+
return this.engine.findOne(object, { where: { id }, context: { isSystem: true } });
66136648
}
66146649

66156650
/**

‎scripts/query-options-erasure-baseline.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
"nonTest": {
3535
"packages/cloud-connection/src/marketplace-install-local-plugin.ts": 1,
3636
"packages/core/src/security/resolve-authz-context.ts": 1,
37-
"packages/metadata-protocol/src/protocol.ts": 6,
3837
"packages/metadata-protocol/src/seed-loader.ts": 3,
3938
"packages/objectql/src/engine.ts": 8,
4039
"packages/plugins/plugin-approvals/src/approval-service.ts": 10,

0 commit comments

Comments
 (0)