Skip to content

Commit 6c9f359

Browse files
committed
fix(rest): the export audit event carries the query as read under readQuery, and two test files meet their gates
The rest slot census (rest-server-canonical-query-ast.test.ts) counts every `query:` line in data-action-routes.ts as a server-built QueryAST slot. The export audit event's `query` member (its type and its literal) read as two new slots, 8 against the pinned 6, though neither reaches the engine. The member is now `readQuery` in the door, the plugin-audit writer and their tests; the census is back to 6 with nothing re-pinned. rest-api-plugin-slot-lookups.test.ts, edited by this branch, reaches `@objectstack/objectql` through two route handlers' dynamic imports; a module-top side-effect import pays that load during collection (check:test-source-alias, "The source reach"). The export of an object name the registry does not know, past an export gate that admitted it, is pinned as built: one failed event, 404 OBJECT_NOT_FOUND. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwj6uu1L1hVh8hXHro
1 parent df6af0c commit 6c9f359

6 files changed

Lines changed: 47 additions & 11 deletions

File tree

‎packages/plugins/plugin-audit/src/data-export-audit.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ describe('[#30955] the export writer on the real `audit` slot', () => {
156156

157157
it('the row never carries an exported cell value: not the filter comparands, not the search term', async () => {
158158
await audit.recordDataExport(exportEvent({
159-
query: {
159+
readQuery: {
160160
filter: {
161161
name: SECRET,
162162
$or: [
@@ -216,7 +216,7 @@ describe('[#30955] the filter summary keeps names and operators, never a compara
216216
const { terms, omitted } = summarizeExportFilter(filter);
217217
expect(terms).toHaveLength(EXPORT_FILTER_SUMMARY_MAX_TERMS);
218218
expect(omitted).toBe(8);
219-
const row = buildDataExportAuditRow(exportEvent({ query: { filter } }));
219+
const row = buildDataExportAuditRow(exportEvent({ readQuery: { filter } }));
220220
expect(metadataOf(row).filter_terms_omitted).toBe(8);
221221
});
222222

@@ -251,7 +251,7 @@ describe('[#30955] a lost row is loud, once per cause, and never throws', () =>
251251
const logger = { error: vi.fn(), warn: vi.fn(), debug: vi.fn() };
252252
const writer = createDataExportAuditWriter({ getEngine: () => engine, logger });
253253

254-
await expect(writer.recordDataExport(exportEvent({ query: { filter: { name: SECRET }, search: SECRET } }))).resolves.toBeUndefined();
254+
await expect(writer.recordDataExport(exportEvent({ readQuery: { filter: { name: SECRET }, search: SECRET } }))).resolves.toBeUndefined();
255255
await expect(writer.recordDataExport(exportEvent())).resolves.toBeUndefined();
256256

257257
expect(engine.insert).toHaveBeenCalledTimes(2);

‎packages/plugins/plugin-audit/src/data-export-audit.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export interface DataExportAuditEvent {
105105
* The query as the door read it. Reduced to names and operators before it
106106
* reaches the row: the comparands and the search term are never recorded.
107107
*/
108-
query?: { filter?: unknown; search?: unknown; searchFields?: unknown };
108+
readQuery?: { filter?: unknown; search?: unknown; searchFields?: unknown };
109109
outcome: DataExportOutcome;
110110
/** On a failure: the HTTP status the door answered. */
111111
status?: number;
@@ -310,7 +310,7 @@ export function buildDataExportAuditRow(
310310
const ctx = event.context ?? {};
311311
const userId = nonEmpty(ctx.userId);
312312
const performedBy = nonEmpty(ctx.performedBy?.clientId);
313-
const query = event.query ?? {};
313+
const query = event.readQuery ?? {};
314314

315315
const limit = wholeNumber(event.limit);
316316
const requestedLimit = wholeNumber(event.requestedLimit);

‎packages/rest/src/data-action-routes.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -371,9 +371,11 @@ async function assertImportObjectExists(p: any, objectName: string, environmentI
371371
*
372372
* One event per export that cleared every gate and began reading, whether the
373373
* stream completed or failed; a refused request is no export and hands none.
374-
* `query` is the query AS READ: the writer reduces it to field names and
374+
* `readQuery` is the query AS READ: the writer reduces it to field names and
375375
* operators before anything reaches the ledger, and records only that a search
376-
* term applied, never the term.
376+
* term applied, never the term. It is not spelled `query` on purpose: it is no
377+
* query handed to the engine, and `rest-server-canonical-query-ast.test.ts`
378+
* censuses every `query:` line in this file as a server-built QueryAST slot.
377379
*/
378380
interface DataExportAuditEvent {
379381
context: unknown;
@@ -384,7 +386,7 @@ interface DataExportAuditEvent {
384386
requestedLimit: number;
385387
limitCap: number;
386388
truncated: boolean;
387-
query: { filter?: unknown; search?: string; searchFields?: string[] };
389+
readQuery: { filter?: unknown; search?: string; searchFields?: string[] };
388390
outcome: 'succeeded' | 'failed';
389391
status?: number;
390392
errorCode?: string;
@@ -1602,7 +1604,7 @@ export function registerDataActionEndpoints(this: DataActionRouteHost, basePath:
16021604
limit,
16031605
requestedLimit,
16041606
limitCap: HARD_CAP,
1605-
query: {
1607+
readQuery: {
16061608
...(filter !== undefined ? { filter } : {}),
16071609
...(search ? { search } : {}),
16081610
...(search && searchFields ? { searchFields } : {}),

‎packages/rest/src/export-integration.test.ts‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,7 @@ describe('[#30955] export route — the ledger hears of every export it streams'
801801
limitCap: 50_000,
802802
truncated: false,
803803
outcome: 'succeeded',
804-
query: {},
804+
readQuery: {},
805805
});
806806
expect(events[0].context).toMatchObject({ userId: 'test-user', tenantId: 'org_a' });
807807
});
@@ -834,7 +834,7 @@ describe('[#30955] export route — the ledger hears of every export it streams'
834834
const filter = { done: true };
835835
await run('task', { format: 'csv', filter: JSON.stringify(filter), search: '代码', searchFields: 'title' });
836836
expect(events).toHaveLength(1);
837-
expect(events[0].query).toEqual({ filter, search: '代码', searchFields: ['title'] });
837+
expect(events[0].readQuery).toEqual({ filter, search: '代码', searchFields: ['title'] });
838838
expect(events[0].rowCount).toBe(1);
839839
});
840840

@@ -869,6 +869,29 @@ describe('[#30955] export route — the ledger hears of every export it streams'
869869
expect(events[0].errorCode).toBe('INVALID_FIELD');
870870
});
871871

872+
it('an export of an object name the registry does not know, past an export gate that let it through, hands ONE failed event (404 OBJECT_NOT_FOUND)', async () => {
873+
// Pinned as built, for the review to judge: the gates passed and the read
874+
// began, so by the door's one rule this is an export that failed, not a
875+
// refusal. Only a caller the export gate admits for that name reaches it.
876+
const gateAsked: string[] = [];
877+
const { events, run } = await bootAudited({
878+
canExport: async (object) => { gateAsked.push(object); return true; },
879+
});
880+
const answer = await run('no_such_object', { format: 'csv' });
881+
expect(answer.status).toBe(404);
882+
expect(answer.json?.code ?? answer.json?.error?.code).toBe('OBJECT_NOT_FOUND');
883+
expect(gateAsked).toEqual(['no_such_object']);
884+
expect(events).toHaveLength(1);
885+
expect(events[0]).toMatchObject({
886+
objectName: 'no_such_object',
887+
outcome: 'failed',
888+
status: 404,
889+
errorCode: 'OBJECT_NOT_FOUND',
890+
rowCount: 0,
891+
truncated: false,
892+
});
893+
});
894+
872895
it('a sink that throws changes nothing about the file the caller gets', async () => {
873896
const { run } = await bootAudited({ sink: { recordDataExport: async () => { throw new Error('ledger down'); } } });
874897
const answer = await run('task', { format: 'csv' });

‎packages/rest/src/rest-api-plugin-slot-lookups.test.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@
6060
*/
6161

6262
import { describe, it, expect, vi } from 'vitest';
63+
// The real `RestServer` this file extends reaches `@objectstack/objectql` only
64+
// through a dynamic import inside two route handlers (`meta-object-state-routes.ts`,
65+
// `meta-record-currency-backfill-route.ts`). A module-top import pays that load
66+
// during COLLECTION, which vitest clocks against nothing, so no case's timeout
67+
// window can ever hold it (`scripts/check-test-source-alias.mjs`, "The source
68+
// reach"). The source keeps its dynamic import; this only decides where the
69+
// first load is paid.
70+
import '@objectstack/objectql';
6371

6472
const captured = vi.hoisted(() => ({ ctorArgs: [] as unknown[][] }));
6573

‎packages/rest/src/rest-server-canonical-query-ast.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,9 @@ describe('[#16638] §1 CONTROLS on the census instrument itself', () => {
453453
// [#27627] The sum unchanged by the app metadata bundle (no slot).
454454
// [#28470] The sum 5 → 6: the record page's stream reads (`readStream`).
455455
// [#30675] The sum unchanged by the record-currency code backfill (no slot).
456+
// [#30955] The sum unchanged by the export door's audit event: it hands
457+
// the plugin-audit writer the query as read under `readQuery`, which is
458+
// not a QueryAST slot and reaches no engine.
456459
expect(Object.keys(REGISTRAR_QUERY_SLOT_FLOORS).sort()).toEqual([...REST_SERVER_REGISTRAR_FILES].sort());
457460
expect(CENSUS.filter(({ file }) => REGISTRAR_SOURCES.has(file)).map(({ file }) => file))
458461
.toEqual([...REST_SERVER_REGISTRAR_FILES]);

0 commit comments

Comments
 (0)