@afora/core 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +36 -7
  2. package/dist/core/application/start.d.ts +7 -0
  3. package/dist/core/application/types/application.interface.d.ts +47 -2
  4. package/dist/core/command/command.d.ts +11 -4
  5. package/dist/core/command/types/command.interface.d.ts +26 -0
  6. package/dist/core/entity/abstract-entity.d.ts +28 -1
  7. package/dist/core/entity/entity.d.ts +1 -1
  8. package/dist/core/entity/event.d.ts +13 -2
  9. package/dist/core/entity/exceptions.d.ts +8 -0
  10. package/dist/core/entity/types/entity.interface.d.ts +66 -2
  11. package/dist/core/entity/version.d.ts +13 -0
  12. package/dist/core/event-policy/event-policy.d.ts +25 -0
  13. package/dist/core/event-policy/exceptions.d.ts +11 -0
  14. package/dist/core/event-policy/publish.d.ts +17 -0
  15. package/dist/core/event-policy/publishers.d.ts +18 -0
  16. package/dist/core/event-policy/types/event-policy.interface.d.ts +72 -0
  17. package/dist/core/output/coerce.d.ts +9 -0
  18. package/dist/core/output/openapi-sink.d.ts +138 -38
  19. package/dist/core/output/status.d.ts +2 -1
  20. package/dist/core/parse/parse.d.ts +10 -0
  21. package/dist/core/parse/strip.d.ts +13 -0
  22. package/dist/core/property/input.d.ts +20 -0
  23. package/dist/core/record/plugin.d.ts +6 -2
  24. package/dist/core/repository/exceptions.d.ts +30 -0
  25. package/dist/core/repository/in-memory.d.ts +14 -1
  26. package/dist/core/repository/plan.d.ts +20 -0
  27. package/dist/core/repository/provider.d.ts +1 -1
  28. package/dist/core/repository/query.d.ts +9 -2
  29. package/dist/core/repository/repository.d.ts +40 -8
  30. package/dist/core/repository/state.d.ts +39 -2
  31. package/dist/core/repository/types/query.interface.d.ts +106 -5
  32. package/dist/core/repository/types/repository.interface.d.ts +113 -7
  33. package/dist/core/use-case/exceptions.d.ts +36 -0
  34. package/dist/core/use-case/plugin-catalog.d.ts +11 -0
  35. package/dist/core/use-case/plugin.d.ts +25 -0
  36. package/dist/core/use-case/steps/types/steps.interface.d.ts +23 -6
  37. package/dist/core/use-case/target.d.ts +9 -0
  38. package/dist/core/use-case/types/plugin.interface.d.ts +185 -0
  39. package/dist/experimental.js +3 -3
  40. package/dist/{index-3nd9bjnc.js → index-qxfkyd6k.js} +646 -635
  41. package/dist/{index-gbgc40xe.js → index-t5qwtyj5.js} +2798 -295
  42. package/dist/index.d.ts +35 -15
  43. package/dist/index.js +99 -15
  44. package/dist/std/blueprint-plugin.d.ts +6 -2
  45. package/dist/std/blueprint.d.ts +3 -0
  46. package/dist/std/command.d.ts +11 -0
  47. package/dist/std/entity.d.ts +2 -1
  48. package/dist/std/event-policies.d.ts +2 -0
  49. package/dist/std/properties/exceptions.d.ts +11 -0
  50. package/dist/std/properties/json.d.ts +54 -0
  51. package/dist/std/properties/password.d.ts +16 -1
  52. package/dist/std/properties/time.d.ts +4 -0
  53. package/dist/std/use-cases/auth.d.ts +5 -0
  54. package/dist/std/use-cases/crud.d.ts +25 -0
  55. package/dist/std/use-cases/index.d.ts +11 -0
  56. package/dist/std/use-cases/lifecycle.d.ts +13 -0
  57. package/dist/std/use-cases/shared.d.ts +88 -0
  58. package/dist/std/use-cases/types.d.ts +291 -0
  59. package/dist/testing/index.d.ts +2 -0
  60. package/dist/testing/index.js +355 -2
  61. package/dist/testing/memory-publisher.d.ts +16 -0
  62. package/dist/testing/provider-contract.d.ts +14 -0
  63. package/package.json +3 -2
package/README.md CHANGED
@@ -1,4 +1,24 @@
1
- # afora
1
+ <h1 align="center">afora</h1>
2
+
3
+ <p align="center"><strong>Model the domain. Keep the invariants.</strong><br>Domain modeling for TypeScript, built on TypeBox.</p>
4
+
5
+ <p align="center">
6
+ <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>
7
+ <a href="https://github.com/afora-framework/core/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/afora-framework/core/actions/workflows/ci.yml/badge.svg?branch=main"></a>
8
+ <a href="https://www.npmjs.com/package/@afora/core"><img alt="npm provenance" src="https://img.shields.io/badge/npm-provenance-2ea44f?logo=npm"></a>
9
+ <a href="https://standardschema.dev"><img alt="Standard Schema V1" src="https://img.shields.io/badge/Standard%20Schema-V1-3178c6"></a>
10
+ <img alt="Bun >= 1.4.4 and Node >= 22" src="https://img.shields.io/badge/runtime-Bun%20%E2%89%A5%201.4.4%20%7C%20Node%20%E2%89%A5%2022-555">
11
+ <a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/github/license/afora-framework/core"></a>
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="https://getafora.com">Website</a> ·
16
+ <a href="docs/README.md">Docs</a> ·
17
+ <a href="#quick-start">Quick start</a> ·
18
+ <a href="docs/afora-json.md">afora.json</a> ·
19
+ <a href="CHANGELOG.md">Changelog</a> ·
20
+ <a href="https://github.com/afora-framework/core/discussions">Discussions</a>
21
+ </p>
2
22
 
