@afora/core 0.0.7 → 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 (49) hide show
  1. package/README.md +34 -6
  2. package/dist/core/application/start.d.ts +7 -0
  3. package/dist/core/application/types/application.interface.d.ts +36 -2
  4. package/dist/core/command/types/command.interface.d.ts +7 -0
  5. package/dist/core/entity/abstract-entity.d.ts +28 -1
  6. package/dist/core/entity/entity.d.ts +1 -1
  7. package/dist/core/entity/event.d.ts +13 -2
  8. package/dist/core/entity/exceptions.d.ts +8 -0
  9. package/dist/core/entity/types/entity.interface.d.ts +66 -2
  10. package/dist/core/entity/version.d.ts +13 -0
  11. package/dist/core/event-policy/event-policy.d.ts +25 -0
  12. package/dist/core/event-policy/exceptions.d.ts +11 -0
  13. package/dist/core/event-policy/publish.d.ts +17 -0
  14. package/dist/core/event-policy/publishers.d.ts +18 -0
  15. package/dist/core/event-policy/types/event-policy.interface.d.ts +72 -0
  16. package/dist/core/output/coerce.d.ts +9 -0
  17. package/dist/core/output/openapi-sink.d.ts +138 -38
  18. package/dist/core/output/status.d.ts +1 -1
  19. package/dist/core/parse/parse.d.ts +10 -0
  20. package/dist/core/parse/strip.d.ts +13 -0
  21. package/dist/core/repository/exceptions.d.ts +30 -0
  22. package/dist/core/repository/in-memory.d.ts +14 -1
  23. package/dist/core/repository/plan.d.ts +20 -0
  24. package/dist/core/repository/provider.d.ts +1 -1
  25. package/dist/core/repository/query.d.ts +9 -2
  26. package/dist/core/repository/repository.d.ts +40 -8
  27. package/dist/core/repository/state.d.ts +30 -2
  28. package/dist/core/repository/types/query.interface.d.ts +106 -5
  29. package/dist/core/repository/types/repository.interface.d.ts +106 -7
  30. package/dist/experimental.js +4 -4
  31. package/dist/{index-r78dsvtc.js → index-qxfkyd6k.js} +612 -612
  32. package/dist/{index-q6hggpzw.js → index-t5qwtyj5.js} +1807 -280
  33. package/dist/index.d.ts +22 -11
  34. package/dist/index.js +62 -14
  35. package/dist/std/blueprint-plugin.d.ts +6 -2
  36. package/dist/std/blueprint.d.ts +3 -0
  37. package/dist/std/entity.d.ts +2 -1
  38. package/dist/std/event-policies.d.ts +2 -0
  39. package/dist/std/properties/exceptions.d.ts +11 -0
  40. package/dist/std/properties/json.d.ts +54 -0
  41. package/dist/std/properties/time.d.ts +4 -0
  42. package/dist/std/use-cases/crud.d.ts +6 -4
  43. package/dist/std/use-cases/shared.d.ts +32 -4
  44. package/dist/std/use-cases/types.d.ts +100 -21
  45. package/dist/testing/index.d.ts +2 -0
  46. package/dist/testing/index.js +347 -2
  47. package/dist/testing/memory-publisher.d.ts +16 -0
  48. package/dist/testing/provider-contract.d.ts +14 -0
  49. 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,7 @@ 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.
14
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)).
15
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.
16
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`.
@@ -301,7 +321,14 @@ Every field (a property, blueprint, list or record class) implements Standard Sc
301
321
 
302
322
  ## Packages
303
323
 
304
- `@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. |
305
332
 
306
333
  ## Example
307
334
 
@@ -336,9 +363,9 @@ Where the others win: Zod and Valibot are faster, have far larger ecosystems and
336
363
 
337
364
  ## Contributing
338
365
 
339
- 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.
340
367
 
341
- 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.
342
369
 
343
370
  Scripts:
344
371
 
@@ -360,8 +387,9 @@ bun run build # build dist/ (ESM + .d.ts)
360
387
  bun run test:codegen # tests of packages/codegen
361
388
  bun run test:mcp # tests of packages/mcp
362
389
  bun run test:hono # tests of packages/hono
390
+ bun run test:ws # tests of packages/ws
363
391
  bun run test:drizzle # tests of packages/drizzle (SQLite and PGlite)
