@aventara/core 1.0.0-rc.3 → 1.0.0-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +231 -1
  2. package/ERRORS.md +6 -6
  3. package/README.md +76 -12
  4. package/dist/adapters/adapter-field.d.ts +8 -0
  5. package/dist/adapters/adapter-identifier.d.ts +9 -0
  6. package/dist/adapters/adapter-identifier.js +6 -1
  7. package/dist/adapters/adapter-operation.d.ts +13 -0
  8. package/dist/compiler/contract.compiler.d.ts +6 -0
  9. package/dist/compiler/contract.compiler.js +99 -15
  10. package/dist/config/computed-fields.js +2 -9
  11. package/dist/config/config-diagnostic.guidance.js +3 -3
  12. package/dist/config/config.resolver.d.ts +14 -0
  13. package/dist/config/config.resolver.js +31 -0
  14. package/dist/config/config.validator.js +82 -0
  15. package/dist/config/exact-framework-config.d.ts +5 -0
  16. package/dist/config/framework-config.d.ts +30 -1
  17. package/dist/contracts/client-contract-structure.validator.d.ts +8 -0
  18. package/dist/contracts/client-contract-structure.validator.js +6 -1
  19. package/dist/contracts/contract.d.ts +15 -1
  20. package/dist/contracts/field.d.ts +28 -0
  21. package/dist/contracts/scalar-value-type.d.ts +3 -1
  22. package/dist/framework/contract-view.d.ts +41 -10
  23. package/dist/framework/framework-construction.error.d.ts +2 -1
  24. package/dist/framework/framework-construction.error.js +11 -1
  25. package/dist/framework/framework.d.ts +4 -4
  26. package/dist/framework/framework.factory.js +39 -11
  27. package/dist/framework/operation-facade.d.ts +29 -10
  28. package/dist/framework/operation-facade.js +2 -1
  29. package/dist/index.d.ts +10 -4
  30. package/dist/internal.d.ts +2 -2
  31. package/dist/internal.js +2 -0
  32. package/dist/operations/operation-arguments.d.ts +44 -28
  33. package/dist/operations/operation-call.d.ts +9 -5
  34. package/dist/operations/operation-result.d.ts +6 -4
  35. package/dist/operations/operation-validator-core.js +6 -0
  36. package/dist/policy/policy-admission.d.ts +22 -0
  37. package/dist/policy/policy-admission.js +409 -0
  38. package/dist/policy/policy-after-update.runtime.d.ts +29 -0
  39. package/dist/policy/policy-after-update.runtime.js +132 -0
  40. package/dist/policy/policy-condition.d.ts +15 -0
  41. package/dist/policy/policy-condition.js +36 -0
  42. package/dist/policy/policy-engine.d.ts +118 -0
  43. package/dist/policy/policy-engine.js +1 -0
  44. package/dist/policy/policy-nested-write.runtime.d.ts +38 -0
  45. package/dist/policy/policy-nested-write.runtime.js +453 -0
  46. package/dist/policy/policy-read.runtime.d.ts +81 -0
  47. package/dist/policy/policy-read.runtime.js +503 -0
  48. package/dist/policy/policy-request.d.ts +41 -0
  49. package/dist/policy/policy-request.js +63 -0
  50. package/dist/policy/policy-session.d.ts +93 -0
  51. package/dist/policy/policy-session.js +235 -0
  52. package/dist/policy/policy-startup.validator.d.ts +8 -0
  53. package/dist/policy/policy-startup.validator.js +104 -0
  54. package/dist/policy/policy-write.runtime.d.ts +37 -0
  55. package/dist/policy/policy-write.runtime.js +409 -0
  56. package/dist/prebuilt/core-version.generated.d.ts +2 -0
  57. package/dist/prebuilt/core-version.generated.js +1 -0
  58. package/dist/prebuilt/prebuilt-changes.d.ts +28 -0
  59. package/dist/prebuilt/prebuilt-changes.js +65 -0
  60. package/dist/prebuilt/prebuilt-contracts.d.ts +40 -0
  61. package/dist/prebuilt/prebuilt-contracts.emitter.d.ts +16 -0
  62. package/dist/prebuilt/prebuilt-contracts.emitter.js +50 -0
  63. package/dist/prebuilt/prebuilt-contracts.js +1 -0
  64. package/dist/prebuilt/prebuilt-contracts.loader.d.ts +21 -0
  65. package/dist/prebuilt/prebuilt-contracts.loader.js +77 -0
  66. package/dist/prebuilt/prebuilt-inputs.fingerprint.d.ts +16 -0
  67. package/dist/prebuilt/prebuilt-inputs.fingerprint.js +192 -0
  68. package/dist/protocol/absent-route.responder.d.ts +4 -4
  69. package/dist/protocol/http-request.decoder.d.ts +2 -2
  70. package/dist/protocol/http-request.reader.d.ts +2 -2
  71. package/dist/protocol/protocol-headers.d.ts +3 -3
  72. package/dist/runtime/computed-read-runtime.d.ts +6 -1
  73. package/dist/runtime/computed-read-runtime.js +226 -52
  74. package/dist/runtime/context-executor.runtime.d.ts +2 -2
  75. package/dist/runtime/context-executor.runtime.js +4 -2
  76. package/dist/runtime/create-requirement.js +1 -1
  77. package/dist/runtime/diagnostics-reporter.d.ts +5 -5
  78. package/dist/runtime/execution-options.d.ts +8 -0
  79. package/dist/runtime/framework-warning.emitter.d.ts +6 -0
  80. package/dist/runtime/framework-warning.emitter.js +8 -0
  81. package/dist/runtime/level-lifecycle.runtime.js +3 -2
  82. package/dist/runtime/operation-code.guidance.js +3 -3
  83. package/dist/runtime/operation-request.d.ts +16 -9
  84. package/dist/runtime/operation-runtime.d.ts +15 -7
  85. package/dist/runtime/operation-runtime.js +139 -14
  86. package/dist/runtime/owned-execution.registry.d.ts +2 -2
  87. package/dist/runtime/readonly-value.d.ts +5 -1
  88. package/dist/runtime/readonly-value.js +67 -28
  89. package/dist/runtime/referential-action.d.ts +1 -1
  90. package/dist/transactions/sql-statement.d.ts +2 -2
  91. package/dist/transactions/transaction-plan-assembler.d.ts +2 -2
  92. package/dist/transactions/transaction-plan-validator.d.ts +5 -5
  93. package/dist/transactions/transaction-request.d.ts +8 -8
  94. package/dist/transactions/transaction-runtime.d.ts +5 -2
  95. package/dist/transactions/transaction-runtime.js +33 -6
  96. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,235 @@