3
23
  Domain modeling on [TypeBox](https://github.com/sinclairzx81/typebox): entities with invariants, sensitive data masking and exceptions that know their layer, usable by any [Standard Schema](https://standardschema.dev) consumer.
4
24
 
@@ -10,7 +30,8 @@ afora is not a general-purpose validator. It is for code that models a domain, a
10
30
  - **Typed change namespace and side effects.** `change: ["password"]` generates `change.password(value)` (also destructurable, as `const { change } = user`), which re-validates the value and runs `onSetAll` and `onSet`.
11
31
  - **Exceptions that know their layer.** Each error carries a layer (`Property`, `Record`, `UseCase`, `Output`...) and a `trace` that grows as it goes up.
12
32
  - **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.
13
- - **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`, an OpenAPI 3.1 document, or MCP tools, with problem+json errors and masked responses.
33
+ - **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.
34
+ - **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)).
14
35
  - **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.
15
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 and on Drizzle ORM with `@afora/drizzle`.
16
37
 
@@ -300,11 +321,18 @@ Every field (a property, blueprint, list or record class) implements Standard Sc
300
321
 
301
322
  ## Packages
302
323
 
303
- `@afora/codegen` generates afora code from an `afora.json` (and extracts the `afora.json` of an `Application` back), and `@afora/mcp` is an MCP server that lets an agent build a domain with it: see [packages/codegen](packages/codegen/README.md) and [packages/mcp](packages/mcp/README.md). `@afora/hono` is a `hono` sink for outputs: it serves the operations as a Hono app, with problem+json errors, masked responses and an optional OpenAPI document ([packages/hono](packages/hono/README.md)). `@afora/drizzle` is a repository provider on Drizzle ORM (SQLite and Postgres) that passes the provider contract kit: see [packages/drizzle](packages/drizzle/README.md).
324
+ | Package | What it does |
325
+ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
326
+ | [`@afora/core`](https://www.npmjs.com/package/@afora/core) | Properties, blueprints, records, entities, repositories, use cases, commands and outputs (this repository's root). |
327
+ | [`@afora/codegen`](packages/codegen/README.md) | Generates afora code from an `afora.json`, and extracts the `afora.json` of an `Application` back. |
328
+ | [`@afora/mcp`](packages/mcp/README.md) | An MCP server that lets an agent build a domain with `@afora/codegen`. |
329
+ | [`@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. |
330
+ | [`@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
+ | [`@afora/drizzle`](packages/drizzle/README.md) | A repository provider on Drizzle ORM (SQLite and Postgres) that passes the provider contract kit. |
304
332
 
305
333
  ## Example
306
334
 
307
- `examples/blog` is a blog (users with the `authenticatable` plugin, posts with soft deletion, use cases written as steps, 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()`), a password hashing function 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`.
335
+ `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`.
308
336
 
309
337
  ## Performance
310
338
 
@@ -335,9 +363,9 @@ Where the others win: Zod and Valibot are faster, have far larger ecosystems and
335
363
 
336
364
  ## Contributing
337
365
 
338
- Issues open from a template: bug report, feature request, or feedback from building on afora. Questions go to [Discussions](https://github.com/afora-framework/core/discussions). Pull requests follow [the PR template](.github/PULL_REQUEST_TEMPLATE.md).
366
+ Read [CONTRIBUTING.md](CONTRIBUTING.md) for the setup, the checks and how releases work. Issues open from a template: bug report, feature request, or feedback from building on afora. Questions go to [Discussions](https://github.com/afora-framework/core/discussions); security issues go through [SECURITY.md](SECURITY.md), never a public issue.
339
367
 
340
- Commits follow [Conventional Commits](https://www.conventionalcommits.org) (`feat(repository): …`, `fix: …`), checked by commitlint in a Husky `commit-msg` hook; the `pre-commit` hook runs Biome and Knip. CI runs lint, Knip, type checks, unit and interop tests, the build and the Node smoke test on every push and pull request. Pushing a `v*` tag that matches `package.json` publishes to npm with provenance.
368
+ Commits follow [Conventional Commits](https://www.conventionalcommits.org) (`feat(repository): …`, `fix: …`), checked by commitlint in a Husky `commit-msg` hook; the `pre-commit` hook runs Biome and Knip. CI runs lint, Knip, type checks, unit and interop tests, the build and the Node smoke test on every push and pull request. A pull request that bumps the version in `package.json`, once merged to `main`, publishes to npm with provenance and creates the tag and the GitHub release.
341
369
 
342
370
  Scripts:
343
371
 
@@ -359,8 +387,9 @@ bun run build # build dist/ (ESM + .d.ts)
359
387
  bun run test:codegen # tests of packages/codegen
360
388
  bun run test:mcp # tests of packages/mcp
361
389
  bun run test:hono # tests of packages/hono
390
+ bun run test:ws # tests of packages/ws
362
391
  bun run test:drizzle # tests of packages/drizzle (SQLite and PGlite)
363
- bun run build:packages # build the core, then packages/codegen, packages/mcp, packages/hono and packages/drizzle
392
+ bun run build:packages # build the core, then packages/codegen, packages/mcp, packages/hono, packages/ws and packages/drizzle
364
393
  ```
365
394
 
366
395
  ## Versioning
@@ -73,3 +73,10 @@ export declare function stopProvider(provider: IRepositoryProvider): Promise<voi
73
73
  * @internal
74
74
  */
75
75
  export declare function checkTransactions(source: string, provider: IRepositoryProvider, commands: readonly string[]): void;
76
+ /**
77
+ * Throws `InvalidApplicationDeclaration` when `provider` does not read by
78
+ * cursor (`capabilities.keyset`) and some of `repositories` declare
79
+ * `findCursor`.
80
+ * @internal
81
+ */
82
+ export declare function checkCapabilities(source: string, provider: IRepositoryProvider, repositories: readonly string[]): void;
@@ -1,11 +1,13 @@
1
1
  import type { AnyBlueprint, BlueprintInput, IBlueprint } from "../../blueprint/types/blueprint.interface.js";
2
2
  import type { AnyCommand } from "../../command/types/command.interface.js";
3
+ import type { EntityExpires } from "../../entity/types/entity.interface.js";
3
4
  import type { AnyEnvironment, EnvironmentValue } from "../../environment/environment.js";
5
+ import type { EventPolicyDescription, IEventPublisher } from "../../event-policy/types/event-policy.interface.js";
4
6
  import type { ExceptionLayers } from "../../exception/enums/exception-layers.js";
5
7
  import type { ILogger, LogLevel, LogSettings } from "../../logger/logger.js";
6
8
  import type { AnyOutput, StageDescription } from "../../output/types/output.interface.js";
7
9
  import type { RecordPluginDescriptor } from "../../record/types/plugin.interface.js";
8
- import type { ProviderDescription } from "../../repository/types/query.interface.js";
10
+ import type { ProviderDescription, RepositoryIndex, RepositoryScans } from "../../repository/types/query.interface.js";
9
11
  import type { AnyRepository, IRepositoryProvider } from "../../repository/types/repository.interface.js";
10
12
  import type { Kind } from "../../symbols/kind.js";
11
13
  import type { UseCaseStepDescription } from "../../use-case/steps/types/steps.interface.js";
@@ -96,6 +98,13 @@ export interface ApplicationSettings<Name extends string, Extended extends reado
96
98
  * it, the one of the last extended application that has one; none by default.
97
99
  */
98
100
  readonly log?: LogSettings | LogFactory;
101
+ /**
102
+ * Where event policies such as `publishEvents` publish, by key: a
103
+ * WebSocket hub, a queue, a broker. Commands of the application find them
104
+ * by key, those of the extended applications included; a command whose
105
+ * policy names a key that is not here throws `UnregisteredApplicationReference`.
106
+ */
107
+ readonly publishers?: Readonly<Record<string, IEventPublisher>>;
99
108
  }
