@aventara/core 1.0.0-rc.3 → 1.0.0-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +231 -1
  2. package/ERRORS.md +6 -6
  3. package/README.md +76 -12
  4. package/dist/adapters/adapter-field.d.ts +8 -0
  5. package/dist/adapters/adapter-identifier.d.ts +9 -0
  6. package/dist/adapters/adapter-identifier.js +6 -1
  7. package/dist/adapters/adapter-operation.d.ts +13 -0
  8. package/dist/compiler/contract.compiler.d.ts +6 -0
  9. package/dist/compiler/contract.compiler.js +99 -15
  10. package/dist/config/computed-fields.js +2 -9
  11. package/dist/config/config-diagnostic.guidance.js +3 -3
  12. package/dist/config/config.resolver.d.ts +14 -0
  13. package/dist/config/config.resolver.js +31 -0
  14. package/dist/config/config.validator.js +82 -0
  15. package/dist/config/exact-framework-config.d.ts +5 -0
  16. package/dist/config/framework-config.d.ts +30 -1
  17. package/dist/contracts/client-contract-structure.validator.d.ts +8 -0
  18. package/dist/contracts/client-contract-structure.validator.js +6 -1
  19. package/dist/contracts/contract.d.ts +15 -1
  20. package/dist/contracts/field.d.ts +28 -0
  21. package/dist/contracts/scalar-value-type.d.ts +3 -1
  22. package/dist/framework/contract-view.d.ts +41 -10
  23. package/dist/framework/framework-construction.error.d.ts +2 -1
  24. package/dist/framework/framework-construction.error.js +11 -1
  25. package/dist/framework/framework.d.ts +4 -4
  26. package/dist/framework/framework.factory.js +39 -11
  27. package/dist/framework/operation-facade.d.ts +29 -10
  28. package/dist/framework/operation-facade.js +2 -1
  29. package/dist/index.d.ts +10 -4
  30. package/dist/internal.d.ts +2 -2
  31. package/dist/internal.js +2 -0
  32. package/dist/operations/operation-arguments.d.ts +44 -28
  33. package/dist/operations/operation-call.d.ts +9 -5
  34. package/dist/operations/operation-result.d.ts +6 -4
  35. package/dist/operations/operation-validator-core.js +6 -0
  36. package/dist/policy/policy-admission.d.ts +22 -0
  37. package/dist/policy/policy-admission.js +409 -0
  38. package/dist/policy/policy-after-update.runtime.d.ts +29 -0
  39. package/dist/policy/policy-after-update.runtime.js +132 -0
  40. package/dist/policy/policy-condition.d.ts +15 -0
  41. package/dist/policy/policy-condition.js +36 -0
  42. package/dist/policy/policy-engine.d.ts +118 -0
  43. package/dist/policy/policy-engine.js +1 -0
  44. package/dist/policy/policy-nested-write.runtime.d.ts +38 -0
  45. package/dist/policy/policy-nested-write.runtime.js +453 -0
  46. package/dist/policy/policy-read.runtime.d.ts +81 -0
  47. package/dist/policy/policy-read.runtime.js +503 -0
  48. package/dist/policy/policy-request.d.ts +41 -0
  49. package/dist/policy/policy-request.js +63 -0
  50. package/dist/policy/policy-session.d.ts +93 -0
  51. package/dist/policy/policy-session.js +235 -0
  52. package/dist/policy/policy-startup.validator.d.ts +8 -0
  53. package/dist/policy/policy-startup.validator.js +104 -0
  54. package/dist/policy/policy-write.runtime.d.ts +37 -0
  55. package/dist/policy/policy-write.runtime.js +409 -0
  56. package/dist/prebuilt/core-version.generated.d.ts +2 -0
  57. package/dist/prebuilt/core-version.generated.js +1 -0
  58. package/dist/prebuilt/prebuilt-changes.d.ts +28 -0
  59. package/dist/prebuilt/prebuilt-changes.js +65 -0
  60. package/dist/prebuilt/prebuilt-contracts.d.ts +40 -0
  61. package/dist/prebuilt/prebuilt-contracts.emitter.d.ts +16 -0
  62. package/dist/prebuilt/prebuilt-contracts.emitter.js +50 -0
  63. package/dist/prebuilt/prebuilt-contracts.js +1 -0
  64. package/dist/prebuilt/prebuilt-contracts.loader.d.ts +21 -0
  65. package/dist/prebuilt/prebuilt-contracts.loader.js +77 -0
  66. package/dist/prebuilt/prebuilt-inputs.fingerprint.d.ts +16 -0
  67. package/dist/prebuilt/prebuilt-inputs.fingerprint.js +192 -0
  68. package/dist/protocol/absent-route.responder.d.ts +4 -4
  69. package/dist/protocol/http-request.decoder.d.ts +2 -2
  70. package/dist/protocol/http-request.reader.d.ts +2 -2
  71. package/dist/protocol/protocol-headers.d.ts +3 -3
  72. package/dist/runtime/computed-read-runtime.d.ts +6 -1
  73. package/dist/runtime/computed-read-runtime.js +226 -52
  74. package/dist/runtime/context-executor.runtime.d.ts +2 -2
  75. package/dist/runtime/context-executor.runtime.js +4 -2
  76. package/dist/runtime/create-requirement.js +1 -1
  77. package/dist/runtime/diagnostics-reporter.d.ts +5 -5
  78. package/dist/runtime/execution-options.d.ts +8 -0
  79. package/dist/runtime/framework-warning.emitter.d.ts +6 -0
  80. package/dist/runtime/framework-warning.emitter.js +8 -0
  81. package/dist/runtime/level-lifecycle.runtime.js +3 -2
  82. package/dist/runtime/operation-code.guidance.js +3 -3
  83. package/dist/runtime/operation-request.d.ts +16 -9
  84. package/dist/runtime/operation-runtime.d.ts +15 -7
  85. package/dist/runtime/operation-runtime.js +139 -14
  86. package/dist/runtime/owned-execution.registry.d.ts +2 -2
  87. package/dist/runtime/readonly-value.d.ts +5 -1
  88. package/dist/runtime/readonly-value.js +67 -28
  89. package/dist/runtime/referential-action.d.ts +1 -1
  90. package/dist/transactions/sql-statement.d.ts +2 -2
  91. package/dist/transactions/transaction-plan-assembler.d.ts +2 -2
  92. package/dist/transactions/transaction-plan-validator.d.ts +5 -5
  93. package/dist/transactions/transaction-request.d.ts +8 -8
  94. package/dist/transactions/transaction-runtime.d.ts +5 -2
  95. package/dist/transactions/transaction-runtime.js +33 -6
  96. package/package.json +2 -2
