@aventara/core 1.0.0-rc.2 → 1.0.0-rc.4
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 +175 -0
- package/ERRORS.md +9 -9
- package/README.md +72 -12
- package/dist/adapters/adapter-operation.d.ts +7 -0
- package/dist/compiler/contract.compiler.d.ts +6 -0
- package/dist/compiler/contract.compiler.js +73 -12
- package/dist/config/computed-fields.js +2 -9
- package/dist/config/config-diagnostic.guidance.js +3 -3
- package/dist/config/config.resolver.d.ts +14 -0
- package/dist/config/config.resolver.js +31 -0
- package/dist/config/config.validator.js +82 -0
- package/dist/config/exact-framework-config.d.ts +8 -1
- package/dist/config/framework-config.d.ts +26 -1
- package/dist/contracts/client-contract-structure.validator.d.ts +8 -0
- package/dist/contracts/client-contract-structure.validator.js +6 -1
- package/dist/contracts/field.d.ts +11 -0
- package/dist/framework/contract-view.d.ts +41 -10
- package/dist/framework/framework.factory.js +39 -11
- package/dist/framework/operation-facade.d.ts +24 -5
- package/dist/framework/operation-facade.js +2 -1
- package/dist/index.d.ts +4 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/operations/database-refusal.d.ts +1 -1
- package/dist/operations/database-refusal.js +1 -1
- package/dist/operations/operation-arguments.d.ts +1 -1
- package/dist/operations/operation-call.d.ts +9 -5
- package/dist/operations/operation-result.d.ts +5 -3
- package/dist/operations/operation-validator-core.js +66 -33
- package/dist/operations/validation-code.guidance.js +2 -2
- package/dist/policy/policy-admission.d.ts +22 -0
- package/dist/policy/policy-admission.js +400 -0
- package/dist/policy/policy-after-update.runtime.d.ts +29 -0
- package/dist/policy/policy-after-update.runtime.js +131 -0
- package/dist/policy/policy-condition.d.ts +15 -0
- package/dist/policy/policy-condition.js +36 -0
- package/dist/policy/policy-engine.d.ts +111 -0
- package/dist/policy/policy-engine.js +1 -0
- package/dist/policy/policy-nested-write.runtime.d.ts +38 -0
- package/dist/policy/policy-nested-write.runtime.js +449 -0
- package/dist/policy/policy-read.runtime.d.ts +83 -0
- package/dist/policy/policy-read.runtime.js +494 -0
- package/dist/policy/policy-request.d.ts +37 -0
- package/dist/policy/policy-request.js +52 -0
- package/dist/policy/policy-session.d.ts +83 -0
- package/dist/policy/policy-session.js +234 -0
- package/dist/policy/policy-startup.validator.d.ts +8 -0
- package/dist/policy/policy-startup.validator.js +106 -0
- package/dist/policy/policy-write.runtime.d.ts +30 -0
- package/dist/policy/policy-write.runtime.js +394 -0
- package/dist/prebuilt/core-version.generated.d.ts +2 -0
- package/dist/prebuilt/core-version.generated.js +1 -0
- package/dist/prebuilt/prebuilt-contracts.d.ts +40 -0
- package/dist/prebuilt/prebuilt-contracts.emitter.d.ts +16 -0
- package/dist/prebuilt/prebuilt-contracts.emitter.js +50 -0
- package/dist/prebuilt/prebuilt-contracts.js +1 -0
- package/dist/prebuilt/prebuilt-contracts.loader.d.ts +21 -0
- package/dist/prebuilt/prebuilt-contracts.loader.js +122 -0
- package/dist/prebuilt/prebuilt-inputs.fingerprint.d.ts +16 -0
- package/dist/prebuilt/prebuilt-inputs.fingerprint.js +192 -0
- package/dist/runtime/computed-read-runtime.d.ts +6 -1
- package/dist/runtime/computed-read-runtime.js +224 -45
- package/dist/runtime/context-executor.runtime.js +4 -2
- package/dist/runtime/execution-options.d.ts +8 -0
- package/dist/runtime/field-validator.runtime.d.ts +9 -0
- package/dist/runtime/field-validator.runtime.js +36 -2
- package/dist/runtime/framework-warning.emitter.d.ts +6 -0
- package/dist/runtime/framework-warning.emitter.js +8 -0
- package/dist/runtime/level-lifecycle.runtime.js +3 -2
- package/dist/runtime/operation-code.guidance.js +4 -4
- package/dist/runtime/operation-request.d.ts +7 -0
- package/dist/runtime/operation-runtime.d.ts +9 -1
- package/dist/runtime/operation-runtime.js +119 -11
- package/dist/runtime/readonly-value.d.ts +5 -1
- package/dist/runtime/readonly-value.js +67 -28
- package/dist/transactions/transaction-runtime.d.ts +4 -1
- package/dist/transactions/transaction-runtime.js +27 -5
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,180 @@
|
|
|
1
1
|
# @aventara/core
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e3d03a7: The error catalog says more about policies. `A2003` now names the cases a policy adds: a unique read of a row outside the
|
|
8
|
+
caller's read scope, and a single-row update or delete of a row the caller cannot read, are misses, while a row the
|
|
9
|
+
caller can read but not change answers `A4002`. `CONFIG_REQUIRED`, `CONFIG_UNKNOWN_PROPERTY` and `CONFIG_INVALID_VALUE`
|
|
10
|
+
name the `policy` key's rules: `policy.engine` and `policy.principal` are required, `policy` and `prebuilt` are root-only,
|
|
11
|
+
and the engine's `governed`, `paths` and `afterChecks` lists must name what the Core Contract has. Wording only; no
|
|
12
|
+
behaviour changes.
|
|
13
|
+
- d10639a: Faster reads. The read-only copies stages are told cost about half what they did: a read with a computed field
|
|
14
|
+
copies each row in one pass, a row with several computed fields shares its nested objects between the stages'
|
|
15
|
+
copies, and after hooks share one frozen copy of the result. A row whose computed field returns a value at once no
|
|
16
|
+
longer waits a turn per row. The MCP endpoint builds its JSON Schema validator once instead of on every request.
|
|
17
|
+
What stages are told, the order they run in and what a request answers are unchanged.
|
|
18
|
+
- f67e68c: A service method can return any façade node or variant without a return type. Under `declaration: true` (a Nest
|
|
19
|
+
build) the declaration file now writes a short reference, under half a kilobyte per method at any schema size, where it
|
|
20
|
+
wrote the whole Contract view (about 440 KB per method over 50 Resources). A method returning a variant
|
|
21
|
+
(`this.framework.application.Post.find.many`) no longer needs an annotation to compile, and one returning a transaction
|
|
22
|
+
variant (`this.framework.appTx.Post.create.one`) no longer runs the compiler out of memory.
|
|
23
|
+
|
|
24
|
+
New type names, exported from `@aventara/core` so a declaration can write them (you never need to write them yourself):
|
|
25
|
+
|
|
26
|
+
- `AvContractView`, the Contract view the façades are typed over;
|
|
27
|
+
- one call type per surface and shape: `AvApplicationProjectingCall`, `AvApplicationPlainCall`,
|
|
28
|
+
`AvClientProjectingCall`, `AvClientPlainCall`, `AvAppTxProjectingCall`, `AvAppTxPlainCall`,
|
|
29
|
+
`AvClientTxProjectingCall`, `AvClientTxPlainCall`.
|
|
30
|
+
|
|
31
|
+
A configuration with `fields` entries still writes those entries after each reference; annotate the method if that
|
|
32
|
+
matters (see the README's "Returning a façade from a method").
|
|
33
|
+
|
|
34
|
+
- 708556c: Policy engines check nested writes. A create, update or upsert whose data writes a relation through a directive
|
|
35
|
+
(`$create`, `$connect`, `$connectOrCreate`, `$update`, `$upsert`, `$delete`, `$disconnect`, `$set`) now runs under a
|
|
36
|
+
policy, where before it was refused. It runs as one adapter call inside the write's interactive transaction, and one
|
|
37
|
+
failed check rolls the whole write back with `403 A4002`.
|
|
38
|
+
|
|
39
|
+
- Every row the nested write creates is read back as stored (its parent's reference filled, database defaults
|
|
40
|
+
present) and asked `can({ action: "create", row })`, its `exists` lookups seeing the rows the same call wrote. A
|
|
41
|
+
create whose own data holds a nested write is asked about its row the same way, after the insert.
|
|
42
|
+
- A row a directive links or unlinks is held to the scope of the row that changes: a linked row that holds the
|
|
43
|
+
reference (a to-many relation's rows, the back end of a one-to-one, a many-to-many relation's rows) must be in its
|
|
44
|
+
update scope, one whose parent holds the reference (a to-one relation's target) in its read scope. One `exists`
|
|
45
|
+
per Resource answers it, inside the transaction. A linked row that changes is an update: after the call it gets
|
|
46
|
+
the after check (`can({ action: "update", row })`) and the move-out re-read. Linking a new row to a one-to-one's
|
|
47
|
+
back end unlinks the old one, which is held the same way.
|
|
48
|
+
- A to-many nested `$update` or `$delete` acts only on the rows within the update (or delete) scope, as
|
|
49
|
+
`update.many` and `delete.many` do: core reads those rows' identifiers inside the transaction and hands the
|
|
50
|
+
adapter `where: { id: { in: [...] } }`. A to-one one refuses the write when its row is outside the scope. A nested
|
|
51
|
+
`$update`'s changed rows get the after check and the move-out re-read after the call.
|
|
52
|
+
- The fields a nested record sends are held to its Resource's `fieldsOf({ access: "write" })`, and a relation written
|
|
53
|
+
through a directive to the parent's grant, its reference fields with it.
|
|
54
|
+
- Where the operation's `transactionsStyle` is `"none"`, a nested write under a policy is refused before it runs.
|
|
55
|
+
|
|
56
|
+
- 3a0dfca: A write the policy checks costs fewer database round trips. What each check decides and refuses is unchanged.
|
|
57
|
+
|
|
58
|
+
- A single-row update or delete carries the caller's scopes inside its own `where` and runs as one statement, with no
|
|
59
|
+
read of its row before it. One that matches nothing still answers `A4002` when the caller can read the row and
|
|
60
|
+
`A2003` when it cannot, with one lookup on that miss only. The Prisma adapter sends it as
|
|
61
|
+
`update({ where: { id, AND: [scope] } })`.
|
|
62
|
+
- A create asks `can({ action: "create", row })` and its `exists` lookups before the insert, without a transaction. A
|
|
63
|
+
transaction around them never closed the race between the lookup and the insert at the database's default
|
|
64
|
+
isolation, and cost a BEGIN and a COMMIT.
|
|
65
|
+
- A transaction opens only when a check after the write may roll it back: an update's after check or move-out, a
|
|
66
|
+
created row held to a write grant, a nested write, or returned rows checked by `exists`. A plain checked create or
|
|
67
|
+
delete is one statement, its interceptors and hooks outside any transaction.
|
|
68
|
+
- The after check asks `can` about the row the update returned, so it reads nothing again. Only a move-out reads the
|
|
69
|
+
changed rows again. An `update.count` runs as the returning `update.many` when the Contract offers it.
|
|
70
|
+
|
|
71
|
+
Round trips per request on PostgreSQL: `create.one` 3 → 1, `delete.unique` 4 → 1, `update.unique` 5 → 3,
|
|
72
|
+
`update.many` 4 → 3, `update.count` 5 → 3. Reads, and a Framework without a policy, are unchanged.
|
|
73
|
+
|
|
74
|
+
- 99169d2: Policy engines, reads first. `createFramework` takes an optional root `policy: { engine, principal }`: an engine that
|
|
75
|
+
answers three questions (`can`, `scopeOf`, `fieldsOf`, each now or as a promise), and a function that derives a
|
|
76
|
+
request's principal from its `ctx.$state`. Core never imports an engine; one satisfies `AvPolicyEngine` by its shape,
|
|
77
|
+
and a scope or grant is a fragment of Aventara's own `where` (`AvPolicyWhere`).
|
|
78
|
+
|
|
79
|
+
- Client and agent requests are checked (an MCP tool call is an agent request). An application call is checked only
|
|
80
|
+
when it names a principal: `framework.application.Note.find.many(args, { as: user })`. A call a stage makes through
|
|
81
|
+
`ctx` never is.
|
|
82
|
+
- Before any guard, `can` and the fields the request names or receives (`fieldsOf`); a refusal is `403 A4002`. At the
|
|
83
|
+
adapter call, after every developer stage, the read scope is ANDed into the root `where`, every to-many include and
|
|
84
|
+
its `$count`, and every relation filter, at every level. A to-one include of a row out of scope refuses the request;
|
|
85
|
+
it is never nulled.
|
|
86
|
+
- Governed fields (the engine's `governed` list) are left out of every default projection and carry `governed: true`
|
|
87
|
+
in the Contracts. Name one to get it: `select: ["$defaults", "email"]`, where `$defaults` is the level's default
|
|
88
|
+
projection. It is refused where a returned row does not grant it, and a filter or sort by it is checked before the
|
|
89
|
+
query runs, so it cannot reveal values the caller may not read.
|
|
90
|
+
- The engine's `governed` and `paths` lists are checked against the Core Contract at startup. A rule's paths are lists
|
|
91
|
+
of segments, as other paths in core are: `["Note", "campaign", "members", "some", "userId", "equals"]`.
|
|
92
|
+
- Writes are checked as well; see the policy writes entry.
|
|
93
|
+
|
|
94
|
+
Without `policy`, nothing changes: the Contracts and their hashes are the same bytes.
|
|
95
|
+
|
|
96
|
+
- 0fd2608: A write the policy checks costs less CPU after the round-trip changes. The operation core hands its adapter no longer
|
|
97
|
+
carries the request's policy session, so an adapter that keeps its operations no longer keeps each request's session
|
|
98
|
+
with them. The fields a write returns only for the after check are dropped as soon as the check has read them, so a
|
|
99
|
+
read compute stage is told the row the request asked for, not every scalar field. What each check decides, the
|
|
100
|
+
fields `can` is asked about, and the database round trips are unchanged.
|
|
101
|
+
- 154e440: Policy engines check writes. Every create, update, delete and upsert a client or agent makes (or an application call
|
|
102
|
+
made `{ as }` a principal) is now checked by the configuration's `policy`, where before it was refused.
|
|
103
|
+
|
|
104
|
+
- The fields a write sends are held to `fieldsOf({ access: "write" })` before any guard; a field outside it answers
|
|
105
|
+
`403 A4002`. A field writable on some rows only is checked against every row an update reaches, and against the
|
|
106
|
+
row a create inserts, after the insert and inside its transaction.
|
|
107
|
+
- A create asks `can({ action: "create", row })` about each new row on its final data. Its rules can look rows up
|
|
108
|
+
through `exists`, which runs inside the write's transaction, so a plan that creates a campaign and then its first
|
|
109
|
+
note sees the campaign.
|
|
110
|
+
- An update or delete acts only on rows the caller may both change (`scopeOf` for the action) and read. A single-row
|
|
111
|
+
one that matches nothing answers `A4002` when the caller can read the row, and `A2003` when it cannot, so the
|
|
112
|
+
answer does not reveal that the row exists. A `many` or `count` one answers for the caller's rows only.
|
|
113
|
+
- An update that moves a row out of what the caller reads is rolled back with `A4002`. So is one that fails an `after`
|
|
114
|
+
condition: inside the transaction core asks `can({ action: "update", row })` about each changed row of every
|
|
115
|
+
checked update. The engine's new `afterChecks` member (`{ Note: ["update"] }`) names the updates whose grants carry
|
|
116
|
+
such a condition, which are the ones refused where there is no interactive transaction.
|
|
117
|
+
- Where the operation's `transactionsStyle` is `"interactive"`, a checked write runs in a transaction of its own. Where
|
|
118
|
+
it is `"none"` (Cloudflare D1), a write that can only be checked after it runs is refused before it runs; every other
|
|
119
|
+
write runs, a create looking up before it inserts, and an unlisted update without the after check.
|
|
120
|
+
- Nested writes (a relation directive in a write's data) are refused under a policy for now.
|
|
121
|
+
|
|
122
|
+
`afterChecks` is checked against the Core Contract at startup and changes no Contract and no prebuilt fingerprint.
|
|
123
|
+
|
|
124
|
+
- 2fcffdb: Prebuilt Contracts. `createFramework` takes an optional `prebuilt` key: the four Contracts compiled ahead of time (type
|
|
125
|
+
`AvPrebuiltContracts`), so a serverless or edge cold start loads them instead of compiling them. A file is used only
|
|
126
|
+
when this core compiled it, for this protocol, from the same configuration and adapter model; its Contracts, hashes
|
|
127
|
+
and `/_contract` bytes are then the ones compiling would give. Any other file is ignored whole: the framework compiles
|
|
128
|
+
at startup exactly as without one and warns once (`AVENTARA_PREBUILT_STALE`), naming the Resources whose configuration
|
|
129
|
+
or model changed, or the version that differs. Without `prebuilt`, nothing changes. The build step that writes the
|
|
130
|
+
file arrives with `@aventara/build`.
|
|
131
|
+
|
|
132
|
+
## 1.0.0-rc.3
|
|
133
|
+
|
|
134
|
+
### Patch Changes
|
|
135
|
+
|
|
136
|
+
- b38034d: An issue inside one entry of a to-many relation directive's array names that entry's index:
|
|
137
|
+
`["arguments", "data", "posts", "$update", 1, "data", "category"]`, where it read `[…, "$update", "data", "category"]`.
|
|
138
|
+
This holds for `$create`, `$connect`, `$connectOrCreate`, `$update`, `$delete`, `$upsert`, `$set` and `$disconnect`,
|
|
139
|
+
as the runtime's own nested-write paths already did. A directive given one object, not an array, keeps its issue at the
|
|
140
|
+
directive.
|
|
141
|
+
- b38034d: An empty boolean node in the `where` of a write is refused, as an `undefined` there is, and nothing runs. In
|
|
142
|
+
`update.many`, `update.count`, `update.first`, `delete.many`, `delete.count`, `delete.first` and a to-many `$update`
|
|
143
|
+
or `$delete`, an empty `NOT: {}`, `AND: {}` or `[]`, `OR: []` or `AND`/`OR` entry `{}`, at any depth, answers
|
|
144
|
+
`422 A2004` with a `V1001` issue at the node:
|
|
145
|
+
`A boolean node must hold a condition here: an empty one matches every row, and the write would reach more rows.`
|
|
146
|
+
The ORM reads these as no condition, so `delete.count({ where: { NOT: {} } })` deleted every row, and `delete.first`
|
|
147
|
+
with one deleted an arbitrary row. A `where` that is `{}` itself still means every row, and reads pass empty nodes through as before. Field filters are unchanged: a scalar's `{}` and a
|
|
148
|
+
to-many relation's `{}` were already refused, and a to-one relation's `{}` matches the rows that have a related row.
|
|
149
|
+
- b38034d: The error catalog (`AvErrorCatalog`, `ERRORS.md`) catches up with the code: `A2004` says a value the database refused
|
|
150
|
+
may stand at the value itself where the adapter can place it (a bigint its driver cannot bind) and may be too long for
|
|
151
|
+
its index; `V1001` lists an `undefined` in a filter, selector or cursor and an empty boolean node in a bulk write's
|
|
152
|
+
`where`; `V1004` lists a value the database refused as too long or out of range, a bigint a driver cannot bind among
|
|
153
|
+
them.
|
|
154
|
+
- b38034d: A value write compute writes to a field the caller's layer hides is judged by that field's validators and Json shape
|
|
155
|
+
(the Core Contract's), and a value they refuse is the server's error: `500 A3000`, the issues to the `diagnostics` sink
|
|
156
|
+
on an `AvComputedValueError`, with nothing of the hidden field in the answer. Before, a shaped Json field hidden from
|
|
157
|
+
the layer was not judged at all, and a hidden field's own validators answered the caller `A2004` with an issue at the
|
|
158
|
+
hidden field's path. A layer that shows the field still answers `A2004` at it.
|
|
159
|
+
- b38034d: On PostgreSQL, a `@unique` value too long for its index (an index row past the B-tree's limit of about 2704 bytes, SQLSTATE
|
|
160
|
+
`54000`) is answered `422 A2004`, not `500 A3001`: one `V1004` issue at `["arguments"]`, as a value too long for its
|
|
161
|
+
column already was. The adapter reads the SQLSTATE, never the message. The issue's message for a value too long now
|
|
162
|
+
reads `The database refused this value: it is longer than the database can hold for its column.`
|
|
163
|
+
(it said `longer than its column can hold`, which an index refusal is not).
|
|
164
|
+
- b38034d: A Pick layer's list entry that names what its parent hid is refused with the sentence written inline too. Inline in
|
|
165
|
+
`createFramework` or `AventaraModule.forRoot`, the error is at the key and reads:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
Type '["select"]' is not assignable to type '…'.
|
|
169
|
+
…
|
|
170
|
+
Property '"Error: client cannot expose 'Post.internalNote': hidden by root"' is missing in type '["select"]' but required in type '{ readonly "Error: client cannot expose 'Post.internalNote': hidden by root": true; }'.
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
where it ended at `Type 'string' is not assignable to type 'never'.` at the list's element. A list entry is now typed as
|
|
174
|
+
an object keyed by the sentence, which no list has, so a configuration that is not inline (an `aventaraConfig()` handed
|
|
175
|
+
to `AventaraModule.forRootAsync`) ends at the same `Property '"Error: …"' is missing` line, where it ended at
|
|
176
|
+
`Type '"select"' is not assignable to type '"Error: …"'`. An entry written `true` or as an object is unchanged.
|
|
177
|
+
|
|
3
178
|
## 1.0.0-rc.2
|
|
4
179
|
|
|
5
180
|
### 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.
|
|
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.
|
|
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
|
|
|
@@ -30,8 +30,8 @@ The caller can fix these. The generated client and server-side calls throw the c
|
|
|
30
30
|
| [`A2000`](https://aventara-framework.vercel.app/docs/error-codes#a2000) | Invalid request. | The HTTP request cannot be interpreted: malformed JSON, a body that is not a JSON object, an unknown `/_*` path, a body that does not decode in its Content-Encoding, or a missing Aventara-Protocol-Version or Aventara-Contract-Hash header (the message names it). | Send a JSON object with both identity headers. The generated client does this for you. |
|
|
31
31
|
| [`A2001`](https://aventara-framework.vercel.app/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-framework.vercel.app/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
|
-
| [`A2003`](https://aventara-framework.vercel.app/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. | Catch AvNotFoundError, or use a first-style operation, which answers A1001 with null. Check the $connect identifier. |
|
|
34
|
-
| [`A2004`](https://aventara-framework.vercel.app/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 values after the pipes and write compute, 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 out of its range (V1004), or holding a character it cannot store or a pattern it cannot read (V1001), with the issue at `["arguments"]
|
|
33
|
+
| [`A2003`](https://aventara-framework.vercel.app/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-framework.vercel.app/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 values after the pipes and write compute, 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-framework.vercel.app/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-framework.vercel.app/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-framework.vercel.app/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. |
|
|
@@ -63,7 +63,7 @@ Produced by your guards; the framework never authenticates by itself.
|
|
|
63
63
|
|---|---|---|---|
|
|
64
64
|
| [`A4000`](https://aventara-framework.vercel.app/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
65
|
| [`A4001`](https://aventara-framework.vercel.app/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, or a stage threw A4001. | Call as a caller the guard admits, or stop sending the guarded field. To answer another code, throw an AvError from the guard. |
|
|
66
|
-
| [`A4002`](https://aventara-framework.vercel.app/docs/error-codes#a4002) | Policy denied. |
|
|
66
|
+
| [`A4002`](https://aventara-framework.vercel.app/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)
|
|
69
69
|
|
|
@@ -72,10 +72,10 @@ Each entry of `cause.issues[]` under `A2001`, `A2002`, `A2004`, `A2007` or `A200
|
|
|
72
72
|
| Code | Meaning | Cause | Fix |
|
|
73
73
|
|---|---|---|---|
|
|
74
74
|
| [`V1000`](https://aventara-framework.vercel.app/docs/error-codes#v1000) | Required value missing. | A required value is absent: a create leaves out a required field that has no database default and no compute.create (the path names the field), a required relation and its reference field are both missing, a typed Json field lacks a required key, or a $ref has no fingerprint. | Send the value at the issue's path. When the server must fill it, give the field a compute.create stage or a database default; a pipe cannot fill it. |
|
|
75
|
-
| [`V1001`](https://aventara-framework.vercel.app/docs/error-codes#v1001) | Invalid runtime/wire type. | A value of the wrong type: a string where a number is expected, a negative limit, a non-integer id, null given to an operator with no null meaning (gt, contains, in), two directives in one scalar action object (`{ $increment: 1, $decrement: 2 }`), or a server-side value that is not plain data. | Send the type the contract states at that path (the generated client's types say it). |
|
|
75
|
+
| [`V1001`](https://aventara-framework.vercel.app/docs/error-codes#v1001) | Invalid runtime/wire type. | A value of the wrong type: a string where a number is expected, a negative limit, a non-integer id, null given to an operator with no null meaning (gt, contains, in), two directives in one scalar action object (`{ $increment: 1, $decrement: 2 }`), or a server-side value that is not plain data. Also a value that would make the call reach more rows: `undefined` in a filter, a unique selector or a cursor, an empty boolean node (`NOT: {}`, `AND: []`, `OR: []`) in the where of a write (update or delete many, count or first, a to-many $update or $delete). | Send the type the contract states at that path (the generated client's types say it). |
|
|
76
76
|
| [`V1002`](https://aventara-framework.vercel.app/docs/error-codes#v1002) | Invalid enum value. | A value that is not a member of the enum, or outside a typed Json field's AvZ.enum or AvZ.literal. | Send one of the enum's members. |
|
|
77
77
|
| [`V1003`](https://aventara-framework.vercel.app/docs/error-codes#v1003) | Invalid format (for example UUID/DateTime/base64). | A malformed datetime, decimal, base64 or UUID string, or a failed pattern or format validator (`check: "string_format"`). | Send the canonical encoding (ISO-8601 UTC datetime, decimal string, base64 bytes) and satisfy the validator that check and params name. |
|
|
78
|
-
| [`V1004`](https://aventara-framework.vercel.app/docs/error-codes#v1004) | Value outside accepted range. | A number, length or size outside its range: a failed bound, length or size validator, a BigInt outside signed 64 bits, an AvZ.int() outside the safe integer range, or a transaction with no operations. | Send a value inside the range; check and params name the validator that failed. |
|
|
78
|
+
| [`V1004`](https://aventara-framework.vercel.app/docs/error-codes#v1004) | Value outside accepted range. | A number, length or size outside its range: a failed bound, length or size validator, a BigInt outside signed 64 bits, an AvZ.int() outside the safe integer range, or a transaction with no operations. Under A2004 it is also a value the database refused as too long or out of its range, a BigInt beyond what the data source's driver can bind among them (some bind at most ±(2^53 − 1)). | Send a value inside the range; check and params name the validator that failed. |
|
|
79
79
|
| [`V1005`](https://aventara-framework.vercel.app/docs/error-codes#v1005) | Unknown or inaccessible field. | A field that does not exist or is hidden in the caller's layer, an unknown Resource (under A2001), or a key a typed Json field's shape does not declare. | Use a field the caller's contract exposes, expose it through exemptions, or make the call from server code. |
|
|
80
80
|
| [`V1006`](https://aventara-framework.vercel.app/docs/error-codes#v1006) | Unsupported filter/order/mutation directive. | An operator, directive or nested write the field does not offer (at any depth, inside not too; a to-many $update's data takes only what a bulk write takes), a mode it does not offer, or an operation that is not available (under A2002). | Use what the contract lists for that field or Resource, or expose it through exemptions. |
|
|
81
81
|
| [`V1007`](https://aventara-framework.vercel.app/docs/error-codes#v1007) | Invalid unique identifier shape. | The where of a unique operation is not exactly one complete identifier. | Pass exactly one identifier: the id, or every field of one @unique or @@unique. |
|
|
@@ -100,10 +100,10 @@ Each entry of `cause.issues[]` under `A2001`, `A2002`, `A2004`, `A2007` or `A200
|
|
|
100
100
|
|
|
101
101
|
| Code | Meaning | Cause | Fix |
|
|
102
102
|
|---|---|---|---|
|
|
103
|
-
| [`CONFIG_REQUIRED`](https://aventara-framework.vercel.app/docs/error-codes#config_required) | A required configuration key is missing. | A key that must be present is absent: api, adapter, or an overlay's exposed when the overlay sets exemptions. | Add the key at the diagnostic's path. |
|
|
104
|
-
| [`CONFIG_UNKNOWN_PROPERTY`](https://aventara-framework.vercel.app/docs/error-codes#config_unknown_property) | Unknown configuration key. | A key the configuration does not define at that place: a typo, or a key written at the wrong level of the tree. | Remove it, or move it where it belongs (the configuration reference's "Where a key can go"). |
|
|
103
|
+
| [`CONFIG_REQUIRED`](https://aventara-framework.vercel.app/docs/error-codes#config_required) | A required configuration key is missing. | A key that must be present is absent: api, adapter, or an overlay's exposed when the overlay sets exemptions, or policy.engine or policy.principal when policy is set. | Add the key at the diagnostic's path. |
|
|
104
|
+
| [`CONFIG_UNKNOWN_PROPERTY`](https://aventara-framework.vercel.app/docs/error-codes#config_unknown_property) | Unknown configuration key. | A key the configuration does not define at that place: a typo, or a key written at the wrong level of the tree. `policy` and `prebuilt` are root-only keys; in an overlay they read as unknown. | Remove it, or move it where it belongs (the configuration reference's "Where a key can go"). |
|
|
105
105
|
| [`CONFIG_INVALID_TYPE`](https://aventara-framework.vercel.app/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
|
-
| [`CONFIG_INVALID_VALUE`](https://aventara-framework.vercel.app/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,
|
|
106
|
+
| [`CONFIG_INVALID_VALUE`](https://aventara-framework.vercel.app/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 (the message names the Resource or the rule). | Use a value the message allows. |
|
|
107
107
|
| [`CONFIG_MOVED_PROPERTY`](https://aventara-framework.vercel.app/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
108
|
| [`CONFIG_FORBIDDEN_IN_OVERLAY`](https://aventara-framework.vercel.app/docs/error-codes#config_forbidden_in_overlay) | A root-only key is written in an overlay. | A field definition in application, client or agent. | Define the field once at the root (`resources.<R>.fields.<f>.definition`); an overlay only chooses its visibility. |
|
|
109
109
|
|
package/README.md
CHANGED
|
@@ -170,10 +170,8 @@ after compute and see every required field present.
|
|
|
170
170
|
## Returning a façade from a method
|
|
171
171
|
|
|
172
172
|
A Nest project emits declarations (`declaration: true`), so a method without a return type has its type written out.
|
|
173
|
-
Every node of the typed façades has a name for that
|
|
174
|
-
|
|
175
|
-
`clientTx` (`AvClientFacade`, `AvAppTxResourceFacade`, …). The declaration writes the name, with the Contract view as
|
|
176
|
-
its argument, so this compiles:
|
|
173
|
+
Every node of the typed façades has a name for that, and so does the Contract view they are typed over, so a method can
|
|
174
|
+
return any of them without an annotation:
|
|
177
175
|
|
|
178
176
|
```ts
|
|
179
177
|
@Injectable()
|
|
@@ -183,21 +181,43 @@ export class PostsService {
|
|
|
183
181
|
posts() {
|
|
184
182
|
return this.framework.application.Post;
|
|
185
183
|
}
|
|
184
|
+
|
|
185
|
+
findMany() {
|
|
186
|
+
return this.framework.application.Post.find.many;
|
|
187
|
+
}
|
|
186
188
|
}
|
|
187
189
|
```
|
|
188
190
|
|
|
189
|
-
The
|
|
190
|
-
return type to keep it small: `AppFramework["application"]["Post"]` is written as that reference, about 0.2 KB.
|
|
191
|
+
The declaration file writes a short reference, under half a kilobyte per method at any schema size:
|
|
191
192
|
|
|
192
193
|
```ts
|
|
193
|
-
posts():
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
posts(): import("@aventara/core").AvApplicationResourceFacade<
|
|
195
|
+
import("@aventara/core").AvContractView<import("…/discovery.artifact.js").AvModel, never, "omit", never, "omit", never, never>,
|
|
196
|
+
"Post">;
|
|
197
|
+
findMany(): import("@aventara/core").AvApplicationProjectingCall<
|
|
198
|
+
import("@aventara/core").AvContractView<import("…/discovery.artifact.js").AvModel, never, "omit", never, "omit", never, never>,
|
|
199
|
+
"Post", { readonly family: "find"; readonly variant: "many" }>;
|
|
196
200
|
```
|
|
197
201
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
202
|
+
The names, one set per surface (`application`, `client`, `appTx`, `clientTx`):
|
|
203
|
+
|
|
204
|
+
| Node | `framework.application` | the others |
|
|
205
|
+
| --- | --- | --- |
|
|
206
|
+
| the whole surface | `AvApplicationFacade` | `AvClientFacade`, `AvAppTxFacade`, `AvClientTxFacade` |
|
|
207
|
+
| one Resource | `AvApplicationResourceFacade` | `AvClientResourceFacade`, … |
|
|
208
|
+
| 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` |
|
|
211
|
+
|
|
212
|
+
Each takes the view, `AvContractView`, as its first argument. The view's arguments are your model by its name, the root's
|
|
213
|
+
and the overlay's exposure as written, and the `fields` entries your configuration's `resources` blocks hold. A
|
|
214
|
+
configuration with no `fields` entries writes `never` there. One that has them writes those entries, stages erased, after
|
|
215
|
+
every reference: measured at about 23 KB per method over 50 Resources that each configure a field. Annotate the method
|
|
216
|
+
(`posts(): AppFramework["application"]["Post"]`) if that matters to you; the annotation is written as that reference.
|
|
217
|
+
|
|
218
|
+
You never need to write these names yourself. They exist so a declaration file can name what a method returns, and a
|
|
219
|
+
project that reads only your declarations types the method exactly as your Framework does. An application that publishes
|
|
220
|
+
no declarations can set `"declaration": false` and skip the question.
|
|
201
221
|
|
|
202
222
|
The configuration itself costs almost nothing to declare. The Prisma adapter's discovery artifact names the model's
|
|
203
223
|
type (`AvModel`), so the scaffold's `aventaraConfig()`, a `buildFramework` and its `AppFramework` alias write
|
|
@@ -310,6 +330,46 @@ It compiles to `framework.contracts.agent`, identified by its own hash, and `fra
|
|
|
310
330
|
endpoint. A host runs agent calls through `AvProtocol.bind(framework, { origin: "agent" })`, which validates them
|
|
311
331
|
exactly as client calls are validated, against the Agent Contract.
|
|
312
332
|
|
|
333
|
+
## A policy engine
|
|
334
|
+
|
|
335
|
+
`policy` hands authorization to an engine Aventara never imports. The engine answers three questions per
|
|
336
|
+
Resource — may the principal run this action (`can`), which rows (`scopeOf`, a `where` fragment), which fields
|
|
337
|
+
(`fieldsOf`) — and core enforces the answers on every client and agent request:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
const framework = await createFramework({
|
|
341
|
+
api: "/api",
|
|
342
|
+
adapter,
|
|
343
|
+
policy: {
|
|
344
|
+
engine, // satisfies AvPolicyEngine
|
|
345
|
+
principal: ({ $state }) => $state.user ?? null,
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
await framework.application.Note.find.many({}, { as: user }); // checked as `user`
|
|
350
|
+
await framework.application.Note.find.many({}); // server code: not checked
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
The read scope is applied at the adapter call, after every developer stage, to the root `where`, every to-many include
|
|
354
|
+
and its `$count`, and every relation filter. A field the engine governs (its `governed` list) is never in a default
|
|
355
|
+
projection; name it to get it, `select: ["$defaults", "email"]`. A refusal answers `403 A4002`.
|
|
356
|
+
|
|
357
|
+
Writes are checked too. A create asks `can` about each new row, whose rules may look rows up through `exists`. An
|
|
358
|
+
update or delete acts only on rows the caller may change and read; a single-row one that misses answers `A4002` when
|
|
359
|
+
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
|
|
361
|
+
(`can` asked about the row the update returned), is rolled back. Where `transactionsStyle` is `"interactive"`, a
|
|
362
|
+
checked write with a check after it (an update, a nested write) runs in a transaction of its own, and a create or
|
|
363
|
+
delete checked before it runs is one statement; where it is `"none"`, a write that could only be checked after it runs
|
|
364
|
+
is refused (for after conditions, the updates the engine lists in `afterChecks`).
|
|
365
|
+
|
|
366
|
+
Nested writes (`$create`, `$connect` and the other relation directives) run as one call inside that transaction. Before
|
|
367
|
+
it, every row they link is held to the right scope: a linked row that holds the reference must be one the caller may
|
|
368
|
+
change, one whose parent holds it one the caller may read. A to-many `$update` or `$delete` acts only on the rows the
|
|
369
|
+
caller may change or delete, as `update.many` does. After the call, every row they changed is checked as an update is
|
|
370
|
+
(the after check, the move-out re-read), and every row they created is read back as stored and asked `can`; one refusal
|
|
371
|
+
rolls the whole write back. Where `transactionsStyle` is `"none"`, a nested write under a policy is refused.
|
|
372
|
+
|
|
313
373
|
## License
|
|
314
374
|
|
|
315
375
|
PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
|
|
@@ -5,6 +5,13 @@ import type { AvAdapterModel } from "./adapter-model.js";
|
|
|
5
5
|
* Provider execution request after framework semantic validation.
|
|
6
6
|
* Operation identity and arguments remain provider-neutral; adapters receive
|
|
7
7
|
* only validated semantic argument objects, never raw transport input.
|
|
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.
|
|
8
15
|
*/
|
|
9
16
|
export type AvAdapterOperation<M extends AvAdapterModel = AvAdapterModel> = AvOperationIdentity & {
|
|
10
17
|
readonly resource: Extract<keyof M["resources"], string>;
|
|
@@ -30,3 +30,9 @@ export type ContractCompilationResult = {
|
|
|
30
30
|
* Agent Contract from the view of the layer it extends.
|
|
31
31
|
*/
|
|
32
32
|
export declare function compileContracts<M extends AvAdapterModel>(model: M, config: ResolvedFrameworkConfig<M>): Promise<ContractCompilationResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Binds Contracts loaded from a prebuilt file to the runtime pieces
|
|
35
|
+
* {@link compileContracts} binds to the ones it compiles, from the live
|
|
36
|
+
* `config` and `model` they were compiled from.
|
|
37
|
+
*/
|
|
38
|
+
export declare function bindPrebuiltContracts<M extends AvAdapterModel>(model: M, config: ResolvedFrameworkConfig<M>, contracts: AvContracts): void;
|
|
@@ -1002,6 +1002,7 @@ function projectCore(model, config, diagnostics) {
|
|
|
1002
1002
|
baseline.transactions = resolveTransactions(model, "root", scope, diagnostics);
|
|
1003
1003
|
checkResourceTree(model, baseline, "root", config.trees.root, config.definitions, diagnostics);
|
|
1004
1004
|
applyFieldDefinitions(model, baseline, config.definitions, diagnostics);
|
|
1005
|
+
markGovernedFields(baseline, config.policy?.governed);
|
|
1005
1006
|
applyCompute(baseline, scope.compute);
|
|
1006
1007
|
checkLayerValidators(baseline.resources, "root", config.trees.root, diagnostics);
|
|
1007
1008
|
const validators = compileValidatorTable(baseline.resources, scope.pipelines);
|
|
@@ -1014,6 +1015,27 @@ function projectCore(model, config, diagnostics) {
|
|
|
1014
1015
|
validateProjection(projection, "root", diagnostics);
|
|
1015
1016
|
return { baseline, projection, validators };
|
|
1016
1017
|
}
|
|
1018
|
+
function markGovernedFields(baseline, governed) {
|
|
1019
|
+
if (governed === undefined) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
for (const [resourceName, names] of Object.entries(governed)) {
|
|
1023
|
+
const resource = Object.hasOwn(baseline.resources, resourceName)
|
|
1024
|
+
? baseline.resources[resourceName]
|
|
1025
|
+
: undefined;
|
|
1026
|
+
if (resource === undefined) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
for (const name of names) {
|
|
1030
|
+
const field = Object.hasOwn(resource.fields, name)
|
|
1031
|
+
? resource.fields[name]
|
|
1032
|
+
: undefined;
|
|
1033
|
+
if (field !== undefined) {
|
|
1034
|
+
field.governed = true;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1017
1039
|
function projectDerivedScope(model, scopeName, core, parentView, config, diagnostics) {
|
|
1018
1040
|
const scope = config[scopeName];
|
|
1019
1041
|
const baseline = cloneValue(core.baseline);
|
|
@@ -1095,18 +1117,57 @@ export async function compileContracts(model, config) {
|
|
|
1095
1117
|
};
|
|
1096
1118
|
}
|
|
1097
1119
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1120
|
+
const contracts = { core, application, client, agent };
|
|
1121
|
+
bindRuntimePieces(contracts, compilerConfig.definitions, {
|
|
1122
|
+
core: coreCompilation.validators,
|
|
1123
|
+
application: applicationProjection.validators,
|
|
1124
|
+
client: clientProjection.validators,
|
|
1125
|
+
agent: agentProjection.validators,
|
|
1126
|
+
});
|
|
1127
|
+
return { ok: true, contracts };
|
|
1128
|
+
}
|
|
1129
|
+
function bindRuntimePieces(contracts, definitions, validators) {
|
|
1130
|
+
for (const origin of ["core", "application", "client", "agent"]) {
|
|
1131
|
+
bindShapedFields(contracts[origin], definitions);
|
|
1132
|
+
bindFieldValidators(contracts[origin], validators[origin]);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
export function bindPrebuiltContracts(model, config, contracts) {
|
|
1136
|
+
const compilerConfig = config;
|
|
1137
|
+
const fields = validatedFieldsView(model, compilerConfig.definitions);
|
|
1138
|
+
bindRuntimePieces(contracts, compilerConfig.definitions, {
|
|
1139
|
+
core: compileValidatorTable(fields, compilerConfig.root.pipelines),
|
|
1140
|
+
application: compileValidatorTable(fields, compilerConfig.application.pipelines),
|
|
1141
|
+
client: compileValidatorTable(fields, compilerConfig.client.pipelines),
|
|
1142
|
+
agent: compileValidatorTable(fields, compilerConfig.agent.pipelines),
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
function validatedFieldsView(model, definitions) {
|
|
1146
|
+
const resources = { ...model.resources };
|
|
1147
|
+
for (const [resourceName, resourceDefinitions] of Object.entries(definitions)) {
|
|
1148
|
+
const resource = Object.hasOwn(model.resources, resourceName)
|
|
1149
|
+
? model.resources[resourceName]
|
|
1150
|
+
: undefined;
|
|
1151
|
+
if (resource === undefined) {
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
let added;
|
|
1155
|
+
for (const [fieldName, definition] of Object.entries(resourceDefinitions)) {
|
|
1156
|
+
if (Object.hasOwn(resource.fields, fieldName)) {
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
added ??= {};
|
|
1160
|
+
added[fieldName] = {
|
|
1161
|
+
kind: "scalar",
|
|
1162
|
+
type: definition.type,
|
|
1163
|
+
list: definition.list,
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
if (added !== undefined) {
|
|
1167
|
+
resources[resourceName] = { fields: { ...resource.fields, ...added } };
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return resources;
|
|
1110
1171
|
}
|
|
1111
1172
|
async function identifiedContract(projection) {
|
|
1112
1173
|
const members = canonicalizeContractMembers({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { emitFrameworkWarning } from "../runtime/framework-warning.emitter.js";
|
|
1
2
|
import { computeOfLayer } from "./field-compute.resolver.js";
|
|
2
3
|
function layerComputedFields(resources) {
|
|
3
4
|
const result = {};
|
|
@@ -68,13 +69,5 @@ export function warnOnStaleComputedFields(generated, configured) {
|
|
|
68
69
|
const message = `generated types are stale: run aventara:prepare. The computed fields the pre-compute stages are typed with differ from the configuration's:\n${drift
|
|
69
70
|
.map((line) => ` - ${line}`)
|
|
70
71
|
.join("\n")}`;
|
|
71
|
-
|
|
72
|
-
if (typeof emitWarning === "function") {
|
|
73
|
-
emitWarning(message, {
|
|
74
|
-
type: "AventaraWarning",
|
|
75
|
-
code: STALE_GENERATED_TYPES_WARNING,
|
|
76
|
-
});
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
globalThis.console?.warn(`AventaraWarning: ${message}`);
|
|
72
|
+
emitFrameworkWarning(message, STALE_GENERATED_TYPES_WARNING);
|
|
80
73
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export const CONFIG_DIAGNOSTIC_GUIDANCE = {
|
|
2
2
|
CONFIG_REQUIRED: {
|
|
3
3
|
message: "A required configuration key is missing.",
|
|
4
|
-
cause: "A key that must be present is absent: api, adapter, or an overlay's exposed when the overlay sets exemptions.",
|
|
4
|
+
cause: "A key that must be present is absent: api, adapter, or an overlay's exposed when the overlay sets exemptions, or policy.engine or policy.principal when policy is set.",
|
|
5
5
|
fix: "Add the key at the diagnostic's path.",
|
|
6
6
|
},
|
|
7
7
|
CONFIG_UNKNOWN_PROPERTY: {
|
|
8
8
|
message: "Unknown configuration key.",
|
|
9
|
-
cause: "A key the configuration does not define at that place: a typo, or a key written at the wrong level of the tree.",
|
|
9
|
+
cause: "A key the configuration does not define at that place: a typo, or a key written at the wrong level of the tree. `policy` and `prebuilt` are root-only keys; in an overlay they read as unknown.",
|
|
10
10
|
fix: 'Remove it, or move it where it belongs (the configuration reference\'s "Where a key can go").',
|
|
11
11
|
},
|
|
12
12
|
CONFIG_INVALID_TYPE: {
|
|
@@ -16,7 +16,7 @@ export const CONFIG_DIAGNOSTIC_GUIDANCE = {
|
|
|
16
16
|
},
|
|
17
17
|
CONFIG_INVALID_VALUE: {
|
|
18
18
|
message: "A configuration value is not allowed.",
|
|
19
|
-
cause: 'A malformed api or mcp path, a negative limit, an unknown transactionsStyle, a format that is not a named zod format,
|
|
19
|
+
cause: '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 (the message names the Resource or the rule).',
|
|
20
20
|
fix: "Use a value the message allows.",
|
|
21
21
|
},
|
|
22
22
|
CONFIG_MOVED_PROPERTY: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AvAdapter } from "../adapters/adapter.interface.js";
|
|
2
2
|
import type { AvAdapterModel } from "../adapters/adapter-model.js";
|
|
3
3
|
import type { AvTransactionCapability } from "../contracts/transaction-capability.js";
|
|
4
|
+
import type { AvPolicyEngine } from "../policy/policy-engine.js";
|
|
4
5
|
import type { AvDiagnosticsSink } from "../runtime/diagnostics-reporter.js";
|
|
5
6
|
import type { AvMisuseSink } from "../runtime/misuse-reporter.js";
|
|
6
7
|
import type { AvConfigAgentParent } from "./config-layer.js";
|
|
@@ -8,6 +9,17 @@ import { type ResolvedComputeConfig } from "./field-compute.resolver.js";
|
|
|
8
9
|
import type { AvConfig, AvConfigLimits } from "./framework-config.js";
|
|
9
10
|
import type { AvConfigFieldDefinition } from "./framework-field-definition.js";
|
|
10
11
|
import { type ResolvedPipelineTree } from "./pipeline-tree.resolver.js";
|
|
12
|
+
/** The root's `policy`, its engine's passive lists read once. */
|
|
13
|
+
export type ResolvedPolicyConfig = {
|
|
14
|
+
readonly engine: AvPolicyEngine;
|
|
15
|
+
readonly principal: NonNullable<AvConfig["policy"]>["principal"];
|
|
16
|
+
/** The governed fields and relations, by Resource. */
|
|
17
|
+
readonly governed: Readonly<Record<string, readonly string[]>>;
|
|
18
|
+
/** The rule paths, by rule name, each a list of segments. */
|
|
19
|
+
readonly paths: Readonly<Record<string, readonly (readonly string[])[]>>;
|
|
20
|
+
/** The writes whose grants carry an `after` condition, by Resource. */
|
|
21
|
+
readonly afterChecks: Readonly<Record<string, readonly string[]>>;
|
|
22
|
+
};
|
|
11
23
|
/** The root's field definitions, by Resource and field name. */
|
|
12
24
|
export type ResolvedFieldDefinitions = Readonly<Record<string, Readonly<Record<string, AvConfigFieldDefinition>>>>;
|
|
13
25
|
/** One resolved layer of the configuration: root, application, client or agent. */
|
|
@@ -63,6 +75,8 @@ export type ResolvedFrameworkConfig<M extends AvAdapterModel = AvAdapterModel> =
|
|
|
63
75
|
readonly diagnostics: AvDiagnosticsSink | undefined;
|
|
64
76
|
/** The misuse sink, from the root configuration only. */
|
|
65
77
|
readonly misuse: AvMisuseSink | undefined;
|
|
78
|
+
/** The policy, from the root configuration only; absent, nothing is checked. */
|
|
79
|
+
readonly policy: ResolvedPolicyConfig | undefined;
|
|
66
80
|
/** Every field definition, from the root only; the same for all three layers. */
|
|
67
81
|
readonly definitions: ResolvedFieldDefinitions;
|
|
68
82
|
/** Each layer's `resources` tree as authored: the compiler checks its names. */
|