@afora/core 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +157 -25
  2. package/dist/adapters.d.ts +25 -0
  3. package/dist/adapters.js +56 -0
  4. package/dist/core/application/exceptions.d.ts +17 -0
  5. package/dist/core/application/migrate.d.ts +19 -0
  6. package/dist/core/application/schema.d.ts +30 -0
  7. package/dist/core/application/start.d.ts +7 -0
  8. package/dist/core/application/types/application.interface.d.ts +73 -2
  9. package/dist/core/auth/jwt.d.ts +30 -0
  10. package/dist/core/auth/types/jwt.interface.d.ts +66 -0
  11. package/dist/core/blueprint/exceptions.d.ts +5 -0
  12. package/dist/core/blueprint/optional.d.ts +3 -1
  13. package/dist/core/blueprint/types/blueprint.interface.d.ts +79 -5
  14. package/dist/core/blueprint/variant.d.ts +32 -0
  15. package/dist/core/command/types/command.interface.d.ts +7 -0
  16. package/dist/core/entity/abstract-entity.d.ts +33 -1
  17. package/dist/core/entity/entity.d.ts +1 -1
  18. package/dist/core/entity/event.d.ts +13 -2
  19. package/dist/core/entity/exceptions.d.ts +17 -0
  20. package/dist/core/entity/trust.d.ts +24 -0
  21. package/dist/core/entity/types/entity.interface.d.ts +96 -2
  22. package/dist/core/entity/version.d.ts +13 -0
  23. package/dist/core/event-policy/event-policy.d.ts +25 -0
  24. package/dist/core/event-policy/exceptions.d.ts +11 -0
  25. package/dist/core/event-policy/publish.d.ts +17 -0
  26. package/dist/core/event-policy/publishers.d.ts +18 -0
  27. package/dist/core/event-policy/types/event-policy.interface.d.ts +72 -0
  28. package/dist/core/output/access.d.ts +16 -0
  29. package/dist/core/output/coerce.d.ts +9 -0
  30. package/dist/core/output/exceptions.d.ts +16 -0
  31. package/dist/core/output/http.d.ts +36 -0
  32. package/dist/core/output/openapi-sink.d.ts +160 -39
  33. package/dist/core/output/stage.d.ts +7 -2
  34. package/dist/core/output/status.d.ts +2 -2
  35. package/dist/core/output/types/http.interface.d.ts +130 -0
  36. package/dist/core/output/types/output.interface.d.ts +25 -5
  37. package/dist/core/parse/issue.d.ts +28 -3
  38. package/dist/core/parse/parse.d.ts +19 -0
  39. package/dist/core/parse/strip.d.ts +13 -0
  40. package/dist/core/property/abstract-property.d.ts +9 -0
  41. package/dist/core/property/define-property.d.ts +34 -6
  42. package/dist/core/property/types/property-options.interface.d.ts +10 -0
  43. package/dist/core/property/types/property-type.interface.d.ts +4 -0
  44. package/dist/core/record/abstract-record.d.ts +7 -0
  45. package/dist/core/record/types/record.interface.d.ts +12 -1
  46. package/dist/core/repository/exceptions.d.ts +43 -0
  47. package/dist/core/repository/in-memory.d.ts +16 -3
  48. package/dist/core/repository/migrate.d.ts +20 -0
  49. package/dist/core/repository/plan.d.ts +20 -0
  50. package/dist/core/repository/provider.d.ts +1 -1
  51. package/dist/core/repository/query.d.ts +9 -2
  52. package/dist/core/repository/repository.d.ts +40 -8
  53. package/dist/core/repository/state.d.ts +32 -2
  54. package/dist/core/repository/types/query.interface.d.ts +106 -5
  55. package/dist/core/repository/types/repository.interface.d.ts +139 -7
  56. package/dist/core/repository/types/schema.interface.d.ts +126 -0
  57. package/dist/core/repository/upcast.d.ts +64 -0
  58. package/dist/core/schema/schema.d.ts +28 -0
  59. package/dist/core/symbols/kind.d.ts +1 -1
  60. package/dist/core/use-case/access.d.ts +12 -0
  61. package/dist/core/use-case/decorators.d.ts +40 -0
  62. package/dist/core/use-case/route.d.ts +18 -0
  63. package/dist/core/use-case/steps/types/check.interface.d.ts +214 -0
  64. package/dist/core/use-case/types/access.interface.d.ts +39 -0
  65. package/dist/core/use-case/types/route.interface.d.ts +66 -0
  66. package/dist/core/use-case/use-case.d.ts +52 -2
  67. package/dist/experimental.d.ts +13 -1
  68. package/dist/experimental.js +64 -26
  69. package/dist/index-3aae5jfq.js +2997 -0
  70. package/dist/index-hg1z2s2v.js +1055 -0
  71. package/dist/index-ny3as79r.js +4567 -0
  72. package/dist/index-xegv6a1p.js +4291 -0
  73. package/dist/index-y280ew0k.js +396 -0
  74. package/dist/index.d.ts +55 -48
  75. package/dist/index.js +141 -169
  76. package/dist/std/blueprint-plugin.d.ts +6 -2
  77. package/dist/std/blueprint.d.ts +3 -0
  78. package/dist/std/entity.d.ts +2 -1
  79. package/dist/std/event-policies.d.ts +2 -0
  80. package/dist/std/properties/exceptions.d.ts +11 -0
  81. package/dist/std/properties/json.d.ts +54 -0
  82. package/dist/std/properties/password.d.ts +0 -2
  83. package/dist/std/properties/time.d.ts +4 -0
  84. package/dist/std/record-plugins.d.ts +2 -2
  85. package/dist/std/resource.d.ts +31 -0
  86. package/dist/std/runtime.d.ts +6 -0
  87. package/dist/std/use-cases/auth.d.ts +5 -1
  88. package/dist/std/use-cases/crud.d.ts +6 -4
  89. package/dist/std/use-cases/index.d.ts +1 -1
  90. package/dist/std/use-cases/shared.d.ts +34 -6
  91. package/dist/std/use-cases/types.d.ts +197 -22
  92. package/dist/testing/http-provider-contract.d.ts +32 -0
  93. package/dist/testing/index.d.ts +4 -0
  94. package/dist/testing/index.js +839 -3
  95. package/dist/testing/memory-publisher.d.ts +16 -0
  96. package/dist/testing/provider-contract.d.ts +31 -1
  97. package/package.json +17 -3
  98. package/dist/index-q6hggpzw.js +0 -6667
  99. package/dist/index-r78dsvtc.js +0 -1721