1
1
  # @aventara/core
2
2
 
3
+ ## 1.0.0-rc.8
4
+
5
+ 1.0.0-rc.5, rc.6 and rc.7 were tagged but never published; 1.0.0-rc.8 contains everything listed under them and the changes below.
6
+
7
+ ### Patch Changes
8
+
9
+ - 1c8e4ee: Helpers typed off the façades compile again in projects that emit declarations. In 1.0.0-rc.7 a function that took
10
+ `Parameters<AppFramework["application"]["Post"]["find"]["many"]>[0]` and returned its `where` (or built a new one
11
+ from it, `{ AND: [args.where ?? {}, { published: { equals: true } }] }`) without a return type failed `nest build`
12
+ (and `tsc` with `"declaration": true`) with `TS4023: ... has or is using name 'ScalarFilterOperands' ... but cannot be
13
+ named`, usually beside `TS7056`. A nested write's data could fail the same way, or name `NO_ADVERTISED_MEMBER`, and a
14
+ nested `$update`'s `where` was written into the `.d.ts` as `any`. The declaration now names the argument types
15
+ `@aventara/core` exports (`AvWhereInput`, `AvUpdateDataInput`, `AvCreateDataInput`, ...), and these new exports cover
16
+ the rest:
17
+
18
+ - `AvScalarFilterOperands`: one field's filter operators, which a filter's `not` holds again.
19
+ - `AvScalarWhereInput`: the `where` of a nested `$update` or `$delete` that only takes plain fields.
20
+ - `AvNoAdvertisedMember`: a write member that advertises nothing; it takes `{}` and refuses every key.
21
+ - `AvSqlStatementBuilder` and `AvSqlStatementValue`: the type of `framework.appTx.$sql` and the values it binds.
22
+ A method returning `framework.appTx.$sql` failed with `TS2883`/`TS2742` before.
23
+
24
+ `framework.execute` and `framework.executeTransaction` take request types that `@aventara/core` exports:
25
+ `AvOperationRequest` (`AvApplicationOperationRequest`, `AvClientOperationRequest`, `AvAgentOperationRequest`),
26
+ `AvTransactionRequest` (`AvApplicationTransactionRequest`, `AvClientTransactionRequest`,
27
+ `AvAgentTransactionRequest`) and `AvRequestProtocol` (a remote request's `protocol`). They were named from
28
+ `@aventara/core/internal` (`FrameworkOperationRequest`, `FrameworkTransactionRequest`, ...), so a declaration that
29
+ spelled them reached into the package's internal entry. Those names are gone from `@aventara/core/internal`.
30
+
31
+ An adapter's `referentialActions` edges name the fields that hold the reference `referenceFields`, as a relation of
32
+ the Core Contract does. They were called `references`, which the Core Contract uses for the target's fields.
33
+
34
+ Reading a projecting variant's argument type (`Parameters<…["find"]["many"]>[0]`) also type-checks faster: about
35
+ 6,000 type instantiations over a three-field model, where rc.7 took about 29,000. The part of the argument other than
36
+ `select`/`include` is now marked `NoInfer` in the call's parameter, so an editor shows that parameter as
37
+ `NoInfer<Omit<…>> & …`. What it accepts is unchanged.
38
+
39
+ A create's `data` type now states each required field once, as a required member (`Readonly<{ title: string; } & {
40
+ body?: ... }>` in an editor's expanded hover). Before, a required field appeared as an optional member beside a
41
+ separate `{ readonly title: WrittenValue }`, and a declaration file wrote both. What a create accepts and refuses is
42
+ unchanged.
43
+
44
+ - Operation results read as their value types in an editor and in emitted declarations. A result field that is nullable,
45
+ an enum or a `boolean` (such as a `Boolean @default(false)` column) was shown as `AvScalarValueType<…>`, with the
46
+ whole Contract view as its argument, and a project with `"declaration": true` wrote that view into its `.d.ts` once
47
+ per such field: a Nest service of five methods with no return types came out at 524 lines. Those fields now read
48
+ `body: string | null`, `published: boolean`, `publishedAt: Date | null`, and that service's declaration is 44 lines.
49
+ A result, and a relation included in it, is shown as its fields rather than as `ProjectedFields<…>`. This covers
50
+ `framework.application` and `framework.client` results, the same fields in a write's `data`, and clients generated
51
+ by `avclient`. What each type accepts is unchanged.
52
+
53
+ ## 1.0.0-rc.7
54
+
55
+ 1.0.0-rc.5 and 1.0.0-rc.6 were tagged but never published; 1.0.0-rc.7 contains everything listed under them and the changes below.
56
+
57
+ ### Patch Changes
58
+
59
+ - f917c1c: A helper that reads `framework.contracts.core` compiles again in projects that emit declarations. In 1.0.0-rc.6 a
60
+ function returning a Resource or a field of the Core Contract without a return type, such as
61
+ `core.resources[resource]?.fields[relation]`, failed `nest build` (and `tsc` with `"declaration": true`, which Nest's
62
+ `tsconfig.json` sets) with `TS2883: The inferred type of 'relationOf' cannot be named without a reference to
63
+ 'AvCoreField'` under TypeScript 6 (`TS2742` under 5.9), or with `TS4058`/`TS4023` naming `AvCoreResource`.
64
+ `@aventara/core` now exports the three types the Core Contract is written with: `AvCoreResource` (a Resource),
65
+ `AvCoreField` (one of its fields) and `AvCoreRelationField` (a relation field, with the target fields its reference
66
+ names in `references`).
67
+
68
+ A policy's startup check states the path once: `CONFIG_INVALID_VALUE at policy.engine.governed.Nope: "Nope" is no
69
+ Resource of the Core Contract`, where rc.6 repeated `policy.engine.governed.Nope:` inside the message. Each
70
+ diagnostic's `message` no longer starts with its `path`; read the path from `path`.
71
+
72
+ ## 1.0.0-rc.6
73
+
74
+ 1.0.0-rc.5 was tagged but never published. 1.0.0-rc.6 contains everything listed under 1.0.0-rc.5 and the fix below.
75
+
76
+ ### Patch Changes
77
+
78
+ - 1cf72e0: Typed policy engines compile again in projects that emit declarations. In 1.0.0-rc.5 an engine written
79
+ `as const satisfies AvPolicyEngine` failed `nest build` (and `tsc` with `"declaration": true`, which Nest's
80
+ `tsconfig.json` sets) with `TS2883: The inferred type of 'policyEngine' cannot be named without a reference to
81
+ 'PolicyFieldsAccess'` under TypeScript 6, or `TS2742` under 5.9, and so did the configuration and the framework holding
82
+ it. `fieldsOf`'s question is now written out in full in `AvPolicyEngine`, so a declaration can spell it.
83
+
84
+ A read question's `action` is now typed `undefined`, so an engine can destructure it from either question:
85
+ `fieldsOf({ resource, access, action })`. A write question's `action` is `"create"` or `"update"`, as before.
86
+
87
+ ## 1.0.0-rc.5
88
+
89
+ ### Patch Changes
90
+
91
+ - 4795ff1: Two additions for policy engines. A Framework without a policy, and every Contract a caller is served, are unchanged.
92
+
93
+ - A policy engine can check a create whose row reaches another Resource through a relation. Discovery now states, on the end of a relation that holds the reference, which of the target's fields its reference fields name, pair by pair (`Note.campaign`: `referenceFields: ["campaignId"]`, `references: ["id"]`). An engine reads them from the model it is built from (a Prisma discovery artifact's `model`) or from `framework.contracts.core`, and asks one `exists` about the row they name: "a player creates a note only in a campaign they play in". Only the adapter's model and the Core Contract carry them. The Application, Client and Agent Contracts, `/_contract`, the MCP tools and every ClientContract hash are unchanged. The discovery artifact's model hash, and so a prebuilt Contracts file's fingerprint, change: regenerate the artifact and rebuild the prebuilt file.
94
+ - `fieldsOf({ access: "write" })` also names the action that writes the record, `action: "create" | "update"` (an upsert's branches each their own, a nested record its directive's), so an engine can lock a field on update and still let a create write it. A read question names none. An engine that ignores it keeps working.
95
+
96
+ - 9e178ca: Fixes for applications that check requests with a policy engine. A Framework without a policy behaves as before.
97
+
98
+ - A cursor on a row the caller cannot read is answered exactly as a cursor on a row that does not exist, at the root and in a to-many relation, whatever values the caller sends beside the identifier. Before, the page started at the hidden row, so it told that the row exists and where its values sort. The Prisma adapter reads the cursor's row under the read scope first: one statement more for a cursor on a readable row, and only that read for one on a row the caller cannot read.
99
+ - A unique update with a nested write (`$create`, `$connect`, …) on a row the caller can read but not change answers `A4002`, as the same update without one does. It answered `A2003`. A row the caller cannot read still answers `A2003`.
100
+ - The `exists` questions a policy asks select the Resource's primary identifier, never another unique field such as a governed `email`.
101
+ - `policy.principal` may answer with a promise. A synchronous principal works as before; a rejection answers `A3000`, as a throw does.
102
+ - `AvConstructionError.message` names the first problem and how many more there are, for example `Aventara framework configuration validation failed: CONFIG_INVALID_TYPE at adapter.execute: adapter.execute must be a function (+2 more)`. `stage` and `diagnostics` are unchanged.
103
+
104
+ ## 1.0.0-rc.4
105
+
106
+ ### Patch Changes
107
+
108
+ - e3d03a7: The error catalog says more about policies. `A2003` now names the cases a policy adds: a unique read of a row outside the
109
+ caller's read scope, and a single-row update or delete of a row the caller cannot read, are misses, while a row the
110
+ caller can read but not change answers `A4002`. `CONFIG_REQUIRED`, `CONFIG_UNKNOWN_PROPERTY` and `CONFIG_INVALID_VALUE`
111
+ name the `policy` key's rules: `policy.engine` and `policy.principal` are required, `policy` and `prebuilt` are root-only,
112
+ and the engine's `governed`, `paths` and `afterChecks` lists must name what the Core Contract has. Wording only; no
113
+ behaviour changes.
114
+ - d10639a: Faster reads. The read-only copies stages are told cost about half what they did: a read with a computed field
115
+ copies each row in one pass, a row with several computed fields shares its nested objects between the stages'
116
+ copies, and after hooks share one frozen copy of the result. A row whose computed field returns a value at once no
117
+ longer waits a turn per row. The MCP endpoint builds its JSON Schema validator once instead of on every request.
118
+ What stages are told, the order they run in and what a request answers are unchanged.
119
+ - f67e68c: A service method can return any façade node or variant without a return type. Under `declaration: true` (a Nest
120
+ build) the declaration file now writes a short reference, under half a kilobyte per method at any schema size, where it
121
+ wrote the whole Contract view (about 440 KB per method over 50 Resources). A method returning a variant
122
+ (`this.framework.application.Post.find.many`) no longer needs an annotation to compile, and one returning a transaction
123
+ variant (`this.framework.appTx.Post.create.one`) no longer runs the compiler out of memory.
124
+
125
+ New type names, exported from `@aventara/core` so a declaration can write them (you never need to write them yourself):
126
+
127
+ - `AvContractView`, the Contract view the façades are typed over;
128
+ - one call type per surface and shape: `AvApplicationProjectingCall`, `AvApplicationPlainCall`,
129
+ `AvClientProjectingCall`, `AvClientPlainCall`, `AvAppTxProjectingCall`, `AvAppTxPlainCall`,
130
+ `AvClientTxProjectingCall`, `AvClientTxPlainCall`.
131
+
132
+ A configuration with `fields` entries still writes those entries after each reference; annotate the method if that
133
+ matters (see the README's "Returning a façade from a method").
134
+
135
+ - 708556c: Policy engines check nested writes. A create, update or upsert whose data writes a relation through a directive
136
+ (`$create`, `$connect`, `$connectOrCreate`, `$update`, `$upsert`, `$delete`, `$disconnect`, `$set`) now runs under a
137
+ policy, where before it was refused. It runs as one adapter call inside the write's interactive transaction, and one
138
+ failed check rolls the whole write back with `403 A4002`.
139
+
140
+ - Every row the nested write creates is read back as stored (its parent's reference filled, database defaults
141
+ present) and asked `can({ action: "create", row })`, its `exists` lookups seeing the rows the same call wrote. A
142
+ create whose own data holds a nested write is asked about its row the same way, after the insert.
143
+ - A row a directive links or unlinks is held to the scope of the row that changes: a linked row that holds the
144
+ 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
145
+ update scope, one whose parent holds the reference (a to-one relation's target) in its read scope. One `exists`
146
+ per Resource answers it, inside the transaction. A linked row that changes is an update: after the call it gets
147
+ the after check (`can({ action: "update", row })`) and the move-out re-read. Linking a new row to a one-to-one's
148
+ back end unlinks the old one, which is held the same way.
149
+ - A to-many nested `$update` or `$delete` acts only on the rows within the update (or delete) scope, as
150
+ `update.many` and `delete.many` do: core reads those rows' identifiers inside the transaction and hands the
151
+ adapter `where: { id: { in: [...] } }`. A to-one one refuses the write when its row is outside the scope. A nested
152
+ `$update`'s changed rows get the after check and the move-out re-read after the call.
153
+ - The fields a nested record sends are held to its Resource's `fieldsOf({ access: "write" })`, and a relation written
154
+ through a directive to the parent's grant, its reference fields with it.
155
+ - Where the operation's `transactionsStyle` is `"none"`, a nested write under a policy is refused before it runs.
156
+
157
+ - 3a0dfca: A write the policy checks costs fewer database round trips. What each check decides and refuses is unchanged.
158
+
159
+ - A single-row update or delete carries the caller's scopes inside its own `where` and runs as one statement, with no
160
+ read of its row before it. One that matches nothing still answers `A4002` when the caller can read the row and
161
+ `A2003` when it cannot, with one lookup on that miss only. The Prisma adapter sends it as
162
+ `update({ where: { id, AND: [scope] } })`.
163
+ - A create asks `can({ action: "create", row })` and its `exists` lookups before the insert, without a transaction. A
164
+ transaction around them never closed the race between the lookup and the insert at the database's default
165
+ isolation, and cost a BEGIN and a COMMIT.
166
+ - A transaction opens only when a check after the write may roll it back: an update's after check or move-out, a
167
+ created row held to a write grant, a nested write, or returned rows checked by `exists`. A plain checked create or
168
+ delete is one statement, its interceptors and hooks outside any transaction.
169
+ - The after check asks `can` about the row the update returned, so it reads nothing again. Only a move-out reads the
170
+ changed rows again. An `update.count` runs as the returning `update.many` when the Contract offers it.
171
+
172
+ Round trips per request on PostgreSQL: `create.one` 3 → 1, `delete.unique` 4 → 1, `update.unique` 5 → 3,
173
+ `update.many` 4 → 3, `update.count` 5 → 3. Reads, and a Framework without a policy, are unchanged.
174
+
175
+ - 99169d2: Policy engines, reads first. `createFramework` takes an optional root `policy: { engine, principal }`: an engine that
176
+ answers three questions (`can`, `scopeOf`, `fieldsOf`, each now or as a promise), and a function that derives a
177
+ request's principal from its `ctx.$state`. Core never imports an engine; one satisfies `AvPolicyEngine` by its shape,
178
+ and a scope or grant is a fragment of Aventara's own `where` (`AvPolicyWhere`).
179
+
180
+ - Client and agent requests are checked (an MCP tool call is an agent request). An application call is checked only
181
+ when it names a principal: `framework.application.Note.find.many(args, { as: user })`. A call a stage makes through
182
+ `ctx` never is.
183
+ - Before any guard, `can` and the fields the request names or receives (`fieldsOf`); a refusal is `403 A4002`. At the
184
+ adapter call, after every developer stage, the read scope is ANDed into the root `where`, every to-many include and
185
+ its `$count`, and every relation filter, at every level. A to-one include of a row out of scope refuses the request;
186
+ it is never nulled.
187
+ - Governed fields (the engine's `governed` list) are left out of every default projection and carry `governed: true`
188
+ in the Contracts. Name one to get it: `select: ["$defaults", "email"]`, where `$defaults` is the level's default
189
+ projection. It is refused where a returned row does not grant it, and a filter or sort by it is checked before the
190
+ query runs, so it cannot reveal values the caller may not read.
191
+ - The engine's `governed` and `paths` lists are checked against the Core Contract at startup. A rule's paths are lists
192
+ of segments, as other paths in core are: `["Note", "campaign", "members", "some", "userId", "equals"]`.
193
+ - Writes are checked as well; see the policy writes entry.
194
+
195
+ Without `policy`, nothing changes: the Contracts and their hashes are the same bytes.
196
+
197
+ - 0fd2608: A write the policy checks costs less CPU after the round-trip changes. The operation core hands its adapter no longer
198
+ carries the request's policy session, so an adapter that keeps its operations no longer keeps each request's session
199
+ with them. The fields a write returns only for the after check are dropped as soon as the check has read them, so a
200
+ read compute stage is told the row the request asked for, not every scalar field. What each check decides, the
201
+ fields `can` is asked about, and the database round trips are unchanged.
202
+ - 154e440: Policy engines check writes. Every create, update, delete and upsert a client or agent makes (or an application call
203
+ made `{ as }` a principal) is now checked by the configuration's `policy`, where before it was refused.
204
+
205
+ - The fields a write sends are held to `fieldsOf({ access: "write" })` before any guard; a field outside it answers
206
+ `403 A4002`. A field writable on some rows only is checked against every row an update reaches, and against the
207
+ row a create inserts, after the insert and inside its transaction.
208
+ - A create asks `can({ action: "create", row })` about each new row on its final data. Its rules can look rows up
209
+ through `exists`, which runs inside the write's transaction, so a plan that creates a campaign and then its first
210
+ note sees the campaign.
211
+ - An update or delete acts only on rows the caller may both change (`scopeOf` for the action) and read. A single-row
212
+ one that matches nothing answers `A4002` when the caller can read the row, and `A2003` when it cannot, so the
213
+ answer does not reveal that the row exists. A `many` or `count` one answers for the caller's rows only.
214
+ - An update that moves a row out of what the caller reads is rolled back with `A4002`. So is one that fails an `after`
215
+ condition: inside the transaction core asks `can({ action: "update", row })` about each changed row of every
216
+ checked update. The engine's new `afterChecks` member (`{ Note: ["update"] }`) names the updates whose grants carry
217
+ such a condition, which are the ones refused where there is no interactive transaction.
218
+ - Where the operation's `transactionsStyle` is `"interactive"`, a checked write runs in a transaction of its own. Where
219
+ it is `"none"` (Cloudflare D1), a write that can only be checked after it runs is refused before it runs; every other
220
+ write runs, a create looking up before it inserts, and an unlisted update without the after check.
221
+ - Nested writes (a relation directive in a write's data) are refused under a policy for now.
222
+
223
+ `afterChecks` is checked against the Core Contract at startup and changes no Contract and no prebuilt fingerprint.
224
+
225
+ - 2fcffdb: Prebuilt Contracts. `createFramework` takes an optional `prebuilt` key: the four Contracts compiled ahead of time (type
226
+ `AvPrebuiltContracts`), so a serverless or edge cold start loads them instead of compiling them. A file is used only
227
+ when this core compiled it, for this protocol, from the same configuration and adapter model; its Contracts, hashes
228
+ and `/_contract` bytes are then the ones compiling would give. Any other file is ignored whole: the framework compiles
229
+ at startup exactly as without one and warns once (`AVENTARA_PREBUILT_STALE`), naming the Resources whose configuration
230
+ or model changed, or the version that differs. Without `prebuilt`, nothing changes. The build step that writes the
231
+ file arrives with `@aventara/build`.
232
+
3
233
  ## 1.0.0-rc.3
4
234
 
5
235
  ### Patch Changes
@@ -27,7 +257,7 @@
27
257
  on an `AvComputedValueError`, with nothing of the hidden field in the answer. Before, a shaped Json field hidden from
28
258
  the layer was not judged at all, and a hidden field's own validators answered the caller `A2004` with an issue at the
29
259
  hidden field's path. A layer that shows the field still answers `A2004` at it.
30
- - b38034d: On PostgreSQL, a `@unique` value too long for its index (an index row past the B-tree's 8191 bytes, SQLSTATE
260
+ - 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
31
261
  `54000`) is answered `422 A2004`, not `500 A3001`: one `V1004` issue at `["arguments"]`, as a value too long for its
32
262
  column already was. The adapter reads the SQLSTATE, never the message. The issue's message for a value too long now
33
263
  reads `The database refused this value: it is longer than the database can hold for its column.`
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 answers, reports in a validation issue, or refuses to start with. Generated from `AvErrorCatalog` (exported by `@aventara/core`); do not edit.
3
+ Every code `@aventara/core` 1.0.0-rc.8 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,7 +30,7 @@ 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. |
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
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. |
@@ -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. | A guard or hook threw new AvError("A4002", ...) with a policy reason of your own. | Read cause.message for the policy's reason. |
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
 
@@ -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, or forceTransaction under an effective transactionsStyle "none". | Use a value the message allows. |
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: a whole surface (`AvApplicationFacade`), one Resource
174
- (`AvApplicationResourceFacade`), one family (`AvApplicationFamilyFacade`), and the same three for `client`, `appTx` and
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 view is written out in full, about 400 KB of declaration per such method over 50 Resources. Give the method a
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(): AppFramework["application"]["Post"] {
194
- return this.framework.application.Post;
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
- A variant (`this.framework.application.Post.find.many`) is a call type the declaration cannot name: give that method a
199
- return type, or return the call's result, whose row type is small. An application that publishes no declarations can
200
- set `"declaration": false` and skip the question.
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,50 @@ 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", action })`, `action` being `"create"` or `"update"` (an upsert's branches and a nested
361
+ record's directive each their own), so a field can be locked on update and still written at create. A rule that
362
+ reaches another Resource through a relation (a note only in a campaign its author plays in) reads which of the
363
+ target's fields the reference names from the Core Contract (`framework.contracts.core`, a relation's `references`
364
+ beside its `referenceFields`); no Contract a caller is served states them. An update that moves a row out of what the caller reads, or fails an `after` condition
365
+ (`can` asked about the row the update returned), is rolled back. Where `transactionsStyle` is `"interactive"`, a
366
+ checked write with a check after it (an update, a nested write) runs in a transaction of its own, and a create or
367
+ delete checked before it runs is one statement; where it is `"none"`, a write that could only be checked after it runs
368
+ is refused (for after conditions, the updates the engine lists in `afterChecks`).
369
+
370
+ Nested writes (`$create`, `$connect` and the other relation directives) run as one call inside that transaction. Before
371
+ it, every row they link is held to the right scope: a linked row that holds the reference must be one the caller may
372
+ change, one whose parent holds it one the caller may read. A to-many `$update` or `$delete` acts only on the rows the
373
+ caller may change or delete, as `update.many` does. After the call, every row they changed is checked as an update is
374
+ (the after check, the move-out re-read), and every row they created is read back as stored and asked `can`; one refusal
375
+ rolls the whole write back. Where `transactionsStyle` is `"none"`, a nested write under a policy is refused.
376
+
313
377
  ## License
