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

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 (89) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/ERRORS.md +2 -2
  3. package/README.md +80 -7
  4. package/dist/adapters/adapter-field.d.ts +8 -0
  5. package/dist/adapters/adapter-identifier.d.ts +9 -0
  6. package/dist/adapters/adapter-identifier.js +6 -1
  7. package/dist/adapters/adapter-operation.d.ts +12 -6
  8. package/dist/compiler/contract-compiler-diagnostic.guidance.js +2 -2
  9. package/dist/compiler/contract.compiler.js +26 -3
  10. package/dist/config/config.resolver.d.ts +2 -0
  11. package/dist/config/config.resolver.js +7 -2
  12. package/dist/config/config.validator.js +36 -12
  13. package/dist/config/exact-framework-config.d.ts +11 -1
  14. package/dist/config/framework-config.d.ts +14 -1
  15. package/dist/contracts/contract-canonicalizer.js +2 -0
  16. package/dist/contracts/contract.d.ts +15 -1
  17. package/dist/contracts/field.d.ts +17 -0
  18. package/dist/contracts/scalar-value-type.d.ts +3 -1
  19. package/dist/framework/contract-view.d.ts +9 -1
  20. package/dist/framework/framework-construction.error.d.ts +2 -1
  21. package/dist/framework/framework-construction.error.js +13 -1
  22. package/dist/framework/framework.d.ts +26 -5
  23. package/dist/framework/framework.factory.js +29 -10
  24. package/dist/framework/operation-facade.d.ts +59 -12
  25. package/dist/index.d.ts +8 -4
  26. package/dist/internal.d.ts +2 -2
  27. package/dist/internal.js +2 -0
  28. package/dist/operations/operation-arguments.d.ts +43 -27
  29. package/dist/operations/operation-call.d.ts +2 -2
  30. package/dist/operations/operation-result.d.ts +1 -1
  31. package/dist/policy/access-provider.interface.d.ts +36 -0
  32. package/dist/policy/access-provider.interface.js +1 -0
  33. package/dist/policy/access-provider.registry.d.ts +9 -0
  34. package/dist/policy/access-provider.registry.js +10 -0
  35. package/dist/policy/policy-admission.js +12 -3
  36. package/dist/policy/policy-after-update.runtime.js +5 -4
  37. package/dist/policy/policy-engine.d.ts +11 -4
  38. package/dist/policy/policy-nested-write.runtime.js +11 -7
  39. package/dist/policy/policy-read.runtime.d.ts +0 -2
  40. package/dist/policy/policy-read.runtime.js +18 -9
  41. package/dist/policy/policy-request.d.ts +7 -3
  42. package/dist/policy/policy-request.js +14 -3
  43. package/dist/policy/policy-session.d.ts +14 -4
  44. package/dist/policy/policy-session.js +12 -11
  45. package/dist/policy/policy-startup.validator.js +13 -15
  46. package/dist/policy/policy-write.runtime.d.ts +7 -0
  47. package/dist/policy/policy-write.runtime.js +23 -8
  48. package/dist/prebuilt/core-version.generated.d.ts +1 -1
  49. package/dist/prebuilt/core-version.generated.js +1 -1
  50. package/dist/prebuilt/prebuilt-changes.d.ts +28 -0
  51. package/dist/prebuilt/prebuilt-changes.js +65 -0
  52. package/dist/prebuilt/prebuilt-contracts.loader.js +11 -56
  53. package/dist/prebuilt/prebuilt-inputs.fingerprint.js +16 -1
  54. package/dist/protocol/absent-route.responder.d.ts +4 -4
  55. package/dist/protocol/http-request.decoder.d.ts +2 -2
  56. package/dist/protocol/http-request.reader.d.ts +2 -2
  57. package/dist/protocol/http-response.encoder.js +2 -2
  58. package/dist/protocol/protocol-headers.d.ts +3 -3
  59. package/dist/runtime/caller-data.copier.js +3 -4
  60. package/dist/runtime/computed-read-runtime.js +2 -7
  61. package/dist/runtime/context-executor.runtime.d.ts +2 -2
  62. package/dist/runtime/core-class.brand.d.ts +10 -0
  63. package/dist/runtime/core-class.brand.js +21 -0
  64. package/dist/runtime/create-requirement.js +1 -1
  65. package/dist/runtime/decimal.d.ts +14 -0
  66. package/dist/runtime/decimal.js +21 -0
  67. package/dist/runtime/diagnostics-reporter.d.ts +5 -5
  68. package/dist/runtime/diagnostics-reporter.js +13 -1
  69. package/dist/runtime/framework-error.js +9 -0
  70. package/dist/runtime/operation-request.d.ts +9 -9
  71. package/dist/runtime/operation-runtime.d.ts +8 -8
  72. package/dist/runtime/operation-runtime.js +30 -9
  73. package/dist/runtime/owned-execution.registry.d.ts +2 -2
  74. package/dist/runtime/readonly-value.d.ts +1 -1
  75. package/dist/runtime/readonly-value.js +2 -2
  76. package/dist/runtime/referential-action.d.ts +1 -1
  77. package/dist/runtime/scalar-runtime.js +4 -5
  78. package/dist/transactions/operation-arguments-encoder.js +8 -3
  79. package/dist/transactions/sql-statement.d.ts +2 -2
  80. package/dist/transactions/sql-statement.validator.js +2 -2
  81. package/dist/transactions/transaction-integrity-error.js +2 -0
  82. package/dist/transactions/transaction-plan-assembler.d.ts +11 -4
  83. package/dist/transactions/transaction-plan-assembler.js +17 -2
  84. package/dist/transactions/transaction-plan-validator.d.ts +5 -5
  85. package/dist/transactions/transaction-request.d.ts +8 -8
  86. package/dist/transactions/transaction-runtime.d.ts +2 -2
  87. package/dist/transactions/transaction-runtime.js +12 -7
  88. package/dist/transactions/transaction-slot-path-error.js +2 -0
  89. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,162 @@
1
1
  # @aventara/core
2
2
 
