@aventara/core 0.1.0-pilot.4 → 1.0.0-rc.0
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 +338 -0
- package/README.md +100 -3
- package/dist/adapters/adapter-model.d.ts +8 -0
- package/dist/adapters/adapter.interface.d.ts +6 -2
- package/dist/compiler/contract-compiler-diagnostic.d.ts +6 -0
- package/dist/compiler/contract.compiler.js +67 -0
- package/dist/config/config.resolver.d.ts +3 -0
- package/dist/config/config.resolver.js +1 -0
- package/dist/config/config.validator.js +6 -0
- package/dist/config/define-fields.d.ts +10 -3
- package/dist/config/framework-config.d.ts +4 -1
- package/dist/config/framework-fields-config.d.ts +6 -0
- package/dist/contracts/client-contract-structure.validator.d.ts +21 -1
- package/dist/contracts/client-contract-structure.validator.js +99 -2
- package/dist/contracts/contract-canonicalizer.js +32 -0
- package/dist/contracts/contract.d.ts +7 -0
- package/dist/contracts/field.d.ts +10 -0
- package/dist/contracts/json-shape.d.ts +65 -0
- package/dist/contracts/json-shape.js +1 -0
- package/dist/contracts/reference-write-limit.d.ts +7 -0
- package/dist/contracts/reference-write-limit.js +1 -0
- package/dist/contracts/scalar-value-type.d.ts +12 -1
- package/dist/framework/contract-view.d.ts +126 -21
- package/dist/framework/framework.d.ts +5 -3
- package/dist/framework/framework.factory.js +56 -13
- package/dist/index.d.ts +8 -40
- package/dist/index.js +0 -20
- package/dist/internal.d.ts +35 -0
- package/dist/internal.js +16 -0
- package/dist/json/avz.d.ts +96 -0
- package/dist/json/avz.js +212 -0
- package/dist/json/index.d.ts +6 -0
- package/dist/json/index.js +1 -0
- package/dist/json/json-shape.registry.d.ts +22 -0
- package/dist/json/json-shape.registry.js +16 -0
- package/dist/json/zod-issue.translator.d.ts +51 -0
- package/dist/json/zod-issue.translator.js +142 -0
- package/dist/operations/operation-arguments.d.ts +151 -101
- package/dist/operations/operation-identity.d.ts +2 -0
- package/dist/operations/operation-identity.js +3 -0
- package/dist/operations/operation-validator-core.d.ts +1 -1
- package/dist/operations/operation-validator-core.js +28 -10
- package/dist/operations/operation-validator.d.ts +20 -5
- package/dist/operations/operation-validator.js +26 -10
- package/dist/operations/projection-scope.resolver.d.ts +1 -1
- package/dist/operations/structural-limits.walker.d.ts +19 -0
- package/dist/operations/structural-limits.walker.js +344 -0
- package/dist/protocol/absent-route.responder.d.ts +2 -1
- package/dist/protocol/absent-route.responder.js +32 -6
- package/dist/protocol/http-request.decoder.d.ts +6 -0
- package/dist/protocol/http-request.decoder.js +11 -3
- package/dist/protocol/protocol.binder.d.ts +4 -2
- package/dist/protocol/protocol.binder.js +3 -1
- package/dist/protocol/protocol.handler.d.ts +6 -2
- package/dist/protocol/protocol.handler.js +14 -4
- package/dist/protocol/unmounted-route.error.d.ts +19 -0
- package/dist/protocol/unmounted-route.error.js +10 -0
- package/dist/runtime/caller-data.copier.d.ts +59 -0
- package/dist/runtime/caller-data.copier.js +470 -0
- package/dist/runtime/caller-data.door.d.ts +56 -0
- package/dist/runtime/caller-data.door.js +65 -0
- package/dist/runtime/computed-read-runtime.js +3 -10
- package/dist/runtime/computed-value.error.d.ts +13 -0
- package/dist/runtime/computed-value.error.js +9 -0
- package/dist/runtime/create-requirement.d.ts +43 -7
- package/dist/runtime/create-requirement.js +89 -9
- package/dist/runtime/diagnostics-reporter.d.ts +20 -4
- package/dist/runtime/diagnostics-reporter.js +11 -4
- package/dist/runtime/error-boundary.d.ts +8 -0
- package/dist/runtime/error-boundary.js +65 -35
- package/dist/runtime/field-behavior.d.ts +2 -2
- package/dist/runtime/framework-error.d.ts +12 -0
- package/dist/runtime/misuse-reporter.d.ts +32 -0
- package/dist/runtime/misuse-reporter.js +19 -0
- package/dist/runtime/mutation-planner.js +13 -11
- package/dist/runtime/operation-request.d.ts +4 -11
- package/dist/runtime/operation-runtime.d.ts +14 -7
- package/dist/runtime/operation-runtime.js +37 -18
- package/dist/runtime/outcome-mapper.js +12 -2
- package/dist/runtime/output-validator.d.ts +10 -0
- package/dist/runtime/output-validator.js +11 -0
- package/dist/runtime/owned-execution.registry.d.ts +13 -0
- package/dist/runtime/owned-execution.registry.js +7 -0
- package/dist/runtime/pipeline.d.ts +19 -7
- package/dist/runtime/referential-action.d.ts +7 -0
- package/dist/runtime/request-envelope.validator.d.ts +5 -0
- package/dist/runtime/request-envelope.validator.js +118 -0
- package/dist/runtime/scalar-runtime.d.ts +30 -2
- package/dist/runtime/scalar-runtime.js +162 -79
- package/dist/transactions/transaction-plan-assembler.d.ts +5 -1
- package/dist/transactions/transaction-plan-assembler.js +74 -75
- package/dist/transactions/transaction-plan-validator.d.ts +30 -7
- package/dist/transactions/transaction-plan-validator.js +204 -153
- package/dist/transactions/transaction-reference-collector.d.ts +30 -0
- package/dist/transactions/transaction-reference-collector.js +204 -0
- package/dist/transactions/transaction-reference-resolver.js +10 -5
- package/dist/transactions/transaction-reference-validator.js +24 -17
- package/dist/transactions/transaction-reference.d.ts +23 -51
- package/dist/transactions/transaction-request.d.ts +3 -3
- package/dist/transactions/transaction-response.d.ts +6 -1
- package/dist/transactions/transaction-response.js +24 -1
- package/dist/transactions/transaction-runtime.d.ts +4 -2
- package/dist/transactions/transaction-runtime.js +4 -3
- package/package.json +17 -3
- package/dist/operations/operation-boolean-nodes.d.ts +0 -11
- package/dist/operations/operation-boolean-nodes.js +0 -283
- package/dist/operations/operation-nesting.d.ts +0 -8
- package/dist/operations/operation-nesting.js +0 -307
|
@@ -6,6 +6,7 @@ import { DEFAULT_CONTRACT_LIMITS, } from "../contracts/contract-limits.js";
|
|
|
6
6
|
import { AVENTARA_PROTOCOL_VERSION } from "../contracts/contract-protocol.js";
|
|
7
7
|
import { advertisedDirectives, advertisedDirectivesAt, advertisedReducers, advertisedReducersAt, holdsAt, holdsInAnyScope, } from "../contracts/field.js";
|
|
8
8
|
import { isOperationVariantAvailable, } from "../contracts/resource-operations.js";
|
|
9
|
+
import { bindShapedField, jsonShapeOf } from "../json/json-shape.registry.js";
|
|
9
10
|
const UNIQUE_VARIANTS = [
|
|
10
11
|
["find", "unique"],
|
|
11
12
|
["update", "unique"],
|
|
@@ -99,6 +100,9 @@ function createBaseline(model) {
|
|
|
99
100
|
])),
|
|
100
101
|
limits: { ...DEFAULT_CONTRACT_LIMITS },
|
|
101
102
|
transactions: model.transactions,
|
|
103
|
+
...(model.referenceWrites === undefined
|
|
104
|
+
? {}
|
|
105
|
+
: { referenceWrites: model.referenceWrites }),
|
|
102
106
|
};
|
|
103
107
|
}
|
|
104
108
|
function scalarFieldPath(scope, resource, field) {
|
|
@@ -111,6 +115,10 @@ function structuralValueMatches(field, config, key) {
|
|
|
111
115
|
return JSON.stringify(field[key]) === JSON.stringify(config[key]);
|
|
112
116
|
}
|
|
113
117
|
function addConfiguredScalarField(config, path, diagnostics) {
|
|
118
|
+
if (config.json !== undefined) {
|
|
119
|
+
refuseJsonShape(path, "is not a field of the adapter model", diagnostics);
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
114
122
|
const missing = ["type", "nullable", "list"].filter((key) => config[key] === undefined);
|
|
115
123
|
if (missing.length > 0) {
|
|
116
124
|
diagnostics.push(diagnostic("COMPILER_INCOMPLETE_FIELD", path, `framework-added scalar field is missing required properties: ${missing.join(", ")}`));
|
|
@@ -157,6 +165,8 @@ export function capabilityAddsBaseline(fieldKind, baseline, key, configured) {
|
|
|
157
165
|
}
|
|
158
166
|
const baselineValue = baseline[key];
|
|
159
167
|
switch (axis.kind) {
|
|
168
|
+
case "limit":
|
|
169
|
+
return baselineValue !== undefined && configured !== baselineValue;
|
|
160
170
|
case "flag":
|
|
161
171
|
return configured === true && baselineValue !== true;
|
|
162
172
|
case "tokens": {
|
|
@@ -213,6 +223,31 @@ function advertisedTokensAt(tokenKey, baselineValue, position) {
|
|
|
213
223
|
? advertisedDirectivesAt(baselineValue, position)
|
|
214
224
|
: advertisedReducersAt(baselineValue, position);
|
|
215
225
|
}
|
|
226
|
+
function fieldLabel(path) {
|
|
227
|
+
const [, resource, , field] = path.split(".").slice(1);
|
|
228
|
+
return `${resource}.${field}`;
|
|
229
|
+
}
|
|
230
|
+
function refuseJsonShape(path, reason, diagnostics) {
|
|
231
|
+
diagnostics.push(diagnostic("COMPILER_INVALID_JSON_SHAPE", `${path}.json`, `field "${fieldLabel(path)}" ${reason}; only a Json field the adapter discovered may declare a "json" shape`));
|
|
232
|
+
}
|
|
233
|
+
function applyJsonShape(field, baselineField, shape, path, diagnostics) {
|
|
234
|
+
const type = baselineField.type;
|
|
235
|
+
if (type.scalar !== "json") {
|
|
236
|
+
refuseJsonShape(path, `is ${type.scalar === undefined ? "an enum" : `${/^[aeiou]/.test(type.scalar) ? "an" : "a"} ${type.scalar}`} field`, diagnostics);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const runtime = jsonShapeOf(shape);
|
|
240
|
+
if (runtime === undefined) {
|
|
241
|
+
diagnostics.push(diagnostic("COMPILER_INVALID_JSON_SHAPE", `${path}.json`, `field "${fieldLabel(path)}": "json" must be a shape built with AvZ from "@aventara/core/json"; a Zod schema or any other value is not accepted`));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (runtime.validate(null).length === 0 ||
|
|
245
|
+
runtime.validate(undefined).length === 0) {
|
|
246
|
+
diagnostics.push(diagnostic("COMPILER_INVALID_JSON_SHAPE", `${path}.json`, `field "${fieldLabel(path)}": a field's shape cannot admit null or absence at its top level; whether the field may be null comes from the adapter model`));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
field.json = cloneValue(runtime.schema);
|
|
250
|
+
}
|
|
216
251
|
function applyExistingScalarConfig(field, baselineField, config, path, diagnostics) {
|
|
217
252
|
for (const key of ["type", "nullable", "list"]) {
|
|
218
253
|
if (!structuralValueMatches(field, config, key)) {
|
|
@@ -222,6 +257,9 @@ function applyExistingScalarConfig(field, baselineField, config, path, diagnosti
|
|
|
222
257
|
if (config.format !== undefined) {
|
|
223
258
|
field.format = config.format;
|
|
224
259
|
}
|
|
260
|
+
if (config.json !== undefined) {
|
|
261
|
+
applyJsonShape(field, baselineField, config.json, path, diagnostics);
|
|
262
|
+
}
|
|
225
263
|
if (config.lifecycle !== undefined) {
|
|
226
264
|
diagnostics.push(diagnostic("COMPILER_INVALID_LIFECYCLE", `${path}.lifecycle`, "existing adapter field lifecycle cannot be redefined by framework field configuration"));
|
|
227
265
|
}
|
|
@@ -243,6 +281,9 @@ function applyFrameworkOwnedScalarConfig(field, config, path, diagnostics) {
|
|
|
243
281
|
field[key] = cloneValue(config[key]);
|
|
244
282
|
}
|
|
245
283
|
}
|
|
284
|
+
if (config.json !== undefined) {
|
|
285
|
+
refuseJsonShape(path, "is not a field of the adapter model", diagnostics);
|
|
286
|
+
}
|
|
246
287
|
if (config.format !== undefined) {
|
|
247
288
|
field.format = config.format;
|
|
248
289
|
}
|
|
@@ -391,6 +432,7 @@ const RELATION_CAPABILITY_AXES = {
|
|
|
391
432
|
},
|
|
392
433
|
create: { kind: "scopedTokens", tokenKey: "directives", axis: WRITE_SCOPES },
|
|
393
434
|
update: { kind: "scopedTokens", tokenKey: "directives", axis: WRITE_SCOPES },
|
|
435
|
+
nestedWriteFilter: { kind: "limit", values: ["scalar"] },
|
|
394
436
|
};
|
|
395
437
|
function capabilityAxisFor(kind, key) {
|
|
396
438
|
const axes = kind === "scalar" ? SCALAR_CAPABILITY_AXES : RELATION_CAPABILITY_AXES;
|
|
@@ -478,6 +520,10 @@ function applyScopedTokenRestriction(tokenKey, axis, baselineValue, current, key
|
|
|
478
520
|
return true;
|
|
479
521
|
}
|
|
480
522
|
function applyCapabilityRestriction(fieldKind, inherited, restoration, current, key, value, path, diagnostics) {
|
|
523
|
+
if (capabilityAxisFor(fieldKind, key)?.kind === "limit") {
|
|
524
|
+
unavailableCapability(path, `capability "${key}" is an adapter limit and takes no restriction`, diagnostics);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
481
527
|
if (value === false) {
|
|
482
528
|
delete current[key];
|
|
483
529
|
return;
|
|
@@ -728,6 +774,10 @@ function positionListFault(axis, positions) {
|
|
|
728
774
|
}
|
|
729
775
|
function representationFault(axis, value) {
|
|
730
776
|
switch (axis.kind) {
|
|
777
|
+
case "limit":
|
|
778
|
+
return typeof value === "string" && axis.values.includes(value)
|
|
779
|
+
? undefined
|
|
780
|
+
: `one of ${axis.values.join(", ")} is expected`;
|
|
731
781
|
case "flag":
|
|
732
782
|
return typeof value === "boolean" ? undefined : "a boolean is expected";
|
|
733
783
|
case "tokens":
|
|
@@ -827,6 +877,20 @@ function projectDerivedScope(model, scopeName, core, scope, diagnostics) {
|
|
|
827
877
|
validateProjection(projection, baseline, scopeName, scope.restrictions, diagnostics);
|
|
828
878
|
return projection;
|
|
829
879
|
}
|
|
880
|
+
function bindShapedFields(contract, fields) {
|
|
881
|
+
for (const [resourceName, resource] of Object.entries(contract.resources)) {
|
|
882
|
+
for (const [fieldName, field] of Object.entries(resource.fields)) {
|
|
883
|
+
if (field.kind !== "scalar" || field.json === undefined) {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
const shape = jsonShapeOf(fields?.[resourceName]?.[fieldName]?.json);
|
|
887
|
+
if (shape === undefined) {
|
|
888
|
+
throw new Error(`the Contract compiler found no shape for shaped field "${resourceName}.${fieldName}"`);
|
|
889
|
+
}
|
|
890
|
+
bindShapedField(field, shape);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
830
894
|
export async function compileContracts(model, config) {
|
|
831
895
|
const compilerConfig = config;
|
|
832
896
|
const diagnostics = [];
|
|
@@ -856,6 +920,9 @@ export async function compileContracts(model, config) {
|
|
|
856
920
|
],
|
|
857
921
|
};
|
|
858
922
|
}
|
|
923
|
+
bindShapedFields(core, compilerConfig.root.fields);
|
|
924
|
+
bindShapedFields(application, compilerConfig.application.fields);
|
|
925
|
+
bindShapedFields(client, compilerConfig.client.fields);
|
|
859
926
|
return {
|
|
860
927
|
ok: true,
|
|
861
928
|
contracts: { core, application, client },
|
|
@@ -2,6 +2,7 @@ import type { Adapter } from "../adapters/adapter.interface.js";
|
|
|
2
2
|
import type { AdapterModel } from "../adapters/adapter-model.js";
|
|
3
3
|
import type { TransactionCapability } from "../contracts/transaction-capability.js";
|
|
4
4
|
import type { FrameworkDiagnosticsSink } from "../runtime/diagnostics-reporter.js";
|
|
5
|
+
import type { FrameworkMisuseSink } from "../runtime/misuse-reporter.js";
|
|
5
6
|
import type { FrameworkBehaviorsConfig } from "./framework-behaviors-config.js";
|
|
6
7
|
import type { FrameworkConfig, FrameworkLimitsConfig } from "./framework-config.js";
|
|
7
8
|
import type { FrameworkFieldsConfig } from "./framework-fields-config.js";
|
|
@@ -27,6 +28,8 @@ export type ResolvedFrameworkConfig<M extends AdapterModel = AdapterModel> = {
|
|
|
27
28
|
readonly adapter: Adapter<M>;
|
|
28
29
|
/** The diagnostics sink, from the root configuration only. */
|
|
29
30
|
readonly diagnostics: FrameworkDiagnosticsSink | undefined;
|
|
31
|
+
/** The misuse sink, from the root configuration only. */
|
|
32
|
+
readonly misuse: FrameworkMisuseSink | undefined;
|
|
30
33
|
readonly root: ResolvedFrameworkScopeConfig<M>;
|
|
31
34
|
readonly application: ResolvedFrameworkScopeConfig<M>;
|
|
32
35
|
readonly client: ResolvedFrameworkScopeConfig<M>;
|
|
@@ -103,6 +103,7 @@ export function resolveFrameworkConfig(config) {
|
|
|
103
103
|
entrypoint: resolveEntrypoint(config.entrypoint),
|
|
104
104
|
adapter: config.adapter,
|
|
105
105
|
diagnostics: config.diagnostics,
|
|
106
|
+
misuse: config.misuse,
|
|
106
107
|
root: resolveScope(root),
|
|
107
108
|
application: resolveScope(root, config.application),
|
|
108
109
|
client: resolveScope(root, config.client),
|
|
@@ -10,6 +10,7 @@ const ROOT_PROPERTIES = new Set([
|
|
|
10
10
|
"pipelines",
|
|
11
11
|
"transactions",
|
|
12
12
|
"diagnostics",
|
|
13
|
+
"misuse",
|
|
13
14
|
"application",
|
|
14
15
|
"client",
|
|
15
16
|
]);
|
|
@@ -206,6 +207,11 @@ export function validateFrameworkConfig(value) {
|
|
|
206
207
|
typeof value.diagnostics !== "function") {
|
|
207
208
|
diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", "diagnostics", "diagnostics must be a function when provided"));
|
|
208
209
|
}
|
|
210
|
+
if (Object.hasOwn(value, "misuse") &&
|
|
211
|
+
value.misuse !== undefined &&
|
|
212
|
+
typeof value.misuse !== "function") {
|
|
213
|
+
diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", "misuse", "misuse must be a function when provided"));
|
|
214
|
+
}
|
|
209
215
|
if (Object.hasOwn(value, "application")) {
|
|
210
216
|
validateScope(value.application, "application", diagnostics);
|
|
211
217
|
}
|
|
@@ -10,17 +10,24 @@ export type KnownScalarFieldConfig<M extends AdapterModel = AdapterModel> = Omit
|
|
|
10
10
|
};
|
|
11
11
|
/**
|
|
12
12
|
* A scalar field the configuration adds, which the adapter did not discover.
|
|
13
|
-
* `type`, `nullable` and `list` are required.
|
|
13
|
+
* `type`, `nullable` and `list` are required; it takes no `json` shape.
|
|
14
14
|
*/
|
|
15
|
-
export type AddedScalarFieldConfig<M extends AdapterModel = AdapterModel> = Omit<FrameworkScalarFieldConfig<M>, "type" | "nullable" | "list"> & {
|
|
15
|
+
export type AddedScalarFieldConfig<M extends AdapterModel = AdapterModel> = Omit<FrameworkScalarFieldConfig<M>, "type" | "nullable" | "list" | "json"> & {
|
|
16
16
|
readonly type: FrameworkScalarTypeRef<M>;
|
|
17
17
|
readonly nullable: boolean;
|
|
18
18
|
readonly list: boolean;
|
|
19
|
+
readonly json?: never;
|
|
19
20
|
};
|
|
20
21
|
/** The shape one authored field name admits, by what the adapter knows of it. */
|
|
21
22
|
type ScalarFieldConfigFor<M extends AdapterModel, R extends FrameworkResourceKey<M>, F> = string extends AdapterFieldKey<M, R> ? FrameworkScalarFieldConfig<M> : F extends AdapterFieldKey<M, R> ? M["resources"][R]["fields"][F] extends {
|
|
22
23
|
readonly kind: "relation";
|
|
23
|
-
} ? never :
|
|
24
|
+
} ? never : M["resources"][R]["fields"][F] extends {
|
|
25
|
+
readonly type: {
|
|
26
|
+
readonly scalar: "json";
|
|
27
|
+
};
|
|
28
|
+
} ? KnownScalarFieldConfig<M> : Omit<KnownScalarFieldConfig<M>, "json"> & {
|
|
29
|
+
readonly json?: never;
|
|
30
|
+
} : AddedScalarFieldConfig<M>;
|
|
24
31
|
/** The shape an authored `fields` block is checked against, keyed on its own names. */
|
|
25
32
|
export type FrameworkFieldsShape<M extends AdapterModel, Self> = {
|
|
26
33
|
readonly [R in keyof Self]: R extends FrameworkResourceKey<M> ? {
|
|
@@ -3,6 +3,7 @@ import type { AdapterModel } from "../adapters/adapter-model.js";
|
|
|
3
3
|
import type { ContractLimits } from "../contracts/contract-limits.js";
|
|
4
4
|
import type { TransactionCapability } from "../contracts/transaction-capability.js";
|
|
5
5
|
import type { FrameworkDiagnosticsSink } from "../runtime/diagnostics-reporter.js";
|
|
6
|
+
import type { FrameworkMisuseSink } from "../runtime/misuse-reporter.js";
|
|
6
7
|
import type { AnyFrameworkConfig } from "./any-framework-config.js";
|
|
7
8
|
import type { FrameworkBehaviorsConfig } from "./framework-behaviors-config.js";
|
|
8
9
|
import type { FrameworkFieldsConfig } from "./framework-fields-config.js";
|
|
@@ -28,7 +29,7 @@ export type FrameworkScopeConfig<M extends AdapterModel = AdapterModel, Cfg = An
|
|
|
28
29
|
/** Request limits; see {@link FrameworkLimitsConfig}. */
|
|
29
30
|
readonly limits?: FrameworkLimitsConfig;
|
|
30
31
|
/** Guards, pipes, hooks, interceptors and filters run around each operation. */
|
|
31
|
-
readonly pipelines?: PipelineConfig<M
|
|
32
|
+
readonly pipelines?: PipelineConfig<NoInfer<M>>;
|
|
32
33
|
/** `"none"` turns transactions off; `"interactive"` needs adapter support. */
|
|
33
34
|
readonly transactions?: TransactionCapability;
|
|
34
35
|
};
|
|
@@ -40,6 +41,8 @@ export type FrameworkConfig<M extends AdapterModel = AdapterModel, Cfg = AnyFram
|
|
|
40
41
|
readonly entrypoint: string;
|
|
41
42
|
readonly adapter: Adapter<M>;
|
|
42
43
|
readonly diagnostics?: FrameworkDiagnosticsSink;
|
|
44
|
+
/** Told about every caller value refused as unreadable or not plain data. */
|
|
45
|
+
readonly misuse?: FrameworkMisuseSink;
|
|
43
46
|
readonly application?: FrameworkScopeConfig<M, Cfg>;
|
|
44
47
|
readonly client?: FrameworkScopeConfig<M, Cfg>;
|
|
45
48
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AdapterModel } from "../adapters/adapter-model.js";
|
|
2
2
|
import type { ScalarFieldCapabilities, ScalarFormat } from "../contracts/field.js";
|
|
3
|
+
import type { JsonShape } from "../contracts/json-shape.js";
|
|
3
4
|
import type { LifecycleTrait } from "../contracts/lifecycle-trait.js";
|
|
4
5
|
/** The lifecycle traits a configuration may set on a field. */
|
|
5
6
|
export type FrameworkFieldLifecycleTrait = Extract<LifecycleTrait, "VIRTUAL">;
|
|
@@ -29,6 +30,11 @@ export type FrameworkScalarFieldConfig<M extends AdapterModel = AdapterModel> =
|
|
|
29
30
|
readonly lifecycle?: readonly FrameworkFieldLifecycleTrait[];
|
|
30
31
|
/** The field's capabilities; see {@link ScalarFieldCapabilities}. */
|
|
31
32
|
readonly capabilities?: Partial<ScalarFieldCapabilities>;
|
|
33
|
+
/**
|
|
34
|
+
* A Json field's shape, built with `AvZ` from `@aventara/core/json`: writes
|
|
35
|
+
* to the field are checked against it, and its value is typed by it.
|
|
36
|
+
*/
|
|
37
|
+
readonly json?: JsonShape;
|
|
32
38
|
};
|
|
33
39
|
/** Field settings for one Resource, by field name. */
|
|
34
40
|
export type FrameworkResourceFieldsConfig<M extends AdapterModel = AdapterModel> = Readonly<Record<string, FrameworkScalarFieldConfig<M>>>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClientContract } from "./contract.js";
|
|
2
|
+
import type { JsonSchema } from "./json-shape.js";
|
|
2
3
|
/**
|
|
3
4
|
* Whether an untrusted value is a `ClientContract`: every member present, of its JSON
|
|
4
5
|
* kind, inside its closed vocabulary, and nothing else. An enum's values must be
|
|
@@ -44,6 +45,12 @@ export type StructureNode = {
|
|
|
44
45
|
} | {
|
|
45
46
|
readonly is: "union";
|
|
46
47
|
readonly arms: readonly StructureNode[];
|
|
48
|
+
} | {
|
|
49
|
+
/**
|
|
50
|
+
* A Json Shape's schema: recursive, so judged by its own reader
|
|
51
|
+
* ({@link jsonSchemaDefect}) rather than spelled out in nodes.
|
|
52
|
+
*/
|
|
53
|
+
readonly is: "jsonSchema";
|
|
47
54
|
};
|
|
48
55
|
/**
|
|
49
56
|
* The TypeScript type a structure description admits, dispatched through a
|
|
@@ -79,6 +86,7 @@ type StructuredBy<N> = {
|
|
|
79
86
|
readonly union: N extends {
|
|
80
87
|
readonly arms: readonly (infer A)[];
|
|
81
88
|
} ? Structured<A> : never;
|
|
89
|
+
readonly jsonSchema: JsonSchema;
|
|
82
90
|
};
|
|
83
91
|
type StructuredObject<R, O> = {
|
|
84
92
|
readonly [K in keyof R]: Structured<R[K]>;
|
|
@@ -219,6 +227,9 @@ export declare const CLIENT_CONTRACT_STRUCTURE: {
|
|
|
219
227
|
readonly format: {
|
|
220
228
|
readonly is: "string";
|
|
221
229
|
};
|
|
230
|
+
readonly json: {
|
|
231
|
+
readonly is: "jsonSchema";
|
|
232
|
+
};
|
|
222
233
|
};
|
|
223
234
|
};
|
|
224
235
|
readonly relation: {
|
|
@@ -396,6 +407,10 @@ export declare const CLIENT_CONTRACT_STRUCTURE: {
|
|
|
396
407
|
readonly optional: Record<never, never>;
|
|
397
408
|
}];
|
|
398
409
|
};
|
|
410
|
+
readonly nestedWriteFilter: {
|
|
411
|
+
readonly is: "literal";
|
|
412
|
+
readonly values: readonly ["scalar"];
|
|
413
|
+
};
|
|
399
414
|
};
|
|
400
415
|
};
|
|
401
416
|
};
|
|
@@ -1057,7 +1072,12 @@ export declare const CLIENT_CONTRACT_STRUCTURE: {
|
|
|
1057
1072
|
readonly optional: Record<never, never>;
|
|
1058
1073
|
};
|
|
1059
1074
|
};
|
|
1060
|
-
readonly optional:
|
|
1075
|
+
readonly optional: {
|
|
1076
|
+
readonly referenceWrites: {
|
|
1077
|
+
readonly is: "literal";
|
|
1078
|
+
readonly values: readonly ["uniform"];
|
|
1079
|
+
};
|
|
1080
|
+
};
|
|
1061
1081
|
};
|
|
1062
1082
|
/**
|
|
1063
1083
|
* The answer of {@link validateClientContractStructure}. A refusal names the
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PROJECTION_SCOPES, WRITE_SCOPES } from "./capability.js";
|
|
2
2
|
import { AVENTARA_PROTOCOL_VERSION } from "./contract-protocol.js";
|
|
3
3
|
import { LIFECYCLE_TRAITS } from "./lifecycle-trait.js";
|
|
4
|
+
import { REFERENCE_WRITE_LIMITS } from "./reference-write-limit.js";
|
|
4
5
|
import { TRANSACTION_CAPABILITIES } from "./transaction-capability.js";
|
|
5
6
|
const STRING = { is: "string" };
|
|
6
7
|
const NUMBER = { is: "number" };
|
|
@@ -106,7 +107,7 @@ const SCALAR_FIELD = object({
|
|
|
106
107
|
create: list(literal(SCALAR_MUTATION_DIRECTIVES)),
|
|
107
108
|
update: list(literal(SCALAR_MUTATION_DIRECTIVES)),
|
|
108
109
|
}),
|
|
109
|
-
}, { format: STRING });
|
|
110
|
+
}, { format: STRING, json: { is: "jsonSchema" } });
|
|
110
111
|
const RELATION_FIELD = object({
|
|
111
112
|
kind: literal(["relation"]),
|
|
112
113
|
target: STRING,
|
|
@@ -124,6 +125,7 @@ const RELATION_FIELD = object({
|
|
|
124
125
|
})),
|
|
125
126
|
create: RELATION_MUTATION,
|
|
126
127
|
update: RELATION_MUTATION,
|
|
128
|
+
nestedWriteFilter: literal(["scalar"]),
|
|
127
129
|
}),
|
|
128
130
|
});
|
|
129
131
|
const OPERATION_AVAILABILITY = union(literal([true]), object({ projection: literal(PROJECTION_SCOPES) }, { write: literal(WRITE_SCOPES) }), object({ write: literal(WRITE_SCOPES) }, { projection: literal(PROJECTION_SCOPES) }));
|
|
@@ -177,7 +179,7 @@ export const CLIENT_CONTRACT_STRUCTURE = object({
|
|
|
177
179
|
version: literal([AVENTARA_PROTOCOL_VERSION]),
|
|
178
180
|
hash: { is: "prefixed", prefix: "sha256:" },
|
|
179
181
|
}),
|
|
180
|
-
});
|
|
182
|
+
}, { referenceWrites: literal(REFERENCE_WRITE_LIMITS) });
|
|
181
183
|
export function validateClientContractStructure(value) {
|
|
182
184
|
const defect = defectOf(CLIENT_CONTRACT_STRUCTURE, value, "$", 0);
|
|
183
185
|
if (defect !== undefined) {
|
|
@@ -233,6 +235,101 @@ function defectOf(node, value, path, depth) {
|
|
|
233
235
|
}
|
|
234
236
|
case "union":
|
|
235
237
|
return unionDefect(node.arms, value, path, depth);
|
|
238
|
+
case "jsonSchema":
|
|
239
|
+
return jsonSchemaDefect(value, path, depth);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const JSON_SCHEMA_SCALAR_TYPES = [
|
|
243
|
+
"string",
|
|
244
|
+
"number",
|
|
245
|
+
"integer",
|
|
246
|
+
"boolean",
|
|
247
|
+
"null",
|
|
248
|
+
];
|
|
249
|
+
const JSON_PRIMITIVE = union(STRING, NUMBER, BOOLEAN);
|
|
250
|
+
function jsonSchemaDefect(value, path, depth) {
|
|
251
|
+
const kind = kindDefect(value, "object", path, depth);
|
|
252
|
+
if (kind !== undefined)
|
|
253
|
+
return kind;
|
|
254
|
+
const schema = value;
|
|
255
|
+
const keys = Object.keys(schema).sort().join(",");
|
|
256
|
+
const child = (member, entry, at = `${path}.${member}`) => jsonSchemaDefect(entry, at, depth + 1);
|
|
257
|
+
const children = (member, minimum) => {
|
|
258
|
+
const listKind = kindDefect(schema[member], "array", `${path}.${member}`, depth + 1);
|
|
259
|
+
if (listKind !== undefined)
|
|
260
|
+
return listKind;
|
|
261
|
+
const entries = schema[member];
|
|
262
|
+
if (entries.length < minimum) {
|
|
263
|
+
return at(`${path}.${member}`, depth + 1, `needs at least ${minimum} entries`);
|
|
264
|
+
}
|
|
265
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
266
|
+
const defect = child(member, entries[index], `${path}.${member}[${index}]`);
|
|
267
|
+
if (defect !== undefined)
|
|
268
|
+
return defect;
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
};
|
|
272
|
+
switch (keys) {
|
|
273
|
+
case "type":
|
|
274
|
+
return literalDefect(JSON_SCHEMA_SCALAR_TYPES, schema.type, `${path}.type`, depth + 1);
|
|
275
|
+
case "const":
|
|
276
|
+
return Object.is(schema.const, null)
|
|
277
|
+
? undefined
|
|
278
|
+
: defectOf(JSON_PRIMITIVE, schema.const, `${path}.const`, depth + 1);
|
|
279
|
+
case "enum,type":
|
|
280
|
+
return (literalDefect(["string"], schema.type, `${path}.type`, depth + 1) ??
|
|
281
|
+
defectOf({ is: "list", item: STRING, nonEmpty: true, unique: true }, schema.enum, `${path}.enum`, depth + 1));
|
|
282
|
+
case "additionalProperties,properties,type":
|
|
283
|
+
case "additionalProperties,properties,required,type": {
|
|
284
|
+
const typeDefect = literalDefect(["object"], schema.type, `${path}.type`, depth + 1) ??
|
|
285
|
+
literalDefect([false], schema.additionalProperties, `${path}.additionalProperties`, depth + 1);
|
|
286
|
+
if (typeDefect !== undefined)
|
|
287
|
+
return typeDefect;
|
|
288
|
+
const propertiesKind = kindDefect(schema.properties, "object", `${path}.properties`, depth + 1);
|
|
289
|
+
if (propertiesKind !== undefined)
|
|
290
|
+
return propertiesKind;
|
|
291
|
+
const properties = schema.properties;
|
|
292
|
+
for (const name of Object.keys(properties)) {
|
|
293
|
+
const defect = child("properties", properties[name], `${path}.properties.${name}`);
|
|
294
|
+
if (defect !== undefined)
|
|
295
|
+
return defect;
|
|
296
|
+
}
|
|
297
|
+
if (!Object.hasOwn(schema, "required"))
|
|
298
|
+
return undefined;
|
|
299
|
+
const requiredDefect = defectOf({ is: "list", item: STRING, nonEmpty: false, unique: true }, schema.required, `${path}.required`, depth + 1);
|
|
300
|
+
if (requiredDefect !== undefined)
|
|
301
|
+
return requiredDefect;
|
|
302
|
+
const undeclared = schema.required.findIndex((name) => !Object.hasOwn(properties, name));
|
|
303
|
+
return undeclared === -1
|
|
304
|
+
? undefined
|
|
305
|
+
: at(`${path}.required[${undeclared}]`, depth + 1, "names a key that is not one of the object's properties");
|
|
306
|
+
}
|
|
307
|
+
case "additionalProperties,type":
|
|
308
|
+
return (literalDefect(["object"], schema.type, `${path}.type`, depth + 1) ??
|
|
309
|
+
child("additionalProperties", schema.additionalProperties));
|
|
310
|
+
case "items,type":
|
|
311
|
+
return (literalDefect(["array"], schema.type, `${path}.type`, depth + 1) ??
|
|
312
|
+
child("items", schema.items));
|
|
313
|
+
case "items,maxItems,minItems,prefixItems,type": {
|
|
314
|
+
const defect = literalDefect(["array"], schema.type, `${path}.type`, depth + 1) ??
|
|
315
|
+
literalDefect([false], schema.items, `${path}.items`, depth + 1) ??
|
|
316
|
+
children("prefixItems", 1);
|
|
317
|
+
if (defect !== undefined)
|
|
318
|
+
return defect;
|
|
319
|
+
const length = schema.prefixItems.length;
|
|
320
|
+
for (const bound of ["minItems", "maxItems"]) {
|
|
321
|
+
if (schema[bound] !== length) {
|
|
322
|
+
return at(`${path}.${bound}`, depth + 1, `is ${JSON.stringify(schema[bound])}, not the tuple's length ${length}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
case "anyOf":
|
|
328
|
+
return children("anyOf", 2);
|
|
329
|
+
case "oneOf":
|
|
330
|
+
return children("oneOf", 1);
|
|
331
|
+
default:
|
|
332
|
+
return at(path, depth, `is not a Json Shape schema: its keywords {${keys}} are not one of the forms Aventara emits`);
|
|
236
333
|
}
|
|
237
334
|
}
|
|
238
335
|
function literalDefect(values, value, path, depth) {
|
|
@@ -83,10 +83,42 @@ function normalizeCapabilities(capabilities) {
|
|
|
83
83
|
}
|
|
84
84
|
return normalized;
|
|
85
85
|
}
|
|
86
|
+
function normalizeJsonSchema(schema) {
|
|
87
|
+
if (Array.isArray(schema)) {
|
|
88
|
+
return schema.map(normalizeJsonSchema);
|
|
89
|
+
}
|
|
90
|
+
if (typeof schema !== "object" || schema === null) {
|
|
91
|
+
return schema;
|
|
92
|
+
}
|
|
93
|
+
const normalized = {};
|
|
94
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
95
|
+
if (key === "required" && Array.isArray(value)) {
|
|
96
|
+
normalized[key] = [...value].sort((left, right) => compareCodeUnits(String(left), String(right)));
|
|
97
|
+
}
|
|
98
|
+
else if (key === "properties" &&
|
|
99
|
+
typeof value === "object" &&
|
|
100
|
+
value !== null) {
|
|
101
|
+
normalized[key] = Object.fromEntries(Object.entries(value).map(([name, child]) => [
|
|
102
|
+
name,
|
|
103
|
+
normalizeJsonSchema(child),
|
|
104
|
+
]));
|
|
105
|
+
}
|
|
106
|
+
else if (key === "const" || key === "enum") {
|
|
107
|
+
normalized[key] = value;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
normalized[key] = normalizeJsonSchema(value);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return normalized;
|
|
114
|
+
}
|
|
86
115
|
function normalizeField(field) {
|
|
87
116
|
if (field.kind === "scalar") {
|
|
88
117
|
return {
|
|
89
118
|
...field,
|
|
119
|
+
...(field.json !== undefined
|
|
120
|
+
? { json: normalizeJsonSchema(field.json) }
|
|
121
|
+
: {}),
|
|
90
122
|
lifecycle: normalizeStringSet(field.lifecycle, LIFECYCLE_TRAITS),
|
|
91
123
|
capabilities: normalizeCapabilities(field.capabilities),
|
|
92
124
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ContractLimits } from "./contract-limits.js";
|
|
2
2
|
import type { ClientContractProtocol } from "./contract-protocol.js";
|
|
3
3
|
import type { EnumDefinition } from "./enum-definition.js";
|
|
4
|
+
import type { ReferenceWriteLimit } from "./reference-write-limit.js";
|
|
4
5
|
import type { Resource } from "./resource.js";
|
|
5
6
|
import type { ScalarRegistry } from "./scalar.js";
|
|
6
7
|
import type { TransactionCapability } from "./transaction-capability.js";
|
|
@@ -16,6 +17,12 @@ export interface Contract {
|
|
|
16
17
|
readonly limits: ContractLimits;
|
|
17
18
|
/** Whether interactive transactions are available. */
|
|
18
19
|
readonly transactions: TransactionCapability;
|
|
20
|
+
/**
|
|
21
|
+
* `"uniform"` when one record must write the relations that hold a reference
|
|
22
|
+
* either all as relations or all through their reference fields; a record
|
|
23
|
+
* that mixes them is refused with `A2004`. Absent: each relation either way.
|
|
24
|
+
*/
|
|
25
|
+
readonly referenceWrites?: ReferenceWriteLimit;
|
|
19
26
|
}
|
|
20
27
|
/** The Contract compiled from the root configuration. */
|
|
21
28
|
export interface CoreContract extends Contract {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PaginationCapability, ProjectionScope, RelationFilterCapability, RelationMutationDirective, RelationReducer, ScalarFilterOperator, ScalarMutationDirective, WriteScope } from "./capability.js";
|
|
2
2
|
import type { Contract } from "./contract.js";
|
|
3
|
+
import type { JsonSchema } from "./json-shape.js";
|
|
3
4
|
import type { LifecycleTrait } from "./lifecycle-trait.js";
|
|
4
5
|
/** A format name that refines a scalar type, carried in the Contract as data. */
|
|
5
6
|
export type ScalarFormat = string;
|
|
@@ -63,6 +64,13 @@ export type RelationFieldCapabilities = {
|
|
|
63
64
|
readonly reducers?: RelationReducerCapability;
|
|
64
65
|
readonly create?: RelationMutationCapability;
|
|
65
66
|
readonly update?: RelationMutationCapability;
|
|
67
|
+
/**
|
|
68
|
+
* What the `where` of a nested `$update` or `$delete` through this relation may
|
|
69
|
+
* filter: `"scalar"` — the target's scalar fields only, combined with
|
|
70
|
+
* `AND`/`OR`/`NOT`, and no relation filter at any depth. Absent: any filter the
|
|
71
|
+
* target takes.
|
|
72
|
+
*/
|
|
73
|
+
readonly nestedWriteFilter?: "scalar";
|
|
66
74
|
};
|
|
67
75
|
/**
|
|
68
76
|
* The reducers a relation advertises, in any projection position. Use
|
|
@@ -105,6 +113,8 @@ export type ScalarField<C extends Contract> = {
|
|
|
105
113
|
readonly list: boolean;
|
|
106
114
|
readonly lifecycle: readonly LifecycleTrait[];
|
|
107
115
|
readonly capabilities: ScalarFieldCapabilities;
|
|
116
|
+
/** A `json` field's shape, as JSON Schema: what every value written to it is. */
|
|
117
|
+
readonly json?: JsonSchema;
|
|
108
118
|
};
|
|
109
119
|
/** A relation field; the `target` Resource describes the related row. */
|
|
110
120
|
export type RelationField<C extends Contract> = {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The phantom key a Json Shape's TypeScript type travels under. Type-level only:
|
|
3
|
+
* no value ever holds it.
|
|
4
|
+
*/
|
|
5
|
+
declare const jsonShapeValue: unique symbol;
|
|
6
|
+
/**
|
|
7
|
+
* A Json Shape, as the type system sees it: the TypeScript type `Value` of a
|
|
8
|
+
* value that conforms to it. `AvZ` builds every one; a value from anywhere else —
|
|
9
|
+
* a Zod schema included — is not one.
|
|
10
|
+
*/
|
|
11
|
+
export interface JsonShape<Value = unknown> {
|
|
12
|
+
readonly [jsonShapeValue]: Value;
|
|
13
|
+
}
|
|
14
|
+
/** The TypeScript type a {@link JsonShape} describes. */
|
|
15
|
+
export type JsonShapeValue<Shape extends JsonShape> = Shape[typeof jsonShapeValue];
|
|
16
|
+
/** A JSON primitive: what a `const` names. */
|
|
17
|
+
export type JsonSchemaPrimitive = string | number | boolean | null;
|
|
18
|
+
/**
|
|
19
|
+
* A Json Shape in a Contract: passive JSON Schema (draft 2020-12), restricted to
|
|
20
|
+
* the forms Aventara emits.
|
|
21
|
+
*/
|
|
22
|
+
export type JsonSchema = {
|
|
23
|
+
readonly type: "string" | "number" | "integer" | "boolean" | "null";
|
|
24
|
+
} | {
|
|
25
|
+
readonly const: JsonSchemaPrimitive;
|
|
26
|
+
} | {
|
|
27
|
+
readonly type: "string";
|
|
28
|
+
readonly enum: readonly [string, ...string[]];
|
|
29
|
+
} | {
|
|
30
|
+
readonly type: "object";
|
|
31
|
+
readonly properties: {
|
|
32
|
+
readonly [key: string]: JsonSchema;
|
|
33
|
+
};
|
|
34
|
+
readonly required?: readonly string[];
|
|
35
|
+
readonly additionalProperties: false;
|
|
36
|
+
} | {
|
|
37
|
+
readonly type: "object";
|
|
38
|
+
readonly additionalProperties: JsonSchema;
|
|
39
|
+
} | {
|
|
40
|
+
readonly type: "array";
|
|
41
|
+
readonly items: JsonSchema;
|
|
42
|
+
} | {
|
|
43
|
+
readonly type: "array";
|
|
44
|
+
readonly prefixItems: readonly [JsonSchema, ...JsonSchema[]];
|
|
45
|
+
readonly items: false;
|
|
46
|
+
readonly minItems: number;
|
|
47
|
+
readonly maxItems: number;
|
|
48
|
+
} | {
|
|
49
|
+
readonly anyOf: readonly [JsonSchema, JsonSchema, ...JsonSchema[]];
|
|
50
|
+
} | {
|
|
51
|
+
readonly oneOf: readonly [JsonSchema, ...JsonSchema[]];
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* The type-level carrier of a shaped Json field's TypeScript type, on the field's
|
|
55
|
+
* `json` member in a typed Contract view. Never present at runtime.
|
|
56
|
+
*/
|
|
57
|
+
export type JsonFieldType<Value> = {
|
|
58
|
+
readonly "~jsonType": Value;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* A shaped Json field's `json` member in a typed Contract view: the schema, and
|
|
62
|
+
* the type it describes.
|
|
63
|
+
*/
|
|
64
|
+
export type ShapedJsonSchema<Value> = JsonSchema & JsonFieldType<Value>;
|
|
65
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The limits a data source may put on how one record writes its references. */
|
|
2
|
+
export declare const REFERENCE_WRITE_LIMITS: readonly ["uniform"];
|
|
3
|
+
/**
|
|
4
|
+
* `"uniform"`: one record writes the relations that hold a reference either all
|
|
5
|
+
* as relations or all through their reference fields, never some each way.
|
|
6
|
+
*/
|
|
7
|
+
export type ReferenceWriteLimit = (typeof REFERENCE_WRITE_LIMITS)[number];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const REFERENCE_WRITE_LIMITS = ["uniform"];
|