@afora/core 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +36 -7
  2. package/dist/core/application/start.d.ts +7 -0
  3. package/dist/core/application/types/application.interface.d.ts +47 -2
  4. package/dist/core/command/command.d.ts +11 -4
  5. package/dist/core/command/types/command.interface.d.ts +26 -0
  6. package/dist/core/entity/abstract-entity.d.ts +28 -1
  7. package/dist/core/entity/entity.d.ts +1 -1
  8. package/dist/core/entity/event.d.ts +13 -2
  9. package/dist/core/entity/exceptions.d.ts +8 -0
  10. package/dist/core/entity/types/entity.interface.d.ts +66 -2
  11. package/dist/core/entity/version.d.ts +13 -0
  12. package/dist/core/event-policy/event-policy.d.ts +25 -0
  13. package/dist/core/event-policy/exceptions.d.ts +11 -0
  14. package/dist/core/event-policy/publish.d.ts +17 -0
  15. package/dist/core/event-policy/publishers.d.ts +18 -0
  16. package/dist/core/event-policy/types/event-policy.interface.d.ts +72 -0
  17. package/dist/core/output/coerce.d.ts +9 -0
  18. package/dist/core/output/openapi-sink.d.ts +138 -38
  19. package/dist/core/output/status.d.ts +2 -1
  20. package/dist/core/parse/parse.d.ts +10 -0
  21. package/dist/core/parse/strip.d.ts +13 -0
  22. package/dist/core/property/input.d.ts +20 -0
  23. package/dist/core/record/plugin.d.ts +6 -2
  24. package/dist/core/repository/exceptions.d.ts +30 -0
  25. package/dist/core/repository/in-memory.d.ts +14 -1
  26. package/dist/core/repository/plan.d.ts +20 -0
  27. package/dist/core/repository/provider.d.ts +1 -1
  28. package/dist/core/repository/query.d.ts +9 -2
  29. package/dist/core/repository/repository.d.ts +40 -8
  30. package/dist/core/repository/state.d.ts +39 -2
  31. package/dist/core/repository/types/query.interface.d.ts +106 -5
  32. package/dist/core/repository/types/repository.interface.d.ts +113 -7
  33. package/dist/core/use-case/exceptions.d.ts +36 -0
  34. package/dist/core/use-case/plugin-catalog.d.ts +11 -0
  35. package/dist/core/use-case/plugin.d.ts +25 -0
  36. package/dist/core/use-case/steps/types/steps.interface.d.ts +23 -6
  37. package/dist/core/use-case/target.d.ts +9 -0
  38. package/dist/core/use-case/types/plugin.interface.d.ts +185 -0
  39. package/dist/experimental.js +3 -3
  40. package/dist/{index-3nd9bjnc.js → index-qxfkyd6k.js} +646 -635
  41. package/dist/{index-gbgc40xe.js → index-t5qwtyj5.js} +2798 -295
  42. package/dist/index.d.ts +35 -15
  43. package/dist/index.js +99 -15
  44. package/dist/std/blueprint-plugin.d.ts +6 -2
  45. package/dist/std/blueprint.d.ts +3 -0
  46. package/dist/std/command.d.ts +11 -0
  47. package/dist/std/entity.d.ts +2 -1
  48. package/dist/std/event-policies.d.ts +2 -0
  49. package/dist/std/properties/exceptions.d.ts +11 -0
  50. package/dist/std/properties/json.d.ts +54 -0
  51. package/dist/std/properties/password.d.ts +16 -1
  52. package/dist/std/properties/time.d.ts +4 -0
  53. package/dist/std/use-cases/auth.d.ts +5 -0
  54. package/dist/std/use-cases/crud.d.ts +25 -0
  55. package/dist/std/use-cases/index.d.ts +11 -0
  56. package/dist/std/use-cases/lifecycle.d.ts +13 -0
  57. package/dist/std/use-cases/shared.d.ts +88 -0
  58. package/dist/std/use-cases/types.d.ts +291 -0
  59. package/dist/testing/index.d.ts +2 -0
  60. package/dist/testing/index.js +355 -2
  61. package/dist/testing/memory-publisher.d.ts +16 -0
  62. package/dist/testing/provider-contract.d.ts +14 -0
  63. package/package.json +3 -2