@@ -0,0 +1,31 @@
1
+ import type { AnyRepository } from "../core/repository/types/repository.interface.js";
2
+ import type { ResourceAction, ResourceOptions, ResourceUseCases, StdUseCaseContext } from "./use-cases/types.js";
3
+ /**
4
+ * The use cases of a CRUD resource over `repository`, for the `useCases` of
5
+ * `commandOf`: the standard `create`, `get`, `list`, `update` and `delete`,
6
+ * each with the route of a REST resource at `path`.
7
+ *
8
+ * ```ts
9
+ * const Blog = commandOf({
10
+ * name: "Blog",
11
+ * useCases: resourceOf(Posts, { path: "/posts" }),
12
+ * });
13
+ * // POST /posts (201), GET /posts, GET /posts/{id}, PATCH /posts/{id}, DELETE /posts/{id}
14
+ * ```
15
+ *
16
+ * It is only a shortcut for
17
+ * `(ctx) => ({ create: ctx.create(Posts, { route }), ... })`: the command it
18
+ * goes into is an ordinary one, and `describe()` shows the same standard use
19
+ * cases. `actions` picks some of the operations, and `access` says who may
20
+ * call them. A repository that lacks the action of an operation throws
21
+ * `UseCaseUnavailable` when the command is declared. For anything else an
22
+ * operation needs (`omit`, `owner`, hooks), write that one with `ctx` and
23
+ * spread the rest: `(ctx) => ({ ...resourceOf(Posts, options)(ctx), create: ctx.create(Posts, { ... }) })`.
24
+ */
25
+ export declare function resourceOf<Repository extends AnyRepository, const Actions extends readonly ResourceAction[] = readonly [
26
+ "create",
27
+ "get",
28
+ "list",
29
+ "update",
30
+ "delete"
31
+ ]>(repository: Repository, options: ResourceOptions<Actions>): (ctx: StdUseCaseContext) => Pick<ResourceUseCases<Repository>, Actions[number]>;
@@ -78,6 +78,12 @@ export declare function configure(services: RuntimeServices): void;
78
78
  export declare function resetRuntime(): void;
79
79
  /** The hasher in use; the default is chosen on first call, not at import. */
80
80
  export declare function currentHasher(): PasswordHasher;
81
+ /**
82
+ * Whether the generator in use is the default one, whose ids `Id` accepts as
83
+ * they are; one that `configure` put in its place is checked as any value.
84
+ * @internal
85
+ */
86
+ export declare function makesDefaultIds(): boolean;
81
87
  /** Makes an id with the generator in use. */
82
88
  export declare function generateId(): string;
83
89
  /** `verifySync` of the hasher in use, or throws `SyncVerifyUnsupported`. */
@@ -1,5 +1,9 @@
1
- import type { LoginMaker, RegisterMaker } from "./types.js";
1
+ import type { LoginMaker, LogoutMaker, RefreshMaker, RegisterMaker } from "./types.js";
2
2
  /** Stores an authenticatable entity, unless its email is taken. */
3
3
  export declare const register: import("../../index.js").IUseCasePlugin<"register", RegisterMaker>;
4
4
  /** Gives the authenticatable entity of an email, when the password matches. */
5
5
  export declare const login: import("../../index.js").IUseCasePlugin<"login", LoginMaker>;
6
+ /** Exchanges a refresh token for new tokens. */
7
+ export declare const refresh: import("../../index.js").IUseCasePlugin<"refresh", RefreshMaker>;
8
+ /** Gives empty tokens, for a route that keeps them in cookies to remove them. */
9
+ export declare const logout: import("../../index.js").IUseCasePlugin<"logout", LogoutMaker>;
@@ -3,11 +3,13 @@ import type { AnyExceptionClass } from "../../core/use-case/use-case.js";
3
3
  import { type Options, type Step } from "./shared.js";
4
4
  import type { CreateMaker, DeleteMaker, GetMaker, ListMaker, RestoreMaker, UpdateMaker } from "./types.js";
5
5
  /**
6
- * The declaration of `create` and of `register`, which adds `before` and
7
- * its errors: an input of the keys of the entity, the entity built from it
8
- * and stored.
6
+ * The declaration of `create` and of `register`, which adds `checks` and
7
+ * their errors: an input of the keys of the entity, the entity built from
8
+ * it and stored. The option `before` runs first, and the steps after it read
9
+ * the input it gives; `checks` read it under `$input` all the same, rooted
10
+ * by the function given.
9
11
  */