3
+ ## 1.0.0-rc.9
4
+
5
+ ### Patch Changes
6
+
7
+ - 205a379: `createFramework` takes `access`, an access provider: the policy engine and the request state from one object.
8
+ `AvAccessProvider` is the new main-entry type it is written against, with two members:
9
+
10
+ - `policy`: the engine every client and agent request is checked against, as `policy.engine` takes it.
11
+ - `state({ headers, request })`: seeds each request's `ctx.$state`. Its `me` is the principal the engine is asked
12
+ about, `null` for an anonymous caller.
13
+
14
+ Aventara Access provides one, and a hand-written provider works the same way (a till's PIN sign-in, say: see the
15
+ README). The host calls `state` once per request; server code that calls the Framework directly passes the state
16
+ itself. A governed field of the provider's engine is left out of the default projection's type, as with `policy`.
17
+ `access` together with `policy` is refused at startup (`CONFIG_INVALID_VALUE` at `policy`) and is a type error at
18
+ `policy`. A provider whose `policy` is no engine or whose `state` is no function is refused at startup, member by
19
+ member, and the engine's `governed`, `paths` and `afterChecks` are checked against the Core Contract under
20
+ `access.policy`. A prebuilt Contracts file counts the provider by its engine's governed set alone.
21
+
22
+ - a0bb05e: A Framework's `diagnostics` sink receives every report when the process holds more than one copy of `@aventara/core`
23
+ (Next.js giving each layer its own copy, a duplicate install, a monorepo). A protocol failure such as a result with no
24
+ wire form (`500 A3003`), or an MCP endpoint failure, raised through another copy than the one that built the Framework
25
+ now reaches the sink instead of being silently dropped. Copies of one major version share the sinks; nothing in the
26
+ public API or its types changed.
27
+ - 86ab319: Values and errors from `@aventara/core` are recognized across duplicate copies of the package in one process. A
28
+ bundler that gives each layer its own copy (Next.js instrumentation, route handlers and pages), a duplicate install, a
29
+ monorepo, Vite SSR, per-function bundles and test runners' module isolation all load core more than once.
30
+
31
+ - A decimal made by one copy is an `AvDecimal` to every other: `value instanceof AvDecimal` is `true`, the protocol
32
+ encodes it (a `find.many` selecting a decimal no longer answers `500 A3003` "Result serialization failed"), and a
33
+ Framework accepts it in arguments, `$sql` values and Adapter results.
34
+ - `AvError` and its subclasses (`AvValidationError`, `AvNotFoundError`, `AvConflictError`, `AvAuthError`,
35
+ `AvInternalError`, `AvProtocolError`, `AvContractMismatchError`), `AvConstructionError`,
36
+ `AvTransactionIntegrityError` and `AvTransactionSlotPathError` pass `instanceof` across copies, with the same
37
+ subclass rules: every one is an `AvError`, and each is only its own subclass. An `AvError` a pipeline throws with
38
+ another copy's class is answered as the outcome it names. A subclass you declare yourself is checked as before.
39
+ - Copies recognize each other within one major version only. Nothing in the public API or its types changed.
40
+
41
+ - fdf52e6: `framework.serverClient` and `framework.serverClientTx`: the client layer for server code, called with application
42
+ values, such as a Next.js Server Component's.
43
+
44
+ - `framework.serverClient.<Resource>.<family>.<variant>(args, options?)` is typed from the Client Contract's view, as
45
+ `framework.client` is: what the client layer hides is absent. Its arguments are the application forms (`Date`,
46
+ `bigint`, `AvDecimal`, `Uint8Array`), the ones `framework.application` takes, and their wire strings are type errors.
47
+ Its results are `framework.client`'s. At runtime a value already in its wire form is accepted too, as
48
+ `framework.client` reads it.
49
+ - Each call copies its arguments, encodes the copy into the wire forms against the Client Contract, and runs the request
50
+ `framework.client` would run for those wire forms: the same client pipelines, validation, exposure and policy, the
51
+ same answers and the same `AvError` classes. `framework.client` itself is unchanged and keeps the wire forms.
52
+ - `framework.serverClientTx` builds client-origin transaction handles from application values. One
53
+ `framework.transaction([...])` may hold them beside `clientTx` handles, never beside `appTx` ones.
54
+ - New main-entry type names: `AvServerClientFacade`, `AvServerClientResourceFacade`, `AvServerClientFamilyFacade`,
55
+ `AvServerClientProjectingCall`, `AvServerClientPlainCall`, `AvServerClientTxFacade`,
56
+ `AvServerClientTxResourceFacade`, `AvServerClientTxFamilyFacade`, `AvServerClientTxProjectingCall`,
57
+ `AvServerClientTxPlainCall`.
58
+
59
+ ## 1.0.0-rc.8
60
+
61
+ 1.0.0-rc.5, rc.6 and rc.7 were tagged but never published; 1.0.0-rc.8 contains everything listed under them and the changes below.
62
+
63
+ ### Patch Changes
64
+
65
+ - 1c8e4ee: Helpers typed off the façades compile again in projects that emit declarations. In 1.0.0-rc.7 a function that took
66
+ `Parameters<AppFramework["application"]["Post"]["find"]["many"]>[0]` and returned its `where` (or built a new one
67
+ from it, `{ AND: [args.where ?? {}, { published: { equals: true } }] }`) without a return type failed `nest build`
68
+ (and `tsc` with `"declaration": true`) with `TS4023: ... has or is using name 'ScalarFilterOperands' ... but cannot be
69
+ named`, usually beside `TS7056`. A nested write's data could fail the same way, or name `NO_ADVERTISED_MEMBER`, and a
70
+ nested `$update`'s `where` was written into the `.d.ts` as `any`. The declaration now names the argument types
71
+ `@aventara/core` exports (`AvWhereInput`, `AvUpdateDataInput`, `AvCreateDataInput`, ...), and these new exports cover
72
+ the rest:
73
+
74
+ - `AvScalarFilterOperands`: one field's filter operators, which a filter's `not` holds again.
75
+ - `AvScalarWhereInput`: the `where` of a nested `$update` or `$delete` that only takes plain fields.
76
+ - `AvNoAdvertisedMember`: a write member that advertises nothing; it takes `{}` and refuses every key.
77
+ - `AvSqlStatementBuilder` and `AvSqlStatementValue`: the type of `framework.appTx.$sql` and the values it binds.
78
+ A method returning `framework.appTx.$sql` failed with `TS2883`/`TS2742` before.
79
+
80
+ `framework.execute` and `framework.executeTransaction` take request types that `@aventara/core` exports:
81
+ `AvOperationRequest` (`AvApplicationOperationRequest`, `AvClientOperationRequest`, `AvAgentOperationRequest`),
82
+ `AvTransactionRequest` (`AvApplicationTransactionRequest`, `AvClientTransactionRequest`,
83
+ `AvAgentTransactionRequest`) and `AvRequestProtocol` (a remote request's `protocol`). They were named from
84
+ `@aventara/core/internal` (`FrameworkOperationRequest`, `FrameworkTransactionRequest`, ...), so a declaration that
85
+ spelled them reached into the package's internal entry. Those names are gone from `@aventara/core/internal`.
86
+
87
+ An adapter's `referentialActions` edges name the fields that hold the reference `referenceFields`, as a relation of
88
+ the Core Contract does. They were called `references`, which the Core Contract uses for the target's fields.
89
+
90
+ Reading a projecting variant's argument type (`Parameters<…["find"]["many"]>[0]`) also type-checks faster: about
91
+ 6,000 type instantiations over a three-field model, where rc.7 took about 29,000. The part of the argument other than
92
+ `select`/`include` is now marked `NoInfer` in the call's parameter, so an editor shows that parameter as
93
+ `NoInfer<Omit<…>> & …`. What it accepts is unchanged.
94
+
95
+ A create's `data` type now states each required field once, as a required member (`Readonly<{ title: string; } & {
96
+ body?: ... }>` in an editor's expanded hover). Before, a required field appeared as an optional member beside a
97
+ separate `{ readonly title: WrittenValue }`, and a declaration file wrote both. What a create accepts and refuses is
98
+ unchanged.
99
+
100
+ - Operation results read as their value types in an editor and in emitted declarations. A result field that is nullable,
101
+ an enum or a `boolean` (such as a `Boolean @default(false)` column) was shown as `AvScalarValueType<…>`, with the
102
+ whole Contract view as its argument, and a project with `"declaration": true` wrote that view into its `.d.ts` once
103
+ per such field: a Nest service of five methods with no return types came out at 524 lines. Those fields now read
104
+ `body: string | null`, `published: boolean`, `publishedAt: Date | null`, and that service's declaration is 44 lines.
105
+ A result, and a relation included in it, is shown as its fields rather than as `ProjectedFields<…>`. This covers
106
+ `framework.application` and `framework.client` results, the same fields in a write's `data`, and clients generated
107
+ by `avclient`. What each type accepts is unchanged.
108
+
109
+ ## 1.0.0-rc.7
110
+
111
+ 1.0.0-rc.5 and 1.0.0-rc.6 were tagged but never published; 1.0.0-rc.7 contains everything listed under them and the changes below.
112
+
113
+ ### Patch Changes
114
+
115
+ - f917c1c: A helper that reads `framework.contracts.core` compiles again in projects that emit declarations. In 1.0.0-rc.6 a
116
+ function returning a Resource or a field of the Core Contract without a return type, such as
117
+ `core.resources[resource]?.fields[relation]`, failed `nest build` (and `tsc` with `"declaration": true`, which Nest's
118
+ `tsconfig.json` sets) with `TS2883: The inferred type of 'relationOf' cannot be named without a reference to
119
+ 'AvCoreField'` under TypeScript 6 (`TS2742` under 5.9), or with `TS4058`/`TS4023` naming `AvCoreResource`.
120
+ `@aventara/core` now exports the three types the Core Contract is written with: `AvCoreResource` (a Resource),
121
+ `AvCoreField` (one of its fields) and `AvCoreRelationField` (a relation field, with the target fields its reference
122
+ names in `references`).
123
+
124
+ A policy's startup check states the path once: `CONFIG_INVALID_VALUE at policy.engine.governed.Nope: "Nope" is no
125
+ Resource of the Core Contract`, where rc.6 repeated `policy.engine.governed.Nope:` inside the message. Each
126
+ diagnostic's `message` no longer starts with its `path`; read the path from `path`.
127
+
128
+ ## 1.0.0-rc.6
129
+
130
+ 1.0.0-rc.5 was tagged but never published. 1.0.0-rc.6 contains everything listed under 1.0.0-rc.5 and the fix below.
131
+
132
+ ### Patch Changes
133
+
134
+ - 1cf72e0: Typed policy engines compile again in projects that emit declarations. In 1.0.0-rc.5 an engine written
135
+ `as const satisfies AvPolicyEngine` failed `nest build` (and `tsc` with `"declaration": true`, which Nest's
136
+ `tsconfig.json` sets) with `TS2883: The inferred type of 'policyEngine' cannot be named without a reference to
137
+ 'PolicyFieldsAccess'` under TypeScript 6, or `TS2742` under 5.9, and so did the configuration and the framework holding
138
+ it. `fieldsOf`'s question is now written out in full in `AvPolicyEngine`, so a declaration can spell it.
139
+
140
+ A read question's `action` is now typed `undefined`, so an engine can destructure it from either question:
141
+ `fieldsOf({ resource, access, action })`. A write question's `action` is `"create"` or `"update"`, as before.
142
+
143
+ ## 1.0.0-rc.5
144
+
145
+ ### Patch Changes
146
+
147
+ - 4795ff1: Two additions for policy engines. A Framework without a policy, and every Contract a caller is served, are unchanged.
148
+
149
+ - A policy engine can check a create whose row reaches another Resource through a relation. Discovery now states, on the end of a relation that holds the reference, which of the target's fields its reference fields name, pair by pair (`Note.campaign`: `referenceFields: ["campaignId"]`, `references: ["id"]`). An engine reads them from the model it is built from (a Prisma discovery artifact's `model`) or from `framework.contracts.core`, and asks one `exists` about the row they name: "a player creates a note only in a campaign they play in". Only the adapter's model and the Core Contract carry them. The Application, Client and Agent Contracts, `/_contract`, the MCP tools and every ClientContract hash are unchanged. The discovery artifact's model hash, and so a prebuilt Contracts file's fingerprint, change: regenerate the artifact and rebuild the prebuilt file.
150
+ - `fieldsOf({ access: "write" })` also names the action that writes the record, `action: "create" | "update"` (an upsert's branches each their own, a nested record its directive's), so an engine can lock a field on update and still let a create write it. A read question names none. An engine that ignores it keeps working.
151
+
152
+ - 9e178ca: Fixes for applications that check requests with a policy engine. A Framework without a policy behaves as before.
153
+
154
+ - A cursor on a row the caller cannot read is answered exactly as a cursor on a row that does not exist, at the root and in a to-many relation, whatever values the caller sends beside the identifier. Before, the page started at the hidden row, so it told that the row exists and where its values sort. The Prisma adapter reads the cursor's row under the read scope first: one statement more for a cursor on a readable row, and only that read for one on a row the caller cannot read.
155
+ - A unique update with a nested write (`$create`, `$connect`, …) on a row the caller can read but not change answers `A4002`, as the same update without one does. It answered `A2003`. A row the caller cannot read still answers `A2003`.
156
+ - The `exists` questions a policy asks select the Resource's primary identifier, never another unique field such as a governed `email`.
157
+ - `policy.principal` may answer with a promise. A synchronous principal works as before; a rejection answers `A3000`, as a throw does.
158
+ - `AvConstructionError.message` names the first problem and how many more there are, for example `Aventara framework configuration validation failed: CONFIG_INVALID_TYPE at adapter.execute: adapter.execute must be a function (+2 more)`. `stage` and `diagnostics` are unchanged.
159
+
3
160
  ## 1.0.0-rc.4
4
161
 
5
162
  ### Patch Changes
package/ERRORS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Aventara error codes
2
2
 
3
- Every code `@aventara/core` 1.0.0-rc.4 answers, reports in a validation issue, or refuses to start with. Generated from `AvErrorCatalog` (exported by `@aventara/core`); do not edit.
3
+ Every code `@aventara/core` 1.0.0-rc.9 answers, reports in a validation issue, or refuses to start with. Generated from `AvErrorCatalog` (exported by `@aventara/core`); do not edit.
4
4
 
5
5
  Branch on the code, never on the message. In code, `AvErrorCatalog[code]` gives the same entry: `{ code, kind, message, cause, fix, docs }`.
6
6
 
@@ -134,7 +134,7 @@ Each entry of `cause.issues[]` under `A2001`, `A2002`, `A2004`, `A2007` or `A200
134
134
  | [`COMPILER_INVALID_LIFECYCLE`](https://aventara-framework.vercel.app/docs/error-codes#compiler_invalid_lifecycle) | Invalid field lifecycle. | An added field declares a lifecycle other than VIRTUAL, a VIRTUAL field claims a database-generated lifecycle or a provider-side capability, or a definition redefines a discovered field's lifecycle. | Give an added field lifecycle: ["VIRTUAL"] and only the capabilities it can serve; computed lifecycles come from compute stages. |
135
135
  | [`COMPILER_MISSING_READ_COMPUTE`](https://aventara-framework.vercel.app/docs/error-codes#compiler_missing_read_compute) | A selectable virtual field has no compute.read. | A VIRTUAL field that can be selected has no pipelines.compute.read stage. | Add a compute.read stage, or make the field write-only with capabilities.select: false. |
136
136
  | [`COMPILER_INVALID_COMPUTE_DEPENDENCY`](https://aventara-framework.vercel.app/docs/error-codes#compiler_invalid_compute_dependency) | Invalid compute.read dependency. | A dependsOn entry that is not a stored scalar field of the same Resource. | List only stored scalar fields of the Resource in dependsOn. |
137
- | [`COMPILER_RESERVED_RESOURCE_NAME`](https://aventara-framework.vercel.app/docs/error-codes#compiler_reserved_resource_name) | Reserved Resource name. | A model name starts with $, which is reserved for the framework's own names (ctx.$sql, $ref). | Rename the model; the table it maps to can keep its name. |
137
+ | [`COMPILER_RESERVED_RESOURCE_NAME`](https://aventara-framework.vercel.app/docs/error-codes#compiler_reserved_resource_name) | Reserved Resource name. | A model name starts with $, which is reserved for the framework's own names (ctx.$sql, $ref), or, on a host that adds members beside the Resources, is one of those members' names (on Next.js: tx and transaction on avClient, appTx and transaction on avApp). | Rename the model; the table it maps to can keep its name. A host member's name is also left free when the layer it belongs to hides the Resource. |
138
138
  | [`COMPILER_MISSING_IDENTIFIER`](https://aventara-framework.vercel.app/docs/error-codes#compiler_missing_identifier) | A unique operation has no identifier. | A layer exposes a unique variant (find.unique, update.unique, delete.unique, upsert.unique) and no complete identifier of the Resource: a Pick that did not pick one, or an Omit that hid every one. | Expose an identifier field (id, or every field of one unique), or exempt the unique variants. |
139
139
  | [`COMPILER_HIDDEN_REQUIRED_FIELD`](https://aventara-framework.vercel.app/docs/error-codes#compiler_hidden_required_field) | A layer that creates a Resource hides a required field. | A layer still creates the Resource (a create or upsert variant, or a nested create) while a required field, or a required relation's reference field, is hidden or has its create capability exempted, and no compute.create fills it. | Expose the field, give it a compute.create stage, or exempt the layer's create operations. |
140
140
  | [`COMPILER_INVALID_VALIDATOR`](https://aventara-framework.vercel.app/docs/error-codes#compiler_invalid_validator) | A field validator cannot apply. | A validator that is not built with AvZ, does not fit the field's type (AvZ.maxLength on an int, AvZ.maxItems on a field that is not a list), or sits on a relation or Json field. | Use an AvZ check that fits the field's type; a Json field's checks go in its json shape. |
package/README.md CHANGED
@@ -118,6 +118,27 @@ try {
118
118
  generated client throws the same classes by the same table. `framework.execute` and `framework.executeTransaction`, the
119
119
  entries a transport calls, still answer the envelope.
120
120
 
121
+ ## The client layer, called with your own values
122
+
123
+ `framework.client` reads its arguments in the wire forms a decoded request body carries: a `datetime` is its ISO string,
124
+ a `bigint` its base-10 string. That suits a test that should behave like a remote caller. Server code that acts for a
125
+ remote caller, such as a Server Component or a Server Action, holds `Date`s and `bigint`s instead. It calls
126
+ `framework.serverClient`:
127
+
128
+ ```ts
129
+ const recent = await framework.serverClient.Post.find.many({
130
+ where: { createdAt: { gt: new Date(Date.now() - 86_400_000) } },
131
+ select: ["id", "title"],
132
+ });
133
+ ```
134
+
135
+ `serverClient` has the client layer's types and rules: what the client layer hides is absent, and the client
136
+ pipelines, validation, exposure and policy run. Its arguments are the application forms (`Date`, `bigint`,
137
+ `AvDecimal`, `Uint8Array`), the same ones `framework.application` takes, and its results are `framework.client`'s.
138
+ Each call copies its arguments, encodes them into the wire forms and runs the request `framework.client` would run.
139
+ `framework.serverClientTx` builds client-layer transaction handles the same way; one `framework.transaction([...])`
140
+ may hold them beside `clientTx` handles, never beside `appTx` ones.
141
+
121
142
  ## Every code, explained
122
143
 
123
144
  `AvErrorCatalog` holds every code this package answers, reports in a validation issue, or refuses to start with: its
@@ -199,15 +220,15 @@ findMany(): import("@aventara/core").AvApplicationProjectingCall<
199
220
  "Post", { readonly family: "find"; readonly variant: "many" }>;
200
221
  ```
201
222
 
202
- The names, one set per surface (`application`, `client`, `appTx`, `clientTx`):
223
+ The names, one set per surface (`application`, `client`, `serverClient`, `appTx`, `clientTx`, `serverClientTx`):
203
224
 
204
225
  | Node | `framework.application` | the others |
205
226
  | --- | --- | --- |
206
- | the whole surface | `AvApplicationFacade` | `AvClientFacade`, `AvAppTxFacade`, `AvClientTxFacade` |
227
+ | the whole surface | `AvApplicationFacade` | `AvClientFacade`, `AvServerClientFacade`, `AvAppTxFacade`, `AvClientTxFacade`, `AvServerClientTxFacade` |
207
228
  | one Resource | `AvApplicationResourceFacade` | `AvClientResourceFacade`, … |
208
229
  | one family | `AvApplicationFamilyFacade` | `AvClientFamilyFacade`, … |
209
- | a variant that takes a `select` or `include` | `AvApplicationProjectingCall` | `AvClientProjectingCall`, `AvAppTxProjectingCall`, `AvClientTxProjectingCall` |
210
- | a variant that does not (the `count` variants: `find.count`, `update.count`, …) | `AvApplicationPlainCall` | `AvClientPlainCall`, `AvAppTxPlainCall`, `AvClientTxPlainCall` |
230
+ | a variant that takes a `select` or `include` | `AvApplicationProjectingCall` | `AvClientProjectingCall`, `AvServerClientProjectingCall`, `AvAppTxProjectingCall`, `AvClientTxProjectingCall`, `AvServerClientTxProjectingCall` |
231
+ | a variant that does not (the `count` variants: `find.count`, `update.count`, …) | `AvApplicationPlainCall` | `AvClientPlainCall`, `AvServerClientPlainCall`, `AvAppTxPlainCall`, `AvClientTxPlainCall`, `AvServerClientTxPlainCall` |
211
232
 
212
233
  Each takes the view, `AvContractView`, as its first argument. The view's arguments are your model by its name, the root's
213
234
  and the overlay's exposure as written, and the `fields` entries your configuration's `resources` blocks hold. A
@@ -230,8 +251,8 @@ never with the model the inline stages were typed from.
230
251
 
231
252
  ## Server-side calls copy their input
232
253
 
233
- `framework.application`, `framework.client`, `framework.execute`, `framework.executeTransaction`, and the `appTx` /
234
- `clientTx` builders read what you pass them once, when you call them, and copy it into plain data the framework owns.
254
+ `framework.application`, `framework.client`, `framework.serverClient`, `framework.execute`,
255
+ `framework.executeTransaction`, and the `appTx` / `clientTx` / `serverClientTx` builders read what you pass them once, when you call them, and copy it into plain data the framework owns.
235
256
  Validation, your guards and pipes, computed values and the adapter all see only the copy, and your stages see it
236
257
  frozen: only a pipe changes the arguments, by returning new ones, so what the validators checked is what the adapter
237
258
  writes. In practice:
@@ -357,7 +378,11 @@ projection; name it to get it, `select: ["$defaults", "email"]`. A refusal answe
357
378
  Writes are checked too. A create asks `can` about each new row, whose rules may look rows up through `exists`. An
358
379
  update or delete acts only on rows the caller may change and read; a single-row one that misses answers `A4002` when
359
380
  the caller can read the row and `A2003` when it cannot. A field the write sends must be in
360
- `fieldsOf({ access: "write" })`. An update that moves a row out of what the caller reads, or fails an `after` condition
381
+ `fieldsOf({ access: "write", action })`, `action` being `"create"` or `"update"` (an upsert's branches and a nested
382
+ record's directive each their own), so a field can be locked on update and still written at create. A rule that
383
+ reaches another Resource through a relation (a note only in a campaign its author plays in) reads which of the
384
+ target's fields the reference names from the Core Contract (`framework.contracts.core`, a relation's `references`
385
+ beside its `referenceFields`); no Contract a caller is served states them. An update that moves a row out of what the caller reads, or fails an `after` condition
361
386
  (`can` asked about the row the update returned), is rolled back. Where `transactionsStyle` is `"interactive"`, a
362
387
  checked write with a check after it (an update, a nested write) runs in a transaction of its own, and a create or
363
388
  delete checked before it runs is one statement; where it is `"none"`, a write that could only be checked after it runs
@@ -370,6 +395,54 @@ caller may change or delete, as `update.many` does. After the call, every row th
370
395
  (the after check, the move-out re-read), and every row they created is read back as stored and asked `can`; one refusal
371
396
  rolls the whole write back. Where `transactionsStyle` is `"none"`, a nested write under a policy is refused.
372
397
 
398
+ ## An access provider
399
+
400
+ `access` takes the policy engine and the request state from one object, an `AvAccessProvider`. Aventara Access
401
+ provides one, and you can write your own. It has two members:
402
+
403
+ - `policy`: the engine, as `policy.engine` takes it.
404
+ - `state({ headers, request })`: seeds each request's `ctx.$state`. Its `me` is the principal the engine is asked
405
+ about (`null` for an anonymous caller).
406
+
407
+ The host calls `state` once per request: `@aventara/nest` with the platform request's headers and the request itself,
408
+ `@aventara/next` with the request's headers (and the route's `Request` on a route call). With `access`, leave out
409
+ `policy` and the host's own `state` option; either one beside it is refused at startup, and as a type error where the
410
+ configuration's type shows both. Server code that calls the Framework directly passes the state itself,
411
+ `{ state: await access.state({ headers }) }`.
412
+
413
+ A point-of-sale till with its own PIN sign-in, written by hand:
414
+
415
+ ```ts
416
+ import type { AvAccessProvider } from "@aventara/core";
417
+
418
+ type Cashier = { id: string; role: "cashier" | "manager" };
419
+
420
+ declare module "@aventara/core" {
421
+ interface AvState {
422
+ me?: Cashier | null;
423
+ }
424
+ }
425
+
426
+ export const tillAccess = {
427
+ // The till sends the PIN the cashier typed as `x-pos-pin`.
428
+ state: async ({ headers }) => ({ me: await cashierByPin(headers.get("x-pos-pin")) }),
429
+ policy: {
430
+ can: ({ principal, resource, action }) => {
431
+ const me = principal as Cashier | null;
432
+ if (me === null) return false; // no PIN, no till
433
+ if (resource === "Refund" && action !== "read") return me.role === "manager";
434
+ return true;
435
+ },
436
+ scopeOf: () => true,
437
+ fieldsOf: () => ({ always: "all", perRow: {} }),
438
+ },
439
+ } satisfies AvAccessProvider;
440
+
441
+ const framework = await createFramework({ api: "/api", adapter, access: tillAccess });
442
+ ```
443
+
444
+ Every stage reads the cashier as `ctx.$state.me`, typed by the `AvState` declaration above.
445
+
373
446
  ## License
374
447
 
375
448
  PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
@@ -38,6 +38,14 @@ export type AvAdapterRelationField<M extends AvAdapterModel> = {
38
38
  * one. Absent on the end that holds no reference (`User.posts`); never empty.
39
39
  */
40
40
  readonly referenceFields?: readonly [string, ...string[]];
41
+ /**
42
+ * The target's scalar fields that `referenceFields` name, pair by pair in
43
+ * the same order (`Post.author`: `referenceFields: ["authorId"]`,
44
+ * `references: ["id"]`, so `Post.authorId` holds `User.id`). Stated only
45
+ * beside `referenceFields`, with as many entries; absent on the end that
46
+ * holds no reference, where the other end (`inverse`) states the pairs.
47
+ */
48
+ readonly references?: readonly [string, ...string[]];
41
49
  };
42
50
  /** One field the adapter discovered: a scalar or a relation. */
43
51
  export type AvAdapterField<M extends AvAdapterModel> = AvAdapterScalarField<M> | AvAdapterRelationField<M>;
@@ -13,3 +13,12 @@ export type AvAdapterIdentifier<TFields extends Readonly<Record<string, AvAdapte
13
13
  AvAdapterScalarFieldKey<TFields>,
14
14
  ...AvAdapterScalarFieldKey<TFields>[]
15
15
  ];
16
+ /**
17
+ * The Resource's identifier as the Adapter states it first: in the Adapter's
18
+ * own order (an ORM adapter's is its schema's, so the primary key). The Core
19
+ * Contract sorts identifiers for its hash, so its first one is the
20
+ * alphabetically first (`email` before `id`) and says nothing about which is
21
+ * the primary key; the model keeps the order it declared. Empty for a Resource
22
+ * the model does not hold or that has no identifier.
23
+ */
24
+ export declare function adapterPrimaryIdentifier(model: AvAdapterModel, resource: string): readonly string[];
@@ -1 +1,6 @@
1
- export {};
1
+ export function adapterPrimaryIdentifier(model, resource) {
2
+ const resources = model.resources;
3
+ return ((Object.hasOwn(resources, resource)
4
+ ? resources[resource]?.identifiers?.[0]
5
+ : undefined) ?? []);
6
+ }
@@ -6,12 +6,18 @@ import type { AvAdapterModel } from "./adapter-model.js";
6
6
  * Operation identity and arguments remain provider-neutral; adapters receive
7
7
  * only validated semantic argument objects, never raw transport input.
8
8
  *
9
- * One shape exists here only: the flat unique selector of an `update.unique` or
10
- * a `delete.unique` may carry `AND`, a list of conditions in the general
11
- * `where` grammar the selected row must also meet. A row the selector names
12
- * that misses them is a miss (the same `null` as a row that is not there). An
13
- * adapter must honour it or refuse the operation, never drop it: dropping it
14
- * would write a row the conditions exclude.
9
+ * Two shapes exist here only, both an `AND`: a list of conditions in the
10
+ * general `where` grammar the addressed row must also meet.
11
+ *
12
+ * - The flat unique selector of an `update.unique` or a `delete.unique` may
13
+ * carry it. A row the selector names that misses them is a miss (the same
14
+ * `null` as a row that is not there).
15
+ * - A `cursor` may carry it, at the root of a `find.many` and in a to-many
16
+ * relation's arguments. A row the cursor names that misses them is a cursor
17
+ * that names no row: the same answer as a cursor on a row that is not there.
18
+ *
19
+ * An adapter must honour it or refuse the operation, never drop it: dropping it
20
+ * would write a row the conditions exclude, or position a page at one.
15
21
  */
16
22
  export type AvAdapterOperation<M extends AvAdapterModel = AvAdapterModel> = AvOperationIdentity & {
17
23
  readonly resource: Extract<keyof M["resources"], string>;
@@ -66,8 +66,8 @@ export const CONTRACT_COMPILER_DIAGNOSTIC_GUIDANCE = {
66
66
  },
67
67
  COMPILER_RESERVED_RESOURCE_NAME: {
68
68
  message: "Reserved Resource name.",
69
- cause: "A model name starts with $, which is reserved for the framework's own names (ctx.$sql, $ref).",
70
- fix: "Rename the model; the table it maps to can keep its name.",
69
+ cause: "A model name starts with $, which is reserved for the framework's own names (ctx.$sql, $ref), or, on a host that adds members beside the Resources, is one of those members' names (on Next.js: tx and transaction on avClient, appTx and transaction on avApp).",
70
+ fix: "Rename the model; the table it maps to can keep its name. A host member's name is also left free when the layer it belongs to hides the Resource.",
71
71
  },
72
72
  COMPILER_MISSING_IDENTIFIER: {
73
73
  message: "A unique operation has no identifier.",
@@ -81,15 +81,28 @@ function cloneAdapterField(field) {
81
81
  ...(Array.isArray(field.referenceFields) && field.referenceFields.length > 0
82
82
  ? { referenceFields: [...field.referenceFields] }
83
83
  : {}),
84
+ ...(Array.isArray(field.referenceFields) &&
85
+ Array.isArray(field.references) &&
86
+ field.references.length > 0 &&
87
+ field.references.length === field.referenceFields.length
88
+ ? { references: [...field.references] }
89
+ : {}),
84
90
  };
85
91
  }
86
- function pruneHiddenPairings(projection) {
92
+ function pruneHiddenPairings(projection, references) {
87
93
  const resources = projection.resources;
88
94
  for (const [name, resource] of Object.entries(resources)) {
89
95
  for (const field of Object.values(resource.fields)) {
90
96
  if (field.kind !== "relation") {
91
97
  continue;
92
98
  }
99
+ if (Array.isArray(field.references) &&
100
+ (references === "none" ||
101
+ !wholePairing(resource.fields, Object.hasOwn(resources, String(field.target))
102
+ ? resources[String(field.target)]?.fields
103
+ : undefined, field.referenceFields, field.references))) {
104
+ delete field.references;
105
+ }
93
106
  if (Array.isArray(field.referenceFields)) {
94
107
  const shown = field.referenceFields.filter((reference) => typeof reference === "string" &&
95
108
  Object.hasOwn(resource.fields, reference) &&
@@ -113,6 +126,16 @@ function pruneHiddenPairings(projection) {
113
126
  }
114
127
  }
115
128
  }
129
+ function wholePairing(fields, targetFields, referenceFields, references) {
130
+ const shownIn = (within, name) => typeof name === "string" &&
131
+ within !== undefined &&
132
+ Object.hasOwn(within, name) &&
133
+ within[name]?.kind === "scalar";
134
+ return (Array.isArray(referenceFields) &&
135
+ referenceFields.length === references.length &&
136
+ referenceFields.every((name) => shownIn(fields, name)) &&
137
+ references.every((name) => shownIn(targetFields, name)));
138
+ }
116
139
  function cloneOperations(operations) {
117
140
  return cloneValue(operations);
118
141
  }
@@ -1008,7 +1031,7 @@ function projectCore(model, config, diagnostics) {
1008
1031
  const validators = compileValidatorTable(baseline.resources, scope.pipelines);
1009
1032
  const projection = projectExposure({ layer: "root", style: exposureStyle(scope), diagnostics }, baseline, baseline, scope.exemptions);
1010
1033
  qualifyFilterModes(projection);
1011
- pruneHiddenPairings(projection);
1034
+ pruneHiddenPairings(projection, "shown");
1012
1035
  projectCreateRequirements(model, projection.resources);
1013
1036
  checkHiddenRequirements(model, "root", baseline.resources, baseline.resources, projection.resources, diagnostics);
1014
1037
  applyValidators(projection.resources, validators);
@@ -1059,7 +1082,7 @@ function projectDerivedScope(model, scopeName, core, parentView, config, diagnos
1059
1082
  ...(scopeName === "agent" ? { parent: config.agent.parent } : {}),
1060
1083
  }, baseline, parent, scope.exemptions);
1061
1084
  qualifyFilterModes(projection);
1062
- pruneHiddenPairings(projection);
1085
+ pruneHiddenPairings(projection, "none");
1063
1086
  projectCreateRequirements(model, projection.resources);
1064
1087
  checkHiddenRequirements(model, scopeName, baseline.resources, parent.resources, projection.resources, diagnostics);
1065
1088
  applyValidators(projection.resources, validators);
@@ -11,6 +11,8 @@ import type { AvConfigFieldDefinition } from "./framework-field-definition.js";
11
11
  import { type ResolvedPipelineTree } from "./pipeline-tree.resolver.js";
12
12
  /** The root's `policy`, its engine's passive lists read once. */
13
13
  export type ResolvedPolicyConfig = {
14
+ /** Where the engine was configured, for the startup diagnostics' paths. */
15
+ readonly at: "policy.engine" | "access.policy";
14
16
  readonly engine: AvPolicyEngine;
15
17
  readonly principal: NonNullable<AvConfig["policy"]>["principal"];
16
18
  /** The governed fields and relations, by Resource. */
@@ -158,11 +158,16 @@ function frozenPathLists(value) {
158
158
  Object.freeze(paths.map((path) => Object.freeze([...path]))),
159
159
  ])));
160
160
  }
161
- function resolvePolicy(policy) {
161
+ const accessPrincipal = ({ $state, }) => $state.me ?? null;
162
+ function resolvePolicy(configured, access) {
163
+ const policy = access === undefined
164
+ ? configured
165
+ : { engine: access.policy, principal: accessPrincipal };
162
166
  if (policy === undefined) {
163
167
  return undefined;
164
168
  }
165
169
  return Object.freeze({
170
+ at: access === undefined ? "policy.engine" : "access.policy",
166
171
  engine: policy.engine,
167
172
  principal: policy.principal,
168
173
  governed: frozenLists(policy.engine.governed),
@@ -188,7 +193,7 @@ export function resolveFrameworkConfig(config) {
188
193
  adapter: config.adapter,
189
194
  diagnostics: config.diagnostics,
190
195
  misuse: config.misuse,
191
- policy: resolvePolicy(config.policy),
196
+ policy: resolvePolicy(config.policy, config.access),
192
197
  definitions: definitionsOf(root.resources),
193
198
  trees: {
194
199
  root: root.resources,
@@ -7,6 +7,7 @@ const ROOT_ONLY_PROPERTIES = new Set([
7
7
  "adapter",
8
8
  "prebuilt",
9
9
  "policy",
10
+ "access",
10
11
  "diagnostics",
11
12
  "misuse",
12
13
  "application",
@@ -628,6 +629,12 @@ export function validateFrameworkConfig(value) {
628
629
  if (Object.hasOwn(value, "policy") && value.policy !== undefined) {
629
630
  validatePolicy(value.policy, diagnostics);
630
631
  }
632
+ if (Object.hasOwn(value, "access") && value.access !== undefined) {
633
+ validateAccess(value.access, diagnostics);
634
+ if (Object.hasOwn(value, "policy") && value.policy !== undefined) {
635
+ diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", "policy", "policy cannot be configured with access: access.policy is the policy engine, and ctx.$state.me the principal; remove policy"));
636
+ }
637
+ }
631
638
  if (Object.hasOwn(value, "diagnostics") &&
632
639
  value.diagnostics !== undefined &&
633
640
  typeof value.diagnostics !== "function") {
@@ -670,26 +677,43 @@ function validatePolicy(value, diagnostics) {
670
677
  diagnostics.push(diagnostic("CONFIG_REQUIRED", "policy.engine", "policy.engine is required"));
671
678
  return;
672
679
  }
673
- const engine = value.engine;
680
+ validatePolicyEngine(value.engine, "policy.engine", diagnostics);
681
+ }
682
+ function validateAccess(value, diagnostics) {
683
+ if (value === null || typeof value !== "object") {
684
+ diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", "access", "access must be an access provider, { policy, state }"));
685
+ return;
686
+ }
687
+ const provider = value;
688
+ if (typeof provider.state !== "function") {
689
+ diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", "access.state", "access.state must be a function"));
690
+ }
691
+ if (provider.policy === undefined) {
692
+ diagnostics.push(diagnostic("CONFIG_REQUIRED", "access.policy", "access.policy is required"));
693
+ return;
694
+ }
695
+ validatePolicyEngine(provider.policy, "access.policy", diagnostics);
696
+ }
697
+ function validatePolicyEngine(engine, at, diagnostics) {
674
698
  if (engine === null || typeof engine !== "object") {
675
- diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", "policy.engine", "policy.engine must be an object"));
699
+ diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", at, `${at} must be an object`));
676
700
  return;
677
701
  }
678
702
  const members = engine;
679
703
  for (const question of ["can", "scopeOf", "fieldsOf"]) {
680
704
  if (typeof members[question] !== "function") {
681
- diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", `policy.engine.${question}`, `policy.engine.${question} must be a function`));
705
+ diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", `${at}.${question}`, `${at}.${question} must be a function`));
682
706
  }
683
707
  }
684
- validatePolicyLists(members.governed, "governed", { all: "string lists", one: "a list of strings" }, isStringList, diagnostics);
685
- validatePolicyLists(members.paths, "paths", {
708
+ validatePolicyLists(members.governed, at, "governed", { all: "string lists", one: "a list of strings" }, isStringList, diagnostics);
709
+ validatePolicyLists(members.paths, at, "paths", {
686
710
  all: "path lists",
687
711
  one: "a list of paths, each a list of strings",
688
712
  }, (paths) => Array.isArray(paths) && paths.every(isStringList), diagnostics);
689
- validatePolicyLists(members.afterChecks, "afterChecks", { all: "action lists", one: "a list of actions" }, isStringList, diagnostics);
690
- validateAfterCheckActions(members.afterChecks, diagnostics);
713
+ validatePolicyLists(members.afterChecks, at, "afterChecks", { all: "action lists", one: "a list of actions" }, isStringList, diagnostics);
714
+ validateAfterCheckActions(members.afterChecks, at, diagnostics);
691
715
  }
692
- function validateAfterCheckActions(lists, diagnostics) {
716
+ function validateAfterCheckActions(lists, at, diagnostics) {
693
717
  if (!isPlainRecord(lists)) {
694
718
  return;
695
719
  }
@@ -699,7 +723,7 @@ function validateAfterCheckActions(lists, diagnostics) {
699
723
  }
700
724
  for (const action of actions) {
701
725
  if (action !== "update") {
702
- diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", `policy.engine.afterChecks.${resource}`, `policy.engine.afterChecks.${resource}: "${action}" is no action an after check runs on; only "update" is`));
726
+ diagnostics.push(diagnostic("CONFIG_INVALID_VALUE", `${at}.afterChecks.${resource}`, `${at}.afterChecks.${resource}: "${action}" is no action an after check runs on; only "update" is`));
703
727
  }
704
728
  }
705
729
  }
@@ -707,17 +731,17 @@ function validateAfterCheckActions(lists, diagnostics) {
707
731
  function isStringList(value) {
708
732
  return (Array.isArray(value) && value.every((name) => typeof name === "string"));
709
733
  }
710
- function validatePolicyLists(lists, list, entry, isEntry, diagnostics) {
734
+ function validatePolicyLists(lists, at, list, entry, isEntry, diagnostics) {
711
735
  if (lists === undefined) {
712
736
  return;
713
737
  }
714
738
  if (!isPlainRecord(lists)) {
715
- diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", `policy.engine.${list}`, `policy.engine.${list} must be an object of ${entry.all} when provided`));
739
+ diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", `${at}.${list}`, `${at}.${list} must be an object of ${entry.all} when provided`));
716
740
  return;
717
741
  }
718
742
  for (const [key, value] of Object.entries(lists)) {
719
743
  if (!isEntry(value)) {
720
- diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", `policy.engine.${list}.${key}`, `policy.engine.${list}.${key} must be ${entry.one}`));
744
+ diagnostics.push(diagnostic("CONFIG_INVALID_TYPE", `${at}.${list}.${key}`, `${at}.${list}.${key} must be ${entry.one}`));
721
745
  }
722
746
  }
723
747
  }
@@ -243,6 +243,7 @@ type RootSchema<M extends AvAdapterModel, Cfg> = LayerKeysSchema & {
243
243
  readonly engine: Leaf;
244
244
  readonly principal: Leaf;
245
245
  };
246
+ readonly access: Leaf;
246
247
  readonly diagnostics: Leaf;
247
248
  readonly misuse: Leaf;
248
249
  readonly resources: RootResourcesSchema<M>;
@@ -264,7 +265,16 @@ type Exact<S, V> = [S] extends [Leaf] ? unknown : V extends readonly unknown[] ?
264
265
  * rc.0 key, a `definition` in an overlay, a stage a relation field does not take,
265
266
  * or a name the model and the root do not have.
266
267
  */
267
- export type AvConfigExact<M extends AvAdapterModel, Cfg> = Exact<RootSchema<M, Cfg>, Cfg> & ExposedWithExemptions<Cfg, "application"> & ExposedWithExemptions<Cfg, "client"> & ExposedWithExemptions<Cfg, "agent"> & ForceNeedsTransactions<M, Cfg>;
268
+ export type AvConfigExact<M extends AvAdapterModel, Cfg> = Exact<RootSchema<M, Cfg>, Cfg> & ExposedWithExemptions<Cfg, "application"> & ExposedWithExemptions<Cfg, "client"> & ExposedWithExemptions<Cfg, "agent"> & ForceNeedsTransactions<M, Cfg> & AccessWithoutPolicy<Cfg>;
269
+ /** `policy` refused when the configuration also states `access`. */
270
+ type AccessWithoutPolicy<Cfg> = Cfg extends {
271
+ readonly access: object;
272
+ readonly policy: object;
273
+ } ? {
274
+ readonly policy: readonly [
275
+ "Error: `access` brings the policy engine; remove `policy`"
276
+ ];
277
+ } : unknown;
268
278
  /** A layer's own literal `transactionsStyle`, `undefined` when it states none. */
269
279
  type StyleOf<Layer> = Layer extends {
270
280
  readonly transactionsStyle: infer Style;