@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
@@ -1,16 +1,24 @@
1
1
  import { type EnvironmentDependencies, type MergedEnvironment } from "../environment/environment.js";
2
2
  import { Kind } from "../symbols/kind.js";
3
- import type { AnyRepository, AnyRepositoryEntity, EntityInstanceOf, IRepositoryProvider, RepositoryAction, RepositoryContext, RepositoryData, RepositoryOf as RepositoryType } from "./types/repository.interface.js";
3
+ import type { ProviderCapabilities, RepositoryIndex, RepositoryScans } from "./types/query.interface.js";
4
+ import type { AnyRepository, AnyRepositoryEntity, EntityInstanceOf, EntityKeyOf, IRepositoryProvider, ProviderKeyset, RepositoryAction, RepositoryContext, RepositoryData, RepositoryOf as RepositoryType } from "./types/repository.interface.js";
4
5
  /** What `repositoryOf` takes. */
5
- export interface RepositorySettings<Entity extends AnyRepositoryEntity, Actions extends readonly RepositoryAction<Entity>[], Environment extends EnvironmentDependencies | undefined> {
6
+ export interface RepositorySettings<Entity extends AnyRepositoryEntity, Actions extends readonly RepositoryAction<Entity>[], Environment extends EnvironmentDependencies | undefined, Capabilities extends ProviderCapabilities = ProviderCapabilities> {
6
7
  /** The entity class whose entities it holds. */
7
8
  readonly entity: Entity;
8
9
  /** Its methods, e.g. `["findById", "findByEmail", "findManyByName", "create"]`. */
9
10
  readonly actions: Actions;
10
11
  /** Physically remove rows (default), or retain them with `deletedAt`. */
11
12
  readonly deletion?: "hard" | "soft";
12
- /** How it reads and writes; a new `inMemoryProvider()` by default. */
13
- readonly provider?: IRepositoryProvider<MergedEnvironment<Environment>>;
13
+ /**
14
+ * How it reads and writes; a new `inMemoryProvider()` by default. When the
15
+ * type of its `capabilities.keyset` cannot be `true`, the repository has no
16
+ * `findCursor`. Only `capabilities` is inferred, so the methods of a
17
+ * provider written inline keep their parameter types.
18
+ */
19
+ readonly provider?: IRepositoryProvider<MergedEnvironment<Environment>> & {
20
+ readonly capabilities?: Capabilities;
21
+ };
14
22
  /** The environments the provider gets, merged, as `context.env`. */
15
23
  readonly environmentDependencies?: Environment;
16
24
  /**
@@ -19,7 +27,25 @@ export interface RepositorySettings<Entity extends AnyRepositoryEntity, Actions
19
27
  * loaded when read.
20
28
  */
21
29
  readonly relations?: readonly AnyRepository[];
30
+ /**
31
+ * Its access patterns, by name, e.g.
32
+ * `{ byTypeStatus: { where: ["type", "status"], orderBy: "publishedAt" } }`:
33
+ * a query that matches one reads through it, and the provider gets its
34
+ * name as `query.index` and every index as `context.indexes`.
35
+ */
36
+ readonly indexes?: Readonly<Record<string, RepositoryIndex<EntityKeyOf<Entity>>>>;
37
+ /**
38
+ * What a query only a scan serves does: runs (`"allow"`, the default),
39
+ * logs `repository-scan` and runs (`"warn"`), or throws
40
+ * `InvalidRepositoryQuery` (`"reject"`).
41
+ */
42
+ readonly scans?: RepositoryScans;
22
43
  }
44
+ /**
45
+ * The repository `repositoryOf` gives: without `findCursor` when its provider
46
+ * cannot read by cursor (`Keyset` is `false`).
47
+ */
48
+ type RepositoryOfProvider<Entity extends AnyRepositoryEntity, Actions extends readonly RepositoryAction<Entity>[], Environment, Keyset extends boolean> = Keyset extends true ? RepositoryType<Entity, Actions, Environment> : RepositoryType<Entity, Actions, Environment, false>;
23
49
  /**
24
50
  * A repository: the base of every class `repositoryOf` makes. Its methods are
25
51
  * the actions it was declared with; extend it, through `RepositoryOf`, to add
@@ -51,19 +77,24 @@ export declare abstract class AbstractRepository<Entity extends AnyRepositoryEnt
51
77
  * A repository of `entity` with the methods of `actions`: `findBy<Key>` for a
52
78
  * unique key (`id`, `options.unique`, a `unique` property), giving the entity
53
79
  * or `undefined`; `findManyBy<Key>` for any other key, giving a list;
54
- * `create`, `update`, `delete`, `restore` (soft deletion only), `count`, and the queries `findMany`,
55
- * `exists`, `findPage` and `findCursor`. Every method is async and reads and writes through `provider`
80
+ * `create`, `update`, `delete`, `restore` (soft deletion only), `increment`, `count`, and the queries `findMany`,
81
+ * `exists`, `findPage` and `findCursor`, and `forEachPage`, which walks the
82
+ * pages of a cursor query, resumably. Every method is async and reads and writes through `provider`
56
83
  * (a new `inMemoryProvider()` by default), which gets the variables of
57
84
  * `environmentDependencies` as `context.env`. The repository can be a
58
85
  * dependency of a use case, and `RepositoryOf` extends it with a class.
59
86
  */
60
- export declare function repositoryOf<Entity extends AnyRepositoryEntity, const Actions extends readonly RepositoryAction<Entity>[], const Environment extends EnvironmentDependencies | undefined = undefined>(settings: RepositorySettings<Entity, Actions, Environment>): RepositoryType<Entity, Actions, MergedEnvironment<Environment>>;
87
+ export declare function repositoryOf<Entity extends AnyRepositoryEntity, const Actions extends readonly RepositoryAction<Entity>[], const Environment extends EnvironmentDependencies | undefined = undefined, const Capabilities extends ProviderCapabilities = {
88
+ readonly keyset: true;
89
+ }>(settings: RepositorySettings<Entity, Actions, Environment, Capabilities>): RepositoryOfProvider<Entity, Actions, MergedEnvironment<Environment>, ProviderKeyset<{
90
+ readonly capabilities: Capabilities;
91
+ }>>;
61
92
  /**
62
93
  * A repository of `Entity` with the methods of `Actions`, whose provider gets
63
94
  * `Environment` as `context.env`, e.g.
64
95
  * `RepositoryOf<typeof User, ["findById", "create"]>`.
65
96
  */
66
- export type RepositoryOf<Entity extends AnyRepositoryEntity, Actions extends readonly RepositoryAction<Entity>[], Environment = Readonly<Record<never, never>>> = RepositoryType<Entity, Actions, Environment>;
97
+ export type RepositoryOf<Entity extends AnyRepositoryEntity, Actions extends readonly RepositoryAction<Entity>[], Environment = Readonly<Record<never, never>>, Keyset extends boolean = true> = RepositoryType<Entity, Actions, Environment, Keyset>;
67
98
  /**
68
99
  * The class of `repository`, to extend: its instances share the entity,
69
100
  * actions, provider and environment of `repository`, e.g.
@@ -73,3 +104,4 @@ export type RepositoryOf<Entity extends AnyRepositoryEntity, Actions extends rea
73
104
  export declare function RepositoryOf<Repository extends AnyRepository>(repository: Repository): new () => Repository;
74
105
  /** Whether `value` is a repository, made by `repositoryOf` or extending one. */
75
106
  export declare function isRepository(value: unknown): value is AnyRepository;
107
+ export {};
@@ -1,4 +1,7 @@
1
+ import { SCHEMA_VERSION_KEY } from "../entity/abstract-entity.js";
1
2
  import { type EnvironmentDependencies } from "../environment/environment.js";
3
+ import type { ILogger } from "../logger/logger.js";
4
+ import type { RepositoryIndex, RepositoryScans } from "./types/query.interface.js";
2
5
  import type { AnyRepositoryEntity, IRepositoryProvider, RepositoryContext } from "./types/repository.interface.js";
3
6
  /** What a repository class holds, shared by the classes extending it. */
4
7
  export interface RepositoryState {
@@ -12,6 +15,12 @@ export interface RepositoryState {
12
15
  readonly environment: EnvironmentDependencies | undefined;
13
16
  /** The repository of each entity class its entities hold, at any depth. */
14
17
  readonly relations: ReadonlyMap<object, RepositoryState>;
18
+ /** Its declared indexes, normalized and frozen. */
19
+ readonly indexes: Readonly<Record<string, RepositoryIndex>>;
20
+ /** What it does with a query only a scan serves. */
21
+ readonly scans: RepositoryScans;
22
+ /** The logger of the application that bound it, for `scans: "warn"`. */
23
+ readonly logger?: ILogger;
15
24
  context?: RepositoryContext;
16
25
  }
17
26
  export declare const states: WeakMap<object, RepositoryState>;
@@ -19,6 +28,7 @@ export declare const states: WeakMap<object, RepositoryState>;
19
28
  export declare function findState(repository: unknown): RepositoryState | undefined;
20
29
  /** The state of the repository class of `repository`, or of a class it extends. */
21
30
  export declare function stateOf(repository: object): RepositoryState;
31
+ export { SCHEMA_VERSION_KEY };
22
32
  /** The context of the provider, with the environment read on first use. */
23
33
  export declare function contextOf(state: RepositoryState): RepositoryContext;
24
34
  /** What an application reads from a repository, without reaching its state. */
@@ -26,6 +36,8 @@ export interface RepositoryInspection {
26
36
  readonly entity: AnyRepositoryEntity;
27
37
  readonly actions: readonly string[];
28
38
  readonly deletion: "hard" | "soft";
39
+ readonly indexes: Readonly<Record<string, RepositoryIndex>>;
40
+ readonly scans: RepositoryScans;
29
41
  readonly explicitProvider: boolean;
30
42
  /** The provider, only when the repository declared it. */
31
43
  readonly provider: IRepositoryProvider | undefined;
@@ -46,9 +58,27 @@ export declare function declareRepository(state: RepositoryState, repository: ob
46
58
  * @internal
47
59
  */
48
60
  export declare function relatedRepositoryOf(repository: object, Entity: object): object | undefined;
61
+ /**
62
+ * Throws `RepositoryUsedOutsideApplication` when `state` is a declaration an
63
+ * application bound to its provider: its calls would read another store.
64
+ */
65
+ export declare function assertUnbound(state: RepositoryState): void;
66
+ /** Where the application binding a repository comes from. */
67
+ export interface RepositoryBinding {
68
+ /** The name of the application. */
69
+ readonly application: string;
70
+ /** The key of the repository in the application. */
71
+ readonly key: string;
72
+ /** The logger of the application, for `scans: "warn"`. */
73
+ readonly logger?: ILogger;
74
+ }
49
75
  /**
50
76
  * A repository sharing the declaration of `repository` that reads and writes
51
77
  * through `provider`, unless it declared a provider of its own; a declared
52
- * provider found in `instances` is replaced by its value.
78
+ * provider found in `instances` is replaced by its value. With `binding`,
79
+ * the declaration (made by `repositoryOf`) throws
80
+ * `RepositoryUsedOutsideApplication` from then on.
53
81
  */
54
- export declare function bindRepository<Repository extends object>(repository: Repository, provider: IRepositoryProvider | undefined, instances?: ReadonlyMap<object, IRepositoryProvider>): Repository;
82
+ export declare function bindRepository<Repository extends object>(repository: Repository, provider: IRepositoryProvider | undefined, instances?: ReadonlyMap<object, IRepositoryProvider>, binding?: RepositoryBinding): Repository;
83
+ /** The provider `repository` reads and writes through. @internal */
84
+ 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,8 @@ 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
+ import type { SchemaPlan } from "./schema.interface.js";
7
8
  /** Any class made by `entityOf`, or extending one, that a repository holds. */
8
9
  export type AnyRepositoryEntity = AnyRecordClass & AnyEntityClass & {
9
10
  readonly [EntityUniqueKeys]: readonly string[];
@@ -38,6 +39,38 @@ export interface RepositoryCreate<Entity> {
38
39
  export interface RepositoryUpdate<Entity> {
39
40
  update(entity: EntityInstanceOf<Entity>): Promise<EntityInstanceOf<Entity>>;
40
41
  }
42
+ type ChangeKeyOf<Entity> = EntityInstanceOf<Entity> extends {
43
+ readonly change: infer Change;
44
+ } ? keyof Change & string : never;
45
+ /**
46
+ * The keys `increment` takes for `Entity`: those of its `change` list that
47
+ * hold a number.
48
+ */
49
+ export type EntityIncrementKeyOf<Entity> = {
50
+ [Key in ChangeKeyOf<Entity>]: NonNullable<ValueAt<Entity, Key>> extends number ? Key : never;
51
+ }[ChangeKeyOf<Entity>];
52
+ /** What `increment` adds to each key, e.g. `{ views: 1 }`. */
53
+ export type RepositoryIncrementDeltas<Entity> = {
54
+ readonly [Key in EntityIncrementKeyOf<Entity>]?: number;
55
+ };
56
+ /** The options of `increment`. */
57
+ export interface RepositoryIncrementOptions<Entity> {
58
+ /**
59
+ * The input of the entity to create when the id holds none, without its
60
+ * `id` (the one given) and with the keys of the deltas optional (0 when
61
+ * left out); the deltas are added to it.
62
+ */
63
+ readonly create?: Omit<Entity extends new (value: infer Input) => unknown ? Input : never, "id" | EntityIncrementKeyOf<Entity>> & {
64
+ readonly [Key in EntityIncrementKeyOf<Entity>]?: number;
65
+ };
66
+ }
67
+ /**
68
+ * `increment`: adds the deltas to the numeric keys of the entity of the id
69
+ * (or the one given) in one atomic write, and gives the entity as stored.
70
+ */
71
+ export interface RepositoryIncrement<Entity> {
72
+ increment(entity: EntityInstanceOf<Entity> | string, deltas: RepositoryIncrementDeltas<Entity>, options?: RepositoryIncrementOptions<Entity>): Promise<EntityInstanceOf<Entity>>;
73
+ }
41
74
  /** `delete`: destroys the entity (or the one of the id), removes it and gives it. */
42
75
  export interface RepositoryDelete<Entity> {
43
76
  delete(entity: EntityInstanceOf<Entity> | string): Promise<EntityInstanceOf<Entity>>;
@@ -74,19 +107,60 @@ export type RepositoryFindManyBy<Entity, Actions extends string = string> = {
74
107
  * so that a class extending a repository can override them.
75
108
  * @internal
76
109
  */
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);
110
+ 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);
78
111
  /** A method a repository of `Entity` can have, e.g. `"findByEmail"`. */
79
- export type RepositoryAction<Entity> = keyof RepositoryFindBy<Entity> | keyof RepositoryFindManyBy<Entity> | "create" | "update" | "delete" | "restore" | "count" | "findMany" | "exists" | "findPage" | "findCursor";
112
+ export type RepositoryAction<Entity> = keyof RepositoryFindBy<Entity> | keyof RepositoryFindManyBy<Entity> | "create" | "update" | "delete" | "restore" | "increment" | "count" | "findMany" | "exists" | "findPage" | "findCursor" | "explain" | "forEachPage";
113
+ /**
114
+ * Whether a repository given `Provider` keeps `findCursor`: `false` when the
115
+ * type of its `capabilities.keyset` cannot be `true` (left out of the
116
+ * capabilities, or `false`), `true` for `true`, for a wide `boolean`, and
117
+ * with no provider (the default in-memory one reads by cursor). `repositoryOf`
118
+ * infers only the `capabilities` of a provider: one without them keeps
119
+ * `findCursor` in the type and is rejected at declaration.
120
+ */
121
+ export type ProviderKeyset<Provider> = undefined extends Provider ? true : Provider extends {
122
+ readonly capabilities?: infer Capabilities;
123
+ } ? [NonNullable<Capabilities>] extends [{
124
+ readonly keyset?: infer Keyset;
125
+ }] ? true extends Keyset ? true : false : false : false;
80
126
  /**
81
127
  * A repository of `Entity` with the methods of `Actions`, whose provider gets
82
128
  * `Environment` as `context.env`, e.g.
83
129
  * `RepositoryOf<typeof User, ["findById", "create"]>`.
84
130
  */
85
- export type RepositoryOf<Entity extends AnyRepositoryEntity, Actions extends readonly RepositoryAction<Entity>[], Environment = Readonly<Record<never, never>>> = AbstractRepository<Entity, Actions, Environment> & RepositoryMethods<Entity, Actions[number]>;
131
+ 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">>;
86
132
  /** Any repository, whatever its entity, actions and environment. */
87
133
  export type AnyRepository = AbstractRepository<any, any, any>;
88
134
  /** A value of an entity as a provider stores it: plain, unmasked. */
89
135
  export type RepositoryData = Readonly<Record<string, unknown>>;
136
+ /** What `update` of a provider gets after the data. */
137
+ export interface ProviderUpdateOptions {
138
+ /** The `version` the stored row must hold for the write to happen. */
139
+ readonly expectedVersion?: number;
140
+ }
141
+ /** What `increment` of a provider gets after the deltas. */
142
+ export interface ProviderIncrementOptions {
143
+ /** Values stored as they are by the same write, e.g. `updatedAt`. */
144
+ readonly set?: RepositoryData;
145
+ /**
146
+ * The row to insert when the id holds none, or holds one that expired
147
+ * (which it replaces).
148
+ */
149
+ readonly create?: RepositoryData;
150
+ /**
151
+ * For an entity that expires (`context.expiry`), the current date and
152
+ * time (ISO 8601) by the clock of the entity: a row whose expiry is at or
153
+ * before it counts as absent. A provider called without it uses the
154
+ * current time.
155
+ */
156
+ readonly now?: string;
157
+ }
158
+ /** `RepositoryContext.schema`. */
159
+ export interface RepositorySchema {
160
+ readonly version: number;
161
+ readonly key: string;
162
+ readonly previousNames: readonly string[];
163
+ }
90
164
  /** What a provider gets with every call: whose entities, and the environment. */
91
165
  export interface RepositoryContext<Environment = unknown> {
92
166
  /** The entity class; name a table after `recordNameOf(entity)`, not `entity.name`. */
@@ -98,6 +172,28 @@ export interface RepositoryContext<Environment = unknown> {
98
172
  readonly name: string;
99
173
  /** The keys no two entities may share, `id` first. */
100
174
  readonly unique: readonly string[];
175
+ /**
176
+ * The indexes the repository declares, by name, for a provider to build
177
+ * (a GSI, an SQL index); a repository always gives it, empty when it
178
+ * declares none (optional for contexts built by hand).
179
+ */
180
+ readonly indexes?: Readonly<Record<string, RepositoryIndex>>;
181
+ /**
182
+ * For an entity declared with `expires`, the key holding the date at which
183
+ * a record expires (`expiresAt`, or the key of `at`): reads leave out the
184
+ * records whose date is at or before `query.now`, unless
185
+ * `query.withExpired`. A record without a date never expires.
186
+ */
187
+ readonly expiry?: {
188
+ readonly key: string;
189
+ };
190
+ /**
191
+ * For an entity declared with `schemaVersion`: the current version, the
192
+ * key of each row holding it (`SCHEMA_VERSION_KEY`, which repositories
193
+ * write and strip, so a provider keeping it in a column of its own maps
194
+ * that key), and the names the entity had before.
195
+ */
196
+ readonly schema?: RepositorySchema;
101
197
  /** The variables of the `environmentDependencies` of the repository. */
102
198
  readonly env: Environment;
103
199
  }
@@ -107,17 +203,53 @@ export interface RepositoryContext<Environment = unknown> {
107
203
  * `RepositoryUniqueViolation`; `update` and `remove` throw
108
204
  * `RepositoryEntityNotFound` for an unknown id.
109
205
  */
206
+ /**
207
+ * What `IRepositoryProvider.migrate` gives when it did not apply the plan to
208
+ * the store (it wrote it to a file to be run later): `app.migrate` skips the
209
+ * backfill of the entity and reports it `deferred`.
210
+ */
211
+ export type ProviderMigrationResult = {
212
+ readonly deferred: true;
213
+ };
110
214
  export interface IRepositoryProvider<Environment = unknown> {
111
- findOne(context: RepositoryContext<Environment>, query?: RepositoryQuery): Promise<RepositoryData | undefined>;
215
+ findOne(context: RepositoryContext<Environment>, query?: ProviderQuery): Promise<RepositoryData | undefined>;
112
216
  /** `query.after` is only given to a provider with `capabilities.keyset`. */
113
217
  findMany(context: RepositoryContext<Environment>, query?: ProviderQuery): Promise<readonly RepositoryData[]>;
114
218
  insert(context: RepositoryContext<Environment>, data: RepositoryData): Promise<void>;
115
- update(context: RepositoryContext<Environment>, data: RepositoryData): Promise<void>;
219
+ /**
220
+ * With `options.expectedVersion`, writes only when the stored `version` is
221
+ * that one, and throws `RepositoryConcurrencyConflict` otherwise; only a
222
+ * provider with `capabilities.versioning` is given it.
223
+ */
224
+ update(context: RepositoryContext<Environment>, data: RepositoryData, options?: ProviderUpdateOptions): Promise<void>;
116
225
  remove(context: RepositoryContext<Environment>, id: string): Promise<void>;
117
- count(context: RepositoryContext<Environment>, query?: RepositoryQuery): Promise<number>;
226
+ /**
227
+ * Adds each of `deltas` to the stored number of its key (a missing one
228
+ * counts as 0) and stores `options.set` in the same atomic write, then
229
+ * gives the row as stored. Only rows that are not soft-deleted, nor expired
230
+ * (`context.expiry`, at `options.now`), match. When the id holds none (or
231
+ * an expired one), inserts `options.create` as it is, replacing the expired
232
+ * row, and gives it, or throws `RepositoryEntityNotFound` without it; a
233
+ * concurrent insert of the same id still gets the deltas added. A
234
+ * soft-deleted row with `create` is a `RepositoryUniqueViolation` on `id`.
235
+ * Advertised by `capabilities.atomic`.
236
+ */
237
+ increment?(context: RepositoryContext<Environment>, id: string, deltas: Readonly<Record<string, number>>, options?: ProviderIncrementOptions): Promise<RepositoryData>;
238
+ count(context: RepositoryContext<Environment>, query?: ProviderQuery): Promise<number>;
118
239
  readonly capabilities?: ProviderCapabilities;
119
240
  transaction?<Result>(run: (provider: IRepositoryProvider<Environment>) => Promise<Result>): Promise<Result>;
120
241
  /** Releases what the provider holds (connections, files); `app.stop()` calls it. */
121
242
  close?(): void | Promise<void>;
243
+ /**
244
+ * Applies the structural part of `plan` (columns, tables) for a provider
245
+ * with a schema of its own, as SQL; `app.migrate({ mode: "apply" })` calls
246
+ * it before the backfill. Document stores leave it out: they rely on the
247
+ * upgrade on read and the backfill. Only destructive changes when
248
+ * `plan.destructive`. Gives `{ deferred: true }` when it did not apply
249
+ * the plan to the store (it wrote it to a file to be run later): the
250
+ * backfill of the entity is then skipped, as its rows cannot be written
251
+ * in the new shape yet.
252
+ */
253
+ migrate?(context: RepositoryContext<Environment>, plan: SchemaPlan): Promise<void | ProviderMigrationResult>;
122
254
  }
123
255
  export {};
@@ -0,0 +1,126 @@
1
+ import type { Issue } from "../../parse/types/parse.interface.js";
2
+ /**
3
+ * Schema evolution: the lock of the stored shapes, the changes between two of
4
+ * them, and the plan a provider applies (`IRepositoryProvider.migrate`).
5
+ */
6
+ /** What a change between two shapes is. */
7
+ export type SchemaChangeKind = "added-required" | "added-optional" | "removed" | "renamed" | "type-changed" | "tightened" | "loosened" | "made-optional" | "made-required" | "metadata" | "unique-added" | "unique-removed" | "entity-added" | "entity-removed" | "entity-renamed";
8
+ /**
9
+ * Whether afora handles a change alone (`automatic`), needs it declared
10
+ * (`fill`, `renamedFrom`, `previousNames`, an `upgrade` step: `declared`), or
11
+ * needs the stored rows checked before it holds (`check`).
12
+ */
13
+ export type SchemaChangeClass = "automatic" | "declared" | "check";
14
+ /** One change of an entity between the lock and the code. */
15
+ export interface SchemaChange {
16
+ /** The entity, by its current name (its previous one when removed). */
17
+ readonly entity: string;
18
+ /** The path of the key, dot separated for nested blueprints; absent for the entity itself. */
19
+ readonly key?: string;
20
+ readonly change: SchemaChangeKind;
21
+ readonly class: SchemaChangeClass;
22
+ /** Whether the code declares what the change needs (always true when `automatic`). */
23
+ readonly satisfied: boolean;
24
+ /** Whether applying it deletes stored data (a removed key, the rows of an old name). */
25
+ readonly destructive: boolean;
26
+ /** For `renamed` and `entity-renamed`, the old name. */
27
+ readonly from?: string;
28
+ /** What is missing or what to check, for messages and stubs. */
29
+ readonly detail?: string;
30
+ }
31
+ /** A key of an entity in the lock. */
32
+ export interface SchemaLockField {
33
+ /** The JSON Schema of the value. */
34
+ readonly schema: Readonly<Record<string, unknown>>;
35
+ readonly optional: boolean;
36
+ readonly unique?: boolean;
37
+ readonly fill?: unknown;
38
+ readonly renamedFrom?: string;
39
+ /** The keys of a nested blueprint or record. */
40
+ readonly fields?: Readonly<Record<string, SchemaLockField>>;
41
+ }
42
+ /** An entity in the lock. */
43
+ export interface SchemaLockEntity {
44
+ readonly schemaVersion: number;
45
+ readonly previousNames: readonly string[];
46
+ readonly unique: readonly string[];
47
+ readonly fields: Readonly<Record<string, SchemaLockField>>;
48
+ /**
49
+ * The versions its `upgrade` has a step to, ascending; absent when none.
50
+ * `diffSchemas` reads it from the current side to tell whether a change
51
+ * that needs a step has one.
52
+ */
53
+ readonly upgrade?: readonly number[];
54
+ }
55
+ /** `afora.lock.json`: the stored shapes, by entity name, normalized. */
56
+ export interface SchemaLock {
57
+ readonly lockVersion: 1;
58
+ readonly entities: Readonly<Record<string, SchemaLockEntity>>;
59
+ }
60
+ /** What `IRepositoryProvider.migrate` gets: the changes of one entity. */
61
+ export interface SchemaPlan {
62
+ /** The entity, by its current name. */
63
+ readonly entity: string;
64
+ readonly changes: readonly SchemaChange[];
65
+ /** Whether destructive changes may be applied (`app.migrate({ destructive })`). */
66
+ readonly destructive: boolean;
67
+ }
68
+ /** What `app.migrate` takes. */
69
+ export interface SchemaMigrationOptions {
70
+ /** The entities to walk, by name; every entity with a repository by default. */
71
+ readonly entities?: readonly string[];
72
+ /** `check` only reads and reports; `apply` rewrites the rows. */
73
+ readonly mode: "check" | "apply";
74
+ /**
75
+ * Remove the keys the blueprints no longer declare from the stored rows,
76
+ * and the rows under `previousNames` once moved. Default `false`.
77
+ */
78
+ readonly destructive?: boolean;
79
+ /** Rows read per page. Default 100. */
80
+ readonly pageSize?: number;
81
+ /**
82
+ * The committed lock (`afora.lock.json`): in `apply`, the changes of each
83
+ * entity from it to the code are the plan given to
84
+ * `IRepositoryProvider.migrate`. Without it the plan has no changes.
85
+ */
86
+ readonly lock?: SchemaLock;
87
+ }
88
+ /** A row `app.migrate` cannot bring up to the current shape. */
89
+ export interface SchemaMigrationInvalidRow {
90
+ readonly id: string;
91
+ /** Why: the issues of the parse, or one for an error of the upgrade. */
92
+ readonly issues: readonly Issue[];
93
+ }
94
+ /** What `app.migrate` found and did for one entity. */
95
+ export interface EntityMigrationReport {
96
+ /** Rows read, under the current name and the previous ones. */
97
+ readonly scanned: number;
98
+ /** Rows that needed nothing. */
99
+ readonly upToDate: number;
100
+ /** Rows rewritten in the current shape (in `check`, that would be). */
101
+ readonly upgraded: number;
102
+ /**
103
+ * Rows whose only pending change is removing keys, left as they are
104
+ * without `destructive`.
105
+ */
106
+ readonly removalPending: number;
107
+ /** Rows the entity rejects once brought up, left as they are. */
108
+ readonly invalid: readonly SchemaMigrationInvalidRow[];
109
+ /** Rows moved from a previous name (in `check`, that would be). */
110
+ readonly movedFromPreviousNames: number;
111
+ /** Paths of the stored keys the blueprint no longer declares, sorted. */
112
+ readonly removedKeys: readonly string[];
113
+ /**
114
+ * Whether `provider.migrate` deferred the plan (`{ deferred: true }`, it
115
+ * wrote it to be run later): the rows were then not walked, and every
116
+ * count is 0. Run `app.migrate` again once the plan is applied.
117
+ */
118
+ readonly deferred: boolean;
119
+ }
120
+ /** What `app.migrate` gives. */
121
+ export interface SchemaMigrationReport {
122
+ readonly mode: "check" | "apply";
123
+ readonly destructive: boolean;
124
+ /** By entity name. */
125
+ readonly entities: Readonly<Record<string, EntityMigrationReport>>;
126
+ }