@aventara/core 1.0.0-rc.11 → 1.0.0-rc.13
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 +157 -0
- package/ERRORS.md +8 -7
- package/README.md +3 -2
- package/dist/avz/avz-check.factory.d.ts +2 -0
- package/dist/avz/avz-check.factory.js +12 -0
- package/dist/avz/avz.d.ts +6 -0
- package/dist/avz/avz.js +1 -0
- package/dist/compiler/contract-compiler-diagnostic.d.ts +13 -2
- package/dist/compiler/contract-compiler-diagnostic.guidance.js +7 -2
- package/dist/compiler/contract.compiler.js +110 -15
- package/dist/compiler/create-requirements.projector.d.ts +45 -3
- package/dist/compiler/create-requirements.projector.js +239 -28
- package/dist/compiler/managed-elsewhere.projector.d.ts +3 -0
- package/dist/compiler/managed-elsewhere.projector.js +1 -1
- package/dist/config/computed-fields.d.ts +8 -0
- package/dist/config/computed-fields.js +1 -1
- package/dist/config/config-diagnostic.guidance.js +2 -2
- package/dist/config/config.resolver.d.ts +2 -2
- package/dist/config/config.validator.js +15 -6
- package/dist/config/exact-framework-config.d.ts +1 -0
- package/dist/config/field-pipeline-config.d.ts +2 -2
- package/dist/config/forced-operation.validator.js +3 -2
- package/dist/config/framework-config.d.ts +8 -3
- package/dist/config/framework-exemptions-config.d.ts +13 -8
- package/dist/config/framework-resources-config.d.ts +11 -3
- package/dist/contracts/client-contract-structure.validator.d.ts +4 -1
- package/dist/contracts/client-contract-structure.validator.js +3 -2
- package/dist/contracts/contract-limits.d.ts +8 -1
- package/dist/contracts/contract-limits.js +1 -0
- package/dist/contracts/contract.d.ts +7 -3
- package/dist/contracts/transaction-capability.d.ts +8 -0
- package/dist/contracts/transaction-capability.js +7 -0
- package/dist/framework/access-facade.js +14 -6
- package/dist/framework/contract-view.d.ts +104 -12
- package/dist/framework/operation-facade.d.ts +8 -3
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/operations/validation-code.guidance.js +1 -1
- package/dist/policy/access-provider.interface.d.ts +7 -2
- package/dist/policy/access-question.runtime.js +2 -1
- package/dist/prebuilt/core-version.generated.d.ts +1 -1
- package/dist/prebuilt/core-version.generated.js +1 -1
- package/dist/protocol/absent-route.responder.d.ts +2 -2
- package/dist/protocol/absent-route.responder.js +2 -1
- package/dist/protocol/av-protocol.d.ts +1 -0
- package/dist/protocol/av-protocol.js +1 -0
- package/dist/protocol/protocol-surface.js +2 -1
- package/dist/protocol/protocol.handler.js +3 -1
- package/dist/runtime/context-executor.runtime.js +2 -1
- package/dist/runtime/create-requirement.d.ts +5 -1
- package/dist/runtime/create-requirement.js +10 -2
- package/dist/runtime/field-check.evaluator.js +39 -2
- package/dist/runtime/field-validator.runtime.d.ts +4 -3
- package/dist/runtime/field-validator.runtime.js +29 -0
- package/dist/runtime/mutation-planner.js +26 -9
- package/dist/runtime/operation-code.guidance.js +3 -3
- package/dist/runtime/operation-runtime.d.ts +10 -9
- package/dist/runtime/operation-runtime.js +20 -18
- package/dist/runtime/pipeline.d.ts +1 -15
- package/dist/runtime/state-refusal.mapper.d.ts +16 -0
- package/dist/runtime/state-refusal.mapper.js +40 -0
- package/dist/transactions/operation-fingerprint.d.ts +15 -2
- package/dist/transactions/transaction-plan-validator.js +31 -1
- package/dist/transactions/transaction-runtime.js +1 -1
- package/package.json +1 -1
- package/src/runtime/field-check.evaluator.ts +46 -2
|
@@ -8,7 +8,7 @@ import { AVENTARA_PROTOCOL_VERSION } from "../contracts/contract-protocol.js";
|
|
|
8
8
|
import { advertisedDirectives, advertisedReducers, holdsInAnyScope, } from "../contracts/field.js";
|
|
9
9
|
import { isOperationVariantAvailable, } from "../contracts/resource-operations.js";
|
|
10
10
|
import { isRecord } from "../runtime/record.guard.js";
|
|
11
|
-
import { checkCreateRequirements, checkHiddenRequirements, projectCreateRequirements, } from "./create-requirements.projector.js";
|
|
11
|
+
import { checkCreateRequirements, checkHiddenRequirements, checkReadOnlyNestedWrites, projectCreateRequirements, withheldKey, withholdReadOnlyNestedWrites, } from "./create-requirements.projector.js";
|
|
12
12
|
import { applyValidators, bindFieldValidators, checkLayerValidators, compileValidatorTable, } from "./field-validator.compiler.js";
|
|
13
13
|
import { explainManagedRefusals, hideManagedElsewhere, managedElsewhereOf, managedWriteReason, markCoreResources, pruneManagedEnums, withholdManagedWrites, } from "./managed-elsewhere.projector.js";
|
|
14
14
|
const UNIQUE_VARIANTS = [
|
|
@@ -491,6 +491,9 @@ function capabilityAxisFor(kind, key) {
|
|
|
491
491
|
function unavailableCapability(path, message, diagnostics) {
|
|
492
492
|
diagnostics.push(diagnostic("COMPILER_UNAVAILABLE_CAPABILITY", path, message));
|
|
493
493
|
}
|
|
494
|
+
function parentLayerOf(walk) {
|
|
495
|
+
return walk.layer === "agent" ? (walk.parent ?? "client") : "root";
|
|
496
|
+
}
|
|
494
497
|
function otherOverlay(layer) {
|
|
495
498
|
return layer === "client" ? "application" : "client";
|
|
496
499
|
}
|
|
@@ -504,9 +507,23 @@ function exemptsWhole(entry) {
|
|
|
504
507
|
}
|
|
505
508
|
function exemptsAnything(entry) {
|
|
506
509
|
return (entry === true ||
|
|
510
|
+
entry === READ ||
|
|
507
511
|
(Array.isArray(entry) && entry.length > 0) ||
|
|
508
512
|
isRecord(entry));
|
|
509
513
|
}
|
|
514
|
+
const READ = "read";
|
|
515
|
+
const READ_CAPABILITIES = {
|
|
516
|
+
scalar: ["select", "filter", "order", "orderNulls"],
|
|
517
|
+
relation: ["select", "include", "filter", "order", "pagination", "reducers"],
|
|
518
|
+
};
|
|
519
|
+
const readCapabilitiesCovered = [true, true];
|
|
520
|
+
void readCapabilitiesCovered;
|
|
521
|
+
function readCapabilitiesOf(kind, parentCapabilities) {
|
|
522
|
+
return READ_CAPABILITIES[kind].filter((capability) => {
|
|
523
|
+
const axis = capabilityAxisFor(kind, capability);
|
|
524
|
+
return (axis !== undefined && memberOffered(axis, parentCapabilities[capability]));
|
|
525
|
+
});
|
|
526
|
+
}
|
|
510
527
|
function listedNames(entry, path) {
|
|
511
528
|
if (!Array.isArray(entry)) {
|
|
512
529
|
return [];
|
|
@@ -585,6 +602,34 @@ function offeredTokens(walk, axis, universeValue, listed, capability, label) {
|
|
|
585
602
|
}
|
|
586
603
|
return tokens;
|
|
587
604
|
}
|
|
605
|
+
const WITHHELD_LACKS = {
|
|
606
|
+
delete: ["has no delete operation", "a delete operation"],
|
|
607
|
+
update: [
|
|
608
|
+
"updates nothing",
|
|
609
|
+
"an update operation or a field writable on update",
|
|
610
|
+
],
|
|
611
|
+
create: [
|
|
612
|
+
"creates nothing",
|
|
613
|
+
"a create operation or a field writable on create",
|
|
614
|
+
],
|
|
615
|
+
};
|
|
616
|
+
function joined(names) {
|
|
617
|
+
return names.length <= 1
|
|
618
|
+
? (names[0] ?? "")
|
|
619
|
+
: `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
|
|
620
|
+
}
|
|
621
|
+
function withheldWrite(walk, universeField, label, path, directives, member, withheld) {
|
|
622
|
+
const target = String(universeField.target);
|
|
623
|
+
const above = parentLayerOf(walk);
|
|
624
|
+
const reasons = [
|
|
625
|
+
...new Set(directives.flatMap((name) => withheld.get(name) ?? [])),
|
|
626
|
+
];
|
|
627
|
+
const named = member ?? joined(directives);
|
|
628
|
+
const message = reasons.length === 1 && reasons[0] === "delete"
|
|
629
|
+
? `the ${walk.layer} layer lets relation ${label} delete rows of resource "${target}" ($delete), but "${target}" has no delete operation in ${above}, the layer ${walk.layer} starts from, so ${above} offers no $delete on ${label}${member === undefined ? "" : ` and no ${member} at all, $delete being all it had`}; take ${named} off ${label}, or give "${target}" a delete operation in ${above}`
|
|
630
|
+
: `the ${walk.layer} layer lets relation ${label} write rows of resource "${target}" (${joined(directives)}), but "${target}" ${reasons.map((reason) => WITHHELD_LACKS[reason][0]).join(", and ")} in ${above}, the layer ${walk.layer} starts from, so ${above} offers no ${joined(directives)} on ${label}${member === undefined ? "" : ` and no ${member} at all`}; take ${named} off ${label}, or give "${target}" ${reasons.map((reason) => WITHHELD_LACKS[reason][1]).join(", and ")} in ${above}`;
|
|
631
|
+
walk.diagnostics.push(diagnostic("COMPILER_READ_ONLY_NESTED_WRITE", path.replace(/(\[\d+\]|\.[^.[\]]+)$/, ""), message));
|
|
632
|
+
}
|
|
588
633
|
function exemptCapability(walk, universeField, parentCapabilities, result, capability, entry, label, path) {
|
|
589
634
|
if (entry === undefined) {
|
|
590
635
|
return;
|
|
@@ -624,7 +669,19 @@ function exemptCapability(walk, universeField, parentCapabilities, result, capab
|
|
|
624
669
|
if (tokens !== undefined && tokens.length === 0) {
|
|
625
670
|
return;
|
|
626
671
|
}
|
|
672
|
+
const withheldHere = new Map();
|
|
673
|
+
const prefix = withheldKey(label.slice(1, -1), capability, "");
|
|
674
|
+
for (const [key, reason] of walk.withheldWrites ?? []) {
|
|
675
|
+
if (key.startsWith(prefix)) {
|
|
676
|
+
withheldHere.set(key.slice(prefix.length), reason);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
627
679
|
if (!parentHas) {
|
|
680
|
+
if (withheldHere.size > 0 &&
|
|
681
|
+
(tokens === undefined || tokens.every((token) => withheldHere.has(token)))) {
|
|
682
|
+
withheldWrite(walk, universeField, label, path, tokens ?? [...withheldHere.keys()], tokens === undefined ? capability : undefined, withheldHere);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
628
685
|
widening(walk, "COMPILER_UNAVAILABLE_CAPABILITY", path, `capability "${capability}" of field ${label}`);
|
|
629
686
|
return;
|
|
630
687
|
}
|
|
@@ -635,7 +692,14 @@ function exemptCapability(walk, universeField, parentCapabilities, result, capab
|
|
|
635
692
|
const parentTokens = memberTokens(axis, parentValue) ?? [];
|
|
636
693
|
const hidden = tokens.filter((token) => !parentTokens.includes(token));
|
|
637
694
|
if (hidden.length > 0) {
|
|
638
|
-
|
|
695
|
+
const withheld = hidden.filter((token) => withheldHere.has(token));
|
|
696
|
+
const widened = hidden.filter((token) => !withheldHere.has(token));
|
|
697
|
+
if (withheld.length > 0) {
|
|
698
|
+
withheldWrite(walk, universeField, label, path, withheld, undefined, withheldHere);
|
|
699
|
+
}
|
|
700
|
+
if (widened.length > 0) {
|
|
701
|
+
widening(walk, "COMPILER_UNAVAILABLE_CAPABILITY", path, `${widened.map((token) => `"${token}"`).join(", ")} of capability "${capability}" of field ${label}`);
|
|
702
|
+
}
|
|
639
703
|
return;
|
|
640
704
|
}
|
|
641
705
|
result[capability] = withTokens(axis, parentValue, parentTokens.filter((token) => tokens.includes(token)));
|
|
@@ -650,7 +714,12 @@ function exemptField(walk, universeField, parentField, entry, label, path) {
|
|
|
650
714
|
}
|
|
651
715
|
}
|
|
652
716
|
}
|
|
653
|
-
if (
|
|
717
|
+
if (entry === READ) {
|
|
718
|
+
for (const capability of readCapabilitiesOf(universeField.kind, parentCapabilities)) {
|
|
719
|
+
exemptCapability(walk, universeField, parentCapabilities, result, capability, true, label, path);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
else if (Array.isArray(entry)) {
|
|
654
723
|
for (const [capability, capabilityPath] of listedNames(entry, path)) {
|
|
655
724
|
exemptCapability(walk, universeField, parentCapabilities, result, capability, true, label, capabilityPath);
|
|
656
725
|
}
|
|
@@ -670,7 +739,11 @@ function exemptField(walk, universeField, parentField, entry, label, path) {
|
|
|
670
739
|
return { ...cloneValue(parentField), capabilities: result };
|
|
671
740
|
}
|
|
672
741
|
function exemptFields(walk, resourceName, universe, parent, entries, path) {
|
|
673
|
-
const named =
|
|
742
|
+
const named = entries === READ
|
|
743
|
+
? Object.fromEntries(Object.keys(parent.fields).map((name) => [name, READ]))
|
|
744
|
+
: isRecord(entries)
|
|
745
|
+
? entries
|
|
746
|
+
: {};
|
|
674
747
|
for (const fieldName of Object.keys(named)) {
|
|
675
748
|
if (universe.fields[fieldName] === undefined) {
|
|
676
749
|
walk.diagnostics.push(diagnostic("COMPILER_UNKNOWN_FIELD", `${path}.${fieldName}`, `field "${fieldName}" is neither discovered by the adapter nor defined at the root`));
|
|
@@ -866,9 +939,14 @@ function pruneEmptyOperationFamilies(operations) {
|
|
|
866
939
|
function identifierLabel(identifier) {
|
|
867
940
|
return identifier.map((name) => `\`${name}\``).join(" and ");
|
|
868
941
|
}
|
|
942
|
+
function isAddressable(field, parentField) {
|
|
943
|
+
return (field?.kind === "scalar" &&
|
|
944
|
+
(capabilityIsEnabled(field.capabilities.filter) ||
|
|
945
|
+
!capabilityIsEnabled(parentField?.capabilities?.filter)));
|
|
946
|
+
}
|
|
869
947
|
function reconcileIdentifiers(walk, resource, parent, path) {
|
|
870
948
|
resource.identifiers = parent.identifiers
|
|
871
|
-
.filter((identifier) => identifier.every((name) => resource.fields[name]
|
|
949
|
+
.filter((identifier) => identifier.every((name) => isAddressable(resource.fields[name], parent.fields[name])))
|
|
872
950
|
.map((identifier) => [...identifier]);
|
|
873
951
|
const first = parent.identifiers[0];
|
|
874
952
|
if (resource.identifiers.length > 0 || first === undefined) {
|
|
@@ -882,8 +960,8 @@ function reconcileIdentifiers(walk, resource, parent, path) {
|
|
|
882
960
|
}
|
|
883
961
|
const variantPath = `${path}.operations.${family}.${variant}`;
|
|
884
962
|
walk.diagnostics.push(diagnostic("COMPILER_MISSING_IDENTIFIER", variantPath, walk.style === "pick"
|
|
885
|
-
? `${variantPath} needs an identifier field; pick ${identifierLabel(first)} (or another complete identifier) too`
|
|
886
|
-
: `${variantPath} needs an identifier field, and the exemptions hide every complete identifier; keep ${identifierLabel(first)} (or another complete identifier) exposed, or exempt the operation too`));
|
|
963
|
+
? `${variantPath} needs an identifier field; pick ${identifierLabel(first)} with its filter (or another complete identifier) too`
|
|
964
|
+
: `${variantPath} needs an identifier field, and the exemptions hide (or stop filtering on) every complete identifier; keep ${identifierLabel(first)} (or another complete identifier) exposed and filterable, or exempt the operation too`));
|
|
887
965
|
}
|
|
888
966
|
}
|
|
889
967
|
function validateScalarReference(projection, field, path, diagnostics) {
|
|
@@ -1020,8 +1098,8 @@ function resolveLimits(scope) {
|
|
|
1020
1098
|
}
|
|
1021
1099
|
function resolveTransactions(model, scopeName, scope, diagnostics) {
|
|
1022
1100
|
const requested = scope.transactionsStyle ?? model.transactions;
|
|
1023
|
-
if (requested
|
|
1024
|
-
diagnostics.push(diagnostic("COMPILER_UNAVAILABLE_CAPABILITY", `${scopeName}.transactionsStyle`,
|
|
1101
|
+
if (requested !== "none" && model.transactions === "none") {
|
|
1102
|
+
diagnostics.push(diagnostic("COMPILER_UNAVAILABLE_CAPABILITY", `${scopeName}.transactionsStyle`, `transaction capability "${requested}" is not available in the canonical adapter model`));
|
|
1025
1103
|
}
|
|
1026
1104
|
return requested;
|
|
1027
1105
|
}
|
|
@@ -1031,6 +1109,11 @@ function semanticObject(contract) {
|
|
|
1031
1109
|
function exposureStyle(layer) {
|
|
1032
1110
|
return layer.exposed === false ? "pick" : "omit";
|
|
1033
1111
|
}
|
|
1112
|
+
function withholdOmittedWrites(style, projection) {
|
|
1113
|
+
return style === "omit"
|
|
1114
|
+
? withholdReadOnlyNestedWrites(projection.resources)
|
|
1115
|
+
: new Map();
|
|
1116
|
+
}
|
|
1034
1117
|
function projectCore(model, config, diagnostics) {
|
|
1035
1118
|
const scope = config.root;
|
|
1036
1119
|
const managed = managedElsewhereOf(config.trees.root);
|
|
@@ -1049,13 +1132,15 @@ function projectCore(model, config, diagnostics) {
|
|
|
1049
1132
|
const projection = projectExposure({ layer: "root", style: exposureStyle(scope), diagnostics }, baseline, baseline, scope.exemptions);
|
|
1050
1133
|
qualifyFilterModes(projection);
|
|
1051
1134
|
pruneHiddenPairings(projection, "shown");
|
|
1135
|
+
const withheldWrites = withholdOmittedWrites(exposureStyle(scope), projection);
|
|
1052
1136
|
projectCreateRequirements(model, projection.resources);
|
|
1053
1137
|
const rootChecked = diagnostics.length;
|
|
1054
1138
|
checkHiddenRequirements(model, "root", baseline.resources, unwithheld.resources, projection.resources, diagnostics);
|
|
1139
|
+
checkReadOnlyNestedWrites("root", undefined, projection.resources, diagnostics);
|
|
1055
1140
|
explainManagedRefusals(diagnostics, rootChecked, "root", baseline, managed);
|
|
1056
1141
|
applyValidators(projection.resources, validators);
|
|
1057
1142
|
validateProjection(projection, "root", diagnostics);
|
|
1058
|
-
return { baseline, projection, validators };
|
|
1143
|
+
return { baseline, projection, validators, withheldWrites };
|
|
1059
1144
|
}
|
|
1060
1145
|
function markGovernedFields(baseline, governed) {
|
|
1061
1146
|
if (governed === undefined) {
|
|
@@ -1078,7 +1163,7 @@ function markGovernedFields(baseline, governed) {
|
|
|
1078
1163
|
}
|
|
1079
1164
|
}
|
|
1080
1165
|
}
|
|
1081
|
-
function projectDerivedScope(model, scopeName, core, parentView, config, diagnostics) {
|
|
1166
|
+
function projectDerivedScope(model, scopeName, core, parentView, parentWithheld, config, diagnostics) {
|
|
1082
1167
|
const scope = config[scopeName];
|
|
1083
1168
|
const managed = managedElsewhereOf(config.trees.root);
|
|
1084
1169
|
const baseline = cloneValue(core.baseline);
|
|
@@ -1105,6 +1190,7 @@ function projectDerivedScope(model, scopeName, core, parentView, config, diagnos
|
|
|
1105
1190
|
layer: scopeName,
|
|
1106
1191
|
style: exposureStyle(scope),
|
|
1107
1192
|
diagnostics,
|
|
1193
|
+
withheldWrites: parentWithheld,
|
|
1108
1194
|
...(scopeName === "agent" ? { parent: config.agent.parent } : {}),
|
|
1109
1195
|
}, baseline, parent, scope.exemptions);
|
|
1110
1196
|
qualifyFilterModes(projection);
|
|
@@ -1112,13 +1198,18 @@ function projectDerivedScope(model, scopeName, core, parentView, config, diagnos
|
|
|
1112
1198
|
if (unhidden !== parent) {
|
|
1113
1199
|
pruneManagedEnums(projection, unhidden, managed);
|
|
1114
1200
|
}
|
|
1201
|
+
const withheldWrites = new Map([
|
|
1202
|
+
...parentWithheld,
|
|
1203
|
+
...withholdOmittedWrites(exposureStyle(scope), projection),
|
|
1204
|
+
]);
|
|
1115
1205
|
projectCreateRequirements(model, projection.resources);
|
|
1116
1206
|
const layerChecked = diagnostics.length;
|
|
1117
1207
|
checkHiddenRequirements(model, scopeName, baseline.resources, unhidden.resources, projection.resources, diagnostics);
|
|
1208
|
+
checkReadOnlyNestedWrites(scopeName, unhidden.resources, projection.resources, diagnostics);
|
|
1118
1209
|
explainManagedRefusals(diagnostics, layerChecked, scopeName, baseline, managed);
|
|
1119
1210
|
applyValidators(projection.resources, validators);
|
|
1120
1211
|
validateProjection(projection, scopeName, diagnostics);
|
|
1121
|
-
return { projection, validators };
|
|
1212
|
+
return { projection, validators, withheldWrites };
|
|
1122
1213
|
}
|
|
1123
1214
|
function bindShapedFields(contract, definitions) {
|
|
1124
1215
|
for (const [resourceName, resource] of Object.entries(contract.resources)) {
|
|
@@ -1140,14 +1231,18 @@ export async function compileContracts(model, config) {
|
|
|
1140
1231
|
checkReservedResourceNames(model, diagnostics);
|
|
1141
1232
|
checkCreateRequirements(model, diagnostics);
|
|
1142
1233
|
const coreCompilation = projectCore(model, compilerConfig, diagnostics);
|
|
1143
|
-
const applicationProjection = projectDerivedScope(model, "application", coreCompilation, coreCompilation.projection, compilerConfig, diagnostics);
|
|
1144
|
-
const clientProjection = projectDerivedScope(model, "client", coreCompilation, coreCompilation.projection, compilerConfig, diagnostics);
|
|
1234
|
+
const applicationProjection = projectDerivedScope(model, "application", coreCompilation, coreCompilation.projection, coreCompilation.withheldWrites, compilerConfig, diagnostics);
|
|
1235
|
+
const clientProjection = projectDerivedScope(model, "client", coreCompilation, coreCompilation.projection, coreCompilation.withheldWrites, compilerConfig, diagnostics);
|
|
1145
1236
|
const agentParent = compilerConfig.agent.parent;
|
|
1146
1237
|
const agentProjection = projectDerivedScope(model, "agent", coreCompilation, agentParent === "root"
|
|
1147
1238
|
? coreCompilation.projection
|
|
1148
1239
|
: agentParent === "application"
|
|
1149
1240
|
? applicationProjection.projection
|
|
1150
|
-
: clientProjection.projection,
|
|
1241
|
+
: clientProjection.projection, agentParent === "root"
|
|
1242
|
+
? coreCompilation.withheldWrites
|
|
1243
|
+
: agentParent === "application"
|
|
1244
|
+
? applicationProjection.withheldWrites
|
|
1245
|
+
: clientProjection.withheldWrites, compilerConfig, diagnostics);
|
|
1151
1246
|
markCoreResources(coreCompilation.projection, model, managedElsewhereOf(compilerConfig.trees.root));
|
|
1152
1247
|
if (diagnostics.length > 0) {
|
|
1153
1248
|
return { ok: false, diagnostics };
|
|
@@ -12,11 +12,18 @@ type ProjectedResource = {
|
|
|
12
12
|
readonly fields: Readonly<Record<string, ProjectedField>>;
|
|
13
13
|
createRequirements?: AvCreateRequirements;
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* The Adapter's statement with the layer's non-null added fields required too.
|
|
17
|
+
* The runtime reads it over the Core Contract (`recordWriteFactsOf`): what write
|
|
18
|
+
* compute must not leave missing.
|
|
19
|
+
*/
|
|
20
|
+
export declare function withVirtualRequirements(stated: AvCreateRequirements | undefined, resource: ProjectedResource): AvCreateRequirements | undefined;
|
|
15
21
|
/**
|
|
16
22
|
* Sets every Resource's `createRequirements` in one layer's projection to the
|
|
17
|
-
* Adapter's statement as that layer sees it,
|
|
18
|
-
*
|
|
19
|
-
* its compute stages
|
|
23
|
+
* Adapter's statement as that layer sees it, its non-null added (`VIRTUAL`)
|
|
24
|
+
* fields required too, and removes it where nothing is left. Reads the
|
|
25
|
+
* projection's final fields: its exposure, and the lifecycle its compute stages
|
|
26
|
+
* gave them.
|
|
20
27
|
*/
|
|
21
28
|
export declare function projectCreateRequirements(model: AvAdapterModel, resources: Readonly<Record<string, ProjectedResource>>): void;
|
|
22
29
|
/** A Resource's operations, as far as this check reads them. */
|
|
@@ -38,6 +45,41 @@ type ResourceWithOperations = ProjectedResource & {
|
|
|
38
45
|
* its final view.
|
|
39
46
|
*/
|
|
40
47
|
export declare function checkHiddenRequirements(model: AvAdapterModel, layer: AvConfigLayerName, universe: Readonly<Record<string, ProjectedResource>>, parent: Readonly<Record<string, ProjectedResource>>, projection: Readonly<Record<string, ResourceWithOperations>>, diagnostics: AvContractCompilerDiagnostic[]): void;
|
|
48
|
+
/**
|
|
49
|
+
* Refuses, for one layer, every relation that writes rows of a Resource the
|
|
50
|
+
* layer keeps read-only: a nested `$create`, `$connectOrCreate` or `$upsert`
|
|
51
|
+
* into a Resource with no create or upsert variant and no field writable on
|
|
52
|
+
* create, a nested `$update` or `$upsert` into one with no update or upsert
|
|
53
|
+
* variant and no field writable on update, or a nested `$delete` into one with
|
|
54
|
+
* no delete variant. Reported once, by the layer that makes it so: an overlay
|
|
55
|
+
* is not refused for a relation its parent was already refused for. `parent` is
|
|
56
|
+
* what the layer starts from; the root's is none, so an Adapter that offers such
|
|
57
|
+
* a write is refused at the root. An Omit layer has none left to refuse:
|
|
58
|
+
* {@link withholdReadOnlyNestedWrites} dropped them first.
|
|
59
|
+
*/
|
|
60
|
+
export declare function checkReadOnlyNestedWrites(layer: AvConfigLayerName, parent: Readonly<Record<string, ResourceWithOperations>> | undefined, projection: Readonly<Record<string, ResourceWithOperations>>, diagnostics: AvContractCompilerDiagnostic[]): void;
|
|
61
|
+
/** Why the narrowing dropped a directive: its target deletes, updates or creates nothing in the layer. */
|
|
62
|
+
export type WithheldReason = "delete" | "update" | "create";
|
|
63
|
+
/**
|
|
64
|
+
* The directives an Omit layer's narrowing dropped, by relation, stage and
|
|
65
|
+
* directive (`"posts.comments.update.$delete"`), each with why.
|
|
66
|
+
*/
|
|
67
|
+
export type WithheldWrites = ReadonlyMap<string, WithheldReason>;
|
|
68
|
+
/** The key of one dropped directive in {@link WithheldWrites}. */
|
|
69
|
+
export declare function withheldKey(relation: string, stage: string, directive: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Narrows an Omit layer's view so no relation writes rows of a Resource the
|
|
72
|
+
* view cannot write: `$create`, `$connectOrCreate` and `$upsert` into one that
|
|
73
|
+
* creates nothing (no create or upsert variant, no field writable on create),
|
|
74
|
+
* `$update` and `$upsert` into one that updates nothing (no update or upsert
|
|
75
|
+
* variant, no field writable on update), `$delete` into one that deletes
|
|
76
|
+
* nothing (no delete variant). A member emptied goes with its last directive.
|
|
77
|
+
* Dropping a directive can leave another Resource writing nothing (its only
|
|
78
|
+
* write capability was it), so the pass repeats to a fixed point. Returns
|
|
79
|
+
* every directive it dropped, with why. A Pick layer's view is not narrowed:
|
|
80
|
+
* {@link checkReadOnlyNestedWrites} refuses what it grants.
|
|
81
|
+
*/
|
|
82
|
+
export declare function withholdReadOnlyNestedWrites(resources: Readonly<Record<string, ResourceWithOperations>>): WithheldWrites;
|
|
41
83
|
/**
|
|
42
84
|
* Every name in the Adapter's create requirements that is not what its member
|
|
43
85
|
* says: a `required` name that is no field of the Resource, a group whose first
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { advertisedDirectives, advertisedDirectivesAt, } from "../contracts/field.js";
|
|
2
2
|
import { isOperationVariantAvailable, writeScopeOf, } from "../contracts/resource-operations.js";
|
|
3
3
|
import { isRecord } from "../runtime/record.guard.js";
|
|
4
|
+
import { keptDirectives } from "./managed-elsewhere.projector.js";
|
|
4
5
|
function own(record, key) {
|
|
5
6
|
return Object.hasOwn(record, key) ? record[key] : undefined;
|
|
6
7
|
}
|
|
@@ -32,6 +33,23 @@ function computedOnCreate(resource, name) {
|
|
|
32
33
|
Array.isArray(field.lifecycle) &&
|
|
33
34
|
field.lifecycle.includes("COMPUTED_ON_CREATE"));
|
|
34
35
|
}
|
|
36
|
+
function virtualRequirements(resource) {
|
|
37
|
+
return Object.entries(resource.fields)
|
|
38
|
+
.filter(([, field]) => field.kind === "scalar" &&
|
|
39
|
+
field.nullable === false &&
|
|
40
|
+
Array.isArray(field.lifecycle) &&
|
|
41
|
+
field.lifecycle.includes("VIRTUAL"))
|
|
42
|
+
.map(([name]) => name);
|
|
43
|
+
}
|
|
44
|
+
export function withVirtualRequirements(stated, resource) {
|
|
45
|
+
const [first, ...rest] = [
|
|
46
|
+
...(stated?.required ?? []),
|
|
47
|
+
...virtualRequirements(resource),
|
|
48
|
+
];
|
|
49
|
+
return first === undefined
|
|
50
|
+
? stated
|
|
51
|
+
: { ...stated, required: [first, ...rest] };
|
|
52
|
+
}
|
|
35
53
|
function projectOne(stated, resource, resources) {
|
|
36
54
|
const required = [];
|
|
37
55
|
const requireOnce = (name) => {
|
|
@@ -99,7 +117,7 @@ function projectOne(stated, resource, resources) {
|
|
|
99
117
|
export function projectCreateRequirements(model, resources) {
|
|
100
118
|
for (const [name, resource] of Object.entries(resources)) {
|
|
101
119
|
delete resource.createRequirements;
|
|
102
|
-
const stated = own(model.resources, name)?.createRequirements;
|
|
120
|
+
const stated = withVirtualRequirements(own(model.resources, name)?.createRequirements, resource);
|
|
103
121
|
if (stated === undefined) {
|
|
104
122
|
continue;
|
|
105
123
|
}
|
|
@@ -118,6 +136,7 @@ const UPDATING_DIRECTIVES = [
|
|
|
118
136
|
"$update",
|
|
119
137
|
"$upsert",
|
|
120
138
|
];
|
|
139
|
+
const DELETING_DIRECTIVES = ["$delete"];
|
|
121
140
|
function variantsOf(resource, families) {
|
|
122
141
|
const variants = [];
|
|
123
142
|
for (const family of families) {
|
|
@@ -159,6 +178,36 @@ function createVariantsOf(resource) {
|
|
|
159
178
|
return variants;
|
|
160
179
|
}
|
|
161
180
|
function createPaths(resources) {
|
|
181
|
+
const { created, updated } = writtenResources(resources);
|
|
182
|
+
const paths = new Map();
|
|
183
|
+
const add = (name, path) => {
|
|
184
|
+
const list = paths.get(name) ?? [];
|
|
185
|
+
list.push(path);
|
|
186
|
+
paths.set(name, list);
|
|
187
|
+
};
|
|
188
|
+
for (const [name, resource] of Object.entries(resources)) {
|
|
189
|
+
const variants = createVariantsOf(resource);
|
|
190
|
+
if (variants.length > 0) {
|
|
191
|
+
add(name, { kind: "operation", variants });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const [parent, resource] of Object.entries(resources)) {
|
|
195
|
+
for (const [relation, field] of Object.entries(resource.fields)) {
|
|
196
|
+
const target = field.target;
|
|
197
|
+
if (field.kind !== "relation" || typeof target !== "string") {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if ((created.has(parent) &&
|
|
201
|
+
advertises(field, "create", CREATING_DIRECTIVES)) ||
|
|
202
|
+
(updated.has(parent) &&
|
|
203
|
+
advertises(field, "update", CREATING_DIRECTIVES))) {
|
|
204
|
+
add(target, { kind: "nested", parent, relation });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return paths;
|
|
209
|
+
}
|
|
210
|
+
function writtenResources(resources) {
|
|
162
211
|
const created = new Set();
|
|
163
212
|
const updated = new Set();
|
|
164
213
|
for (const [name, resource] of Object.entries(resources)) {
|
|
@@ -197,33 +246,7 @@ function createPaths(resources) {
|
|
|
197
246
|
}
|
|
198
247
|
}
|
|
199
248
|
}
|
|
200
|
-
|
|
201
|
-
const add = (name, path) => {
|
|
202
|
-
const list = paths.get(name) ?? [];
|
|
203
|
-
list.push(path);
|
|
204
|
-
paths.set(name, list);
|
|
205
|
-
};
|
|
206
|
-
for (const [name, resource] of Object.entries(resources)) {
|
|
207
|
-
const variants = createVariantsOf(resource);
|
|
208
|
-
if (variants.length > 0) {
|
|
209
|
-
add(name, { kind: "operation", variants });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
for (const [parent, resource] of Object.entries(resources)) {
|
|
213
|
-
for (const [relation, field] of Object.entries(resource.fields)) {
|
|
214
|
-
const target = field.target;
|
|
215
|
-
if (field.kind !== "relation" || typeof target !== "string") {
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
if ((created.has(parent) &&
|
|
219
|
-
advertises(field, "create", CREATING_DIRECTIVES)) ||
|
|
220
|
-
(updated.has(parent) &&
|
|
221
|
-
advertises(field, "update", CREATING_DIRECTIVES))) {
|
|
222
|
-
add(target, { kind: "nested", parent, relation });
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return paths;
|
|
249
|
+
return { created, updated };
|
|
227
250
|
}
|
|
228
251
|
function pathLabel(path) {
|
|
229
252
|
return path.kind === "operation"
|
|
@@ -320,6 +343,7 @@ export function checkHiddenRequirements(model, layer, universe, parent, projecti
|
|
|
320
343
|
const parentResource = own(parent, name);
|
|
321
344
|
if (stated === undefined ||
|
|
322
345
|
routes.length === 0 ||
|
|
346
|
+
createsNothing(resource) ||
|
|
323
347
|
universeResource === undefined ||
|
|
324
348
|
parentResource === undefined) {
|
|
325
349
|
continue;
|
|
@@ -367,6 +391,193 @@ export function checkHiddenRequirements(model, layer, universe, parent, projecti
|
|
|
367
391
|
}
|
|
368
392
|
}
|
|
369
393
|
}
|
|
394
|
+
function createsNothing(resource) {
|
|
395
|
+
return (variantsOf(resource, ["create", "upsert"]).length === 0 &&
|
|
396
|
+
!Object.keys(resource.fields).some((name) => creatable(resource, name)));
|
|
397
|
+
}
|
|
398
|
+
function updatesNothing(resource) {
|
|
399
|
+
return (variantsOf(resource, ["update", "upsert"]).length === 0 &&
|
|
400
|
+
!Object.values(resource.fields).some((field) => {
|
|
401
|
+
const capabilities = isRecord(field.capabilities)
|
|
402
|
+
? field.capabilities
|
|
403
|
+
: undefined;
|
|
404
|
+
return field.kind === "scalar"
|
|
405
|
+
? capabilities?.update !== undefined
|
|
406
|
+
: advertisedDirectives(capabilities?.update) !== undefined;
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
function deletesNothing(resource) {
|
|
410
|
+
return variantsOf(resource, ["delete"]).length === 0;
|
|
411
|
+
}
|
|
412
|
+
function advertisedOf(field, stage, directives) {
|
|
413
|
+
const capabilities = isRecord(field.capabilities)
|
|
414
|
+
? field.capabilities
|
|
415
|
+
: undefined;
|
|
416
|
+
const advertised = advertisedDirectives(capabilities?.[stage]) ?? [];
|
|
417
|
+
return directives.filter((name) => advertised.includes(name));
|
|
418
|
+
}
|
|
419
|
+
function readOnlyNestedWrites(resources) {
|
|
420
|
+
const { created, updated } = writtenResources(resources);
|
|
421
|
+
const writes = [];
|
|
422
|
+
for (const [parent, resource] of Object.entries(resources)) {
|
|
423
|
+
for (const [relation, field] of Object.entries(resource.fields)) {
|
|
424
|
+
const target = field.target;
|
|
425
|
+
const targetResource = field.kind === "relation" && typeof target === "string"
|
|
426
|
+
? own(resources, target)
|
|
427
|
+
: undefined;
|
|
428
|
+
if (typeof target !== "string" || targetResource === undefined) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const reached = (directives) => [
|
|
432
|
+
...new Set([
|
|
433
|
+
...(created.has(parent)
|
|
434
|
+
? advertisedOf(field, "create", directives)
|
|
435
|
+
: []),
|
|
436
|
+
...(updated.has(parent)
|
|
437
|
+
? advertisedOf(field, "update", directives)
|
|
438
|
+
: []),
|
|
439
|
+
]),
|
|
440
|
+
];
|
|
441
|
+
const creating = createsNothing(targetResource)
|
|
442
|
+
? reached(CREATING_DIRECTIVES)
|
|
443
|
+
: [];
|
|
444
|
+
const updating = updatesNothing(targetResource)
|
|
445
|
+
? reached(UPDATING_DIRECTIVES)
|
|
446
|
+
: [];
|
|
447
|
+
const deleting = deletesNothing(targetResource)
|
|
448
|
+
? reached(DELETING_DIRECTIVES)
|
|
449
|
+
: [];
|
|
450
|
+
if (creating.length > 0 || updating.length > 0 || deleting.length > 0) {
|
|
451
|
+
writes.push({
|
|
452
|
+
parent,
|
|
453
|
+
relation,
|
|
454
|
+
target,
|
|
455
|
+
creating,
|
|
456
|
+
updating,
|
|
457
|
+
deleting,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return writes;
|
|
463
|
+
}
|
|
464
|
+
export function checkReadOnlyNestedWrites(layer, parent, projection, diagnostics) {
|
|
465
|
+
const inherited = new Map((parent === undefined ? [] : readOnlyNestedWrites(parent)).map((write) => [
|
|
466
|
+
`${write.parent}.${write.relation}`,
|
|
467
|
+
write,
|
|
468
|
+
]));
|
|
469
|
+
for (const found of readOnlyNestedWrites(projection)) {
|
|
470
|
+
const at = `${found.parent}.${found.relation}`;
|
|
471
|
+
const above = inherited.get(at);
|
|
472
|
+
const write = {
|
|
473
|
+
...found,
|
|
474
|
+
creating: (above?.creating.length ?? 0) > 0 ? [] : found.creating,
|
|
475
|
+
updating: (above?.updating.length ?? 0) > 0 ? [] : found.updating,
|
|
476
|
+
deleting: (above?.deleting.length ?? 0) > 0 ? [] : found.deleting,
|
|
477
|
+
};
|
|
478
|
+
if (write.creating.length === 0 &&
|
|
479
|
+
write.updating.length === 0 &&
|
|
480
|
+
write.deleting.length === 0) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const directives = [
|
|
484
|
+
...new Set([...write.creating, ...write.updating, ...write.deleting]),
|
|
485
|
+
];
|
|
486
|
+
const lacks = [
|
|
487
|
+
...(write.creating.length === 0
|
|
488
|
+
? []
|
|
489
|
+
: ["no create operation and no field writable on create"]),
|
|
490
|
+
...(write.updating.length === 0
|
|
491
|
+
? []
|
|
492
|
+
: ["no update operation and no field writable on update"]),
|
|
493
|
+
...(write.deleting.length === 0 ? [] : ["no delete operation"]),
|
|
494
|
+
];
|
|
495
|
+
const gives = [
|
|
496
|
+
...(write.creating.length === 0
|
|
497
|
+
? []
|
|
498
|
+
: ["a create operation or a field writable on create"]),
|
|
499
|
+
...(write.updating.length === 0
|
|
500
|
+
? []
|
|
501
|
+
: ["an update operation or a field writable on update"]),
|
|
502
|
+
...(write.deleting.length === 0 ? [] : ["a delete operation"]),
|
|
503
|
+
];
|
|
504
|
+
const keep = `take ${listed(directives)} off "${at}" (keep only the directives it may use, such as $connect)`;
|
|
505
|
+
diagnostics.push({
|
|
506
|
+
code: "COMPILER_READ_ONLY_NESTED_WRITE",
|
|
507
|
+
path: `${layer}.exemptions.resources.${write.parent}.fields.${write.relation}`,
|
|
508
|
+
message: write.creating.length === 0 && write.updating.length === 0
|
|
509
|
+
? `the ${layer} layer lets relation "${at}" delete rows of resource "${write.target}" ($delete), but "${write.target}" has no delete operation in that layer; ${keep}, or give "${write.target}" a delete operation`
|
|
510
|
+
: `the ${layer} layer lets relation "${at}" write rows of resource "${write.target}" (${listed(directives)}), but "${write.target}" is read-only in that layer: it has ${lacks.join(", and ")}; ${keep}, or give "${write.target}" ${gives.join(", and ")}`,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function dropDirectives(field, stage, dropped) {
|
|
515
|
+
if (!isRecord(field.capabilities)) {
|
|
516
|
+
return [];
|
|
517
|
+
}
|
|
518
|
+
const capabilities = field.capabilities;
|
|
519
|
+
const advertised = advertisedDirectives(capabilities[stage]) ?? [];
|
|
520
|
+
const removed = advertised.filter((name) => dropped.includes(name));
|
|
521
|
+
if (removed.length === 0) {
|
|
522
|
+
return [];
|
|
523
|
+
}
|
|
524
|
+
const narrowed = keptDirectives(capabilities[stage], advertised.filter((name) => !dropped.includes(name)));
|
|
525
|
+
if (narrowed === undefined) {
|
|
526
|
+
delete capabilities[stage];
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
capabilities[stage] = narrowed;
|
|
530
|
+
}
|
|
531
|
+
return removed;
|
|
532
|
+
}
|
|
533
|
+
function* relationsOf(resources) {
|
|
534
|
+
for (const [parent, resource] of Object.entries(resources)) {
|
|
535
|
+
for (const [relation, field] of Object.entries(resource.fields)) {
|
|
536
|
+
const target = field.target;
|
|
537
|
+
const targetResource = field.kind === "relation" && typeof target === "string"
|
|
538
|
+
? own(resources, target)
|
|
539
|
+
: undefined;
|
|
540
|
+
if (typeof target === "string" && targetResource !== undefined) {
|
|
541
|
+
yield [parent, relation, field, target, targetResource];
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
export function withheldKey(relation, stage, directive) {
|
|
547
|
+
return `${relation}.${stage}.${directive}`;
|
|
548
|
+
}
|
|
549
|
+
export function withholdReadOnlyNestedWrites(resources) {
|
|
550
|
+
const withheld = new Map();
|
|
551
|
+
const drop = (at, field, stage, directives, reason) => {
|
|
552
|
+
const removed = dropDirectives(field, stage, directives);
|
|
553
|
+
for (const directive of removed) {
|
|
554
|
+
withheld.set(withheldKey(at, stage, directive), reason);
|
|
555
|
+
}
|
|
556
|
+
return removed.length > 0;
|
|
557
|
+
};
|
|
558
|
+
let changed = true;
|
|
559
|
+
while (changed) {
|
|
560
|
+
changed = false;
|
|
561
|
+
for (const [parent, relation, field, , target] of relationsOf(resources)) {
|
|
562
|
+
const at = `${parent}.${relation}`;
|
|
563
|
+
for (const stage of ["create", "update"]) {
|
|
564
|
+
if (deletesNothing(target)) {
|
|
565
|
+
changed =
|
|
566
|
+
drop(at, field, stage, DELETING_DIRECTIVES, "delete") || changed;
|
|
567
|
+
}
|
|
568
|
+
if (createsNothing(target)) {
|
|
569
|
+
changed =
|
|
570
|
+
drop(at, field, stage, CREATING_DIRECTIVES, "create") || changed;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (updatesNothing(target)) {
|
|
574
|
+
changed =
|
|
575
|
+
drop(at, field, "update", UPDATING_DIRECTIVES, "update") || changed;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return withheld;
|
|
580
|
+
}
|
|
370
581
|
function misnamed(path, message) {
|
|
371
582
|
return { code: "COMPILER_UNKNOWN_FIELD", path, message };
|
|
372
583
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AvAdapterModel } from "../adapters/adapter-model.js";
|
|
2
|
+
import type { AvRelationMutationDirective } from "../contracts/capability.js";
|
|
2
3
|
import type { AvContractCompilerDiagnostic } from "./contract-compiler-diagnostic.js";
|
|
3
4
|
type ProjectedField = Record<string, unknown> & {
|
|
4
5
|
kind: "scalar" | "relation";
|
|
@@ -14,6 +15,8 @@ type ProjectedContract = {
|
|
|
14
15
|
};
|
|
15
16
|
/** The Resources the root's `resources` tree marks `managedElsewhere: true`. */
|
|
16
17
|
export declare function managedElsewhereOf(rootTree: unknown): ReadonlySet<string>;
|
|
18
|
+
/** A relation mutation capability with only `kept` of its directives (its scopes kept), or `undefined` when none is left. */
|
|
19
|
+
export declare function keptDirectives(capability: unknown, kept: readonly AvRelationMutationDirective[]): unknown;
|
|
17
20
|
/**
|
|
18
21
|
* Takes every write of a managed Resource away from `baseline`: its write
|
|
19
22
|
* families, its fields' `create`/`update`, and the directives of every relation
|
|
@@ -22,7 +22,7 @@ export function managedElsewhereOf(rootTree) {
|
|
|
22
22
|
}
|
|
23
23
|
return managed;
|
|
24
24
|
}
|
|
25
|
-
function keptDirectives(capability, kept) {
|
|
25
|
+
export function keptDirectives(capability, kept) {
|
|
26
26
|
if (Array.isArray(capability)) {
|
|
27
27
|
const left = capability.filter((directive) => kept.includes(directive));
|
|
28
28
|
return left.length > 0 ? left : undefined;
|