314
378
 
315
379
  PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
@@ -38,6 +38,14 @@ export type AvAdapterRelationField<M extends AvAdapterModel> = {
38
38
  * one. Absent on the end that holds no reference (`User.posts`); never empty.
39
39
  */
40
40
  readonly referenceFields?: readonly [string, ...string[]];
41
+ /**
42
+ * The target's scalar fields that `referenceFields` name, pair by pair in
43
+ * the same order (`Post.author`: `referenceFields: ["authorId"]`,
44
+ * `references: ["id"]`, so `Post.authorId` holds `User.id`). Stated only
45
+ * beside `referenceFields`, with as many entries; absent on the end that
46
+ * holds no reference, where the other end (`inverse`) states the pairs.
47
+ */
48
+ readonly references?: readonly [string, ...string[]];
41
49
  };
42
50
  /** One field the adapter discovered: a scalar or a relation. */
43
51
  export type AvAdapterField<M extends AvAdapterModel> = AvAdapterScalarField<M> | AvAdapterRelationField<M>;
@@ -13,3 +13,12 @@ export type AvAdapterIdentifier<TFields extends Readonly<Record<string, AvAdapte
13
13
  AvAdapterScalarFieldKey<TFields>,
14
14
  ...AvAdapterScalarFieldKey<TFields>[]