100
109
  /**
101
110
  * What an application gives the factories it runs on start (`providerFrom`,
@@ -293,6 +302,22 @@ export interface RecordDescription {
293
302
  readonly change: readonly string[];
294
303
  };
295
304
  readonly plugins: readonly RecordPluginDescriptor[];
305
+ /**
306
+ * The lifecycle events of an entity, by cycle: each event's name and
307
+ * whether `eventOf` was given a handler. Present only when it has some.
308
+ */
309
+ readonly events?: Readonly<Partial<Record<"onCreate" | "onUpdate" | "onDelete", readonly {
310
+ readonly name: string;
311
+ readonly handler: boolean;
312
+ }[]>>>;
313
+ /** Whether the entity is versioned (`entityOf(..., { versioned: true })`); present only when it is. */
314
+ readonly versioned?: true;
315
+ /**
316
+ * When the records of an entity expire, as `entityOf` was given it:
317
+ * `{ after: duration }` (kept in its `expiresAt` identity key, left out of
318
+ * `fields`) or `{ at: key }`. Present only when it has one.
319
+ */
320
+ readonly expires?: EntityExpires;
296
321
  }
297
322
  /** What a use case depends on, by the name it has in the application. */
298
323
  export interface UseCaseDependencyDescription {
@@ -340,6 +365,17 @@ export interface UseCaseDescription {
340
365
  } | {
341
366
  readonly kind: "opaque";
342
367
  };
368
+ /**
369
+ * The use case plugin that made it, the registered repository it was made
370
+ * for and the options given, with classes by name as in the steps; absent
371
+ * for a use case declared by hand.
372
+ */
373
+ readonly generic?: {
374
+ readonly name: string;
375
+ readonly version: string;
376
+ readonly repository: string;
377
+ readonly options: Readonly<Record<string, unknown>>;
378
+ };
343
379
  }
344
380
  /** A command: its metadata, context, operations and policies. */
