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/rest-server-canonical-query-ast.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@objectstack/rest": patch
---

The REST server's own `findData` calls now build the canonical QueryAST instead of an undeclared wire dialect, and the helper that erased the type on that one slot is gone.

Four server-built query literals in `rest-server.ts` — the import-job loader, the import-job listing, the export chunk loop and the public reference picker — spelled their query in transport aliases (`$filter`, `$top`, `$skip`, `$orderby`, `$expand`, plus the bare `filters` / `select` / `sort`). None of those spellings is declared by `QuerySchema`, so three of them were routed through a `wireDialectQuery` helper that cast the `query` member to `FindDataRequest['query']`, and the fourth escaped the compiler entirely because its protocol handle was typed `any`. All four now spell `object` / `where` / `orderBy` / `limit` / `offset` / `fields` / `expand`, so the slot compiles against the declared contract like every other member of the request, and the helper is retired.

**No behaviour moves, and that is measured rather than asserted.** `@objectstack/metadata-protocol`'s `findData` folds every alias onto its canonical key by the spec's own table (`RPC_QUERY_ALIAS_SLOTS`) and moves the value verbatim, so both spellings reach `engine.find` as the same option bag. `rest-server-canonical-query-ast.test.ts` drives all four before/after pairs through the real normalizer and asserts that equality, and reads the source to keep the erasure retired — a cast compiles, so a type-check alone could not hold this ground.

**Nothing is removed from the published surface.** `wireDialectQuery` was a module-local `const` in `rest-server.ts`: it carried no `export` keyword, `packages/rest/src/index.ts` never named it, and it appeared in no other file in the tree. Deleting it moves no exported symbol, which is why this is a patch.

**What this change deliberately does NOT do:** it does not touch how the HTTP door treats a *caller's* query. The wire aliases stay accepted on `GET /data/:object` exactly as before — declaring them in the spec's alias table is a separate piece of work — and `GET /data/:object` still forwards the caller's own querystring bag untouched.
23 changes: 17 additions & 6 deletions packages/rest/src/public-form-lookup-picker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,16 +203,27 @@ describe('#7467 a spec-valid stored form carrying a publicPicker reaches the loo
// the declared object override, the declared cap, the declared filter
// rows ahead of the visitor's search predicate, id + displayFields
// projection, offset pinned to 0 (no anonymous pagination).
//
// [#16337] The KEYS are the canonical QueryAST ones (`where` / `fields`
// / `orderBy`); until then the route spelled them `filters` / `select` /
// `sort`, wire aliases the normalizer folds onto exactly these. The
// VALUES are byte-identical across that rewrite, which is the point —
// and note what `where` carries: `ViewFilterRule` rows, the dialect
// `FormFieldPublicPickerSchema.filter` declares, NOT a
// `FilterCondition`. `findData` is stubbed in this suite, so it never
// meets the ingress's verdict on that value; the real normalizer
// refuses it (#16581) — ⛔ do not "repair" it by editing this
// expectation.
expect(findData).toHaveBeenCalledTimes(1);
const call = findData.mock.calls[0][0];
expect(call.object).toBe('sys_user');
expect(call.query.limit).toBe(10);
expect(call.query.offset).toBe(0);
expect(call.query.select).toEqual(['id', 'name', 'email']);
expect(call.query.fields).toEqual(['id', 'name', 'email']);
// [#7485] Ordering is fixed, not authorable: first display field,
// ascending. The route's `picker.sort ??` read is retired.
expect(call.query.sort).toEqual([{ field: 'name', order: 'asc' }]);
expect(call.query.filters).toEqual([
expect(call.query.orderBy).toEqual([{ field: 'name', order: 'asc' }]);
expect(call.query.where).toEqual([
{ field: 'is_active', operator: 'equals', value: true },
{ field: 'name', operator: 'contains', value: 'ad' },
]);
Expand Down Expand Up @@ -299,7 +310,7 @@ describe('#7485 publicPicker.sort is retired — not declarable, and not read',
// The stored `{ field: 'email', order: 'desc' }` reaches `findData`
// nowhere: the fixed default is the only ordering the route composes.
expect(findData).toHaveBeenCalledTimes(1);
expect(findData.mock.calls[0][0].query.sort).toEqual([{ field: 'name', order: 'asc' }]);
expect(findData.mock.calls[0][0].query.orderBy).toEqual([{ field: 'name', order: 'asc' }]);
});

it('…and the fixed sort tracks displayFields[0], including the no-displayFields default', async () => {
Expand All @@ -310,15 +321,15 @@ describe('#7485 publicPicker.sort is retired — not declarable, and not read',
const stored = await persistedBody(studioForm([{ field: 'owner', publicPicker: { object: 'sys_user' } }]));
const { findData, lookup } = routesOver(stored, []);
await lookup.handler({ params: { slug: 'contact', field: 'owner' }, query: {} } as any, mockRes());
expect(findData.mock.calls[0][0].query.sort).toEqual([{ field: 'name', order: 'asc' }]);
expect(findData.mock.calls[0][0].query.orderBy).toEqual([{ field: 'name', order: 'asc' }]);

const stored2 = await persistedBody(studioForm([{
field: 'owner',
publicPicker: { displayFields: ['email', 'name'], object: 'sys_user' },
}]));
const second = routesOver(stored2, []);
await second.lookup.handler({ params: { slug: 'contact', field: 'owner' }, query: {} } as any, mockRes());
expect(second.findData.mock.calls[0][0].query.sort).toEqual([{ field: 'email', order: 'asc' }]);
expect(second.findData.mock.calls[0][0].query.orderBy).toEqual([{ field: 'email', order: 'asc' }]);
});
});

Expand Down
Loading
Loading