10
- export declare function createCompilation(target: UseCaseTargetInfo, options: Options, verb: string, before?: readonly Step[], errors?: readonly AnyExceptionClass[]): UseCaseCompilation;
12
+ export declare function createCompilation(target: UseCaseTargetInfo, options: Options, verb: string, checks?: (input: string) => readonly Step[], errors?: readonly AnyExceptionClass[]): UseCaseCompilation;
11
13
  /** Builds an entity from the input and stores it. */
12
14
  export declare const create: import("../../index.js").IUseCasePlugin<"create", CreateMaker>;
13
15
  /** Gives the entity of an id. */
@@ -6,6 +6,6 @@ import type { StdUseCasePlugins } from "./types.js";
6
6
  * `list`, `update`, `delete` and `restore` for any entity; `publish`,
7
7
  * `unpublish`, `archive`, `unarchive`, `tag` and `untag` for the entities
8
8
  * with those record plugins; `register` and `login` for the
9
- * authenticatable ones. `StdUseCases.context` is that `ctx`.
9
+ * authenticatable ones, and `refresh` and `logout` for the tokens of `login`. `StdUseCases.context` is that `ctx`.
10
10
  */
11
11
  export declare const StdUseCases: IUseCaseCatalog<StdUseCasePlugins>;
@@ -1,6 +1,6 @@
1
1
  import type { BlueprintField } from "../../core/blueprint/types/blueprint.interface.js";
2
2
  import type { UseCaseCompilation, UseCaseFieldPlan, UseCaseTargetInfo } from "../../core/use-case/types/plugin.interface.js";
3
- import type { AnyExceptionClass } from "../../core/use-case/use-case.js";
3
+ import type { AnyExceptionClass, UseCaseDependency } from "../../core/use-case/use-case.js";
4
4
  /** A blueprint of the fields given, with no context of properties. */
5
5
  export declare const fields: import("../../index.js").BlueprintBuilder<readonly []>;
6
6
  export type Options = Readonly<Record<string, unknown>>;
@@ -10,8 +10,10 @@ export declare const capitalize: (key: string) => string;
10
10
  export declare function literal(value: unknown): unknown;
11
11
  /** The option `key`, checked to be one of the `allowed` options of the use case. */
12
12
  export declare function checkOptions(target: UseCaseTargetInfo, options: Options, allowed: readonly string[]): void;
13
- /** The name, description, tags and `deprecated` of the use case. */
14
- export declare function metaOf(target: UseCaseTargetInfo, options: Options, verb: string): Pick<UseCaseCompilation, "name" | "description" | "tags" | "deprecated">;
13
+ /** The options of the use cases that write: `before`, `after`, `dependencies`, `throws`. */
14
+ export declare const HOOKS: readonly ["before", "after", "dependencies", "throws"];
15
+ /** The name, description, tags, `deprecated`, route and access of the use case. */
16
+ export declare function metaOf(target: UseCaseTargetInfo, options: Options, verb: string): Pick<UseCaseCompilation, "name" | "description" | "tags" | "deprecated" | "route" | "access">;
15
17
  /** The key of the response that holds the entity: the option `as`, `item` by default. */
16
18
  export declare function asOf(target: UseCaseTargetInfo, options: Options): string;
17
19
  /** The error of `kind` the options give, else `fallback`. */
@@ -43,18 +45,44 @@ export declare function contextOf(owner: Owner | undefined): Pick<UseCaseCompila
43
45
  * when there is none, and when it is not the caller's.
44
46
  */
45
47
  export declare function loadSteps(owner: Owner | undefined, notFound: AnyExceptionClass): Step[];
48
+ /** What the options `before`, `after`, `dependencies` and `throws` add to a use case. */
49
+ export interface Hooks {
50
+ /** The root of the input the steps read: `$input`, or `$prepared` after `before`. */
51
+ readonly input: string;
52
+ /** The dependencies the options give, joined to those the use case makes. */
53
+ readonly dependencies: Readonly<Record<string, UseCaseDependency>>;
54
+ readonly throws: readonly AnyExceptionClass[];
55
+ /** The step of `before`, if any: it binds `prepared`, the input it gives back or the one it got. */
56
+ before(entity?: string): Step[];
57
+ /** The step of `after`, if any, over the entity written. */
58
+ after(entity: string): Step[];
59
+ }
60
+ /**
61
+ * The options `before`, `after`, `dependencies` and `throws`, checked: each
62
+ * hook becomes a `run` step, whose function gets `{ input, entity?,
63
+ * dependencies, context }` (the bound dependencies of the call, overrides
64
+ * included).
65
+ */
66
+ export declare function hooksOf(target: UseCaseTargetInfo, options: Options): Hooks;
67
+ /**
68
+ * The dependencies a use case makes, joined to those of the option
69
+ * `dependencies`; a name taken by both is unavailable.
70
+ */
71
+ export declare function joinDependencies(target: UseCaseTargetInfo, made: Readonly<Record<string, UseCaseDependency>>, hooks: Hooks): Readonly<Record<string, UseCaseDependency>>;
46
72
  /** The id field of the entity of `target`. */
47
73
  export declare const idOf: (target: UseCaseTargetInfo) => BlueprintField;
48
74
  /** The classes of `errors`, each once. */
49
75
  export declare const distinct: (...errors: readonly AnyExceptionClass[]) => AnyExceptionClass[];
50
76
  /**
51
77
  * A use case over one entity found by id: `{ id }` in, `{ [as]: entity }`
52
- * out; loads it, checks the owner, runs `steps` (which leave the entity to
53
- * give under `result`) and gives it.
78
+ * out; loads it, checks the owner, runs `before` (with `hooks`), `steps`
79
+ * (which leave the entity to give under `result`) and `after`, and gives it.
80
+ * `steps` read the input under `input`: `$input`, or what `before` gave.
54
81
  */