364
- 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
365
393
  ```
366
394
 
367
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 {
@@ -370,7 +395,8 @@ export interface CommandDescription {
370
395
  readonly transaction: boolean;
371
396
  /** The names of its environments, when it declares any. */
372
397
  readonly environments?: readonly string[];
373
- 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 | {
374
400
  readonly kind: "opaque";
375
401
  };
376
402
  readonly onEventError?: {
@@ -422,6 +448,10 @@ export interface ApplicationDescription {
422
448
  readonly log?: LogDescription | {
423
449
  readonly kind: "opaque";
424
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
+ }>>;
425
455
  readonly properties: Readonly<Record<string, PropertyDescription & {
426
456
  readonly from: string;
427
457
  }>>;
@@ -440,6 +470,10 @@ export interface ApplicationDescription {
440
470
  readonly entity: string;
441
471
  readonly actions: readonly string[];
442
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;
443
477
  /** The names of the repositories of the entities its entity holds. */
444
478
  readonly relations: readonly string[];
445
479
  /** The names of its `environmentDependencies`. */
@@ -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>;
@@ -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;
@@ -0,0 +1,72 @@
1
+ import type { CommandEventHandler, CommandEventInfo } from "../../command/types/command.interface.js";
2
+ import type { JsonValue } from "../../json.js";
3
+ import type { Kind } from "../../symbols/kind.js";
4
+ /** The options of an event policy: JSON, so it can live in an `afora.json`. */
5
+ export type EventPolicyOptions = {
6
+ readonly [key: string]: JsonValue;
7
+ };
8
+ /** What `eventPolicyOf` takes. */
9
+ export interface EventPolicySettings<Options extends object = EventPolicyOptions> {
10
+ /** The name the policy is known by in a catalog and in `afora.json`. */
11
+ readonly name: string;
12
+ /** Why `options` cannot be used, or `undefined` when they can. */
13
+ readonly validate?: (options: Options) => string | undefined;
14
+ /** The handler a command awaits with the events of a call that succeeded. */
15
+ readonly handle: (events: Parameters<CommandEventHandler>[0], info: CommandEventInfo, options: Options) => void | Promise<void>;
16
+ /** The keys of the application `publishers` the policy uses, so an application checks them when declared. */
17
+ readonly publishers?: (options: Options) => readonly string[];
18
+ }
19
+ /**
20
+ * Makes an event policy for the `events` setting of `commandOf`: a
21
+ * `CommandEventHandler` that remembers its name and options.
22
+ */
23
+ export type EventPolicyFactory<Options extends object = EventPolicyOptions> = (options?: Options) => CommandEventHandler;
24
+ /** How an event policy made by a factory is described: by name and options. */
25
+ export interface EventPolicyDescription {
26
+ readonly kind: "policy";
27
+ readonly name: string;
28
+ readonly options: EventPolicyOptions;
29
+ }
30
+ /** Event policy factories by name: what resolves `{ "policy": name, "options": {...} }` of an `afora.json`. */
31
+ export interface IEventPolicyCatalog {
32
+ readonly [Kind]: "event-policy-catalog";
33
+ readonly policies: Readonly<Record<string, EventPolicyFactory<never>>>;
34
+ /** Makes the policy `name` with `options`; throws `InvalidEventPolicy` for an unknown name or invalid options. */
35
+ resolve(name: string, options?: EventPolicyOptions): CommandEventHandler;
36
+ }
37
+ /** An event as a publisher receives it: JSON, the entity masked by its `toJSON`. */
38
+ export interface PublishedEvent {
39
+ /** The name given to `eventOf`. */
40
+ readonly name: string;
41
+ readonly command: string;
42
+ readonly operation: string;
43
+ /** The entity as JSON: sensitive values are masked. */
44
+ readonly entity: JsonValue;
45
+ }
46
+ /**
47
+ * Where published events go: a WebSocket hub, a queue, a broker. Declared
48
+ * under a key in `applicationOf({ publishers })`.
49
+ */
50
+ export interface IEventPublisher {
51
+ publish(topic: string, event: PublishedEvent): void | Promise<void>;
52
+ }
53
+ /** The options of the `publishEvents` policy. */
54
+ export interface PublishEventsOptions {
55
+ /** The key of the publisher in `applicationOf({ publishers })`. */
56
+ readonly publisher: string;
57
+ /**
58
+ * The topic of each event. `{event}`, `{command}`, `{operation}` and
59
+ * `{id}` (the id of the entity) are replaced: `"posts/{id}"`.
60
+ */
61
+ readonly topic: string;
62
+ /** The names of the events to publish; all of them by default. */
63
+ readonly events?: readonly string[];
64
+ /** Whether each event runs its own handler first, as `"after-commit"` does. `true` by default. */
65
+ readonly handle?: boolean;
66
+ /**
67
+ * What a failed publish does: `"log"` (default) writes an error to the
68
+ * logger and goes on, since the call already committed; `"throw"` makes
69
+ * the call fail with `CommandEventFailed`.
70
+ */
71
+ readonly onPublishError?: "log" | "throw";
72
+ }
@@ -0,0 +1,9 @@
1
+ import type { AnyBlueprint } from "../blueprint/types/blueprint.interface.js";
2
+ /**
3
+ * Text values (a query string, headers, the values of a session) as the
4
+ * scalar fields of `blueprint` take them: a number or a boolean where the
5
+ * field is one rather than a string, else as is, so the blueprint rejects
6
+ * invalid text with its own issue. `undefined` values are left out. For
7
+ * sinks, which read text from their transport.
8
+ */
9
+ export declare function coerceTextValues(blueprint: AnyBlueprint, texts: Readonly<Record<string, string | undefined>>): Record<string, unknown>;