@afora/core 0.0.6 → 0.0.7
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 +2 -1
- package/dist/core/application/types/application.interface.d.ts +11 -0
- package/dist/core/command/command.d.ts +11 -4
- package/dist/core/command/types/command.interface.d.ts +19 -0
- package/dist/core/output/status.d.ts +2 -1
- package/dist/core/property/input.d.ts +20 -0
- package/dist/core/record/plugin.d.ts +6 -2
- package/dist/core/repository/repository.d.ts +1 -1
- package/dist/core/repository/state.d.ts +9 -0
- package/dist/core/repository/types/repository.interface.d.ts +9 -2
- package/dist/core/use-case/exceptions.d.ts +36 -0
- package/dist/core/use-case/plugin-catalog.d.ts +11 -0
- package/dist/core/use-case/plugin.d.ts +25 -0
- package/dist/core/use-case/steps/types/steps.interface.d.ts +23 -6
- package/dist/core/use-case/target.d.ts +9 -0
- package/dist/core/use-case/types/plugin.interface.d.ts +185 -0
- package/dist/experimental.js +2 -2
- package/dist/{index-gbgc40xe.js → index-q6hggpzw.js} +1024 -48
- package/dist/{index-3nd9bjnc.js → index-r78dsvtc.js} +68 -57
- package/dist/index.d.ts +15 -6
- package/dist/index.js +41 -5
- package/dist/std/command.d.ts +11 -0
- package/dist/std/properties/password.d.ts +16 -1
- package/dist/std/use-cases/auth.d.ts +5 -0
- package/dist/std/use-cases/crud.d.ts +23 -0
- package/dist/std/use-cases/index.d.ts +11 -0
- package/dist/std/use-cases/lifecycle.d.ts +13 -0
- package/dist/std/use-cases/shared.d.ts +60 -0
- package/dist/std/use-cases/types.d.ts +212 -0
- package/dist/testing/index.js +10 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ afora is not a general-purpose validator. It is for code that models a domain, a
|
|
|
11
11
|
- **Exceptions that know their layer.** Each error carries a layer (`Property`, `Record`, `UseCase`, `Output`...) and a `trace` that grows as it goes up.
|
|
12
12
|
- **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
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.
|
|
14
|
+
- **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
15
|
- **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
16
|
- **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
17
|
|
|
@@ -304,7 +305,7 @@ Every field (a property, blueprint, list or record class) implements Standard Sc
|
|
|
304
305
|
|
|
305
306
|
## Example
|
|
306
307
|
|
|
307
|
-
`examples/blog` is a blog (users with the `authenticatable` plugin, posts with soft deletion, use
|
|
308
|
+
`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
309
|
|
|
309
310
|
## Performance
|
|
310
311
|
|
|
@@ -340,6 +340,17 @@ export interface UseCaseDescription {
|
|
|
340
340
|
} | {
|
|
341
341
|
readonly kind: "opaque";
|
|
342
342
|
};
|
|
343
|
+
/**
|
|
344
|
+
* The use case plugin that made it, the registered repository it was made
|
|
345
|
+
* for and the options given, with classes by name as in the steps; absent
|
|
346
|
+
* for a use case declared by hand.
|
|
347
|
+
*/
|
|
348
|
+
readonly generic?: {
|
|
349
|
+
readonly name: string;
|
|
350
|
+
readonly version: string;
|
|
351
|
+
readonly repository: string;
|
|
352
|
+
readonly options: Readonly<Record<string, unknown>>;
|
|
353
|
+
};
|
|
343
354
|
}
|
|
344
355
|
/** A command: its metadata, context, operations and policies. */
|
|
345
356
|
export interface CommandDescription {
|
|
@@ -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 {
|
|
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
|
|
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;
|
|
@@ -48,6 +48,25 @@ export interface CommandSettings<UseCases extends CommandUseCases, Context exten
|
|
|
48
48
|
readonly events?: CommandEvents;
|
|
49
49
|
readonly onEventError?: CommandEventErrorHandler;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* The `useCases` of a command made by a builder: the object, or a function
|
|
53
|
+
* of the use case plugins of its catalogs that gives it.
|
|
54
|
+
*/
|
|
55
|
+
export type CommandUseCasesOption<UseCases extends CommandUseCases, Generics> = UseCases | ((ctx: Generics) => UseCases);
|
|
56
|
+
/** `CommandSettings` with `useCases` as a builder takes it. */
|
|
57
|
+
export interface CommandBuilderSettings<UseCases extends CommandUseCases, Context extends IBlueprintCollection | undefined, Environment extends EnvironmentDependencies | undefined, Generics> extends Omit<CommandSettings<UseCases, Context, Environment>, "useCases"> {
|
|
58
|
+
/**
|
|
59
|
+
* The use cases, by operation name; or a function that gets the use
|
|
60
|
+
* case plugins of the builder's catalogs, each called with a repository,
|
|
61
|
+
* and gives them: `(ctx) => ({ create: ctx.create(Posts) })`.
|
|
62
|
+
*/
|
|
63
|
+
readonly useCases: CommandUseCasesOption<UseCases, Generics>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* What `commandBuilderOf` gives: `commandOf`, with the use case plugins
|
|
67
|
+
* `Generics` to pick from.
|
|
68
|
+
*/
|
|
69
|
+
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
70
|
/** The names of the operations of a command. */
|
|
52
71
|
export type CommandOperation<Command extends {
|
|
53
72
|
readonly useCases: object;
|
|
@@ -10,7 +10,8 @@ export declare function raisedOf(error: Exception): Exception;
|
|
|
10
10
|
* The HTTP-style status of an error, in this order: `statuses` by exception
|
|
11
11
|
* name; 400 for invalid input or context (and any exception whose
|
|
12
12
|
* `details.issues` is a list, except `InvalidCommandEnvironment`, a 500); 404 for a missing entity or operation; 409 for
|
|
13
|
-
* a unique violation
|
|
13
|
+
* a unique violation or an entity that already exists; 401 for
|
|
14
|
+
* `UseCaseInvalidCredentials`; 422 for a class in `operation.errors`; 500 for the
|
|
14
15
|
* rest, errors that are not exceptions included. Exceptions match by name,
|
|
15
16
|
* or by class for `operation.errors`.
|
|
16
17
|
*/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AnyPropertyConstructor } from "./types/property-collection.type.js";
|
|
2
|
+
/**
|
|
3
|
+
* The static of a property class whose callers give another form of its
|
|
4
|
+
* value than the one it stores: a password is given plain and stored as a
|
|
5
|
+
* hash. Classes extending or refining the property keep it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PropertyInput: unique symbol;
|
|
8
|
+
/** What a property class holds under `PropertyInput`. */
|
|
9
|
+
export interface IPropertyInput<Value = unknown> {
|
|
10
|
+
/** The property that validates what a caller gives, of the same value type. */
|
|
11
|
+
readonly accepts: AnyPropertyConstructor;
|
|
12
|
+
/** Turns what a caller gave into the value the property stores. */
|
|
13
|
+
convert(value: Value): Value | Promise<Value>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* What `field` declares under `PropertyInput`; `undefined` for a property
|
|
17
|
+
* that takes its value as it stores it, and for a field that is not a
|
|
18
|
+
* property.
|
|
19
|
+
*/
|
|
20
|
+
export declare function propertyInputOf(field: unknown): IPropertyInput | undefined;
|
|
@@ -9,8 +9,12 @@ export declare const RecordPlugins: unique symbol;
|
|
|
9
9
|
export declare const IdentifiedRecord: unique symbol;
|
|
10
10
|
/** Whether `version` is a semantic version. */
|
|
11
11
|
export declare function isVersion(version: unknown): version is string;
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Whether a plugin of version `available` satisfies a reference to
|
|
14
|
+
* `requested`: the same major version (the same minor below `1.0.0`), and
|
|
15
|
+
* not older.
|
|
16
|
+
*/
|
|
17
|
+
export declare function satisfies(available: string, requested: string): boolean;
|
|
14
18
|
/** The `onSet` and `onSetAll` a plugin runs, for changes to its `keys`. */
|
|
15
19
|
interface RecordPluginHook {
|
|
16
20
|
readonly plugin: AnyRecordPlugin;
|
|
@@ -51,7 +51,7 @@ export declare abstract class AbstractRepository<Entity extends AnyRepositoryEnt
|
|
|
51
51
|
* A repository of `entity` with the methods of `actions`: `findBy<Key>` for a
|
|
52
52
|
* unique key (`id`, `options.unique`, a `unique` property), giving the entity
|
|
53
53
|
* or `undefined`; `findManyBy<Key>` for any other key, giving a list;
|
|
54
|
-
* `create`, `update`, `delete`, `count`, and the queries `findMany`,
|
|
54
|
+
* `create`, `update`, `delete`, `restore` (soft deletion only), `count`, and the queries `findMany`,
|
|
55
55
|
* `exists`, `findPage` and `findCursor`. Every method is async and reads and writes through `provider`
|
|
56
56
|
* (a new `inMemoryProvider()` by default), which gets the variables of
|
|
57
57
|
* `environmentDependencies` as `context.env`. The repository can be a
|
|
@@ -37,6 +37,15 @@ export interface RepositoryInspection {
|
|
|
37
37
|
}
|
|
38
38
|
/** The declaration of `repository`; `undefined` when it is not one. */
|
|
39
39
|
export declare function inspectRepository(repository: unknown): RepositoryInspection | undefined;
|
|
40
|
+
/** Keeps `repository` as the one `repositoryOf` made for `state`. */
|
|
41
|
+
export declare function declareRepository(state: RepositoryState, repository: object): void;
|
|
42
|
+
/**
|
|
43
|
+
* The repository `repositoryOf` made for the entity class `Entity` that the
|
|
44
|
+
* entities of `repository` hold; `undefined` when it has no such relation.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export declare function relatedRepositoryOf(repository: object, Entity: object): object | undefined;
|
|
40
49
|
/**
|
|
41
50
|
* A repository sharing the declaration of `repository` that reads and writes
|
|
42
51
|
* through `provider`, unless it declared a provider of its own; a declared
|
|
@@ -42,6 +42,13 @@ export interface RepositoryUpdate<Entity> {
|
|
|
42
42
|
export interface RepositoryDelete<Entity> {
|
|
43
43
|
delete(entity: EntityInstanceOf<Entity> | string): Promise<EntityInstanceOf<Entity>>;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* `restore`: brings back a soft-deleted entity (or the one of the id) and
|
|
47
|
+
* gives it, alive; only for a repository with `deletion: "soft"`.
|
|
48
|
+
*/
|
|
49
|
+
export interface RepositoryRestore<Entity> {
|
|
50
|
+
restore(entity: EntityInstanceOf<Entity> | string): Promise<EntityInstanceOf<Entity>>;
|
|
51
|
+
}
|
|
45
52
|
/** `count`: how many entities it holds. */
|
|
46
53
|
export interface RepositoryCount<Entity = unknown> {
|
|
47
54
|
count(query?: RepositoryQuery<Entity>): Promise<number>;
|
|
@@ -67,9 +74,9 @@ export type RepositoryFindManyBy<Entity, Actions extends string = string> = {
|
|
|
67
74
|
* so that a class extending a repository can override them.
|
|
68
75
|
* @internal
|
|
69
76
|
*/
|
|
70
|
-
export type RepositoryMethods<Entity, Actions extends string = string> = RepositoryFindBy<Entity, Actions> & RepositoryFindManyBy<Entity, Actions> & ("create" extends Actions ? RepositoryCreate<Entity> : unknown) & ("update" extends Actions ? RepositoryUpdate<Entity> : unknown) & ("delete" extends Actions ? RepositoryDelete<Entity> : unknown) & ("count" extends Actions ? RepositoryCount<Entity> : unknown) & ("findMany" extends Actions ? RepositoryFindMany<Entity> : unknown) & ("exists" extends Actions ? RepositoryExists<Entity> : unknown) & ("findPage" extends Actions ? RepositoryFindPage<Entity> : unknown) & ("findCursor" extends Actions ? RepositoryFindCursor<Entity> : unknown);
|
|
77
|
+
export type RepositoryMethods<Entity, Actions extends string = string> = RepositoryFindBy<Entity, Actions> & RepositoryFindManyBy<Entity, Actions> & ("create" extends Actions ? RepositoryCreate<Entity> : unknown) & ("update" extends Actions ? RepositoryUpdate<Entity> : unknown) & ("delete" extends Actions ? RepositoryDelete<Entity> : unknown) & ("restore" extends Actions ? RepositoryRestore<Entity> : unknown) & ("count" extends Actions ? RepositoryCount<Entity> : unknown) & ("findMany" extends Actions ? RepositoryFindMany<Entity> : unknown) & ("exists" extends Actions ? RepositoryExists<Entity> : unknown) & ("findPage" extends Actions ? RepositoryFindPage<Entity> : unknown) & ("findCursor" extends Actions ? RepositoryFindCursor<Entity> : unknown);
|
|
71
78
|
/** A method a repository of `Entity` can have, e.g. `"findByEmail"`. */
|
|
72
|
-
export type RepositoryAction<Entity> = keyof RepositoryFindBy<Entity> | keyof RepositoryFindManyBy<Entity> | "create" | "update" | "delete" | "count" | "findMany" | "exists" | "findPage" | "findCursor";
|
|
79
|
+
export type RepositoryAction<Entity> = keyof RepositoryFindBy<Entity> | keyof RepositoryFindManyBy<Entity> | "create" | "update" | "delete" | "restore" | "count" | "findMany" | "exists" | "findPage" | "findCursor";
|
|
73
80
|
/**
|
|
74
81
|
* A repository of `Entity` with the methods of `Actions`, whose provider gets
|
|
75
82
|
* `Environment` as `context.env`, e.g.
|
|
@@ -8,3 +8,39 @@ export declare const InvalidUseCaseStep: import("../../index.js").DeclaredExcept
|
|
|
8
8
|
readonly reason: string;
|
|
9
9
|
readonly code?: string;
|
|
10
10
|
}>;
|
|
11
|
+
/**
|
|
12
|
+
* `useCasePluginOf` or `useCaseCatalogOf` was given what cannot be a plugin
|
|
13
|
+
* or a catalog: an invalid name or version, no `compile`, two plugins with
|
|
14
|
+
* one name; or a plugin was called with something that is not a repository
|
|
15
|
+
* or with options that are not a plain object. `source` is the plugin name.
|
|
16
|
+
*/
|
|
17
|
+
export declare const InvalidUseCasePlugin: import("../../index.js").DeclaredExceptionClass<{
|
|
18
|
+
readonly reason: string;
|
|
19
|
+
}>;
|
|
20
|
+
/** A catalog has no use case plugin of that name; `source` is `catalog`. */
|
|
21
|
+
export declare const UnknownUseCasePlugin: import("../../index.js").DeclaredExceptionClass<{
|
|
22
|
+
readonly name: string;
|
|
23
|
+
}>;
|
|
24
|
+
/** The use case plugin of a catalog does not satisfy the version asked for; `source` is the plugin name. */
|
|
25
|
+
export declare const IncompatibleUseCasePlugin: import("../../index.js").DeclaredExceptionClass<{
|
|
26
|
+
readonly reason: string;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* A use case plugin cannot make its use case for a repository: the
|
|
30
|
+
* repository lacks an action, the entity lacks a record plugin, or an option
|
|
31
|
+
* names what the entity does not have. `source` is the plugin name.
|
|
32
|
+
*/
|
|
33
|
+
export declare const UseCaseUnavailable: import("../../index.js").DeclaredExceptionClass<{
|
|
34
|
+
readonly repository: string;
|
|
35
|
+
readonly reason: string;
|
|
36
|
+
readonly missing?: {
|
|
37
|
+
readonly actions: readonly string[];
|
|
38
|
+
readonly plugins: readonly string[];
|
|
39
|
+
};
|
|
40
|
+
}>;
|
|
41
|
+
/** What a use case made by a plugin throws for an entity it did not find, unless given another error. */
|
|
42
|
+
export declare const UseCaseEntityNotFound: import("../../index.js").DeclaredExceptionClass<undefined>;
|
|
43
|
+
/** What a use case made by a plugin throws for an entity that already exists, unless given another error. */
|
|
44
|
+
export declare const UseCaseEntityConflict: import("../../index.js").DeclaredExceptionClass<undefined>;
|
|
45
|
+
/** What a use case made by a plugin throws for credentials that do not match, unless given another error. */
|
|
46
|
+
export declare const UseCaseInvalidCredentials: import("../../index.js").DeclaredExceptionClass<undefined>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AnyUseCasePlugin, IUseCaseCatalog, UseCasePluginsByName } from "./types/plugin.interface.js";
|
|
2
|
+
/** Whether `value` is a catalog made by `useCaseCatalogOf`. */
|
|
3
|
+
export declare function isUseCaseCatalog(value: unknown): value is IUseCaseCatalog;
|
|
4
|
+
/**
|
|
5
|
+
* A catalog of use case `plugins`, by name: what `commandBuilderOf` gives
|
|
6
|
+
* the `useCases` of a command to pick from, as
|
|
7
|
+
* `(ctx) => ({ create: ctx.create(Posts) })`; `catalog.context` is that
|
|
8
|
+
* `ctx`, for use outside a command. Throws `InvalidUseCasePlugin` for what
|
|
9
|
+
* is not a plugin and for two plugins with the same name.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useCaseCatalogOf<const Plugins extends readonly AnyUseCasePlugin[]>(...plugins: Plugins): IUseCaseCatalog<UseCasePluginsByName<Plugins>>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AnyUseCasePlugin, IUseCasePlugin, UseCaseMaker, UseCaseOriginInfo, UseCasePluginSettings } from "./types/plugin.interface.js";
|
|
2
|
+
/** Whether `value` is a plugin made by `useCasePluginOf`. */
|
|
3
|
+
export declare function isUseCasePlugin(value: unknown): value is AnyUseCasePlugin;
|
|
4
|
+
/**
|
|
5
|
+
* The plugin, the repository and the options a use case class was made
|
|
6
|
+
* from, by a plugin's `make`; `undefined` for a class declared by hand. A
|
|
7
|
+
* class extending one keeps its origin.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useCaseOriginOf(UseCase: unknown): UseCaseOriginInfo | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* A use case that is made for a repository. `settings.compile` gets what is
|
|
12
|
+
* known of the repository and its entity (`UseCaseTargetInfo`) and the
|
|
13
|
+
* options of the call, and gives the settings of `useCaseOf` with `steps`;
|
|
14
|
+
* `settings.requires` lists the actions and record plugins it cannot do
|
|
15
|
+
* without. The plugin's `make(repository, options)` gives the class, the
|
|
16
|
+
* same for the same repository and equal options.
|
|
17
|
+
*
|
|
18
|
+
* `Make` types that call: give it to say which repositories are accepted
|
|
19
|
+
* (`UseCaseTarget`) and what the use case takes and returns. Without it any
|
|
20
|
+
* repository is accepted and the class is `AnyUseCaseClass`.
|
|
21
|
+
*
|
|
22
|
+
* Throws `InvalidUseCasePlugin` for a name that is not an identifier, a
|
|
23
|
+
* version that is not semantic or a `compile` that is not a function.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useCasePluginOf<const Name extends string, Make extends UseCaseMaker = UseCaseMaker>(name: Name, settings: UseCasePluginSettings): IUseCasePlugin<Name, Make>;
|
|
@@ -10,6 +10,11 @@ export type StepReference = `$${string}`;
|
|
|
10
10
|
export type StepValue = StepReference | string | number | boolean | null | readonly StepValue[] | {
|
|
11
11
|
readonly [key: string]: StepValue;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* The arguments of a `find`, `invoke` or `run`: a list of values, or one
|
|
15
|
+
* reference to a list, which is spread (`args: "$input.tags"`).
|
|
16
|
+
*/
|
|
17
|
+
export type StepArgs = readonly StepValue[] | StepReference;
|
|
13
18
|
/** The actions of a repository that only read. */
|
|
14
19
|
export type StepReadAction = `findBy${string}` | `findManyBy${string}` | "findMany" | "exists" | "findPage" | "findCursor" | "count";
|
|
15
20
|
type RepositoryKeys<Dependencies> = {
|
|
@@ -40,7 +45,7 @@ export type StepEnsureKind = "exists" | "absent" | "equals" | "notEquals" | "tru
|
|
|
40
45
|
export type UseCaseStep<Dependencies extends UseCaseCollection = UseCaseCollection> = {
|
|
41
46
|
/** `"<repository>.<read action>"`. */
|
|
42
47
|
readonly find: StepFindTarget<Dependencies>;
|
|
43
|
-
readonly args?:
|
|
48
|
+
readonly args?: StepArgs;
|
|
44
49
|
readonly as?: string;
|
|
45
50
|
} | {
|
|
46
51
|
readonly ensure: "exists" | "absent" | "true" | "false";
|
|
@@ -64,12 +69,14 @@ export type UseCaseStep<Dependencies extends UseCaseCollection = UseCaseCollecti
|
|
|
64
69
|
readonly change: StepReference;
|
|
65
70
|
readonly key: string;
|
|
66
71
|
readonly value: StepValue;
|
|
72
|
+
/** Skips the step when `value` resolves to `undefined`: a partial update. */
|
|
73
|
+
readonly ifPresent?: boolean;
|
|
67
74
|
readonly as?: string;
|
|
68
75
|
} | {
|
|
69
76
|
/** A reference to a record or entity. */
|
|
70
77
|
readonly invoke: StepReference;
|
|
71
78
|
readonly method: string;
|
|
72
|
-
readonly args?:
|
|
79
|
+
readonly args?: StepArgs;
|
|
73
80
|
readonly as?: string;
|
|
74
81
|
} | {
|
|
75
82
|
readonly create: StepRepositoryWith<Dependencies, "create">;
|
|
@@ -83,6 +90,11 @@ export type UseCaseStep<Dependencies extends UseCaseCollection = UseCaseCollecti
|
|
|
83
90
|
readonly delete: StepRepositoryWith<Dependencies, "delete">;
|
|
84
91
|
readonly value: StepValue;
|
|
85
92
|
readonly as?: string;
|
|
93
|
+
} | {
|
|
94
|
+
/** A soft-deleted entity or its id; the repository gives it back alive. */
|
|
95
|
+
readonly restore: StepRepositoryWith<Dependencies, "restore">;
|
|
96
|
+
readonly value: StepValue;
|
|
97
|
+
readonly as?: string;
|
|
86
98
|
} | {
|
|
87
99
|
readonly call: UseCaseKeys<Dependencies>;
|
|
88
100
|
readonly input: StepValue;
|
|
@@ -90,7 +102,7 @@ export type UseCaseStep<Dependencies extends UseCaseCollection = UseCaseCollecti
|
|
|
90
102
|
} | {
|
|
91
103
|
readonly run: string;
|
|
92
104
|
readonly fn: (...args: any[]) => unknown;
|
|
93
|
-
readonly args?:
|
|
105
|
+
readonly args?: StepArgs;
|
|
94
106
|
readonly as?: string;
|
|
95
107
|
} | {
|
|
96
108
|
readonly return: StepValue;
|
|
@@ -102,7 +114,7 @@ export type StepErrorDescription = string | {
|
|
|
102
114
|
/** A `UseCaseStep` as `describe()` gives it: classes by name, functions as `opaque`. */
|
|
103
115
|
export type UseCaseStepDescription = {
|
|
104
116
|
readonly find: string;
|
|
105
|
-
readonly args?:
|
|
117
|
+
readonly args?: StepArgs;
|
|
106
118
|
readonly as?: string;
|
|
107
119
|
} | {
|
|
108
120
|
readonly ensure: StepEnsureKind;
|
|
@@ -118,11 +130,12 @@ export type UseCaseStepDescription = {
|
|
|
118
130
|
readonly change: string;
|
|
119
131
|
readonly key: string;
|
|
120
132
|
readonly value: StepValue;
|
|
133
|
+
readonly ifPresent?: boolean;
|
|
121
134
|
readonly as?: string;
|
|
122
135
|
} | {
|
|
123
136
|
readonly invoke: string;
|
|
124
137
|
readonly method: string;
|
|
125
|
-
readonly args?:
|
|
138
|
+
readonly args?: StepArgs;
|
|
126
139
|
readonly as?: string;
|
|
127
140
|
} | {
|
|
128
141
|
readonly create: string;
|
|
@@ -136,6 +149,10 @@ export type UseCaseStepDescription = {
|
|
|
136
149
|
readonly delete: string;
|
|
137
150
|
readonly value: StepValue;
|
|
138
151
|
readonly as?: string;
|
|
152
|
+
} | {
|
|
153
|
+
readonly restore: string;
|
|
154
|
+
readonly value: StepValue;
|
|
155
|
+
readonly as?: string;
|
|
139
156
|
} | {
|
|
140
157
|
readonly call: string;
|
|
141
158
|
readonly input: StepValue;
|
|
@@ -145,7 +162,7 @@ export type UseCaseStepDescription = {
|
|
|
145
162
|
readonly fn: {
|
|
146
163
|
readonly kind: "opaque";
|
|
147
164
|
};
|
|
148
|
-
readonly args?:
|
|
165
|
+
readonly args?: StepArgs;
|
|
149
166
|
readonly as?: string;
|
|
150
167
|
} | {
|
|
151
168
|
readonly return: StepValue;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AnyRepository } from "../repository/types/repository.interface.js";
|
|
2
|
+
import type { UseCaseTargetInfo } from "./types/plugin.interface.js";
|
|
3
|
+
/**
|
|
4
|
+
* What a use case plugin reads of `repository`: its entity, its actions and
|
|
5
|
+
* a plan of every key, to derive blueprints and steps from. `source` names
|
|
6
|
+
* who asks, in the `UseCaseUnavailable` it throws. Throws a `TypeError` when
|
|
7
|
+
* `repository` was not made by `repositoryOf`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useCaseTargetOf(repository: AnyRepository, source?: string): UseCaseTargetInfo;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { BlueprintField } from "../../blueprint/types/blueprint.interface.js";
|
|
2
|
+
import type { AnyRepository, AnyRepositoryEntity, EntityInstanceOf } from "../../repository/types/repository.interface.js";
|
|
3
|
+
import type { Kind } from "../../symbols/kind.js";
|
|
4
|
+
import type { AnyUseCaseClass, UseCaseStepsSettings } from "../use-case.js";
|
|
5
|
+
/** What a use case plugin needs of the repository it is made for. */
|
|
6
|
+
export interface UseCasePluginRequirements {
|
|
7
|
+
/** The actions the repository must declare, e.g. `["findById", "update"]`. */
|
|
8
|
+
readonly actions?: readonly string[];
|
|
9
|
+
/** The record plugins the entity must have, by name. */
|
|
10
|
+
readonly plugins?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
/** What a key of the entity is, for a use case that reads or writes it. */
|
|
13
|
+
export interface UseCaseFieldPlan {
|
|
14
|
+
readonly key: string;
|
|
15
|
+
readonly field: BlueprintField;
|
|
16
|
+
/** One of the identity keys: `id`, `createdAt`, `updatedAt`, `deletedAt`. */
|
|
17
|
+
readonly identity: boolean;
|
|
18
|
+
/** The record plugin that adds the key, by name. */
|
|
19
|
+
readonly plugin: string | undefined;
|
|
20
|
+
readonly sensitive: boolean;
|
|
21
|
+
readonly unique: boolean;
|
|
22
|
+
readonly optional: boolean;
|
|
23
|
+
/** Whether the entity has `change.<key>`. */
|
|
24
|
+
readonly change: boolean;
|
|
25
|
+
readonly kind: "property" | "blueprint" | "list" | "record" | "entity" | "entity-list";
|
|
26
|
+
/** The entity class the key holds, for `entity` and `entity-list`. */
|
|
27
|
+
readonly related: AnyRepositoryEntity | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* What an input takes for the key: `<key>Id` and the `id` field of the
|
|
30
|
+
* entity it holds, for an entity; the property a `PropertyInput`
|
|
31
|
+
* accepts, for a property that declares one; else the key and the field.
|
|
32
|
+
*/
|
|
33
|
+
readonly input: {
|
|
34
|
+
readonly key: string;
|
|
35
|
+
readonly field: BlueprintField;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Turns what the input gave into what the entity stores, for a property
|
|
39
|
+
* that declares a `PropertyInput` (a plain password into its hash); gives
|
|
40
|
+
* `undefined` for `undefined`.
|
|
41
|
+
*/
|
|
42
|
+
readonly convert: ((value: unknown) => unknown) | undefined;
|
|
43
|
+
}
|
|
44
|
+
/** What `compile` gets of the repository a use case is made for. */
|
|
45
|
+
export interface UseCaseTargetInfo {
|
|
46
|
+
readonly repository: AnyRepository;
|
|
47
|
+
/** The name `repositoryOf` gave it. */
|
|
48
|
+
readonly repositoryName: string;
|
|
49
|
+
readonly entity: AnyRepositoryEntity;
|
|
50
|
+
/** The declared name of the entity, else the name of its class. */
|
|
51
|
+
readonly entityName: string;
|
|
52
|
+
readonly actions: readonly string[];
|
|
53
|
+
readonly deletion: "hard" | "soft";
|
|
54
|
+
/** Every key of the entity, in the order of its blueprint. */
|
|
55
|
+
readonly fields: readonly UseCaseFieldPlan[];
|
|
56
|
+
readonly changeKeys: readonly string[];
|
|
57
|
+
readonly uniqueKeys: readonly string[];
|
|
58
|
+
/** The field plan of `key`; throws `UseCaseUnavailable` when the entity has no such key. */
|
|
59
|
+
field(key: string): UseCaseFieldPlan;
|
|
60
|
+
/**
|
|
61
|
+
* The repository of the entity the key holds, with the `actions` it must
|
|
62
|
+
* declare; throws `UseCaseUnavailable` when the repository has no such
|
|
63
|
+
* relation or the related one lacks an action.
|
|
64
|
+
*/
|
|
65
|
+
relation(key: string, actions?: readonly string[]): AnyRepository;
|
|
66
|
+
/** Throws `UseCaseUnavailable` for this target, with `reason`. */
|
|
67
|
+
unavailable(reason: string): never;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The declaration a plugin compiles: what `useCaseOf` takes, with `steps`.
|
|
71
|
+
* The steps are data here, checked by `useCaseOf` when the class is made.
|
|
72
|
+
*/
|
|
73
|
+
export interface UseCaseCompilation extends Omit<UseCaseStepsSettings<any, any, any, any>, "steps"> {
|
|
74
|
+
readonly steps: readonly Readonly<Record<string, unknown>>[];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The call signature of a plugin without one of its own: any repository,
|
|
78
|
+
* any options.
|
|
79
|
+
*/
|
|
80
|
+
export type UseCaseMaker = (repository: any, options?: any) => AnyUseCaseClass;
|
|
81
|
+
/** What `useCasePluginOf` takes, after the name. */
|
|
82
|
+
export interface UseCasePluginSettings {
|
|
83
|
+
/** A semantic version, e.g. `1.2.0`; `1.0.0` when left out. */
|
|
84
|
+
readonly version?: string;
|
|
85
|
+
/** What the use case does, for catalogs and tools. */
|
|
86
|
+
readonly description?: string;
|
|
87
|
+
readonly requires?: UseCasePluginRequirements;
|
|
88
|
+
/**
|
|
89
|
+
* The use case for `target` and `options`, as the settings of
|
|
90
|
+
* `useCaseOf` with `steps`. The options are the plain object the caller
|
|
91
|
+
* gave, `{}` when none.
|
|
92
|
+
*/
|
|
93
|
+
compile(target: UseCaseTargetInfo, options: Readonly<Record<string, unknown>>): UseCaseCompilation;
|
|
94
|
+
}
|
|
95
|
+
/** The plain, JSON form of a use case plugin, as `plugin.toJSON()` gives it. */
|
|
96
|
+
export interface UseCasePluginDescriptor {
|
|
97
|
+
readonly kind: "use-case-plugin";
|
|
98
|
+
readonly name: string;
|
|
99
|
+
readonly version: string;
|
|
100
|
+
readonly description?: string;
|
|
101
|
+
readonly requires: Required<UseCasePluginRequirements>;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A use case that is made for a repository: its blueprints, dependencies and
|
|
105
|
+
* steps follow the entity, the actions and the plugins. Made by
|
|
106
|
+
* `useCasePluginOf`.
|
|
107
|
+
*/
|
|
108
|
+
export interface IUseCasePlugin<Name extends string = string, Make extends UseCaseMaker = UseCaseMaker> {
|
|
109
|
+
readonly [Kind]: "use-case-plugin";
|
|
110
|
+
readonly name: Name;
|
|
111
|
+
readonly version: string;
|
|
112
|
+
readonly description?: string;
|
|
113
|
+
readonly requires: Required<UseCasePluginRequirements>;
|
|
114
|
+
/**
|
|
115
|
+
* The use case class for a repository and options: the same class for
|
|
116
|
+
* the same repository and equal options. Throws `UseCaseUnavailable`
|
|
117
|
+
* when the repository or its entity lacks what the plugin requires.
|
|
118
|
+
*/
|
|
119
|
+
readonly make: Make;
|
|
120
|
+
toJSON(): UseCasePluginDescriptor;
|
|
121
|
+
}
|
|
122
|
+
/** Any use case plugin. */
|
|
123
|
+
export type AnyUseCasePlugin = IUseCasePlugin<string, any>;
|
|
124
|
+
/** Where a use case class made by a plugin comes from, as `useCaseOriginOf` gives it. */
|
|
125
|
+
export interface UseCaseOriginInfo {
|
|
126
|
+
readonly plugin: AnyUseCasePlugin;
|
|
127
|
+
readonly repository: AnyRepository;
|
|
128
|
+
readonly options: Readonly<Record<string, unknown>>;
|
|
129
|
+
}
|
|
130
|
+
/** @internal */
|
|
131
|
+
export type UseCasePluginsByName<Plugins extends readonly AnyUseCasePlugin[]> = {
|
|
132
|
+
readonly [Plugin in Plugins[number] as Plugin["name"]]: Plugin;
|
|
133
|
+
};
|
|
134
|
+
/** @internal The `make` of each plugin, by name. */
|
|
135
|
+
export type UseCaseContextOf<Plugins> = {
|
|
136
|
+
readonly [Name in keyof Plugins]: Plugins[Name] extends {
|
|
137
|
+
readonly make: infer Make;
|
|
138
|
+
} ? Make : never;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Use case plugins by name: what `commandBuilderOf` gives `useCases` to pick
|
|
142
|
+
* from, as `(ctx) => ({ create: ctx.create(Posts) })`. Made by
|
|
143
|
+
* `useCaseCatalogOf`.
|
|
144
|
+
*/
|
|
145
|
+
export interface IUseCaseCatalog<Plugins extends Readonly<Record<string, AnyUseCasePlugin>> = Readonly<Record<string, AnyUseCasePlugin>>> {
|
|
146
|
+
readonly [Kind]: "use-case-catalog";
|
|
147
|
+
readonly plugins: Plugins;
|
|
148
|
+
readonly list: readonly AnyUseCasePlugin[];
|
|
149
|
+
/** The `make` of each plugin, by name. */
|
|
150
|
+
readonly context: UseCaseContextOf<Plugins>;
|
|
151
|
+
/** The plugin of a name, or of a name and a version (`create@1.0.0`). */
|
|
152
|
+
resolve(reference: string): AnyUseCasePlugin;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* @internal The context of a list of catalogs: the `make` of every plugin,
|
|
156
|
+
* a later catalog winning a name.
|
|
157
|
+
*/
|
|
158
|
+
export type UseCasePluginContext<Catalogs extends readonly IUseCaseCatalog[]> = Catalogs extends readonly [
|
|
159
|
+
infer Head extends IUseCaseCatalog,
|
|
160
|
+
...infer Tail extends readonly IUseCaseCatalog[]
|
|
161
|
+
] ? Omit<Head["context"], keyof UseCasePluginContext<Tail>> & UseCasePluginContext<Tail> : Record<never, never>;
|
|
162
|
+
type ActionsOf<Repository> = Repository extends {
|
|
163
|
+
readonly actions: readonly (infer Action)[];
|
|
164
|
+
} ? Action : never;
|
|
165
|
+
/** The entity class of a repository. */
|
|
166
|
+
export type EntityOfRepository<Repository> = Repository extends {
|
|
167
|
+
readonly entity: infer Entity;
|
|
168
|
+
} ? Entity : never;
|
|
169
|
+
/** What the type of an argument shows when the repository lacks an action. */
|
|
170
|
+
export interface MissingRepositoryAction<Action extends string> {
|
|
171
|
+
readonly "this use case needs the repository to declare the action": Action;
|
|
172
|
+
}
|
|
173
|
+
/** What the type of an argument shows when the entity lacks a record plugin. */
|
|
174
|
+
export interface MissingRecordPlugin<Plugin extends string> {
|
|
175
|
+
readonly "this use case needs the entity to have the plugin": Plugin;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The precondition of a use case plugin, in the type of its repository
|
|
179
|
+
* argument, written `repository: Repository & UseCaseTarget<Repository, ...>`:
|
|
180
|
+
* nothing when `Repository` declares every `Action` and its entities have
|
|
181
|
+
* the `Members` of the record plugin `Plugin`; else the type that names what
|
|
182
|
+
* is missing.
|
|
183
|
+
*/
|
|
184
|
+
export type UseCaseTarget<Repository, Action extends string, Members = unknown, Plugin extends string = never> = [Action] extends [ActionsOf<Repository>] ? EntityInstanceOf<EntityOfRepository<Repository>> extends Members ? unknown : MissingRecordPlugin<Plugin> : MissingRepositoryAction<Exclude<Action, ActionsOf<Repository>>>;
|
|
185
|
+
export {};
|
package/dist/experimental.js
CHANGED
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
isBlueprintList2,
|
|
8
8
|
AbstractRecord2,
|
|
9
9
|
declareRecord,
|
|
10
|
-
UndeclaredBlueprintProperty2,
|
|
11
10
|
optionalOf2,
|
|
11
|
+
UndeclaredBlueprintProperty2,
|
|
12
12
|
blueprintFromProperties
|
|
13
|
-
} from "./index-
|
|
13
|
+
} from "./index-r78dsvtc.js";
|
|
14
14
|
|
|
15
15
|
// src/core/blueprint/slice.ts
|
|
16
16
|
function propertiesOfField(field) {
|