55
82
  export declare function entityUseCase(target: UseCaseTargetInfo, options: Options, verb: string, allowed: readonly string[], steps: (context: {
56
83
  readonly owner: Owner | undefined;
84
+ readonly input: string;
57
85
  }) => {
58
86
  readonly steps: readonly Step[];
59
87
  readonly result: string;
60
- }): UseCaseCompilation;
88
+ }, hooks?: boolean): UseCaseCompilation;
@@ -1,10 +1,14 @@
1
- import type { AnyBlueprint, AnyBlueprintList, AnyRecordClass, BlueprintField, IBlueprint, IBlueprintCollection, IBlueprintList, OptionalBlueprint, OptionalBlueprintList, OptionalRecord } from "../../core/blueprint/types/blueprint.interface.js";
1
+ import type { DescribedJwtOptions } from "../../core/auth/types/jwt.interface.js";
2
+ import type { AnyBlueprint, AnyBlueprintList, AnyRecordClass, BlueprintField, BlueprintValue, IBlueprint, IBlueprintCollection, IBlueprintList, OptionalBlueprint, OptionalBlueprintList, OptionalRecord } from "../../core/blueprint/types/blueprint.interface.js";
2
3
  import type { EntityIdentityKey } from "../../core/entity/types/entity.interface.js";
4
+ import type { EntityVersion } from "../../core/entity/version.js";
3
5
  import type { OptionalProperty } from "../../core/property/types/optional-property.type.js";
4
6
  import type { AnyPropertyConstructor } from "../../core/property/types/property-collection.type.js";
5
7
  import type { AnyRepository, AnyRepositoryEntity, EntityInstanceOf } from "../../core/repository/types/repository.interface.js";
8
+ import type { AccessDeclaration } from "../../core/use-case/types/access.interface.js";
6
9
  import type { IUseCasePlugin, UseCaseTarget } from "../../core/use-case/types/plugin.interface.js";
7
- import type { AnyExceptionClass, StepsUseCaseConstructor } from "../../core/use-case/use-case.js";
10
+ import type { RouteSettings } from "../../core/use-case/types/route.interface.js";
11
+ import type { AnyExceptionClass, DependencyFunctions, StepsUseCaseConstructor, UseCaseCollection } from "../../core/use-case/use-case.js";
8
12
  import type { Integer, StringProperty } from "../properties/primitives.js";