345
381
  export interface CommandDescription {
@@ -359,7 +395,8 @@ export interface CommandDescription {
359
395
  readonly transaction: boolean;
360
396
  /** The names of its environments, when it declares any. */
361
397
  readonly environments?: readonly string[];
362
- readonly events: "after-commit" | "manual" | {
398
+ /** The policy: a name, a policy made by `eventPolicyOf` by name and options, else opaque. */
399
+ readonly events: "after-commit" | "manual" | EventPolicyDescription | {
363
400
  readonly kind: "opaque";
364
401
  };
365
402
  readonly onEventError?: {
@@ -411,6 +448,10 @@ export interface ApplicationDescription {
411
448
  readonly log?: LogDescription | {
412
449
  readonly kind: "opaque";
413
450
  };
451
+ /** Its publishers (inherited ones included), by key, with the application that declared each; present only when it has some. */
452
+ readonly publishers?: Readonly<Record<string, {
453
+ readonly from: string;
454
+ }>>;
414
455
  readonly properties: Readonly<Record<string, PropertyDescription & {
415
456
  readonly from: string;
416
457
  }>>;
@@ -429,6 +470,10 @@ export interface ApplicationDescription {
429
470
  readonly entity: string;
430
471
  readonly actions: readonly string[];
431
472
  readonly deletion: "hard" | "soft";
473
+ /** Its declared indexes, by name. */
474
+ readonly indexes: Readonly<Record<string, RepositoryIndex>>;
475
+ /** What it does with a query only a scan serves. */
476
+ readonly scans: RepositoryScans;
432
477
  /** The names of the repositories of the entities its entity holds. */
433
478
  readonly relations: readonly string[];
434
479
  /** The names of its `environmentDependencies`. */
@@ -1,7 +1,6 @@
1
- import type { IBlueprintCollection } from "../blueprint/types/blueprint.interface.js";
2
- import { type EnvironmentDependencies } from "../environment/environment.js";
3
1
  import type { IRepositoryProvider } from "../repository/types/repository.interface.js";
4
- import type { AnyCommand, CommandOf, CommandSettings, CommandUseCases } from "./types/command.interface.js";
2
+ import type { IUseCaseCatalog, UseCasePluginContext } from "../use-case/types/plugin.interface.js";
3
+ import type { AnyCommand, CommandBuilder } from "./types/command.interface.js";
5
4
  /**
6
5
  * Checks now that the repositories of `command` share a provider that has
7
6
  * `transaction`, as its first call would; an application calls it when it
@@ -15,11 +14,19 @@ export declare function verifyCommand(command: object): void;
15
14
  * @internal
16
15
  */
17
16
  export declare function commandTransactionProvider(command: object): IRepositoryProvider | undefined;
17
+ /**
18
+ * A `commandOf` whose `useCases` may be a function of the use case plugins
19
+ * of `catalogs`, by name, a later catalog winning a name:
20
+ * `useCases: (ctx) => ({ create: ctx.create(Posts), custom: MyUseCase })`.
21
+ * Throws `InvalidCommandDeclaration` for what is not a catalog made by
22
+ * `useCaseCatalogOf`.
23
+ */
24
+ export declare function commandBuilderOf<const Catalogs extends readonly IUseCaseCatalog[] = []>(...catalogs: Catalogs): CommandBuilder<UseCasePluginContext<Catalogs>>;
18
25
  /**
19
26
  * Groups use cases under operation names and is the boundary of a call: the
20
27
  * context is validated once, a transaction can wrap the use case, and the
21
28
  * events it raised are handled by a policy once it succeeded.
22
29
  */
23
- export declare function commandOf<const UseCases extends CommandUseCases, Context extends IBlueprintCollection | undefined = undefined, Environment extends EnvironmentDependencies | undefined = undefined>(settings: CommandSettings<UseCases, Context, Environment>): CommandOf<UseCases, Context, Environment>;
30
+ export declare const commandOf: CommandBuilder<Record<never, never>>;
24
31
  /** Whether `value` is a command made by `commandOf`. */
25
32
  export declare function isCommand(value: unknown): value is AnyCommand;
@@ -1,6 +1,7 @@
1
1
  import type { BlueprintInput, IBlueprint, IBlueprintCollection } from "../../blueprint/types/blueprint.interface.js";
2
2
  import type { EntityEvent } from "../../entity/event.js";
3
3
  import type { AnyEnvironment, EnvironmentDependencies, MergedEnvironment } from "../../environment/environment.js";
4
+ import type { IEventPublisher } from "../../event-policy/types/event-policy.interface.js";
4
5
  import type { ILogger } from "../../logger/logger.js";
5
6
  import type { Kind } from "../../symbols/kind.js";
6
7
  import type { AnyUseCaseClass, UseCaseInput, UseCaseOptions, UseCaseResponse } from "../../use-case/use-case.js";
@@ -19,6 +20,12 @@ export interface CommandEventInfo {
19
20
  * application's; a logger that writes nothing without either.
20
21
  */
21
22
  readonly logger: ILogger;
23
+ /**
24
+ * The publisher declared under `name` in `applicationOf({ publishers })`:
25
+ * of the application that bound the command, else of the started
26
+ * application. Throws `PublisherNotBound` when neither declares it.
27
+ */
28
+ publisher(name: string): IEventPublisher;
22
29
  }
23
30
  /** Handles all the events of a call, once it succeeded. */
24
31
  export type CommandEventHandler = (events: readonly EntityEvent[], info: CommandEventInfo) => void | Promise<void>;
@@ -48,6 +55,25 @@ export interface CommandSettings<UseCases extends CommandUseCases, Context exten
48
55
  readonly events?: CommandEvents;
49
56
  readonly onEventError?: CommandEventErrorHandler;
50
57
  }
58
+ /**
59
+ * The `useCases` of a command made by a builder: the object, or a function
60
+ * of the use case plugins of its catalogs that gives it.
61
+ */
62
+ export type CommandUseCasesOption<UseCases extends CommandUseCases, Generics> = UseCases | ((ctx: Generics) => UseCases);
63
+ /** `CommandSettings` with `useCases` as a builder takes it. */
64
+ export interface CommandBuilderSettings<UseCases extends CommandUseCases, Context extends IBlueprintCollection | undefined, Environment extends EnvironmentDependencies | undefined, Generics> extends Omit<CommandSettings<UseCases, Context, Environment>, "useCases"> {
65
+ /**
66
+ * The use cases, by operation name; or a function that gets the use
67
+ * case plugins of the builder's catalogs, each called with a repository,
68
+ * and gives them: `(ctx) => ({ create: ctx.create(Posts) })`.
69
+ */
70
+ readonly useCases: CommandUseCasesOption<UseCases, Generics>;
71
+ }
72
+ /**
73
+ * What `commandBuilderOf` gives: `commandOf`, with the use case plugins
74
+ * `Generics` to pick from.
75
+ */
76
+ export type CommandBuilder<Generics> = <const UseCases extends CommandUseCases, Context extends IBlueprintCollection | undefined = undefined, Environment extends EnvironmentDependencies | undefined = undefined>(settings: CommandBuilderSettings<UseCases, Context, Environment, Generics>) => CommandOf<UseCases, Context, Environment>;
51
77
  /** The names of the operations of a command. */
52
78
  export type CommandOperation<Command extends {
53
79
  readonly useCases: object;
@@ -12,10 +12,37 @@ export declare const RESERVED_ENTITY_KEYS: readonly string[];
12
12
  export declare const EntityClock: unique symbol;
13
13
  /** The unique keys of a class made by `entityOf`, as `options.unique` lists them. */
14
14
  export declare const EntityUniqueKeys: unique symbol;
15
+ /** The storage options of an entity class (`versioned`, `expires`). */
16
+ export declare const EntitySettings: unique symbol;
17
+ /**
18
+ * The milliseconds after `createdAt` at which the entities of a class
19
+ * declared with `expires: { after }` expire, kept in `expiresAt`.
20
+ *
21
+ * @internal
22
+ */
23
+ export declare const EntityLifetime: unique symbol;
24
+ /** The key the entities of a class declared with `expires: { after }` keep their expiry in. */
25
+ export declare const EXPIRES_AT = "expiresAt";
26
+ /** The key a versioned entity adds to its identity. */
27
+ export declare const ENTITY_VERSION_KEY = "version";
28
+ /** Whether `entityClass` was declared with `versioned: true`. */
29
+ export declare function isVersionedEntityClass(entityClass: unknown): boolean;
30
+ /**
31
+ * The identity keys of the entities of `entityClass`, which only the entity
32
+ * changes: `ENTITY_IDENTITY_KEYS`, `version` when it is versioned, and
33
+ * `expiresAt` when it expires `after` a duration.
34
+ */
35
+ export declare function identityKeysOf(entityClass: unknown): readonly string[];
36
+ /**
37
+ * Stores `version` as the version of `entity`, as its repository does once
38
+ * a write succeeded. @internal
39
+ */
40
+ export declare function storeEntityVersion(entity: object, version: number): void;
15
41
  /**
16
42
  * A record with identity: an `id` and a `createdAt`, made when it is built
17
43
  * and never changed after, an `updatedAt` set by each change, and a
18
- * `deletedAt` set by `destroy`, after which it no longer changes.
44
+ * `deletedAt` set by `destroy`, after which it no longer changes. A versioned
45
+ * entity also has a `version`, 1 when built, which only its repository changes.
19
46
  */
20
47
  export declare abstract class AbstractEntity<Properties extends IBlueprintCollection, Sensitive extends string = never> extends AbstractRecord<Properties, Sensitive> {
21
48
  constructor(value: BlueprintInput<Properties>);
@@ -9,7 +9,7 @@ import type { EntityBuilder, EntityBuilderSettings, IEntityIdentity } from "./ty
9
9
  * `options.plugins` picks from the plugins of `plugins`, catalogs made by
10
10
  * `recordPluginCatalogOf`.
11
11
  */
12
- export declare function entityBuilderOf<Identity extends IEntityIdentity, const Catalogs extends readonly IRecordPluginCatalog[] = []>({ identity, generateId, now, plugins: catalogs, }: EntityBuilderSettings<Identity, Catalogs>): EntityBuilder<Identity, RecordPluginContext<Catalogs>>;
12
+ export declare function entityBuilderOf<Identity extends IEntityIdentity, const Catalogs extends readonly IRecordPluginCatalog[] = []>({ identity, generateId, now, expiresAt, plugins: catalogs, }: EntityBuilderSettings<Identity, Catalogs>): EntityBuilder<Identity, RecordPluginContext<Catalogs>>;
13
13
  /** Whether `value` is a class made by `entityOf`, or one extending it. */
14
14
  export declare function isEntityClass(value: unknown): value is AnyRecordClass & AnyEntityClass;
15
15
  /** Whether `value` is an entity, an instance of a class made by `entityOf`. */
@@ -32,8 +32,19 @@ export interface EntityEvents {
32
32
  readonly onDelete?: EntityEventReference;
33
33
  }
34
34
  export declare const EntityEventSettings: unique symbol;
35
- /** Defines an event without executing its handler. */
36
- export declare function eventOf<Class extends AnyEntityClass>(name: string, entityClass: Class, handler: (entity: InstanceType<Class>, context: EventHandlerContext) => Promise<void>): EventDefinition;
35
+ /**
36
+ * Defines an event without executing its handler. Without a handler, the
37
+ * event only carries the entity, for a policy such as `publishEvents`, and
38
+ * `handle()` does nothing.
39
+ */
40
+ export declare function eventOf<Class extends AnyEntityClass>(name: string, entityClass: Class, handler?: (entity: InstanceType<Class>, context: EventHandlerContext) => Promise<void>): EventDefinition;
41
+ /** Whether `event` was given a handler by `eventOf`. @internal */
42
+ export declare function eventHasHandler(event: EventDefinition): boolean;
43
+ /**
44
+ * The lifecycle events of an entity class, by cycle, with their function
45
+ * references resolved: what `describe()` gives. @internal
46
+ */
47
+ export declare function lifecycleEventsOf(entityClass: object): Partial<Record<keyof EntityEvents, readonly EventDefinition[]>>;
37
48
  export declare function enqueueEvent(entity: object, event: EventDefinition): void;
38
49
  export declare function pullEntityEvents<Entity extends object>(entity: Entity): EntityEvent<Entity>[];
39
50
  export declare function enqueueLifecycle(entity: object, cycle: keyof EntityEvents): void;
@@ -17,3 +17,11 @@ export declare const ImmutableEntityKey: import("../../index.js").DeclaredExcept
17
17
  export declare const DeletedEntity: import("../../index.js").DeclaredExceptionClass<{
18
18
  readonly id: string;
19
19
  }>;
20
+ /**
21
+ * The `expires` option of an entity is invalid: `at` names no date key of its
22
+ * blueprint, `after` is not a duration of positive numbers, or the entity
23
+ * builder has no `expiresAt` field; `source` is the entity name.
24
+ */
25
+ export declare const InvalidEntityExpiry: import("../../index.js").DeclaredExceptionClass<{
26
+ readonly reason: string;
27
+ }>;
@@ -1,11 +1,14 @@
1
+ import type { TString } from "typebox";
1
2
  import type { BlueprintField, BlueprintInput, BlueprintValue, IBlueprint, IBlueprintCollection, JoinedProperties } from "../../blueprint/types/blueprint.interface.js";
2
3
  import type { ParseResult } from "../../parse/types/parse.interface.js";
4
+ import type { PropertyClass } from "../../property/property.js";
3
5
  import type { AnyRecordPlugin, IRecordPluginCatalog, PluginMembers, PluginSensitiveKey, PluginUniqueKey, WithPluginProperties } from "../../record/types/plugin.interface.js";
4
6
  import type { MarkedUniqueKey, RecordChanges, RecordKey, RecordOptions, RecordSettings } from "../../record/types/record.interface.js";
5
7
  import type { StandardSchemaV1 } from "../../standard/standard-schema.js";
6
8
  import type { Kind } from "../../symbols/kind.js";
7
9
  import type { AbstractEntity, EntityUniqueKeys } from "../abstract-entity.js";
8
10
  import type { EntityClassBrand, EntityEvents } from "../event.js";
11
+ import type { EntityVersion } from "../version.js";
9
12
  /** The keys every entity has, which its blueprint cannot declare. */
10
13
  export type EntityIdentityKey = "id" | "createdAt" | "updatedAt" | "deletedAt";
11
14
  /**
@@ -15,6 +18,12 @@ export type EntityIdentityKey = "id" | "createdAt" | "updatedAt" | "deletedAt";
15
18
  export type IEntityIdentity = {
16
19
  readonly [Key in EntityIdentityKey]: BlueprintField;
17
20
  };
21
+ /** The identity key a versioned entity adds: `version`, a positive integer. */
22
+ export type IEntityVersion = {
23
+ readonly version: typeof EntityVersion;
24
+ };
25
+ /** `Identity`, with `version` when `Versioned` is `true`. */
26
+ export type VersionedIdentity<Identity extends IEntityIdentity, Versioned extends boolean> = Versioned extends true ? Identity & IEntityVersion : Identity;
18
27
  /** What an entity builder needs: the identity fields, and how to make an id and a timestamp. */
19
28
  export interface EntityBuilderSettings<Identity extends IEntityIdentity, Catalogs extends readonly IRecordPluginCatalog[] = []> {
20
29
  /** The blueprint of `id`, `createdAt`, `updatedAt` and `deletedAt`. */
@@ -23,6 +32,11 @@ export interface EntityBuilderSettings<Identity extends IEntityIdentity, Catalog
23
32
  readonly generateId: () => string;
24
33
  /** The current date and time, as the timestamps accept it. */
25
34
  readonly now: () => string;
35
+ /**
36
+ * The field of `expiresAt`, a date and time, which entities declared with
37
+ * `expires: { after }` get; without it such a declaration throws.
38
+ */
39
+ readonly expiresAt?: ExpiresAtField;
26
40
  /**
27
41
  * Catalogs of plugins, made by `recordPluginCatalogOf`, that
28
42
  * `options.plugins` picks from, as `(ctx) => [ctx.publishable]`.
@@ -45,7 +59,55 @@ export type EntityProperties<Identity extends IEntityIdentity, Properties extend
45
59
  */
46
60
  export type EntityInput<Identity extends IEntityIdentity, Properties extends IBlueprintCollection> = BlueprintInput<Properties> & Partial<BlueprintInput<Identity>>;
47
61
  /** What `entityOf` takes, after the blueprint, to describe its keys. */
48
- export type EntityOptions<Properties extends IBlueprintCollection, Change extends readonly RecordKey<Properties>[] = readonly RecordKey<Properties>[], Unique extends readonly RecordKey<Properties>[] = readonly RecordKey<Properties>[], Plugins extends readonly AnyRecordPlugin[] = readonly AnyRecordPlugin[], Context = unknown, Sensitive extends readonly RecordKey<Properties>[] = readonly RecordKey<Properties>[]> = RecordOptions<Properties, Change, Unique, Plugins, Context, Sensitive> & EntityEvents;
62
+ export type EntityOptions<Properties extends IBlueprintCollection, Change extends readonly RecordKey<Properties>[] = readonly RecordKey<Properties>[], Unique extends readonly RecordKey<Properties>[] = readonly RecordKey<Properties>[], Plugins extends readonly AnyRecordPlugin[] = readonly AnyRecordPlugin[], Context = unknown, Sensitive extends readonly RecordKey<Properties>[] = readonly RecordKey<Properties>[], Expires extends EntityExpires<RecordKey<Properties>> | undefined = EntityExpires<RecordKey<Properties>> | undefined> = RecordOptions<Properties, Change, Unique, Plugins, Context, Sensitive> & EntityEvents & EntityStorageOptions<RecordKey<Properties>, Expires>;
63
+ /** How an entity is stored: optimistic concurrency and expiry. */
64
+ export interface EntityStorageOptions<Key extends string = string, Expires extends EntityExpires<Key> | undefined = EntityExpires<Key> | undefined> {
65
+ /**
66
+ * Adds a `version` identity key (1 when created) that the `update` of its
67
+ * repository checks and increments, throwing `RepositoryConcurrencyConflict`
68
+ * when the stored one differs: optimistic concurrency.
69
+ */
70
+ readonly versioned?: boolean;
71
+ /**
72
+ * When records of the entity expire, which reads then leave out:
73
+ * `{ after: { days: 400 } }` from `createdAt`, kept in an `expiresAt`
74
+ * identity key, or `{ at: "until" }`, the date held by a key of the
75
+ * blueprint. A record with no date never expires.
76
+ */
77
+ readonly expires?: Expires;
78
+ }
79
+ /** A duration, e.g. `{ days: 400 }`. */
80
+ export interface EntityDuration {
81
+ readonly days?: number;
82
+ readonly hours?: number;
83
+ readonly minutes?: number;
84
+ readonly seconds?: number;
85
+ }
86
+ /**
87
+ * `after` a duration from `createdAt` (stored as `expiresAt`), or `at` the
88
+ * date held by a key of the blueprint, `Key`.
89
+ */
90
+ export type EntityExpires<Key extends string = string> = {
91
+ readonly after: EntityDuration;
92
+ } | {
93
+ readonly at: Key;
94
+ };
95
+ /**
96
+ * The field `expiresAt` of the entities that expire `after` a duration: a
97
+ * required date and time, as `createdAt`.
98
+ */
99
+ export type ExpiresAtField = PropertyClass<TString, string>;
100
+ /** `Identity`, with `expiresAt` when `Expires` is `{ after }`. */
101
+ export type ExpiringIdentity<Identity extends IEntityIdentity, Expires> = Expires extends {
102
+ readonly after: EntityDuration;
103
+ } ? Identity & {
104
+ readonly expiresAt: ExpiresAtField;
105
+ } : Identity;
106
+ /** What `entityOf` stores under `EntitySettings`. */
107
+ export interface EntityStorageSettings {
108
+ readonly versioned: boolean;
109
+ readonly expires?: EntityExpires;
110
+ }
49
111
  /**
50
112
  * The unique keys of an entity: `id`, those listed in `options.unique`, and
51
113
  * those holding a property declared `unique`, as `Slug`.
@@ -86,4 +148,6 @@ export type EntityConstructor<Identity extends IEntityIdentity, Properties exten
86
148
  * What `entityBuilderOf` returns: `entityOf`, declaring entities with the
87
149
  * identity it was given, whose `options.plugins` picks from `Context`.
88
150
  */
89
- export type EntityBuilder<Identity extends IEntityIdentity, Context = Record<never, never>> = <Properties extends IBlueprintCollection & Partial<Record<EntityIdentityKey, never>>, const Change extends readonly RecordKey<Properties>[] = [], const Unique extends readonly RecordKey<Properties>[] = [], const Plugins extends readonly AnyRecordPlugin[] = [], const Sensitive extends readonly RecordKey<Properties>[] = []>(blueprint: IBlueprint<Properties, boolean>, options?: EntityOptions<NoInfer<Properties>, Change, Unique, Plugins, Context, Sensitive>) => EntityConstructor<Identity, WithPluginProperties<Properties, Plugins>, Change, readonly (Unique[number] | PluginUniqueKey<Plugins>)[], PluginMembers<Plugins>, Sensitive[number] | PluginSensitiveKey<Plugins>>;
151
+ export type EntityBuilder<Identity extends IEntityIdentity, Context = Record<never, never>> = <Properties extends IBlueprintCollection & Partial<Record<EntityIdentityKey, never>>, const Change extends readonly RecordKey<Properties>[] = [], const Unique extends readonly RecordKey<Properties>[] = [], const Plugins extends readonly AnyRecordPlugin[] = [], const Sensitive extends readonly RecordKey<Properties>[] = [], const Versioned extends boolean = false, const Expires extends EntityExpires<RecordKey<Properties>> | undefined = undefined>(blueprint: IBlueprint<Properties, boolean>, options?: EntityOptions<NoInfer<Properties>, Change, Unique, Plugins, Context, Sensitive, Expires> & {
152
+ readonly versioned?: Versioned;
153
+ }) => EntityConstructor<VersionedIdentity<ExpiringIdentity<Identity, Expires>, Versioned>, WithPluginProperties<Properties, Plugins>, Change, readonly (Unique[number] | PluginUniqueKey<Plugins>)[], PluginMembers<Plugins>, Sensitive[number] | PluginSensitiveKey<Plugins>>;
@@ -0,0 +1,13 @@
1
+ import { Type } from "typebox";
2
+ declare const EntityVersion_base: import("../../index.js").PropertyClass<Type.TInteger, number>;
3
+ /**
4
+ * The `version` of a versioned entity: a positive integer, 1 when created,
5
+ * which each `update` of its repository checks and increments.
6
+ */
7
+ export declare class EntityVersion extends EntityVersion_base {
8
+ }
9
+ /** The identity key a versioned entity adds after the others. */
10
+ export declare const VersionIdentity: import("../../index.js").IBlueprint<import("../property/types/property-options.interface.js").StoredFields<{
11
+ version: typeof EntityVersion;
12
+ }>, false>;
13
+ export {};
@@ -0,0 +1,25 @@
1
+ import type { EventPolicyDescription, EventPolicyFactory, EventPolicyOptions, EventPolicySettings, IEventPolicyCatalog } from "./types/event-policy.interface.js";
2
+ /**
3
+ * A factory of event policies for the `events` setting of `commandOf`:
4
+ * `events: publishEvents({ publisher: "realtime", topic: "posts/{id}" })`.
5
+ * Its options are JSON, checked by `validate` when the policy is made (an
6
+ * `InvalidEventPolicy` otherwise), so the policy can be written in an
7
+ * `afora.json` and an application describes it by name and options instead
8
+ * of `{ kind: "opaque" }`. `handle` gets the events of a call that succeeded,
9
+ * what a handler policy gets, and the options.
10
+ */
11
+ export declare function eventPolicyOf<Options extends object = EventPolicyOptions>(settings: EventPolicySettings<Options>): EventPolicyFactory<Options>;
12
+ /**
13
+ * The name and options of a policy made by `eventPolicyOf`; `undefined` for
14
+ * any other value, such as a handler written by hand.
15
+ */
16
+ export declare function eventPolicyDescriptionOf(handler: unknown): EventPolicyDescription | undefined;
17
+ /** The keys of the application `publishers` a policy uses; none for a handler written by hand. @internal */
18
+ export declare function eventPolicyPublishersOf(handler: unknown): readonly string[];
19
+ /**
20
+ * A catalog of event policy factories, by name: what resolves
21
+ * `{ "policy": name, "options": {...} }` back to a policy. Throws
22
+ * `InvalidEventPolicy` for a function that is not a factory made by
23
+ * `eventPolicyOf` or for two with the same name.
24
+ */
25
+ export declare function eventPolicyCatalogOf(...policies: readonly EventPolicyFactory<never>[]): IEventPolicyCatalog;
@@ -0,0 +1,11 @@
1
+ /** `eventPolicyOf`, a policy's options or a catalog got something it cannot use; `details.reason` says what. */
2
+ export declare const InvalidEventPolicy: import("../../index.js").DeclaredExceptionClass<{
3
+ readonly reason: string;
4
+ }>;
5
+ /**
6
+ * An event policy asked for a publisher that neither the application that
7
+ * bound the command nor the started application declares.
8
+ */
9
+ export declare const PublisherNotBound: import("../../index.js").DeclaredExceptionClass<{
10
+ readonly publisher: string;
11
+ }>;
@@ -0,0 +1,17 @@
1
+ import type { PublishEventsOptions } from "./types/event-policy.interface.js";
2
+ /**
3
+ * An event policy that publishes the events of a call to a publisher of the
4
+ * application, once the call succeeded (and committed):
5
+ * `events: publishEvents({ publisher: "realtime", topic: "posts/{id}" })`.
6
+ *
7
+ * For each event, in order, it runs the event's own handler first (unless
8
+ * `handle: false`), then publishes `{ name, command, operation, entity }`
9
+ * to the topic, the entity as JSON with sensitive values masked; the context
10
+ * and the variables are never published. `events` limits it to some names.
11
+ * A handler that fails stops the call with `CommandEventFailed`, as with
12
+ * `"after-commit"`. A publish that fails is logged and skipped
13
+ * (`onPublishError: "log"`, the default) or fails the call (`"throw"`). A
14
+ * publisher the application does not declare fails the call, caused by
15
+ * `PublisherNotBound`.
16
+ */
17
+ export declare const publishEvents: import("./types/event-policy.interface.js").EventPolicyFactory<PublishEventsOptions>;
@@ -0,0 +1,18 @@
1
+ import type { IEventPublisher } from "./types/event-policy.interface.js";
2
+ /** The publishers of an application, by key. */
3
+ export type Publishers = Readonly<Record<string, IEventPublisher>>;
4
+ /**
5
+ * Carries the publishers of an application, when it declares any: on the
6
+ * application itself. @internal
7
+ */
8
+ export declare const ApplicationPublishersKey: unique symbol;
9
+ /** Gives `command` the publishers of the application that bound it. @internal */
10
+ export declare function bindPublishers(command: object, publishers: Publishers): void;
11
+ /** Whether `value` can publish: an object with a `publish` function. @internal */
12
+ export declare function isEventPublisher(value: unknown): value is IEventPublisher;
13
+ /**
14
+ * The publisher `name` for a call of `command`: one of the application that
15
+ * bound the command, else one of the started application. Throws
16
+ * `PublisherNotBound` when neither declares it. @internal
17
+ */
18
+ export declare function resolvePublisher(command: object, name: string, source: string): IEventPublisher;