Skip to content

Commit 55ec8fe

Browse files
committed
test(objectql): type the engine options in the can() value-site pin rather than erasing them
Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9e622fb commit 55ec8fe

1 file changed

Lines changed: 25 additions & 24 deletions

File tree

‎packages/objectql/src/engine-formula-default-permission.test.ts‎

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
*/
2929

3030
import { describe, it, expect, beforeEach } from 'vitest';
31+
import type { ExecutionContext } from '@objectstack/spec/kernel';
3132
import { ObjectQL } from './engine.js';
3233
import { ValidationError } from './validation/record-validator.js';
3334

@@ -127,8 +128,8 @@ function countingResolver(answer: unknown | (() => unknown)) {
127128
return { fn, asks };
128129
}
129130

130-
const ACTING = { userId: 'u1', positions: ['sales_rep'] } as any;
131-
const SYSTEM = { isSystem: true } as any;
131+
const ACTING: ExecutionContext = { userId: 'u1', positions: ['sales_rep'] };
132+
const SYSTEM: ExecutionContext = { isSystem: true };
132133
/** The `objects` slot of `/auth/me/permissions`: `edit` granted on crm_account, `delete` not. */
133134
const MAP = { crm_account: { allowRead: true, allowEdit: true } };
134135
const BOOM = Object.assign(new Error('permission store unreachable'), { code: 'AUTHZ_STORE_UNAVAILABLE', status: 503 });
@@ -260,28 +261,28 @@ describe('#20082 — formula fields and CEL defaults answer `can` from the secur
260261

261262
it('formula field on find — one resolution for the whole result set', async () => {
262263
seed(4);
263-
const rows = await engine.find('crm_case', { context: ACTING } as any) as Array<Record<string, unknown>>;
264+
const rows = await engine.find('crm_case', { context: ACTING }) as Array<Record<string, unknown>>;
264265
expect(rows).toHaveLength(4);
265266
expectFormulaCells(rows, 1);
266267
expect(asks).toHaveLength(row.resolver ? 1 : 0);
267268
});
268269

269270
it('formula field on findOne', async () => {
270271
seed(2);
271-
const rec = await engine.findOne('crm_case', { where: { id: 'c1' }, context: ACTING } as any) as Record<string, unknown>;
272+
const rec = await engine.findOne('crm_case', { where: { id: 'c1' }, context: ACTING }) as Record<string, unknown>;
272273
expectFormulaCells([rec], 1);
273274
expect(asks).toHaveLength(row.resolver ? 1 : 0);
274275
});
275276

276277
it('formula field on the update echo', async () => {
277278
seed(1);
278-
const echo = await engine.update('crm_case', { subject: 'renamed' }, { where: { id: 'c0' }, context: ACTING } as any) as Record<string, unknown>;
279+
const echo = await engine.update('crm_case', { subject: 'renamed' }, { where: { id: 'c0' }, context: ACTING }) as Record<string, unknown>;
279280
expectFormulaCells([echo], 1);
280281
expect(asks).toHaveLength(row.resolver ? 1 : 0);
281282
});
282283

283284
it('CEL defaultValue on insert — and the insert echo\'s formula reuses the same resolution', async () => {
284-
const write = engine.insert('crm_case', { subject: 'n' }, { context: ACTING } as any);
285+
const write = engine.insert('crm_case', { subject: 'n' }, { context: ACTING });
285286
if (row.defaulted === 'refused') {
286287
// Fail CLOSED with the resolution's own error, untouched: ⛔ not read
287288
// as "no grants" (which would STORE `false`), and nothing written.
@@ -315,7 +316,7 @@ describe('#20082 — formula fields and CEL defaults answer `can` from the secur
315316
});
316317

317318
it('a `required` field defaulted by `can`', async () => {
318-
const write = engine.insert('crm_gate', { subject: 'g' }, { context: ACTING } as any);
319+
const write = engine.insert('crm_gate', { subject: 'g' }, { context: ACTING });
319320
if (row.defaulted === 'refused') {
320321
expect(await refusal(write)).toBe(BOOM);
321322
expect(created('crm_gate')).toHaveLength(0);
@@ -344,14 +345,14 @@ describe('#20082 — formula fields and CEL defaults answer `can` from the secur
344345
fields: { flag: { type: 'boolean', required: true, defaultValue: cel("current_user.can('crm_account', 'delete')") } },
345346
} as any, 'test-package');
346347
engine.registerEffectiveObjectPermissionsResolver(countingResolver(MAP).fn);
347-
await engine.insert('crm_gate_denied', {}, { context: ACTING } as any);
348+
await engine.insert('crm_gate_denied', {}, { context: ACTING });
348349
expect(created('crm_gate_denied')[0].data.flag).toBe(false);
349350
});
350351

351352
it('ONE resolution for a write that needs the map three ways: default, option gate and response formula', async () => {
352353
const r = countingResolver(MAP);
353354
engine.registerEffectiveObjectPermissionsResolver(r.fn);
354-
const echo = await engine.insert('crm_case', { subject: 'x', stage: 'escalated' }, { context: ACTING } as any) as Record<string, unknown>;
355+
const echo = await engine.insert('crm_case', { subject: 'x', stage: 'escalated' }, { context: ACTING }) as Record<string, unknown>;
355356
expect(created('crm_case')[0].data).toMatchObject({ stage: 'escalated', edit_flag: true, delete_flag: false });
356357
expect(echo.may_edit_account).toBe(true);
357358
expect(r.asks).toHaveLength(1);
@@ -360,49 +361,49 @@ describe('#20082 — formula fields and CEL defaults answer `can` from the secur
360361
it('ONE resolution for a batch insert of N rows, and for a by-id update picking a gated option', async () => {
361362
const r = countingResolver(MAP);
362363
engine.registerEffectiveObjectPermissionsResolver(r.fn);
363-
await engine.insert('crm_case', Array.from({ length: 6 }, (_, i) => ({ subject: `b${i}` })) as any, { context: ACTING } as any);
364+
await engine.insert('crm_case', Array.from({ length: 6 }, (_, i) => ({ subject: `b${i}` })) as any, { context: ACTING });
364365
expect(created('crm_case')).toHaveLength(6);
365366
expect(created('crm_case').every((w) => w.data.edit_flag === true && w.data.delete_flag === false)).toBe(true);
366367
expect(r.asks).toHaveLength(1);
367368

368-
await engine.update('crm_case', { stage: 'escalated' }, { where: { id: 'r_1' }, context: ACTING } as any);
369+
await engine.update('crm_case', { stage: 'escalated' }, { where: { id: 'r_1' }, context: ACTING });
369370
expect(r.asks).toHaveLength(2);
370371
});
371372

372373
it('validate() previews the defaults and the option gate with ONE resolution', async () => {
373374
const r = countingResolver(MAP);
374375
engine.registerEffectiveObjectPermissionsResolver(r.fn);
375-
const preview = await engine.validate('crm_gate', [{ subject: 'a' }, { subject: 'b' }], { mode: 'insert', context: ACTING } as any);
376+
const preview = await engine.validate('crm_gate', [{ subject: 'a' }, { subject: 'b' }], { mode: 'insert', context: ACTING });
376377
expect(preview.valid).toBe(true);
377378
expect(r.asks).toHaveLength(1);
378379
});
379380

380381
it('validate() rejects the same way the write does when a row\'s `can` default needed a failed resolution', async () => {
381382
engine.registerEffectiveObjectPermissionsResolver(countingResolver(() => { throw BOOM; }).fn);
382383
await expect(
383-
engine.validate('crm_gate', [{ subject: 'a' }], { mode: 'insert', context: ACTING } as any),
384+
engine.validate('crm_gate', [{ subject: 'a' }], { mode: 'insert', context: ACTING }),
384385
).rejects.toBe(BOOM);
385386
// A row that supplies the field previews without asking.
386-
const supplied = await engine.validate('crm_gate', [{ subject: 'a', flag: false }], { mode: 'insert', context: ACTING } as any);
387+
const supplied = await engine.validate('crm_gate', [{ subject: 'a', flag: false }], { mode: 'insert', context: ACTING });
387388
expect(supplied.valid).toBe(true);
388389
});
389390

390391
it('never cached across operations: two finds ask twice', async () => {
391392
seed(3);
392393
const r = countingResolver(MAP);
393394
engine.registerEffectiveObjectPermissionsResolver(r.fn);
394-
await engine.find('crm_case', { context: ACTING } as any);
395-
await engine.find('crm_case', { context: ACTING } as any);
395+
await engine.find('crm_case', { context: ACTING });
396+
await engine.find('crm_case', { context: ACTING });
396397
expect(r.asks).toHaveLength(2);
397398
});
398399

399400
it('control: no `can` anywhere ⇒ never asks, even a resolver that would THROW, and no warn', async () => {
400401
const r = countingResolver(() => { throw BOOM; });
401402
engine.registerEffectiveObjectPermissionsResolver(r.fn);
402-
const echo = await engine.insert('crm_note', { body: 'hi' }, { context: ACTING } as any) as Record<string, unknown>;
403+
const echo = await engine.insert('crm_note', { body: 'hi' }, { context: ACTING }) as Record<string, unknown>;
403404
expect(echo.shout).toBe('hi!');
404405
expect(created('crm_note')[0].data.author).toBe('u1');
405-
const rows = await engine.find('crm_note', { context: ACTING } as any) as Array<Record<string, unknown>>;
406+
const rows = await engine.find('crm_note', { context: ACTING }) as Array<Record<string, unknown>>;
406407
expect(rows[0].shout).toBe('hi!');
407408
expect(r.asks).toHaveLength(0);
408409
expect(formulaWarns()).toHaveLength(0);
@@ -414,7 +415,7 @@ describe('#20082 — formula fields and CEL defaults answer `can` from the secur
414415
// The formula leg of the echo still reads null (with its warn): only the
415416
// default path was spared, because only the default path did not need it.
416417
await expect(
417-
engine.insert('crm_case', { subject: 's', edit_flag: true, delete_flag: false }, { context: ACTING } as any),
418+
engine.insert('crm_case', { subject: 's', edit_flag: true, delete_flag: false }, { context: ACTING }),
418419
).resolves.toBeTruthy();
419420
expect(created('crm_case')[0].data).toMatchObject({ edit_flag: true, delete_flag: false });
420421
expect(r.asks).toHaveLength(1); // the echo's formulas — the default path asked nothing
@@ -424,8 +425,8 @@ describe('#20082 — formula fields and CEL defaults answer `can` from the secur
424425
seed(2);
425426
const r = countingResolver(MAP);
426427
engine.registerEffectiveObjectPermissionsResolver(r.fn);
427-
await engine.find('crm_case', { context: SYSTEM } as any);
428-
await engine.find('crm_case', {} as any);
428+
await engine.find('crm_case', { context: SYSTEM });
429+
await engine.find('crm_case', {});
429430
expect(r.asks).toHaveLength(0);
430431
expect(formulaWarns()).toHaveLength(0);
431432
});
@@ -435,7 +436,7 @@ describe('#20082 — formula fields and CEL defaults answer `can` from the secur
435436
const out = await engine.insertMany('crm_case', [
436437
{ subject: 'needs' },
437438
{ subject: 'supplies', edit_flag: false, delete_flag: true },
438-
], { context: ACTING } as any);
439+
], { context: ACTING });
439440
expect(out[0].ok).toBe(false);
440441
expect((out[0] as { error: unknown }).error).toBe(BOOM);
441442
expect(out[1].ok).toBe(true);
@@ -444,13 +445,13 @@ describe('#20082 — formula fields and CEL defaults answer `can` from the secur
444445

445446
it('a map that is not the published shape fails closed the same way (formula\'s door)', async () => {
446447
engine.registerEffectiveObjectPermissionsResolver(countingResolver({ crm_account: true }).fn);
447-
const err = await refusal(engine.insert('crm_case', { subject: 'n' }, { context: ACTING } as any));
448+
const err = await refusal(engine.insert('crm_case', { subject: 'n' }, { context: ACTING }));
448449
expect(err).toBeInstanceOf(TypeError);
449450
expect(String(err.message)).toContain("the entry for 'crm_account' is not an EffectiveObjectPermission");
450451
expect(created('crm_case')).toHaveLength(0);
451452

452453
seed(1);
453-
const rows = await engine.find('crm_case', { context: ACTING } as any) as Array<Record<string, unknown>>;
454+
const rows = await engine.find('crm_case', { context: ACTING }) as Array<Record<string, unknown>>;
454455
expect(rows[0].may_edit_account).toBeNull();
455456
expect(formulaWarns().at(-1)?.meta).toMatchObject({ reason: 'permission-resolution-failed' });
456457
});

0 commit comments

Comments
 (0)