9
13
  type EntityOf<Repository> = Repository extends {
10
14
  readonly entity: infer Entity extends AnyRepositoryEntity;
@@ -43,7 +47,41 @@ export interface StdUseCaseOptions {
43
47
  readonly tags?: readonly string[];
44
48
  readonly deprecated?: boolean;
45
49
  readonly errors?: StdUseCaseErrors;
50
+ /** Where the use case is served over HTTP: the `route` of `useCaseOf`. */
51
+ readonly route?: RouteSettings;
52
+ /** Who may call it: the `access` of `useCaseOf`. */
53
+ readonly access?: AccessDeclaration;
46
54
  }
55
+ /**
56
+ * The tokens `login` and `refresh` issue: the options of `jwtOf`, plus what
57
+ * a token says of the entity. JSON, so it lives in an `afora.json`: the
58
+ * secret is named by its environment variable (`{ env: "JWT_SECRET" }`).
59
+ * Experimental: it may change in any release.
60
+ */
61
+ export interface StdTokenOptions extends DescribedJwtOptions {
62
+ /** Also issues a refresh token, which `refresh` exchanges; `expiresIn` is its life in seconds. */
63
+ readonly refresh?: {
64
+ readonly expiresIn: number;
65
+ };
66
+ /** Scopes every token gets. */
67
+ readonly scopes?: readonly string[];
68
+ /** A key of the entity that holds a list of scopes, added to `scopes`. */
69
+ readonly scopesFrom?: string;
70
+ /** Claims of the token, each from a key of the entity: `{ tenant: "tenantId" }`. */
71
+ readonly claims?: {
72
+ readonly [claim: string]: string;
73
+ };
74
+ }
75
+ /** The response keys the `token` option adds. */
76
+ type TokenResponse<Options> = Options extends {
77
+ readonly token: infer Token;
78
+ } ? {
79
+ readonly token: typeof StringProperty;
80
+ } & (Token extends {
81
+ readonly refresh: object;
82
+ } ? {
83
+ readonly refreshToken: typeof StringProperty;
84
+ } : unknown) : unknown;
47
85
  /** Who owns the entities: the caller, whose id the context gives. */
48
86
  export interface StdOwnerOption<Repository> {
49
87
  /**
@@ -88,6 +126,79 @@ export interface StdListOptions<Repository> extends StdUseCaseOptions, StdOwnerO
88
126
  }[];
89
127
  }
90
128
  type None = Record<never, never>;
129
+ /** What `before` and `after` get of the call. */
130
+ export interface StdHookArgs<Input, Dependencies, Context> {
131
+ /** The input of the call, parsed; for `after`, the one `before` gave back. */
132
+ readonly input: Input;
133
+ /**
134
+ * The dependencies of the call, as `execute` resolved them (overrides
135
+ * included): `repository`, those the use case makes to load what the
136
+ * input names, and those of the option `dependencies`.
137
+ */
138
+ readonly dependencies: Dependencies;
139
+ /** The context of the call: the id of the caller with `owner`, else `undefined`. */
140
+ readonly context: Context;
141
+ }
142
+ /**
143
+ * The options of a standard use case that writes: `before` runs before the
144
+ * write and may give back the input to write in place of the one given;
145
+ * `after` runs after it, within the transaction of the command; both get the
146
+ * dependencies of the use case and those of `dependencies`, and `throws` adds
147
+ * the exceptions they may throw to its errors (for `describe()` and statuses).
148
+ */
149
+ export interface StdHookOptions<Input, Entity, Dependencies extends UseCaseCollection, Bound, Context, Loaded extends boolean> {
150
+ /** Repositories, use cases or environments the hooks use, by name, beside those the use case makes. */
151
+ readonly dependencies?: Dependencies;
152
+ /**
153
+ * Runs before the write (after the entity is loaded and its owner
154
+ * checked, for a use case over one entity, which gets it as `entity`);
155
+ * what it gives back, if anything, is the input the use case goes on with.
156
+ */
157
+ readonly before?: (args: StdHookArgs<Input, Bound, Context> & (Loaded extends true ? {
158
+ readonly entity: Entity;
159
+ } : unknown)) => Input | void | Promise<Input | void>;
160
+ /** Runs after the write, with the entity written, before the response is given. */
161
+ readonly after?: (args: StdHookArgs<Input, Bound, Context> & {
162
+ readonly entity: Entity;
163
+ }) => unknown;
164
+ /** Exceptions (made by `exceptionOf`) `before` or `after` may throw: added to the errors of the use case. */
165
+ readonly throws?: readonly AnyExceptionClass[];
166
+ }
167
+ /** What `get`, `list` and `login` take in place of the hooks: none. */
168
+ export interface StdNoHookOptions {
169
+ readonly before?: never;
170
+ readonly after?: never;
171
+ readonly dependencies?: never;
172
+ readonly throws?: never;
173
+ }
174
+ type ContextValue<Repository, Options> = OwnerContext<Repository, Options> extends infer Context extends IBlueprintCollection ? Readonly<BlueprintValue<Context>> : undefined;
175
+ /** `Options` as `Hookable` infers them. */
176
+ type Shape<Omitted, Values, Owner> = {
177
+ readonly omit: readonly Omitted[];
178
+ readonly values: Values;
179
+ } & ([Owner] extends [undefined] ? unknown : {
180
+ readonly owner: Owner;
181
+ });
182
+ /** `StdHookOptions` for a use case over `Repository` that takes `Input`. */
183
+ type HookOptionsOf<Repository extends AnyRepository, Options, Input extends IBlueprintCollection, Dependencies extends UseCaseCollection, Loaded extends boolean> = StdHookOptions<BlueprintValue<Input>, EntityInstanceOf<EntityOf<Repository>>, Dependencies, DependencyFunctions<{
184
+ readonly repository: Repository;
185
+ } & Dependencies>, ContextValue<Repository, Options>, Loaded>;
186
+ /**
187
+ * The options of a use case that writes, as its maker reads them: those that
188
+ * type its input and context, inferred one by one (`Options` is inferred from
189
+ * the whole object only once the hooks are typed), and the hooks, typed by
190
+ * them and not inferred from. The types of `before` and `after` are only
191
+ * worked out for a call that gives them.
192
+ */
193
+ interface Hookable<Repository extends AnyRepository, Input extends IBlueprintCollection, Omitted, Values, Owner, Dependencies extends UseCaseCollection, Loaded extends boolean> {
194
+ readonly omit?: readonly Omitted[];
195
+ readonly values?: Values;
196
+ readonly owner?: Owner;
197
+ readonly dependencies?: Dependencies;
198
+ readonly throws?: readonly AnyExceptionClass[];
199
+ readonly before?: NoInfer<HookOptionsOf<Repository, Shape<Omitted, Values, Owner>, Input, Dependencies, Loaded>["before"]>;
200
+ readonly after?: NoInfer<HookOptionsOf<Repository, Shape<Omitted, Values, Owner>, Input, Dependencies, Loaded>["after"]>;
201
+ }
91
202
  type AsOf<Options> = Options extends {
92
203
  readonly as: infer Key extends string;
93
204
  } ? Key : "item";
@@ -111,9 +222,13 @@ type LeftOut<Options> = EntityIdentityKey | OwnerKey<Options> | (Options extends
111
222
  } ? Key : never) | (Options extends {
112
223
  readonly values: infer Values;
113
224
  } ? keyof Values : never);
225
+ /** `"version"` for a versioned entity, whose `version` is part of its identity. */
226
+ type VersionKey<Repository> = Props<Repository> extends {
227
+ readonly version: typeof EntityVersion;
228
+ } ? "version" : never;
114
229
  /** The input of `create`: the keys of the entity, an entity it holds by `<key>Id`. */
115
230
  type CreateInput<Repository, Options> = {
116
- readonly [Key in KeyOf<Repository> as Key extends LeftOut<Options> ? never : Props<Repository>[Key] extends AnyRepositoryEntity ? `${Key}Id` : HoldsEntity<Props<Repository>[Key]> extends true ? never : Key]: Props<Repository>[Key] extends AnyRepositoryEntity ? FieldAt<PropertiesOf<Props<Repository>[Key]>, "id"> : Props<Repository>[Key];
231
+ readonly [Key in KeyOf<Repository> as Key extends LeftOut<Options> | VersionKey<Repository> ? never : Props<Repository>[Key] extends AnyRepositoryEntity ? `${Key}Id` : HoldsEntity<Props<Repository>[Key]> extends true ? never : Key]: Props<Repository>[Key] extends AnyRepositoryEntity ? FieldAt<PropertiesOf<Props<Repository>[Key]>, "id"> : Props<Repository>[Key];
117
232
  };
118
233
  type ChangeKey<Repository> = EntityInstanceOf<EntityOf<Repository>> extends {
119
234
  readonly change: infer Changes;
@@ -122,18 +237,18 @@ type ChangeKey<Repository> = EntityInstanceOf<EntityOf<Repository>> extends {
122
237
  type UpdateInput<Repository> = {
123
238
  readonly [Key in "id" | ChangeKey<Repository> as Key extends "id" ? Key : HoldsEntity<Props<Repository>[Key]> extends true ? never : Key]: Key extends "id" ? IdField<Repository> : Optional<Props<Repository>[Key]>;
124
239
  };
125
- type EntityUseCase<Repository extends AnyRepository, Options, Input extends IBlueprintCollection> = StepsUseCaseConstructor<Input, EntityResponse<Repository, Options>, {
240
+ type EntityUseCase<Repository extends AnyRepository, Options, Input extends IBlueprintCollection, Dependencies extends UseCaseCollection = None> = StepsUseCaseConstructor<Input, EntityResponse<Repository, Options>, {
126
241
  readonly repository: Repository;
127
- }, OwnerContext<Repository, Options>>;
242
+ } & Dependencies, OwnerContext<Repository, Options>>;
128
243
  type ById<Repository> = {
129
244
  readonly id: IdField<Repository>;
130
245
  };
131
246
  /** `create`: for a repository with the `create` action. */
132
- export type CreateMaker = <Repository extends AnyRepository, const Options extends StdCreateOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "create">, options?: Options) => EntityUseCase<Repository, Options, CreateInput<Repository, Options>>;
247
+ export type CreateMaker = <Repository extends AnyRepository, const Options extends StdCreateOptions<Repository> = None, const Omitted extends KeyOf<Repository> = never, const Values = None, const Owner = undefined, const Dependencies extends UseCaseCollection = None>(repository: Repository & UseCaseTarget<Repository, "create">, options?: Options & Hookable<Repository, CreateInput<Repository, Shape<Omitted, Values, Owner>>, Omitted, Values, Owner, Dependencies, false>) => EntityUseCase<Repository, Options, CreateInput<Repository, Options>, Dependencies>;
133
248
  /** `get`: for a repository with `findById`. */
134
- export type GetMaker = <Repository extends AnyRepository, const Options extends StdEntityOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findById">, options?: Options) => EntityUseCase<Repository, Options, ById<Repository>>;
249
+ export type GetMaker = <Repository extends AnyRepository, const Options extends StdEntityOptions<Repository> & StdNoHookOptions = None>(repository: Repository & UseCaseTarget<Repository, "findById">, options?: Options) => EntityUseCase<Repository, Options, ById<Repository>>;
135
250
  /** `list`: for a repository with `findPage`. */
136
- export type ListMaker = <Repository extends AnyRepository, const Options extends StdListOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findPage">, options?: Options) => StepsUseCaseConstructor<{
251
+ export type ListMaker = <Repository extends AnyRepository, const Options extends StdListOptions<Repository> & StdNoHookOptions = None>(repository: Repository & UseCaseTarget<Repository, "findPage">, options?: Options) => StepsUseCaseConstructor<{
137
252
  readonly limit: OptionalProperty<typeof Integer>;
138
253
  readonly offset: OptionalProperty<typeof Integer>;
139
254
  }, {
@@ -145,46 +260,69 @@ export type ListMaker = <Repository extends AnyRepository, const Options extends
145
260
  readonly repository: Repository;
146
261
  }, OwnerContext<Repository, Options>>;
147
262
  /** `update`: for a repository with `findById` and `update`. */
148
- export type UpdateMaker = <Repository extends AnyRepository, const Options extends StdEntityOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findById" | "update">, options?: Options) => EntityUseCase<Repository, Options, UpdateInput<Repository>>;
263
+ export type UpdateMaker = <Repository extends AnyRepository, const Options extends StdEntityOptions<Repository> = None, const Owner = undefined, const Dependencies extends UseCaseCollection = None>(repository: Repository & UseCaseTarget<Repository, "findById" | "update">, options?: Options & Hookable<Repository, UpdateInput<Repository>, never, None, Owner, Dependencies, true>) => EntityUseCase<Repository, Options, UpdateInput<Repository>, Dependencies>;
149
264
  /** `delete`: for a repository with `findById` and `delete`. */
150
- export type DeleteMaker = <Repository extends AnyRepository, const Options extends StdEntityOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findById" | "delete">, options?: Options) => EntityUseCase<Repository, Options, ById<Repository>>;
265
+ export type DeleteMaker = <Repository extends AnyRepository, const Options extends StdEntityOptions<Repository> = None, const Owner = undefined, const Dependencies extends UseCaseCollection = None>(repository: Repository & UseCaseTarget<Repository, "findById" | "delete">, options?: Options & Hookable<Repository, ById<Repository>, never, None, Owner, Dependencies, true>) => EntityUseCase<Repository, Options, ById<Repository>, Dependencies>;
151
266
  /** `restore`: for a repository with soft deletion and the `restore` action. */
152
267
  export type RestoreMaker = <Repository extends AnyRepository, const Options extends StdUseCaseOptions & {
153
268
  readonly as?: string;
154
- } = None>(repository: Repository & UseCaseTarget<Repository, "restore">, options?: Options) => StepsUseCaseConstructor<ById<Repository>, EntityResponse<Repository, Options>, {
269
+ } = None, const Dependencies extends UseCaseCollection = None>(repository: Repository & UseCaseTarget<Repository, "restore">, options?: Options & Hookable<Repository, ById<Repository>, never, None, undefined, Dependencies, false>) => StepsUseCaseConstructor<ById<Repository>, EntityResponse<Repository, Options>, {
155
270
  readonly repository: Repository;
156
- }>;
271
+ } & Dependencies>;
157
272
  /** `publish` and `unpublish`: for a publishable entity. */
158
- export type PublishMaker = <Repository extends AnyRepository, const Options extends StdLifecycleOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findById" | "update", {
273
+ export type PublishMaker = <Repository extends AnyRepository, const Options extends StdLifecycleOptions<Repository> = None, const Owner = undefined, const Dependencies extends UseCaseCollection = None>(repository: Repository & UseCaseTarget<Repository, "findById" | "update", {
159
274
  publish(at?: string): unknown;
160
275
  unpublish(): unknown;
161
- }, "publishable">, options?: Options) => EntityUseCase<Repository, Options, ById<Repository>>;
276
+ }, "publishable">, options?: Options & Hookable<Repository, ById<Repository>, never, None, Owner, Dependencies, true>) => EntityUseCase<Repository, Options, ById<Repository>, Dependencies>;
162
277
  /** `archive` and `unarchive`: for an archivable entity. */
