@aventara/nest 0.1.0-pilot.4 → 1.0.0-rc.1
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 +314 -0
- package/README.md +134 -59
- package/dist/aventara.module.d.ts +19 -8
- package/dist/aventara.module.js +18 -6
- package/dist/express-protocol.mounter.d.ts +13 -3
- package/dist/express-protocol.mounter.js +35 -6
- package/dist/fastify-protocol.mounter.d.ts +13 -4
- package/dist/fastify-protocol.mounter.js +29 -5
- package/dist/framework.token.d.ts +1 -1
- package/dist/host-bootstrap.validator.js +7 -2
- package/dist/internal-failure.logger.d.ts +4 -4
- package/dist/internal-failure.logger.js +5 -3
- package/dist/mcp-binding.loader.d.ts +30 -0
- package/dist/mcp-binding.loader.js +25 -0
- package/dist/node-exchange.translator.d.ts +6 -0
- package/dist/node-exchange.translator.js +9 -0
- package/dist/protocol-host.factory.d.ts +13 -3
- package/dist/protocol-host.factory.js +21 -10
- package/dist/protocol-route.table.d.ts +11 -1
- package/dist/protocol-route.table.js +6 -2
- package/package.json +16 -6
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# @aventara/nest
|
|
2
|
+
|
|
3
|
+
## 1.0.0-rc.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7e3d06f: Breaking: the configuration has a new shape. Every layer (the root, `application` and `client`) now holds the
|
|
8
|
+
same six keys: `exposed`, `transactionsStyle`, `limits`, `pipelines`, `resources` and `exemptions`. Everything about
|
|
9
|
+
one Resource lives under `resources.<Resource>`: its fields, and each field's `definition` and `pipelines`. The old
|
|
10
|
+
keys are refused when the framework starts, with a message that names where each one moved:
|
|
11
|
+
|
|
12
|
+
| Before (rc.0) | Now (rc.1) |
|
|
13
|
+
| ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
14
|
+
| `fields: { Post: { url: { type, nullable, … } } }` | `resources: { Post: { fields: { url: { definition: { type, nullable, … } } } } }` |
|
|
15
|
+
| `behaviors: { Post: { fields: { slug: { create } } } }` | `resources: { Post: { fields: { slug: { pipelines: { compute: { create } } } } } }` |
|
|
16
|
+
| `transactions: "none"` | `transactionsStyle: "none"` |
|
|
17
|
+
| `restrictions` | `exposed` + `exemptions` (see below) |
|
|
18
|
+
| `@aventara/core/json` | `@aventara/core/avz` |
|
|
19
|
+
|
|
20
|
+
Before:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
createFramework({
|
|
24
|
+
api: "/api",
|
|
25
|
+
adapter,
|
|
26
|
+
transactions: "interactive",
|
|
27
|
+
fields: {
|
|
28
|
+
Post: {
|
|
29
|
+
url: {
|
|
30
|
+
type: { scalar: "string" },
|
|
31
|
+
nullable: false,
|
|
32
|
+
list: false,
|
|
33
|
+
lifecycle: ["VIRTUAL"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
behaviors: {
|
|
38
|
+
Post: {
|
|
39
|
+
fields: {
|
|
40
|
+
url: {
|
|
41
|
+
read: {
|
|
42
|
+
dependsOn: ["slug"],
|
|
43
|
+
compute: ({ record }) => `/p/${record.slug}`,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
After:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
createFramework({
|
|
56
|
+
api: "/api",
|
|
57
|
+
adapter,
|
|
58
|
+
transactionsStyle: "interactive",
|
|
59
|
+
resources: {
|
|
60
|
+
Post: {
|
|
61
|
+
fields: {
|
|
62
|
+
url: {
|
|
63
|
+
definition: {
|
|
64
|
+
type: { scalar: "string" },
|
|
65
|
+
nullable: false,
|
|
66
|
+
list: false,
|
|
67
|
+
lifecycle: ["VIRTUAL"],
|
|
68
|
+
format: "url",
|
|
69
|
+
},
|
|
70
|
+
pipelines: {
|
|
71
|
+
compute: {
|
|
72
|
+
read: {
|
|
73
|
+
dependsOn: ["slug"],
|
|
74
|
+
compute: ({ record }) => `/p/${record.slug}`,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
What else changed:
|
|
86
|
+
|
|
87
|
+
- A field is defined at the root only. A `definition` in `application` or `client` is refused
|
|
88
|
+
(`CONFIG_FORBIDDEN_IN_OVERLAY`). Define the field
|
|
89
|
+
at the root; `application` and `client` can still add compute stages to it.
|
|
90
|
+
- Computed values chain. When the root and an overlay both compute a field, the root's stage runs first and the
|
|
91
|
+
overlay's receives its result as `value`. A stage can no longer be turned off with `false`. Returning `undefined`
|
|
92
|
+
keeps the current value. An upsert computes its create branch with `compute.create` and its update branch with
|
|
93
|
+
`compute.update`.
|
|
94
|
+
- A discovered field keeps the adapter's facts. On a field the adapter discovered you may restate its own `type`,
|
|
95
|
+
`nullable` and `list`, and give a Json field its shape (`definition.json`). Setting a different type, any `format`
|
|
96
|
+
or any `capabilities` on it is refused. `format` is for fields you add, and it must be one of zod's named string
|
|
97
|
+
formats (`"email"`, `"url"`, `"uuid"`, …); any other name is a type error and refused at startup.
|
|
98
|
+
- A misplaced key is a type error. In a configuration written inline in `createFramework({ ... })` or
|
|
99
|
+
`AventaraModule.forRoot`, a key that has no place there (a typo, an old key, `compute` on a relation field, a field
|
|
100
|
+
name neither your schema nor the root has) is now a type error at that key, and still refused at startup.
|
|
101
|
+
- A Resource whose name starts with `$` is refused at startup (`$` names are the framework's).
|
|
102
|
+
- The Json shape builder moved from `@aventara/core/json` to `@aventara/core/avz`. Change the import; `AvZ` is
|
|
103
|
+
the same.
|
|
104
|
+
|
|
105
|
+
`exposed` and `exemptions`, which replace `restrictions`, have their own entry.
|
|
106
|
+
|
|
107
|
+
- 4907afc: New: `AventaraModule.forRoot` and `forRootAsync` take a `state` option that seeds each protocol request's
|
|
108
|
+
`ctx.$state` (see `@aventara/core`'s changelog). It is called once per request with the platform's own request, so
|
|
109
|
+
what your middleware set there reaches every guard, pipe, hook and interceptor of the request:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
AventaraModule.forRoot({
|
|
113
|
+
config,
|
|
114
|
+
state: (req: Request) => ({ user: req.user }), // Passport's req.user, on Express
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
On Express the request is Express's `req`. On Fastify it is Fastify's `request`; middleware added with `app.use`
|
|
119
|
+
writes to its Node request, `request.raw`, so read `request.raw.user` there. Fastify answers a protocol route in its
|
|
120
|
+
`onRequest` hook, so only what runs before that (`app.use` middleware, an earlier `onRequest` hook) can be read. A
|
|
121
|
+
`state` function that throws an `AvError` answers that error (`AvError("A4001", …)` is a `403`); any other throw is
|
|
122
|
+
`500 A3000`. A request the protocol refuses before running (a stale client, an unknown route) never calls it. A
|
|
123
|
+
`state` that is not a function is refused while Nest builds the application.
|
|
124
|
+
|
|
125
|
+
- Breaking: the configuration's mount path is `api` (it was `entrypoint`), and the module reads `framework.api`.
|
|
126
|
+
`forRoot({ config: { api: "/api", adapter } })`. The Fastify refusal of a path with `*` now says "Change `api`, or
|
|
127
|
+
host on Express."
|
|
128
|
+
|
|
129
|
+
- New: with `mcp` in the configuration, the module mounts the MCP endpoint of `@aventara/mcp` at that path (and, with
|
|
130
|
+
`mcp.authorization`, its OAuth metadata at `/.well-known/oauth-protected-resource` plus the path), on Express and on
|
|
131
|
+
Fastify, as it mounts the protocol's routes: the body read raw, the request's URL and peer address handed over, the
|
|
132
|
+
answer written as it is. The `state` option seeds each tool call's `ctx.$state`. `@aventara/mcp` is an optional peer
|
|
133
|
+
dependency, loaded only when `mcp` is configured; with `mcp` set and the package missing, the application refuses to
|
|
134
|
+
start and names it.
|
|
135
|
+
|
|
136
|
+
- Fixed on Express: a request whose path matched a mounted route only because Express ignores case reached the route
|
|
137
|
+
without its body, and was answered as if it had sent none. Express matches routes case-insensitively by default,
|
|
138
|
+
while the body reader compared the path exactly. The MCP routes now answer their own path only, exactly as written,
|
|
139
|
+
so `/MCP` or `/mcp/` goes on to Nest's `404`, as on Fastify. The protocol's routes keep Express's matching (it also
|
|
140
|
+
answers the protocol under `/API` for `api: "/api"`), and the body reader now matches the `api` path the same way, so
|
|
141
|
+
`POST /API/_resources/…` gets its body.
|
|
142
|
+
|
|
143
|
+
- The host's log line for an internal failure names the MCP endpoint when the endpoint itself failed
|
|
144
|
+
(`A3000 on the MCP endpoint (agent origin, request …)`).
|
|
145
|
+
|
|
146
|
+
- Updated dependencies [800123b]
|
|
147
|
+
- Updated dependencies [8bb1d6a]
|
|
148
|
+
- Updated dependencies [68d4395]
|
|
149
|
+
- Updated dependencies [7e3d06f]
|
|
150
|
+
- Updated dependencies [1b5d3c0]
|
|
151
|
+
- Updated dependencies [6317811]
|
|
152
|
+
- Updated dependencies [6d0e5d6]
|
|
153
|
+
- Updated dependencies [7a0cab9]
|
|
154
|
+
- Updated dependencies [eb2a718]
|
|
155
|
+
- Updated dependencies [03312c5]
|
|
156
|
+
- Updated dependencies [123f6dc]
|
|
157
|
+
- Updated dependencies [b65df2e]
|
|
158
|
+
- Updated dependencies [c6e5010]
|
|
159
|
+
- Updated dependencies [c1f9389]
|
|
160
|
+
- Updated dependencies [9c52482]
|
|
161
|
+
- Updated dependencies [8396148]
|
|
162
|
+
- Updated dependencies [e8fbb02]
|
|
163
|
+
- Updated dependencies [916e5f6]
|
|
164
|
+
- Updated dependencies [6cbd158]
|
|
165
|
+
- @aventara/core@1.0.0-rc.1
|
|
166
|
+
|
|
167
|
+
## 1.0.0-rc.0
|
|
168
|
+
|
|
169
|
+
### Major Changes
|
|
170
|
+
|
|
171
|
+
- First release candidate of Aventara 1.0. The public API is now considered stable; changes before 1.0.0 are possible
|
|
172
|
+
based on feedback.
|
|
173
|
+
|
|
174
|
+
### Patch Changes
|
|
175
|
+
|
|
176
|
+
- 24ffbab: Each package ships its `CHANGELOG.md`, so the release notes are in the installed package beside the README.
|
|
177
|
+
- 24ffbab: **Behaviour changes:** Pipelines and guards read where a request came from as `request.origin` (was `request.scope`).
|
|
178
|
+
The same property is renamed everywhere it means `"application"` or `"client"`: the pipeline context
|
|
179
|
+
(`context.origin`), the request objects `framework.execute` and `framework.executeTransaction` take
|
|
180
|
+
(`{ origin: "client", … }`), the context a computed field's behaviour receives, and every diagnostic the diagnostics
|
|
181
|
+
sink receives. There is no `scope` alias: replace `scope` with `origin` in those places. Nothing sent over HTTP
|
|
182
|
+
changes.
|
|
183
|
+
|
|
184
|
+
The Nest host's log line for an internal failure now reads `(client origin, request …)` instead of
|
|
185
|
+
`(client scope, request …)`.
|
|
186
|
+
|
|
187
|
+
- Updated dependencies [24ffbab]
|
|
188
|
+
- Updated dependencies [24ffbab]
|
|
189
|
+
- Updated dependencies [24ffbab]
|
|
190
|
+
- Updated dependencies [24ffbab]
|
|
191
|
+
- Updated dependencies [24ffbab]
|
|
192
|
+
- Updated dependencies [24ffbab]
|
|
193
|
+
- Updated dependencies [24ffbab]
|
|
194
|
+
- Updated dependencies [24ffbab]
|
|
195
|
+
- Updated dependencies [24ffbab]
|
|
196
|
+
- Updated dependencies [24ffbab]
|
|
197
|
+
- Updated dependencies [24ffbab]
|
|
198
|
+
- Updated dependencies [24ffbab]
|
|
199
|
+
- Updated dependencies [a973e89]
|
|
200
|
+
- Updated dependencies [1e65e9a]
|
|
201
|
+
- Updated dependencies
|
|
202
|
+
- Updated dependencies [a973e89]
|
|
203
|
+
- Updated dependencies [a973e89]
|
|
204
|
+
- Updated dependencies [1e65e9a]
|
|
205
|
+
- Updated dependencies [1e65e9a]
|
|
206
|
+
- Updated dependencies [24ffbab]
|
|
207
|
+
- Updated dependencies [2d00ad7]
|
|
208
|
+
- Updated dependencies [384a50c]
|
|
209
|
+
- Updated dependencies [d43d1cc]
|
|
210
|
+
- Updated dependencies [3f98749]
|
|
211
|
+
- Updated dependencies [032f792]
|
|
212
|
+
- Updated dependencies [dbe6961]
|
|
213
|
+
- @aventara/core@1.0.0-rc.0
|
|
214
|
+
|
|
215
|
+
## 0.1.0-pilot.4
|
|
216
|
+
|
|
217
|
+
### Patch Changes
|
|
218
|
+
|
|
219
|
+
- f37dcd6: The documentation shipped with the package — the comments an editor shows on hover, and the README — now describes
|
|
220
|
+
what each export does, how to use it, what it returns and what it throws. Notes about how the package was built and
|
|
221
|
+
why are no longer part of what is published.
|
|
222
|
+
- Updated dependencies [f37dcd6]
|
|
223
|
+
- @aventara/core@0.1.0-pilot.4
|
|
224
|
+
|
|
225
|
+
## 0.1.0-pilot.3
|
|
226
|
+
|
|
227
|
+
### Patch Changes
|
|
228
|
+
|
|
229
|
+
- @aventara/core@0.1.0-pilot.3
|
|
230
|
+
|
|
231
|
+
## 0.1.0-pilot.2
|
|
232
|
+
|
|
233
|
+
### Patch Changes
|
|
234
|
+
|
|
235
|
+
- 5535994: Internal failures are visible in the host's log: when `AventaraModule` builds the framework from `config`, every
|
|
236
|
+
`A3000`–`A3003` writes one error line through Nest's `Logger` (context `Aventara`) with the code, the operation, the
|
|
237
|
+
scope, the request id and the thrown value's class name — never the error's message. A configured `diagnostics` sink
|
|
238
|
+
still receives the raw error.
|
|
239
|
+
- e69e88d: The published packages are written for the people who use them. The JavaScript ships without comments; the type
|
|
240
|
+
declarations keep their documentation, and it, the error messages, the protocol fixtures' labels and the comments in a
|
|
241
|
+
generated client no longer carry the project's internal development notes.
|
|
242
|
+
- ab58424: A pipe's output is checked against the server's own rules before it reaches the adapter. The request is validated
|
|
243
|
+
against the caller's Contract; after each pipe, the arguments are validated against the Core Contract (plus fields the
|
|
244
|
+
caller's scope adds), so a pipe may still write what the caller cannot — a field the client Contract hides, such as a
|
|
245
|
+
password hash — but not what the server cannot store. A refusal is the new internal code `A3004 PIPE_OUTPUT_INVALID` (HTTP
|
|
246
|
+
500). The caller receives only the generic `A server pipeline produced invalid arguments.`, like every internal
|
|
247
|
+
failure; the pipe (`pipes[i]` and its function name), the operation, the offending field and its V-code go to the
|
|
248
|
+
diagnostics sink as a `PipeOutputError`, and `@aventara/nest` writes them into its error log line. Nothing reaches the
|
|
249
|
+
adapter. This holds for operations and transaction steps alike (a plan answers with `cause.operation`). Generated
|
|
250
|
+
clients map `A3004` to `InternalError`; the protocol fixtures in `@aventara/testing` include an `A3004` row.
|
|
251
|
+
- Updated dependencies [c2a3b7c]
|
|
252
|
+
- Updated dependencies [e69e88d]
|
|
253
|
+
- Updated dependencies [ab58424]
|
|
254
|
+
- Updated dependencies [23937f8]
|
|
255
|
+
- @aventara/core@0.1.0-pilot.2
|
|
256
|
+
|
|
257
|
+
## 0.1.0-pilot.1
|
|
258
|
+
|
|
259
|
+
### Patch Changes
|
|
260
|
+
|
|
261
|
+
- 1c45eb7: Startup says what Aventara mounted. Its routes are platform routes, which Nest's `RouterExplorer` never lists, so a
|
|
262
|
+
started server showed no sign of them. On mount, the module now logs one line through Nest's `Logger` (context
|
|
263
|
+
`AventaraModule`), read off `protocol.surface()`: `Aventara mounted 10 operations at /api (+ GET /api/_contract, POST
|
|
264
|
+
/api/_transactions)`. The application's logger settings govern it.
|
|
265
|
+
- 799346e: The package READMEs (each package's npm page) are rewritten for the people who use the packages. The Prisma 7
|
|
266
|
+
adapter's README now states the actual behaviour of a to-many relation or `$count` projected on
|
|
267
|
+
`update.many`/`create.many`: a validation refusal, `A2004` with `V1008`, before any statement runs (it said `A3001`).
|
|
268
|
+
Every install or run command names the `@pilot` dist-tag.
|
|
269
|
+
- e58aebf: The READMEs show how to call the API by hand: a copy-pasteable `curl` that fetches `/api/_contract` and sends a
|
|
270
|
+
`find.many` with both identity headers, and runs as printed against a scaffolded server. They also say that
|
|
271
|
+
Resource keys are the model names as written (`/_resources/User/…`, `avClient.User.find.many`), and that list results are
|
|
272
|
+
`readonly` arrays (`readonly User[]`).
|
|
273
|
+
- Updated dependencies [a45f8a8]
|
|
274
|
+
- Updated dependencies [799346e]
|
|
275
|
+
- @aventara/core@0.1.0-pilot.1
|
|
276
|
+
|
|
277
|
+
## 0.1.0-pilot.0
|
|
278
|
+
|
|
279
|
+
### Minor Changes
|
|
280
|
+
|
|
281
|
+
- First pilot release of `@aventara/nest`: it hosts an Aventara Framework in a NestJS 12 application (peers
|
|
282
|
+
`@nestjs/common` and `@nestjs/core` `^12.0.0`). It exports `AventaraModule`, `AVENTARA_FRAMEWORK`, `InjectFramework`
|
|
283
|
+
and the types `AventaraModuleOptions` / `AventaraModuleAsyncOptions`.
|
|
284
|
+
|
|
285
|
+
- **Mounting.** `AventaraModule.forRoot({ framework })` uses your own instance; `forRoot({ config })` calls
|
|
286
|
+
`createFramework(config)` itself; `forRootAsync({ imports, inject, useFactory })` resolves either. Both forms are
|
|
287
|
+
generic like `createFramework`, so a configuration is checked against its adapter's model. The module is global;
|
|
288
|
+
inject the Framework with `@InjectFramework()`. A configuration error rejects `NestFactory.create` before the
|
|
289
|
+
application listens, and passing both `framework` and `config`, neither, a value that is not a Framework, or
|
|
290
|
+
mounting the module twice is refused with a fixed message.
|
|
291
|
+
- **Routes.** One platform route per route the protocol advertises — every operation, `GET /_contract`, and
|
|
292
|
+
`POST /_transactions` when transactions are interactive — at its literal path under `FrameworkConfig.entrypoint`
|
|
293
|
+
(a Nest global prefix does not move it), on Express and on Fastify. Any other path under the entrypoint's `/_`
|
|
294
|
+
namespace gets the protocol's own answer; every other path goes on to Nest.
|
|
295
|
+
- **What runs on them.** Nest's guards, interceptors and pipes do not; `app.use` middleware and `enableCors()` do.
|
|
296
|
+
`MiddlewareConsumer` middleware does not run on them on Express, and does on Fastify — exclude `<entrypoint>/_*`
|
|
297
|
+
(for example `.exclude("api/_*path")`) to keep it off. A throw while translating a request reaches your exception
|
|
298
|
+
filter.
|
|
299
|
+
- **Request bodies.** Read raw and bounded by the contract's `limits.maxRequestBytes`; `gzip`, `deflate` and `br` are
|
|
300
|
+
decoded and the limit counts decoded bytes (`413 A2010`); any other coding answers `415 A2011`, and a body that does
|
|
301
|
+
not decode in the coding it names answers `400 A2000`. Nothing is executed in either case. Your own routes keep Nest's
|
|
302
|
+
parsed `req.body`.
|
|
303
|
+
- **Platform differences.** On Fastify, Resource keys or an entrypoint holding `*` or a reserved character
|
|
304
|
+
(`: + # $ & , / ; = ? @`) are refused at startup, naming them — rename them, or host on Express. Express answers a
|
|
305
|
+
non-canonical percent-encoding of a protocol route with `500 A3000`, and matches the entrypoint in any case by
|
|
306
|
+
default.
|
|
307
|
+
- Works under Nest's testing module (`Test.createTestingModule(…).compile()` then `createNestApplication()`), and every
|
|
308
|
+
export has a `default` condition, so a CommonJS NestJS project can `require()` it. The README covers the ways to
|
|
309
|
+
mount the module, typed injection, what fails before listen, and the platform differences.
|
|
310
|
+
|
|
311
|
+
### Patch Changes
|
|
312
|
+
|
|
313
|
+
- Updated dependencies
|
|
314
|
+
- @aventara/core@0.1.0-pilot.0
|
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# `@aventara/nest`
|
|
2
2
|
|
|
3
|
-
Hosts an Aventara `
|
|
4
|
-
`AvProtocol.bind(framework)` from `@aventara/core/protocol`
|
|
3
|
+
Hosts an Aventara `AvFramework` in a NestJS application. It mounts the framework's HTTP protocol
|
|
4
|
+
(`AvProtocol.bind(framework)` from `@aventara/core/protocol`) on Nest's HTTP platform, and puts the `AvFramework` into
|
|
5
5
|
Nest's dependency injection. Every rule of the protocol (routes, decoding, statuses, envelopes, headers, absence) is
|
|
6
6
|
core's; this package only translates a Nest request into the protocol's request and writes the protocol's answer back.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
It runs on Nest 12 (`@nestjs/common` and `@nestjs/core` `^12.0.0`), on Express (Nest's default) or Fastify.
|
|
9
|
+
`@aventara/core` is its one dependency.
|
|
10
10
|
|
|
11
11
|
## Mounting it
|
|
12
12
|
|
|
13
|
-
`AventaraModule` is a global module, imported once in your root module. It takes the `
|
|
13
|
+
`AventaraModule` is a global module, imported once in your root module. It takes the `AvFramework` in one of four forms.
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
16
|
// src/app.module.ts — the shape `aventara init` writes (`@aventara/cli`)
|
|
@@ -33,14 +33,27 @@ import { PrismaModule, PrismaService } from "./prisma.service.js";
|
|
|
33
33
|
})
|
|
34
34
|
export class AppModule {}
|
|
35
35
|
|
|
36
|
-
// src/aventara.config.ts — the framework's configuration:
|
|
36
|
+
// src/aventara.config.ts — the framework's configuration: api, exposure, pipelines, resources
|
|
37
|
+
import type { AvDiagnostic } from "@aventara/core";
|
|
37
38
|
import { createPrismaAdapter } from "@aventara/prisma7-adapter";
|
|
39
|
+
import { Logger } from "@nestjs/common";
|
|
38
40
|
import { discovery } from "./generated/aventara/discovery.artifact.js";
|
|
39
41
|
import type { PrismaService } from "./prisma.service.js";
|
|
40
42
|
|
|
43
|
+
const logger = new Logger("Aventara");
|
|
44
|
+
|
|
41
45
|
export async function aventaraConfig(prisma: PrismaService) {
|
|
42
46
|
return {
|
|
43
|
-
|
|
47
|
+
api: "/api",
|
|
48
|
+
exposed: true,
|
|
49
|
+
// The raw error of a server failure, with its stack, in Nest's log.
|
|
50
|
+
diagnostics: (diagnostic: AvDiagnostic) => {
|
|
51
|
+
const { error } = diagnostic;
|
|
52
|
+
logger.error(
|
|
53
|
+
`${diagnostic.code} (request ${diagnostic.requestId}): raw error`,
|
|
54
|
+
error instanceof Error ? error.stack : String(error),
|
|
55
|
+
);
|
|
56
|
+
},
|
|
44
57
|
adapter: await createPrismaAdapter({
|
|
45
58
|
client: prisma,
|
|
46
59
|
discovery,
|
|
@@ -53,15 +66,18 @@ export async function aventaraConfig(prisma: PrismaService) {
|
|
|
53
66
|
|
|
54
67
|
| Form | When |
|
|
55
68
|
|---|---|
|
|
56
|
-
| `AventaraModule.forRoot({ framework })` | You called `createFramework(config)` yourself and want your own typed `
|
|
69
|
+
| `AventaraModule.forRoot({ framework })` | You called `createFramework(config)` yourself and want your own typed `AvFramework<M, Cfg>` in code. |
|
|
57
70
|
| `AventaraModule.forRoot({ config })` | The module calls `createFramework(config)` for you. |
|
|
58
71
|
| `AventaraModule.forRootAsync({ imports, inject, useFactory })` resolving `{ framework }` or `{ config }` | What builds the Framework (a Prisma client, a configuration service) is itself a provider. |
|
|
59
72
|
|
|
60
|
-
|
|
73
|
+
Each form also takes `state`, which seeds every protocol request's `ctx.$state` from the platform request (see
|
|
74
|
+
*Request state*).
|
|
75
|
+
|
|
76
|
+
The `config` form is typed exactly like `createFramework`'s parameter: an annotated `AvConfig<M>`, an inline
|
|
61
77
|
literal, an inferred constant and a `satisfies` check are all accepted, and checked against your Adapter's model.
|
|
62
78
|
|
|
63
|
-
|
|
64
|
-
|
|
79
|
+
`@InjectFramework()` (or `@Inject(AVENTARA_FRAMEWORK)`) injects the `AvFramework`. DI carries no type argument, so type
|
|
80
|
+
the parameter with your own alias:
|
|
65
81
|
|
|
66
82
|
```ts
|
|
67
83
|
export type AppFramework = Awaited<ReturnType<typeof buildFramework>>;
|
|
@@ -72,23 +88,23 @@ export class ReportsService {
|
|
|
72
88
|
}
|
|
73
89
|
```
|
|
74
90
|
|
|
75
|
-
|
|
76
|
-
`framework`
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
91
|
+
These reject while Nest builds the application, before it listens: both or neither of `framework` and `config`; a
|
|
92
|
+
`framework` value that is not a Framework; a `state` option that is not a function; the module mounted twice in one application; a platform other than Express
|
|
93
|
+
or Fastify (or none, as in `createApplicationContext`); an `AvConstructionError`, yours or the module's own, which
|
|
94
|
+
propagates unchanged. Nest's default `abortOnError: true` exits the process on a bootstrap error instead of rejecting;
|
|
95
|
+
pass `abortOnError: false` to `NestFactory.create` to catch it.
|
|
80
96
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
unsupported platform before the application listens. A testing module initialized with no
|
|
84
|
-
|
|
97
|
+
Nest's testing module works. `Test.createTestingModule({ imports: [AppModule] }).compile()` builds the providers before
|
|
98
|
+
any HTTP platform exists; the module waits for the one `createNestApplication()` supplies, and still refuses an
|
|
99
|
+
unsupported platform before the application listens. A testing module initialized with no application at all is
|
|
100
|
+
refused at `init()`, as a platform of `none`.
|
|
85
101
|
|
|
86
|
-
|
|
102
|
+
The module owns no resource, so there is nothing to shut down. Your Prisma provider closes its own client in its
|
|
87
103
|
`onModuleDestroy`, with `app.enableShutdownHooks()` for signals.
|
|
88
104
|
|
|
89
105
|
## Calling it by hand
|
|
90
106
|
|
|
91
|
-
Against the shape `aventara init` writes
|
|
107
|
+
Against the shape `aventara init` writes (the `api` mount path `/api`, a Prisma `User` model):
|
|
92
108
|
|
|
93
109
|
<!-- pilot-gate:curl -->
|
|
94
110
|
```bash
|
|
@@ -108,18 +124,19 @@ curl -s -X POST http://localhost:3000/api/_resources/User/find/many \
|
|
|
108
124
|
Leave out an identity header and the answer is `400 A2000`, naming the header that is missing. The hash changes
|
|
109
125
|
whenever the schema or the configuration does: fetch it again, and regenerate the client.
|
|
110
126
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
127
|
+
Resource keys are the model names, as written. `model User` is `User` everywhere: on the wire
|
|
128
|
+
(`/_resources/User/find/many`) and in the generated client (`avClient.User.find.many({})`). Nothing is renamed,
|
|
129
|
+
lower-cased or pluralized.
|
|
130
|
+
|
|
131
|
+
Results are read-only. A list result is a `readonly` array, so type it `readonly User[]`:
|
|
132
|
+
`const users: readonly User[] = await avClient.User.find.many({});`.
|
|
116
133
|
|
|
117
134
|
## What runs on a protocol route
|
|
118
135
|
|
|
119
|
-
The protocol's routes are platform routes, registered from the module's `configure()
|
|
120
|
-
`protocol.surface()` entry
|
|
121
|
-
<
|
|
122
|
-
rest of `<
|
|
136
|
+
The protocol's routes are platform routes, registered from the module's `configure()`. There is one per
|
|
137
|
+
`protocol.surface()` entry (every advertised operation, `GET <api>/_contract`, and `POST
|
|
138
|
+
<api>/_transactions` only when the ClientContract advertises `interactive`), plus the absence handler for the
|
|
139
|
+
rest of `<api>/_*`. Nest's `RouterExplorer` does not list platform routes, so the module logs one line of its
|
|
123
140
|
own when it mounts them, through Nest's `Logger` (context `AventaraModule`; `logger: false` silences it like any other):
|
|
124
141
|
|
|
125
142
|
```text
|
|
@@ -127,16 +144,19 @@ own when it mounts them, through Nest's `Logger` (context `AventaraModule`; `log
|
|
|
127
144
|
```
|
|
128
145
|
|
|
129
146
|
An internal failure (`A3000`–`A3004`, answered as a 500) is logged as one error line through Nest's `Logger`
|
|
130
|
-
(context `Aventara`): the code, the operation, the
|
|
131
|
-
|
|
132
|
-
configure one:
|
|
147
|
+
(context `Aventara`): the code, the operation, the origin and the request id, and the thrown value's class name. It
|
|
148
|
+
never logs the message, which can hold SQL or a connection string. The raw error still goes to your `diagnostics` sink, when you
|
|
149
|
+
configure one. The `aventara.config.ts` that `aventara new` and `aventara init` write has one: it logs the raw error with its
|
|
150
|
+
stack through the same `Logger`, at error level, right after this line.
|
|
133
151
|
|
|
134
152
|
```text
|
|
135
|
-
[Nest] ERROR [Aventara] A3001 on Post.create.one (client
|
|
153
|
+
[Nest] ERROR [Aventara] A3001 on Post.create.one (client origin, request 3f1c…): PrismaClientValidationError. The raw error goes to the framework's diagnostics sink.
|
|
154
|
+
[Nest] ERROR [Aventara] A3001 (request 3f1c…): raw error
|
|
155
|
+
PrismaClientValidationError: …
|
|
136
156
|
```
|
|
137
157
|
|
|
138
|
-
For `A3004`
|
|
139
|
-
operation, the offending field and its V-code
|
|
158
|
+
For `A3004` (one of your pipes returned arguments the server contract refuses), the line also names the pipe, the
|
|
159
|
+
operation, the offending field and its V-code. The caller only ever sees the generic message.
|
|
140
160
|
|
|
141
161
|
The line is added when the module builds the framework from `config`; a framework you build yourself and pass as
|
|
142
162
|
`framework` keeps exactly the `diagnostics` sink you gave it.
|
|
@@ -146,42 +166,97 @@ The framework's validation is outermost:
|
|
|
146
166
|
| | Express | Fastify |
|
|
147
167
|
|---|---|---|
|
|
148
168
|
| Your `app.use(...)` middleware, `app.enableCors()`, helmet | runs | runs |
|
|
149
|
-
| Nest guards, interceptors, pipes (global or not) |
|
|
150
|
-
| `MiddlewareConsumer` middleware |
|
|
151
|
-
| Your controller at `<
|
|
152
|
-
| Your controller at `<
|
|
169
|
+
| Nest guards, interceptors, pipes (global or not) | never | never |
|
|
170
|
+
| `MiddlewareConsumer` middleware | never | runs, because Fastify runs Nest middleware before routing; add `.exclude("api/_*path")` (your `api` path) to keep it off |
|
|
171
|
+
| Your controller at `<api>/_contract` (or any `_` path) | never reached | the application does not start |
|
|
172
|
+
| Your controller at `<api>/health` (outside `_`) | reached | reached |
|
|
153
173
|
|
|
154
|
-
|
|
155
|
-
`transport.headers`
|
|
156
|
-
|
|
174
|
+
Authorization therefore belongs in Aventara pipelines (guards). A guard reads the request's headers as
|
|
175
|
+
`ctx.$transport.headers` (lower-cased names, repeats joined), and whatever your middleware found as `ctx.$state`,
|
|
176
|
+
through the module's `state` option (next section).
|
|
157
177
|
|
|
158
|
-
|
|
159
|
-
|
|
178
|
+
## Request state
|
|
179
|
+
|
|
180
|
+
`state` seeds each protocol request's `ctx.$state`, the one object every stage of that request reads and writes. It is
|
|
181
|
+
called once per request, with the platform's own request, after the protocol has accepted it (a stale client or an
|
|
182
|
+
unknown route never calls it):
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
declare module "@aventara/core" {
|
|
186
|
+
interface AvState {
|
|
187
|
+
user?: { id: string; roles: string[] };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
AventaraModule.forRoot({
|
|
192
|
+
config,
|
|
193
|
+
state: (req: Request) => ({ user: req.user }), // what your auth middleware set
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// In the configuration: a guard reads it.
|
|
197
|
+
const signedIn: AvGuard = (ctx) => ctx.$state.user !== undefined;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- `forRootAsync`'s `useFactory` returns `state` beside `framework` or `config`.
|
|
201
|
+
- On Express, `req` is Express's request. On Fastify it is Fastify's `request`; `app.use` middleware writes to its
|
|
202
|
+
Node request, so read `request.raw.user`. Fastify answers a protocol route from its `onRequest` hook, so only what
|
|
203
|
+
runs before it (`app.use` middleware, an earlier `onRequest` hook) has run.
|
|
204
|
+
- Use `app.use(...)` for the middleware that finds the user (a Passport `authenticate` call, a session or JWT check).
|
|
205
|
+
Nest guards never run on protocol routes, and `MiddlewareConsumer` middleware does not on Express (the table above).
|
|
206
|
+
- Throw `new AvError("A4001", "…")` from `state` to refuse the request (`403`); any other throw is `500 A3000`, with the
|
|
207
|
+
error in your `diagnostics` sink.
|
|
208
|
+
- A plan sent to `/_transactions` gets one state for all its steps and transaction stages. A ctx call a stage makes
|
|
209
|
+
inherits it. Compute reads it but cannot write it.
|
|
210
|
+
- Server code that calls the framework in process seeds it per call:
|
|
211
|
+
`framework.application.Post.create.one(args, { state: { user } })`.
|
|
212
|
+
|
|
213
|
+
`AvConfig.api` alone decides where the protocol is mounted. `app.setGlobalPrefix(...)` does not move it; set
|
|
214
|
+
`api` to where you want it, and point the generated client at the same URL.
|
|
160
215
|
|
|
161
216
|
## Requests
|
|
162
217
|
|
|
163
|
-
-
|
|
218
|
+
- Bodies on protocol routes are read raw, never through Nest's body parser, up to `limits.maxRequestBytes`. The
|
|
164
219
|
protocol's decoder answers an oversized body (`413 A2010`), another content type (`415 A2011`) and malformed JSON
|
|
165
220
|
(`400 A2000`). Your own routes keep Nest's parsed `req.body`. No `NestFactory.create` option is needed.
|
|
166
|
-
-
|
|
221
|
+
- For `Content-Encoding`, `gzip`, `deflate` and `br` are inflated, and the limit counts decoded bytes. Any other coding
|
|
167
222
|
answers `415 A2011`; a body that does not decode in the coding it names answers `400 A2000`. These two answers carry a
|
|
168
223
|
newly minted `Aventara-Request-Id`, not the client's.
|
|
169
|
-
-
|
|
170
|
-
`req.headers` cannot change it, and `X-Request-Id` is never copied into it
|
|
171
|
-
|
|
224
|
+
- The request id is the client's `Aventara-Request-Id` as it arrived, or one core mints. A middleware that rewrites
|
|
225
|
+
`req.headers` cannot change it, and `X-Request-Id` is never copied into it, so set `Aventara-Request-Id` at your
|
|
226
|
+
proxy.
|
|
227
|
+
- A query string is not part of the protocol path; it is ignored.
|
|
228
|
+
|
|
229
|
+
## The MCP endpoint
|
|
230
|
+
|
|
231
|
+
With `mcp` in the configuration, the module also mounts the MCP server of `@aventara/mcp` at that path, on Express and
|
|
232
|
+
Fastify alike, and logs `Aventara mounted the MCP endpoint at /mcp (…)`. Install it beside this package, at the same
|
|
233
|
+
version:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
npm install @aventara/mcp@rc
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
It is an optional peer dependency: an application without `mcp` does not need it, and one with `mcp` and without the
|
|
240
|
+
package refuses to start, naming it. As for the protocol's routes, the module only mounts: the endpoint's body is read
|
|
241
|
+
raw, the request's URL comes from the platform's `protocol` and `host` (set `trust proxy` on Express or `trustProxy` on
|
|
242
|
+
Fastify behind a proxy), and every answer is `@aventara/mcp`'s. The `state` option seeds each tool call's `ctx.$state`.
|
|
243
|
+
See `@aventara/mcp`'s README for the tools, the contract resource and authorization.
|
|
172
244
|
|
|
173
245
|
## Platform differences
|
|
174
246
|
|
|
175
|
-
-
|
|
176
|
-
reserved character (`: + # $ & , / ; = ? @`) in its route path, or an
|
|
247
|
+
- Fastify cannot mount some Resource keys or `api` paths as literal routes. A Resource key that needs `*` or a
|
|
248
|
+
reserved character (`: + # $ & , / ; = ? @`) in its route path, or an `api` path containing `*`, is refused at startup
|
|
177
249
|
with a message naming it. Rename it, or host on Express, which mounts every key.
|
|
178
|
-
-
|
|
179
|
-
Fastify's is not. That is Express's own `case sensitive routing` setting, off by default, and yours to change.
|
|
180
|
-
|
|
181
|
-
|
|
250
|
+
- Express routing is case-insensitive by default, so it also answers the protocol under, say, `/API/_contract`;
|
|
251
|
+
Fastify's is not. That is Express's own `case sensitive routing` setting, off by default, and yours to change. The
|
|
252
|
+
MCP endpoint is the exception: on both platforms it answers its own path only, exactly as written (`/mcp`, not
|
|
253
|
+
`/MCP` or `/mcp/`).
|
|
254
|
+
- Express answers `500 A3000` to a protocol route requested with a percent-encoding a generated client never sends
|
|
255
|
+
(for example `%6Eotes` for `notes`); Fastify and the in-process protocol execute it. This is a known divergence.
|
|
182
256
|
- On Express the response carries Express's `X-Powered-By` header; `app.disable("x-powered-by")` removes it.
|
|
183
257
|
|
|
184
258
|
## License
|
|
185
259
|
|
|
186
|
-
PolyForm Shield 1.0.0 with an additional permission
|
|
187
|
-
use it to build a product that competes with Aventara itself. See `LICENSE` and
|
|
260
|
+
PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
|
|
261
|
+
may not use it to build a product that competes with Aventara itself. See `LICENSE` and
|
|
262
|
+
`LICENSE-ADDITIONAL-PERMISSION.md`.
|