@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.
Files changed (108) hide show
  1. package/CHANGELOG.md +338 -0
  2. package/README.md +100 -3
  3. package/dist/adapters/adapter-model.d.ts +8 -0
  4. package/dist/adapters/adapter.interface.d.ts +6 -2
  5. package/dist/compiler/contract-compiler-diagnostic.d.ts +6 -0
  6. package/dist/compiler/contract.compiler.js +67 -0
  7. package/dist/config/config.resolver.d.ts +3 -0
  8. package/dist/config/config.resolver.js +1 -0
  9. package/dist/config/config.validator.js +6 -0
  10. package/dist/config/define-fields.d.ts +10 -3
  11. package/dist/config/framework-config.d.ts +4 -1
  12. package/dist/config/framework-fields-config.d.ts +6 -0
  13. package/dist/contracts/client-contract-structure.validator.d.ts +21 -1
  14. package/dist/contracts/client-contract-structure.validator.js +99 -2
  15. package/dist/contracts/contract-canonicalizer.js +32 -0
  16. package/dist/contracts/contract.d.ts +7 -0
  17. package/dist/contracts/field.d.ts +10 -0
  18. package/dist/contracts/json-shape.d.ts +65 -0
  19. package/dist/contracts/json-shape.js +1 -0
  20. package/dist/contracts/reference-write-limit.d.ts +7 -0
  21. package/dist/contracts/reference-write-limit.js +1 -0
  22. package/dist/contracts/scalar-value-type.d.ts +12 -1
  23. package/dist/framework/contract-view.d.ts +126 -21
  24. package/dist/framework/framework.d.ts +5 -3
  25. package/dist/framework/framework.factory.js +56 -13
  26. package/dist/index.d.ts +8 -40
  27. package/dist/index.js +0 -20
  28. package/dist/internal.d.ts +35 -0
  29. package/dist/internal.js +16 -0
  30. package/dist/json/avz.d.ts +96 -0
  31. package/dist/json/avz.js +212 -0
  32. package/dist/json/index.d.ts +6 -0
  33. package/dist/json/index.js +1 -0
  34. package/dist/json/json-shape.registry.d.ts +22 -0
  35. package/dist/json/json-shape.registry.js +16 -0
  36. package/dist/json/zod-issue.translator.d.ts +51 -0
  37. package/dist/json/zod-issue.translator.js +142 -0
  38. package/dist/operations/operation-arguments.d.ts +151 -101
  39. package/dist/operations/operation-identity.d.ts +2 -0
  40. package/dist/operations/operation-identity.js +3 -0
  41. package/dist/operations/operation-validator-core.d.ts +1 -1
  42. package/dist/operations/operation-validator-core.js +28 -10
  43. package/dist/operations/operation-validator.d.ts +20 -5
  44. package/dist/operations/operation-validator.js +26 -10
  45. package/dist/operations/projection-scope.resolver.d.ts +1 -1
  46. package/dist/operations/structural-limits.walker.d.ts +19 -0
  47. package/dist/operations/structural-limits.walker.js +344 -0
  48. package/dist/protocol/absent-route.responder.d.ts +2 -1
  49. package/dist/protocol/absent-route.responder.js +32 -6
  50. package/dist/protocol/http-request.decoder.d.ts +6 -0
  51. package/dist/protocol/http-request.decoder.js +11 -3
  52. package/dist/protocol/protocol.binder.d.ts +4 -2
  53. package/dist/protocol/protocol.binder.js +3 -1
  54. package/dist/protocol/protocol.handler.d.ts +6 -2
  55. package/dist/protocol/protocol.handler.js +14 -4
  56. package/dist/protocol/unmounted-route.error.d.ts +19 -0
  57. package/dist/protocol/unmounted-route.error.js +10 -0
  58. package/dist/runtime/caller-data.copier.d.ts +59 -0
  59. package/dist/runtime/caller-data.copier.js +470 -0
  60. package/dist/runtime/caller-data.door.d.ts +56 -0
  61. package/dist/runtime/caller-data.door.js +65 -0
  62. package/dist/runtime/computed-read-runtime.js +3 -10
  63. package/dist/runtime/computed-value.error.d.ts +13 -0
  64. package/dist/runtime/computed-value.error.js +9 -0
  65. package/dist/runtime/create-requirement.d.ts +43 -7
  66. package/dist/runtime/create-requirement.js +89 -9
  67. package/dist/runtime/diagnostics-reporter.d.ts +20 -4
  68. package/dist/runtime/diagnostics-reporter.js +11 -4
  69. package/dist/runtime/error-boundary.d.ts +8 -0
  70. package/dist/runtime/error-boundary.js +65 -35
  71. package/dist/runtime/field-behavior.d.ts +2 -2
  72. package/dist/runtime/framework-error.d.ts +12 -0
  73. package/dist/runtime/misuse-reporter.d.ts +32 -0
  74. package/dist/runtime/misuse-reporter.js +19 -0
  75. package/dist/runtime/mutation-planner.js +13 -11
  76. package/dist/runtime/operation-request.d.ts +4 -11
  77. package/dist/runtime/operation-runtime.d.ts +14 -7
  78. package/dist/runtime/operation-runtime.js +37 -18
  79. package/dist/runtime/outcome-mapper.js +12 -2
  80. package/dist/runtime/output-validator.d.ts +10 -0
  81. package/dist/runtime/output-validator.js +11 -0
  82. package/dist/runtime/owned-execution.registry.d.ts +13 -0
  83. package/dist/runtime/owned-execution.registry.js +7 -0
  84. package/dist/runtime/pipeline.d.ts +19 -7
  85. package/dist/runtime/referential-action.d.ts +7 -0
  86. package/dist/runtime/request-envelope.validator.d.ts +5 -0
  87. package/dist/runtime/request-envelope.validator.js +118 -0
  88. package/dist/runtime/scalar-runtime.d.ts +30 -2
  89. package/dist/runtime/scalar-runtime.js +162 -79
  90. package/dist/transactions/transaction-plan-assembler.d.ts +5 -1
  91. package/dist/transactions/transaction-plan-assembler.js +74 -75
  92. package/dist/transactions/transaction-plan-validator.d.ts +30 -7
  93. package/dist/transactions/transaction-plan-validator.js +204 -153
  94. package/dist/transactions/transaction-reference-collector.d.ts +30 -0
  95. package/dist/transactions/transaction-reference-collector.js +204 -0
  96. package/dist/transactions/transaction-reference-resolver.js +10 -5
  97. package/dist/transactions/transaction-reference-validator.js +24 -17
  98. package/dist/transactions/transaction-reference.d.ts +23 -51
  99. package/dist/transactions/transaction-request.d.ts +3 -3
  100. package/dist/transactions/transaction-response.d.ts +6 -1
  101. package/dist/transactions/transaction-response.js +24 -1
  102. package/dist/transactions/transaction-runtime.d.ts +4 -2
  103. package/dist/transactions/transaction-runtime.js +4 -3
  104. package/package.json +17 -3
  105. package/dist/operations/operation-boolean-nodes.d.ts +0 -11
  106. package/dist/operations/operation-boolean-nodes.js +0 -283
  107. package/dist/operations/operation-nesting.d.ts +0 -8
  108. 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 : KnownScalarFieldConfig<M> : AddedScalarFieldConfig<M>;
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: Record<never, never>;
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"];