163
- export type ArchiveMaker = <Repository extends AnyRepository, const Options extends StdLifecycleOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findById" | "update", {
278
+ export type ArchiveMaker = <Repository extends AnyRepository, const Options extends StdLifecycleOptions<Repository> = None, const Owner = undefined, const Dependencies extends UseCaseCollection = None>(repository: Repository & UseCaseTarget<Repository, "findById" | "update", {
164
279
  archive(at?: string): unknown;
165
280
  readonly isArchived: boolean;
166
- }, "archivable">, options?: Options) => EntityUseCase<Repository, Options, ById<Repository>>;
281
+ }, "archivable">, options?: Options & Hookable<Repository, ById<Repository>, never, None, Owner, Dependencies, true>) => EntityUseCase<Repository, Options, ById<Repository>, Dependencies>;
167
282
  /** `tag` and `untag`: for a taggable entity. */
168
- export type TagMaker = <Repository extends AnyRepository, const Options extends StdEntityOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findById" | "update", {
283
+ export type TagMaker = <Repository extends AnyRepository, const Options extends StdEntityOptions<Repository> = None, const Owner = undefined, const Dependencies extends UseCaseCollection = None>(repository: Repository & UseCaseTarget<Repository, "findById" | "update", {
169
284
  tag(...tags: string[]): unknown;
170
285
  untag(...tags: string[]): unknown;
171
- }, "taggable">, options?: Options) => EntityUseCase<Repository, Options, ById<Repository> & {
286
+ }, "taggable">, options?: Options & Hookable<Repository, TagInput<Repository>, never, None, Owner, Dependencies, true>) => EntityUseCase<Repository, Options, TagInput<Repository>, Dependencies>;
287
+ type TagInput<Repository> = ById<Repository> & {
172
288
  readonly tags: IBlueprintList<typeof StringProperty>;
173
- }>;
289
+ };
174
290
  type Authenticatable = {
175
291
  verifyPassword(plain: string): Promise<boolean>;
176
292
  };
