@aventara/core 1.0.0-rc.2 → 1.0.0-rc.4

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 (78) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/ERRORS.md +9 -9
  3. package/README.md +72 -12
  4. package/dist/adapters/adapter-operation.d.ts +7 -0
  5. package/dist/compiler/contract.compiler.d.ts +6 -0
  6. package/dist/compiler/contract.compiler.js +73 -12
  7. package/dist/config/computed-fields.js +2 -9
  8. package/dist/config/config-diagnostic.guidance.js +3 -3
  9. package/dist/config/config.resolver.d.ts +14 -0
  10. package/dist/config/config.resolver.js +31 -0
  11. package/dist/config/config.validator.js +82 -0
  12. package/dist/config/exact-framework-config.d.ts +8 -1
  13. package/dist/config/framework-config.d.ts +26 -1
  14. package/dist/contracts/client-contract-structure.validator.d.ts +8 -0
  15. package/dist/contracts/client-contract-structure.validator.js +6 -1
  16. package/dist/contracts/field.d.ts +11 -0
  17. package/dist/framework/contract-view.d.ts +41 -10
  18. package/dist/framework/framework.factory.js +39 -11
  19. package/dist/framework/operation-facade.d.ts +24 -5
  20. package/dist/framework/operation-facade.js +2 -1
  21. package/dist/index.d.ts +4 -1
  22. package/dist/internal.d.ts +1 -0
  23. package/dist/internal.js +1 -0
  24. package/dist/operations/database-refusal.d.ts +1 -1
  25. package/dist/operations/database-refusal.js +1 -1
  26. package/dist/operations/operation-arguments.d.ts +1 -1
  27. package/dist/operations/operation-call.d.ts +9 -5
  28. package/dist/operations/operation-result.d.ts +5 -3
  29. package/dist/operations/operation-validator-core.js +66 -33
  30. package/dist/operations/validation-code.guidance.js +2 -2
  31. package/dist/policy/policy-admission.d.ts +22 -0
  32. package/dist/policy/policy-admission.js +400 -0
  33. package/dist/policy/policy-after-update.runtime.d.ts +29 -0
  34. package/dist/policy/policy-after-update.runtime.js +131 -0
  35. package/dist/policy/policy-condition.d.ts +15 -0
  36. package/dist/policy/policy-condition.js +36 -0
  37. package/dist/policy/policy-engine.d.ts +111 -0
  38. package/dist/policy/policy-engine.js +1 -0
  39. package/dist/policy/policy-nested-write.runtime.d.ts +38 -0
  40. package/dist/policy/policy-nested-write.runtime.js +449 -0
  41. package/dist/policy/policy-read.runtime.d.ts +83 -0
  42. package/dist/policy/policy-read.runtime.js +494 -0
  43. package/dist/policy/policy-request.d.ts +37 -0
  44. package/dist/policy/policy-request.js +52 -0
  45. package/dist/policy/policy-session.d.ts +83 -0
  46. package/dist/policy/policy-session.js +234 -0
  47. package/dist/policy/policy-startup.validator.d.ts +8 -0
  48. package/dist/policy/policy-startup.validator.js +106 -0
  49. package/dist/policy/policy-write.runtime.d.ts +30 -0
  50. package/dist/policy/policy-write.runtime.js +394 -0
  51. package/dist/prebuilt/core-version.generated.d.ts +2 -0
  52. package/dist/prebuilt/core-version.generated.js +1 -0
  53. package/dist/prebuilt/prebuilt-contracts.d.ts +40 -0
  54. package/dist/prebuilt/prebuilt-contracts.emitter.d.ts +16 -0
  55. package/dist/prebuilt/prebuilt-contracts.emitter.js +50 -0
  56. package/dist/prebuilt/prebuilt-contracts.js +1 -0
  57. package/dist/prebuilt/prebuilt-contracts.loader.d.ts +21 -0
  58. package/dist/prebuilt/prebuilt-contracts.loader.js +122 -0
  59. package/dist/prebuilt/prebuilt-inputs.fingerprint.d.ts +16 -0
  60. package/dist/prebuilt/prebuilt-inputs.fingerprint.js +192 -0
  61. package/dist/runtime/computed-read-runtime.d.ts +6 -1
  62. package/dist/runtime/computed-read-runtime.js +224 -45
  63. package/dist/runtime/context-executor.runtime.js +4 -2
  64. package/dist/runtime/execution-options.d.ts +8 -0
  65. package/dist/runtime/field-validator.runtime.d.ts +9 -0
  66. package/dist/runtime/field-validator.runtime.js +36 -2
  67. package/dist/runtime/framework-warning.emitter.d.ts +6 -0
  68. package/dist/runtime/framework-warning.emitter.js +8 -0
  69. package/dist/runtime/level-lifecycle.runtime.js +3 -2
  70. package/dist/runtime/operation-code.guidance.js +4 -4
  71. package/dist/runtime/operation-request.d.ts +7 -0
  72. package/dist/runtime/operation-runtime.d.ts +9 -1
  73. package/dist/runtime/operation-runtime.js +119 -11
  74. package/dist/runtime/readonly-value.d.ts +5 -1
  75. package/dist/runtime/readonly-value.js +67 -28
  76. package/dist/transactions/transaction-runtime.d.ts +4 -1
  77. package/dist/transactions/transaction-runtime.js +27 -5
  78. package/package.json +2 -2
