@aventara/nest 0.0.0-stage → 0.1.0-pilot.0
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/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +137 -2
- package/dist/aventara.module.d.ts +49 -0
- package/dist/aventara.module.js +77 -0
- package/dist/express-protocol.mounter.d.ts +42 -0
- package/dist/express-protocol.mounter.js +65 -0
- package/dist/fastify-protocol.mounter.d.ts +68 -0
- package/dist/fastify-protocol.mounter.js +83 -0
- package/dist/framework.token.d.ts +10 -0
- package/dist/framework.token.js +13 -0
- package/dist/host-bootstrap.validator.d.ts +9 -0
- package/dist/host-bootstrap.validator.js +60 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/node-exchange.translator.d.ts +30 -0
- package/dist/node-exchange.translator.js +33 -0
- package/dist/protocol-host.factory.d.ts +40 -0
- package/dist/protocol-host.factory.js +65 -0
- package/dist/protocol-route.table.d.ts +18 -0
- package/dist/protocol-route.table.js +5 -0
- package/dist/request-body.reader.d.ts +45 -0
- package/dist/request-body.reader.js +130 -0
- package/package.json +48 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Required Notice: Copyright 2026 Mohamed Ragheb
|
|
2
|
+
|
|
3
|
+
# PolyForm Shield License 1.0.0
|
|
4
|
+
|
|
5
|
+
<https://polyformproject.org/licenses/shield/1.0.0>
|
|
6
|
+
|
|
7
|
+
## Acceptance
|
|
8
|
+
|
|
9
|
+
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
|
|
14
|
+
|
|
15
|
+
## Distribution License
|
|
16
|
+
|
|
17
|
+
The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
|
|
18
|
+
|
|
19
|
+
## Notices
|
|
20
|
+
|
|
21
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
|
|
22
|
+
|
|
23
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
24
|
+
|
|
25
|
+
## Changes and New Works License
|
|
26
|
+
|
|
27
|
+
The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
|
|
28
|
+
|
|
29
|
+
## Patent License
|
|
30
|
+
|
|
31
|
+
The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
|
|
32
|
+
|
|
33
|
+
## Noncompete
|
|
34
|
+
|
|
35
|
+
Any purpose is a permitted purpose, except for providing any product that competes with the software or any product the licensor or any of its affiliates provides using the software.
|
|
36
|
+
|
|
37
|
+
## Competition
|
|
38
|
+
|
|
39
|
+
Goods and services compete even when they provide functionality through different kinds of interfaces or for different technical platforms. Applications can compete with services, libraries with plugins, frameworks with development tools, and so on, even if they're written in different programming languages or for different computer architectures. Goods and services compete even when provided free of charge. If you market a product as a practical substitute for the software or another product, it definitely competes.
|
|
40
|
+
|
|
41
|
+
## New Products
|
|
42
|
+
|
|
43
|
+
If you are using the software to provide a product that does not compete, but the licensor or any of its affiliates brings your product into competition by providing a new version of the software or another product using the software, you may continue using versions of the software available under these terms beforehand to provide your competing product, but not any later versions.
|
|
44
|
+
|
|
45
|
+
## Discontinued Products
|
|
46
|
+
|
|
47
|
+
You may begin using the software to compete with a product or service that the licensor or any of its affiliates has stopped providing, unless the licensor includes a plain-text line beginning with `Licensor Line of Business:` with the software that mentions that line of business. For example:
|
|
48
|
+
|
|
49
|
+
> Licensor Line of Business: YoyodyneCMS Content Management System (http://example.com/cms)
|
|
50
|
+
|
|
51
|
+
## Sales of Business
|
|
52
|
+
|
|
53
|
+
If the licensor or any of its affiliates sells a line of business developing the software or using the software to provide a product, the buyer can also enforce [Noncompete](#noncompete) for that product.
|
|
54
|
+
|
|
55
|
+
## Fair Use
|
|
56
|
+
|
|
57
|
+
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
|
58
|
+
|
|
59
|
+
## No Other Rights
|
|
60
|
+
|
|
61
|
+
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
|
62
|
+
|
|
63
|
+
## Patent Defense
|
|
64
|
+
|
|
65
|
+
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
|
66
|
+
|
|
67
|
+
## Violations
|
|
68
|
+
|
|
69
|
+
The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
|
|
70
|
+
|
|
71
|
+
## No Liability
|
|
72
|
+
|
|
73
|
+
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
|
74
|
+
|
|
75
|
+
## Definitions
|
|
76
|
+
|
|
77
|
+
The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
|
|
78
|
+
|
|
79
|
+
A **product** can be a good or service, or a combination of them.
|
|
80
|
+
|
|
81
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
82
|
+
|
|
83
|
+
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all its affiliates.
|
|
84
|
+
|
|
85
|
+
**Affiliates** means the other organizations than an organization has control over, is under the control of, or is under common control with.
|
|
86
|
+
|
|
87
|
+
**Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
88
|
+
|
|
89
|
+
**Your licenses** are all the licenses granted to you for the software under these terms.
|
|
90
|
+
|
|
91
|
+
**Use** means anything you do with the software requiring one of your licenses.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Additional Permission — Aventara
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Mohamed Ragheb
|
|
4
|
+
|
|
5
|
+
In addition to the permissions of the PolyForm Shield License 1.0.0 in `LICENSE`, the licensor grants the following additional permission:
|
|
6
|
+
|
|
7
|
+
The Noncompete section applies only to providing a product that competes with the software itself — the Aventara framework, its packages and its tooling. Providing a product that competes with any other product the licensor or its affiliates provide using the software is a permitted purpose.
|
|
8
|
+
|
|
9
|
+
This additional permission only adds to your permissions; it does not restrict or replace any term of `LICENSE`.
|
package/README.md
CHANGED
|
@@ -1,3 +1,138 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@aventara/nest`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Hosts an Aventara `Framework` in a NestJS application. It does one thing: it mounts the framework's HTTP protocol —
|
|
4
|
+
`AvProtocol.bind(framework)` from `@aventara/core/protocol` — on Nest's HTTP platform, and puts the `Framework` into
|
|
5
|
+
Nest's dependency injection. Every rule of the protocol (routes, decoding, statuses, envelopes, headers, absence) is
|
|
6
|
+
core's; this package only translates a Nest request into the protocol's request and writes the protocol's answer back.
|
|
7
|
+
|
|
8
|
+
- **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
|
+
|
|
11
|
+
## Mounting it
|
|
12
|
+
|
|
13
|
+
`AventaraModule` is a global module, imported once in your root module. It takes the `Framework` in one of four forms.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// src/app.module.ts — the shape `aventara init` writes (`@aventara/cli`)
|
|
17
|
+
import { Module } from "@nestjs/common";
|
|
18
|
+
import { AventaraModule } from "@aventara/nest";
|
|
19
|
+
import { aventaraConfig } from "./aventara.config.js";
|
|
20
|
+
import { PrismaModule, PrismaService } from "./prisma.service.js";
|
|
21
|
+
|
|
22
|
+
@Module({
|
|
23
|
+
imports: [
|
|
24
|
+
PrismaModule,
|
|
25
|
+
AventaraModule.forRootAsync({
|
|
26
|
+
imports: [PrismaModule],
|
|
27
|
+
inject: [PrismaService],
|
|
28
|
+
useFactory: async (prisma: PrismaService) => ({
|
|
29
|
+
config: await aventaraConfig(prisma),
|
|
30
|
+
}),
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
})
|
|
34
|
+
export class AppModule {}
|
|
35
|
+
|
|
36
|
+
// src/aventara.config.ts — the framework's configuration: entrypoint, restrictions, pipelines
|
|
37
|
+
import { createPrismaAdapter } from "@aventara/prisma7-adapter";
|
|
38
|
+
import { discovery } from "./generated/aventara/discovery.artifact.js";
|
|
39
|
+
import type { PrismaService } from "./prisma.service.js";
|
|
40
|
+
|
|
41
|
+
export async function aventaraConfig(prisma: PrismaService) {
|
|
42
|
+
return {
|
|
43
|
+
entrypoint: "/api",
|
|
44
|
+
adapter: await createPrismaAdapter({
|
|
45
|
+
client: prisma,
|
|
46
|
+
discovery,
|
|
47
|
+
provider: "postgresql",
|
|
48
|
+
driver: "@prisma/adapter-pg",
|
|
49
|
+
}),
|
|
50
|
+
} as const;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Form | When |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `AventaraModule.forRoot({ framework })` | You called `createFramework(config)` yourself and want your own typed `Framework<M, Cfg>` in code. |
|
|
57
|
+
| `AventaraModule.forRoot({ config })` | The module calls `createFramework(config)` for you. |
|
|
58
|
+
| `AventaraModule.forRootAsync({ imports, inject, useFactory })` resolving `{ framework }` or `{ config }` | What builds the Framework (a Prisma client, a configuration service) is itself a provider. |
|
|
59
|
+
|
|
60
|
+
The `config` form is typed exactly like `createFramework`'s parameter: an annotated `FrameworkConfig<M>`, an inline
|
|
61
|
+
literal, an inferred constant and a `satisfies` check are all accepted, and checked against your Adapter's model.
|
|
62
|
+
|
|
63
|
+
**Injecting it.** `@InjectFramework()` (or `@Inject(AVENTARA_FRAMEWORK)`) injects the `Framework`. DI carries no type
|
|
64
|
+
argument, so type the parameter with your own alias:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
export type AppFramework = Awaited<ReturnType<typeof buildFramework>>;
|
|
68
|
+
|
|
69
|
+
@Injectable()
|
|
70
|
+
export class ReportsService {
|
|
71
|
+
constructor(@InjectFramework() private readonly framework: AppFramework) {}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Fails before listen.** These reject while Nest builds the application, before it listens: both or neither of
|
|
76
|
+
`framework` and `config`; a `framework` value that is not a Framework; the module mounted twice in one application; a
|
|
77
|
+
platform other than Express or Fastify (or none, as in `createApplicationContext`); a `FrameworkConstructionError`,
|
|
78
|
+
yours or the module's own, which propagates unchanged. Nest's default `abortOnError: true` **exits the process** on a
|
|
79
|
+
bootstrap error instead of rejecting; pass `abortOnError: false` to `NestFactory.create` to catch it.
|
|
80
|
+
|
|
81
|
+
**Nest's testing module works.** `Test.createTestingModule({ imports: [AppModule] }).compile()` builds the providers
|
|
82
|
+
before any HTTP platform exists; the module waits for the one `createNestApplication()` supplies, and still refuses an
|
|
83
|
+
unsupported platform before the application listens (ADR 0011's amendment). A testing module initialized with no
|
|
84
|
+
application at all is refused at `init()`, as a platform of `none`.
|
|
85
|
+
|
|
86
|
+
**Nothing to shut down.** The module owns no resource. Your Prisma provider closes its own client — in its
|
|
87
|
+
`onModuleDestroy`, with `app.enableShutdownHooks()` for signals.
|
|
88
|
+
|
|
89
|
+
## What runs on a protocol route
|
|
90
|
+
|
|
91
|
+
The protocol's routes are platform routes, registered from the module's `configure()`, one per
|
|
92
|
+
`protocol.surface()` entry — every advertised operation, `GET <entrypoint>/_contract`, and `POST
|
|
93
|
+
<entrypoint>/_transactions` only when the ClientContract advertises `interactive` — plus the absence handler for the
|
|
94
|
+
rest of `<entrypoint>/_*`. The framework's validation is outermost:
|
|
95
|
+
|
|
96
|
+
| | Express | Fastify |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| Your `app.use(...)` middleware, `app.enableCors()`, helmet | runs | runs |
|
|
99
|
+
| Nest guards, interceptors, pipes (global or not) | **never** | **never** |
|
|
100
|
+
| `MiddlewareConsumer` middleware | **never** | **runs** — Fastify runs Nest middleware before routing; add `.exclude("api/_*path")` (your entrypoint) to keep it off |
|
|
101
|
+
| Your controller at `<entrypoint>/_contract` (or any `_` path) | never reached | the application does not start |
|
|
102
|
+
| Your controller at `<entrypoint>/health` (outside `_`) | reached | reached |
|
|
103
|
+
|
|
104
|
+
Authentication and authorization therefore belong in Aventara **pipelines** (guards), which read
|
|
105
|
+
`transport.headers` — lower-cased names, repeats joined. A Passport strategy run as Nest middleware sets `req.user`,
|
|
106
|
+
which a pipeline cannot see: verify the bearer token or cookie in the Aventara guard instead.
|
|
107
|
+
|
|
108
|
+
**Where it is mounted.** `FrameworkConfig.entrypoint` alone decides it. `app.setGlobalPrefix(...)` does not move the
|
|
109
|
+
protocol; set the entrypoint to where you want it, and point the generated client at the same URL.
|
|
110
|
+
|
|
111
|
+
## Requests
|
|
112
|
+
|
|
113
|
+
- **Bodies** on protocol routes are read raw — never through Nest's body parser — up to `limits.maxRequestBytes`; the
|
|
114
|
+
protocol's decoder answers an oversized body (`413 A2010`), another content type (`415 A2011`) and malformed JSON
|
|
115
|
+
(`400 A2000`). Your own routes keep Nest's parsed `req.body`. No `NestFactory.create` option is needed.
|
|
116
|
+
- **`Content-Encoding`:** `gzip`, `deflate` and `br` are inflated, and the limit counts decoded bytes. Any other coding
|
|
117
|
+
answers `415 A2011`; a body that does not decode in the coding it names answers `400 A2000`. These two answers carry a
|
|
118
|
+
newly minted `Aventara-Request-Id`, not the client's.
|
|
119
|
+
- **The request id** is the client's `Aventara-Request-Id` as it arrived, or one core mints. A middleware that rewrites
|
|
120
|
+
`req.headers` cannot change it, and `X-Request-Id` is never copied into it — set `Aventara-Request-Id` at your proxy.
|
|
121
|
+
- **A query string** is not part of the protocol path; it is ignored.
|
|
122
|
+
|
|
123
|
+
## Platform differences
|
|
124
|
+
|
|
125
|
+
- **Fastify cannot mount some Resource keys or entrypoints as literal routes.** A Resource key that needs `*` or a
|
|
126
|
+
reserved character (`: + # $ & , / ; = ? @`) in its route path, or an entrypoint containing `*`, is refused at startup
|
|
127
|
+
with a message naming it. Rename it, or host on Express, which mounts every key.
|
|
128
|
+
- **Express routing is case-insensitive** by default, so it also answers the protocol under, say, `/API/_contract`;
|
|
129
|
+
Fastify's is not. That is Express's own `case sensitive routing` setting, off by default, and yours to change.
|
|
130
|
+
- **Express answers `500 A3000`** to a protocol route requested with a percent-encoding a generated client never sends
|
|
131
|
+
(for example `%6Eotes` for `notes`); Fastify and the in-process protocol execute it. A known divergence (register
|
|
132
|
+
F-843).
|
|
133
|
+
- On Express the response carries Express's `X-Powered-By` header; `app.disable("x-powered-by")` removes it.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
PolyForm Shield 1.0.0 with an additional permission — free to use, including in commercial applications; you may not
|
|
138
|
+
use it to build a product that competes with Aventara itself. See `LICENSE` and `LICENSE-ADDITIONAL-PERMISSION.md`.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type AdapterModel, type AnyFrameworkConfig, type Framework, type FrameworkConfig } from "@aventara/core";
|
|
2
|
+
import { type DynamicModule, type FactoryProvider, type ModuleMetadata, type NestModule, type OnModuleInit } from "@nestjs/common";
|
|
3
|
+
import { type PendingProtocolHost } from "./protocol-host.factory.js";
|
|
4
|
+
/**
|
|
5
|
+
* How the application's Framework enters Nest (Q1, ruled): either the
|
|
6
|
+
* developer's own instance — they called `createFramework` — or the
|
|
7
|
+
* configuration the module hands to `createFramework` itself. Exactly one arm;
|
|
8
|
+
* the `?: never` halves make both a type error.
|
|
9
|
+
*
|
|
10
|
+
* The `config` arm is typed exactly as `createFramework`'s parameter
|
|
11
|
+
* (`Cfg & FrameworkConfig<M, Cfg>`; U12, architect 2026-10-05), so a
|
|
12
|
+
* configuration annotated `FrameworkConfig<M>`, written inline, inferred or
|
|
13
|
+
* checked with `satisfies` is accepted — and checked against its own Adapter
|
|
14
|
+
* model — wherever `createFramework` would accept it. The defaults are the
|
|
15
|
+
* untyped forms.
|
|
16
|
+
*/
|
|
17
|
+
export type AventaraModuleOptions<M extends AdapterModel = AdapterModel, Cfg = AnyFrameworkConfig> = {
|
|
18
|
+
readonly framework: Framework;
|
|
19
|
+
readonly config?: never;
|
|
20
|
+
} | {
|
|
21
|
+
readonly config: Cfg & FrameworkConfig<M, Cfg>;
|
|
22
|
+
readonly framework?: never;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* `forRootAsync`'s options: the Nest idiom when what builds the Framework
|
|
26
|
+
* (a Prisma client, a configuration service) is itself a provider. The
|
|
27
|
+
* factory resolves to either arm of {@link AventaraModuleOptions} (Q1).
|
|
28
|
+
*/
|
|
29
|
+
export type AventaraModuleAsyncOptions<M extends AdapterModel = AdapterModel, Cfg = AnyFrameworkConfig> = {
|
|
30
|
+
readonly imports?: ModuleMetadata["imports"];
|
|
31
|
+
readonly inject?: FactoryProvider["inject"];
|
|
32
|
+
readonly useFactory: (...deps: never[]) => AventaraModuleOptions<M, Cfg> | Promise<AventaraModuleOptions<M, Cfg>>;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The global module that hosts one Aventara Framework (Q2, Q3), and mounts its
|
|
36
|
+
* protocol on the application's platform from `configure()` (Q5): after the
|
|
37
|
+
* developer's `app.use` / `enableCors`, before `MiddlewareConsumer` middleware
|
|
38
|
+
* and every controller, so the reserved `_` namespace cannot be shadowed.
|
|
39
|
+
*/
|
|
40
|
+
export declare class AventaraModule implements NestModule, OnModuleInit {
|
|
41
|
+
private readonly host;
|
|
42
|
+
constructor(host: PendingProtocolHost);
|
|
43
|
+
configure(): void;
|
|
44
|
+
/** An application context never receives a platform: refused here, at its init (N1). */
|
|
45
|
+
onModuleInit(): void;
|
|
46
|
+
static forRoot<M extends AdapterModel, const Cfg = AnyFrameworkConfig>(options: AventaraModuleOptions<M, Cfg>): DynamicModule;
|
|
47
|
+
static forRootAsync<M extends AdapterModel, const Cfg = AnyFrameworkConfig>(options: AventaraModuleAsyncOptions<M, Cfg>): DynamicModule;
|
|
48
|
+
private static mount;
|
|
49
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createFramework, } from "@aventara/core";
|
|
2
|
+
import { Inject, Module, } from "@nestjs/common";
|
|
3
|
+
import { HttpAdapterHost } from "@nestjs/core";
|
|
4
|
+
import { AVENTARA_FRAMEWORK } from "./framework.token.js";
|
|
5
|
+
import { checkedOptions, recordMount } from "./host-bootstrap.validator.js";
|
|
6
|
+
import { createPendingProtocolHost, } from "./protocol-host.factory.js";
|
|
7
|
+
/**
|
|
8
|
+
* One value per `forRoot`/`forRootAsync` call, carried in the module's
|
|
9
|
+
* metadata: under Nest's opt-in `deep-hash` module ids, two calls whose
|
|
10
|
+
* metadata serialized alike would collapse into one module and the second
|
|
11
|
+
* mount would vanish instead of being refused (Q18).
|
|
12
|
+
*/
|
|
13
|
+
const MOUNT = Symbol("AVENTARA_MOUNT");
|
|
14
|
+
let mounts = 0;
|
|
15
|
+
/** The bound protocol and its mounter for this application's platform. */
|
|
16
|
+
const PROTOCOL_HOST = Symbol("AVENTARA_PROTOCOL_HOST");
|
|
17
|
+
/**
|
|
18
|
+
* The Framework the options name: the developer's instance, or the one
|
|
19
|
+
* `createFramework` builds from the configuration — whose
|
|
20
|
+
* `FrameworkConstructionError` propagates unchanged (Q1).
|
|
21
|
+
*/
|
|
22
|
+
async function frameworkOf(options) {
|
|
23
|
+
const checked = checkedOptions(options);
|
|
24
|
+
return checked.framework ?? createFramework(checked.config);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The global module that hosts one Aventara Framework (Q2, Q3), and mounts its
|
|
28
|
+
* protocol on the application's platform from `configure()` (Q5): after the
|
|
29
|
+
* developer's `app.use` / `enableCors`, before `MiddlewareConsumer` middleware
|
|
30
|
+
* and every controller, so the reserved `_` namespace cannot be shadowed.
|
|
31
|
+
*/
|
|
32
|
+
export class AventaraModule {
|
|
33
|
+
host;
|
|
34
|
+
constructor(host) {
|
|
35
|
+
this.host = host;
|
|
36
|
+
}
|
|
37
|
+
configure() {
|
|
38
|
+
this.host.mount();
|
|
39
|
+
}
|
|
40
|
+
/** An application context never receives a platform: refused here, at its init (N1). */
|
|
41
|
+
onModuleInit() {
|
|
42
|
+
this.host.assertReady();
|
|
43
|
+
}
|
|
44
|
+
static forRoot(options) {
|
|
45
|
+
return AventaraModule.mount([], [], () => options);
|
|
46
|
+
}
|
|
47
|
+
static forRootAsync(options) {
|
|
48
|
+
return AventaraModule.mount(options.imports ?? [], options.inject ?? [], options.useFactory);
|
|
49
|
+
}
|
|
50
|
+
static mount(imports, inject, resolve) {
|
|
51
|
+
mounts += 1;
|
|
52
|
+
return {
|
|
53
|
+
module: AventaraModule,
|
|
54
|
+
global: true,
|
|
55
|
+
imports,
|
|
56
|
+
providers: [
|
|
57
|
+
{ provide: MOUNT, useValue: mounts },
|
|
58
|
+
{
|
|
59
|
+
provide: AVENTARA_FRAMEWORK,
|
|
60
|
+
inject: [HttpAdapterHost, ...inject],
|
|
61
|
+
useFactory: async (application, ...deps) => {
|
|
62
|
+
recordMount(application);
|
|
63
|
+
return frameworkOf(await resolve(...deps));
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
provide: PROTOCOL_HOST,
|
|
68
|
+
inject: [HttpAdapterHost, AVENTARA_FRAMEWORK],
|
|
69
|
+
useFactory: (application, framework) => createPendingProtocolHost(application, framework),
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
exports: [AVENTARA_FRAMEWORK],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
Inject(PROTOCOL_HOST)(AventaraModule, undefined, 0);
|
|
77
|
+
Module({})(AventaraModule);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AvProtocolBinding } from "@aventara/core/protocol";
|
|
2
|
+
import { type NodeIncoming, type NodeOutgoing } from "./node-exchange.translator.js";
|
|
3
|
+
import type { ProtocolHost } from "./protocol-host.factory.js";
|
|
4
|
+
/**
|
|
5
|
+
* The protocol on Nest's Express platform (Q4, Q5, Q8, Q9, Q11, Q12, P1–P3).
|
|
6
|
+
*
|
|
7
|
+
* - **The pre-parser** (Q8), installed while Nest instantiates providers —
|
|
8
|
+
* before Nest's own body parsers: a request under `<entrypoint>/_` has its
|
|
9
|
+
* body read raw, bounded and decoded, and kept for its route. Its stream is
|
|
10
|
+
* then finished, so Nest's parsers skip it; every other request keeps
|
|
11
|
+
* Nest's parsed body. It reads; it answers nothing.
|
|
12
|
+
* - **The routes**, mounted from the module's `configure()` (Q5): one platform
|
|
13
|
+
* route per `protocol.surface()` entry, each at its escaped literal path
|
|
14
|
+
* under the entrypoint, then one fallback — `protocol.answerAbsent`, the
|
|
15
|
+
* absence handler, not a catch-all: it executes nothing, and a path it does
|
|
16
|
+
* not own (`undefined`) goes on to Nest.
|
|
17
|
+
*
|
|
18
|
+
* Its whole job is translation: the Express request into an
|
|
19
|
+
* `AvProtocolRequest`, the `AvProtocolResponse` back onto the wire. Express's
|
|
20
|
+
* types are the slices below (P4: no `@types/node`, no `@types/express`).
|
|
21
|
+
*/
|
|
22
|
+
/** Express's request: Node's `IncomingMessage`, plus `originalUrl`. */
|
|
23
|
+
type ExpressRequest = NodeIncoming & {
|
|
24
|
+
readonly method: string;
|
|
25
|
+
readonly originalUrl: string;
|
|
26
|
+
};
|
|
27
|
+
/** Express's response, written through Node's own `writeHead` (P1: never `send`/`json`). */
|
|
28
|
+
type ExpressResponse = NodeOutgoing;
|
|
29
|
+
/** Express passes `next`; Nest's handler type declares it as an optional `Function`. */
|
|
30
|
+
type ExpressHandler = (request: ExpressRequest, response: ExpressResponse, next?: unknown) => unknown;
|
|
31
|
+
/** The slice of Nest's `ExpressAdapter` this mounter registers on. */
|
|
32
|
+
export type ExpressPlatform = {
|
|
33
|
+
get(path: string, handler: ExpressHandler): unknown;
|
|
34
|
+
post(path: string, handler: ExpressHandler): unknown;
|
|
35
|
+
use(handler: ExpressHandler): unknown;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Installs the pre-parser on `platform` now, and returns the host whose
|
|
39
|
+
* `mount` registers the routes and the absence fallback under `entrypoint`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createExpressProtocolHost(platform: ExpressPlatform, binding: AvProtocolBinding, entrypoint: string, maxRequestBytes: number): ProtocolHost;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { NO_BYTES, pathnameOf, protocolRequest, writeAnswer, } from "./node-exchange.translator.js";
|
|
2
|
+
import { PROTOCOL_ROUTES } from "./protocol-route.table.js";
|
|
3
|
+
import { readRequestBody, unreadBodyAnswer, } from "./request-body.reader.js";
|
|
4
|
+
/** The platform method that registers a route of each surface method. */
|
|
5
|
+
const REGISTER = { GET: "get", POST: "post" };
|
|
6
|
+
/** A request the pre-parser did not read: zero bytes. */
|
|
7
|
+
const NOTHING_READ = { kind: "read", content: NO_BYTES };
|
|
8
|
+
/**
|
|
9
|
+
* Installs the pre-parser on `platform` now, and returns the host whose
|
|
10
|
+
* `mount` registers the routes and the absence fallback under `entrypoint`.
|
|
11
|
+
*/
|
|
12
|
+
export function createExpressProtocolHost(platform, binding, entrypoint, maxRequestBytes) {
|
|
13
|
+
const bodies = new WeakMap();
|
|
14
|
+
const bodyOf = (request) => bodies.get(request) ?? NOTHING_READ;
|
|
15
|
+
platform.use(async (request, _response, next) => {
|
|
16
|
+
if (pathnameOf(request.originalUrl).startsWith(`${entrypoint}/_`)) {
|
|
17
|
+
bodies.set(request, await readRequestBody(request, request.headersDistinct["content-encoding"], maxRequestBytes));
|
|
18
|
+
}
|
|
19
|
+
handOn(next);
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
mount: () => {
|
|
23
|
+
for (const route of binding.surface()) {
|
|
24
|
+
const answer = PROTOCOL_ROUTES[route.kind](binding);
|
|
25
|
+
platform[REGISTER[route.method]](literalPath(`${entrypoint}${route.path}`), async (request, response) => {
|
|
26
|
+
const body = bodyOf(request);
|
|
27
|
+
writeAnswer(response, body.kind === "read"
|
|
28
|
+
? await answer(translated(request, entrypoint, body.content))
|
|
29
|
+
: unreadBodyAnswer(binding, body));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
platform.use((request, response, next) => {
|
|
33
|
+
if (!pathnameOf(request.originalUrl).startsWith(`${entrypoint}/`)) {
|
|
34
|
+
handOn(next);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const body = bodyOf(request);
|
|
38
|
+
const absent = binding.answerAbsent(translated(request, entrypoint, body.kind === "read" ? body.content : NO_BYTES));
|
|
39
|
+
if (absent === undefined) {
|
|
40
|
+
handOn(next);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
writeAnswer(response, absent);
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Hands the request on to whatever Express runs next — Nest's own routes. */
|
|
49
|
+
function handOn(next) {
|
|
50
|
+
if (typeof next === "function") {
|
|
51
|
+
next();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The route path as a literal for Express's router (`path-to-regexp` 8):
|
|
56
|
+
* every character it reads as syntax is escaped, so a Resource key never
|
|
57
|
+
* mounts a parameter or a wildcard (H11, DA-13).
|
|
58
|
+
*/
|
|
59
|
+
function literalPath(path) {
|
|
60
|
+
return path.replace(/[()[\]{}*+?!:\\]/g, "\\$&");
|
|
61
|
+
}
|
|
62
|
+
/** The Express request as the protocol reads it. */
|
|
63
|
+
function translated(request, entrypoint, content) {
|
|
64
|
+
return protocolRequest(request.method, request.originalUrl, request, entrypoint, content);
|
|
65
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { AvProtocolBinding } from "@aventara/core/protocol";
|
|
2
|
+
import { type NodeIncoming, type NodeOutgoing, type PlatformHeaders } from "./node-exchange.translator.js";
|
|
3
|
+
import type { ProtocolHost } from "./protocol-host.factory.js";
|
|
4
|
+
/**
|
|
5
|
+
* The protocol on Nest's Fastify platform (Q10; Q8, Q9, Q11, Q12, P1–P3 as on
|
|
6
|
+
* Express), mounted from the module's `configure()` (Q5). Measured in S4:
|
|
7
|
+
*
|
|
8
|
+
* - **One Fastify route per `protocol.surface()` entry**, at its literal path
|
|
9
|
+
* under the entrypoint, with Fastify's automatic `HEAD` route off (U9), so
|
|
10
|
+
* `HEAD /_contract` reaches the protocol's `405` as on Express (P3).
|
|
11
|
+
* - **Each route is answered from its own `onRequest` hook** — Fastify's first
|
|
12
|
+
* step, before its content-type check and body parsing: the body is read raw
|
|
13
|
+
* off Node's request by the bounded reader, the bound member answers, and the
|
|
14
|
+
* reply is hijacked. Fastify's parsers, `bodyLimit` and early `415` (on a
|
|
15
|
+
* `Content-Type` it cannot parse) never run on a protocol route, so every
|
|
16
|
+
* body failure is the protocol's own answer, as on Express (director,
|
|
17
|
+
* 2026-10-05). The route handler is never reached.
|
|
18
|
+
* - **The absence fallback** is a root `onRequest` hook acting only on a
|
|
19
|
+
* request Fastify matched to no route (`request.is404`) under
|
|
20
|
+
* `<entrypoint>/`: `protocol.answerAbsent`, and `undefined` hands the request
|
|
21
|
+
* on to Nest's own not-found handler (U8, director, 2026-10-05). A not-found
|
|
22
|
+
* handler of our own could not hand back: inside one, Fastify answers its
|
|
23
|
+
* plain-text 404, not Nest's.
|
|
24
|
+
* - **Literal paths** (architect, 2026-10-05): each route is registered
|
|
25
|
+
* `decodeURI`-ed, as find-my-way decodes a request path before matching it
|
|
26
|
+
* (`café`, `a[b]`). A path find-my-way cannot match as a literal — a `*`
|
|
27
|
+
* (its wildcard) or a reserved escape (it keeps those encoded, and never
|
|
28
|
+
* matches them against a static route) — is refused while Nest builds the
|
|
29
|
+
* application, naming the Resource keys; Express mounts them all.
|
|
30
|
+
* - **Writes** go through Node's response after `reply.hijack()` (P1: no
|
|
31
|
+
* charset rewrite, no lower-cased names, no added `Content-Type`), with the
|
|
32
|
+
* headers Fastify hooks already set (CORS) beneath the protocol's own —
|
|
33
|
+
* what Node's `writeHead` does on Express (director, 2026-10-05).
|
|
34
|
+
*
|
|
35
|
+
* Fastify's types are the slices below (P4).
|
|
36
|
+
*/
|
|
37
|
+
/** Fastify's request: Node's own under `raw`, and whether a route matched. */
|
|
38
|
+
type FastifyRequest = {
|
|
39
|
+
readonly raw: NodeIncoming & {
|
|
40
|
+
readonly method: string;
|
|
41
|
+
readonly url: string;
|
|
42
|
+
};
|
|
43
|
+
readonly is404: boolean;
|
|
44
|
+
};
|
|
45
|
+
/** Fastify's reply: hijacked, then written through Node's response under `raw`. */
|
|
46
|
+
type FastifyReply = {
|
|
47
|
+
hijack(): unknown;
|
|
48
|
+
getHeaders(): PlatformHeaders;
|
|
49
|
+
readonly raw: NodeOutgoing;
|
|
50
|
+
};
|
|
51
|
+
type OnRequestHook = (request: FastifyRequest, reply: FastifyReply) => Promise<FastifyReply | undefined>;
|
|
52
|
+
/** The slice of the Fastify instance (`httpAdapter.getInstance()`) this mounter registers on. */
|
|
53
|
+
export type FastifyPlatform = {
|
|
54
|
+
route(options: {
|
|
55
|
+
readonly method: string;
|
|
56
|
+
readonly url: string;
|
|
57
|
+
readonly exposeHeadRoute: false;
|
|
58
|
+
readonly onRequest: OnRequestHook;
|
|
59
|
+
readonly handler: () => never;
|
|
60
|
+
}): unknown;
|
|
61
|
+
addHook(name: "onRequest", hook: OnRequestHook): unknown;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The host whose `mount` registers the routes and the absence hook under
|
|
65
|
+
* `entrypoint` — after refusing, now, a surface it cannot mount literally.
|
|
66
|
+
*/
|
|
67
|
+
export declare function createFastifyProtocolHost(platform: FastifyPlatform, binding: AvProtocolBinding, entrypoint: string, maxRequestBytes: number): ProtocolHost;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { NO_BYTES, pathnameOf, protocolRequest, writeAnswer, } from "./node-exchange.translator.js";
|
|
2
|
+
import { PROTOCOL_ROUTES } from "./protocol-route.table.js";
|
|
3
|
+
import { readRequestBody, unreadBodyAnswer } from "./request-body.reader.js";
|
|
4
|
+
/**
|
|
5
|
+
* What find-my-way cannot match in a static route path: its wildcard, and the
|
|
6
|
+
* escapes of the characters `decodeURI` leaves encoded (: + # $ & , / ; = ? @).
|
|
7
|
+
*/
|
|
8
|
+
const UNMATCHABLE = /\*|%(?:3A|2B|23|24|26|2C|2F|3B|3D|3F|40)/i;
|
|
9
|
+
/**
|
|
10
|
+
* Refuses an entrypoint find-my-way cannot match literally — a `*`, its
|
|
11
|
+
* wildcard; every other character a canonical entrypoint may carry raw
|
|
12
|
+
* mounts (measured S6; a `:` is doubled) — then, naming them, the Resource
|
|
13
|
+
* keys whose routes it cannot match literally.
|
|
14
|
+
*/
|
|
15
|
+
function checkLiteralRoutes(binding, entrypoint) {
|
|
16
|
+
if (entrypoint.includes("*")) {
|
|
17
|
+
throw new Error(`AventaraModule cannot mount the entrypoint \`${entrypoint}\` as a literal path on Nest's Fastify platform: it may not carry \`*\`. Change the entrypoint, or host on Express.`);
|
|
18
|
+
}
|
|
19
|
+
const refused = binding
|
|
20
|
+
.surface()
|
|
21
|
+
.flatMap((route) => route.kind === "operation" && UNMATCHABLE.test(route.path)
|
|
22
|
+
? [route.resource]
|
|
23
|
+
: [])
|
|
24
|
+
.filter((key, index, keys) => keys.indexOf(key) === index);
|
|
25
|
+
if (refused.length > 0) {
|
|
26
|
+
throw new Error(`AventaraModule cannot mount Resource ${refused.map((key) => `\`${key}\``).join(", ")} as literal routes on Nest's Fastify platform: a route path may not carry \`*\` or a reserved character (: + # $ & , / ; = ? @). Rename the Resource, or host on Express.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The host whose `mount` registers the routes and the absence hook under
|
|
31
|
+
* `entrypoint` — after refusing, now, a surface it cannot mount literally.
|
|
32
|
+
*/
|
|
33
|
+
export function createFastifyProtocolHost(platform, binding, entrypoint, maxRequestBytes) {
|
|
34
|
+
checkLiteralRoutes(binding, entrypoint);
|
|
35
|
+
const translated = (request, content) => protocolRequest(request.raw.method, request.raw.url, request.raw, entrypoint, content);
|
|
36
|
+
return {
|
|
37
|
+
mount: () => {
|
|
38
|
+
for (const route of binding.surface()) {
|
|
39
|
+
const answer = PROTOCOL_ROUTES[route.kind](binding);
|
|
40
|
+
platform.route({
|
|
41
|
+
method: route.method,
|
|
42
|
+
url: literalPath(`${entrypoint}${route.path}`),
|
|
43
|
+
exposeHeadRoute: false,
|
|
44
|
+
onRequest: async (request, reply) => {
|
|
45
|
+
const body = await readRequestBody(request.raw, request.raw.headersDistinct["content-encoding"], maxRequestBytes);
|
|
46
|
+
write(reply, body.kind === "read"
|
|
47
|
+
? await answer(translated(request, body.content))
|
|
48
|
+
: unreadBodyAnswer(binding, body));
|
|
49
|
+
return reply;
|
|
50
|
+
},
|
|
51
|
+
handler: () => {
|
|
52
|
+
throw new Error("AventaraModule: a protocol route's onRequest hook answers every request; its handler is unreachable.");
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
platform.addHook("onRequest", async (request, reply) => {
|
|
57
|
+
if (!request.is404 ||
|
|
58
|
+
!pathnameOf(request.raw.url).startsWith(`${entrypoint}/`)) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const absent = binding.answerAbsent(translated(request, NO_BYTES));
|
|
62
|
+
if (absent === undefined) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
write(reply, absent);
|
|
66
|
+
return reply;
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The route path as find-my-way matches it: `decodeURI`-ed, as it decodes a
|
|
73
|
+
* request path before the lookup, and every `:` (raw only in the entrypoint —
|
|
74
|
+
* the surface encodes it) doubled, find-my-way's escape for a literal colon.
|
|
75
|
+
*/
|
|
76
|
+
function literalPath(path) {
|
|
77
|
+
return decodeURI(path).replaceAll(":", "::");
|
|
78
|
+
}
|
|
79
|
+
/** The protocol's answer on the hijacked reply, the hooks' headers beneath it. */
|
|
80
|
+
function write(reply, answer) {
|
|
81
|
+
reply.hijack();
|
|
82
|
+
writeAnswer(reply.raw, answer, reply.getHeaders());
|
|
83
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DI token under which `AventaraModule` provides the application's
|
|
3
|
+
* `Framework` (Q2). A symbol, so injection is exact: an abstract-class token
|
|
4
|
+
* would type the injected value as an empty class, not the application's own
|
|
5
|
+
* `Framework<M, Cfg>` (Q3 — the application types the injection with its
|
|
6
|
+
* own alias).
|
|
7
|
+
*/
|
|
8
|
+
export declare const AVENTARA_FRAMEWORK: unique symbol;
|
|
9
|
+
/** `@InjectFramework()` — `@Inject(AVENTARA_FRAMEWORK)` in one call (Q2). */
|
|
10
|
+
export declare function InjectFramework(): ParameterDecorator;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Inject } from "@nestjs/common";
|
|
2
|
+
/**
|
|
3
|
+
* The DI token under which `AventaraModule` provides the application's
|
|
4
|
+
* `Framework` (Q2). A symbol, so injection is exact: an abstract-class token
|
|
5
|
+
* would type the injected value as an empty class, not the application's own
|
|
6
|
+
* `Framework<M, Cfg>` (Q3 — the application types the injection with its
|
|
7
|
+
* own alias).
|
|
8
|
+
*/
|
|
9
|
+
export const AVENTARA_FRAMEWORK = Symbol("AVENTARA_FRAMEWORK");
|
|
10
|
+
/** `@InjectFramework()` — `@Inject(AVENTARA_FRAMEWORK)` in one call (Q2). */
|
|
11
|
+
export function InjectFramework() {
|
|
12
|
+
return Inject(AVENTARA_FRAMEWORK);
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AventaraModuleOptions } from "./aventara.module.js";
|
|
2
|
+
/** The options as given — `forRootAsync`'s factory may resolve anything at runtime. */
|
|
3
|
+
export declare function checkedOptions(options: unknown): AventaraModuleOptions;
|
|
4
|
+
/** Records one mount in `application`; a second is refused. */
|
|
5
|
+
export declare function recordMount(application: object): void;
|
|
6
|
+
/** The Nest HTTP platforms the module mounts the protocol on (Q10). */
|
|
7
|
+
export type HostPlatform = "express" | "fastify";
|
|
8
|
+
/** The application's platform, as `httpAdapter.getType()` names it — or a refusal. */
|
|
9
|
+
export declare function checkedPlatform(type: string | undefined): HostPlatform;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Q18's bootstrap checks, all at module setup, so `NestFactory.create`
|
|
3
|
+
* rejects before the application listens: plain `Error`s with fixed messages
|
|
4
|
+
* (no new public name — Q18 a). A `FrameworkConstructionError` is core's and
|
|
5
|
+
* passes through untouched; nothing here wraps it.
|
|
6
|
+
*/
|
|
7
|
+
const NEITHER_ARM = "AventaraModule: the options carry neither `framework` nor `config`; pass exactly one.";
|
|
8
|
+
const BOTH_ARMS = "AventaraModule: the options carry both `framework` and `config`; pass exactly one.";
|
|
9
|
+
const NOT_A_FRAMEWORK = "AventaraModule: `framework` is not an Aventara Framework (it has no string `entrypoint` and no `contracts.client`); pass what `createFramework` resolved to.";
|
|
10
|
+
const MOUNTED_TWICE = "AventaraModule is mounted twice in one application; import AventaraModule.forRoot or forRootAsync once.";
|
|
11
|
+
/** The options as given — `forRootAsync`'s factory may resolve anything at runtime. */
|
|
12
|
+
export function checkedOptions(options) {
|
|
13
|
+
const record = typeof options === "object" && options !== null
|
|
14
|
+
? options
|
|
15
|
+
: {};
|
|
16
|
+
const hasFramework = record.framework !== undefined;
|
|
17
|
+
const hasConfig = record.config !== undefined;
|
|
18
|
+
if (hasFramework && hasConfig) {
|
|
19
|
+
throw new Error(BOTH_ARMS);
|
|
20
|
+
}
|
|
21
|
+
if (!hasFramework && !hasConfig) {
|
|
22
|
+
throw new Error(NEITHER_ARM);
|
|
23
|
+
}
|
|
24
|
+
if (hasFramework && !isFramework(record.framework)) {
|
|
25
|
+
throw new Error(NOT_A_FRAMEWORK);
|
|
26
|
+
}
|
|
27
|
+
return record;
|
|
28
|
+
}
|
|
29
|
+
/** What the host reads of a Framework: its entrypoint, and its compiled ClientContract. */
|
|
30
|
+
function isFramework(value) {
|
|
31
|
+
if (typeof value !== "object" || value === null) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const candidate = value;
|
|
35
|
+
return (typeof candidate.entrypoint === "string" &&
|
|
36
|
+
typeof candidate.contracts?.client === "object" &&
|
|
37
|
+
candidate.contracts.client !== null);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Applications that already mount the module, keyed by an object Nest creates
|
|
41
|
+
* once per application (its `HttpAdapterHost`). Weak, so a closed
|
|
42
|
+
* application's entry goes with it.
|
|
43
|
+
*/
|
|
44
|
+
const mounted = new WeakSet();
|
|
45
|
+
/** Records one mount in `application`; a second is refused. */
|
|
46
|
+
export function recordMount(application) {
|
|
47
|
+
if (mounted.has(application)) {
|
|
48
|
+
throw new Error(MOUNTED_TWICE);
|
|
49
|
+
}
|
|
50
|
+
mounted.add(application);
|
|
51
|
+
}
|
|
52
|
+
const HOST_PLATFORMS = ["express", "fastify"];
|
|
53
|
+
/** The application's platform, as `httpAdapter.getType()` names it — or a refusal. */
|
|
54
|
+
export function checkedPlatform(type) {
|
|
55
|
+
const platform = HOST_PLATFORMS.find((supported) => supported === type);
|
|
56
|
+
if (platform === undefined) {
|
|
57
|
+
throw new Error(`AventaraModule mounts the protocol on Nest's Express or Fastify platform; this application's platform is \`${type ?? "none"}\`.`);
|
|
58
|
+
}
|
|
59
|
+
return platform;
|
|
60
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AvProtocolRequest, AvProtocolResponse } from "@aventara/core/protocol";
|
|
2
|
+
/**
|
|
3
|
+
* The translation both platforms share, because both hand the host Node's own
|
|
4
|
+
* request and response (Q10: shared only as measured — S4 ran the corpus on
|
|
5
|
+
* Express and Fastify through it). Node's types are the slices below (P4).
|
|
6
|
+
*/
|
|
7
|
+
/** The slice of Node's `IncomingMessage` the host reads. */
|
|
8
|
+
export type NodeIncoming = AsyncIterable<Uint8Array> & {
|
|
9
|
+
readonly headersDistinct: Readonly<Record<string, readonly string[]>>;
|
|
10
|
+
};
|
|
11
|
+
/** The slice of Node's `ServerResponse` the host writes (P1: no platform reply path). */
|
|
12
|
+
export type NodeOutgoing = {
|
|
13
|
+
writeHead(status: number, headers: Readonly<Record<string, string | number | readonly string[]>>): unknown;
|
|
14
|
+
end(body: string): unknown;
|
|
15
|
+
};
|
|
16
|
+
/** Headers a platform's own hooks set on the reply before the protocol answered. */
|
|
17
|
+
export type PlatformHeaders = Readonly<Record<string, string | number | readonly string[] | undefined>>;
|
|
18
|
+
/** Zero bytes: the body of a request whose body the protocol does not read. */
|
|
19
|
+
export declare const NO_BYTES: Uint8Array<ArrayBuffer>;
|
|
20
|
+
/** A request URL's raw pathname — query dropped (Q12), not URL-decoded (P2). */
|
|
21
|
+
export declare function pathnameOf(url: string): string;
|
|
22
|
+
/** The request as the protocol reads it: path relative to the entrypoint, `headersDistinct` (Q11), the raw arm (Q8). */
|
|
23
|
+
export declare function protocolRequest(method: string, url: string, incoming: NodeIncoming, entrypoint: string, content: Uint8Array): AvProtocolRequest;
|
|
24
|
+
/**
|
|
25
|
+
* The protocol's answer, written as it is (P1). Headers the platform's hooks
|
|
26
|
+
* already set (CORS, say) go beneath it; a protocol header of the same name,
|
|
27
|
+
* in any case, wins — what Node's `writeHead` does with `setHeader`'s on
|
|
28
|
+
* Express.
|
|
29
|
+
*/
|
|
30
|
+
export declare function writeAnswer(response: NodeOutgoing, answer: AvProtocolResponse, beneath?: PlatformHeaders): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Zero bytes: the body of a request whose body the protocol does not read. */
|
|
2
|
+
export const NO_BYTES = new Uint8Array();
|
|
3
|
+
/** A request URL's raw pathname — query dropped (Q12), not URL-decoded (P2). */
|
|
4
|
+
export function pathnameOf(url) {
|
|
5
|
+
const query = url.indexOf("?");
|
|
6
|
+
return query === -1 ? url : url.slice(0, query);
|
|
7
|
+
}
|
|
8
|
+
/** The request as the protocol reads it: path relative to the entrypoint, `headersDistinct` (Q11), the raw arm (Q8). */
|
|
9
|
+
export function protocolRequest(method, url, incoming, entrypoint, content) {
|
|
10
|
+
return {
|
|
11
|
+
method,
|
|
12
|
+
path: pathnameOf(url).slice(entrypoint.length),
|
|
13
|
+
headers: incoming.headersDistinct,
|
|
14
|
+
body: { kind: "raw", content },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The protocol's answer, written as it is (P1). Headers the platform's hooks
|
|
19
|
+
* already set (CORS, say) go beneath it; a protocol header of the same name,
|
|
20
|
+
* in any case, wins — what Node's `writeHead` does with `setHeader`'s on
|
|
21
|
+
* Express.
|
|
22
|
+
*/
|
|
23
|
+
export function writeAnswer(response, answer, beneath = {}) {
|
|
24
|
+
const own = new Set(Object.keys(answer.headers).map((name) => name.toLowerCase()));
|
|
25
|
+
const headers = {};
|
|
26
|
+
for (const [name, value] of Object.entries(beneath)) {
|
|
27
|
+
if (value !== undefined && !own.has(name.toLowerCase())) {
|
|
28
|
+
headers[name] = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
response.writeHead(answer.status, { ...headers, ...answer.headers });
|
|
32
|
+
response.end(answer.body);
|
|
33
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Framework } from "@aventara/core";
|
|
2
|
+
import type { AbstractHttpAdapter } from "@nestjs/core";
|
|
3
|
+
/** The protocol, bound once at startup, and the one call that mounts it. */
|
|
4
|
+
export type ProtocolHost = {
|
|
5
|
+
readonly mount: () => void;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Binds `framework`'s protocol for the application's platform, refusing an
|
|
9
|
+
* unsupported platform while providers are instantiated — so
|
|
10
|
+
* `NestFactory.create` rejects before the application listens (Q18). The
|
|
11
|
+
* routes are mounted later, from the module's `configure()` (Q5).
|
|
12
|
+
*/
|
|
13
|
+
export declare function createProtocolHost(adapter: AbstractHttpAdapter | undefined, framework: Framework): ProtocolHost;
|
|
14
|
+
/** What `AventaraModule` holds: a host, perhaps waiting for its platform, and the check that it has one. */
|
|
15
|
+
export type PendingProtocolHost = ProtocolHost & {
|
|
16
|
+
/** Throws when no platform arrived, or the one that did is refused. */
|
|
17
|
+
readonly assertReady: () => void;
|
|
18
|
+
};
|
|
19
|
+
/** The parts of Nest's `HttpAdapterHost` the module reads. */
|
|
20
|
+
export type AdapterHostView = {
|
|
21
|
+
readonly httpAdapter: AbstractHttpAdapter | undefined;
|
|
22
|
+
readonly init$: {
|
|
23
|
+
subscribe(next: () => void): unknown;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* N1 (plan B23) — a host for an application whose HTTP adapter may arrive after
|
|
28
|
+
* the providers are instantiated.
|
|
29
|
+
*
|
|
30
|
+
* `NestFactory.create` sets the adapter first: the host is created at once, and
|
|
31
|
+
* an unsupported platform rejects then, as before (Q18). Nest's testing module
|
|
32
|
+
* compiles first and receives its adapter from `createNestApplication()`, which
|
|
33
|
+
* sets it on the `HttpAdapterHost` — `init$` fires — before `app.init()`
|
|
34
|
+
* registers Nest's body parsers: the host is created at that moment, so a
|
|
35
|
+
* platform's body reader is still installed ahead of them (Q8). A refusal there
|
|
36
|
+
* is kept and raised by `mount()` or `assertReady()`, from `app.init()` — still
|
|
37
|
+
* before the application listens. With no adapter at all
|
|
38
|
+
* (`createApplicationContext`), `assertReady()` refuses from `onModuleInit`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createPendingProtocolHost(application: AdapterHostView, framework: Framework): PendingProtocolHost;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { AvProtocol } from "@aventara/core/protocol";
|
|
2
|
+
import { createExpressProtocolHost } from "./express-protocol.mounter.js";
|
|
3
|
+
import { createFastifyProtocolHost, } from "./fastify-protocol.mounter.js";
|
|
4
|
+
import { checkedPlatform, } from "./host-bootstrap.validator.js";
|
|
5
|
+
const PLATFORM_MOUNTERS = {
|
|
6
|
+
express: (adapter, binding, entrypoint, maxRequestBytes) => createExpressProtocolHost({
|
|
7
|
+
get: (path, handler) => adapter.get(path, handler),
|
|
8
|
+
post: (path, handler) => adapter.post(path, handler),
|
|
9
|
+
use: (handler) => adapter.use(handler),
|
|
10
|
+
}, binding, entrypoint, maxRequestBytes),
|
|
11
|
+
fastify: (adapter, binding, entrypoint, maxRequestBytes) => createFastifyProtocolHost(adapter.getInstance(), binding, entrypoint, maxRequestBytes),
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Binds `framework`'s protocol for the application's platform, refusing an
|
|
15
|
+
* unsupported platform while providers are instantiated — so
|
|
16
|
+
* `NestFactory.create` rejects before the application listens (Q18). The
|
|
17
|
+
* routes are mounted later, from the module's `configure()` (Q5).
|
|
18
|
+
*/
|
|
19
|
+
export function createProtocolHost(adapter, framework) {
|
|
20
|
+
const platform = checkedPlatform(adapter?.getType());
|
|
21
|
+
return PLATFORM_MOUNTERS[platform](adapter, AvProtocol.bind(framework), framework.entrypoint, framework.contracts.client.limits.maxRequestBytes);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* N1 (plan B23) — a host for an application whose HTTP adapter may arrive after
|
|
25
|
+
* the providers are instantiated.
|
|
26
|
+
*
|
|
27
|
+
* `NestFactory.create` sets the adapter first: the host is created at once, and
|
|
28
|
+
* an unsupported platform rejects then, as before (Q18). Nest's testing module
|
|
29
|
+
* compiles first and receives its adapter from `createNestApplication()`, which
|
|
30
|
+
* sets it on the `HttpAdapterHost` — `init$` fires — before `app.init()`
|
|
31
|
+
* registers Nest's body parsers: the host is created at that moment, so a
|
|
32
|
+
* platform's body reader is still installed ahead of them (Q8). A refusal there
|
|
33
|
+
* is kept and raised by `mount()` or `assertReady()`, from `app.init()` — still
|
|
34
|
+
* before the application listens. With no adapter at all
|
|
35
|
+
* (`createApplicationContext`), `assertReady()` refuses from `onModuleInit`.
|
|
36
|
+
*/
|
|
37
|
+
export function createPendingProtocolHost(application, framework) {
|
|
38
|
+
if (application.httpAdapter !== undefined) {
|
|
39
|
+
const host = createProtocolHost(application.httpAdapter, framework);
|
|
40
|
+
return { mount: host.mount, assertReady: () => undefined };
|
|
41
|
+
}
|
|
42
|
+
let host;
|
|
43
|
+
let refusal;
|
|
44
|
+
application.init$.subscribe(() => {
|
|
45
|
+
try {
|
|
46
|
+
host = createProtocolHost(application.httpAdapter, framework);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
refusal = error;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const ready = () => {
|
|
53
|
+
if (refusal !== undefined) {
|
|
54
|
+
throw refusal;
|
|
55
|
+
}
|
|
56
|
+
// No adapter ever arrived: refused with the same sentence as at creation.
|
|
57
|
+
return host ?? createProtocolHost(undefined, framework);
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
mount: () => ready().mount(),
|
|
61
|
+
assertReady: () => {
|
|
62
|
+
ready();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AvProtocolBinding, AvProtocolRequest, AvProtocolResponse } from "@aventara/core/protocol";
|
|
2
|
+
/**
|
|
3
|
+
* One route `protocol.surface()` lists. Core keeps `SurfaceRoute` a module
|
|
4
|
+
* type, so the host derives it from the binding — no new core export.
|
|
5
|
+
*/
|
|
6
|
+
export type SurfaceRoute = ReturnType<AvProtocolBinding["surface"]>[number];
|
|
7
|
+
/** What a mounted route answers its request with: a bound protocol member. */
|
|
8
|
+
export type RouteAnswer = (request: AvProtocolRequest) => AvProtocolResponse | Promise<AvProtocolResponse>;
|
|
9
|
+
/**
|
|
10
|
+
* The bound member each kind of surface route is answered by — total over
|
|
11
|
+
* `SurfaceRoute["kind"]`, so a route kind core adds fails to compile here
|
|
12
|
+
* rather than mounting a route nothing answers. Every platform's mounter
|
|
13
|
+
* reads this one table; none decides an answer of its own.
|
|
14
|
+
*/
|
|
15
|
+
export type ProtocolRouteTable = {
|
|
16
|
+
readonly [K in SurfaceRoute["kind"]]: (binding: AvProtocolBinding) => RouteAnswer;
|
|
17
|
+
};
|
|
18
|
+
export declare const PROTOCOL_ROUTES: ProtocolRouteTable;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AvProtocolBinding, AvProtocolResponse } from "@aventara/core/protocol";
|
|
2
|
+
/**
|
|
3
|
+
* The raw arm's body reader (Q8, Q9): a request body read off a Node stream,
|
|
4
|
+
* decoded per its `Content-Encoding`, and stored up to `maxRequestBytes + 1`
|
|
5
|
+
* DECODED bytes — one more than the limit is all the decoder needs to answer
|
|
6
|
+
* `A2010`, and counting decoded bytes is also the decompression-bomb bound
|
|
7
|
+
* (§12.3, §20.1). Past the bound the stream is still read to its end and
|
|
8
|
+
* discarded, so the connection stays usable (U1); nothing more is inflated.
|
|
9
|
+
*
|
|
10
|
+
* Platform-free: it reads an async iterable of bytes, as Node's
|
|
11
|
+
* `IncomingMessage` is. `node:zlib` is loaded by a specifier the compiler does
|
|
12
|
+
* not resolve, typed by the slice below (P4: no `@types/node`).
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* What the reader hands on: the stored bytes; an unsupported coding (Q9,
|
|
16
|
+
* answered `A2011`); or a body that does not decode in the coding it names —
|
|
17
|
+
* a malformed request (director, 2026-10-05: answered `A2000`).
|
|
18
|
+
*/
|
|
19
|
+
export type RequestBodyRead = {
|
|
20
|
+
readonly kind: "read";
|
|
21
|
+
readonly content: Uint8Array;
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: "unsupported-coding";
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: "undecodable";
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The answer to a body the reader could not hand on, through the bound
|
|
29
|
+
* `failure` member — the one place the host names an A-code (Q9): an
|
|
30
|
+
* unsupported coding is `415 A2011`; a body that does not decode in its
|
|
31
|
+
* coding is malformed, `400 A2000` (director, 2026-10-05). The request id is
|
|
32
|
+
* minted by core: it exposes no adoption of the client's id to a host
|
|
33
|
+
* (register row, S8).
|
|
34
|
+
*/
|
|
35
|
+
export declare function unreadBodyAnswer(binding: AvProtocolBinding, read: Exclude<RequestBodyRead, {
|
|
36
|
+
readonly kind: "read";
|
|
37
|
+
}>): AvProtocolResponse;
|
|
38
|
+
/**
|
|
39
|
+
* Reads `source` to its end. `contentEncoding` is the header's values as
|
|
40
|
+
* received; absent, empty or `identity` is no coding; one of `gzip`,
|
|
41
|
+
* `deflate`, `br` (any case) is inflated; anything else — a stacked coding
|
|
42
|
+
* included — is unsupported. A body that does not decode in the coding it
|
|
43
|
+
* names is undecodable. Either way the stream is still read to its end.
|
|
44
|
+
*/
|
|
45
|
+
export declare function readRequestBody(source: AsyncIterable<Uint8Array>, contentEncoding: readonly string[] | undefined, cap: number): Promise<RequestBodyRead>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The answer to a body the reader could not hand on, through the bound
|
|
3
|
+
* `failure` member — the one place the host names an A-code (Q9): an
|
|
4
|
+
* unsupported coding is `415 A2011`; a body that does not decode in its
|
|
5
|
+
* coding is malformed, `400 A2000` (director, 2026-10-05). The request id is
|
|
6
|
+
* minted by core: it exposes no adoption of the client's id to a host
|
|
7
|
+
* (register row, S8).
|
|
8
|
+
*/
|
|
9
|
+
export function unreadBodyAnswer(binding, read) {
|
|
10
|
+
return read.kind === "unsupported-coding"
|
|
11
|
+
? binding.failure("A2011")
|
|
12
|
+
: binding.failure("A2000");
|
|
13
|
+
}
|
|
14
|
+
const INFLATERS = {
|
|
15
|
+
gzip: (zlib) => zlib.createGunzip(),
|
|
16
|
+
deflate: (zlib) => zlib.createInflate(),
|
|
17
|
+
br: (zlib) => zlib.createBrotliDecompress(),
|
|
18
|
+
};
|
|
19
|
+
let zlibModule;
|
|
20
|
+
const zlib = () => {
|
|
21
|
+
const specifier = "node:zlib";
|
|
22
|
+
zlibModule ??= import(specifier);
|
|
23
|
+
return zlibModule;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Reads `source` to its end. `contentEncoding` is the header's values as
|
|
27
|
+
* received; absent, empty or `identity` is no coding; one of `gzip`,
|
|
28
|
+
* `deflate`, `br` (any case) is inflated; anything else — a stacked coding
|
|
29
|
+
* included — is unsupported. A body that does not decode in the coding it
|
|
30
|
+
* names is undecodable. Either way the stream is still read to its end.
|
|
31
|
+
*/
|
|
32
|
+
export async function readRequestBody(source, contentEncoding, cap) {
|
|
33
|
+
const coding = codingOf(contentEncoding);
|
|
34
|
+
if (coding === undefined) {
|
|
35
|
+
// Read to the end and discarded all the same: the stream finished is
|
|
36
|
+
// what keeps the platform's own parsers off it (Q8), and the
|
|
37
|
+
// connection usable.
|
|
38
|
+
for await (const _ of source) {
|
|
39
|
+
}
|
|
40
|
+
return { kind: "unsupported-coding" };
|
|
41
|
+
}
|
|
42
|
+
const stored = boundedStore(cap + 1);
|
|
43
|
+
if (coding === "identity") {
|
|
44
|
+
for await (const chunk of source) {
|
|
45
|
+
stored.add(chunk);
|
|
46
|
+
}
|
|
47
|
+
return { kind: "read", content: stored.content() };
|
|
48
|
+
}
|
|
49
|
+
const inflater = INFLATERS[coding](await zlib());
|
|
50
|
+
const inflated = (async () => {
|
|
51
|
+
for await (const chunk of inflater) {
|
|
52
|
+
if (!stored.add(chunk)) {
|
|
53
|
+
inflater.destroy();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
})();
|
|
58
|
+
// Settles when the inflater stops — at its end, at the bound (destroyed),
|
|
59
|
+
// or at a decoding error. Node calls no write callback once an inflater
|
|
60
|
+
// has failed, so each write also waits on this, never on the callback
|
|
61
|
+
// alone. Marking it handled here keeps an early decoding error from being
|
|
62
|
+
// reported as unhandled before it is awaited below.
|
|
63
|
+
const stopped = inflated.then(() => undefined, () => undefined);
|
|
64
|
+
for await (const chunk of source) {
|
|
65
|
+
if (!inflater.destroyed) {
|
|
66
|
+
await Promise.race([
|
|
67
|
+
new Promise((written) => {
|
|
68
|
+
inflater.write(chunk, () => written());
|
|
69
|
+
}),
|
|
70
|
+
stopped,
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!inflater.destroyed) {
|
|
75
|
+
inflater.end();
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await inflated;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return { kind: "undecodable" };
|
|
82
|
+
}
|
|
83
|
+
return { kind: "read", content: stored.content() };
|
|
84
|
+
}
|
|
85
|
+
/** The coding the header names, `identity` for none, `undefined` for an unsupported one. */
|
|
86
|
+
function codingOf(contentEncoding) {
|
|
87
|
+
const named = (contentEncoding ?? [])
|
|
88
|
+
.join(",")
|
|
89
|
+
.split(",")
|
|
90
|
+
.map((token) => token.trim().toLowerCase())
|
|
91
|
+
.filter((token) => token !== "");
|
|
92
|
+
if (named.length === 0) {
|
|
93
|
+
return "identity";
|
|
94
|
+
}
|
|
95
|
+
if (named.length > 1) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
const [only] = named;
|
|
99
|
+
return only === "identity" ||
|
|
100
|
+
only === "gzip" ||
|
|
101
|
+
only === "deflate" ||
|
|
102
|
+
only === "br"
|
|
103
|
+
? only
|
|
104
|
+
: undefined;
|
|
105
|
+
}
|
|
106
|
+
/** Stores bytes up to `limit`; `add` answers whether there is room for more. */
|
|
107
|
+
function boundedStore(limit) {
|
|
108
|
+
const chunks = [];
|
|
109
|
+
let size = 0;
|
|
110
|
+
return {
|
|
111
|
+
add(chunk) {
|
|
112
|
+
const room = limit - size;
|
|
113
|
+
if (room > 0) {
|
|
114
|
+
const kept = chunk.byteLength <= room ? chunk : chunk.subarray(0, room);
|
|
115
|
+
chunks.push(kept);
|
|
116
|
+
size += kept.byteLength;
|
|
117
|
+
}
|
|
118
|
+
return size < limit;
|
|
119
|
+
},
|
|
120
|
+
content() {
|
|
121
|
+
const bytes = new Uint8Array(size);
|
|
122
|
+
let offset = 0;
|
|
123
|
+
for (const chunk of chunks) {
|
|
124
|
+
bytes.set(chunk, offset);
|
|
125
|
+
offset += chunk.byteLength;
|
|
126
|
+
}
|
|
127
|
+
return bytes;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aventara/nest",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.1.0-pilot.0",
|
|
4
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
5
|
+
"description": "NestJS host integration for Aventara.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": "^22.18.0 || >=24.2.0"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"LICENSE-ADDITIONAL-PERMISSION.md"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@aventara/core": "0.1.0-pilot.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@nestjs/common": "^12.0.0",
|
|
28
|
+
"@nestjs/core": "^12.0.0"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@aventara/prisma7-adapter": "0.1.0-pilot.0",
|
|
35
|
+
"@aventara/testing": "0.1.0-pilot.0",
|
|
36
|
+
"@nestjs/common": "^12.1.2",
|
|
37
|
+
"@nestjs/core": "^12.1.2",
|
|
38
|
+
"@nestjs/platform-express": "^12.1.2",
|
|
39
|
+
"@nestjs/platform-fastify": "^12.1.2",
|
|
40
|
+
"@nestjs/testing": "^12.1.2",
|
|
41
|
+
"@prisma/adapter-better-sqlite3": "7.10.0",
|
|
42
|
+
"reflect-metadata": "^0.2.2",
|
|
43
|
+
"rxjs": "^7.8.2"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.json",
|
|
47
|
+
"typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
|
|
48
|
+
"test": "vitest run --typecheck --config ../../vitest.config.mts --root .",
|
|
49
|
+
"lint": "biome lint src"
|
|
50
|
+
}
|
|
6
51
|
}
|