177
293
  /** `register`: for an authenticatable entity, with `findByEmail` and `create`. */
178
- export type RegisterMaker = <Repository extends AnyRepository, const Options extends StdCreateOptions<Repository> = None>(repository: Repository & UseCaseTarget<Repository, "findByEmail" | "create", Authenticatable, "authenticatable">, options?: Options) => EntityUseCase<Repository, Options, CreateInput<Repository, Options>>;
294
+ export type RegisterMaker = <Repository extends AnyRepository, const Options extends StdCreateOptions<Repository> = None, const Omitted extends KeyOf<Repository> = never, const Values = None, const Owner = undefined, const Dependencies extends UseCaseCollection = None>(repository: Repository & UseCaseTarget<Repository, "findByEmail" | "create", Authenticatable, "authenticatable">, options?: Options & Hookable<Repository, CreateInput<Repository, Shape<Omitted, Values, Owner>>, Omitted, Values, Owner, Dependencies, false>) => EntityUseCase<Repository, Options, CreateInput<Repository, Options>, Dependencies>;
179
295
  /** `login`: for an authenticatable entity, with `findByEmail`. */
180
296
  export type LoginMaker = <Repository extends AnyRepository, const Options extends StdUseCaseOptions & {
181
297
  readonly as?: string;
182
- } = None>(repository: Repository & UseCaseTarget<Repository, "findByEmail", Authenticatable, "authenticatable">, options?: Options) => StepsUseCaseConstructor<{
298
+ /** Issues a token for the entity (and a refresh token, with `refresh`), in the response. */
299
+ readonly token?: StdTokenOptions;
300
+ } & StdNoHookOptions = None>(repository: Repository & UseCaseTarget<Repository, "findByEmail", Authenticatable, "authenticatable">, options?: Options) => StepsUseCaseConstructor<{
183
301
  readonly email: FieldAt<Props<Repository>, "email">;
184
302
  readonly password: typeof StringProperty;
185
- }, EntityResponse<Repository, Options>, {
303
+ }, EntityResponse<Repository, Options> & TokenResponse<Options>, {
186
304
  readonly repository: Repository;
187
305
  }>;
