Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/30518-protocol-find-data-to-facet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@objectstack/metadata-protocol': patch
'@objectstack/spec': patch
---

refactor(metadata-protocol): `findData` hands its body to its facet

Clause-②: no
Metadata-protocol: none

- `ObjectStackProtocolImplementation#findData` keeps its signature and docblock on the class and delegates in one line to `ProtocolDataCrudFacet`, which now holds the body, moved unchanged. The built `index.d.ts` and `index.d.cts` are byte-equal before and after, and no behaviour changes. Nothing to do on upgrade.
- `getDiscovery` stays on the class: its body reads `typeof this.searchAll === 'function'`, which a forwarded host method would always answer true. The move tool now refuses a method read as a value.
- `@objectstack/spec`'s published liveness ledger (`liveness/query.json`) cites `findData` at its new file.
2 changes: 1 addition & 1 deletion docs/qa/platform-checklist/areas/api-backend.json
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@
"source": [
"packages/spec/src/data/filter.zod.ts#AST_OPERATOR_MAP (AST_OPERATOR_MAP / FieldOperatorsSchema — the operator variant source)",
"packages/spec/src/data/query.zod.ts#QueryAST (QueryAST keys)",
"packages/metadata-protocol/src/protocol.ts#UNSUPPORTED_QUERY_PARAM (supported $-params + UNSUPPORTED_QUERY_PARAM #2926 ⑩, INVALID_FIELD #4134)",
"packages/metadata-protocol/src/protocol-data-crud.ts#UNSUPPORTED_QUERY_PARAM (supported $-params + UNSUPPORTED_QUERY_PARAM #2926 ⑩, INVALID_FIELD #4134)",
"packages/spec/src/data/filter.zod.ts#checkFilterConditionComparands (the request-schema refusal of a scalar list comparand on the POST door, #20116 — the $-object spelling, located at query.where.<field>.$nin)",
"packages/spec/src/data/data-engine.zod.ts#lowerFoldedTransportValues (the AST/FilterArray spelling on the POST door, refused at query.where when it cannot be lowered)",
"packages/spec/src/data/data-engine.zod.ts#QueryTransportParamsSchema (`filter` is a declared transport spelling of `where` — why negative[0] no longer probes with it)",
Expand Down
2 changes: 1 addition & 1 deletion docs/qa/platform-checklist/areas/search.json
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@
"examples/app-showcase/src/security/permission-sets.ts#invoice_own_rows (invoice_own_rows: owner == current_user.email, positions ['contributor'])",
"examples/app-showcase/src/data/seed/index.ts (INV-1001..1012 owner spread; 'ada sees INV-1001/1002's lines but never linus's INV-1003')",
"packages/objectql/src/engine.ts#expandSearchOnAst (expandSearchOnAst ANDs into ast.where; count() rides the read middleware — the #2737 total-leak fix)",
"packages/metadata-protocol/src/protocol.ts#findData (the paged arm counts a searched list: engine.count is handed the query's search and searchFields, so total is the counted number of matches and hasMore agrees with it; the `countable` exception and its page-local estimate were deleted by #22790, PR #22857) + packages/rest/src/list-search-total-door.test.ts ('[#22790] a paged search reports the counted total', including 'a member counts only the matches it can read')"
"packages/metadata-protocol/src/protocol-data-crud.ts#findData (the paged arm counts a searched list: engine.count is handed the query's search and searchFields, so total is the counted number of matches and hasMore agrees with it; the `countable` exception and its page-local estimate were deleted by #22790, PR #22857) + packages/rest/src/list-search-total-door.test.ts ('[#22790] a paged search reports the counted total', including 'a member counts only the matches it can read')"
],
"history": [
{ "revision": 1, "date": "2026-08-07", "change": "new item: RLS × $search both-personas contract; total-leakage clause grounded in the protocol's page-local total for searched lists and the #2737 count middleware fix, persona provisioning gap recorded honestly", "ref": "claude/platform-test-checklist-ocwugl" },
Expand Down
4 changes: 4 additions & 0 deletions packages/metadata-protocol/scripts/host-split.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ export function rewriteSites(sf, method, table, className = CLASS_NAME) {
const member = table.instance.get(name);
if (!member) { refuse(node, `\`this.${name}\` names no declared instance member`); return; }
if (table.statics.has(name)) { refuse(node, `\`${name}\` is both an instance and a static member: the host cannot carry both`); return; }
// [#30518] A method's host entry is a forwarding closure, always a function, so a
// method read other than as a callee (`typeof this.m`, `const f = this.m`) would
// read the closure, not the instance's member. Same rule as a private static below.
if (member.kind === 'method' && !isCallee(p)) { refuse(node, `instance method \`${name}\` read as a value: the host carries a method as a forwarding closure, so only a call is in the closed set`); return; }
sites.push({ start: node.getStart(sf), end: node.getEnd(), text: HOST_PARAM, kind: 'member', member: name });
useInstance(name, isAssignmentTarget(p) ? 'write' : isCallee(p) ? 'call' : 'read');
ts.forEachChild(p, (c) => { if (c !== node) visit(c); });
Expand Down
5 changes: 5 additions & 0 deletions packages/metadata-protocol/scripts/host-split.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ line two \${k}\`;
namedHost() { const host = 1; return host; }
staticField() { return ${C}.LIMIT; }
staticAsValue() { const f = ${C}.helper; return f(1); }
probesMethod() { return { search: typeof this.read === 'function' }; }
methodAsValue() { const f = this.read; return f('k'); }
bareConstructor() { return this.constructor; }
undeclared() { return this.nope; }
*generator() { yield this.count; }
Expand Down Expand Up @@ -142,6 +144,9 @@ describe('host-split: the move', () => {
['namedHost', 'already named `host`'],
['staticField', 'private static `LIMIT` (field) read'],
['staticAsValue', 'private static `helper` (method) read as a value'],
// [#30518] `getDiscovery`'s shape: a host entry forwards a method through a closure, always a function.
['probesMethod', 'instance method `read` read as a value'],
['methodAsValue', 'instance method `read` read as a value'],
['bareConstructor', '`this.constructor` not followed by `.<static>`'],
['undeclared', '`this.nope` names no declared instance member'],
['generator', 'a generator method'],
Expand Down
Loading
Loading