@aventara/core 1.0.0-rc.12 → 1.0.0-rc.14
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 +152 -1
- package/ERRORS.md +7 -6
- package/README.md +3 -2
- package/dist/avz/avz-check.factory.d.ts +2 -0
- package/dist/avz/avz-check.factory.js +12 -0
- package/dist/avz/avz.d.ts +6 -0
- package/dist/avz/avz.js +1 -0
- package/dist/compiler/contract-compiler-diagnostic.d.ts +10 -0
- package/dist/compiler/contract-compiler-diagnostic.guidance.js +5 -0
- package/dist/compiler/contract.compiler.js +102 -12
- package/dist/compiler/create-requirements.projector.d.ts +45 -3
- package/dist/compiler/create-requirements.projector.js +239 -28
- package/dist/compiler/managed-elsewhere.projector.d.ts +3 -0
- package/dist/compiler/managed-elsewhere.projector.js +1 -1
- package/dist/config/computed-fields.d.ts +8 -0
- package/dist/config/computed-fields.js +1 -1
- package/dist/config/config-diagnostic.guidance.js +2 -2
- package/dist/config/config.resolver.d.ts +2 -2
- package/dist/config/config.validator.js +15 -6
- package/dist/config/exact-framework-config.d.ts +1 -0
- package/dist/config/field-pipeline-config.d.ts +2 -2
- package/dist/config/forced-operation.validator.js +3 -2
- package/dist/config/framework-config.d.ts +8 -3
- package/dist/config/framework-exemptions-config.d.ts +13 -8
- package/dist/config/framework-resources-config.d.ts +11 -3
- package/dist/contracts/client-contract-structure.validator.d.ts +4 -1
- package/dist/contracts/client-contract-structure.validator.js +3 -2
- package/dist/contracts/contract-limits.d.ts +8 -1
- package/dist/contracts/contract-limits.js +1 -0
- package/dist/contracts/contract.d.ts +7 -3
- package/dist/contracts/transaction-capability.d.ts +8 -0
- package/dist/contracts/transaction-capability.js +7 -0
- package/dist/framework/access-facade.js +14 -6
- package/dist/framework/contract-view.d.ts +91 -9
- package/dist/framework/operation-facade.d.ts +8 -3
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/operations/validation-code.guidance.js +1 -1
- package/dist/policy/access-provider.interface.d.ts +7 -2
- package/dist/policy/access-question.runtime.js +2 -1
- package/dist/prebuilt/core-version.generated.d.ts +1 -1
- package/dist/prebuilt/core-version.generated.js +1 -1
- package/dist/protocol/absent-route.responder.d.ts +2 -2
- package/dist/protocol/absent-route.responder.js +2 -1
- package/dist/protocol/av-protocol.d.ts +1 -0
- package/dist/protocol/av-protocol.js +1 -0
- package/dist/protocol/protocol-surface.js +2 -1
- package/dist/protocol/protocol.handler.js +3 -1
- package/dist/runtime/context-executor.runtime.js +2 -1
- package/dist/runtime/create-requirement.d.ts +5 -1
- package/dist/runtime/create-requirement.js +10 -2
- package/dist/runtime/field-check.evaluator.js +39 -2
- package/dist/runtime/field-validator.runtime.d.ts +4 -3
- package/dist/runtime/field-validator.runtime.js +29 -0
- package/dist/runtime/mutation-planner.js +26 -9
- package/dist/runtime/operation-code.guidance.js +3 -3
- package/dist/runtime/operation-runtime.d.ts +10 -9
- package/dist/runtime/operation-runtime.js +20 -18
- package/dist/runtime/pipeline.d.ts +1 -15
- package/dist/runtime/state-refusal.mapper.d.ts +16 -0
- package/dist/runtime/state-refusal.mapper.js +40 -0
- package/dist/transactions/operation-fingerprint.d.ts +15 -2
- package/dist/transactions/transaction-plan-validator.js +31 -1
- package/dist/transactions/transaction-runtime.js +1 -1
- package/package.json +1 -1
- package/src/runtime/field-check.evaluator.ts +46 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,156 @@
|
|
|
1
1
|
# @aventara/core
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1c5eaa3: The agent skill's stage order in `reference/stages.md` now lists every stage `SKILL.md` and the pipelines docs list: policy admission before the guards, the policy write check after the re-check, and output validation before the response. It says where a nested write's targets run their guards, field pipes, validators and write compute (after the top-level operation's stages of the same kind), lists the check of a value compute set in a field the caller's layer hides (`A3000`), and gives the `forceTransaction` order in full: framework validation and policy admission before `BEGIN`, the policy write check and output validation inside the transaction.
|
|
8
|
+
|
|
9
|
+
The error catalog's `A4000` and `A4001` entries now state the whole rule for a `state` that rejects with an authentication library's refusal: the envelope it answers, that nothing of the library's error is sent and nothing goes to `diagnostics`, which shapes are not a refusal (`500 A3000`), and every route, MCP tool call and in-process call it applies to.
|
|
10
|
+
|
|
11
|
+
## 1.0.0-rc.13
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- f3b291f: `AvProtocol.fingerprint({ resource, family, variant, args })` on `@aventara/core/protocol` computes a transaction
|
|
16
|
+
plan step's `fingerprint`, so a client written by hand can send a raw `POST /_transactions` plan: the RFC 8785
|
|
17
|
+
canonical JSON of the four members over the step's wire-form arguments, hashed with SHA-256, the first 16 bytes in
|
|
18
|
+
base64url without padding, after `fp1:`. It is the function the server checks a step with, and rejects arguments
|
|
19
|
+
that are not JSON (a lone surrogate, a non-finite number, a `bigint`, a non-plain object).
|
|
20
|
+
- f3b291f: A new limit, `maxTransactionWrites` (default `20`), counts the write steps of one transaction plan: each `create`,
|
|
21
|
+
`update`, `upsert` and `delete` step, and each `appTx.$sql` step, since a statement cannot be classed as a read. A
|
|
22
|
+
plan with more is refused before anything runs (`422 A2009`, one `V1014` at `["operations"]`). It is set like every
|
|
23
|
+
other limit (`limits` at the root, overridden per limit by a layer) and is part of each Contract's `limits`, so every
|
|
24
|
+
Contract hash moves with this release. `maxTransactionOperations` still counts every step, reads and `$sql` steps
|
|
25
|
+
included. The two limits are separate: `maxTransactionWrites` defaults to `20` whatever `maxTransactionOperations` is
|
|
26
|
+
set to, so raising only the step limit still caps a plan at 20 writes; raise both. The limit counts steps, not rows: a
|
|
27
|
+
`create` step with nested `$create`s is one write. `POST /_all` is bounded by `maxAll`, not by either plan limit, and its writes do not roll back together.
|
|
28
|
+
The MCP `transaction` tool's description states the write limit when it is lower than the step limit.
|
|
29
|
+
- f3b291f: `transactionsStyle` takes a third value, `"server"`: the server still opens transactions, and callers send no plans.
|
|
30
|
+
A forced call (`forceTransaction`), a write the policy checks after it runs and `ctx.$transaction` run in a
|
|
31
|
+
transaction as under `"interactive"`, while the layer has no `POST /_transactions` route, a plan sent to it answers as
|
|
32
|
+
that absent route (`404 A2002`, one `V1006` at `["operations"]`), `framework.transaction` refuses a plan of that
|
|
33
|
+
origin the same way, the generated client has no `tx` or `transaction`, and an MCP agent layer under it has no
|
|
34
|
+
`transaction` tool. `client: { transactionsStyle: "server" }` keeps forced writes atomic without letting remote
|
|
35
|
+
callers send multi-step plans. `forceTransaction` under `"none"` is still a startup error; under `"server"` it is
|
|
36
|
+
allowed. Like `"interactive"`, `"server"` needs an adapter with transactions (`COMPILER_UNAVAILABLE_CAPABILITY`
|
|
37
|
+
otherwise). The Contract's `transactions` carries the value, so a layer that states it gets a new Contract hash.
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- 6c3e25c: `AvZ.oneOf(["USD", "EUR"])` is a string field validator for a closed set of values. It builds an anchored pattern of
|
|
42
|
+
the values, each one's pattern syntax escaped (`"C++"` and `"a.b"` match only themselves), so the Contract gains no
|
|
43
|
+
new check: the generated client runs it like any `AvZ.regex`, and the field's type stays `string`. A refused value
|
|
44
|
+
answers `V1003` with the message `Value must be one of "USD", "EUR".` rather than the pattern. An empty list, or a
|
|
45
|
+
value that is not a string, is refused when the check is built, and the types refuse both. When the types should name
|
|
46
|
+
the set as a union, use a Prisma `enum`: Aventara carries it into the Contract and the generated client as a literal
|
|
47
|
+
union.
|
|
48
|
+
- 2aaa27d: A stage's `ctx` create no longer has to write a field that the root's or the `application` overlay's `compute.create`
|
|
49
|
+
fills. A `ctx` call runs that compute, and the generated types already state which fields it fills (the
|
|
50
|
+
`computed-fields.generated.ts` file `aventara-build` writes), so `ctx.Payment.create.one({ data })` type-checks without
|
|
51
|
+
the computed `employeeId`. A field that only the `client` or `agent` overlay computes is still required, because that
|
|
52
|
+
compute does not run for a `ctx` call. A model whose types state no computed fields keeps every requirement. The agent
|
|
53
|
+
skill says so.
|
|
54
|
+
- 2aaa27d: Exemptions take one more value, `"read"`, on a field and on a Resource's `fields`. It names every read capability of
|
|
55
|
+
the field that the layer above offers: a scalar's `select`, `filter`, `order` and `orderNulls`, a relation's `select`,
|
|
56
|
+
`include`, `filter`, `order`, `pagination` and `reducers`. On `fields` it applies to every field of the Resource.
|
|
57
|
+
|
|
58
|
+
- Under `exposed: false` it exposes reading and no write, so a read-only Resource no longer needs every field listed:
|
|
59
|
+
`Category: { operations: { find: true }, fields: "read" }`.
|
|
60
|
+
- Under `exposed: true` it hides reading and keeps the writes, which is the write-only field:
|
|
61
|
+
`passwordHash: "read"`.
|
|
62
|
+
|
|
63
|
+
It compiles to the same Contracts as the capability lists it stands for, hash included. Like every exemption it only
|
|
64
|
+
narrows: it never brings back a capability the layer above hid, and naming a field the layer above hid is refused as
|
|
65
|
+
before. `fields: "read"` names relations too, so a relation into a Resource the layer leaves out still fails with
|
|
66
|
+
`COMPILER_DANGLING_RELATION`. The agent skill documents it.
|
|
67
|
+
|
|
68
|
+
Read-only now means no nested write either. A relation does not write rows of a Resource its layer cannot write: one
|
|
69
|
+
that creates nothing (no create or upsert operation, and no field writable on create) takes no `$create`,
|
|
70
|
+
`$connectOrCreate` or `$upsert`; one that updates nothing (no update or upsert operation, and no field writable on
|
|
71
|
+
update) takes no `$update` or `$upsert`; one with no delete operation takes no `$delete`. The fields count with the
|
|
72
|
+
operations for creates and updates, so a Resource whose create operation is hidden but whose fields stay writable on
|
|
73
|
+
create is still created through its parent. Relation fields count too, even link-only (`$connect`): if a Resource must
|
|
74
|
+
create nothing, hide its relations' `create` as well. This holds however the Resource was made read-only, `"read"` or listed
|
|
75
|
+
capabilities, and also when it has no required field, where a nested `$create: {}` was accepted before. `$connect`,
|
|
76
|
+
`$disconnect` and `$set` stay.
|
|
77
|
+
|
|
78
|
+
What happens follows the layer's `exposed`. Under `exposed: false` the relation's directives are the ones the layer
|
|
79
|
+
grants (by `true` on the Resource or the field, by the relation's `create` or `update`, or by naming the directive), so
|
|
80
|
+
such a grant is refused at startup with `COMPILER_READ_ONLY_NESTED_WRITE`: let the relation only link,
|
|
81
|
+
`author: { select: true, create: ["$connect"] }`. Under `exposed: true` the relation inherits its directives, so they
|
|
82
|
+
are dropped from it and startup succeeds; a call that sends one gets `V1006`, or "Relation … is not writable during
|
|
83
|
+
update." when the relation's update had nothing else. Dropping one can leave another Resource with nothing writable, and
|
|
84
|
+
the nested writes into it go the same way. A layer with `exposed: false` below that names a dropped directive, or grants
|
|
85
|
+
a member that was removed, is refused with `COMPILER_READ_ONLY_NESTED_WRITE`.
|
|
86
|
+
|
|
87
|
+
A configuration under `exposed: true` that hides a Resource's deletes, or every way to create or update it, therefore
|
|
88
|
+
serves Contracts with fewer directives on the relations into it, and its ClientContract hash changes: regenerate the
|
|
89
|
+
frontend client. Such a configuration was refused at startup before when a hidden field was required, and otherwise
|
|
90
|
+
accepted nested creates.
|
|
91
|
+
|
|
92
|
+
- e4e8abd: The same rule now holds for in-process calls. When `state` rejects with an object whose `status` or `statusCode` is
|
|
93
|
+
the number 401 or 403, an `avClient` call in a Next.js Server Component or Server Action rejects with an `AvAuthError`
|
|
94
|
+
of code `A4000` or `A4001`, and so does `framework.serverClient.$access.can` given `headers`. Its message is core's own
|
|
95
|
+
(`"Unauthenticated."` or `"Forbidden."`) and nothing of the rejection reaches the caller. Before, the call rejected
|
|
96
|
+
with the library's own error. A thrown `AvError` and any other rejection still reject the call unchanged.
|
|
97
|
+
|
|
98
|
+
An object whose `status` and `statusCode` are both numbers that disagree, such as `status: 500` beside
|
|
99
|
+
`statusCode: 401`, is no longer read as a refusal: a route answers it `500 A3000`. A number beside a non-number, such
|
|
100
|
+
as Better Auth's `status: "UNAUTHORIZED"` beside `statusCode: 401`, is still read by the number.
|
|
101
|
+
|
|
102
|
+
- e662048: A request whose `state` (the host's `state` option, or the `access` provider's `state`) rejects with an object whose
|
|
103
|
+
`status` or `statusCode` is the number 401 or 403 is now answered `401 A4000` or `403 A4001`, not `500 A3000`. This is
|
|
104
|
+
how authentication libraries refuse a bad credential: Better Auth's `APIError` for a wrong API key carries
|
|
105
|
+
`statusCode: 401`. The answer is core's own envelope (`"Unauthenticated."` or `"Forbidden."`); nothing of the
|
|
106
|
+
rejection, neither its message nor its code, reaches the caller, and it is not reported to `diagnostics`, as a thrown
|
|
107
|
+
`AvError("A4000", …)` is not. It applies on every protocol route that seeds state: operations, cached reads,
|
|
108
|
+
`/_transactions`, `/_all`, `/_access/can`, and MCP tool calls. A thrown `AvError` is still its own outcome, and any
|
|
109
|
+
other rejection, a 401 beside a 403 included, is still `500 A3000`.
|
|
110
|
+
- 6c3e25c: Field validators now judge every value the caller sent before write compute, and each value once. A value the caller
|
|
111
|
+
sent is judged after the guards, the pipes and its field pipes, before any compute stage runs, and that includes a
|
|
112
|
+
field that has a `compute.create` or `compute.update` stage of its own. After compute, only a value compute set or
|
|
113
|
+
changed is judged: a field the caller left out that compute filled, or a caller's value compute replaced. A compute
|
|
114
|
+
stage that returns `undefined` or the caller's own value changes nothing, so that value is not judged again. The order
|
|
115
|
+
is now guards → pipes → validators of the caller's values → write compute → validators of what compute set or changed
|
|
116
|
+
→ hooks.
|
|
117
|
+
|
|
118
|
+
A compute stage never sees a value its own validators refuse. A caller value that also breaks a compute stage (a
|
|
119
|
+
quantity that overflows a total, an unknown currency code handed to `Intl.NumberFormat`) answered `500 A3000` before
|
|
120
|
+
this release; it now answers `422 A2004` with the issue at the field, and no compute stage runs. A bad caller value is
|
|
121
|
+
refused even when compute would have replaced it: before this release that value was dropped silently. Codes and
|
|
122
|
+
issue shapes are unchanged. One answer no longer mixes the two kinds: when a caller value is refused, the computed
|
|
123
|
+
values are not judged yet. In a transaction plan or a forced call, every literal value is judged before the
|
|
124
|
+
transaction opens; a resolved `$ref` value and a value a Resource transaction pipe changed are judged at their step,
|
|
125
|
+
before write compute. The agent skill's stage order says so.
|
|
126
|
+
|
|
127
|
+
- 6c3e25c: A field the configuration adds (`lifecycle: ["VIRTUAL"]`) with `nullable: false` is now a create requirement, like the
|
|
128
|
+
adapter's own non-null columns: in each layer that lets a caller write it at create, and where no `compute.create` (the
|
|
129
|
+
root's or that layer's) fills it, it joins the Contract's `createRequirements.required`. A create without it answers
|
|
130
|
+
`422 A2004` with `V1000` at the field, before any guard runs, and `framework.application`, `framework.client` and the
|
|
131
|
+
generated client type it as required. A layer that hides the field creates without it, and an update is unchanged. A
|
|
132
|
+
nullable added field stays optional. A `compute.create` stage that fills such a field and returns `undefined` answers
|
|
133
|
+
`500 A3000` with an `AvComputedValueError` in the `diagnostics` sink, as it does for a required database column, and
|
|
134
|
+
nothing is written.
|
|
135
|
+
|
|
136
|
+
This changes the Client Contract, and so its hash, for every configuration with such a field: regenerate the frontend
|
|
137
|
+
client. A configuration that sent a `nullable: false` virtual input only sometimes must now send it on every create, or
|
|
138
|
+
state it `nullable: true`.
|
|
139
|
+
|
|
140
|
+
- a708188: A validator on a field whose type takes no check now names why in the type error, in place of `… is not assignable to
|
|
141
|
+
type 'readonly never[]'`. `AvZ.oneOf([...])` on an enum field reads `Type 'AvZCheck<"string">' is not assignable to
|
|
142
|
+
type '"Error: an enum field takes no check: its enum already is the closed set of values it admits"'`. An enum list
|
|
143
|
+
says it takes only a list length (`AvZ.minItems`, `AvZ.maxItems`), and a boolean field or list says the same in its
|
|
144
|
+
own words.
|
|
145
|
+
|
|
146
|
+
- a9260d2: The agent skills cover what an agent needs without opening `node_modules`:
|
|
147
|
+
|
|
148
|
+
- The frontend skill (`avclient skill`) now explains what a call returns (`Date`, `bigint`, `AvDecimal`, `Uint8Array`), which error class each code throws (`AvTransportError` included), `error.cause.issues` for form errors, transactions with `$ref`, and a new `reference/client-setup.md`: `framework.client.ts` (`defineClientConfig`, `env`, `AVENTARA_API_URL`, `generateAt`), regenerating after the server changed, `avclient generate --yes` in CI, and the stale-client error (`A2005`, `AvContractMismatchError`). It also links the docs for agents.
|
|
149
|
+
- The server skill (`aventara skill`) adds `reference/avz.md` (every `AvZ` check and Json shape builder, `AvZ.infer`) and covers the `policy` and `access` keys, MCP token claims in `ctx.$state.mcp.claims`, the Next.js `state` example, `avDehydrate`/`avHydrate` and server transactions in Next.js, testing with `createFakeAdapter` (what `execute` receives and returns), switching the database or driver, `ctx.$sql`, `ctx.$transaction` and `ctx.$transport`, the compute stage types, and the `AVENTARA_PREBUILT_STALE` fallback.
|
|
150
|
+
- With `@aventara/access` installed, the skill now says to write Access's own skill with `avaccess skill` (`npx avaccess skill`, or `pnpm exec avaccess skill`).
|
|
151
|
+
- Corrected: a write-only field is written `"read"` everywhere; a field pipe or compute stage that returns `undefined` keeps the value; a read compute may be a plain function and gets `field`, `value`, `record` and `ctx`.
|
|
152
|
+
- `CONFIG_FORBIDDEN_IN_OVERLAY` in the error catalog now names `managedElsewhere` as well as a field `definition`.
|
|
153
|
+
|
|
3
154
|
## 1.0.0-rc.12
|
|
4
155
|
|
|
5
156
|
### Patch Changes
|
|
@@ -12,7 +163,7 @@
|
|
|
12
163
|
compound identifier goes with any one of its fields, and the generated client and the server façades no longer offer
|
|
13
164
|
it. A layer that keeps a unique variant with no filterable identifier left fails at startup with
|
|
14
165
|
`COMPILER_MISSING_IDENTIFIER`, as hiding the identifier already did: under `exposed: false`, pick `id: ["select",
|
|
15
|
-
|
|
166
|
+
"filter"]`, not `id: ["select"]`, to keep `find.unique`. The agent skill's Pick example says so.
|
|
16
167
|
|
|
17
168
|
## 1.0.0-rc.11
|
|
18
169
|
|
package/ERRORS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Aventara error codes
|
|
2
2
|
|
|
3
|
-
Every code `@aventara/core` 1.0.0-rc.
|
|
3
|
+
Every code `@aventara/core` 1.0.0-rc.14 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
|
|
|
@@ -31,7 +31,7 @@ The caller can fix these. The generated client and server-side calls throw the c
|
|
|
31
31
|
| [`A2001`](https://aventara.razor34.dev/framework/docs/error-codes#a2001) | Unknown Resource. | The Resource is not in the caller's contract: misspelt, hidden by exposure, or added to the schema after aventara:prepare last ran. cause.message is "The requested Resource is not available." and the issue is V1005 at `["resource"]`. | Use the model's exact name. After a schema change run the adapter's generate step (`npm run aventara:prepare` in a scaffolded project), restart, and regenerate the client. A Resource hidden on purpose is reached from server code (framework.application). |
|
|
32
32
|
| [`A2002`](https://aventara.razor34.dev/framework/docs/error-codes#a2002) | Unknown operation. | The operation is not available on that Resource: a typo, removed by exposure, an operation the adapter does not offer (such as delete.many), or a ctx.$sql, appTx.$sql or ctx.$transaction the adapter or the layer cannot run. The issue is V1006 at `["family"]` or `["variant"]`. | Call an operation the contract lists (`GET <api>/_contract`), regenerate a stale client, and check the layer's exemptions and transactionsStyle. |
|
|
33
33
|
| [`A2003`](https://aventara.razor34.dev/framework/docs/error-codes#a2003) | Not found. | A unique operation (find.unique, update.unique, delete.unique) matched no row, or a $connect names a record that does not exist. The operation fails, so no after hook runs. Under a policy, a row outside the caller's read scope is a miss too, and so is a single-row update or delete of a row the caller cannot read, so the answer does not reveal that the row exists. A row the caller can read but not change answers A4002. | Catch AvNotFoundError, or use a first-style operation, which answers A1001 with null. Check the $connect identifier. |
|
|
34
|
-
| [`A2004`](https://aventara.razor34.dev/framework/docs/error-codes#a2004) | Validation failed. | The arguments break a contract rule or a field validator. cause.issues lists each problem by V-code and path; a validator's issue also has check and params. The validators judge the
|
|
34
|
+
| [`A2004`](https://aventara.razor34.dev/framework/docs/error-codes#a2004) | Validation failed. | The arguments break a contract rule or a field validator. cause.issues lists each problem by V-code and path; a validator's issue also has check and params. The validators judge every value the caller sent after its pipes and before write compute, a field with a compute stage included, so a bad value is refused even when compute would replace it; then a value write compute set or changed, so a computed value they refuse is here too, at its field. A required create field left out is here (V1000). So is a value the database itself refused: too long for its column or its index, or out of its range (V1004), or holding a character it cannot store or a pattern it cannot read (V1001), with the issue at `["arguments"]`, or at the value where the adapter can place it (a bigint its driver cannot bind), and its message saying what the database refused. So is a record written through a relation that writes that relation, or a field holding its reference, back (V1012, at that field), where the adapter admits no such write. | Fix the argument each issue's path points at, as its V-code says. A field the server must fill takes a compute.create stage. For a value the database refused, send one its column holds. For a nested write that writes its relation back, leave that field out: the parent supplies it. |
|
|
35
35
|
| [`A2005`](https://aventara.razor34.dev/framework/docs/error-codes#a2005) | Contract mismatch. | The caller's Aventara-Contract-Hash is not the server's current one: the schema, the configuration, the limits or the exposure changed, or the client targets another deployment. | Regenerate the client against the running server: `npm run avclient:generate`. |
|
|
36
36
|
| [`A2006`](https://aventara.razor34.dev/framework/docs/error-codes#a2006) | Protocol mismatch. | Aventara-Protocol-Version is not 1. | Send `Aventara-Protocol-Version: 1`, or upgrade the client. |
|
|
37
37
|
| [`A2007`](https://aventara.razor34.dev/framework/docs/error-codes#a2007) | Invalid transaction reference. | A transaction $ref or fingerprint is invalid: the issues are V1010, V1011, V1016, V1017 or V1018, and cause.operation is the step. A $ref with no fingerprint is A2004 / V1000. | Let the generated client or framework.appTx build plans; check the $ref path and that the referenced step comes earlier. |
|
|
@@ -61,8 +61,8 @@ Produced by your guards; the framework never authenticates by itself.
|
|
|
61
61
|
|
|
62
62
|
| Code | Meaning | Cause | Fix |
|
|
63
63
|
|---|---|---|---|
|
|
64
|
-
| [`A4000`](https://aventara.razor34.dev/framework/docs/error-codes#a4000) | Unauthenticated. | A guard threw new AvError("A4000", ...): authentication is required or failed. The framework never authenticates by itself. | Send the credentials your guards expect, such as the Authorization header. |
|
|
65
|
-
| [`A4001`](https://aventara.razor34.dev/framework/docs/error-codes#a4001) | Forbidden. | A guard returned false ("Operation denied by a framework guard."), a field write guard or a nested write's target guard included,
|
|
64
|
+
| [`A4000`](https://aventara.razor34.dev/framework/docs/error-codes#a4000) | Unauthenticated. | A guard threw new AvError("A4000", ...), or the host's or access provider's state rejected with an object whose status or statusCode is the number 401 (an authentication library's refusal, such as Better Auth's statusCode 401): authentication is required or failed. Such a refusal answers {"data":null,"code":"A4000","cause":{"message":"Unauthenticated."}}, nothing of the library's error, and is not sent to diagnostics. A string status ("401"), another number, or a status and statusCode that are two different numbers is no refusal and answers 500 A3000; a number beside a string is read by the number. The rule holds on every protocol route (operations, cached reads, /_transactions, /_all, /_access/can), on MCP tool calls and in Next.js; in process (avClient in a Server Component or Server Action, serverClient.$access.can given headers) the call rejects with an AvAuthError. The framework never authenticates by itself. | Send the credentials your guards expect, such as the Authorization header. |
|
|
65
|
+
| [`A4001`](https://aventara.razor34.dev/framework/docs/error-codes#a4001) | Forbidden. | A guard returned false ("Operation denied by a framework guard."), a field write guard or a nested write's target guard included, a stage threw A4001, or the host's or access provider's state rejected with an object whose status or statusCode is the number 403, read by the same rule as A4000's 401 (answered with this message only, not sent to diagnostics). | Call as a caller the guard admits, or stop sending the guarded field. To answer another code, throw an AvError from the guard. |
|
|
66
66
|
| [`A4002`](https://aventara.razor34.dev/framework/docs/error-codes#a4002) | Policy denied. | The configuration's policy refused the request: its engine does not allow the operation, a field the request names, writes or returns, a row it reaches, a row it creates, or the row a change leaves (moved out of what the caller reads, or failing its grant's after condition, which rolls the write back); on a database without interactive transactions, a write the policy can only check after it runs; or a stage threw new AvError("A4002", ...). | Read cause.message, which names what the policy refused. Call as a principal the policy allows, leave the refused field or relation out (a governed one is named only where every row grants it), or change only rows the caller may change and still read afterwards. |
|
|
67
67
|
|
|
68
68
|
## Validation issues (V1xxx)
|
|
@@ -85,7 +85,7 @@ Each entry of `cause.issues[]` under `A2001`, `A2002`, `A2004`, `A2007` or `A200
|
|
|
85
85
|
| [`V1011`](https://aventara.razor34.dev/framework/docs/error-codes#v1011) | Transaction reference type mismatch. | The referenced value's type does not fit its target, a nullable source where only a non-null value is accepted included. | Reference a field of the target's type, non-null where the target needs it. |
|
|
86
86
|
| [`V1012`](https://aventara.razor34.dev/framework/docs/error-codes#v1012) | Invalid relation action/cardinality. | A nested write that does not fit the relation (a list where one record is expected), a reference field set together with its own relation, one record mixing reference fields and relation writes where the adapter cannot run both, or a record written through a relation that writes that relation (or a field holding its reference) back, where the adapter admits no such write. Also a to-one $update or $upsert beside $connect, $connectOrCreate, $create, $set or $disconnect on the same relation: the row it would update is the newly linked one. | Write either the relation or its reference field, in the relation's cardinality. Relink a to-one relation in one write and update the linked row in another. |
|
|
87
87
|
| [`V1013`](https://aventara.razor34.dev/framework/docs/error-codes#v1013) | Maximum nesting depth exceeded. | Nested projections, filters or writes deeper than maxNestingDepth. | Nest less, or raise limits.maxNestingDepth. |
|
|
88
|
-
| [`V1014`](https://aventara.razor34.dev/framework/docs/error-codes#v1014) | Configured limit exceeded. | A limit above maxListLimit, or more steps than
|
|
88
|
+
| [`V1014`](https://aventara.razor34.dev/framework/docs/error-codes#v1014) | Configured limit exceeded. | A limit above maxListLimit, more steps than maxTransactionOperations, or more write steps than maxTransactionWrites. | Paginate or split the plan, or raise the limit. |
|
|
89
89
|
| [`V1015`](https://aventara.razor34.dev/framework/docs/error-codes#v1015) | Boolean/filter node limit exceeded. | A where tree with more nodes than maxBooleanNodes. | Simplify the filter (in instead of many OR clauses), or raise limits.maxBooleanNodes. |
|
|
90
90
|
| [`V1016`](https://aventara.razor34.dev/framework/docs/error-codes#v1016) | Invalid transaction reference index. | A $ref to the referencing step itself or a later one, or to an index no step has. | Reference an earlier step only. |
|
|
91
91
|
| [`V1017`](https://aventara.razor34.dev/framework/docs/error-codes#v1017) | Transaction fingerprint mismatch. | A hand-written plan whose fingerprint does not match its step. | Let the generated client, framework.appTx or framework.clientTx build the plan. |
|
|
@@ -105,7 +105,7 @@ Each entry of `cause.issues[]` under `A2001`, `A2002`, `A2004`, `A2007` or `A200
|
|
|
105
105
|
| [`CONFIG_INVALID_TYPE`](https://aventara.razor34.dev/framework/docs/error-codes#config_invalid_type) | A configuration key has the wrong type. | A value of the wrong type, or a key set to an explicit undefined. | Give the value the type the reference states; leave a key out rather than setting it to undefined. |
|
|
106
106
|
| [`CONFIG_INVALID_VALUE`](https://aventara.razor34.dev/framework/docs/error-codes#config_invalid_value) | A configuration value is not allowed. | A malformed api or mcp path, a negative limit, an unknown transactionsStyle, a format that is not a named zod format, forceTransaction under an effective transactionsStyle "none", an after check on any action but update, or a policy engine's governed, paths or afterChecks naming a Resource, field or path the Core Contract does not have, or its unheld naming a Resource the data source's model does not have (the message names the Resource or the rule). | Use a value the message allows. |
|
|
107
107
|
| [`CONFIG_MOVED_PROPERTY`](https://aventara.razor34.dev/framework/docs/error-codes#config_moved_property) | A configuration key moved in rc.1. | Configuration written for 1.0.0-rc.0: restrictions, entrypoint, fields, behaviors, transactions or pipelines.before/after. | Move it where the message says: restrictions to exposed + exemptions, entrypoint to api, fields to `resources.<R>.fields.<f>.definition`, transactions to transactionsStyle, before/after to pipelines.hooks. |
|
|
108
|
-
| [`CONFIG_FORBIDDEN_IN_OVERLAY`](https://aventara.razor34.dev/framework/docs/error-codes#config_forbidden_in_overlay) | A root-only key is written in an overlay. | A field definition in application, client or agent. |
|
|
108
|
+
| [`CONFIG_FORBIDDEN_IN_OVERLAY`](https://aventara.razor34.dev/framework/docs/error-codes#config_forbidden_in_overlay) | A root-only key is written in an overlay. | A field definition, or a Resource's managedElsewhere, in application, client or agent. | State it once at the root (`resources.<R>.fields.<f>.definition`, `resources.<R>.managedElsewhere`); an overlay only chooses visibility. |
|
|
109
109
|
|
|
110
110
|
## Startup: discovery
|
|
111
111
|
|
|
@@ -137,6 +137,7 @@ Each entry of `cause.issues[]` under `A2001`, `A2002`, `A2004`, `A2007` or `A200
|
|
|
137
137
|
| [`COMPILER_RESERVED_RESOURCE_NAME`](https://aventara.razor34.dev/framework/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 is the name of a member beside the Resources: all on serverClient (and so on Next.js's avClient), and on a host that adds its own, one of those (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.razor34.dev/framework/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 it may filter on: a Pick that did not pick one with its filter, or an Omit that hid every one or its filter. | Expose an identifier field with its filter (id, or every field of one unique), or exempt the unique variants. |
|
|
139
139
|
| [`COMPILER_HIDDEN_REQUIRED_FIELD`](https://aventara.razor34.dev/framework/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
|
+
| [`COMPILER_READ_ONLY_NESTED_WRITE`](https://aventara.razor34.dev/framework/docs/error-codes#compiler_read_only_nested_write) | A relation writes rows of a read-only Resource. | A layer with exposed: false grants a relation a write into a Resource it cannot write: $create, $connectOrCreate or $upsert into a Resource with no create or upsert operation and no field writable on create, $update or $upsert into one with no update or upsert operation and no field writable on update, or $delete into one with no delete operation. Read-only, by fields: "read" or by hand-listed capabilities, means no nested write either. Under exposed: true those directives are dropped from the relation instead, and startup succeeds. | Narrow the relation to the directives it may use (author: { select: true, create: ["$connect"] }), or give the target Resource the create, update or delete operation, or a field writable there. |
|
|
140
141
|
| [`COMPILER_INVALID_VALIDATOR`](https://aventara.razor34.dev/framework/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. |
|
|
141
142
|
| [`COMPILER_INVALID_JSON_SHAPE`](https://aventara.razor34.dev/framework/docs/error-codes#compiler_invalid_json_shape) | A Json shape cannot apply. | A json shape on a field that is not a discovered Json field, a shape not built with AvZ, or one that admits null or absence at its top level. | Put the shape on a Json field, build it with AvZ, and keep its top level non-null (the column's nullability decides null). |
|
|
142
143
|
| [`COMPILER_INVALID_CLIENT_CONTRACT`](https://aventara.razor34.dev/framework/docs/error-codes#compiler_invalid_client_contract) | The compiled client contract is invalid. | The finished client contract failed its own structural check: a framework defect. | Report it with the diagnostic's path and message. |
|
package/README.md
CHANGED
|
@@ -53,7 +53,8 @@ of generic JSON. So does a client generated with `avclient`, which emits the typ
|
|
|
53
53
|
|
|
54
54
|
The shape is also one of `meta`'s validators. Every write of `meta` is checked against it before the database is
|
|
55
55
|
asked: a create, update, upsert, nested write or transaction step, including a value a transaction step takes from an
|
|
56
|
-
earlier step. It is checked where validators run
|
|
56
|
+
earlier step. It is checked where validators run (a value you send after every pipe and before write compute, a
|
|
57
|
+
value write compute sets or changes after it) and before any interceptor or hook, and its issues
|
|
57
58
|
come in field order with the other validators'. A value out of shape is refused with `A2004`, one issue per problem at its
|
|
58
59
|
exact path:
|
|
59
60
|
|
|
@@ -396,7 +397,7 @@ answers the optional `grantsOf` is asked, before the write, which of its grants
|
|
|
396
397
|
nothing the request writes), and gets the answer back on the after check as `beforeGrants`. Such an engine refuses
|
|
397
398
|
an after check that carries `before` without `beforeGrants` rather than deciding `before` after the write.
|
|
398
399
|
A typed literal in a fragment the engine answers may be in its application form (`Date`, `AvDecimal`, `bigint`) or its
|
|
399
|
-
wire form (an ISO string, a decimal string, a digit string), as a policy read from JSON states it. Where `transactionsStyle` is `"interactive"`, a
|
|
400
|
+
wire form (an ISO string, a decimal string, a digit string), as a policy read from JSON states it. Where `transactionsStyle` is `"interactive"` or `"server"`, a
|
|
400
401
|
checked write with a check after it (an update, a nested write) runs in a transaction of its own, and a create or
|
|
401
402
|
delete checked before it runs is one statement; where it is `"none"`, a write that could only be checked after it runs
|
|
402
403
|
is refused (for after conditions, the updates the engine lists in `afterChecks`).
|
|
@@ -37,6 +37,8 @@ export declare const minSize: (minimum: number) => AvZCheck<"bytes">;
|
|
|
37
37
|
export declare const size: (exact: number) => AvZCheck<"bytes">;
|
|
38
38
|
/** A string `pattern` matches (a JavaScript regular expression; its flags count). */
|
|
39
39
|
export declare const regex: (pattern: RegExp) => AvZCheck<"string">;
|
|
40
|
+
/** A string equal to one of `values`, compared exactly. */
|
|
41
|
+
export declare const oneOf: <const Values extends readonly string[]>(values: Values & (Values extends readonly [] ? never : unknown)) => AvZCheck<"string">;
|
|
40
42
|
/** A string containing `value`. */
|
|
41
43
|
export declare const includes: (value: string) => AvZCheck<"string">;
|
|
42
44
|
/** A string starting with `prefix`. */
|
|
@@ -68,6 +68,18 @@ export const regex = (pattern) => {
|
|
|
68
68
|
pattern: { source: pattern.source, flags: pattern.flags },
|
|
69
69
|
});
|
|
70
70
|
};
|
|
71
|
+
const PATTERN_SYNTAX = /[\\^$.*+?()[\]{}|/]/g;
|
|
72
|
+
export const oneOf = (values) => {
|
|
73
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
74
|
+
throw new TypeError("AvZ.oneOf: give at least one value.");
|
|
75
|
+
}
|
|
76
|
+
const escaped = values.map((value) => text("oneOf", value).replace(PATTERN_SYNTAX, "\\$&"));
|
|
77
|
+
return build({
|
|
78
|
+
check: "string_format",
|
|
79
|
+
format: "regex",
|
|
80
|
+
pattern: { source: `^(?:${escaped.join("|")})$`, flags: "" },
|
|
81
|
+
});
|
|
82
|
+
};
|
|
71
83
|
export const includes = (value) => build({
|
|
72
84
|
check: "string_format",
|
|
73
85
|
format: "includes",
|
package/dist/avz/avz.d.ts
CHANGED
|
@@ -134,6 +134,12 @@ export declare class AvZ {
|
|
|
134
134
|
static readonly size: (exact: number) => AvZCheck<"bytes">;
|
|
135
135
|
/** A string the pattern matches: a JavaScript regular expression, flags included. */
|
|
136
136
|
static readonly regex: (pattern: RegExp) => AvZCheck<"string">;
|
|
137
|
+
/**
|
|
138
|
+
* A string equal to one of `values`: an anchored pattern of them, each
|
|
139
|
+
* escaped, whose issue lists the values. The field's type stays `string`;
|
|
140
|
+
* for a closed set the types should name, use a database enum.
|
|
141
|
+
*/
|
|
142
|
+
static readonly oneOf: <const Values extends readonly string[]>(values: Values & (Values extends readonly [] ? never : unknown)) => AvZCheck<"string">;
|
|
137
143
|
/** A string containing `value`. */
|
|
138
144
|
static readonly includes: (value: string) => AvZCheck<"string">;
|
|
139
145
|
/** A string starting with `prefix`. */
|
package/dist/avz/avz.js
CHANGED
|
@@ -323,6 +323,7 @@ export class AvZ {
|
|
|
323
323
|
static minSize = checks.minSize;
|
|
324
324
|
static size = checks.size;
|
|
325
325
|
static regex = checks.regex;
|
|
326
|
+
static oneOf = checks.oneOf;
|
|
326
327
|
static includes = checks.includes;
|
|
327
328
|
static startsWith = checks.startsWith;
|
|
328
329
|
static endsWith = checks.endsWith;
|
|
@@ -20,6 +20,16 @@ export type AvContractCompilerDiagnosticCode = "COMPILER_UNKNOWN_RESOURCE" | "CO
|
|
|
20
20
|
* not writable on create with a reference field neither writable nor computed.
|
|
21
21
|
*/
|
|
22
22
|
| "COMPILER_HIDDEN_REQUIRED_FIELD"
|
|
23
|
+
/**
|
|
24
|
+
* A Pick layer lets a relation write rows of a Resource it cannot write: a
|
|
25
|
+
* nested `$create`, `$connectOrCreate` or `$upsert` into a Resource with no
|
|
26
|
+
* create or upsert variant and no field writable on create, a nested
|
|
27
|
+
* `$update` or `$upsert` into one with no update or upsert variant and no
|
|
28
|
+
* field writable on update, or a nested `$delete` into one with no delete
|
|
29
|
+
* variant (an Omit layer drops those directives instead). `path` is the
|
|
30
|
+
* relation's exemption.
|
|
31
|
+
*/
|
|
32
|
+
| "COMPILER_READ_ONLY_NESTED_WRITE"
|
|
23
33
|
/**
|
|
24
34
|
* A field validator cannot apply: it is not a check built with `AvZ`, it does
|
|
25
35
|
* not fit the field's type (`AvZ.maxLength` on an `int` field, `AvZ.maxItems`
|
|
@@ -79,6 +79,11 @@ export const CONTRACT_COMPILER_DIAGNOSTIC_GUIDANCE = {
|
|
|
79
79
|
cause: "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.",
|
|
80
80
|
fix: "Expose the field, give it a compute.create stage, or exempt the layer's create operations.",
|
|
81
81
|
},
|
|
82
|
+
COMPILER_READ_ONLY_NESTED_WRITE: {
|
|
83
|
+
message: "A relation writes rows of a read-only Resource.",
|
|
84
|
+
cause: 'A layer with exposed: false grants a relation a write into a Resource it cannot write: $create, $connectOrCreate or $upsert into a Resource with no create or upsert operation and no field writable on create, $update or $upsert into one with no update or upsert operation and no field writable on update, or $delete into one with no delete operation. Read-only, by fields: "read" or by hand-listed capabilities, means no nested write either. Under exposed: true those directives are dropped from the relation instead, and startup succeeds.',
|
|
85
|
+
fix: 'Narrow the relation to the directives it may use (author: { select: true, create: ["$connect"] }), or give the target Resource the create, update or delete operation, or a field writable there.',
|
|
86
|
+
},
|
|
82
87
|
COMPILER_INVALID_VALIDATOR: {
|
|
83
88
|
message: "A field validator cannot apply.",
|
|
84
89
|
cause: "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.",
|
|
@@ -8,7 +8,7 @@ import { AVENTARA_PROTOCOL_VERSION } from "../contracts/contract-protocol.js";
|
|
|
8
8
|
import { advertisedDirectives, advertisedReducers, holdsInAnyScope, } from "../contracts/field.js";
|
|
9
9
|
import { isOperationVariantAvailable, } from "../contracts/resource-operations.js";
|
|
10
10
|
import { isRecord } from "../runtime/record.guard.js";
|
|
11
|
-
import { checkCreateRequirements, checkHiddenRequirements, projectCreateRequirements, } from "./create-requirements.projector.js";
|
|
11
|
+
import { checkCreateRequirements, checkHiddenRequirements, checkReadOnlyNestedWrites, projectCreateRequirements, withheldKey, withholdReadOnlyNestedWrites, } from "./create-requirements.projector.js";
|
|
12
12
|
import { applyValidators, bindFieldValidators, checkLayerValidators, compileValidatorTable, } from "./field-validator.compiler.js";
|
|
13
13
|
import { explainManagedRefusals, hideManagedElsewhere, managedElsewhereOf, managedWriteReason, markCoreResources, pruneManagedEnums, withholdManagedWrites, } from "./managed-elsewhere.projector.js";
|
|
14
14
|
const UNIQUE_VARIANTS = [
|
|
@@ -491,6 +491,9 @@ function capabilityAxisFor(kind, key) {
|
|
|
491
491
|
function unavailableCapability(path, message, diagnostics) {
|
|
492
492
|
diagnostics.push(diagnostic("COMPILER_UNAVAILABLE_CAPABILITY", path, message));
|
|
493
493
|
}
|
|
494
|
+
function parentLayerOf(walk) {
|
|
495
|
+
return walk.layer === "agent" ? (walk.parent ?? "client") : "root";
|
|
496
|
+
}
|
|
494
497
|
function otherOverlay(layer) {
|
|
495
498
|
return layer === "client" ? "application" : "client";
|
|
496
499
|
}
|
|
@@ -504,9 +507,23 @@ function exemptsWhole(entry) {
|
|
|
504
507
|
}
|
|
505
508
|
function exemptsAnything(entry) {
|
|
506
509
|
return (entry === true ||
|
|
510
|
+
entry === READ ||
|
|
507
511
|
(Array.isArray(entry) && entry.length > 0) ||
|
|
508
512
|
isRecord(entry));
|
|
509
513
|
}
|
|
514
|
+
const READ = "read";
|
|
515
|
+
const READ_CAPABILITIES = {
|
|
516
|
+
scalar: ["select", "filter", "order", "orderNulls"],
|
|
517
|
+
relation: ["select", "include", "filter", "order", "pagination", "reducers"],
|
|
518
|
+
};
|
|
519
|
+
const readCapabilitiesCovered = [true, true];
|
|
520
|
+
void readCapabilitiesCovered;
|
|
521
|
+
function readCapabilitiesOf(kind, parentCapabilities) {
|
|
522
|
+
return READ_CAPABILITIES[kind].filter((capability) => {
|
|
523
|
+
const axis = capabilityAxisFor(kind, capability);
|
|
524
|
+
return (axis !== undefined && memberOffered(axis, parentCapabilities[capability]));
|
|
525
|
+
});
|
|
526
|
+
}
|
|
510
527
|
function listedNames(entry, path) {
|
|
511
528
|
if (!Array.isArray(entry)) {
|
|
512
529
|
return [];
|
|
@@ -585,6 +602,34 @@ function offeredTokens(walk, axis, universeValue, listed, capability, label) {
|
|
|
585
602
|
}
|
|
586
603
|
return tokens;
|
|
587
604
|
}
|
|
605
|
+
const WITHHELD_LACKS = {
|
|
606
|
+
delete: ["has no delete operation", "a delete operation"],
|
|
607
|
+
update: [
|
|
608
|
+
"updates nothing",
|
|
609
|
+
"an update operation or a field writable on update",
|
|
610
|
+
],
|
|
611
|
+
create: [
|
|
612
|
+
"creates nothing",
|
|
613
|
+
"a create operation or a field writable on create",
|
|
614
|
+
],
|
|
615
|
+
};
|
|
616
|
+
function joined(names) {
|
|
617
|
+
return names.length <= 1
|
|
618
|
+
? (names[0] ?? "")
|
|
619
|
+
: `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
|
|
620
|
+
}
|
|
621
|
+
function withheldWrite(walk, universeField, label, path, directives, member, withheld) {
|
|
622
|
+
const target = String(universeField.target);
|
|
623
|
+
const above = parentLayerOf(walk);
|
|
624
|
+
const reasons = [
|
|
625
|
+
...new Set(directives.flatMap((name) => withheld.get(name) ?? [])),
|
|
626
|
+
];
|
|
627
|
+
const named = member ?? joined(directives);
|
|
628
|
+
const message = reasons.length === 1 && reasons[0] === "delete"
|
|
629
|
+
? `the ${walk.layer} layer lets relation ${label} delete rows of resource "${target}" ($delete), but "${target}" has no delete operation in ${above}, the layer ${walk.layer} starts from, so ${above} offers no $delete on ${label}${member === undefined ? "" : ` and no ${member} at all, $delete being all it had`}; take ${named} off ${label}, or give "${target}" a delete operation in ${above}`
|
|
630
|
+
: `the ${walk.layer} layer lets relation ${label} write rows of resource "${target}" (${joined(directives)}), but "${target}" ${reasons.map((reason) => WITHHELD_LACKS[reason][0]).join(", and ")} in ${above}, the layer ${walk.layer} starts from, so ${above} offers no ${joined(directives)} on ${label}${member === undefined ? "" : ` and no ${member} at all`}; take ${named} off ${label}, or give "${target}" ${reasons.map((reason) => WITHHELD_LACKS[reason][1]).join(", and ")} in ${above}`;
|
|
631
|
+
walk.diagnostics.push(diagnostic("COMPILER_READ_ONLY_NESTED_WRITE", path.replace(/(\[\d+\]|\.[^.[\]]+)$/, ""), message));
|
|
632
|
+
}
|
|
588
633
|
function exemptCapability(walk, universeField, parentCapabilities, result, capability, entry, label, path) {
|
|
589
634
|
if (entry === undefined) {
|
|
590
635
|
return;
|
|
@@ -624,7 +669,19 @@ function exemptCapability(walk, universeField, parentCapabilities, result, capab
|
|
|
624
669
|
if (tokens !== undefined && tokens.length === 0) {
|
|
625
670
|
return;
|
|
626
671
|
}
|
|
672
|
+
const withheldHere = new Map();
|
|
673
|
+
const prefix = withheldKey(label.slice(1, -1), capability, "");
|
|
674
|
+
for (const [key, reason] of walk.withheldWrites ?? []) {
|
|
675
|
+
if (key.startsWith(prefix)) {
|
|
676
|
+
withheldHere.set(key.slice(prefix.length), reason);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
627
679
|
if (!parentHas) {
|
|
680
|
+
if (withheldHere.size > 0 &&
|
|
681
|
+
(tokens === undefined || tokens.every((token) => withheldHere.has(token)))) {
|
|
682
|
+
withheldWrite(walk, universeField, label, path, tokens ?? [...withheldHere.keys()], tokens === undefined ? capability : undefined, withheldHere);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
628
685
|
widening(walk, "COMPILER_UNAVAILABLE_CAPABILITY", path, `capability "${capability}" of field ${label}`);
|
|
629
686
|
return;
|
|
630
687
|
}
|
|
@@ -635,7 +692,14 @@ function exemptCapability(walk, universeField, parentCapabilities, result, capab
|
|
|
635
692
|
const parentTokens = memberTokens(axis, parentValue) ?? [];
|
|
636
693
|
const hidden = tokens.filter((token) => !parentTokens.includes(token));
|
|
637
694
|
if (hidden.length > 0) {
|
|
638
|
-
|
|
695
|
+
const withheld = hidden.filter((token) => withheldHere.has(token));
|
|
696
|
+
const widened = hidden.filter((token) => !withheldHere.has(token));
|
|
697
|
+
if (withheld.length > 0) {
|
|
698
|
+
withheldWrite(walk, universeField, label, path, withheld, undefined, withheldHere);
|
|
699
|
+
}
|
|
700
|
+
if (widened.length > 0) {
|
|
701
|
+
widening(walk, "COMPILER_UNAVAILABLE_CAPABILITY", path, `${widened.map((token) => `"${token}"`).join(", ")} of capability "${capability}" of field ${label}`);
|
|
702
|
+
}
|
|
639
703
|
return;
|
|
640
704
|
}
|
|
641
705
|
result[capability] = withTokens(axis, parentValue, parentTokens.filter((token) => tokens.includes(token)));
|
|
@@ -650,7 +714,12 @@ function exemptField(walk, universeField, parentField, entry, label, path) {
|
|
|
650
714
|
}
|
|
651
715
|
}
|
|
652
716
|
}
|
|
653
|
-
if (
|
|
717
|
+
if (entry === READ) {
|
|
718
|
+
for (const capability of readCapabilitiesOf(universeField.kind, parentCapabilities)) {
|
|
719
|
+
exemptCapability(walk, universeField, parentCapabilities, result, capability, true, label, path);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
else if (Array.isArray(entry)) {
|
|
654
723
|
for (const [capability, capabilityPath] of listedNames(entry, path)) {
|
|
655
724
|
exemptCapability(walk, universeField, parentCapabilities, result, capability, true, label, capabilityPath);
|
|
656
725
|
}
|
|
@@ -670,7 +739,11 @@ function exemptField(walk, universeField, parentField, entry, label, path) {
|
|
|
670
739
|
return { ...cloneValue(parentField), capabilities: result };
|
|
671
740
|
}
|
|
672
741
|
function exemptFields(walk, resourceName, universe, parent, entries, path) {
|
|
673
|
-
const named =
|
|
742
|
+
const named = entries === READ
|
|
743
|
+
? Object.fromEntries(Object.keys(parent.fields).map((name) => [name, READ]))
|
|
744
|
+
: isRecord(entries)
|
|
745
|
+
? entries
|
|
746
|
+
: {};
|
|
674
747
|
for (const fieldName of Object.keys(named)) {
|
|
675
748
|
if (universe.fields[fieldName] === undefined) {
|
|
676
749
|
walk.diagnostics.push(diagnostic("COMPILER_UNKNOWN_FIELD", `${path}.${fieldName}`, `field "${fieldName}" is neither discovered by the adapter nor defined at the root`));
|
|
@@ -1025,8 +1098,8 @@ function resolveLimits(scope) {
|
|
|
1025
1098
|
}
|
|
1026
1099
|
function resolveTransactions(model, scopeName, scope, diagnostics) {
|
|
1027
1100
|
const requested = scope.transactionsStyle ?? model.transactions;
|
|
1028
|
-
if (requested
|
|
1029
|
-
diagnostics.push(diagnostic("COMPILER_UNAVAILABLE_CAPABILITY", `${scopeName}.transactionsStyle`,
|
|
1101
|
+
if (requested !== "none" && model.transactions === "none") {
|
|
1102
|
+
diagnostics.push(diagnostic("COMPILER_UNAVAILABLE_CAPABILITY", `${scopeName}.transactionsStyle`, `transaction capability "${requested}" is not available in the canonical adapter model`));
|
|
1030
1103
|
}
|
|
1031
1104
|
return requested;
|
|
1032
1105
|
}
|
|
@@ -1036,6 +1109,11 @@ function semanticObject(contract) {
|
|
|
1036
1109
|
function exposureStyle(layer) {
|
|
1037
1110
|
return layer.exposed === false ? "pick" : "omit";
|
|
1038
1111
|
}
|
|
1112
|
+
function withholdOmittedWrites(style, projection) {
|
|
1113
|
+
return style === "omit"
|
|
1114
|
+
? withholdReadOnlyNestedWrites(projection.resources)
|
|
1115
|
+
: new Map();
|
|
1116
|
+
}
|
|
1039
1117
|
function projectCore(model, config, diagnostics) {
|
|
1040
1118
|
const scope = config.root;
|
|
1041
1119
|
const managed = managedElsewhereOf(config.trees.root);
|
|
@@ -1054,13 +1132,15 @@ function projectCore(model, config, diagnostics) {
|
|
|
1054
1132
|
const projection = projectExposure({ layer: "root", style: exposureStyle(scope), diagnostics }, baseline, baseline, scope.exemptions);
|
|
1055
1133
|
qualifyFilterModes(projection);
|
|
1056
1134
|
pruneHiddenPairings(projection, "shown");
|
|
1135
|
+
const withheldWrites = withholdOmittedWrites(exposureStyle(scope), projection);
|
|
1057
1136
|
projectCreateRequirements(model, projection.resources);
|
|
1058
1137
|
const rootChecked = diagnostics.length;
|
|
1059
1138
|
checkHiddenRequirements(model, "root", baseline.resources, unwithheld.resources, projection.resources, diagnostics);
|
|
1139
|
+
checkReadOnlyNestedWrites("root", undefined, projection.resources, diagnostics);
|
|
1060
1140
|
explainManagedRefusals(diagnostics, rootChecked, "root", baseline, managed);
|
|
1061
1141
|
applyValidators(projection.resources, validators);
|
|
1062
1142
|
validateProjection(projection, "root", diagnostics);
|
|
1063
|
-
return { baseline, projection, validators };
|
|
1143
|
+
return { baseline, projection, validators, withheldWrites };
|
|
1064
1144
|
}
|
|
1065
1145
|
function markGovernedFields(baseline, governed) {
|
|
1066
1146
|
if (governed === undefined) {
|
|
@@ -1083,7 +1163,7 @@ function markGovernedFields(baseline, governed) {
|
|
|
1083
1163
|
}
|
|
1084
1164
|
}
|
|
1085
1165
|
}
|
|
1086
|
-
function projectDerivedScope(model, scopeName, core, parentView, config, diagnostics) {
|
|
1166
|
+
function projectDerivedScope(model, scopeName, core, parentView, parentWithheld, config, diagnostics) {
|
|
1087
1167
|
const scope = config[scopeName];
|
|
1088
1168
|
const managed = managedElsewhereOf(config.trees.root);
|
|
1089
1169
|
const baseline = cloneValue(core.baseline);
|
|
@@ -1110,6 +1190,7 @@ function projectDerivedScope(model, scopeName, core, parentView, config, diagnos
|
|
|
1110
1190
|
layer: scopeName,
|
|
1111
1191
|
style: exposureStyle(scope),
|
|
1112
1192
|
diagnostics,
|
|
1193
|
+
withheldWrites: parentWithheld,
|
|
1113
1194
|
...(scopeName === "agent" ? { parent: config.agent.parent } : {}),
|
|
1114
1195
|
}, baseline, parent, scope.exemptions);
|
|
1115
1196
|
qualifyFilterModes(projection);
|
|
@@ -1117,13 +1198,18 @@ function projectDerivedScope(model, scopeName, core, parentView, config, diagnos
|
|
|
1117
1198
|
if (unhidden !== parent) {
|
|
1118
1199
|
pruneManagedEnums(projection, unhidden, managed);
|
|
1119
1200
|
}
|
|
1201
|
+
const withheldWrites = new Map([
|
|
1202
|
+
...parentWithheld,
|
|
1203
|
+
...withholdOmittedWrites(exposureStyle(scope), projection),
|
|
1204
|
+
]);
|
|
1120
1205
|
projectCreateRequirements(model, projection.resources);
|
|
1121
1206
|
const layerChecked = diagnostics.length;
|
|
1122
1207
|
checkHiddenRequirements(model, scopeName, baseline.resources, unhidden.resources, projection.resources, diagnostics);
|
|
1208
|
+
checkReadOnlyNestedWrites(scopeName, unhidden.resources, projection.resources, diagnostics);
|
|
1123
1209
|
explainManagedRefusals(diagnostics, layerChecked, scopeName, baseline, managed);
|
|
1124
1210
|
applyValidators(projection.resources, validators);
|
|
1125
1211
|
validateProjection(projection, scopeName, diagnostics);
|
|
1126
|
-
return { projection, validators };
|
|
1212
|
+
return { projection, validators, withheldWrites };
|
|
1127
1213
|
}
|
|
1128
1214
|
function bindShapedFields(contract, definitions) {
|
|
1129
1215
|
for (const [resourceName, resource] of Object.entries(contract.resources)) {
|
|
@@ -1145,14 +1231,18 @@ export async function compileContracts(model, config) {
|
|
|
1145
1231
|
checkReservedResourceNames(model, diagnostics);
|
|
1146
1232
|
checkCreateRequirements(model, diagnostics);
|
|
1147
1233
|
const coreCompilation = projectCore(model, compilerConfig, diagnostics);
|
|
1148
|
-
const applicationProjection = projectDerivedScope(model, "application", coreCompilation, coreCompilation.projection, compilerConfig, diagnostics);
|
|
1149
|
-
const clientProjection = projectDerivedScope(model, "client", coreCompilation, coreCompilation.projection, compilerConfig, diagnostics);
|
|
1234
|
+
const applicationProjection = projectDerivedScope(model, "application", coreCompilation, coreCompilation.projection, coreCompilation.withheldWrites, compilerConfig, diagnostics);
|
|
1235
|
+
const clientProjection = projectDerivedScope(model, "client", coreCompilation, coreCompilation.projection, coreCompilation.withheldWrites, compilerConfig, diagnostics);
|
|
1150
1236
|
const agentParent = compilerConfig.agent.parent;
|
|
1151
1237
|
const agentProjection = projectDerivedScope(model, "agent", coreCompilation, agentParent === "root"
|
|
1152
1238
|
? coreCompilation.projection
|
|
1153
1239
|
: agentParent === "application"
|
|
1154
1240
|
? applicationProjection.projection
|
|
1155
|
-
: clientProjection.projection,
|
|
1241
|
+
: clientProjection.projection, agentParent === "root"
|
|
1242
|
+
? coreCompilation.withheldWrites
|
|
1243
|
+
: agentParent === "application"
|
|
1244
|
+
? applicationProjection.withheldWrites
|
|
1245
|
+
: clientProjection.withheldWrites, compilerConfig, diagnostics);
|
|
1156
1246
|
markCoreResources(coreCompilation.projection, model, managedElsewhereOf(compilerConfig.trees.root));
|
|
1157
1247
|
if (diagnostics.length > 0) {
|
|
1158
1248
|
return { ok: false, diagnostics };
|