306
+ /** `refresh`: exchanges a refresh token of `login` for new tokens; needs `findById`. */
307
+ export type RefreshMaker = <Repository extends AnyRepository>(repository: Repository & UseCaseTarget<Repository, "findById">, options: StdUseCaseOptions & {
308
+ readonly token: StdTokenOptions & {
309
+ readonly refresh: {
310
+ readonly expiresIn: number;
311
+ };
312
+ };
313
+ } & StdNoHookOptions) => StepsUseCaseConstructor<{
314
+ readonly refreshToken: typeof StringProperty;
315
+ }, {
316
+ readonly token: typeof StringProperty;
317
+ readonly refreshToken: typeof StringProperty;
318
+ }, {
319
+ readonly repository: Repository;
320
+ }>;
321
+ /** `logout`: gives empty tokens, which a route with `setCookie` turns into removed cookies. */
322
+ export type LogoutMaker = <Repository extends AnyRepository>(repository: Repository, options?: StdUseCaseOptions & StdNoHookOptions) => StepsUseCaseConstructor<Record<never, never>, {
323
+ readonly token: typeof StringProperty;
324
+ readonly refreshToken: typeof StringProperty;
325
+ }, Record<never, never>>;
188
326
  /**
189
327
  * The standard use cases, by name: what the `useCases` of `commandOf` gets
190
328
  * to pick from, each called with a repository and its options.
@@ -204,9 +342,46 @@ export interface StdUseCaseContext {
204
342
  readonly untag: TagMaker;
205
343
  readonly register: RegisterMaker;
206
344
  readonly login: LoginMaker;
345
+ readonly refresh: RefreshMaker;
346
+ readonly logout: LogoutMaker;
207
347
  }
208
348
  /** The plugins of `StdUseCases`, by name. */
209
349
  export type StdUseCasePlugins = {
210
350
  readonly [Name in keyof StdUseCaseContext]: IUseCasePlugin<Name, StdUseCaseContext[Name]>;
211
351
  };
352
+ /** The operations `resourceOf` can give. */
353
+ export type ResourceAction = "create" | "get" | "list" | "update" | "delete";
354
+ /** The use cases of `resourceOf`, by operation: the standard ones, with no option but their route and access. */
355
+ export interface ResourceUseCases<Repository extends AnyRepository> {
356
+ readonly create: EntityUseCase<Repository, None, CreateInput<Repository, None>>;
357
+ readonly get: EntityUseCase<Repository, None, ById<Repository>>;
358
+ readonly list: StepsUseCaseConstructor<{
359
+ readonly limit: OptionalProperty<typeof Integer>;
360
+ readonly offset: OptionalProperty<typeof Integer>;
361
+ }, {
362
+ readonly items: IBlueprintList<EntityOf<Repository>>;
363
+ readonly total: typeof Integer;
364
+ readonly limit: typeof Integer;
365
+ readonly offset: typeof Integer;
366
+ }, {
367
+ readonly repository: Repository;
368
+ }>;
369
+ readonly update: EntityUseCase<Repository, None, UpdateInput<Repository>>;
370
+ readonly delete: EntityUseCase<Repository, None, ById<Repository>>;
371
+ }
372
+ /** The options of `resourceOf`. */
373
+ export interface ResourceOptions<Actions extends readonly ResourceAction[] = readonly ResourceAction[]> {
374
+ /** The path of the collection, e.g. `"/posts"`; one entity is at `{path}/{id}`. */
375
+ readonly path: string;
376
+ /** The operations to give; all five by default. Each needs its repository action. */
377
+ readonly actions?: Actions;
378
+ /**
379
+ * Who may call: one declaration for every operation, or one per operation
380
+ * (`{ create: { scopes: ["posts:write"] }, get: { public: true } }`; an
381
+ * operation left out declares none).
382
+ */
383
+ readonly access?: AccessDeclaration | {
384
+ readonly [Action in ResourceAction]?: AccessDeclaration;
385
+ };
386
+ }
212
387
  export {};
@@ -0,0 +1,32 @@
1
+ import type { Authenticator, HttpOptions, SinkStage } from "../adapters.js";
2
+ import type { ContractRunner } from "./provider-contract.js";
3
+ /** What the kit needs of a sink that serves HTTP. */
4
+ export interface HttpProviderContractSettings<Server> {
5
+ /** The sink under test, made with these options and this authenticator. */
6
+ sink(options: HttpOptions, hooks: {
7
+ readonly authenticator?: Authenticator;
8
+ }): SinkStage<Server, HttpOptions>;
9
+ /** Sends a request to the server the sink built, and gives its response. */
10
+ fetch(server: Server, request: Request): Response | Promise<Response>;
11
+ /** Releases the server (a listening socket), after each case. */
12
+ close?(server: Server): void | Promise<void>;
13
+ }
14
+ /**
15
+ * The cases every sink that serves HTTP must pass, whatever its server: the
16
+ * routes use cases declare, where each input key is read from, the context,
17
+ * access, statuses, problems, cookies, the request id and the document. A
18
+ * sink built on `compileRoutes` passes when it routes each method and path
19
+ * to `handle` and sends the result as is.
20
+ *
21
+ * ```ts
22
+ * import { describe, expect, it } from "bun:test";
23
+ * import { httpProviderContract } from "@afora/core/testing";
24
+ *
25
+ * httpProviderContract(
26
+ * "hono",
27
+ * { sink: (options, hooks) => hono(options, hooks), fetch: (app, request) => app.fetch(request) },
28
+ * { describe, it, expect },
29
+ * );
30
+ * ```
31
+ */
32
+ export declare function httpProviderContract<Server>(name: string, settings: HttpProviderContractSettings<Server>, runner: ContractRunner): void;
@@ -1,2 +1,6 @@
1
+ export type { HttpProviderContractSettings } from "./http-provider-contract.js";
2
+ export { httpProviderContract } from "./http-provider-contract.js";
3
+ export type { MemoryPublisher } from "./memory-publisher.js";
4
+ export { memoryPublisher } from "./memory-publisher.js";
1
5
  export type { ContractRunner, ProviderContractOptions, } from "./provider-contract.js";
2
6
  export { providerContract } from "./provider-contract.js";