@aventara/core 1.0.0-rc.1 → 1.0.0-rc.10
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.
- package/CHANGELOG.md +943 -16
- package/ERRORS.md +142 -0
- package/README.md +274 -16
- package/dist/adapters/adapter-discovery-diagnostic.guidance.d.ts +6 -0
- package/dist/adapters/adapter-discovery-diagnostic.guidance.js +7 -0
- package/dist/adapters/adapter-field.d.ts +22 -0
- package/dist/adapters/adapter-identifier.d.ts +9 -0
- package/dist/adapters/adapter-identifier.js +6 -1
- package/dist/adapters/adapter-model.d.ts +9 -0
- package/dist/adapters/adapter-operation.d.ts +18 -3
- package/dist/adapters/adapter-resource.d.ts +5 -0
- package/dist/adapters/adapter.interface.d.ts +10 -0
- package/dist/avz/avz.d.ts +1 -1
- package/dist/avz/json-shape.registry.d.ts +2 -1
- package/dist/compiler/contract-compiler-diagnostic.guidance.d.ts +6 -0
- package/dist/compiler/contract-compiler-diagnostic.guidance.js +97 -0
- package/dist/compiler/contract.compiler.d.ts +6 -0
- package/dist/compiler/contract.compiler.js +227 -24
- package/dist/compiler/managed-elsewhere.projector.d.ts +52 -0
- package/dist/compiler/managed-elsewhere.projector.js +179 -0
- package/dist/compiler/managed-elsewhere.validator.d.ts +9 -0
- package/dist/compiler/managed-elsewhere.validator.js +74 -0
- package/dist/config/computed-fields.d.ts +62 -0
- package/dist/config/computed-fields.js +73 -0
- package/dist/config/config-diagnostic.guidance.d.ts +6 -0
- package/dist/config/config-diagnostic.guidance.js +32 -0
- package/dist/config/config.resolver.d.ts +24 -0
- package/dist/config/config.resolver.js +65 -0
- package/dist/config/config.validator.js +222 -8
- package/dist/config/define-resource.d.ts +1 -5
- package/dist/config/entrypoint.normalizer.d.ts +3 -2
- package/dist/config/exact-framework-config.d.ts +67 -22
- package/dist/config/field-compute.resolver.d.ts +2 -0
- package/dist/config/field-compute.resolver.js +8 -0
- package/dist/config/framework-config.d.ts +53 -15
- package/dist/config/framework-resources-config.d.ts +25 -14
- package/dist/config/pipeline-config.d.ts +4 -4
- package/dist/config/pipeline-tree.resolver.d.ts +2 -2
- package/dist/contracts/access-route.rules.d.ts +10 -0
- package/dist/contracts/access-route.rules.js +45 -0
- package/dist/contracts/capability.d.ts +26 -1
- package/dist/contracts/capability.js +20 -0
- package/dist/contracts/client-contract-structure.validator.d.ts +301 -89
- package/dist/contracts/client-contract-structure.validator.js +37 -2
- package/dist/contracts/contract-access.d.ts +29 -0
- package/dist/contracts/contract-access.js +1 -0
- package/dist/contracts/contract-canonicalizer.d.ts +9 -1
- package/dist/contracts/contract-canonicalizer.js +30 -4
- package/dist/contracts/contract-hash.d.ts +0 -1
- package/dist/contracts/contract-hash.js +1 -1
- package/dist/contracts/contract.d.ts +27 -1
- package/dist/contracts/field.d.ts +58 -1
- package/dist/contracts/json-schema.projector.d.ts +11 -0
- package/dist/contracts/json-schema.projector.js +78 -0
- package/dist/contracts/scalar-value-type.d.ts +3 -1
- package/dist/framework/access-facade.d.ts +116 -0
- package/dist/framework/access-facade.js +120 -0
- package/dist/framework/contract-view.d.ts +120 -16
- package/dist/framework/framework-construction.error.d.ts +14 -4
- package/dist/framework/framework-construction.error.js +18 -2
- package/dist/framework/framework.d.ts +26 -5
- package/dist/framework/framework.factory.d.ts +1 -1
- package/dist/framework/framework.factory.js +115 -23
- package/dist/framework/operation-facade.d.ts +80 -13
- package/dist/framework/operation-facade.js +2 -1
- package/dist/index.d.ts +17 -5
- package/dist/index.js +1 -0
- package/dist/internal.d.ts +7 -2
- package/dist/internal.js +6 -0
- package/dist/operations/database-refusal.d.ts +51 -0
- package/dist/operations/database-refusal.js +45 -0
- package/dist/operations/operation-arguments.d.ts +148 -60
- package/dist/operations/operation-call.d.ts +9 -5
- package/dist/operations/operation-result.d.ts +6 -4
- package/dist/operations/operation-validator-core.js +159 -36
- package/dist/operations/operation-validator.d.ts +1 -1
- package/dist/operations/structural-limits.walker.d.ts +1 -1
- package/dist/operations/validation-code.guidance.d.ts +6 -0
- package/dist/operations/validation-code.guidance.js +90 -0
- package/dist/operations/write-scope.resolver.d.ts +4 -0
- package/dist/operations/write-scope.resolver.js +2 -0
- package/dist/policy/access-provider.interface.d.ts +91 -0
- package/dist/policy/access-provider.interface.js +1 -0
- package/dist/policy/access-provider.registry.d.ts +17 -0
- package/dist/policy/access-provider.registry.js +15 -0
- package/dist/policy/access-question.d.ts +20 -0
- package/dist/policy/access-question.js +1 -0
- package/dist/policy/access-question.registry.d.ts +5 -0
- package/dist/policy/access-question.registry.js +19 -0
- package/dist/policy/access-question.runtime.d.ts +36 -0
- package/dist/policy/access-question.runtime.js +297 -0
- package/dist/policy/access-verb.table.d.ts +56 -0
- package/dist/policy/access-verb.table.js +21 -0
- package/dist/policy/access-verbs.d.ts +46 -0
- package/dist/policy/access-verbs.js +1 -0
- package/dist/policy/policy-admission.d.ts +20 -0
- package/dist/policy/policy-admission.js +409 -0
- package/dist/policy/policy-after-update.runtime.d.ts +79 -0
- package/dist/policy/policy-after-update.runtime.js +208 -0
- package/dist/policy/policy-condition.d.ts +15 -0
- package/dist/policy/policy-condition.js +36 -0
- package/dist/policy/policy-engine.d.ts +205 -0
- package/dist/policy/policy-engine.js +1 -0
- package/dist/policy/policy-named-rows.d.ts +18 -0
- package/dist/policy/policy-named-rows.js +51 -0
- package/dist/policy/policy-nested-write.runtime.d.ts +44 -0
- package/dist/policy/policy-nested-write.runtime.js +545 -0
- package/dist/policy/policy-read.runtime.d.ts +81 -0
- package/dist/policy/policy-read.runtime.js +503 -0
- package/dist/policy/policy-reference-write.runtime.d.ts +51 -0
- package/dist/policy/policy-reference-write.runtime.js +345 -0
- package/dist/policy/policy-request.d.ts +41 -0
- package/dist/policy/policy-request.js +63 -0
- package/dist/policy/policy-session.d.ts +106 -0
- package/dist/policy/policy-session.js +295 -0
- package/dist/policy/policy-startup.validator.d.ts +10 -0
- package/dist/policy/policy-startup.validator.js +112 -0
- package/dist/policy/policy-write.runtime.d.ts +39 -0
- package/dist/policy/policy-write.runtime.js +532 -0
- package/dist/prebuilt/core-version.generated.d.ts +2 -0
- package/dist/prebuilt/core-version.generated.js +1 -0
- package/dist/prebuilt/prebuilt-changes.d.ts +29 -0
- package/dist/prebuilt/prebuilt-changes.js +65 -0
- package/dist/prebuilt/prebuilt-contracts.d.ts +40 -0
- package/dist/prebuilt/prebuilt-contracts.emitter.d.ts +16 -0
- package/dist/prebuilt/prebuilt-contracts.emitter.js +50 -0
- package/dist/prebuilt/prebuilt-contracts.js +1 -0
- package/dist/prebuilt/prebuilt-contracts.loader.d.ts +19 -0
- package/dist/prebuilt/prebuilt-contracts.loader.js +77 -0
- package/dist/prebuilt/prebuilt-inputs.fingerprint.d.ts +16 -0
- package/dist/prebuilt/prebuilt-inputs.fingerprint.js +229 -0
- package/dist/protocol/absent-route.responder.d.ts +9 -27
- package/dist/protocol/absent-route.responder.js +36 -3
- package/dist/protocol/contract-document.responder.d.ts +2 -1
- package/dist/protocol/http-request.decoder.d.ts +19 -3
- package/dist/protocol/http-request.decoder.js +24 -1
- package/dist/protocol/http-request.reader.d.ts +6 -3
- package/dist/protocol/http-request.reader.js +1 -0
- package/dist/protocol/http-response.encoder.js +2 -2
- package/dist/protocol/protocol-headers.d.ts +3 -3
- package/dist/protocol/protocol-surface.d.ts +6 -1
- package/dist/protocol/protocol-surface.js +7 -0
- package/dist/protocol/protocol.binder.d.ts +16 -1
- package/dist/protocol/protocol.binder.js +4 -1
- package/dist/protocol/protocol.handler.d.ts +12 -1
- package/dist/protocol/protocol.handler.js +86 -17
- package/dist/runtime/caller-data.copier.d.ts +18 -3
- package/dist/runtime/caller-data.copier.js +21 -8
- package/dist/runtime/caller-data.door.d.ts +13 -6
- package/dist/runtime/computed-read-runtime.d.ts +6 -1
- package/dist/runtime/computed-read-runtime.js +232 -45
- package/dist/runtime/computed-value.error.d.ts +3 -2
- package/dist/runtime/computed-value.error.js +3 -2
- package/dist/runtime/context-executor.runtime.d.ts +4 -3
- package/dist/runtime/context-executor.runtime.js +4 -2
- package/dist/runtime/core-class.brand.d.ts +10 -0
- package/dist/runtime/core-class.brand.js +21 -0
- package/dist/runtime/create-requirement.d.ts +31 -5
- package/dist/runtime/create-requirement.js +129 -21
- package/dist/runtime/decimal.d.ts +14 -0
- package/dist/runtime/decimal.js +21 -0
- package/dist/runtime/diagnostics-reporter.d.ts +44 -9
- package/dist/runtime/diagnostics-reporter.js +13 -1
- package/dist/runtime/error-boundary.d.ts +9 -2
- package/dist/runtime/error-boundary.js +12 -0
- package/dist/runtime/error-guidance.d.ts +12 -0
- package/dist/runtime/error-guidance.js +1 -0
- package/dist/runtime/error.catalog.d.ts +49 -0
- package/dist/runtime/error.catalog.js +49 -0
- package/dist/runtime/execution-options.d.ts +8 -0
- package/dist/runtime/field-check.evaluator.d.ts +0 -2
- package/dist/runtime/field-check.evaluator.js +1 -1
- package/dist/runtime/field-compute.d.ts +11 -10
- package/dist/runtime/field-stage.runtime.d.ts +3 -1
- package/dist/runtime/field-stage.runtime.js +8 -4
- package/dist/runtime/field-validator.runtime.d.ts +19 -1
- package/dist/runtime/field-validator.runtime.js +47 -8
- package/dist/runtime/framework-error.js +9 -0
- package/dist/runtime/framework-warning.emitter.d.ts +6 -0
- package/dist/runtime/framework-warning.emitter.js +8 -0
- package/dist/runtime/json-value.d.ts +10 -5
- package/dist/runtime/level-lifecycle.runtime.d.ts +4 -3
- package/dist/runtime/level-lifecycle.runtime.js +3 -5
- package/dist/runtime/mutation-planner.d.ts +1 -6
- package/dist/runtime/mutation-planner.js +3 -14
- package/dist/runtime/operation-code.guidance.d.ts +6 -0
- package/dist/runtime/operation-code.guidance.js +167 -0
- package/dist/runtime/operation-request.d.ts +16 -9
- package/dist/runtime/operation-runtime.d.ts +35 -17
- package/dist/runtime/operation-runtime.js +220 -49
- package/dist/runtime/outcome-mapper.d.ts +2 -1
- package/dist/runtime/output-validator.d.ts +1 -1
- package/dist/runtime/owned-execution.registry.d.ts +13 -3
- package/dist/runtime/pipe-output.validator.d.ts +10 -0
- package/dist/runtime/pipe-output.validator.js +4 -2
- package/dist/runtime/pipeline.d.ts +47 -22
- package/dist/runtime/readonly-value.d.ts +20 -5
- package/dist/runtime/readonly-value.js +74 -26
- package/dist/runtime/referential-action.d.ts +1 -1
- package/dist/runtime/scalar-runtime.d.ts +4 -11
- package/dist/runtime/scalar-runtime.js +91 -22
- package/dist/runtime/unsent-value.normalizer.d.ts +15 -0
- package/dist/runtime/unsent-value.normalizer.js +128 -0
- package/dist/transactions/operation-arguments-encoder.js +8 -3
- package/dist/transactions/operation-fingerprint.js +2 -2
- package/dist/transactions/sql-statement.d.ts +10 -2
- package/dist/transactions/sql-statement.validator.d.ts +2 -2
- package/dist/transactions/sql-statement.validator.js +38 -5
- package/dist/transactions/transaction-envelope.d.ts +5 -4
- package/dist/transactions/transaction-integrity-error.js +2 -0
- package/dist/transactions/transaction-plan-assembler.d.ts +12 -5
- package/dist/transactions/transaction-plan-assembler.js +48 -10
- package/dist/transactions/transaction-plan-validator.d.ts +23 -7
- package/dist/transactions/transaction-plan-validator.js +27 -1
- package/dist/transactions/transaction-reference-resolver.d.ts +3 -2
- package/dist/transactions/transaction-reference-validator.d.ts +43 -3
- package/dist/transactions/transaction-reference-validator.js +83 -57
- package/dist/transactions/transaction-reference.d.ts +1 -1
- package/dist/transactions/transaction-reference.js +5 -0
- package/dist/transactions/transaction-request.d.ts +8 -8
- package/dist/transactions/transaction-runtime.d.ts +5 -2
- package/dist/transactions/transaction-runtime.js +79 -27
- package/dist/transactions/transaction-settlement.runtime.d.ts +4 -3
- package/dist/transactions/transaction-slot-path-error.js +2 -0
- package/dist/transactions/transaction-stage.runtime.d.ts +5 -3
- package/dist/transactions/transaction-stage.runtime.js +7 -4
- package/package.json +6 -5
- package/src/runtime/field-check.evaluator.ts +1 -1
|
@@ -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
|
+
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,79 @@
|
|
|
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
|
+
* Whether the after check also hands `can` each changed row as it was
|
|
20
|
+
* before the update (`before`): only for an update `afterChecks` lists.
|
|
21
|
+
*/
|
|
22
|
+
readonly before: boolean;
|
|
23
|
+
};
|
|
24
|
+
/** The checks after an update whose data is `data`; `undefined` when none applies. */
|
|
25
|
+
export declare function afterUpdate(session: PolicySession, core: AvContractCore, model: AvAdapterModel, resource: string, data: unknown, inTransaction: boolean): Promise<AfterWrite | undefined>;
|
|
26
|
+
/** The fields the after check asks `can` about: the identifier, then every scalar field. */
|
|
27
|
+
export declare function afterCheckFields(core: AvContractCore, model: AvAdapterModel, resource: string): readonly string[];
|
|
28
|
+
/**
|
|
29
|
+
* The rows an update will change as they were before it, and, where the engine
|
|
30
|
+
* answers `grantsOf`, the grants that covered each then, index for index.
|
|
31
|
+
*/
|
|
32
|
+
export type RowsBefore = {
|
|
33
|
+
readonly rows: readonly Row[];
|
|
34
|
+
/** `grantsOf`'s answer about each of `rows`; absent when it was not asked. */
|
|
35
|
+
readonly grants?: readonly (readonly string[])[];
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The rows an update will change, as they are now, carrying
|
|
39
|
+
* {@link afterCheckFields}: `reach` read inside the update's transaction, for
|
|
40
|
+
* the after check's `before`, with the grants that cover each
|
|
41
|
+
* ({@link coveredBefore}). None when the update is none `afterChecks` lists
|
|
42
|
+
* or it reaches no row. A listed update of a Resource whose Core Contract
|
|
43
|
+
* offers no `find.many` is refused before it runs ({@link beforeUnreadable}).
|
|
44
|
+
*/
|
|
45
|
+
export declare function rowsBefore(session: PolicySession, core: AvContractCore, model: AvAdapterModel, resource: string, checks: AfterWrite | undefined, reach: PolicyCondition): Promise<RowsBefore>;
|
|
46
|
+
/**
|
|
47
|
+
* `rows`, read ahead of the write, with the grants the engine says cover each
|
|
48
|
+
* (`grantsOf`, asked now, before the write): for an update `afterChecks` lists
|
|
49
|
+
* whose engine answers that question; otherwise `rows` alone.
|
|
50
|
+
*/
|
|
51
|
+
export declare function coveredBefore(session: PolicySession, core: AvContractCore, model: AvAdapterModel, resource: string, checks: AfterWrite | undefined, rows: readonly Row[]): Promise<RowsBefore>;
|
|
52
|
+
/**
|
|
53
|
+
* The refusal of a listed update whose rows cannot be read as they were: the
|
|
54
|
+
* Core Contract offers no `find.many` (as an update with no identifier to find
|
|
55
|
+
* the changed rows by is refused), or `undefined`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function beforeUnreadable(core: AvContractCore, resource: string, checks: AfterWrite | undefined): AvOperationFailureResponse | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* The fields to read the rows an update will change by, before it: the
|
|
60
|
+
* identifier, and when the after check hands `can` the row as it was, every
|
|
61
|
+
* field it asks about, so the same read is that `before`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function beforeFields(core: AvContractCore, model: AvAdapterModel, resource: string, checks: AfterWrite | undefined): readonly string[];
|
|
64
|
+
/**
|
|
65
|
+
* The condition that `rows` still hold the values they were read with: each
|
|
66
|
+
* row's value of every field the after check reads that the adapter compares
|
|
67
|
+
* by `equals`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function unchangedSince(core: AvContractCore, model: AvAdapterModel, resource: string, rows: readonly Row[]): PolicyCondition;
|
|
70
|
+
/**
|
|
71
|
+
* The changed rows held to the move-out and after checks: the refusal, or
|
|
72
|
+
* `undefined`. `returned` says that `changed` are the rows the write answered,
|
|
73
|
+
* carrying {@link afterCheckFields}; `before` are the rows as they were before
|
|
74
|
+
* the write, carrying them too, each handed to `can` with the changed row of
|
|
75
|
+
* its identifier where `checks.before` says so, with the grants that covered
|
|
76
|
+
* it then where the engine answered them.
|
|
77
|
+
*/
|
|
78
|
+
export declare function checkChanged(session: PolicySession, core: AvContractCore, model: AvAdapterModel, resource: string, checks: AfterWrite, changed: readonly Row[], returned: boolean, before: RowsBefore): Promise<AvOperationFailureResponse | undefined>;
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
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, everyOf, someOf, 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 listed = session.checksAfter(resource, "update");
|
|
78
|
+
const after = inTransaction || listed;
|
|
79
|
+
return moveOut === undefined && !after
|
|
80
|
+
? undefined
|
|
81
|
+
: { moveOut, after, before: after && listed };
|
|
82
|
+
}
|
|
83
|
+
function identityKey(identifier, row) {
|
|
84
|
+
return JSON.stringify(identifier.map((member) => row[member]), (_key, value) => (typeof value === "bigint" ? `#bigint(${value})` : value));
|
|
85
|
+
}
|
|
86
|
+
export function afterCheckFields(core, model, resource) {
|
|
87
|
+
return listOf(core, `after\u0000${resource}`, () => [
|
|
88
|
+
...new Set([
|
|
89
|
+
...adapterPrimaryIdentifier(model, resource),
|
|
90
|
+
...rowFieldsOf(core, resource),
|
|
91
|
+
]),
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
export async function rowsBefore(session, core, model, resource, checks, reach) {
|
|
95
|
+
if (checks?.before !== true || reach === false) {
|
|
96
|
+
return { rows: [] };
|
|
97
|
+
}
|
|
98
|
+
const rows = await session.rows(resource, whereOf(reach), afterCheckFields(core, model, resource));
|
|
99
|
+
return coveredBefore(session, core, model, resource, checks, rows);
|
|
100
|
+
}
|
|
101
|
+
export async function coveredBefore(session, core, model, resource, checks, rows) {
|
|
102
|
+
if (checks?.before !== true || rows.length === 0) {
|
|
103
|
+
return { rows };
|
|
104
|
+
}
|
|
105
|
+
const fields = afterCheckFields(core, model, resource);
|
|
106
|
+
const grants = await Promise.all(rows.map((row) => session.grants(resource, frozenCopy(rowOfFields(row, fields)))));
|
|
107
|
+
return grants.every((answer) => answer !== undefined)
|
|
108
|
+
? { rows, grants }
|
|
109
|
+
: { rows };
|
|
110
|
+
}
|
|
111
|
+
export function beforeUnreadable(core, resource, checks) {
|
|
112
|
+
return checks?.before === true &&
|
|
113
|
+
lookupResource(core, resource)?.operations.find?.many === undefined
|
|
114
|
+
? policyRefusal(`The policy checks this update on Resource "${resource}" against the rows as they were before it, and the Resource offers no find.many to read them by.`)
|
|
115
|
+
: undefined;
|
|
116
|
+
}
|
|
117
|
+
export function beforeFields(core, model, resource, checks) {
|
|
118
|
+
return checks?.before === true
|
|
119
|
+
? afterCheckFields(core, model, resource)
|
|
120
|
+
: adapterPrimaryIdentifier(model, resource);
|
|
121
|
+
}
|
|
122
|
+
function pairedBefore(identifier, asked, before) {
|
|
123
|
+
if (asked.length === 1 && before.length === 1) {
|
|
124
|
+
return [0];
|
|
125
|
+
}
|
|
126
|
+
const earlier = new Map(before.map((row, index) => [identityKey(identifier, row), index]));
|
|
127
|
+
const paired = asked.map((row) => earlier.get(identityKey(identifier, row)));
|
|
128
|
+
return paired.every((index) => index !== undefined) ? paired : undefined;
|
|
129
|
+
}
|
|
130
|
+
export function unchangedSince(core, model, resource, rows) {
|
|
131
|
+
const fields = listOf(core, `cas\u0000${resource}`, () => {
|
|
132
|
+
const owner = Object.hasOwn(model.resources, resource)
|
|
133
|
+
? model.resources[resource]
|
|
134
|
+
: undefined;
|
|
135
|
+
return afterCheckFields(core, model, resource).filter((name) => {
|
|
136
|
+
const field = owner !== undefined && Object.hasOwn(owner.fields, name)
|
|
137
|
+
? owner.fields[name]
|
|
138
|
+
: undefined;
|
|
139
|
+
return (field?.kind === "scalar" &&
|
|
140
|
+
field.capabilities.filter?.includes("equals") === true);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
return someOf(rows.map((row) => everyOf(fields
|
|
144
|
+
.filter((name) => Object.hasOwn(row, name))
|
|
145
|
+
.map((name) => ({ [name]: { equals: row[name] } })))));
|
|
146
|
+
}
|
|
147
|
+
function rowOfFields(row, fields) {
|
|
148
|
+
const picked = {};
|
|
149
|
+
for (const field of fields) {
|
|
150
|
+
if (Object.hasOwn(row, field)) {
|
|
151
|
+
picked[field] = row[field];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return picked;
|
|
155
|
+
}
|
|
156
|
+
export async function checkChanged(session, core, model, resource, checks, changed, returned, before) {
|
|
157
|
+
if (changed.length === 0) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const fresh = session.renewed();
|
|
161
|
+
const identifier = adapterPrimaryIdentifier(model, resource);
|
|
162
|
+
const these = identifiedRows(core, identifier, resource, changed);
|
|
163
|
+
if (these === false) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const kept = checks.moveOut === undefined ? these : allOf(these, checks.moveOut);
|
|
167
|
+
const reread = checks.moveOut !== undefined || !returned;
|
|
168
|
+
const fields = afterCheckFields(core, model, resource);
|
|
169
|
+
const rows = kept === false || !reread
|
|
170
|
+
? []
|
|
171
|
+
: await fresh.rows(resource, whereOf(kept), checks.after && !returned ? fields : [...identifier]);
|
|
172
|
+
if (checks.moveOut !== undefined) {
|
|
173
|
+
const found = new Set(rows.map((row) => identityKey(identifier, row)));
|
|
174
|
+
if (changed.some((row) => !found.has(identityKey(identifier, row)))) {
|
|
175
|
+
return policyRefusal(`The policy does not allow this change: it moves a row of Resource "${resource}" out of what the caller may read.`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (checks.after) {
|
|
179
|
+
if (!returned && rows.length < changed.length) {
|
|
180
|
+
return policyRefusal(`The policy checks the rows this update on Resource "${resource}" changes after it runs, and a changed row cannot be found again by its identifier (the update changes it), so it cannot be checked.`);
|
|
181
|
+
}
|
|
182
|
+
const asked = returned
|
|
183
|
+
? changed.map((row) => rowOfFields(row, fields))
|
|
184
|
+
: rows;
|
|
185
|
+
const paired = checks.before
|
|
186
|
+
? pairedBefore(identifier, asked, before.rows)
|
|
187
|
+
: [];
|
|
188
|
+
if (paired === undefined) {
|
|
189
|
+
return policyRefusal(`The policy checks a changed row of Resource "${resource}" against the row as it was before the update, and a changed row has no such row to pair with (the update changes its identifier).`);
|
|
190
|
+
}
|
|
191
|
+
const answers = await Promise.all(asked.map((row, index) => {
|
|
192
|
+
const at = paired[index];
|
|
193
|
+
const was = at === undefined ? undefined : before.rows[at];
|
|
194
|
+
const grants = at === undefined ? undefined : before.grants?.[at];
|
|
195
|
+
const prior = was === undefined
|
|
196
|
+
? undefined
|
|
197
|
+
: {
|
|
198
|
+
row: frozenCopy(rowOfFields(was, fields)),
|
|
199
|
+
...(grants === undefined ? {} : { grants }),
|
|
200
|
+
};
|
|
201
|
+
return fresh.can(resource, "update", frozenCopy(row), prior);
|
|
202
|
+
}));
|
|
203
|
+
if (answers.includes(false)) {
|
|
204
|
+
return policyRefusal(`The policy does not allow this change: a changed row of Resource "${resource}" does not meet the condition its update grant sets.`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|