@afora/core 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -23
- package/dist/adapters.d.ts +25 -0
- package/dist/adapters.js +56 -0
- package/dist/core/application/exceptions.d.ts +17 -0
- package/dist/core/application/migrate.d.ts +19 -0
- package/dist/core/application/schema.d.ts +30 -0
- package/dist/core/application/types/application.interface.d.ts +37 -0
- package/dist/core/auth/jwt.d.ts +30 -0
- package/dist/core/auth/types/jwt.interface.d.ts +66 -0
- package/dist/core/blueprint/exceptions.d.ts +5 -0
- package/dist/core/blueprint/optional.d.ts +3 -1
- package/dist/core/blueprint/types/blueprint.interface.d.ts +79 -5
- package/dist/core/blueprint/variant.d.ts +32 -0
- package/dist/core/entity/abstract-entity.d.ts +5 -0
- package/dist/core/entity/entity.d.ts +1 -1
- package/dist/core/entity/exceptions.d.ts +9 -0
- package/dist/core/entity/trust.d.ts +24 -0
- package/dist/core/entity/types/entity.interface.d.ts +30 -0
- package/dist/core/output/access.d.ts +16 -0
- package/dist/core/output/exceptions.d.ts +16 -0
- package/dist/core/output/http.d.ts +36 -0
- package/dist/core/output/openapi-sink.d.ts +23 -2
- package/dist/core/output/stage.d.ts +7 -2
- package/dist/core/output/status.d.ts +1 -1
- package/dist/core/output/types/http.interface.d.ts +130 -0
- package/dist/core/output/types/output.interface.d.ts +25 -5
- package/dist/core/parse/issue.d.ts +28 -3
- package/dist/core/parse/parse.d.ts +9 -0
- package/dist/core/property/abstract-property.d.ts +9 -0
- package/dist/core/property/define-property.d.ts +34 -6
- package/dist/core/property/types/property-options.interface.d.ts +10 -0
- package/dist/core/property/types/property-type.interface.d.ts +4 -0
- package/dist/core/record/abstract-record.d.ts +7 -0
- package/dist/core/record/types/record.interface.d.ts +12 -1
- package/dist/core/repository/exceptions.d.ts +13 -0
- package/dist/core/repository/in-memory.d.ts +2 -2
- package/dist/core/repository/migrate.d.ts +20 -0
- package/dist/core/repository/state.d.ts +2 -0
- package/dist/core/repository/types/repository.interface.d.ts +33 -0
- package/dist/core/repository/types/schema.interface.d.ts +126 -0
- package/dist/core/repository/upcast.d.ts +64 -0
- package/dist/core/schema/schema.d.ts +28 -0
- package/dist/core/symbols/kind.d.ts +1 -1
- package/dist/core/use-case/access.d.ts +12 -0
- package/dist/core/use-case/decorators.d.ts +40 -0
- package/dist/core/use-case/route.d.ts +18 -0
- package/dist/core/use-case/steps/types/check.interface.d.ts +214 -0
- package/dist/core/use-case/types/access.interface.d.ts +39 -0
- package/dist/core/use-case/types/route.interface.d.ts +66 -0
- package/dist/core/use-case/use-case.d.ts +52 -2
- package/dist/experimental.d.ts +13 -1
- package/dist/experimental.js +63 -25
- package/dist/index-3aae5jfq.js +2997 -0
- package/dist/index-hg1z2s2v.js +1055 -0
- package/dist/index-ny3as79r.js +4567 -0
- package/dist/index-xegv6a1p.js +4291 -0
- package/dist/index-y280ew0k.js +396 -0
- package/dist/index.d.ts +45 -49
- package/dist/index.js +110 -186
- package/dist/std/properties/password.d.ts +0 -2
- package/dist/std/record-plugins.d.ts +2 -2
- package/dist/std/resource.d.ts +31 -0
- package/dist/std/runtime.d.ts +6 -0
- package/dist/std/use-cases/auth.d.ts +5 -1
- package/dist/std/use-cases/index.d.ts +1 -1
- package/dist/std/use-cases/shared.d.ts +2 -2
- package/dist/std/use-cases/types.d.ts +97 -1
- package/dist/testing/http-provider-contract.d.ts +32 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +495 -4
- package/dist/testing/provider-contract.d.ts +17 -1
- package/package.json +16 -3
- package/dist/index-qxfkyd6k.js +0 -1721
- package/dist/index-t5qwtyj5.js +0 -8194
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
<
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/afora-banner-dark.png">
|
|
4
|
+
<img alt="afora — Model the domain. Keep the invariants." src=".github/assets/afora-banner-light.png" width="100%">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
4
7
|
|
|
5
8
|
<p align="center">
|
|
6
9
|
<a href="https://www.npmjs.com/package/@afora/core"><img alt="npm" src="https://img.shields.io/npm/v/@afora/core?color=cb3837&logo=npm"></a>
|
|
@@ -20,9 +23,34 @@
|
|
|
20
23
|
<a href="https://github.com/afora-framework/core/discussions">Discussions</a>
|
|
21
24
|
</p>
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
**A backend an agent can write without breaking its guarantees.** With afora the whole application is data: an `afora.json` declares the entities, repositories, use cases, commands and HTTP API, [`@afora/codegen`](packages/codegen/README.md) turns it into TypeScript you review and commit, [`@afora/mcp`](packages/mcp/README.md) lets an agent write it, and `extractSpec(app)` gives the spec back from code written by hand. Whoever writes the spec or the code, the runtime keeps the invariants: sensitive fields are masked, undeclared keys are rejected, unique keys are enforced and business errors become problem+json with the right status.
|
|
27
|
+
|
|
28
|
+
### 30 seconds: from afora.json to a running API
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bun add @afora/core @afora/hono hono typebox && bun add -d @afora/codegen
|
|
32
|
+
bunx afora-codegen generate afora.json --stages ./stages.ts # writes src/domain/*.ts
|
|
33
|
+
bun src/server.ts
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
With the [quick start's `afora.json`](#the-same-app-as-an-aforajson) and three small files written by hand (the hashing function, the `hono` stage catalog and the server, shown there), that is the whole API: a `User` with a masked password and a unique email, a use case written as steps, problem+json errors and an OpenAPI document. The same app written by hand in TypeScript is the [quick start](#quick-start), and the [afora.json guide](docs/afora-json.md) covers the whole spec.
|
|
37
|
+
|
|
38
|
+
## Why afora
|
|
39
|
+
|
|
40
|
+
An HTTP backend written by an agent (or in a hurry) tends to break the same few guarantees. In afora each one is a mechanism of the runtime, not a convention the code has to remember:
|
|
41
|
+
|
|
42
|
+
| What usually breaks | What afora does |
|
|
43
|
+
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| A password hash or token ends up in a response | A property declared `sensitive` (`Password`) is masked by `toJSON()` at any depth, so no serializer can leak it ([records](docs/records.md)). |
|
|
45
|
+
| Extra fields in a request body are accepted | Records reject undeclared keys at any depth (issue code `undeclared_key`), and the HTTP sink answers 400 ([Standard Schema](docs/standard-schema.md)). |
|
|
46
|
+
| A business error is returned as a 500 | Exceptions know their layer; an output maps them to problem+json with a status (404, 409, 401, 422 for the errors a use case declares) and hides the detail of a real 500 ([outputs](docs/outputs.md)). |
|
|
47
|
+
| A duplicate email is written twice | `unique` keys are enforced by the repository and its provider, and a violation is a 409 ([repositories](docs/repositories.md)). |
|
|
48
|
+
|
|
49
|
+
[`examples/agent-built`](examples/agent-built/README.md) checks these four invariants on the running API of a ticketing backend built only through the MCP server, and of a Hono + Zod baseline written from the same brief. In the committed run both hold all four, but the afora one wrote no code for any of them: the spec declares the fields and errors, and the runtime does the rest. Scripted runs that measure how often each approach holds them across independent agent runs are in the example, waiting to be run with an API key.
|
|
24
50
|
|
|
25
|
-
afora is not a general-purpose validator. It
|
|
51
|
+
afora is not a general-purpose validator. It models a domain on [TypeBox](https://github.com/sinclairzx81/typebox), and every field plugs into the validators your framework already accepts through [Standard Schema](https://standardschema.dev).
|
|
52
|
+
|
|
53
|
+
## Features
|
|
26
54
|
|
|
27
55
|
- **Records with invariants.** Properties (`Email`, `Slug`, `Password`, your own) validate and normalize every value; blueprints compose them; a record (`class User extends recordOf(...)`) holds the handled value and rejects undeclared keys at any depth.
|
|
28
56
|
- **Sensitive data masking.** A property declared `sensitive` (`Password`) is masked by `toJSON()` at any depth, so `JSON.stringify(user)` cannot leak it.
|
|
@@ -31,9 +59,11 @@ afora is not a general-purpose validator. It is for code that models a domain, a
|
|
|
31
59
|
- **Exceptions that know their layer.** Each error carries a layer (`Property`, `Record`, `UseCase`, `Output`...) and a `trace` that grows as it goes up.
|
|
32
60
|
- **Standard Schema V1 on every field.** Properties, blueprints, lists and record classes work as is in Hono, tRPC and TanStack Form, and `safeParse` never throws for an invalid value.
|
|
33
61
|
- **Commands and outputs.** `commandOf` groups use cases behind one execution boundary (context, transactions, event policies); `outputOf` turns commands into operations and pipes them to a sink: an HTTP API with `@afora/hono`, WebSocket messages with `@afora/ws`, an OpenAPI 3.1 document, or MCP tools, with problem+json errors and masked responses.
|
|
62
|
+
- **Routes and access on the use case.** A use case says where it is served (`route`, or `@Route("GET", "/posts/{slug}")`) and who may call it (`access`, or `@Access({ scopes: [...] })`), next to its input; the blueprint stays the only description of the input, and every server and transport enforces the access before the use case runs. `resourceOf(Posts, { path: "/posts" })` is a whole CRUD. See the [tutorial](docs/tutorial.md).
|
|
63
|
+
- **Any server.** The same output runs on [Hono](packages/hono/README.md), [Fastify](packages/fastify/README.md), [Express](packages/express/README.md) or [Elysia](packages/elysia/README.md), with that server's own middlewares: they share one compiled route table and one contract test suite ([HTTP providers](docs/providers.md)).
|
|
34
64
|
- **Standard use cases.** `commandOf({ useCases: (ctx) => ({ create: ctx.create(Posts), publish: ctx.publish(Posts) }) })` picks ready-made use cases that follow the repository and its entity: the actions it declares, its record plugins and the properties of its keys ([docs](docs/std-use-cases.md)).
|
|
35
65
|
- **The whole application in JSON.** An `afora.json` generates the code (`@afora/codegen`), use cases included, as data (steps), and `extractSpec(app)` gives back the `afora.json` of an application written by hand.
|
|
36
|
-
- **Repositories on any store.** A provider contract with typed queries, soft deletion and transactions, a runner-agnostic kit to test one (`@afora/core/testing`), in memory by default
|
|
66
|
+
- **Repositories on any store.** A provider contract with typed queries, soft deletion and transactions, a runner-agnostic kit to test one (`@afora/core/testing`), in memory by default, on Drizzle ORM with `@afora/drizzle` and on DynamoDB with `@afora/dynamodb`.
|
|
37
67
|
|
|
38
68
|
## Install
|
|
39
69
|
|
|
@@ -58,7 +88,7 @@ configure({ hasher, idGenerator: () => myUuidV7() }); // both optional; `resetRu
|
|
|
58
88
|
|
|
59
89
|
## Quick start
|
|
60
90
|
|
|
61
|
-
A `User` entity with a sensitive key and a unique key, a repository, a use case written as steps, a command that wraps it in a transaction, and an output that serves the command as an HTTP API with an OpenAPI document. Install what it uses:
|
|
91
|
+
A `User` entity with a sensitive key and a unique key, a repository, a use case written as steps that says where it is served, a command that wraps it in a transaction, and an output that serves the command as an HTTP API with an OpenAPI document. Install what it uses:
|
|
62
92
|
|
|
63
93
|
```bash
|
|
64
94
|
bun add @afora/core @afora/hono hono typebox
|
|
@@ -106,9 +136,10 @@ async function hashPassword(plain: string) {
|
|
|
106
136
|
return (await Password.hash(plain)).value;
|
|
107
137
|
}
|
|
108
138
|
|
|
109
|
-
// Use case: the steps are data, so they can also live in an afora.json
|
|
139
|
+
// Use case: the steps and the route are data, so they can also live in an afora.json
|
|
110
140
|
class Register extends useCaseOf({
|
|
111
141
|
name: "Register",
|
|
142
|
+
route: { method: "post", path: "/users", status: 201 }, // where it is served
|
|
112
143
|
blueprint: blueprintOf((ctx) => ({
|
|
113
144
|
name: ctx.text,
|
|
114
145
|
email: ctx.email,
|
|
@@ -167,8 +198,8 @@ if (import.meta.main) Bun.serve({ fetch: api.fetch });
|
|
|
167
198
|
What it does:
|
|
168
199
|
|
|
169
200
|
```
|
|
170
|
-
POST /api/
|
|
171
|
-
->
|
|
201
|
+
POST /api/users { "name": "Ana", "email": " Ana@Example.COM ", "plainPassword": "Str0ng!Pass" }
|
|
202
|
+
-> 201 { "user": { "id": "...", "name": "Ana", "email": "ana@example.com", "password": "********", ... } }
|
|
172
203
|
|
|
173
204
|
the same email again
|
|
174
205
|
-> 409 application/problem+json { "title": "UserAlreadyExists", "layer": "use-case", ... }
|
|
@@ -206,6 +237,7 @@ Everything above, except the hashing function, is data, so it can be written as
|
|
|
206
237
|
},
|
|
207
238
|
"useCases": {
|
|
208
239
|
"Register": {
|
|
240
|
+
"route": { "method": "post", "path": "/users", "status": 201 },
|
|
209
241
|
"input": {
|
|
210
242
|
"fields": {
|
|
211
243
|
"name": "text",
|
|
@@ -283,19 +315,30 @@ export async function hashPassword(plain: string) {
|
|
|
283
315
|
}
|
|
284
316
|
```
|
|
285
317
|
|
|
286
|
-
Then generate, telling codegen where the `hono` stage comes from
|
|
318
|
+
Then generate, telling codegen where the `hono` stage comes from in `stages.ts`:
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
export { stages } from "@afora/hono";
|
|
322
|
+
```
|
|
287
323
|
|
|
288
324
|
```bash
|
|
289
325
|
bun add -d @afora/codegen
|
|
290
|
-
bunx afora-codegen generate afora.json --stages
|
|
326
|
+
bunx afora-codegen generate afora.json --stages ./stages.ts # writes src/domain/*.ts
|
|
291
327
|
```
|
|
292
328
|
|
|
329
|
+
and serve the application from `src/server.ts` (`bun src/server.ts`):
|
|
330
|
+
|
|
293
331
|
```ts
|
|
294
|
-
import { App } from "./
|
|
332
|
+
import { App } from "./domain/index.ts";
|
|
333
|
+
|
|
334
|
+
await App.start();
|
|
335
|
+
export const api = App.outputs.Api.build();
|
|
295
336
|
|
|
296
|
-
Bun.serve({ fetch:
|
|
337
|
+
if (import.meta.main) Bun.serve({ fetch: api.fetch });
|
|
297
338
|
```
|
|
298
339
|
|
|
340
|
+
These three files are `examples/interop/from-json`, which `bun run test:interop` generates and serves.
|
|
341
|
+
|
|
299
342
|
`generate` writes one file per kind (`entities.ts`, `repositories.ts`, `use-cases.ts`, `commands.ts`, `outputs.ts`...), an `application.ts` and an `index.ts`, each starting with a banner saying not to edit it. `afora-codegen extract` goes the other way and gives the `afora.json` of an `Application`. The guide is in [docs/afora-json.md](docs/afora-json.md).
|
|
300
343
|
|
|
301
344
|
## What you get
|
|
@@ -307,6 +350,7 @@ Each concept has a page in [docs/](docs/README.md), in the order they build on o
|
|
|
307
350
|
- [Records](docs/records.md): a class that holds the handled value, masks sensitive keys and generates `change.<key>`; plugins; experimental slicing.
|
|
308
351
|
- [Entities](docs/entities.md): a record with identity, timestamps, `destroy()` and domain events.
|
|
309
352
|
- [Repositories](docs/repositories.md): typed async methods for an entity, queries, soft deletion, providers, transactions and relations.
|
|
353
|
+
- [Schema evolution](docs/schema-evolution.md): `fill`, `renamedFrom`, `schemaVersion` with `upgrade` steps and `previousNames`, so old rows are still read; `app.migrate` to rewrite them, and a schema lock checked in CI and on `start`.
|
|
310
354
|
- [Use cases](docs/use-cases.md): the Command pattern with method injection, written as code or as steps.
|
|
311
355
|
- [Commands](docs/commands.md): use cases under operation names, with a context, a transaction and an event policy.
|
|
312
356
|
- [Outputs](docs/outputs.md): commands through stages into a sink, such as an HTTP API or an OpenAPI document.
|
|
@@ -327,22 +371,79 @@ Every field (a property, blueprint, list or record class) implements Standard Sc
|
|
|
327
371
|
| [`@afora/codegen`](packages/codegen/README.md) | Generates afora code from an `afora.json`, and extracts the `afora.json` of an `Application` back. |
|
|
328
372
|
| [`@afora/mcp`](packages/mcp/README.md) | An MCP server that lets an agent build a domain with `@afora/codegen`. |
|
|
329
373
|
| [`@afora/hono`](packages/hono/README.md) | A `hono` sink for outputs: serves the operations as a Hono app, with problem+json errors, masked responses, signed webhooks and an optional OpenAPI document. |
|
|
374
|
+
| [`@afora/fastify`](packages/fastify/README.md) | A `fastify` sink: the same operations, options and behavior on Fastify. |
|
|
375
|
+
| [`@afora/express`](packages/express/README.md) | An `express` sink: the same on Express 5. |
|
|
376
|
+
| [`@afora/elysia`](packages/elysia/README.md) | An `elysia` sink: the same on Elysia. |
|
|
330
377
|
| [`@afora/ws`](packages/ws/README.md) | A `websocket` sink: operations over JSON messages on any runtime, with topic subscriptions that `publishEvents` feeds with domain events. |
|
|
331
378
|
| [`@afora/drizzle`](packages/drizzle/README.md) | A repository provider on Drizzle ORM (SQLite and Postgres) that passes the provider contract kit. |
|
|
379
|
+
| [`@afora/dynamodb`](packages/dynamodb/README.md) | A repository provider on one DynamoDB table (single-table layout, unique keys as marker items) that passes the provider contract kit on DynamoDB Local. |
|
|
332
380
|
|
|
333
381
|
## Example
|
|
334
382
|
|
|
335
383
|
`examples/blog` is a blog (users with the `authenticatable` plugin, posts with soft deletion, every use case a [standard one](docs/std-use-cases.md), a Hono API on Drizzle and SQLite) whose whole `src/domain` is generated from one `afora.json` by `@afora/codegen`. Only the database, the schema, the provider (made from a `Database` environment with `providerFrom` when the server calls `App.start()`, and named `database` in the `providers` of the spec) and the server are hand-written. Its tests check that the committed code is what the spec generates, drive the API end to end and extract the spec back from the application: `bun run test:blog`.
|
|
336
384
|
|
|
337
|
-
|
|
385
|
+
`examples/agent-built` is an event ticketing API (organizers, events with a capacity, tickets) built only through the `@afora/mcp` tools from a written brief, with the tool calls committed and replayed byte for byte, and an evaluation of four invariants on its running API and on a Hono + Zod baseline: `bun run test:agent-built`. See [its write-up](examples/agent-built/README.md).
|
|
338
386
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
- **Valid input.** afora is about 1.8x to 2.5x slower than Valibot and about 1.1x to 2.1x slower than Zod 4 (flat and nested objects, Bun and Node). It is faster than ArkType in these cases, where ArkType's morphs move it to a slower path.
|
|
342
|
-
- **Invalid input.** This is still where afora is weakest, but much closer than before. Flat objects are about 5x slower than Valibot and 3.7x slower than Zod 4 on Bun, and about 4.4x and 3.6x on Node; nested objects are about 2.9x and 2.1x on Bun, and about 3.1x on Node. It is faster than ArkType here. Most of what remains is TypeBox's own error collection, which runs once for each failing property; the `cause` exception of an issue is built only when it is read.
|
|
343
|
-
- **Floor.** Compiled TypeBox, which only checks and does not transform, is about 9x to 27x faster than afora on valid input. afora builds on it.
|
|
387
|
+
## Performance
|
|
344
388
|
|
|
345
|
-
|
|
389
|
+
`bun run bench` times afora next to TypeBox (compiled), Ajv, Typia, Zod 4, Valibot, ArkType, Effect Schema, Yup and Joi on seven scenarios (a flat object, a nested one, the body of an API request, a list of 1,000 items, a union, six levels of nesting, and a flat object with nothing to transform), with valid and invalid input, on Bun and on Node. Every library declares the same rules, rejects undeclared keys and collects every error, and is checked before it is timed; each row is timed by a process of its own, after a warm-up, in three rounds. The tables are in [`bench/RESULTS.md`](bench/RESULTS.md) and the method in [`bench/README.md`](bench/README.md). Absolute numbers depend on the machine, so read the ratios.
|
|
390
|
+
|
|
391
|
+
<!-- bench:start -->
|
|
392
|
+
|
|
393
|
+

|
|
394
|
+
|
|
395
|
+
Time of each library as a multiple of afora `blueprint.safeParse` on the same case: above 1.00x it is slower than afora, below it is faster. TypeBox, Ajv and Typia only check, they do not transform. afora 0.0.8, Bun 1.4.4 and Node v24.22.2, AMD EPYC 9V45 96-Core Processor; generated from `bench/results/latest.json`.
|
|
396
|
+
|
|
397
|
+
| Node 24 | afora | TypeBox | Ajv | Typia | Zod | Valibot | ArkType | Effect Schema | Yup | Joi |
|
|
398
|
+
| -------------------------------------------------------- | -------: | ------: | ----: | ----: | ----: | ------: | ------: | ------------: | -----: | -----: |
|
|
399
|
+
| Small flat object, valid | 299.5 ns | 0.13x | 0.14x | 0.15x | 1.23x | 1.97x | 10.30x | 2.74x | 12.44x | 8.25x |
|
|
400
|
+
| Small flat object, invalid | 474.6 ns | 9.50x | 0.13x | 0.58x | 3.22x | 2.71x | 27.57x | 4.04x | 68.43x | 7.66x |
|
|
401
|
+
| Nested user (address, 5 tags max, 10 addresses), valid | 2036 ns | 0.12x | 0.17x | 0.13x | 1.27x | 1.02x | 14.85x | 3.24x | 22.52x | 6.67x |
|
|
402
|
+
| Nested user (address, 5 tags max, 10 addresses), invalid | 2688 ns | 9.83x | 0.15x | 0.83x | 1.84x | 1.21x | 21.43x | 3.46x | 38.49x | 6.84x |
|
|
403
|
+
| API request body (an order with 5 items), valid | 2518 ns | 0.35x | 0.35x | 0.24x | 1.21x | 1.16x | 9.55x | 2.46x | 17.06x | 5.63x |
|
|
404
|
+
| API request body (an order with 5 items), invalid | 3540 ns | 5.17x | 0.12x | 0.86x | 1.90x | 1.10x | 14.67x | 2.50x | 30.63x | 5.11x |
|
|
405
|
+
| List of 1,000 items, valid | 152.4 µs | 0.06x | 0.10x | 0.06x | 0.67x | 2.34x | 20.61x | 3.96x | 27.77x | 7.40x |
|
|
406
|
+
| List of 1,000 items, invalid | 155.5 µs | 20.58x | 0.10x | 0.18x | 0.72x | 2.35x | 26.62x | 3.70x | 28.82x | 7.43x |
|
|
407
|
+
| Union of 4 variants (12 events), valid | 579.7 ns | 0.47x | 0.59x | 0.51x | 3.90x | 14.54x | 1.54x | 13.42x | 91.04x | 94.11x |
|
|
408
|
+
| Union of 4 variants (12 events), invalid | 55.0 µs | 0.64x | 0.01x | 0.03x | 0.09x | 0.19x | 0.34x | 0.17x | 1.44x | 1.69x |
|
|
409
|
+
| 6 levels of nesting, valid | 829.3 ns | 0.06x | 0.09x | 0.06x | 0.78x | 1.13x | 14.16x | 4.12x | 19.31x | 5.65x |
|
|
410
|
+
| 6 levels of nesting, invalid | 971.4 ns | 21.55x | 0.10x | 1.69x | 1.99x | 1.72x | 27.50x | 5.35x | 52.46x | 7.37x |
|
|
411
|
+
| Small flat object, checks only, valid | 177.6 ns | 0.23x | 0.24x | 0.25x | 0.82x | 2.68x | 0.44x | 2.34x | 18.10x | 12.73x |
|
|
412
|
+
| Small flat object, checks only, invalid | 424.3 ns | 10.48x | 0.15x | 0.68x | 2.94x | 2.85x | 22.80x | 3.92x | 74.92x | 8.39x |
|
|
413
|
+
|
|
414
|
+
| Bun | afora | TypeBox | Ajv | Typia | Zod | Valibot | ArkType | Effect Schema | Yup | Joi |
|
|
415
|
+
| -------------------------------------------------------- | -------: | ------: | ----: | ----: | ----: | ------: | ------: | ------------: | -----: | -----: |
|
|
416
|
+
| Small flat object, valid | 195.7 ns | 0.15x | 0.18x | 0.17x | 1.63x | 0.95x | 10.95x | 2.58x | 22.27x | 10.44x |
|
|
417
|
+
| Small flat object, invalid | 426.1 ns | 7.87x | 0.21x | 1.26x | 1.62x | 0.97x | 16.14x | 2.50x | 39.07x | 8.12x |
|
|
418
|
+
| Nested user (address, 5 tags max, 10 addresses), valid | 3154 ns | 0.05x | 0.08x | 0.03x | 0.93x | 0.41x | 4.73x | 1.35x | 16.66x | 3.81x |
|
|
419
|
+
| Nested user (address, 5 tags max, 10 addresses), invalid | 2260 ns | 8.15x | 0.16x | 0.99x | 1.64x | 0.91x | 13.57x | 2.40x | 39.16x | 6.88x |
|
|
420
|
+
| API request body (an order with 5 items), valid | 2355 ns | 0.19x | 0.21x | 0.32x | 1.34x | 0.68x | 5.08x | 1.83x | 21.04x | 4.97x |
|
|
421
|
+
| API request body (an order with 5 items), invalid | 2456 ns | 5.33x | 0.13x | 1.13x | 2.06x | 0.97x | 11.38x | 2.24x | 38.34x | 7.81x |
|
|
422
|
+
| List of 1,000 items, valid | 185.7 µs | 0.02x | 0.05x | 0.01x | 1.05x | 0.45x | 7.61x | 1.53x | 29.36x | 5.07x |
|
|
423
|
+
| List of 1,000 items, invalid | 92.7 µs | 23.63x | 0.12x | 0.45x | 2.25x | 0.96x | 23.45x | 3.00x | 65.59x | 11.20x |
|
|
424
|
+
| Union of 4 variants (12 events), valid | 1627 ns | 0.11x | 0.14x | 0.20x | 1.83x | 1.97x | 0.26x | 2.06x | 39.17x | 27.28x |
|
|
425
|
+
| Union of 4 variants (12 events), invalid | 41.8 µs | 0.64x | 0.01x | 0.04x | 0.10x | 0.11x | 0.21x | 0.11x | 1.81x | 2.01x |
|
|
426
|
+
| 6 levels of nesting, valid | 528.2 ns | 0.02x | 0.10x | 0.06x | 1.98x | 1.54x | 11.94x | 2.87x | 39.56x | 7.63x |
|
|
427
|
+
| 6 levels of nesting, invalid | 838.9 ns | 17.86x | 0.09x | 1.60x | 1.74x | 1.59x | 18.13x | 3.09x | 53.04x | 7.92x |
|
|
428
|
+
| Small flat object, checks only, valid | 110.1 ns | 0.28x | 0.35x | 0.29x | 1.83x | 0.92x | 0.34x | 1.91x | 33.82x | 15.15x |
|
|
429
|
+
| Small flat object, checks only, invalid | 414.7 ns | 7.74x | 0.21x | 1.25x | 1.52x | 0.86x | 11.63x | 2.06x | 39.99x | 7.60x |
|
|
430
|
+
|
|
431
|
+
<!-- bench:end -->
|
|
432
|
+
|
|
433
|
+
What the table says, for `blueprint.safeParse`, on a GitHub runner (AMD EPYC, Linux, Bun 1.4.4 and Node 24). The table is of afora at commit `8c4395e`; what changed after it is said where it applies.
|
|
434
|
+
|
|
435
|
+
- **Against Zod 4.** On Bun, faster in 12 of the 14 cases, by 1.05x to 2.25x; Zod 4 is ahead on the valid nested object (7%). On Node, faster in 9 of the 14, by 1.2x to 3.9x; Zod 4 is ahead on the list of 1,000 items (1.4x to 1.5x), on the valid deep object (1.3x) and on the valid flat object with nothing to transform (1.2x). The invalid union is told apart below.
|
|
436
|
+
- **Against Valibot.** On Node afora is faster in 13 of the 14 cases, by 1.02x to 2.9x outside the valid union (14.5x). On Bun it is the other way: Valibot is ahead in 11 of them, within 15% on small objects and by 1.5x to 2.4x on the valid nested object, the valid API body and the valid list, and afora is ahead on the valid union (2x) and the deep object (1.5x to 1.6x).
|
|
437
|
+
- **Against Effect Schema, Joi and Yup.** Faster in every case but the invalid union: 1.35x to 5.4x Effect Schema, 3.8x to 15x Joi and 12x to 75x Yup, and more than that on the valid union (up to 13x, 94x and 91x).
|
|
438
|
+
- **Against ArkType.** Faster wherever a value is transformed or rejected, by 4.7x to 28x, because its morphs and its errors take a slower path. With nothing to transform and a valid value, ArkType is 2.3x to 3x faster than afora, and 3.8x on the valid union on Bun.
|
|
439
|
+
- **An invalid union.** In the table it takes about 10x the time of Zod 4: TypeBox `Errors` reported every variant of the union for each rejected value. Since then a property whose schema is a discriminated union tells a rejected value by the variant it names, without `Errors`, and takes about 0.5x the time of Zod 4 on Bun (measured on an Apple M2; the table follows at the next measurement). Valid values of the same union are 1.8x to 3.9x faster than Zod 4.
|
|
440
|
+
- **Floor.** The checkers that compile and do not transform (TypeBox, Ajv, Typia) are 3x to 9x faster than afora on a valid small object, and 10x to 70x on the larger ones. afora builds on TypeBox. On invalid input afora is 5x to 24x faster than TypeBox `Errors` (outside the union), and Ajv stays 5x to 11x ahead.
|
|
441
|
+
- **Records and entities.** `Record.safeParse`, which also creates the instance, takes 1.0x to 2.1x the time of `blueprint.safeParse` on Bun and up to 4x on Node. `Entity.safeParse` also makes an id and the time stamps, which on a small object is most of its time: 5x to 13x `blueprint.safeParse` on the flat object in the table, 1.0x to 4.7x on the larger ones; since then it takes about three quarters of that on a small object, for not checking again the id and the date it has just made. `new Record(value)` throws on invalid input, which costs an exception with its trace (2x to 6x `blueprint.safeParse` on Bun, 2.5x to 13x on Node); use `safeParse` where invalid input is expected.
|
|
442
|
+
- **Bundle and cold start.** afora is a framework with one entry point: validating one object bundles to 100 kB gzipped (Zod 4: 24 kB, Valibot: 1.7 kB) and takes 40 ms to load, build and run once in a new Node process (85 ms on Bun; Zod 4: 7 to 8 ms).
|
|
443
|
+
|
|
444
|
+
What afora does that the fast rows do not all do: a pipeline per property (`preHandle`, schema check, `afterHandle`), where a property with a handler has the shape of the value checked first, so handlers only see the right types; objects walked key by key instead of one compiled function; frozen results; every issue with `code`, `path` and `source`; and, for `Record.safeParse` and `new Record`, creating the instance. If you validate bulk data in a hot path and need none of that, a compiled checker alone is an order of magnitude faster.
|
|
445
|
+
|
|
446
|
+
CI keeps it there: `bun run bench:guard` times `blueprint.safeParse` and Zod 4 `safeParse` on the fourteen cases and fails when afora is over the limit of a case as a multiple of Zod 4 (1.15x unless `bench/guard.limits.ts` states another, with the reason).
|
|
346
447
|
|
|
347
448
|
## Comparison
|
|
348
449
|
|
|
@@ -378,8 +479,9 @@ bun run test:type-perf # build, then budget the editor cost per edit and snapsho
|
|
|
378
479
|
bun run test:api # build, then snapshot the public surface (exports and declarations) and the afora.json schema
|
|
379
480
|
bun run test:interop # Hono, tRPC and TanStack Form tests (examples/interop), including the quick start
|
|
380
481
|
bun run test:blog # the generated blog (examples/blog): drift, API and extraction
|
|
482
|
+
bun run test:agent-built # the agent-built example (examples/agent-built): drift, replay, API and the invariant evaluation
|
|
381
483
|
bun run smoke # build, then run the package on plain Node
|
|
382
|
-
bun run bench # benchmark against TypeBox, Zod, Valibot and
|
|
484
|
+
bun run bench # benchmark against TypeBox, Ajv, Typia, Zod, Valibot, ArkType, Effect Schema, Yup and Joi
|
|
383
485
|
bun run lint # biome lint
|
|
384
486
|
bun run knip # unused files, exports and dependencies
|
|
385
487
|
bun run fmt # biome format + fix
|
|
@@ -387,9 +489,11 @@ bun run build # build dist/ (ESM + .d.ts)
|
|
|
387
489
|
bun run test:codegen # tests of packages/codegen
|
|
388
490
|
bun run test:mcp # tests of packages/mcp
|
|
389
491
|
bun run test:hono # tests of packages/hono
|
|
492
|
+
bun run test:fastify # tests of packages/fastify (also test:express, test:elysia)
|
|
390
493
|
bun run test:ws # tests of packages/ws
|
|
391
494
|
bun run test:drizzle # tests of packages/drizzle (SQLite and PGlite)
|
|
392
|
-
bun run
|
|
495
|
+
bun run test:dynamodb # tests of packages/dynamodb (DynamoDB Local, with DYNAMODB_ENDPOINT)
|
|
496
|
+
bun run build:packages # build the core, then packages/codegen, packages/mcp, packages/hono, packages/fastify, packages/express, packages/elysia, packages/ws, packages/drizzle and packages/dynamodb
|
|
393
497
|
```
|
|
394
498
|
|
|
395
499
|
## Versioning
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@afora/core/adapters`: the plumbing for authors of sinks, providers and
|
|
3
|
+
* code generators (`@afora/hono`, `@afora/drizzle`, `@afora/codegen`...).
|
|
4
|
+
* Part of the compatibility policy in `VERSIONING.md`, like the main entry.
|
|
5
|
+
* Application code needs only `@afora/core`. See `docs/api-tiers.md`.
|
|
6
|
+
*/
|
|
7
|
+
export { diffSchemas, schemaLockOf, schemaLockProblemsOf, } from "./core/application/schema.js";
|
|
8
|
+
export type { ApplicationDescription, CommandDescription, ExceptionDescription, FieldDescription, FieldsDescription, LogDescription, OutputDescription, PropertyDescription, PropertyLineageDescription, PropertyReference, ProviderReference, RecordDescription, UseCaseDependencyDescription, UseCaseDescription, } from "./core/application/types/application.interface.js";
|
|
9
|
+
export { ENTITY_IDENTITY_KEYS, RESERVED_ENTITY_KEYS, } from "./core/entity/abstract-entity.js";
|
|
10
|
+
export type { EventPolicyDescription } from "./core/event-policy/types/event-policy.interface.js";
|
|
11
|
+
export { coerceTextValues } from "./core/output/coerce.js";
|
|
12
|
+
export { compileRoutes, HTTP_OPTION_KEYS, httpOptionsIssue, } from "./core/output/http.js";
|
|
13
|
+
export { type OperationRoute, operationRoutes, ROUTE_OPTION_KEYS, requestIdHeaderOf, routeOptionsIssue, } from "./core/output/openapi-sink.js";
|
|
14
|
+
export { outputValueOf, PROBLEM_MEDIA_TYPE, problemOf, } from "./core/output/problem.js";
|
|
15
|
+
export { sinkOf, stageCatalogOf, stageKindOf } from "./core/output/stage.js";
|
|
16
|
+
export { statusOf } from "./core/output/status.js";
|
|
17
|
+
export type { Authenticator, AuthRequest, CompiledRoute, CompileRoutesSettings, HttpOptions, HttpRequest, HttpResult, RouteTable, } from "./core/output/types/http.interface.js";
|
|
18
|
+
export type { OperationDescriptor, Problem, ProblemOptions, SinkFactory, SinkSettings, SinkStage, StageDescription, } from "./core/output/types/output.interface.js";
|
|
19
|
+
export { RESERVED_RECORD_KEYS } from "./core/record/abstract-record.js";
|
|
20
|
+
export { SCHEMA_VERSION_KEY } from "./core/repository/state.js";
|
|
21
|
+
export type { ProviderCapabilities, ProviderDescription, ProviderQuery, } from "./core/repository/types/query.interface.js";
|
|
22
|
+
export type { IRepositoryProvider, ProviderIncrementOptions, ProviderKeyset, ProviderMigrationResult, ProviderUpdateOptions, RepositoryContext, RepositoryData, RepositorySchema, } from "./core/repository/types/repository.interface.js";
|
|
23
|
+
export type { SchemaChange, SchemaChangeClass, SchemaChangeKind, SchemaLock, SchemaLockEntity, SchemaLockField, SchemaPlan, } from "./core/repository/types/schema.interface.js";
|
|
24
|
+
export { Demo } from "./core/symbols/demo.js";
|
|
25
|
+
export { Kind } from "./core/symbols/kind.js";
|
package/dist/adapters.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
coerceTextValues2,
|
|
3
|
+
HTTP_OPTION_KEYS2,
|
|
4
|
+
httpOptionsIssue2,
|
|
5
|
+
compileRoutes2
|
|
6
|
+
} from "./index-y280ew0k.js";
|
|
7
|
+
import {
|
|
8
|
+
schemaLockOf2,
|
|
9
|
+
diffSchemas2,
|
|
10
|
+
schemaLockProblemsOf2,
|
|
11
|
+
statusOf2,
|
|
12
|
+
PROBLEM_MEDIA_TYPE2,
|
|
13
|
+
problemOf2,
|
|
14
|
+
outputValueOf2,
|
|
15
|
+
ROUTE_OPTION_KEYS2,
|
|
16
|
+
routeOptionsIssue2,
|
|
17
|
+
requestIdHeaderOf2,
|
|
18
|
+
operationRoutes2
|
|
19
|
+
} from "./index-hg1z2s2v.js";
|
|
20
|
+
import {
|
|
21
|
+
Demo2,
|
|
22
|
+
Kind2,
|
|
23
|
+
RESERVED_RECORD_KEYS2,
|
|
24
|
+
ENTITY_IDENTITY_KEYS2,
|
|
25
|
+
RESERVED_ENTITY_KEYS2,
|
|
26
|
+
SCHEMA_VERSION_KEY2,
|
|
27
|
+
sinkOf2,
|
|
28
|
+
stageKindOf2,
|
|
29
|
+
stageCatalogOf2
|
|
30
|
+
} from "./index-xegv6a1p.js";
|
|
31
|
+
export {
|
|
32
|
+
Demo2 as Demo,
|
|
33
|
+
ENTITY_IDENTITY_KEYS2 as ENTITY_IDENTITY_KEYS,
|
|
34
|
+
HTTP_OPTION_KEYS2 as HTTP_OPTION_KEYS,
|
|
35
|
+
Kind2 as Kind,
|
|
36
|
+
PROBLEM_MEDIA_TYPE2 as PROBLEM_MEDIA_TYPE,
|
|
37
|
+
RESERVED_ENTITY_KEYS2 as RESERVED_ENTITY_KEYS,
|
|
38
|
+
RESERVED_RECORD_KEYS2 as RESERVED_RECORD_KEYS,
|
|
39
|
+
ROUTE_OPTION_KEYS2 as ROUTE_OPTION_KEYS,
|
|
40
|
+
SCHEMA_VERSION_KEY2 as SCHEMA_VERSION_KEY,
|
|
41
|
+
coerceTextValues2 as coerceTextValues,
|
|
42
|
+
compileRoutes2 as compileRoutes,
|
|
43
|
+
diffSchemas2 as diffSchemas,
|
|
44
|
+
httpOptionsIssue2 as httpOptionsIssue,
|
|
45
|
+
operationRoutes2 as operationRoutes,
|
|
46
|
+
outputValueOf2 as outputValueOf,
|
|
47
|
+
problemOf2 as problemOf,
|
|
48
|
+
requestIdHeaderOf2 as requestIdHeaderOf,
|
|
49
|
+
routeOptionsIssue2 as routeOptionsIssue,
|
|
50
|
+
schemaLockOf2 as schemaLockOf,
|
|
51
|
+
schemaLockProblemsOf2 as schemaLockProblemsOf,
|
|
52
|
+
sinkOf2 as sinkOf,
|
|
53
|
+
stageCatalogOf2 as stageCatalogOf,
|
|
54
|
+
stageKindOf2 as stageKindOf,
|
|
55
|
+
statusOf2 as statusOf
|
|
56
|
+
};
|
|
@@ -57,6 +57,23 @@ export declare const ApplicationAlreadyStarted: import("../../index.js").Declare
|
|
|
57
57
|
readonly current: string;
|
|
58
58
|
readonly attempted: string;
|
|
59
59
|
}>;
|
|
60
|
+
/**
|
|
61
|
+
* `app.start({ lock })` found that the code cannot run over the stores the
|
|
62
|
+
* lock describes: an entity of the lock that no entity is named or lists in
|
|
63
|
+
* `previousNames` (renamed without it, its rows would no longer be read), or
|
|
64
|
+
* an entity whose `schemaVersion` is lower than the lock's.
|
|
65
|
+
* `details.problems` lists each.
|
|
66
|
+
*/
|
|
67
|
+
export declare const SchemaLockMismatch: import("../../index.js").DeclaredExceptionClass<{
|
|
68
|
+
readonly problems: readonly string[];
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* `app.migrate` got invalid options, or names an entity without a
|
|
72
|
+
* repository; `source` is the application name.
|
|
73
|
+
*/
|
|
74
|
+
export declare const InvalidSchemaMigration: import("../../index.js").DeclaredExceptionClass<{
|
|
75
|
+
readonly reason: string;
|
|
76
|
+
}>;
|
|
60
77
|
/**
|
|
61
78
|
* `app.start()` found environments whose variables are missing or rejected;
|
|
62
79
|
* `details.environments` gives the keys of each, by name.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SchemaLock, SchemaMigrationOptions, SchemaMigrationReport } from "../repository/types/schema.interface.js";
|
|
2
|
+
import type { ApplicationDescription } from "./types/application.interface.js";
|
|
3
|
+
/** What `app.migrate` reads from its application. */
|
|
4
|
+
export interface MigrationSource {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
/** Its entities, by name. */
|
|
7
|
+
readonly entities: ReadonlyMap<string, object>;
|
|
8
|
+
/** Its bound repositories. */
|
|
9
|
+
readonly repositories: readonly object[];
|
|
10
|
+
describe(): ApplicationDescription;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `app.migrate(options)`: for each entity with a repository (or those of
|
|
14
|
+
* `options.entities`), the backfill of `migrateRepository` through the
|
|
15
|
+
* provider of its first repository, one entity after the other.
|
|
16
|
+
*/
|
|
17
|
+
export declare function migrateApplication(source: MigrationSource, options: SchemaMigrationOptions): Promise<SchemaMigrationReport>;
|
|
18
|
+
/** Whether `value` has the shape of a schema lock. */
|
|
19
|
+
export declare function isLock(value: unknown): value is SchemaLock;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SchemaChange, SchemaLock } from "../repository/types/schema.interface.js";
|
|
2
|
+
import type { ApplicationDescription } from "./types/application.interface.js";
|
|
3
|
+
/**
|
|
4
|
+
* The lock of the entities of `description` (`app.describe()`): per entity,
|
|
5
|
+
* its schema version, previous names, unique keys and fields (each with its
|
|
6
|
+
* JSON Schema, whether it is optional or unique, its `fill` and
|
|
7
|
+
* `renamedFrom`, and the fields of a nested blueprint or record), with
|
|
8
|
+
* registered properties, blueprints and records resolved and every key
|
|
9
|
+
* sorted, so equivalent applications give equal locks. The identity keys
|
|
10
|
+
* and the keys of record plugins are left out.
|
|
11
|
+
*/
|
|
12
|
+
export declare function schemaLockOf(description: ApplicationDescription): SchemaLock;
|
|
13
|
+
/**
|
|
14
|
+
* The changes from `lock` to `current` (both made by `schemaLockOf`), each
|
|
15
|
+
* classed as `automatic` (afora handles it: an optional key added, a key
|
|
16
|
+
* removed, a constraint loosened, metadata), `declared` (it needs a `fill`,
|
|
17
|
+
* a `renamedFrom`, `previousNames` or an `upgrade` step to a version above
|
|
18
|
+
* the lock's; `satisfied` says whether the code has it) or `check` (a new
|
|
19
|
+
* unique key: the stored rows must be checked first). `destructive` marks
|
|
20
|
+
* what deletes stored data once applied: a removed key, the rows of a
|
|
21
|
+
* previous name, a removed entity. Nested blueprints and records are
|
|
22
|
+
* compared key by key. Ordered by entity name, then key.
|
|
23
|
+
*/
|
|
24
|
+
export declare function diffSchemas(lock: SchemaLock, current: SchemaLock): readonly SchemaChange[];
|
|
25
|
+
/**
|
|
26
|
+
* Why the code cannot run over the stores `lock` describes: an entity of
|
|
27
|
+
* the lock that no entity is named, or lists in its `previousNames`, or an
|
|
28
|
+
* entity whose `schemaVersion` is lower than the lock's. Empty when it can.
|
|
29
|
+
*/
|
|
30
|
+
export declare function schemaLockProblemsOf(lock: SchemaLock, current: SchemaLock): readonly string[];
|
|
@@ -9,8 +9,11 @@ import type { AnyOutput, StageDescription } from "../../output/types/output.inte
|
|
|
9
9
|
import type { RecordPluginDescriptor } from "../../record/types/plugin.interface.js";
|
|
10
10
|
import type { ProviderDescription, RepositoryIndex, RepositoryScans } from "../../repository/types/query.interface.js";
|
|
11
11
|
import type { AnyRepository, IRepositoryProvider } from "../../repository/types/repository.interface.js";
|
|
12
|
+
import type { SchemaLock, SchemaMigrationOptions, SchemaMigrationReport } from "../../repository/types/schema.interface.js";
|
|
12
13
|
import type { Kind } from "../../symbols/kind.js";
|
|
13
14
|
import type { UseCaseStepDescription } from "../../use-case/steps/types/steps.interface.js";
|
|
15
|
+
import type { AccessDeclaration } from "../../use-case/types/access.interface.js";
|
|
16
|
+
import type { RouteDeclaration } from "../../use-case/types/route.interface.js";
|
|
14
17
|
import type { AnyExceptionClass, AnyUseCaseClass, UseCaseInput, UseCaseResponse } from "../../use-case/use-case.js";
|
|
15
18
|
import type { ProviderSlot, ProviderStorage } from "../providers.js";
|
|
16
19
|
import type { LogFactory, ProviderFactory } from "../start.js";
|
|
@@ -206,6 +209,17 @@ export interface IApplication<Name extends string, Collections extends Applicati
|
|
|
206
209
|
* started application of the process.
|
|
207
210
|
*/
|
|
208
211
|
stop(): Promise<void>;
|
|
212
|
+
/**
|
|
213
|
+
* Brings the stored rows of its entities up to their current shape,
|
|
214
|
+
* walking each provider directly: in `check` it only reports, in `apply`
|
|
215
|
+
* it gives each provider the plan of its entity (`provider.migrate`, the
|
|
216
|
+
* changes from `lock`), then rewrites the rows below the current
|
|
217
|
+
* `schemaVersion` or with a `fill` or `renamedFrom` pending, and moves the
|
|
218
|
+
* rows stored under `previousNames`. Keys no longer declared, and the
|
|
219
|
+
* rows of the previous names, are only removed with `destructive: true`.
|
|
220
|
+
* Idempotent and resumable.
|
|
221
|
+
*/
|
|
222
|
+
migrate(options: SchemaMigrationOptions): Promise<SchemaMigrationReport>;
|
|
209
223
|
/** The variables of an environment registered in the application. */
|
|
210
224
|
get<Environment extends AnyEnvironment>(environment: Environment): EnvironmentValue<Environment>;
|
|
211
225
|
/** The bound version of a command registered in the application. */
|
|
@@ -231,6 +245,13 @@ export interface ApplicationStartOptions {
|
|
|
231
245
|
readonly register?: boolean;
|
|
232
246
|
/** Stop the started application of the process and take its place, e.g. on hot reload. Default `false`. */
|
|
233
247
|
readonly replace?: boolean;
|
|
248
|
+
/**
|
|
249
|
+
* The committed schema lock (`afora.lock.json`): start throws
|
|
250
|
+
* `SchemaLockMismatch` when an entity of the lock is no longer named, and
|
|
251
|
+
* no entity lists it in `previousNames`, or when the `schemaVersion` of
|
|
252
|
+
* an entity is lower than the lock's.
|
|
253
|
+
*/
|
|
254
|
+
readonly lock?: SchemaLock;
|
|
234
255
|
}
|
|
235
256
|
/** Any application. */
|
|
236
257
|
export type AnyApplication = IApplication<string, any>;
|
|
@@ -290,6 +311,12 @@ export type FieldDescription = {
|
|
|
290
311
|
readonly kind: "record" | "entity";
|
|
291
312
|
readonly ref: string;
|
|
292
313
|
readonly optional: boolean;
|
|
314
|
+
} | {
|
|
315
|
+
/** One of several blueprints, told apart by the value of `key`, the name of the variant. */
|
|
316
|
+
readonly kind: "variant";
|
|
317
|
+
readonly key: string;
|
|
318
|
+
readonly optional: boolean;
|
|
319
|
+
readonly variants: Readonly<Record<string, FieldDescription>>;
|
|
293
320
|
};
|
|
294
321
|
export type FieldsDescription = Readonly<Record<string, FieldDescription>>;
|
|
295
322
|
/** A record or an entity: its fields (an entity's identity and its plugins' keys left out), options and plugins. */
|
|
@@ -318,6 +345,12 @@ export interface RecordDescription {
|
|
|
318
345
|
* `fields`) or `{ at: key }`. Present only when it has one.
|
|
319
346
|
*/
|
|
320
347
|
readonly expires?: EntityExpires;
|
|
348
|
+
/** The `schemaVersion` of an entity; present only when declared. */
|
|
349
|
+
readonly schemaVersion?: number;
|
|
350
|
+
/** The versions the `upgrade` of an entity has a step to, ascending; present only when it has some. */
|
|
351
|
+
readonly upgrade?: readonly number[];
|
|
352
|
+
/** The `previousNames` of an entity; present only when it has some. */
|
|
353
|
+
readonly previousNames?: readonly string[];
|
|
321
354
|
}
|
|
322
355
|
/** What a use case depends on, by the name it has in the application. */
|
|
323
356
|
export interface UseCaseDependencyDescription {
|
|
@@ -333,6 +366,10 @@ export interface UseCaseDescription {
|
|
|
333
366
|
readonly description?: string;
|
|
334
367
|
readonly tags: readonly string[];
|
|
335
368
|
readonly deprecated: boolean;
|
|
369
|
+
/** Where it is served over HTTP, from the `route` setting or `@Route`. */
|
|
370
|
+
readonly route?: RouteDeclaration;
|
|
371
|
+
/** Who may call it, from the `access` setting or `@Access`. */
|
|
372
|
+
readonly access?: AccessDeclaration;
|
|
336
373
|
readonly input: FieldsDescription;
|
|
337
374
|
readonly response: FieldsDescription;
|
|
338
375
|
readonly context?: FieldsDescription;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Jwt, JwtOptions } from "./types/jwt.interface.js";
|
|
2
|
+
/** `jwtOf` got options it cannot use, or its secret is missing; `details.reason` says what. */
|
|
3
|
+
export declare const InvalidJwtOptions: import("../../index.js").DeclaredExceptionClass<{
|
|
4
|
+
readonly reason: string;
|
|
5
|
+
}>;
|
|
6
|
+
/**
|
|
7
|
+
* Why `options` cannot be the options of `jwtOf`, naming them `where`;
|
|
8
|
+
* `undefined` when they can. `extra` are other keys the caller accepts, and
|
|
9
|
+
* `described` is for options that `describe()` and `afora.json` show, where
|
|
10
|
+
* the secret must be named by its environment variable, never written.
|
|
11
|
+
*/
|
|
12
|
+
export declare function jwtOptionsIssue(options: unknown, where: string, extra?: readonly string[], described?: boolean): string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Signs and verifies JSON Web Tokens with a shared secret (HMAC), on Web
|
|
15
|
+
* Crypto, so it runs on Bun, Node and Workers alike:
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* const jwt = jwtOf({ secret: { env: "JWT_SECRET" }, issuer: "blog" });
|
|
19
|
+
* const token = await jwt.sign({ subject: user.id, scopes: ["posts:write"] });
|
|
20
|
+
* const principal = await jwt.verify(token); // { subject, scopes, claims? }
|
|
21
|
+
* hono(options, { authenticator: jwt.authenticator() });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* A token is refused (`Unauthenticated`) when it is malformed, signed with
|
|
25
|
+
* another algorithm or secret, expired, not yet valid, of another issuer,
|
|
26
|
+
* audience or use, or has no subject. Throws `InvalidJwtOptions` for options
|
|
27
|
+
* it cannot use, and when the secret of `{ env }` is missing or shorter than
|
|
28
|
+
* 32 characters, on the first token.
|
|
29
|
+
*/
|
|
30
|
+
export declare function jwtOf(options: JwtOptions): Jwt;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Authenticator } from "../../output/types/http.interface.js";
|
|
2
|
+
import type { Principal } from "../../use-case/types/access.interface.js";
|
|
3
|
+
/** The algorithms `jwtOf` signs and verifies with: HMAC with a shared secret. */
|
|
4
|
+
export type JwtAlgorithm = "HS256" | "HS384" | "HS512";
|
|
5
|
+
/** The options of `jwtOf`. JSON, so they can live in the options of a sink. */
|
|
6
|
+
export interface JwtOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The secret tokens are signed with: the text itself, or the name of the
|
|
9
|
+
* environment variable that holds it (`{ env: "JWT_SECRET" }`), read when
|
|
10
|
+
* the first token is signed or verified. At least 32 characters.
|
|
11
|
+
*/
|
|
12
|
+
readonly secret: string | {
|
|
13
|
+
readonly env: string;
|
|
14
|
+
};
|
|
15
|
+
/** The `iss` claim: set when signing, required when verifying. */
|
|
16
|
+
readonly issuer?: string;
|
|
17
|
+
/** The `aud` claim: set when signing, required when verifying. */
|
|
18
|
+
readonly audience?: string;
|
|
19
|
+
/** The seconds a token lives: 900 (15 minutes) by default. */
|
|
20
|
+
readonly expiresIn?: number;
|
|
21
|
+
/** `"HS256"` by default. */
|
|
22
|
+
readonly algorithm?: JwtAlgorithm;
|
|
23
|
+
}
|
|
24
|
+
/** What a token is signed for. */
|
|
25
|
+
export interface JwtSubject {
|
|
26
|
+
readonly subject: string;
|
|
27
|
+
readonly scopes?: readonly string[];
|
|
28
|
+
readonly claims?: Principal["claims"];
|
|
29
|
+
}
|
|
30
|
+
/** What changes for one token. */
|
|
31
|
+
export interface JwtTokenOptions {
|
|
32
|
+
/** The seconds this token lives, instead of the `expiresIn` of the options. */
|
|
33
|
+
readonly expiresIn?: number;
|
|
34
|
+
/**
|
|
35
|
+
* What the token is for, e.g. `"refresh"`: kept in its `use` claim, and a
|
|
36
|
+
* token verifies only for the use it was signed for (none by default), so
|
|
37
|
+
* a refresh token is not accepted as an access token.
|
|
38
|
+
*/
|
|
39
|
+
readonly use?: string;
|
|
40
|
+
}
|
|
41
|
+
/** Signs and verifies the tokens of one secret (see `jwtOf`). */
|
|
42
|
+
export interface Jwt {
|
|
43
|
+
/** A signed token for `subject`: `sub`, `scope` (the scopes, space separated), `iat`, `exp` and the claims. */
|
|
44
|
+
sign(subject: JwtSubject, options?: JwtTokenOptions): Promise<string>;
|
|
45
|
+
/** The caller a token was signed for. Rejects with `Unauthenticated` for a token it does not accept. */
|
|
46
|
+
verify(token: string, options?: Pick<JwtTokenOptions, "use">): Promise<Principal>;
|
|
47
|
+
/**
|
|
48
|
+
* An authenticator that reads the token from the `authorization` header
|
|
49
|
+
* (`Bearer <token>`; another header with `header`) and, with `cookie`,
|
|
50
|
+
* from that cookie when the header is absent. No token is an anonymous
|
|
51
|
+
* caller; one that does not verify is refused.
|
|
52
|
+
*/
|
|
53
|
+
authenticator(options?: {
|
|
54
|
+
readonly header?: string;
|
|
55
|
+
readonly cookie?: string;
|
|
56
|
+
}): Authenticator;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The options of `jwtOf` where they are described (`describe()`,
|
|
60
|
+
* `afora.json`): the secret is named by its environment variable.
|
|
61
|
+
*/
|
|
62
|
+
export interface DescribedJwtOptions extends Omit<JwtOptions, "secret"> {
|
|
63
|
+
readonly secret: {
|
|
64
|
+
readonly env: string;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
export declare const ReservedBlueprintKey: import("../../index.js").DeclaredExceptionClass<{
|
|
3
3
|
readonly key: string;
|
|
4
4
|
}>;
|
|
5
|
+
/** `variantOf` was given something it cannot make a field of; `reason` says what. */
|
|
6
|
+
export declare const InvalidBlueprintVariant: import("../../index.js").DeclaredExceptionClass<{
|
|
7
|
+
readonly key: string;
|
|
8
|
+
readonly reason: string;
|
|
9
|
+
}>;
|
|
5
10
|
/** A blueprint picks a key that no plugin provides. */
|
|
6
11
|
export declare const UndeclaredBlueprintProperty: import("../../index.js").DeclaredExceptionClass<{
|
|
7
12
|
readonly key: string;
|