Skip to content

Commit e467806

Browse files
committed
test(plugin-approvals): the request-read engine double holds the caller's bound and refuses operators it does not implement
check:objectql-double-limit and check:where-matcher judge every new find double. The double now applies `limit` after the filter, by presence, and copies only the bounded rows. Its matcher lives at module scope, where the matcher gate can lift it, and throws on a combinator or operator outside equality, `$in`, `$or` and `$and`. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP
1 parent 51d33df commit e467806

1 file changed

Lines changed: 33 additions & 16 deletions

File tree

‎packages/plugins/plugin-approvals/src/sys-approval-request-page.test.ts‎

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,26 @@ const SNAPSHOT = { id: 'exp_1', title: 'Q1 travel', amount: 1200 };
122122
const SUBMITTER = 'employee_7';
123123

124124
/**
125-
* One request, read the way the request page loads its record. The console
126-
* routes that `findOne` to `GET /approvals/requests/:id` (objectui's
127-
* `routeApprovalRecordReads`), whose handler answers `ApprovalService.getRequest`'s
128-
* row as it stands; this is that call, as the request's submitter, over a
129-
* find-only engine double holding the stored row.
125+
* The `where` shapes the request read sends: equality, `$in`, `$or`, `$and`.
126+
* Any other operator or combinator is REFUSED, so a read this double cannot
127+
* answer fails the test instead of matching nothing.
130128
*/
131-
async function servedRequestRow(): Promise<AnyRec> {
129+
function matchesWhere(row: AnyRec, where: AnyRec | undefined): boolean {
130+
return Object.entries(where ?? {}).every(([k, v]) => {
131+
if (k === '$or') return (v as AnyRec[]).some((w) => matchesWhere(row, w));
132+
if (k === '$and') return (v as AnyRec[]).every((w) => matchesWhere(row, w));
133+
if (k.startsWith('$')) throw new Error(`engine double: unsupported combinator ${k}`);
134+
if (v != null && typeof v === 'object') {
135+
const ops = Object.keys(v);
136+
if (ops.length === 1 && ops[0] === '$in') return (v.$in as unknown[]).includes(row[k]);
137+
throw new Error(`engine double: unsupported operator ${ops.join(', ')} on ${k}`);
138+
}
139+
return row[k] === v;
140+
});
141+
}
142+
143+
/** A find-only engine double over one stored request and its subject record. */
144+
function storedRequestEngine() {
132145
const tables: Record<string, AnyRec[]> = {
133146
sys_approval_request: [{
134147
id: 'req_1',
@@ -145,19 +158,23 @@ async function servedRequestRow(): Promise<AnyRec> {
145158
}],
146159
expense_report: [{ ...SNAPSHOT, organization_id: 't1' }],
147160
};
148-
const matches = (row: AnyRec, where: AnyRec | undefined): boolean =>
149-
Object.entries(where ?? {}).every(([k, v]) => {
150-
if (k === '$or') return (v as AnyRec[]).some((w) => matches(row, w));
151-
if (k === '$and') return (v as AnyRec[]).every((w) => matches(row, w));
152-
if (v != null && typeof v === 'object' && '$in' in v) return (v.$in as unknown[]).includes(row[k]);
153-
return row[k] === v;
154-
});
155-
const engine = {
161+
return {
156162
async find(object: string, options?: AnyRec) {
157-
return (tables[object] ?? []).filter((r) => matches(r, options?.where ?? options?.filter)).map((r) => ({ ...r }));
163+
const rows = (tables[object] ?? []).filter((r) => matchesWhere(r, options?.where ?? options?.filter));
164+
const page = typeof options?.limit === 'number' ? rows.slice(0, options.limit) : rows;
165+
return page.map((r) => ({ ...r }));
158166
},
159167
};
160-
const row = await new ApprovalService({ engine: engine as any })
168+
}
169+
170+
/**
171+
* One request, read the way the request page loads its record. The console
172+
* routes that `findOne` to `GET /approvals/requests/:id` (objectui's
173+
* `routeApprovalRecordReads`), whose handler answers `ApprovalService.getRequest`'s
174+
* row as it stands; this is that call, as the request's submitter.
175+
*/
176+
async function servedRequestRow(): Promise<AnyRec> {
177+
const row = await new ApprovalService({ engine: storedRequestEngine() as any })
161178
.getRequest('req_1', { userId: SUBMITTER, tenantId: 't1', positions: [], permissions: [] } as any);
162179
expect(row, 'the submitter reads their own request').not.toBeNull();
163180
return row as AnyRec;

0 commit comments

Comments
 (0)