15
15
  ];
16
+ /**
17
+ * The Resource's identifier as the Adapter states it first: in the Adapter's
18
+ * own order (an ORM adapter's is its schema's, so the primary key). The Core
19
+ * Contract sorts identifiers for its hash, so its first one is the
20
+ * alphabetically first (`email` before `id`) and says nothing about which is
21
+ * the primary key; the model keeps the order it declared. Empty for a Resource
22
+ * the model does not hold or that has no identifier.
23
+ */
24
+ export declare function adapterPrimaryIdentifier(model: AvAdapterModel, resource: string): readonly string[];
@@ -1 +1,6 @@
1
- export {};
1
+ export function adapterPrimaryIdentifier(model, resource) {
2
+ const resources = model.resources;
3
+ return ((Object.hasOwn(resources, resource)
4
+ ? resources[resource]?.identifiers?.[0]
5
+ : undefined) ?? []);
6
+ }
@@ -5,6 +5,19 @@ 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
+ * Two shapes exist here only, both an `AND`: a list of conditions in the
10
+ * general `where` grammar the addressed row must also meet.
11
+ *
12
+ * - The flat unique selector of an `update.unique` or a `delete.unique` may
13
+ * carry it. A row the selector names that misses them is a miss (the same
14
+ * `null` as a row that is not there).
15
+ * - A `cursor` may carry it, at the root of a `find.many` and in a to-many
16
+ * relation's arguments. A row the cursor names that misses them is a cursor
17
+ * that names no row: the same answer as a cursor on a row that is not there.
18
+ *
19
+ * An adapter must honour it or refuse the operation, never drop it: dropping it
20
+ * would write a row the conditions exclude, or position a page at one.
8
21
  */
9
22
  export type AvAdapterOperation<M extends AvAdapterModel = AvAdapterModel> = AvOperationIdentity & {
10
23
  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;