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