@@ -0,0 +1,234 @@
1
+ import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
2
+ import { validateOperationArguments } from "../operations/operation-validator-core.js";
3
+ import { normalizeOperationScalarValues } from "../runtime/scalar-runtime.js";
4
+ export class PolicyDefectError extends Error {
5
+ name = "PolicyDefectError";
6
+ }
7
+ function isEmptyRecord(value) {
8
+ return (value !== null &&
9
+ typeof value === "object" &&
10
+ !Array.isArray(value) &&
11
+ Object.keys(value).length === 0);
12
+ }
13
+ function isRecord(value) {
14
+ return value !== null && typeof value === "object" && !Array.isArray(value);
15
+ }
16
+ function checkedFragment(core, resource, where, what) {
17
+ if (!isRecord(where)) {
18
+ throw new PolicyDefectError(`${what} must be a where object.`);
19
+ }
20
+ const request = {
21
+ resource,
22
+ family: "find",
23
+ variant: "many",
24
+ arguments: { where },
25
+ };
26
+ const validation = validateOperationArguments(core, request);
27
+ const normalized = validation.ok
28
+ ? normalizeOperationScalarValues(core, {
29
+ ...request,
30
+ arguments: validation.arguments,
31
+ }, "application")
32
+ : validation;
33
+ if (!normalized.ok) {
34
+ const issues = normalized.issues
35
+ .map((issue) => `${issue.code} at ${(issue.path ?? []).slice(2).join(".") || "where"}: ${issue.message}`)
36
+ .join("; ");
37
+ throw new PolicyDefectError(`${what} is no where of Resource "${resource}" in the Core Contract (${issues}).`);
38
+ }
39
+ return normalized.arguments.where;
40
+ }
41
+ function conditionOf(core, resource, answer, what) {
42
+ if (typeof answer === "boolean") {
43
+ return answer;
44
+ }
45
+ if (isEmptyRecord(answer)) {
46
+ return true;
47
+ }
48
+ return checkedFragment(core, resource, answer, what);
49
+ }
50
+ function fieldsAnswer(core, resource, answer) {
51
+ const what = `fieldsOf(${resource})`;
52
+ if (!isRecord(answer)) {
53
+ throw new PolicyDefectError(`${what} must answer { always, perRow }.`);
54
+ }
55
+ const { always, perRow } = answer;
56
+ if (always !== "all" &&
57
+ !(Array.isArray(always) && always.every((name) => typeof name === "string"))) {
58
+ throw new PolicyDefectError(`${what}.always must be "all" or a list of field names.`);
59
+ }
60
+ if (!isRecord(perRow)) {
61
+ throw new PolicyDefectError(`${what}.perRow must be an object.`);
62
+ }
63
+ const listed = new Set(always === "all" ? [] : always);
64
+ const grants = new Map();
65
+ for (const [field, grant] of Object.entries(perRow)) {
66
+ if (listed.has(field) || grant === undefined) {
67
+ continue;
68
+ }
69
+ const condition = conditionOf(core, resource, grant, `${what}.perRow.${field}`);
70
+ if (condition === true) {
71
+ listed.add(field);
72
+ }
73
+ else if (condition !== false) {
74
+ grants.set(field, condition);
75
+ }
76
+ }
77
+ return { all: always === "all", listed, perRow: grants };
78
+ }
79
+ function questionKey(resource, where) {
80
+ try {
81
+ return `${resource}\u0000${JSON.stringify(where, (_key, value) => typeof value === "bigint" ? `#bigint(${value})` : value)}`;
82
+ }
83
+ catch {
84
+ return undefined;
85
+ }
86
+ }
87
+ function existsOperation(core, resource, where) {
88
+ const contract = lookupResource(core, resource);
89
+ const [identifier] = contract?.identifiers ?? [];
90
+ const first = contract?.operations.find?.first !== undefined;
91
+ return first && identifier !== undefined
92
+ ? {
93
+ resource,
94
+ family: "find",
95
+ variant: "first",
96
+ arguments: { where, select: [...identifier] },
97
+ }
98
+ : {
99
+ resource,
100
+ family: "find",
101
+ variant: "count",
102
+ arguments: { where },
103
+ };
104
+ }
105
+ export function openPolicySession(policy, core, principal, origin, execution) {
106
+ const caches = {
107
+ scopes: new Map(),
108
+ createGrants: new Map(),
109
+ fields: new Map(),
110
+ exists: new WeakMap(),
111
+ };
112
+ return sessionOn(policy, core, principal, origin, execution, caches);
113
+ }
114
+ function sessionOn(policy, core, principal, origin, execution, caches) {
115
+ const { engine } = policy;
116
+ const exists = (resource, where) => {
117
+ const key = questionKey(resource, where);
118
+ let answers = caches.exists.get(execution.executor);
119
+ if (answers === undefined) {
120
+ answers = new Map();
121
+ caches.exists.set(execution.executor, answers);
122
+ }
123
+ const known = key === undefined ? undefined : answers.get(key);
124
+ if (known !== undefined) {
125
+ return known;
126
+ }
127
+ const operation = existsOperation(core, resource, where);
128
+ const answer = execution.boundary
129
+ .execute(() => execution.executor.execute(operation))
130
+ .then((result) => typeof result === "number" ? result > 0 : result != null);
131
+ if (key !== undefined) {
132
+ answers.set(key, answer);
133
+ }
134
+ return answer;
135
+ };
136
+ const engineExists = async (resource, where) => {
137
+ if (typeof resource !== "string" ||
138
+ lookupResource(core, resource) === undefined) {
139
+ throw new PolicyDefectError(`exists was asked about "${String(resource)}", which is no Resource of the Core Contract.`);
140
+ }
141
+ const condition = conditionOf(core, resource, where, "exists's where");
142
+ if (typeof condition === "boolean") {
143
+ return exists(resource, {});
144
+ }
145
+ return exists(resource, condition);
146
+ };
147
+ return {
148
+ principal,
149
+ origin,
150
+ execution,
151
+ async can(resource, action, row) {
152
+ const answer = await engine.can({
153
+ principal,
154
+ origin,
155
+ resource,
156
+ action,
157
+ ...(row === undefined ? {} : { row }),
158
+ exists: engineExists,
159
+ });
160
+ if (typeof answer !== "boolean") {
161
+ throw new PolicyDefectError(`can(${resource}, ${action}) must answer true or false.`);
162
+ }
163
+ return answer;
164
+ },
165
+ scope(resource, action) {
166
+ const key = `${resource}\u0000${action}`;
167
+ let scope = caches.scopes.get(key);
168
+ if (scope === undefined) {
169
+ scope = Promise.resolve()
170
+ .then(() => engine.scopeOf({ principal, origin, resource, action }))
171
+ .then((answer) => conditionOf(core, resource, answer, `scopeOf(${resource}, ${action})`));
172
+ caches.scopes.set(key, scope);
173
+ }
174
+ return scope;
175
+ },
176
+ fields(resource, access) {
177
+ const key = `${resource}\u0000${access}`;
178
+ let fields = caches.fields.get(key);
179
+ if (fields === undefined) {
180
+ fields = Promise.resolve()
181
+ .then(() => engine.fieldsOf({ principal, origin, resource, access }))
182
+ .then((answer) => fieldsAnswer(core, resource, answer));
183
+ caches.fields.set(key, fields);
184
+ }
185
+ return fields;
186
+ },
187
+ exists,
188
+ async rows(resource, where, select) {
189
+ if (lookupResource(core, resource)?.operations.find?.many === undefined) {
190
+ throw new PolicyDefectError(`The policy needs the rows of Resource "${resource}", and its Core Contract offers no find.many.`);
191
+ }
192
+ const operation = {
193
+ resource,
194
+ family: "find",
195
+ variant: "many",
196
+ arguments: { where, select: [...select] },
197
+ };
198
+ const answer = await execution.boundary.execute(() => execution.executor.execute(operation));
199
+ return Array.isArray(answer)
200
+ ? answer.filter((row) => isRecord(row))
201
+ : [];
202
+ },
203
+ grantCreate(field, grant) {
204
+ caches.createGrants.set(field, grant);
205
+ },
206
+ createGrants() {
207
+ return caches.createGrants;
208
+ },
209
+ checksAfter(resource, action) {
210
+ return (Object.hasOwn(policy.afterChecks, resource) &&
211
+ policy.afterChecks[resource]?.includes(action) === true);
212
+ },
213
+ on(next) {
214
+ return sessionOn(policy, core, principal, origin, next, caches);
215
+ },
216
+ renewed() {
217
+ return sessionOn(policy, core, principal, origin, execution, {
218
+ ...caches,
219
+ exists: new WeakMap(),
220
+ });
221
+ },
222
+ };
223
+ }
224
+ export function alwaysReadable(core, resource, fields, field) {
225
+ return (fields.listed.has(field) ||
226
+ (fields.all &&
227
+ lookupField(lookupResource(core, resource), field)?.governed !== true));
228
+ }
229
+ export function fieldAccess(core, resource, fields, field) {
230
+ if (alwaysReadable(core, resource, fields, field)) {
231
+ return "always";
232
+ }
233
+ return fields.perRow.get(field) ?? "never";
234
+ }
@@ -0,0 +1,8 @@
1
+ import type { ResolvedPolicyConfig } from "../config/config.resolver.js";
2
+ import type { AvConfigDiagnostic } from "../config/config-diagnostic.js";
3
+ import type { AvContractCore } from "../contracts/contract.js";
4
+ /**
5
+ * The policy's governed set, rule paths and after checks checked against the
6
+ * Core Contract: every problem, naming the Resource or the rule, or none.
7
+ */
8
+ export declare function policyStartupDiagnostics(core: AvContractCore, policy: ResolvedPolicyConfig): readonly AvConfigDiagnostic[];
@@ -0,0 +1,106 @@
1
+ import { lookupField, lookupResource } from "../contracts/contract-lookup.js";
2
+ const TO_MANY_OPERATORS = new Set(["some", "every", "none"]);
3
+ function invalid(path, message) {
4
+ return { code: "CONFIG_INVALID_VALUE", path, message };
5
+ }
6
+ function governedDiagnostics(core, governed) {
7
+ const diagnostics = [];
8
+ for (const [resourceName, names] of Object.entries(governed)) {
9
+ const path = `policy.engine.governed.${resourceName}`;
10
+ const resource = lookupResource(core, resourceName);
11
+ if (resource === undefined) {
12
+ diagnostics.push(invalid(path, `${path}: "${resourceName}" is no Resource of the Core Contract`));
13
+ continue;
14
+ }
15
+ for (const name of names) {
16
+ if (lookupField(resource, name) === undefined) {
17
+ diagnostics.push(invalid(path, `${path}: "${name}" is no field or relation of Resource "${resourceName}" in the Core Contract`));
18
+ }
19
+ }
20
+ }
21
+ return diagnostics;
22
+ }
23
+ function afterCheckDiagnostics(core, afterChecks) {
24
+ return Object.keys(afterChecks).flatMap((resourceName) => {
25
+ const path = `policy.engine.afterChecks.${resourceName}`;
26
+ return lookupResource(core, resourceName) === undefined
27
+ ? [
28
+ invalid(path, `${path}: "${resourceName}" is no Resource of the Core Contract`),
29
+ ]
30
+ : [];
31
+ });
32
+ }
33
+ function pathFault(core, path) {
34
+ const [resourceName, ...segments] = path;
35
+ let resource = resourceName === undefined ? undefined : lookupResource(core, resourceName);
36
+ if (resource === undefined) {
37
+ return `"${resourceName ?? ""}" is no Resource of the Core Contract`;
38
+ }
39
+ let current = resourceName;
40
+ for (let index = 0; index < segments.length; index += 1) {
41
+ const segment = segments[index];
42
+ const field = lookupField(resource, segment);
43
+ if (field === undefined) {
44
+ return `"${segment}" is no field or relation of Resource "${current}"`;
45
+ }
46
+ const filters = field.capabilities.filter ?? [];
47
+ if (field.kind === "scalar") {
48
+ if (filters.length === 0) {
49
+ return `field "${current}.${segment}" cannot be filtered`;
50
+ }
51
+ const operator = segments[index + 1];
52
+ if (operator !== undefined && !filters.includes(operator)) {
53
+ return `field "${current}.${segment}" has no filter operator "${operator}"`;
54
+ }
55
+ if (index + 2 < segments.length) {
56
+ return `the path goes on past scalar field "${current}.${segment}"`;
57
+ }
58
+ return undefined;
59
+ }
60
+ const target = lookupResource(core, field.target);
61
+ if (target === undefined) {
62
+ return `relation "${current}.${segment}" leads to no Resource of the Core Contract`;
63
+ }
64
+ if (field.cardinality === "many") {
65
+ const operator = segments[index + 1];
66
+ if (operator === undefined || !TO_MANY_OPERATORS.has(operator)) {
67
+ return `to-many relation "${current}.${segment}" must be followed by some, every or none`;
68
+ }
69
+ if (!filters.includes(operator)) {
70
+ return `relation "${current}.${segment}" has no "${operator}" filter`;
71
+ }
72
+ index += 1;
73
+ }
74
+ else if (index + 1 === segments.length) {
75
+ return filters.includes("null")
76
+ ? undefined
77
+ : `relation "${current}.${segment}" has no null filter`;
78
+ }
79
+ else if (!filters.includes("match")) {
80
+ return `relation "${current}.${segment}" cannot filter its target directly`;
81
+ }
82
+ resource = target;
83
+ current = field.target;
84
+ }
85
+ return `the path ends at Resource "${current}", not at a field`;
86
+ }
87
+ function pathDiagnostics(core, paths) {
88
+ const diagnostics = [];
89
+ for (const [rule, list] of Object.entries(paths)) {
90
+ const at = `policy.engine.paths.${rule}`;
91
+ for (const path of list) {
92
+ const fault = pathFault(core, path);
93
+ if (fault !== undefined) {
94
+ diagnostics.push(invalid(at, `rule "${rule}" filters on "${path.join(".")}": ${fault}`));
95
+ }
96
+ }
97
+ }
98
+ return diagnostics;
99
+ }
100
+ export function policyStartupDiagnostics(core, policy) {
101
+ return [
102
+ ...governedDiagnostics(core, policy.governed),
103
+ ...pathDiagnostics(core, policy.paths),
104
+ ...afterCheckDiagnostics(core, policy.afterChecks),
105
+ ];
106
+ }
@@ -0,0 +1,30 @@
1
+ import type { AvAdapterModel } from "../adapters/adapter-model.js";
2
+ import type { AvContractCore } from "../contracts/contract.js";
3
+ import type { AvOperationIdentity } from "../operations/operation-identity.js";
4
+ import type { PlannedReadOperation } from "../runtime/computed-read-runtime.js";
5
+ import type { AvOperationFailureResponse } from "../runtime/operation-response.js";
6
+ import { type PolicyRead } from "./policy-read.runtime.js";
7
+ import type { PolicySession } from "./policy-session.js";
8
+ /** A write's policy: what the adapter runs, and how a unique write's miss answers. */
9
+ export type PolicyWrite = PolicyRead & {
10
+ /** A unique write that matched nothing: `A4002` for a row the caller can read, `A2003` otherwise. */
11
+ readonly miss?: () => Promise<AvOperationFailureResponse>;
12
+ };
13
+ /**
14
+ * Whether a write the policy checks needs an interactive transaction to roll
15
+ * it back in. Where it has none, {@link enforceWritePolicy} refuses exactly
16
+ * these writes before they run. `planned` plans the request's read, asked only
17
+ * when the answer depends on the rows it returns.
18
+ */
19
+ export declare function writeNeedsTransaction(session: PolicySession, core: AvContractCore, request: AvOperationIdentity & {
20
+ readonly resource: string;
21
+ }, planned: () => PlannedReadOperation, adapterArguments: unknown): Promise<boolean>;
22
+ /**
23
+ * One write's policy (see the module comment): the refusal, the answer when it
24
+ * reaches no row, or what the adapter runs and how its answer is checked.
25
+ * `inTransaction` says whether the write runs in an interactive transaction a
26
+ * failed check rolls back.
27
+ */
28
+ export declare function enforceWritePolicy(session: PolicySession, core: AvContractCore, model: AvAdapterModel, request: AvOperationIdentity & {
29
+ readonly resource: string;
30
+ }, plan: PlannedReadOperation, adapterArguments: unknown, inTransaction: boolean): Promise<PolicyWrite>;