@@ -0,0 +1,409 @@
1
+ import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
2
+ import { defaultProjectionFields } from "../runtime/computed-read-runtime.js";
3
+ import { valueAt, writeRecordPathsOf } from "../runtime/field-stage.runtime.js";
4
+ import { isRelationMutationDirective, RELATION_MUTATION_RECORD_SITES, } from "../runtime/mutation-planner.js";
5
+ import { operationFailure } from "../runtime/operation-response.js";
6
+ import { everyOf, noneOf, someOf, } from "./policy-condition.js";
7
+ import { fieldAccess, } from "./policy-session.js";
8
+ export function policyRefusal(message) {
9
+ return operationFailure("A4002", { message });
10
+ }
11
+ export function policyActionsOf(family) {
12
+ switch (family) {
13
+ case "find":
14
+ return ["read"];
15
+ case "create":
16
+ return ["create"];
17
+ case "update":
18
+ return ["update"];
19
+ case "delete":
20
+ return ["delete"];
21
+ case "upsert":
22
+ return ["create", "update"];
23
+ }
24
+ }
25
+ function isRecord(value) {
26
+ return value !== null && typeof value === "object" && !Array.isArray(value);
27
+ }
28
+ class FieldWalk {
29
+ active;
30
+ used = new Map();
31
+ nodes = [];
32
+ constructor(active) {
33
+ this.active = active;
34
+ }
35
+ use(resource, field) {
36
+ let fields = this.used.get(resource);
37
+ if (fields === undefined) {
38
+ fields = new Set();
39
+ this.used.set(resource, fields);
40
+ }
41
+ fields.add(field);
42
+ }
43
+ node(resource, where) {
44
+ const node = { resource, where, fields: new Set() };
45
+ this.nodes.push(node);
46
+ return node;
47
+ }
48
+ relation(resource, name) {
49
+ const field = lookupField(lookupResource(this.active, resource), name);
50
+ return field?.kind === "relation"
51
+ ? field
52
+ : undefined;
53
+ }
54
+ defaults(resource) {
55
+ for (const field of defaultProjectionFields(this.active, resource)) {
56
+ this.use(resource, field);
57
+ }
58
+ }
59
+ projection(resource, args) {
60
+ if (Array.isArray(args.select)) {
61
+ for (const entry of args.select) {
62
+ if (entry === "$count") {
63
+ continue;
64
+ }
65
+ if (entry === "$defaults") {
66
+ this.defaults(resource);
67
+ continue;
68
+ }
69
+ this.projectionEntry(resource, entry);
70
+ }
71
+ return;
72
+ }
73
+ this.defaults(resource);
74
+ if (Array.isArray(args.include)) {
75
+ for (const entry of args.include) {
76
+ this.projectionEntry(resource, entry);
77
+ }
78
+ }
79
+ }
80
+ projectionEntry(resource, entry) {
81
+ if (typeof entry === "string") {
82
+ this.use(resource, entry);
83
+ const relation = this.relation(resource, entry);
84
+ if (relation !== undefined) {
85
+ this.defaults(relation.target);
86
+ }
87
+ return;
88
+ }
89
+ if (!isRecord(entry)) {
90
+ return;
91
+ }
92
+ for (const [name, value] of Object.entries(entry)) {
93
+ this.use(resource, name);
94
+ const relation = this.relation(resource, name);
95
+ if (relation !== undefined) {
96
+ this.level(relation.target, isRecord(value) ? value : {});
97
+ }
98
+ }
99
+ }
100
+ level(resource, args) {
101
+ const node = this.node(resource, args.where);
102
+ this.where(resource, args.where, node);
103
+ this.orderBy(resource, args.orderBy, node);
104
+ if (isRecord(args.cursor)) {
105
+ for (const field of Object.keys(args.cursor)) {
106
+ this.use(resource, field);
107
+ node.fields.add(field);
108
+ }
109
+ }
110
+ this.projection(resource, args);
111
+ }
112
+ where(resource, where, node) {
113
+ if (!isRecord(where)) {
114
+ return;
115
+ }
116
+ for (const [key, value] of Object.entries(where)) {
117
+ if (key === "AND" || key === "OR" || key === "NOT") {
118
+ for (const entry of Array.isArray(value) ? value : [value]) {
119
+ this.where(resource, entry, node);
120
+ }
121
+ continue;
122
+ }
123
+ this.use(resource, key);
124
+ node.fields.add(key);
125
+ const relation = this.relation(resource, key);
126
+ if (relation === undefined || !isRecord(value)) {
127
+ continue;
128
+ }
129
+ const inner = relation.cardinality === "many"
130
+ ? ["some", "every", "none"].flatMap((operator) => isRecord(value[operator]) ? [value[operator]] : [])
131
+ : [value];
132
+ for (const filter of inner) {
133
+ this.where(relation.target, filter, this.node(relation.target, filter));
134
+ }
135
+ }
136
+ }
137
+ orderBy(resource, orderBy, node) {
138
+ for (const entry of Array.isArray(orderBy) ? orderBy : [orderBy]) {
139
+ if (!isRecord(entry)) {
140
+ continue;
141
+ }
142
+ for (const [key, value] of Object.entries(entry)) {
143
+ this.use(resource, key);
144
+ node.fields.add(key);
145
+ const relation = this.relation(resource, key);
146
+ if (relation?.cardinality === "one" && isRecord(value)) {
147
+ this.orderBy(relation.target, value, this.node(relation.target, undefined));
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ function projects(request) {
154
+ return request.family === "upsert" || request.variant !== "count";
155
+ }
156
+ function namedFields(active, request, args) {
157
+ const walk = new FieldWalk(active);
158
+ const level = isRecord(args) ? args : {};
159
+ const root = walk.node(request.resource, level.where);
160
+ walk.where(request.resource, level.where, root);
161
+ walk.orderBy(request.resource, level.orderBy, root);
162
+ if (isRecord(level.cursor)) {
163
+ for (const field of Object.keys(level.cursor)) {
164
+ walk.use(request.resource, field);
165
+ root.fields.add(field);
166
+ }
167
+ }
168
+ if (projects(request)) {
169
+ walk.projection(request.resource, level);
170
+ }
171
+ return walk;
172
+ }
173
+ function withoutConditional(core, resource, where, conditional) {
174
+ if (!isRecord(where)) {
175
+ return true;
176
+ }
177
+ const conditions = [];
178
+ for (const [key, value] of Object.entries(where)) {
179
+ if (key === "AND") {
180
+ for (const entry of Array.isArray(value) ? value : [value]) {
181
+ conditions.push(withoutConditional(core, resource, entry, conditional));
182
+ }
183
+ continue;
184
+ }
185
+ if (key === "OR") {
186
+ conditions.push(Array.isArray(value)
187
+ ? someOf(value.map((entry) => withoutConditional(core, resource, entry, conditional)))
188
+ : true);
189
+ continue;
190
+ }
191
+ conditions.push(holdsConditional(core, resource, { [key]: value }, conditional)
192
+ ? true
193
+ : { [key]: value });
194
+ }
195
+ return everyOf(conditions);
196
+ }
197
+ function holdsConditional(core, resource, where, conditional) {
198
+ if (!isRecord(where)) {
199
+ return false;
200
+ }
201
+ for (const [key, value] of Object.entries(where)) {
202
+ if (key === "AND" || key === "OR" || key === "NOT") {
203
+ if ((Array.isArray(value) ? value : [value]).some((entry) => holdsConditional(core, resource, entry, conditional))) {
204
+ return true;
205
+ }
206
+ continue;
207
+ }
208
+ if (conditional.get(resource)?.has(key) === true) {
209
+ return true;
210
+ }
211
+ const field = lookupField(lookupResource(core, resource), key);
212
+ if (field?.kind !== "relation" || !isRecord(value)) {
213
+ continue;
214
+ }
215
+ const inner = field.cardinality === "many"
216
+ ? [value.some, value.every, value.none]
217
+ : [value];
218
+ if (inner.some((filter) => holdsConditional(core, field.target, filter, conditional))) {
219
+ return true;
220
+ }
221
+ }
222
+ return false;
223
+ }
224
+ export async function admitByPolicy(session, core, active, request, args) {
225
+ for (const action of policyActionsOf(request.family)) {
226
+ if (!(await session.can(request.resource, action))) {
227
+ return policyRefusal(`The policy does not allow ${action} on Resource "${request.resource}".`);
228
+ }
229
+ }
230
+ const named = namedFields(active, request, args);
231
+ const resources = new Set([
232
+ ...named.used.keys(),
233
+ ...named.nodes.map((node) => node.resource),
234
+ ]);
235
+ const answers = new Map(await Promise.all([...resources].map(async (resource) => [
236
+ resource,
237
+ await session.fields(resource, { access: "read" }),
238
+ ])));
239
+ const conditional = new Map();
240
+ for (const [resource, fields] of named.used) {
241
+ const answer = answers.get(resource);
242
+ for (const field of fields) {
243
+ const access = fieldAccess(core, resource, answer, field);
244
+ if (access === "never") {
245
+ return policyRefusal(`The policy does not allow reading field "${field}" of Resource "${resource}".`);
246
+ }
247
+ if (access !== "always") {
248
+ let set = conditional.get(resource);
249
+ if (set === undefined) {
250
+ set = new Set();
251
+ conditional.set(resource, set);
252
+ }
253
+ set.add(field);
254
+ }
255
+ }
256
+ }
257
+ for (const node of named.nodes) {
258
+ const answer = answers.get(node.resource);
259
+ const grants = [];
260
+ const names = [];
261
+ for (const field of node.fields) {
262
+ const access = fieldAccess(core, node.resource, answer, field);
263
+ if (access !== "always" && access !== "never") {
264
+ grants.push(access);
265
+ names.push(field);
266
+ }
267
+ }
268
+ if (grants.length === 0) {
269
+ continue;
270
+ }
271
+ const reach = everyOf([
272
+ await session.scope(node.resource, "read"),
273
+ withoutConditional(core, node.resource, node.where, conditional),
274
+ someOf(grants.map(noneOf)),
275
+ ]);
276
+ if (reach === false) {
277
+ continue;
278
+ }
279
+ if (await session.exists(node.resource, reach === true ? {} : reach)) {
280
+ return policyRefusal(`The policy does not allow filtering, sorting or paging by ${names
281
+ .map((name) => `"${name}"`)
282
+ .join(", ")} of Resource "${node.resource}" on every row this request reaches.`);
283
+ }
284
+ }
285
+ return undefined;
286
+ }
287
+ function sentRecords(request, args) {
288
+ return writeRecordPathsOf(request.family, args).flatMap((path) => {
289
+ const record = valueAt(args, path);
290
+ return isRecord(record)
291
+ ? [
292
+ {
293
+ record,
294
+ stage: request.family === "create" || path[0] === "create"
295
+ ? "create"
296
+ : "update",
297
+ },
298
+ ]
299
+ : [];
300
+ });
301
+ }
302
+ async function admitNestedRecords(session, core, resource, directives) {
303
+ if (!isRecord(directives)) {
304
+ return undefined;
305
+ }
306
+ const owner = lookupResource(core, resource);
307
+ for (const [directive, operand] of Object.entries(directives)) {
308
+ if (!isRelationMutationDirective(directive)) {
309
+ continue;
310
+ }
311
+ for (const entry of Array.isArray(operand) ? operand : [operand]) {
312
+ for (const site of RELATION_MUTATION_RECORD_SITES[directive]) {
313
+ const record = site.key === undefined ? entry : valueAt(entry, [site.key]);
314
+ if (!isRecord(record)) {
315
+ continue;
316
+ }
317
+ const fields = await session.fields(resource, {
318
+ access: "write",
319
+ action: site.stage,
320
+ });
321
+ for (const [field, value] of Object.entries(record)) {
322
+ if (value === undefined) {
323
+ continue;
324
+ }
325
+ const relation = lookupField(owner, field);
326
+ const written = relation?.kind === "relation"
327
+ ? [field, ...(relation.referenceFields ?? [])]
328
+ : [field];
329
+ for (const member of written) {
330
+ if (fieldAccess(core, resource, fields, member) === "never") {
331
+ return policyRefusal(`The policy does not allow writing field "${member}" of Resource "${resource}".`);
332
+ }
333
+ }
334
+ if (relation?.kind === "relation") {
335
+ const refused = await admitNestedRecords(session, core, relation.target, value);
336
+ if (refused !== undefined) {
337
+ return refused;
338
+ }
339
+ }
340
+ }
341
+ }
342
+ }
343
+ }
344
+ return undefined;
345
+ }
346
+ export async function admitWriteByPolicy(session, core, request, args) {
347
+ const records = sentRecords(request, args);
348
+ if (records.length === 0) {
349
+ return undefined;
350
+ }
351
+ const resource = request.resource;
352
+ const grants = [];
353
+ const names = [];
354
+ for (const { record, stage } of records) {
355
+ const fields = await session.fields(resource, {
356
+ access: "write",
357
+ action: stage,
358
+ });
359
+ for (const [field, value] of Object.entries(record)) {
360
+ if (value === undefined) {
361
+ continue;
362
+ }
363
+ const relation = lookupField(lookupResource(core, resource), field);
364
+ const written = relation?.kind === "relation"
365
+ ? [field, ...(relation.referenceFields ?? [])]
366
+ : [field];
367
+ if (relation?.kind === "relation") {
368
+ const refused = await admitNestedRecords(session, core, relation.target, value);
369
+ if (refused !== undefined) {
370
+ return refused;
371
+ }
372
+ }
373
+ for (const member of written) {
374
+ const access = fieldAccess(core, resource, fields, member);
375
+ if (access === "never") {
376
+ return policyRefusal(`The policy does not allow writing field "${member}" of Resource "${resource}".`);
377
+ }
378
+ if (access === "always") {
379
+ continue;
380
+ }
381
+ if (stage === "create") {
382
+ session.grantCreate(member, access);
383
+ continue;
384
+ }
385
+ if (!names.includes(member)) {
386
+ grants.push(access);
387
+ names.push(member);
388
+ }
389
+ }
390
+ }
391
+ }
392
+ if (grants.length === 0) {
393
+ return undefined;
394
+ }
395
+ const level = isRecord(args) ? args : {};
396
+ const reach = everyOf([
397
+ await session.scope(resource, "update"),
398
+ await session.scope(resource, "read"),
399
+ isRecord(level.where) ? level.where : true,
400
+ someOf(grants.map(noneOf)),
401
+ ]);
402
+ if (reach !== false &&
403
+ (await session.exists(resource, reach === true ? {} : reach))) {
404
+ return policyRefusal(`The policy does not allow writing ${names
405
+ .map((name) => `"${name}"`)
406
+ .join(", ")} of Resource "${resource}" on every row this request reaches.`);
407
+ }
408
+ return undefined;
409
+ }
@@ -0,0 +1,29 @@
1
+ import type { AvAdapterModel } from "../adapters/adapter-model.js";
2
+ import type { AvContractCore } from "../contracts/contract.js";
3
+ import type { AvOperationFailureResponse } from "../runtime/operation-response.js";
4
+ import { type PolicyCondition } from "./policy-condition.js";
5
+ import type { PolicySession } from "./policy-session.js";
6
+ type Row = Readonly<Record<string, unknown>>;
7
+ /** The scalar fields of `resource` an after check reads the changed rows by. */
8
+ export declare function rowFieldsOf(core: AvContractCore, resource: string): readonly string[];
9
+ /** What the rows a write changed are checked for, after it. */
10
+ export type AfterWrite = {
11
+ /** The read scope a moved-out row has left, when the data can move one. */
12
+ readonly moveOut: PolicyCondition | undefined;
13
+ /**
14
+ * Whether the after check runs: in a transaction, always; without one, the
15
+ * update is one `afterChecks` lists, and it is refused before it runs.
16
+ */
17
+ readonly after: boolean;
18
+ };
19
+ /** The checks after an update whose data is `data`; `undefined` when none applies. */
20
+ export declare function afterUpdate(session: PolicySession, core: AvContractCore, model: AvAdapterModel, resource: string, data: unknown, inTransaction: boolean): Promise<AfterWrite | undefined>;
21
+ /** The fields the after check asks `can` about: the identifier, then every scalar field. */
22
+ export declare function afterCheckFields(core: AvContractCore, model: AvAdapterModel, resource: string): readonly string[];
23
+ /**
24
+ * The changed rows held to the move-out and after checks: the refusal, or
25
+ * `undefined`. `returned` says that `changed` are the rows the write answered,
26
+ * carrying {@link afterCheckFields}.
27
+ */
28
+ export declare function checkChanged(session: PolicySession, core: AvContractCore, model: AvAdapterModel, resource: string, checks: AfterWrite, changed: readonly Row[], returned?: boolean): Promise<AvOperationFailureResponse | undefined>;
29
+ export {};
@@ -0,0 +1,132 @@
1
+ import { adapterPrimaryIdentifier } from "../adapters/adapter-identifier.js";
2
+ import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
3
+ import { frozenCopy } from "../runtime/readonly-value.js";
4
+ import { policyRefusal } from "./policy-admission.js";
5
+ import { allOf, whereOf } from "./policy-condition.js";
6
+ import { identifiedRows } from "./policy-read.runtime.js";
7
+ function isRecord(value) {
8
+ return value !== null && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+ const fieldLists = new WeakMap();
11
+ function listOf(core, key, make) {
12
+ let lists = fieldLists.get(core);
13
+ if (lists === undefined) {
14
+ lists = new Map();
15
+ fieldLists.set(core, lists);
16
+ }
17
+ let fields = lists.get(key);
18
+ if (fields === undefined) {
19
+ fields = make();
20
+ lists.set(key, fields);
21
+ }
22
+ return fields;
23
+ }
24
+ export function rowFieldsOf(core, resource) {
25
+ return listOf(core, `scalar\u0000${resource}`, () => Object.entries(lookupResource(core, resource)?.fields ?? {}).flatMap(([name, field]) => field?.kind === "scalar" && field.capabilities.select ? [name] : []));
26
+ }
27
+ function touches(core, model, resource, scope, data) {
28
+ const owner = lookupResource(core, resource);
29
+ const read = new Set();
30
+ const walk = (where) => {
31
+ if (!isRecord(where)) {
32
+ return;
33
+ }
34
+ for (const [key, value] of Object.entries(where)) {
35
+ if (key === "AND" || key === "OR" || key === "NOT") {
36
+ for (const entry of Array.isArray(value) ? value : [value]) {
37
+ walk(entry);
38
+ }
39
+ continue;
40
+ }
41
+ read.add(key);
42
+ const field = lookupField(owner, key);
43
+ if (field?.kind !== "relation") {
44
+ continue;
45
+ }
46
+ if (field.cardinality === "one" && field.referenceFields !== undefined) {
47
+ for (const reference of field.referenceFields) {
48
+ read.add(reference);
49
+ }
50
+ }
51
+ else {
52
+ for (const member of adapterPrimaryIdentifier(model, resource)) {
53
+ read.add(member);
54
+ }
55
+ }
56
+ }
57
+ };
58
+ walk(scope);
59
+ return Object.entries(data).some(([name, value]) => {
60
+ if (value === undefined) {
61
+ return false;
62
+ }
63
+ const field = lookupField(owner, name);
64
+ const written = field?.kind === "relation"
65
+ ? [name, ...(field.referenceFields ?? [])]
66
+ : [name];
67
+ return written.some((member) => read.has(member));
68
+ });
69
+ }
70
+ export async function afterUpdate(session, core, model, resource, data, inTransaction) {
71
+ const read = await session.scope(resource, "read");
72
+ const moveOut = read !== true &&
73
+ isRecord(data) &&
74
+ touches(core, model, resource, read, data)
75
+ ? read
76
+ : undefined;
77
+ const after = inTransaction || session.checksAfter(resource, "update");
78
+ return moveOut === undefined && !after ? undefined : { moveOut, after };
79
+ }
80
+ function identityKey(identifier, row) {
81
+ return JSON.stringify(identifier.map((member) => row[member]), (_key, value) => (typeof value === "bigint" ? `#bigint(${value})` : value));
82
+ }
83
+ export function afterCheckFields(core, model, resource) {
84
+ return listOf(core, `after\u0000${resource}`, () => [
85
+ ...new Set([
86
+ ...adapterPrimaryIdentifier(model, resource),
87
+ ...rowFieldsOf(core, resource),
88
+ ]),
89
+ ]);
90
+ }
91
+ function rowOfFields(row, fields) {
92
+ const picked = {};
93
+ for (const field of fields) {
94
+ if (Object.hasOwn(row, field)) {
95
+ picked[field] = row[field];
96
+ }
97
+ }
98
+ return picked;
99
+ }
100
+ export async function checkChanged(session, core, model, resource, checks, changed, returned = false) {
101
+ if (changed.length === 0) {
102
+ return undefined;
103
+ }
104
+ const fresh = session.renewed();
105
+ const identifier = adapterPrimaryIdentifier(model, resource);
106
+ const these = identifiedRows(core, identifier, resource, changed);
107
+ if (these === false) {
108
+ return undefined;
109
+ }
110
+ const kept = checks.moveOut === undefined ? these : allOf(these, checks.moveOut);
111
+ const reread = checks.moveOut !== undefined || !returned;
112
+ const fields = afterCheckFields(core, model, resource);
113
+ const rows = kept === false || !reread
114
+ ? []
115
+ : await fresh.rows(resource, whereOf(kept), checks.after && !returned ? fields : [...identifier]);
116
+ if (checks.moveOut !== undefined) {
117
+ const found = new Set(rows.map((row) => identityKey(identifier, row)));
118
+ if (changed.some((row) => !found.has(identityKey(identifier, row)))) {
119
+ return policyRefusal(`The policy does not allow this change: it moves a row of Resource "${resource}" out of what the caller may read.`);
120
+ }
121
+ }
122
+ if (checks.after) {
123
+ const asked = returned
124
+ ? changed.map((row) => rowOfFields(row, fields))
125
+ : rows;
126
+ const answers = await Promise.all(asked.map((row) => fresh.can(resource, "update", frozenCopy(row))));
127
+ if (answers.includes(false)) {
128
+ return policyRefusal(`The policy does not allow this change: a changed row of Resource "${resource}" does not meet the condition its update grant sets.`);
129
+ }
130
+ }
131
+ return undefined;
132
+ }
@@ -0,0 +1,15 @@
1
+ import type { AvPolicyWhere } from "./policy-engine.js";
2
+ /** A row condition: every row, no row, or a `where` fragment. */
3
+ export type PolicyCondition = boolean | AvPolicyWhere;
4
+ /** Both conditions: a constant folds, two fragments become one `AND`. */
5
+ export declare function allOf(left: PolicyCondition, right: PolicyCondition): PolicyCondition;
6
+ /** Either condition: a constant folds, two fragments become one `OR`. */
7
+ export declare function anyOf(left: PolicyCondition, right: PolicyCondition): PolicyCondition;
8
+ /** Every condition of `conditions`; `true` when there is none. */
9
+ export declare function everyOf(conditions: readonly PolicyCondition[]): PolicyCondition;
10
+ /** Any condition of `conditions`; `false` when there is none. */
11
+ export declare function someOf(conditions: readonly PolicyCondition[]): PolicyCondition;
12
+ /** The negation: a constant flips, a fragment is wrapped in `NOT`. */
13
+ export declare function noneOf(condition: PolicyCondition): PolicyCondition;
14
+ /** A condition as a `where` an `exists` or a read can run: `{}` for every row. */
15
+ export declare function whereOf(condition: Exclude<PolicyCondition, false>): AvPolicyWhere;
@@ -0,0 +1,36 @@
1
+ export function allOf(left, right) {
2
+ if (left === false || right === false) {
3
+ return false;
4
+ }
5
+ if (left === true) {
6
+ return right;
7
+ }
8
+ if (right === true) {
9
+ return left;
10
+ }
11
+ return { AND: [left, right] };
12
+ }
13
+ export function anyOf(left, right) {
14
+ if (left === true || right === true) {
15
+ return true;
16
+ }
17
+ if (left === false) {
18
+ return right;
19
+ }
20
+ if (right === false) {
21
+ return left;
22
+ }
23
+ return { OR: [left, right] };
24
+ }
25
+ export function everyOf(conditions) {
26
+ return conditions.reduce(allOf, true);
27
+ }
28
+ export function someOf(conditions) {
29
+ return conditions.reduce(anyOf, false);
30
+ }
31
+ export function noneOf(condition) {
32
+ return typeof condition === "boolean" ? !condition : { NOT: condition };
33
+ }
34
+ export function whereOf(condition) {
35
+ return condition === true ? {} : condition;
36
+ }