@astrale-os/sdk 0.6.0-beta.41 → 0.6.0-beta.43
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/README.md +22 -2
- package/dist/application/action/context.d.ts +49 -18
- package/dist/application/action/index.d.ts +1 -1
- package/dist/application/error/error.d.ts +16 -1
- package/dist/application/error/error.js +39 -5
- package/dist/application/error/index.d.ts +1 -1
- package/dist/application/error/index.js +1 -1
- package/dist/application/mutation/authoring/builder.d.ts +81 -33
- package/dist/application/mutation/authoring/builder.js +100 -30
- package/dist/application/mutation/authoring/guard.d.ts +37 -0
- package/dist/application/mutation/authoring/guard.js +150 -0
- package/dist/application/mutation/authoring/index.d.ts +5 -2
- package/dist/application/mutation/authoring/index.js +2 -1
- package/dist/application/mutation/authoring/names.d.ts +19 -0
- package/dist/application/mutation/authoring/names.js +83 -0
- package/dist/application/mutation/authoring/transition.d.ts +3 -2
- package/dist/application/mutation/define.d.ts +24 -10
- package/dist/application/mutation/define.js +49 -53
- package/dist/application/mutation/execution/execute.d.ts +2 -2
- package/dist/application/mutation/execution/execute.js +78 -7
- package/dist/application/mutation/execution/executor.d.ts +4 -1
- package/dist/application/mutation/execution/executor.js +6 -1
- package/dist/application/mutation/failure.d.ts +11 -1
- package/dist/application/mutation/failure.js +18 -3
- package/dist/application/mutation/index.d.ts +8 -4
- package/dist/application/mutation/index.js +5 -2
- package/dist/application/mutation/legacy/v6/builder.d.ts +84 -0
- package/dist/application/mutation/legacy/v6/define.d.ts +18 -0
- package/dist/application/mutation/legacy/v6/define.js +56 -0
- package/dist/application/mutation/legacy/v6/mutation.d.ts +28 -0
- package/dist/application/mutation/legacy/v6/mutation.js +1 -0
- package/dist/application/mutation/{authoring → legacy/v6}/precondition.d.ts +4 -0
- package/dist/application/mutation/mutation.d.ts +30 -19
- package/dist/application/query/answer/answer.d.ts +117 -0
- package/dist/application/query/answer/answer.js +1 -0
- package/dist/application/query/answer/conformance.d.ts +4 -0
- package/dist/application/query/answer/conformance.js +27 -0
- package/dist/application/query/answer/facts.d.ts +17 -0
- package/dist/application/query/answer/facts.js +57 -0
- package/dist/application/query/composite/builder.d.ts +1 -1
- package/dist/application/query/composite/builder.js +1 -1
- package/dist/application/query/composite/plan.d.ts +1 -1
- package/dist/application/query/composite/plan.js +1 -1
- package/dist/application/query/composite/query.d.ts +1 -2
- package/dist/application/query/define.d.ts +2 -33
- package/dist/application/query/define.js +3 -130
- package/dist/application/query/definition.d.ts +12 -0
- package/dist/application/query/definition.js +47 -0
- package/dist/application/query/execution/bounds.d.ts +22 -0
- package/dist/application/query/execution/bounds.js +88 -0
- package/dist/application/query/execution/crossings.d.ts +25 -0
- package/dist/application/query/execution/crossings.js +192 -0
- package/dist/application/query/execution/error.d.ts +68 -0
- package/dist/application/query/execution/error.js +50 -0
- package/dist/application/query/execution/execute.d.ts +2 -7
- package/dist/application/query/execution/execute.js +3 -51
- package/dist/application/query/execution/recipe.d.ts +85 -0
- package/dist/application/query/execution/recipe.js +324 -0
- package/dist/application/query/execution/refusal.d.ts +37 -0
- package/dist/application/query/execution/refusal.js +80 -0
- package/dist/application/query/execution/rows.d.ts +42 -0
- package/dist/application/query/execution/rows.js +308 -0
- package/dist/application/query/execution/split.d.ts +24 -0
- package/dist/application/query/execution/split.js +190 -0
- package/dist/application/query/handles/build.d.ts +10 -0
- package/dist/application/query/handles/build.js +53 -0
- package/dist/application/query/handles/conditions.d.ts +94 -0
- package/dist/application/query/handles/conditions.js +1 -0
- package/dist/application/query/handles/domain.d.ts +157 -0
- package/dist/application/query/handles/domain.js +1 -0
- package/dist/application/query/handles/handles.d.ts +268 -0
- package/dist/application/query/handles/handles.js +1 -0
- package/dist/application/query/handles/model.d.ts +96 -0
- package/dist/application/query/handles/model.js +246 -0
- package/dist/application/query/handles/plan.d.ts +99 -0
- package/dist/application/query/handles/plan.js +37 -0
- package/dist/application/query/handles/reads.d.ts +106 -0
- package/dist/application/query/handles/reads.js +839 -0
- package/dist/application/query/handles/relations.d.ts +100 -0
- package/dist/application/query/handles/relations.js +1 -0
- package/dist/application/query/handles/rows.d.ts +94 -0
- package/dist/application/query/handles/rows.js +4 -0
- package/dist/application/query/handles/terms.d.ts +103 -0
- package/dist/application/query/handles/terms.js +282 -0
- package/dist/application/query/index.d.ts +10 -7
- package/dist/application/query/index.js +9 -4
- package/dist/application/query/language/index.d.ts +2 -0
- package/dist/application/query/language/index.js +2 -0
- package/dist/application/query/language/query.d.ts +323 -0
- package/dist/application/query/language/query.js +2 -0
- package/dist/application/query/{collection → legacy/v6/collection}/identity.d.ts +2 -2
- package/dist/application/query/{collection → legacy/v6/collection}/metadata.d.ts +1 -1
- package/dist/application/query/{collection → legacy/v6/collection}/union.d.ts +3 -3
- package/dist/application/query/{collection → legacy/v6/collection}/union.js +1 -1
- package/dist/application/query/{conformance.d.ts → legacy/v6/conformance.d.ts} +3 -3
- package/dist/application/query/{conformance.js → legacy/v6/conformance.js} +1 -1
- package/dist/application/query/legacy/v6/define.d.ts +37 -0
- package/dist/application/query/legacy/v6/define.js +90 -0
- package/dist/application/query/legacy/v6/execution/execute.d.ts +16 -0
- package/dist/application/query/legacy/v6/execution/execute.js +50 -0
- package/dist/application/query/legacy/v6/index.d.ts +6 -0
- package/dist/application/query/legacy/v6/index.js +3 -0
- package/dist/application/query/legacy/v6/kernel.d.ts +6 -0
- package/dist/application/query/legacy/v6/kernel.js +5 -0
- package/dist/application/query/{single → legacy/v6/single}/pagination.js +1 -1
- package/dist/application/query/{single → legacy/v6/single}/query.d.ts +2 -14
- package/dist/application/query/legacy/v6/single/query.js +1 -0
- package/dist/application/query/query.d.ts +16 -1
- package/dist/application/query/recipe/define.d.ts +44 -0
- package/dist/application/query/recipe/define.js +95 -0
- package/dist/application/query/recipe/recipe.d.ts +134 -0
- package/dist/application/query/recipe/recipe.js +1 -0
- package/dist/application/workflow/durable.d.ts +22 -8
- package/dist/deployment/build/build.d.ts +8 -1
- package/dist/deployment/build/material.js +3 -0
- package/dist/execution/invocation/capabilities/bind.d.ts +6 -4
- package/dist/execution/invocation/capabilities/bind.js +39 -3
- package/dist/execution/invocation/dispatch/action.js +4 -2
- package/dist/execution/invocation/dispatch/context.js +1 -1
- package/dist/execution/invocation/dispatch/workflow.js +4 -2
- package/dist/execution/invocation/result/index.d.ts +1 -0
- package/dist/execution/invocation/result/index.js +1 -0
- package/dist/execution/invocation/result/recipe.d.ts +6 -0
- package/dist/execution/invocation/result/recipe.js +17 -0
- package/dist/execution/service/create.js +1 -0
- package/dist/execution/workflows/failure.d.ts +23 -6
- package/dist/execution/workflows/failure.js +78 -18
- package/dist/execution/workflows/receiver.d.ts +3 -0
- package/dist/execution/workflows/receiver.js +1 -0
- package/dist/execution/workflows/run.js +13 -9
- package/dist/execution/workflows/steps.d.ts +3 -0
- package/dist/execution/workflows/steps.js +2 -2
- package/dist/platform/schema/kernel-compiled.d.ts +2 -0
- package/dist/platform/schema/kernel-compiled.js +2 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/recipe.d.ts +17 -0
- package/dist/testing/recipe.js +34 -0
- package/dist/tooling/cli/orchestrate.js +6 -2
- package/dist/tooling/linter/adapters/typescript/index.d.ts +2 -1
- package/dist/tooling/linter/adapters/typescript/index.js +2 -1
- package/dist/tooling/linter/adapters/typescript/query-observation.js +37 -12
- package/dist/tooling/linter/adapters/typescript/recipe-definitions.d.ts +34 -0
- package/dist/tooling/linter/adapters/typescript/recipe-definitions.js +88 -0
- package/dist/tooling/linter/adapters/typescript/type-analysis.d.ts +2 -0
- package/dist/tooling/linter/adapters/typescript/type-analysis.js +10 -0
- package/dist/tooling/linter/implementations/source/global.js +15 -9
- package/dist/tooling/linter/implementations/source/mutations.js +128 -19
- package/dist/tooling/linter/implementations/source/policy-edges.d.ts +2 -0
- package/dist/tooling/linter/implementations/source/policy-edges.js +113 -0
- package/dist/tooling/linter/implementations/source/recipes.d.ts +2 -0
- package/dist/tooling/linter/implementations/source/recipes.js +425 -0
- package/dist/tooling/linter/implementations/source/shared.d.ts +7 -1
- package/dist/tooling/linter/implementations/source/shared.js +31 -10
- package/dist/tooling/linter/implementations/source/verifiers.js +4 -0
- package/dist/tooling/linter/policy/generated.js +2 -2
- package/dist/tooling/linter/requirements/registry.js +4 -0
- package/docs/durable-workflows.md +48 -20
- package/docs/handler-dependencies.md +76 -4
- package/docs/legacy/query-v6.md +62 -0
- package/docs/queries.md +442 -0
- package/package.json +11 -8
- /package/dist/application/{query/single/query.js → mutation/legacy/v6/builder.js} +0 -0
- /package/dist/application/mutation/{authoring → legacy/v6}/precondition.js +0 -0
- /package/dist/application/query/{collection → legacy/v6/collection}/identity.js +0 -0
- /package/dist/application/query/{collection → legacy/v6/collection}/limits.d.ts +0 -0
- /package/dist/application/query/{collection → legacy/v6/collection}/limits.js +0 -0
- /package/dist/application/query/{collection → legacy/v6/collection}/metadata.js +0 -0
- /package/dist/application/query/{execution → legacy/v6/execution}/pages.d.ts +0 -0
- /package/dist/application/query/{execution → legacy/v6/execution}/pages.js +0 -0
- /package/dist/application/query/{single → legacy/v6/single}/index.d.ts +0 -0
- /package/dist/application/query/{single → legacy/v6/single}/index.js +0 -0
- /package/dist/application/query/{single → legacy/v6/single}/node.d.ts +0 -0
- /package/dist/application/query/{single → legacy/v6/single}/node.js +0 -0
- /package/dist/application/query/{single → legacy/v6/single}/pagination.d.ts +0 -0
- /package/dist/application/query/{single → legacy/v6/single}/selection.d.ts +0 -0
- /package/dist/application/query/{single → legacy/v6/single}/selection.js +0 -0
|
@@ -14,6 +14,7 @@ export const mutationRules = [
|
|
|
14
14
|
const targets = [];
|
|
15
15
|
for (const file of production(project, 'mutations')) {
|
|
16
16
|
const builderScopes = mutationBuilderScopes(file);
|
|
17
|
+
const handles = handleParameters(file);
|
|
17
18
|
visit(file.source, (node) => {
|
|
18
19
|
if (!ts.isCallExpression(node))
|
|
19
20
|
return;
|
|
@@ -25,11 +26,11 @@ export const mutationRules = [
|
|
|
25
26
|
return;
|
|
26
27
|
const input = staticObject(node.arguments[0]);
|
|
27
28
|
const selectedClass = input && assignedValue(input, 'class');
|
|
28
|
-
const target = selectedClass && dependencyClassTarget(selectedClass);
|
|
29
|
+
const target = selectedClass && dependencyClassTarget(selectedClass, handles);
|
|
29
30
|
if (selectedClass !== undefined && target !== undefined) {
|
|
30
31
|
targets.push({ file, node: selectedClass, ...target, operation: classOperation });
|
|
31
32
|
}
|
|
32
|
-
else if (selectedClass && expressionSelectsDependency(selectedClass)) {
|
|
33
|
+
else if (selectedClass && expressionSelectsDependency(selectedClass, handles)) {
|
|
33
34
|
evidence.push(ambiguity(file, selectedClass, 'Mutation plan selects a foreign Class dynamically, so its Domain requirement cannot be proven.'));
|
|
34
35
|
}
|
|
35
36
|
});
|
|
@@ -97,6 +98,7 @@ export const mutationRules = [
|
|
|
97
98
|
const machineProperties = machinePropertiesByClass(project);
|
|
98
99
|
for (const file of production(project, 'mutations')) {
|
|
99
100
|
const builderScopes = mutationBuilderScopes(file);
|
|
101
|
+
const handles = handleParameters(file);
|
|
100
102
|
visit(file.source, (node) => {
|
|
101
103
|
if (!ts.isCallExpression(node) ||
|
|
102
104
|
mutationBuilderMember(node, builderScopes) !== 'createNode') {
|
|
@@ -109,7 +111,7 @@ export const mutationRules = [
|
|
|
109
111
|
}
|
|
110
112
|
return;
|
|
111
113
|
}
|
|
112
|
-
const selectedClass = propertiesForClass(input, machineProperties);
|
|
114
|
+
const selectedClass = propertiesForClass(input, machineProperties, handles);
|
|
113
115
|
if (selectedClass.kind === 'ambiguous') {
|
|
114
116
|
evidence.push(ambiguity(file, assignedValue(input, 'class') ?? input, 'Mutation createNode Class has no statically resolvable Schema owner.'));
|
|
115
117
|
return;
|
|
@@ -170,6 +172,7 @@ export const mutationRules = [
|
|
|
170
172
|
const machineProperties = machinePropertiesByClass(project);
|
|
171
173
|
for (const file of production(project, 'mutations')) {
|
|
172
174
|
const builderScopes = mutationBuilderScopes(file);
|
|
175
|
+
const handles = handleParameters(file);
|
|
173
176
|
visit(file.source, (node) => {
|
|
174
177
|
if (!ts.isCallExpression(node))
|
|
175
178
|
return;
|
|
@@ -184,7 +187,7 @@ export const mutationRules = [
|
|
|
184
187
|
evidence.push(ambiguity(file, node, 'Mutation updateNode input is dynamic and may bypass a StateMachine transition.'));
|
|
185
188
|
return;
|
|
186
189
|
}
|
|
187
|
-
const selectedClass = propertiesForClass(input, machineProperties);
|
|
190
|
+
const selectedClass = propertiesForClass(input, machineProperties, handles);
|
|
188
191
|
if (selectedClass.kind === 'ambiguous') {
|
|
189
192
|
evidence.push(ambiguity(file, assignedValue(input, 'class') ?? input, 'Mutation updateNode Class has no statically resolvable Schema owner.'));
|
|
190
193
|
return;
|
|
@@ -265,7 +268,7 @@ export const mutationRules = [
|
|
|
265
268
|
evidence.push(violation(file, input, `StateMachine transition has no ${required} intent.`));
|
|
266
269
|
}
|
|
267
270
|
}
|
|
268
|
-
const selectedClass = propertiesForClass(input, machineProperties);
|
|
271
|
+
const selectedClass = propertiesForClass(input, machineProperties, handles);
|
|
269
272
|
if (selectedClass.kind === 'ambiguous') {
|
|
270
273
|
evidence.push(ambiguity(file, assignedValue(input, 'class') ?? input, 'StateMachine transition Class has no statically resolvable Schema owner.'));
|
|
271
274
|
}
|
|
@@ -305,8 +308,8 @@ export const mutationRules = [
|
|
|
305
308
|
evidence.push(violation(file, object, 'Mutation definition has no build callback.'));
|
|
306
309
|
continue;
|
|
307
310
|
}
|
|
308
|
-
if (build
|
|
309
|
-
evidence.push(violation(file, build,
|
|
311
|
+
if (builderParameter(definition, build) === undefined) {
|
|
312
|
+
evidence.push(violation(file, build, `Mutation build has no callback-scoped builder as its ${definition.id === undefined ? 'second' : 'third'} parameter.`));
|
|
310
313
|
}
|
|
311
314
|
if (returnedValues(build) > 0) {
|
|
312
315
|
evidence.push(violation(file, build, 'Mutation build returns a value.'));
|
|
@@ -350,7 +353,7 @@ export const mutationRules = [
|
|
|
350
353
|
const build = object && callback(object, 'build');
|
|
351
354
|
if (!build)
|
|
352
355
|
continue;
|
|
353
|
-
const builderName = build
|
|
356
|
+
const builderName = builderParameter(definition, build)?.name;
|
|
354
357
|
if (!builderName || !ts.isIdentifier(builderName)) {
|
|
355
358
|
evidence.push(ambiguity(file, build, 'Mutation build builder parameter is not an identifier.'));
|
|
356
359
|
continue;
|
|
@@ -405,7 +408,7 @@ export const mutationRules = [
|
|
|
405
408
|
const build = object && callback(object, 'build');
|
|
406
409
|
if (!build)
|
|
407
410
|
continue;
|
|
408
|
-
const builderName = build
|
|
411
|
+
const builderName = builderParameter(definition, build)?.name;
|
|
409
412
|
visitOwnBody(build, (node) => {
|
|
410
413
|
if (!ts.isCallExpression(node))
|
|
411
414
|
return;
|
|
@@ -516,7 +519,7 @@ function mutationBuilderScopes(file) {
|
|
|
516
519
|
if (definition.origin !== 'resolved')
|
|
517
520
|
continue;
|
|
518
521
|
const build = definition.object && callback(definition.object, 'build');
|
|
519
|
-
const builder = build?.
|
|
522
|
+
const builder = build && builderParameter(definition, build)?.name;
|
|
520
523
|
if (build && builder && ts.isIdentifier(builder))
|
|
521
524
|
add(build, builder);
|
|
522
525
|
}
|
|
@@ -537,7 +540,7 @@ function mutationBuilderScopes(file) {
|
|
|
537
540
|
continue;
|
|
538
541
|
}
|
|
539
542
|
for (const [imported, local] of sourceImport.bindings) {
|
|
540
|
-
if (imported === 'RichMutationBuilder')
|
|
543
|
+
if (imported === 'RichMutationBuilder' || imported === 'RecipeMutationBuilder')
|
|
541
544
|
richBuilderTypes.add(local);
|
|
542
545
|
}
|
|
543
546
|
}
|
|
@@ -659,7 +662,14 @@ function classOperationForBuilder(operation) {
|
|
|
659
662
|
return undefined;
|
|
660
663
|
}
|
|
661
664
|
}
|
|
662
|
-
function dependencyClassTarget(expression) {
|
|
665
|
+
function dependencyClassTarget(expression, handles) {
|
|
666
|
+
// A recipe names a dependency's Class by its handle, `d.<alias>.<Class>`.
|
|
667
|
+
const path = handlePath(expression, handles);
|
|
668
|
+
if (path !== undefined) {
|
|
669
|
+
return path.length === 2 && !OWN_HANDLES.has(path[0])
|
|
670
|
+
? { dependency: path[0], className: path[1] }
|
|
671
|
+
: undefined;
|
|
672
|
+
}
|
|
663
673
|
const chain = staticPropertyChain(expression);
|
|
664
674
|
if (!chain)
|
|
665
675
|
return undefined;
|
|
@@ -670,10 +680,105 @@ function dependencyClassTarget(expression) {
|
|
|
670
680
|
}
|
|
671
681
|
return { dependency: suffix[dependency + 1], className: suffix[dependency + 3] };
|
|
672
682
|
}
|
|
673
|
-
function expressionSelectsDependency(expression) {
|
|
683
|
+
function expressionSelectsDependency(expression, handles) {
|
|
684
|
+
const root = firstIdentifier(unwrap(expression));
|
|
685
|
+
// A handle the source does not name statically, `d[alias]`, may be a dependency's.
|
|
686
|
+
if (root !== undefined && handlePath(root, handles) !== undefined)
|
|
687
|
+
return handlePath(expression, handles) === undefined;
|
|
674
688
|
const chain = staticPropertyChain(expression);
|
|
675
689
|
return chain?.includes('dependencies') ?? false;
|
|
676
690
|
}
|
|
691
|
+
/** The handles of `d` that are not dependency aliases (S1). */
|
|
692
|
+
const OWN_HANDLES = new Set(['kernel', 'core', 'origin', 'document']);
|
|
693
|
+
function handleParameters(file) {
|
|
694
|
+
const parameters = new Set();
|
|
695
|
+
for (const definition of definitionObjects(file, 'defineMutation')) {
|
|
696
|
+
if (definition.origin !== 'resolved' || definition.id === undefined)
|
|
697
|
+
continue;
|
|
698
|
+
const build = definition.object && callback(definition.object, 'build');
|
|
699
|
+
const parameter = build?.parameters[1];
|
|
700
|
+
if (parameter !== undefined)
|
|
701
|
+
parameters.add(parameter);
|
|
702
|
+
}
|
|
703
|
+
const handleTypes = new Set();
|
|
704
|
+
for (const sourceImport of file.imports) {
|
|
705
|
+
if (sourceImport.specifier !== '@astrale-os/sdk' &&
|
|
706
|
+
sourceImport.specifier !== '@astrale-os/sdk/query')
|
|
707
|
+
continue;
|
|
708
|
+
for (const [imported, local] of sourceImport.bindings)
|
|
709
|
+
if (imported === 'Handles')
|
|
710
|
+
handleTypes.add(local);
|
|
711
|
+
}
|
|
712
|
+
if (handleTypes.size > 0)
|
|
713
|
+
visit(file.source, (node) => {
|
|
714
|
+
if (ts.isParameter(node) &&
|
|
715
|
+
node.type !== undefined &&
|
|
716
|
+
ts.isTypeReferenceNode(node.type) &&
|
|
717
|
+
ts.isIdentifier(node.type.typeName) &&
|
|
718
|
+
handleTypes.has(node.type.typeName.text))
|
|
719
|
+
parameters.add(node);
|
|
720
|
+
});
|
|
721
|
+
return parameters;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* The handle a recipe's expression names, as its path from `d`: `d.Item` and a destructured
|
|
725
|
+
* `{ Item }` are `['Item']`, `d.directory.Person` is `['directory', 'Person']`. Undefined for an
|
|
726
|
+
* expression that does not start from the handles, or not statically.
|
|
727
|
+
*/
|
|
728
|
+
function handlePath(expression, handles, seen = new Set()) {
|
|
729
|
+
const value = unwrap(expression);
|
|
730
|
+
if (handles.size === 0)
|
|
731
|
+
return undefined;
|
|
732
|
+
if (ts.isIdentifier(value) && !seen.has(value.text)) {
|
|
733
|
+
const initializer = visibleConstInitializer(value);
|
|
734
|
+
if (initializer !== undefined)
|
|
735
|
+
return handlePath(initializer, handles, new Set([...seen, value.text]));
|
|
736
|
+
}
|
|
737
|
+
const chain = propertyChain(value);
|
|
738
|
+
const root = firstIdentifier(value);
|
|
739
|
+
const binding = root && identifierDeclaration(root);
|
|
740
|
+
const prefix = binding && bindingPath(binding, handles, seen);
|
|
741
|
+
return chain && prefix && [...prefix, ...chain.slice(1)];
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* The path of a binding from the handles: `{ directory: { Person } }` binds `Person` at
|
|
745
|
+
* `['directory', 'Person']`, in a handles parameter or in a constant the handles initialize,
|
|
746
|
+
* `const { directory } = d`.
|
|
747
|
+
*/
|
|
748
|
+
function bindingPath(binding, handles, seen) {
|
|
749
|
+
const path = [];
|
|
750
|
+
let current = binding;
|
|
751
|
+
while (ts.isBindingElement(current.parent)) {
|
|
752
|
+
const element = current.parent;
|
|
753
|
+
const name = element.propertyName ?? element.name;
|
|
754
|
+
if (!ts.isObjectBindingPattern(element.parent) ||
|
|
755
|
+
element.dotDotDotToken ||
|
|
756
|
+
!(ts.isIdentifier(name) || ts.isStringLiteral(name)))
|
|
757
|
+
return undefined;
|
|
758
|
+
path.unshift(name.text);
|
|
759
|
+
current = element.parent;
|
|
760
|
+
}
|
|
761
|
+
const declaration = current.parent;
|
|
762
|
+
if (declaration !== undefined &&
|
|
763
|
+
ts.isVariableDeclaration(declaration) &&
|
|
764
|
+
declaration.name === current &&
|
|
765
|
+
declaration.initializer !== undefined &&
|
|
766
|
+
(declaration.parent.flags & ts.NodeFlags.Const) !== 0 &&
|
|
767
|
+
!seen.has(binding.text)) {
|
|
768
|
+
const prefix = handlePath(declaration.initializer, handles, new Set([...seen, binding.text]));
|
|
769
|
+
return prefix && [...prefix, ...path];
|
|
770
|
+
}
|
|
771
|
+
return declaration !== undefined &&
|
|
772
|
+
ts.isParameter(declaration) &&
|
|
773
|
+
declaration.name === current &&
|
|
774
|
+
handles.has(declaration)
|
|
775
|
+
? path
|
|
776
|
+
: undefined;
|
|
777
|
+
}
|
|
778
|
+
/** The builder a Mutation's `build` receives: second in the closure form, third in a recipe. */
|
|
779
|
+
function builderParameter(definition, build) {
|
|
780
|
+
return build.parameters[definition.id === undefined ? 1 : 2];
|
|
781
|
+
}
|
|
677
782
|
function kernelProvisionCall(call) {
|
|
678
783
|
const chain = propertyChain(call.expression);
|
|
679
784
|
if (chain?.length === 4 &&
|
|
@@ -1180,9 +1285,9 @@ function containingVariableName(node) {
|
|
|
1180
1285
|
}
|
|
1181
1286
|
return undefined;
|
|
1182
1287
|
}
|
|
1183
|
-
function propertiesForClass(input, properties) {
|
|
1288
|
+
function propertiesForClass(input, properties, handles) {
|
|
1184
1289
|
const selectedClass = assignedValue(input, 'class');
|
|
1185
|
-
const selection = selectedClass && selectedClassNames(selectedClass);
|
|
1290
|
+
const selection = selectedClass && selectedClassNames(selectedClass, handles);
|
|
1186
1291
|
if (selection === undefined || !closedClassSelection(selection, properties)) {
|
|
1187
1292
|
return { kind: 'ambiguous' };
|
|
1188
1293
|
}
|
|
@@ -1205,13 +1310,17 @@ function propertiesForClass(input, properties) {
|
|
|
1205
1310
|
}
|
|
1206
1311
|
return hasMachineProperty ? { kind: 'resolved', properties: resolved } : { kind: 'absent' };
|
|
1207
1312
|
}
|
|
1208
|
-
function selectedClassNames(expression, seen = new Set()) {
|
|
1313
|
+
function selectedClassNames(expression, handles, seen = new Set()) {
|
|
1209
1314
|
const value = unwrap(expression);
|
|
1210
1315
|
if (ts.isConditionalExpression(value)) {
|
|
1211
|
-
const whenTrue = selectedClassNames(value.whenTrue, seen);
|
|
1212
|
-
const whenFalse = selectedClassNames(value.whenFalse, seen);
|
|
1316
|
+
const whenTrue = selectedClassNames(value.whenTrue, handles, seen);
|
|
1317
|
+
const whenFalse = selectedClassNames(value.whenFalse, handles, seen);
|
|
1213
1318
|
return whenTrue && whenFalse ? new Set([...whenTrue, ...whenFalse]) : undefined;
|
|
1214
1319
|
}
|
|
1320
|
+
// A recipe names its Class by its handle: the last segment of its path from `d`.
|
|
1321
|
+
const path = handlePath(value, handles);
|
|
1322
|
+
if (path !== undefined)
|
|
1323
|
+
return path.length > 0 ? new Set([path.at(-1)]) : undefined;
|
|
1215
1324
|
if (ts.isIdentifier(value)) {
|
|
1216
1325
|
if (seen.has(value.text))
|
|
1217
1326
|
return undefined;
|
|
@@ -1219,7 +1328,7 @@ function selectedClassNames(expression, seen = new Set()) {
|
|
|
1219
1328
|
if (initializer !== undefined) {
|
|
1220
1329
|
const next = new Set(seen);
|
|
1221
1330
|
next.add(value.text);
|
|
1222
|
-
return selectedClassNames(initializer, next);
|
|
1331
|
+
return selectedClassNames(initializer, handles, next);
|
|
1223
1332
|
}
|
|
1224
1333
|
}
|
|
1225
1334
|
const chain = staticPropertyChain(value);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { declarationOf, unwrap, visit, } from '../../adapters/typescript/index.js';
|
|
3
|
+
import { production, propertyExpression, violation } from './shared.js';
|
|
4
|
+
export const policyEdgeRules = [
|
|
5
|
+
{
|
|
6
|
+
id: 'SCH-EDGE-KEY-ORDER',
|
|
7
|
+
ruleRevision: '7377f52e7387d618840c7a6d520b317c7eda63348c84b86cfc1e389fe5db10b5',
|
|
8
|
+
evaluate(project) {
|
|
9
|
+
return production(project, 'schema').flatMap((file) => edgeOrderEvidence(project, file));
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Each Policy `edge(Class, { [role]: term })` whose ends list another role before the source's. A
|
|
15
|
+
* Class whose source role is not statically visible, or ends that are not plain keys, prove nothing.
|
|
16
|
+
*/
|
|
17
|
+
function edgeOrderEvidence(project, file) {
|
|
18
|
+
const evidence = [];
|
|
19
|
+
visit(file.source, (node) => {
|
|
20
|
+
if (!ts.isCallExpression(node) || !isPolicyEdge(node.expression))
|
|
21
|
+
return;
|
|
22
|
+
const [edgeClass, ends] = node.arguments;
|
|
23
|
+
const object = ends && unwrap(ends);
|
|
24
|
+
if (!edgeClass || !object || !ts.isObjectLiteralExpression(object))
|
|
25
|
+
return;
|
|
26
|
+
const roles = endRoles(object);
|
|
27
|
+
const source = roles && sourceRole(project, file, edgeClass);
|
|
28
|
+
if (!roles || source === undefined || roles[0] === source || !roles.includes(source))
|
|
29
|
+
return;
|
|
30
|
+
evidence.push(violation(file, object, `Policy edge(${edgeClass.getText(file.source)}, { ... }) lists ${roles[0]} first; write the source role ${source} first.`));
|
|
31
|
+
});
|
|
32
|
+
return evidence;
|
|
33
|
+
}
|
|
34
|
+
/** `edge` of a Policy context: destructured from a callback's parameter, or read off it. */
|
|
35
|
+
function isPolicyEdge(expression) {
|
|
36
|
+
const callee = unwrap(expression);
|
|
37
|
+
if (ts.isPropertyAccessExpression(callee))
|
|
38
|
+
return (callee.name.text === 'edge' &&
|
|
39
|
+
ts.isIdentifier(callee.expression) &&
|
|
40
|
+
bindingOf(callee.expression) === 'parameter');
|
|
41
|
+
return ts.isIdentifier(callee) && bindingOf(callee) === 'edge';
|
|
42
|
+
}
|
|
43
|
+
/** How the nearest enclosing callback binds a name: as a parameter, or as its `edge` member. */
|
|
44
|
+
function bindingOf(identifier) {
|
|
45
|
+
for (let current = identifier.parent; current; current = current.parent) {
|
|
46
|
+
if (!ts.isFunctionLike(current))
|
|
47
|
+
continue;
|
|
48
|
+
for (const { name } of current.parameters) {
|
|
49
|
+
if (ts.isIdentifier(name) && name.text === identifier.text)
|
|
50
|
+
return 'parameter';
|
|
51
|
+
if (!ts.isObjectBindingPattern(name))
|
|
52
|
+
continue;
|
|
53
|
+
for (const element of name.elements) {
|
|
54
|
+
if (!ts.isIdentifier(element.name) || element.name.text !== identifier.text)
|
|
55
|
+
continue;
|
|
56
|
+
const key = element.propertyName ?? element.name;
|
|
57
|
+
return ts.isIdentifier(key) && key.text === 'edge' ? 'edge' : undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/** The keys of the ends, in the order written; `undefined` when one is not a plain key. */
|
|
64
|
+
function endRoles(object) {
|
|
65
|
+
const roles = [];
|
|
66
|
+
for (const property of object.properties) {
|
|
67
|
+
if (ts.isShorthandPropertyAssignment(property))
|
|
68
|
+
roles.push(property.name.text);
|
|
69
|
+
else if (ts.isPropertyAssignment(property) &&
|
|
70
|
+
(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)))
|
|
71
|
+
roles.push(property.name.text);
|
|
72
|
+
else
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return roles;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The source role of an Edge Class, read from its declaration: the `as` of a directed Class's
|
|
79
|
+
* source, or of an undirected Class's first endpoint, which lowers to the source.
|
|
80
|
+
*/
|
|
81
|
+
function sourceRole(project, file, value) {
|
|
82
|
+
let reference = unwrap(value);
|
|
83
|
+
if (ts.isArrowFunction(reference) && !ts.isBlock(reference.body))
|
|
84
|
+
reference = unwrap(reference.body);
|
|
85
|
+
if (!ts.isIdentifier(reference))
|
|
86
|
+
return undefined;
|
|
87
|
+
const declaration = declarationOf(project, file, reference);
|
|
88
|
+
if (!declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer)
|
|
89
|
+
return undefined;
|
|
90
|
+
const call = unwrap(declaration.initializer);
|
|
91
|
+
if (!ts.isCallExpression(call))
|
|
92
|
+
return undefined;
|
|
93
|
+
const callee = unwrap(call.expression);
|
|
94
|
+
const input = call.arguments[0] && unwrap(call.arguments[0]);
|
|
95
|
+
if (!ts.isPropertyAccessExpression(callee) ||
|
|
96
|
+
!ts.isIdentifier(callee.expression) ||
|
|
97
|
+
callee.expression.text !== 'edgeClass' ||
|
|
98
|
+
!input ||
|
|
99
|
+
!ts.isObjectLiteralExpression(input))
|
|
100
|
+
return undefined;
|
|
101
|
+
if (callee.name.text === 'directed')
|
|
102
|
+
return roleOf(propertyExpression(input, 'source'));
|
|
103
|
+
if (callee.name.text !== 'undirected')
|
|
104
|
+
return undefined;
|
|
105
|
+
const endpoints = propertyExpression(input, 'endpoints');
|
|
106
|
+
return endpoints && ts.isArrayLiteralExpression(endpoints)
|
|
107
|
+
? roleOf(endpoints.elements[0])
|
|
108
|
+
: undefined;
|
|
109
|
+
}
|
|
110
|
+
function roleOf(end) {
|
|
111
|
+
const role = end && ts.isObjectLiteralExpression(end) ? propertyExpression(end, 'as') : undefined;
|
|
112
|
+
return role && ts.isStringLiteralLike(role) ? role.text : undefined;
|
|
113
|
+
}
|