@@ -1,4 +1,6 @@
1
1
  import { type EnvironmentDependencies } from "../environment/environment.js";
2
+ import type { ILogger } from "../logger/logger.js";
3
+ import type { RepositoryIndex, RepositoryScans } from "./types/query.interface.js";
2
4
  import type { AnyRepositoryEntity, IRepositoryProvider, RepositoryContext } from "./types/repository.interface.js";
3
5
  /** What a repository class holds, shared by the classes extending it. */
4
6
  export interface RepositoryState {
@@ -12,6 +14,12 @@ export interface RepositoryState {
12
14
  readonly environment: EnvironmentDependencies | undefined;
13
15
  /** The repository of each entity class its entities hold, at any depth. */
14
16
  readonly relations: ReadonlyMap<object, RepositoryState>;
17
+ /** Its declared indexes, normalized and frozen. */
18
+ readonly indexes: Readonly<Record<string, RepositoryIndex>>;
19
+ /** What it does with a query only a scan serves. */
20
+ readonly scans: RepositoryScans;
21
+ /** The logger of the application that bound it, for `scans: "warn"`. */
22
+ readonly logger?: ILogger;
15
23
  context?: RepositoryContext;
16
24
  }
17
25
  export declare const states: WeakMap<object, RepositoryState>;
@@ -26,6 +34,8 @@ export interface RepositoryInspection {
26
34
  readonly entity: AnyRepositoryEntity;
27
35
  readonly actions: readonly string[];
28
36
  readonly deletion: "hard" | "soft";
37
+ readonly indexes: Readonly<Record<string, RepositoryIndex>>;
38
+ readonly scans: RepositoryScans;
29
39
  readonly explicitProvider: boolean;
30
40
  /** The provider, only when the repository declared it. */
31
41
  readonly provider: IRepositoryProvider | undefined;
@@ -37,9 +47,36 @@ export interface RepositoryInspection {
37
47
  }
38
48
  /** The declaration of `repository`; `undefined` when it is not one. */
39
49
  export declare function inspectRepository(repository: unknown): RepositoryInspection | undefined;
50
+ /** Keeps `repository` as the one `repositoryOf` made for `state`. */
51
+ export declare function declareRepository(state: RepositoryState, repository: object): void;
52
+ /**
53
+ * The repository `repositoryOf` made for the entity class `Entity` that the
54
+ * entities of `repository` hold; `undefined` when it has no such relation.
55
+ *
56
+ * @internal
57
+ */
58
+ export declare function relatedRepositoryOf(repository: object, Entity: object): object | undefined;
59
+ /**
60
+ * Throws `RepositoryUsedOutsideApplication` when `state` is a declaration an
61
+ * application bound to its provider: its calls would read another store.
62
+ */
63
+ export declare function assertUnbound(state: RepositoryState): void;
64
+ /** Where the application binding a repository comes from. */
65
+ export interface RepositoryBinding {
66
+ /** The name of the application. */
67
+ readonly application: string;
68
+ /** The key of the repository in the application. */
69
+ readonly key: string;
70
+ /** The logger of the application, for `scans: "warn"`. */
71
+ readonly logger?: ILogger;
72
+ }
40
73
  /**
41
74
  * A repository sharing the declaration of `repository` that reads and writes
42
75
  * through `provider`, unless it declared a provider of its own; a declared
43
- * provider found in `instances` is replaced by its value.
76
+ * provider found in `instances` is replaced by its value. With `binding`,
77
+ * the declaration (made by `repositoryOf`) throws
78
+ * `RepositoryUsedOutsideApplication` from then on.
44
79
  */
45
- export declare function bindRepository<Repository extends object>(repository: Repository, provider: IRepositoryProvider | undefined, instances?: ReadonlyMap<object, IRepositoryProvider>): Repository;
80
+ export declare function bindRepository<Repository extends object>(repository: Repository, provider: IRepositoryProvider | undefined, instances?: ReadonlyMap<object, IRepositoryProvider>, binding?: RepositoryBinding): Repository;
81
+ /** The provider `repository` reads and writes through. @internal */
82
+ export declare function providerOfRepository(repository: unknown): IRepositoryProvider | undefined;
@@ -22,6 +22,11 @@ export interface RepositoryQuery<Entity = unknown> {
22
22
  readonly offset?: number;
23
23
  /** Include destroyed rows; repository defaults depend on its deletion policy. */
24
24
  readonly withDeleted?: boolean;
25
+ /**
26
+ * Include expired rows, of an entity declared with `expires`; `false` by
27
+ * default.
28
+ */
29
+ readonly withExpired?: boolean;
25
30
  }
26
31
  /** A page and the unpaginated number of matching live (or explicitly included) rows. */
27
32
  export interface RepositoryPage<Entity> {
@@ -32,8 +37,8 @@ export interface RepositoryPage<Entity> {
32
37
  }
33
38
  /**
34
39
  * The query of `findCursor`: as `RepositoryQuery`, with the `next` of the
35
- * previous page as `after` in place of `offset`. `where`, `orderBy` and
36
- * `withDeleted` must stay the same from one page to the next.
40
+ * previous page as `after` in place of `offset`. `where`, `orderBy`,
41
+ * `withDeleted` and `withExpired` must stay the same from one page to the next.
37
42
  */
38
43
  export interface RepositoryCursorQuery<Entity = unknown> extends Omit<RepositoryQuery<Entity>, "offset" | "limit"> {
39
44
  /** Positive integer; 20 when left out. */
@@ -49,13 +54,56 @@ export interface RepositoryCursorPage<Entity> {
49
54
  readonly next: string | undefined;
50
55
  }
51
56
  /**
52
- * What a provider gets from `findCursor` on `findMany`: `after` holds one
57
+ * What a provider gets. From `findCursor` on `findMany`, `after` holds one
53
58
  * value for each order of `orderBy`, those of the last row already read, and
54
- * the provider gives the rows strictly after it in that order. Only a
55
- * provider with `capabilities.keyset` is given it.
59
+ * the provider gives the rows strictly after it in that order; only a
60
+ * provider with `capabilities.keyset` is given it. For an entity that expires
61
+ * (`context.expiry`), `now` is the current date and time as an ISO 8601
62
+ * string, from the clock of the entity: unless `withExpired`, the provider
63
+ * leaves out the rows whose `context.expiry.key` is at or before it.
56
64
  */
57
65
  export interface ProviderQuery extends RepositoryQuery {
58
66
  readonly after?: readonly unknown[];
67
+ /**
68
+ * The name of the declared index (`context.indexes`) that serves the
69
+ * query, when one does; a provider without `capabilities.indexes` can
70
+ * ignore it, and the rows must be the same either way.
71
+ */
72
+ readonly index?: string;
73
+ /** The current date and time (ISO 8601), given for entities that expire. */
74
+ readonly now?: string;
75
+ }
76
+ /**
77
+ * An access pattern a repository declares: queries whose `where` holds every
78
+ * key of `where` by equality, and whose first order (if any) is `orderBy`.
79
+ */
80
+ export interface RepositoryIndex<Key extends string = string> {
81
+ /** The keys the query matches by equality, at least one. */
82
+ readonly where: readonly Key[];
83
+ /** The scalar key the rows are read in order of. */
84
+ readonly orderBy?: Key;
85
+ }
86
+ /**
87
+ * How a query reaches its rows: by `id`, by another unique key, through a
88
+ * declared index (named by `index`), or by reading every row (`scan`).
89
+ */
90
+ export interface RepositoryPlan {
91
+ readonly access: "id" | "unique" | "index" | "scan";
92
+ /** The declared index that serves the query, when `access` is `"index"`. */
93
+ readonly index?: string;
94
+ }
95
+ /**
96
+ * What a repository does with a query that only a scan serves: run it
97
+ * (`"allow"`, the default), log `repository-scan` and run it (`"warn"`), or
98
+ * throw `InvalidRepositoryQuery` before the provider is called (`"reject"`).
99
+ */
100
+ export type RepositoryScans = "allow" | "warn" | "reject";
101
+ export interface RepositoryExplain<Entity> {
102
+ /**
103
+ * How the query would be read (`{ access, index? }`), without reading
104
+ * anything; takes the query of `findMany` or of `findCursor`.
105
+ */
106
+ explain(query?: RepositoryQuery<Entity> | RepositoryCursorQuery<Entity>): Promise<RepositoryPlan>;
59
107
  }
60
108
  export interface RepositoryFindCursor<Entity> {
61
109
  /**
@@ -65,6 +113,48 @@ export interface RepositoryFindCursor<Entity> {
65
113
  */
66
114
  findCursor(query?: RepositoryCursorQuery<Entity>): Promise<RepositoryCursorPage<Entity>>;
67
115
  }
116
+ /** Where `forEachPage` is in its walk, given with each page. */
117
+ export interface RepositoryWalkPage {
118
+ /** The position of the page in this call, from 0. */
119
+ readonly index: number;
120
+ /**
121
+ * The cursor after this page, `undefined` on the last: save it once the
122
+ * page is done to resume from the next one.
123
+ */
124
+ readonly next?: string;
125
+ }
126
+ /** When `forEachPage` stops before the end. */
127
+ export interface RepositoryWalkOptions {
128
+ /**
129
+ * A deadline (a `Date` or epoch milliseconds) or a function that says when
130
+ * to stop; checked before each page, never while one runs.
131
+ */
132
+ readonly until?: Date | number | (() => boolean);
133
+ }
134
+ /** What `forEachPage` did. */
135
+ export interface RepositoryWalkResult {
136
+ /** Whether it walked to the last page; `false` when `until` stopped it. */
137
+ readonly done: boolean;
138
+ /**
139
+ * When it stopped early, the `after` to resume from: the `next` of the
140
+ * last page it ran, or the `after` it was given if it ran none.
141
+ */
142
+ readonly next?: string;
143
+ /** The pages it ran `fn` on. */
144
+ readonly pages: number;
145
+ /** The items of those pages. */
146
+ readonly items: number;
147
+ }
148
+ export interface RepositoryForEachPage<Entity> {
149
+ /**
150
+ * Reads the pages of `query` by cursor, as `findCursor` does, and runs
151
+ * `fn` on each, one after the other, until the last or until `until`
152
+ * says to stop (checked between pages). Rows added or removed between
153
+ * pages, by `fn` too, are neither repeated nor skipped. An error of `fn`
154
+ * stops the walk and rejects; resume from the last `next` saved.
155
+ */
156
+ forEachPage(query: RepositoryCursorQuery<Entity>, fn: (items: EntityInstanceOf<Entity>[], page: RepositoryWalkPage) => unknown, options?: RepositoryWalkOptions): Promise<RepositoryWalkResult>;
157
+ }
68
158
  export interface RepositoryFindMany<Entity> {
69
159
  findMany(query?: RepositoryQuery<Entity>): Promise<EntityInstanceOf<Entity>[]>;
70
160
  }
@@ -94,6 +184,17 @@ export interface ProviderCapabilities {
94
184
  readonly transactions?: boolean;
95
185
  /** Whether `findMany` honors `after` (`ProviderQuery`), which `findCursor` needs. */
96
186
  readonly keyset?: boolean;
187
+ /**
188
+ * Whether the provider serves the indexes a repository declares
189
+ * (`context.indexes`) from storage, reading `ProviderQuery.index`.
190
+ */
191
+ readonly indexes?: boolean;
192
+ /** Whether `update` honors an expected `version` (versioned entities, #44). */
193
+ readonly versioning?: boolean;
194
+ /** Whether the provider has an atomic `increment` (#34). */
195
+ readonly atomic?: boolean;
196
+ /** Whether reads leave out expired records (entities with `expires`, #35). */
197
+ readonly expiry?: boolean;
97
198
  }
98
199
  /** Declaration input for a named provider, implemented by providerOf in T10. */
99
200
  export interface ProviderSettings<Options extends ProviderOptions = ProviderOptions, Provider extends IRepositoryProvider = IRepositoryProvider> {
@@ -3,7 +3,7 @@ import type { AbstractEntity, EntityUniqueKeys } from "../../entity/abstract-ent
3
3
  import type { AnyEntityClass } from "../../entity/event.js";
4
4
  import type { RecordSettings } from "../../record/types/record.interface.js";
5
5
  import type { AbstractRepository } from "../repository.js";
6
- import type { ProviderCapabilities, ProviderQuery, RepositoryExists, RepositoryFindCursor, RepositoryFindMany, RepositoryFindPage, RepositoryQuery } from "./query.interface.js";
6
+ import type { ProviderCapabilities, ProviderQuery, RepositoryExists, RepositoryExplain, RepositoryFindCursor, RepositoryFindMany, RepositoryFindPage, RepositoryForEachPage, RepositoryIndex, RepositoryQuery } from "./query.interface.js";
7
7
  /** Any class made by `entityOf`, or extending one, that a repository holds. */
8
8
  export type AnyRepositoryEntity = AnyRecordClass & AnyEntityClass & {
9
9
  readonly [EntityUniqueKeys]: readonly string[];
@@ -38,10 +38,49 @@ export interface RepositoryCreate<Entity> {
38
38
  export interface RepositoryUpdate<Entity> {
39
39
  update(entity: EntityInstanceOf<Entity>): Promise<EntityInstanceOf<Entity>>;
40
40
  }
41
+ type ChangeKeyOf<Entity> = EntityInstanceOf<Entity> extends {
42
+ readonly change: infer Change;
43
+ } ? keyof Change & string : never;
44
+ /**
45
+ * The keys `increment` takes for `Entity`: those of its `change` list that
46
+ * hold a number.
47
+ */
48
+ export type EntityIncrementKeyOf<Entity> = {
49
+ [Key in ChangeKeyOf<Entity>]: NonNullable<ValueAt<Entity, Key>> extends number ? Key : never;
50
+ }[ChangeKeyOf<Entity>];
51
+ /** What `increment` adds to each key, e.g. `{ views: 1 }`. */
52
+ export type RepositoryIncrementDeltas<Entity> = {
53
+ readonly [Key in EntityIncrementKeyOf<Entity>]?: number;
54
+ };
55
+ /** The options of `increment`. */
56
+ export interface RepositoryIncrementOptions<Entity> {
57
+ /**
58
+ * The input of the entity to create when the id holds none, without its
59
+ * `id` (the one given) and with the keys of the deltas optional (0 when
60
+ * left out); the deltas are added to it.
61
+ */
62
+ readonly create?: Omit<Entity extends new (value: infer Input) => unknown ? Input : never, "id" | EntityIncrementKeyOf<Entity>> & {
63
+ readonly [Key in EntityIncrementKeyOf<Entity>]?: number;
64
+ };
65
+ }
66
+ /**
67
+ * `increment`: adds the deltas to the numeric keys of the entity of the id
68
+ * (or the one given) in one atomic write, and gives the entity as stored.
69
+ */
70
+ export interface RepositoryIncrement<Entity> {
71
+ increment(entity: EntityInstanceOf<Entity> | string, deltas: RepositoryIncrementDeltas<Entity>, options?: RepositoryIncrementOptions<Entity>): Promise<EntityInstanceOf<Entity>>;
72
+ }
41
73
  /** `delete`: destroys the entity (or the one of the id), removes it and gives it. */
42
74
  export interface RepositoryDelete<Entity> {
43
75
  delete(entity: EntityInstanceOf<Entity> | string): Promise<EntityInstanceOf<Entity>>;
44
76
  }
77
+ /**
78
+ * `restore`: brings back a soft-deleted entity (or the one of the id) and
79
+ * gives it, alive; only for a repository with `deletion: "soft"`.
80
+ */
81
+ export interface RepositoryRestore<Entity> {
82
+ restore(entity: EntityInstanceOf<Entity> | string): Promise<EntityInstanceOf<Entity>>;
83
+ }
45
84
  /** `count`: how many entities it holds. */
46
85
  export interface RepositoryCount<Entity = unknown> {
47
86
  count(query?: RepositoryQuery<Entity>): Promise<number>;
@@ -67,19 +106,54 @@ export type RepositoryFindManyBy<Entity, Actions extends string = string> = {
67
106
  * so that a class extending a repository can override them.
68
107
  * @internal
69
108
  */
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);
109
+ 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) & ("increment" extends Actions ? RepositoryIncrement<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) & ("explain" extends Actions ? RepositoryExplain<Entity> : unknown) & ("forEachPage" extends Actions ? RepositoryForEachPage<Entity> : unknown);
71
110
  /** 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";
111
+ export type RepositoryAction<Entity> = keyof RepositoryFindBy<Entity> | keyof RepositoryFindManyBy<Entity> | "create" | "update" | "delete" | "restore" | "increment" | "count" | "findMany" | "exists" | "findPage" | "findCursor" | "explain" | "forEachPage";
112
+ /**
113
+ * Whether a repository given `Provider` keeps `findCursor`: `false` when the
114
+ * type of its `capabilities.keyset` cannot be `true` (left out of the
115
+ * capabilities, or `false`), `true` for `true`, for a wide `boolean`, and
116
+ * with no provider (the default in-memory one reads by cursor). `repositoryOf`
117
+ * infers only the `capabilities` of a provider: one without them keeps
118
+ * `findCursor` in the type and is rejected at declaration.
119
+ */
120
+ export type ProviderKeyset<Provider> = undefined extends Provider ? true : Provider extends {
121
+ readonly capabilities?: infer Capabilities;
122
+ } ? [NonNullable<Capabilities>] extends [{
123
+ readonly keyset?: infer Keyset;
124
+ }] ? true extends Keyset ? true : false : false : false;
73
125
  /**
74
126
  * A repository of `Entity` with the methods of `Actions`, whose provider gets
75
127
  * `Environment` as `context.env`, e.g.
76
128
  * `RepositoryOf<typeof User, ["findById", "create"]>`.
77
129
  */
78
- export type RepositoryOf<Entity extends AnyRepositoryEntity, Actions extends readonly RepositoryAction<Entity>[], Environment = Readonly<Record<never, never>>> = AbstractRepository<Entity, Actions, Environment> & RepositoryMethods<Entity, Actions[number]>;
130
+ export type RepositoryOf<Entity extends AnyRepositoryEntity, Actions extends readonly RepositoryAction<Entity>[], Environment = Readonly<Record<never, never>>, Keyset extends boolean = true> = AbstractRepository<Entity, Actions, Environment> & RepositoryMethods<Entity, Keyset extends true ? Actions[number] : Exclude<Actions[number], "findCursor">>;
79
131
  /** Any repository, whatever its entity, actions and environment. */
80
132
  export type AnyRepository = AbstractRepository<any, any, any>;
81
133
  /** A value of an entity as a provider stores it: plain, unmasked. */
82
134
  export type RepositoryData = Readonly<Record<string, unknown>>;
135
+ /** What `update` of a provider gets after the data. */
136
+ export interface ProviderUpdateOptions {
137
+ /** The `version` the stored row must hold for the write to happen. */
138
+ readonly expectedVersion?: number;
139
+ }
140
+ /** What `increment` of a provider gets after the deltas. */
141
+ export interface ProviderIncrementOptions {
142
+ /** Values stored as they are by the same write, e.g. `updatedAt`. */
143
+ readonly set?: RepositoryData;
144
+ /**
145
+ * The row to insert when the id holds none, or holds one that expired
146
+ * (which it replaces).
147
+ */
148
+ readonly create?: RepositoryData;
149
+ /**
150
+ * For an entity that expires (`context.expiry`), the current date and
151
+ * time (ISO 8601) by the clock of the entity: a row whose expiry is at or
152
+ * before it counts as absent. A provider called without it uses the
153
+ * current time.
154
+ */
155
+ readonly now?: string;
156
+ }
83
157
  /** What a provider gets with every call: whose entities, and the environment. */
84
158
  export interface RepositoryContext<Environment = unknown> {
85
159
  /** The entity class; name a table after `recordNameOf(entity)`, not `entity.name`. */
@@ -91,6 +165,21 @@ export interface RepositoryContext<Environment = unknown> {
91
165
  readonly name: string;
92
166
  /** The keys no two entities may share, `id` first. */
93
167
  readonly unique: readonly string[];
168
+ /**
169
+ * The indexes the repository declares, by name, for a provider to build
170
+ * (a GSI, an SQL index); a repository always gives it, empty when it
171
+ * declares none (optional for contexts built by hand).
172
+ */
173
+ readonly indexes?: Readonly<Record<string, RepositoryIndex>>;
174
+ /**
175
+ * For an entity declared with `expires`, the key holding the date at which
176
+ * a record expires (`expiresAt`, or the key of `at`): reads leave out the
177
+ * records whose date is at or before `query.now`, unless
178
+ * `query.withExpired`. A record without a date never expires.
179
+ */
180
+ readonly expiry?: {
181
+ readonly key: string;
182
+ };
94
183
  /** The variables of the `environmentDependencies` of the repository. */
95
184
  readonly env: Environment;
96
185
  }
@@ -101,13 +190,30 @@ export interface RepositoryContext<Environment = unknown> {
101
190
  * `RepositoryEntityNotFound` for an unknown id.
102
191
  */
103
192
  export interface IRepositoryProvider<Environment = unknown> {
104
- findOne(context: RepositoryContext<Environment>, query?: RepositoryQuery): Promise<RepositoryData | undefined>;
193
+ findOne(context: RepositoryContext<Environment>, query?: ProviderQuery): Promise<RepositoryData | undefined>;
105
194
  /** `query.after` is only given to a provider with `capabilities.keyset`. */
106
195
  findMany(context: RepositoryContext<Environment>, query?: ProviderQuery): Promise<readonly RepositoryData[]>;
107
196
  insert(context: RepositoryContext<Environment>, data: RepositoryData): Promise<void>;
108
- update(context: RepositoryContext<Environment>, data: RepositoryData): Promise<void>;
197
+ /**
198
+ * With `options.expectedVersion`, writes only when the stored `version` is
199
+ * that one, and throws `RepositoryConcurrencyConflict` otherwise; only a
200
+ * provider with `capabilities.versioning` is given it.
201
+ */
202
+ update(context: RepositoryContext<Environment>, data: RepositoryData, options?: ProviderUpdateOptions): Promise<void>;
109
203
  remove(context: RepositoryContext<Environment>, id: string): Promise<void>;
110
- count(context: RepositoryContext<Environment>, query?: RepositoryQuery): Promise<number>;
204
+ /**
205
+ * Adds each of `deltas` to the stored number of its key (a missing one
206
+ * counts as 0) and stores `options.set` in the same atomic write, then
207
+ * gives the row as stored. Only rows that are not soft-deleted, nor expired
208
+ * (`context.expiry`, at `options.now`), match. When the id holds none (or
209
+ * an expired one), inserts `options.create` as it is, replacing the expired
210
+ * row, and gives it, or throws `RepositoryEntityNotFound` without it; a
211
+ * concurrent insert of the same id still gets the deltas added. A
212
+ * soft-deleted row with `create` is a `RepositoryUniqueViolation` on `id`.
213
+ * Advertised by `capabilities.atomic`.
214
+ */
215
+ increment?(context: RepositoryContext<Environment>, id: string, deltas: Readonly<Record<string, number>>, options?: ProviderIncrementOptions): Promise<RepositoryData>;
216
+ count(context: RepositoryContext<Environment>, query?: ProviderQuery): Promise<number>;
111
217
  readonly capabilities?: ProviderCapabilities;
112
218
  transaction?<Result>(run: (provider: IRepositoryProvider<Environment>) => Promise<Result>): Promise<Result>;
113
219
  /** Releases what the provider holds (connections, files); `app.stop()` calls it. */
@@ -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?: readonly StepValue[];
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?: readonly StepValue[];
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?: readonly StepValue[];
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?: readonly StepValue[];
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?: readonly StepValue[];
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?: readonly StepValue[];
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;