@afora/core 0.0.8 → 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.
- package/README.md +127 -23
- package/dist/adapters.d.ts +25 -0
- package/dist/adapters.js +56 -0
- package/dist/core/application/exceptions.d.ts +17 -0
- package/dist/core/application/migrate.d.ts +19 -0
- package/dist/core/application/schema.d.ts +30 -0
- package/dist/core/application/types/application.interface.d.ts +37 -0
- package/dist/core/auth/jwt.d.ts +30 -0
- package/dist/core/auth/types/jwt.interface.d.ts +66 -0
- package/dist/core/blueprint/exceptions.d.ts +5 -0
- package/dist/core/blueprint/optional.d.ts +3 -1
- package/dist/core/blueprint/types/blueprint.interface.d.ts +79 -5
- package/dist/core/blueprint/variant.d.ts +32 -0
- package/dist/core/entity/abstract-entity.d.ts +5 -0
- package/dist/core/entity/entity.d.ts +1 -1
- package/dist/core/entity/exceptions.d.ts +9 -0
- package/dist/core/entity/trust.d.ts +24 -0
- package/dist/core/entity/types/entity.interface.d.ts +30 -0
- package/dist/core/output/access.d.ts +16 -0
- package/dist/core/output/exceptions.d.ts +16 -0
- package/dist/core/output/http.d.ts +36 -0
- package/dist/core/output/openapi-sink.d.ts +23 -2
- package/dist/core/output/stage.d.ts +7 -2
- package/dist/core/output/status.d.ts +1 -1
- package/dist/core/output/types/http.interface.d.ts +130 -0
- package/dist/core/output/types/output.interface.d.ts +25 -5
- package/dist/core/parse/issue.d.ts +28 -3
- package/dist/core/parse/parse.d.ts +9 -0
- package/dist/core/property/abstract-property.d.ts +9 -0
- package/dist/core/property/define-property.d.ts +34 -6
- package/dist/core/property/types/property-options.interface.d.ts +10 -0
- package/dist/core/property/types/property-type.interface.d.ts +4 -0
- package/dist/core/record/abstract-record.d.ts +7 -0
- package/dist/core/record/types/record.interface.d.ts +12 -1
- package/dist/core/repository/exceptions.d.ts +13 -0
- package/dist/core/repository/in-memory.d.ts +2 -2
- package/dist/core/repository/migrate.d.ts +20 -0
- package/dist/core/repository/state.d.ts +2 -0
- package/dist/core/repository/types/repository.interface.d.ts +33 -0
- package/dist/core/repository/types/schema.interface.d.ts +126 -0
- package/dist/core/repository/upcast.d.ts +64 -0
- package/dist/core/schema/schema.d.ts +28 -0
- package/dist/core/symbols/kind.d.ts +1 -1
- package/dist/core/use-case/access.d.ts +12 -0
- package/dist/core/use-case/decorators.d.ts +40 -0
- package/dist/core/use-case/route.d.ts +18 -0
- package/dist/core/use-case/steps/types/check.interface.d.ts +214 -0
- package/dist/core/use-case/types/access.interface.d.ts +39 -0
- package/dist/core/use-case/types/route.interface.d.ts +66 -0
- package/dist/core/use-case/use-case.d.ts +52 -2
- package/dist/experimental.d.ts +13 -1
- package/dist/experimental.js +63 -25
- package/dist/index-3aae5jfq.js +2997 -0
- package/dist/index-hg1z2s2v.js +1055 -0
- package/dist/index-ny3as79r.js +4567 -0
- package/dist/index-xegv6a1p.js +4291 -0
- package/dist/index-y280ew0k.js +396 -0
- package/dist/index.d.ts +45 -49
- package/dist/index.js +110 -186
- package/dist/std/properties/password.d.ts +0 -2
- package/dist/std/record-plugins.d.ts +2 -2
- package/dist/std/resource.d.ts +31 -0
- package/dist/std/runtime.d.ts +6 -0
- package/dist/std/use-cases/auth.d.ts +5 -1
- package/dist/std/use-cases/index.d.ts +1 -1
- package/dist/std/use-cases/shared.d.ts +2 -2
- package/dist/std/use-cases/types.d.ts +97 -1
- package/dist/testing/http-provider-contract.d.ts +32 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +495 -4
- package/dist/testing/provider-contract.d.ts +17 -1
- package/package.json +16 -3
- package/dist/index-qxfkyd6k.js +0 -1721
- package/dist/index-t5qwtyj5.js +0 -8194
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import type { Exception } from "../exception/exception.js";
|
|
2
|
-
import type { Issue } from "./types/parse.interface.js";
|
|
3
|
-
/**
|
|
4
|
-
|
|
2
|
+
import type { Issue, IssueCode } from "./types/parse.interface.js";
|
|
3
|
+
/**
|
|
4
|
+
* What a step gives back, in place of a value, for a value it rejects; the
|
|
5
|
+
* issues are then found where the step says, so a valid value costs no result
|
|
6
|
+
* object.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare const REJECTED: unique symbol;
|
|
10
|
+
/** Who knows the exception an issue stands for, and builds it only when asked. */
|
|
11
|
+
export interface IssueOrigin {
|
|
12
|
+
exception(): Exception;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* An issue whose `cause` is built the first time it is read, by its origin,
|
|
16
|
+
* which keeps it: every issue of one origin has the same cause. Its own keys
|
|
17
|
+
* are those of any issue, `cause` is not one of them.
|
|
18
|
+
*/
|
|
19
|
+
export declare class LazyIssue implements Issue {
|
|
20
|
+
#private;
|
|
21
|
+
readonly message: string;
|
|
22
|
+
readonly path: readonly (string | number)[];
|
|
23
|
+
readonly code: IssueCode;
|
|
24
|
+
readonly source: string;
|
|
25
|
+
constructor(message: string, path: readonly (string | number)[], code: IssueCode, source: string, origin: IssueOrigin);
|
|
26
|
+
get cause(): Exception;
|
|
27
|
+
/** A copy of the issue at `path`, with the same cause. */
|
|
28
|
+
at(path: readonly (string | number)[]): LazyIssue;
|
|
29
|
+
}
|
|
5
30
|
/** A copy of `issue` at `path`, with the same `cause`, built lazily or not. */
|
|
6
31
|
export declare function movedIssue(issue: Issue, path: readonly (string | number)[]): Issue;
|
|
@@ -18,6 +18,15 @@ export declare function isPlainObject(value: unknown): value is Record<string, u
|
|
|
18
18
|
* is parsing it, e.g. a record name.
|
|
19
19
|
*/
|
|
20
20
|
export declare function parseCollection(properties: IBlueprintCollection, value: unknown, path: Path, source: string): ParseResult<Readonly<Record<string, unknown>>>;
|
|
21
|
+
/**
|
|
22
|
+
* `parseCollection` at the root, as a function of the value alone: what it
|
|
23
|
+
* needs of `properties` is looked up on the first value and kept, instead of
|
|
24
|
+
* on every one. For what parses many values of one shape, a blueprint or a
|
|
25
|
+
* record class. The values of the keys in `vouched` are kept as they are,
|
|
26
|
+
* unchecked: for what the caller made itself and knows to be valid.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export declare function collectionParserOf(properties: IBlueprintCollection, source: string): (value: unknown, vouched?: readonly string[]) => ParseResult<Readonly<Record<string, unknown>>>;
|
|
21
30
|
/**
|
|
22
31
|
* `value` handled by `field`, or every issue found: a property runs its
|
|
23
32
|
* handlers, a blueprint each of its fields, a list each of its items, and a
|
|
@@ -9,6 +9,15 @@ import type { AnyPropertyConstructor } from "./types/property-collection.type.js
|
|
|
9
9
|
import type { PropertyValue } from "./types/property-constructor.type.js";
|
|
10
10
|
import type { PropertyOptions } from "./types/property-options.interface.js";
|
|
11
11
|
import type { IPropertyType } from "./types/property-type.interface.js";
|
|
12
|
+
/**
|
|
13
|
+
* The definition `Class` parses with. It does not depend on any member the
|
|
14
|
+
* constructor sets, and `parse` leaves nothing on it that the next one reads,
|
|
15
|
+
* so every `safeParse` shares one.
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export declare function parsingDefinitionOf(Class: {
|
|
19
|
+
prototype: object;
|
|
20
|
+
}): AbstractDefineProperty<TSchema>;
|
|
12
21
|
/** A property instance; `Value` is the value of its schema, as in `ISchema`. */
|
|
13
22
|
export declare abstract class AbstractProperty<SchemaType extends TSchema, Value = Static<SchemaType>> implements IProperty<SchemaType, Value> {
|
|
14
23
|
get [State](): PropertyStates;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type { Static, TSchema } from "typebox";
|
|
2
|
-
import type { ParseResult } from "../parse/types/parse.interface.js";
|
|
2
|
+
import type { Issue, ParseResult } from "../parse/types/parse.interface.js";
|
|
3
3
|
import { State } from "../symbols/state.js";
|
|
4
4
|
import { PropertyStates } from "./enums/property-states.js";
|
|
5
5
|
import type { IDefineProperty } from "./types/define-property.interface.js";
|
|
6
6
|
import type { IPropertyType } from "./types/property-type.interface.js";
|
|
7
|
+
/**
|
|
8
|
+
* Adds to `issues` those of the value a pipeline has just rejected, each
|
|
9
|
+
* under `path`, where the value is. An issue of the value itself has `path`
|
|
10
|
+
* as it is, not a copy.
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare function takeRejectionIssues(path: readonly (string | number)[], issues: Issue[]): void;
|
|
7
14
|
/** How a property handles its value; `Value` is the value of its schema, as in `ISchema`. */
|
|
8
15
|
export declare abstract class AbstractDefineProperty<SchemaType extends TSchema, Value = Static<SchemaType>> implements IDefineProperty<SchemaType, Value> {
|
|
9
16
|
[State]: PropertyStates;
|
|
@@ -27,9 +34,13 @@ export declare abstract class AbstractDefineProperty<SchemaType extends TSchema,
|
|
|
27
34
|
protected afterHandle(value: Value): Value;
|
|
28
35
|
/** The value, or `MISMATCH` when the schema rejects it. */
|
|
29
36
|
protected handle(value: Value): Value;
|
|
30
|
-
/** `attempt`, with the error of any step as a
|
|
37
|
+
/** `attempt`, with the error of any step as a rejection. */
|
|
31
38
|
private attemptWithoutThrowing;
|
|
32
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Runs the pipeline, updating the state: the handled value, or `REJECTED`
|
|
41
|
+
* with the reason in `rejection`. A handler that is not declared is not
|
|
42
|
+
* run, and neither is the shape check that protects it.
|
|
43
|
+
*/
|
|
33
44
|
private attempt;
|
|
34
45
|
/**
|
|
35
46
|
* Runs a handler: an `Exception` it throws goes up as it is, any other
|
|
@@ -38,11 +49,28 @@ export declare abstract class AbstractDefineProperty<SchemaType extends TSchema,
|
|
|
38
49
|
private runHandler;
|
|
39
50
|
/** The `InvalidPropertyValue` of an error that is not an `Exception`, as its cause. */
|
|
40
51
|
private invalidValue;
|
|
41
|
-
/**
|
|
52
|
+
/** Rejects the value with an `exception` that a step threw. */
|
|
42
53
|
private raise;
|
|
43
54
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
55
|
+
* Rejects `value`, which the schema rejects, with its errors, found once
|
|
56
|
+
* and kept for the details of the exception too.
|
|
46
57
|
*/
|
|
47
58
|
private mismatch;
|
|
48
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Tells that the definitions of `prototype` are as `propertyOf` declares them.
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
export declare function declarePipeline(prototype: object): void;
|
|
65
|
+
/**
|
|
66
|
+
* `definition.parse` without its result object: a function giving the handled
|
|
67
|
+
* value, or `REJECTED`, with the issues for `takeRejectionIssues` to take.
|
|
68
|
+
* `undefined` when `definition` has a `parse` of its own, which must be
|
|
69
|
+
* called instead.
|
|
70
|
+
*
|
|
71
|
+
* For a definition declared by `propertyOf`, the function is the pipeline of
|
|
72
|
+
* `attempt` with what never changes read once: which steps there are, the
|
|
73
|
+
* schema, and no state, which nobody can read.
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
export declare function parserOf(definition: AbstractDefineProperty<TSchema>): ((value: unknown) => unknown) | undefined;
|
|
@@ -14,6 +14,16 @@ export interface PropertyMetadata<Value> {
|
|
|
14
14
|
readonly writeOnly?: boolean;
|
|
15
15
|
/** The demo value, as `default` in `propertyOf`; it is not written to the schema. */
|
|
16
16
|
readonly default?: Value;
|
|
17
|
+
/**
|
|
18
|
+
* The value a repository gives the key when a stored row lacks it, on read
|
|
19
|
+
* and in a backfill (`app.migrate`); unlike `default`, it fills real data.
|
|
20
|
+
*/
|
|
21
|
+
readonly fill?: Value;
|
|
22
|
+
/**
|
|
23
|
+
* The key that held the value in rows stored before the key was renamed:
|
|
24
|
+
* a repository moves it on read and in a backfill.
|
|
25
|
+
*/
|
|
26
|
+
readonly renamedFrom?: string;
|
|
17
27
|
}
|
|
18
28
|
/**
|
|
19
29
|
* Bounds of a number. They only narrow the property: a bound looser than one
|
|
@@ -15,6 +15,10 @@ export interface IPropertyType<SchemaType extends TSchema, Value = Static<Schema
|
|
|
15
15
|
readonly sensitive?: boolean;
|
|
16
16
|
/** Whether a record lists a key holding it in `options.unique`, for repositories to enforce. */
|
|
17
17
|
readonly unique?: boolean;
|
|
18
|
+
/** `fill` of `.with()`: the value given to the key when a stored row lacks it. */
|
|
19
|
+
readonly fill?: Value;
|
|
20
|
+
/** `renamedFrom` of `.with()`: the key that held the value in older rows. */
|
|
21
|
+
readonly renamedFrom?: string;
|
|
18
22
|
}
|
|
19
23
|
/**
|
|
20
24
|
* The type of a property declared `unique`, as `Slug`, so that the keys
|
|
@@ -15,6 +15,13 @@ interface RecordDeclaration<Properties extends IBlueprintCollection> {
|
|
|
15
15
|
export declare const RESERVED_RECORD_KEYS: readonly string[];
|
|
16
16
|
/** Makes each of `keys` a member of the records of `prototype`, e.g. `user.name`. */
|
|
17
17
|
export declare function exposeRecordKeys(prototype: object, keys: readonly string[]): void;
|
|
18
|
+
/**
|
|
19
|
+
* Tells the record whose constructor runs next that the values of `keys` are
|
|
20
|
+
* valid as they are. For a class that makes values for its own record on the
|
|
21
|
+
* way to `super()`, as an entity does of its id.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export declare function vouchFor(keys: readonly string[]): void;
|
|
18
25
|
/** How `recordDataOf` stores the records a record holds. */
|
|
19
26
|
export interface RecordDataOptions {
|
|
20
27
|
/**
|
|
@@ -120,7 +120,18 @@ type JsonField<Field, Data> = Field extends {
|
|
|
120
120
|
readonly properties: infer Properties extends IBlueprintCollection;
|
|
121
121
|
} ? Data extends object ? JsonKeys<Properties, Data, never> : Data : Field extends {
|
|
122
122
|
readonly item: infer Item;
|
|
123
|
-
} ? Data extends readonly (infer ItemData)[] ? JsonField<Item, ItemData>[] : Data :
|
|
123
|
+
} ? Data extends readonly (infer ItemData)[] ? JsonField<Item, ItemData>[] : Data : Field extends {
|
|
124
|
+
readonly key: infer Key extends string;
|
|
125
|
+
readonly variants: infer Variants;
|
|
126
|
+
} ? JsonVariant<Key, Variants, Data> : Data;
|
|
127
|
+
/** What `toJSON` gives for the value `Data` of a variant field: each variant by its own blueprint. */
|
|
128
|
+
type JsonVariant<Key extends string, Variants, Data> = Data extends {
|
|
129
|
+
readonly [Name in Key]: infer Tag extends keyof Variants;
|
|
130
|
+
} ? Variants[Tag] extends AnyRecordClass ? {
|
|
131
|
+
readonly [Name in Key]: Tag;
|
|
132
|
+
} & JsonOfRecord<Variants[Tag], Data> : Variants[Tag] extends {
|
|
133
|
+
readonly properties: infer Properties extends IBlueprintCollection;
|
|
134
|
+
} ? JsonKeys<Properties, Data, never> : Data : Data;
|
|
124
135
|
/** What the `toJSON` of the records of `Class` gives, in place of their plain value. */
|
|
125
136
|
type JsonOfRecord<Class extends AnyRecordClass, Data> = InstanceType<Class> extends {
|
|
126
137
|
toJSON(): infer Json;
|
|
@@ -18,6 +18,19 @@ export declare const InvalidRepositoryDeclaration: import("../../index.js").Decl
|
|
|
18
18
|
}>;
|
|
19
19
|
/** A method of a repository got a value that is not an entity of it. */
|
|
20
20
|
export declare const InvalidRepositoryEntity: import("../../index.js").DeclaredExceptionClass<undefined>;
|
|
21
|
+
/**
|
|
22
|
+
* A stored row holds a schema version (`SCHEMA_VERSION_KEY`) that is not a
|
|
23
|
+
* positive integer, or that is above the `schemaVersion` of its entity (it
|
|
24
|
+
* was written by newer code), or an `upgrade` step did not give an object.
|
|
25
|
+
* `source` is the repository name; reads throw it as the cause of
|
|
26
|
+
* `InvalidRepositoryData`.
|
|
27
|
+
*/
|
|
28
|
+
export declare const InvalidStoredSchemaVersion: import("../../index.js").DeclaredExceptionClass<{
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly stored: unknown;
|
|
31
|
+
readonly current: number;
|
|
32
|
+
readonly reason?: string;
|
|
33
|
+
}>;
|
|
21
34
|
/** A provider gave a value its entity rejects; `source` is the repository name. */
|
|
22
35
|
export declare const InvalidRepositoryData: import("../../index.js").DeclaredExceptionClass<undefined>;
|
|
23
36
|
/** Another entity holds the value of a unique key; `source` is the repository name. */
|
|
@@ -13,8 +13,8 @@ export type InMemoryProvider = IRepositoryProvider & {
|
|
|
13
13
|
};
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
|
-
* A provider that keeps the values in memory, one table per entity
|
|
17
|
-
* enforcing its unique keys. Each `repositoryOf` without a provider gets its
|
|
16
|
+
* A provider that keeps the values in memory, one table per entity name it
|
|
17
|
+
* declares (or per entity class, without one), enforcing its unique keys. Each `repositoryOf` without a provider gets its
|
|
18
18
|
* own; repositories given the same one share the tables of their entities.
|
|
19
19
|
* Its `transaction` works on isolated snapshots: commits on success, discards
|
|
20
20
|
* on rejection, runs one at a time and makes other operations wait; nested
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type RepositoryState } from "./state.js";
|
|
2
|
+
import type { EntityMigrationReport, SchemaPlan } from "./types/schema.interface.js";
|
|
3
|
+
/** How `migrateRepository` walks one repository. */
|
|
4
|
+
export interface RepositoryMigration {
|
|
5
|
+
readonly mode: "check" | "apply";
|
|
6
|
+
readonly destructive: boolean;
|
|
7
|
+
readonly pageSize: number;
|
|
8
|
+
/** Given to `provider.migrate` in `apply`, before the rows are walked. */
|
|
9
|
+
readonly plan: SchemaPlan;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Brings the stored rows of the repository of `state` up to the current
|
|
13
|
+
* shape of its entity (see `app.migrate`): in `apply`, first gives
|
|
14
|
+
* `options.plan` to `provider.migrate`, then rewrites the rows below the
|
|
15
|
+
* current version or with a `renamedFrom` or `fill` pending, and moves the
|
|
16
|
+
* rows of each previous name. Keys no longer declared, and the rows of the
|
|
17
|
+
* previous names, are only removed when `destructive`. Idempotent: a second
|
|
18
|
+
* run finds every valid row up to date.
|
|
19
|
+
*/
|
|
20
|
+
export declare function migrateRepository(state: RepositoryState, options: RepositoryMigration): Promise<EntityMigrationReport>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SCHEMA_VERSION_KEY } from "../entity/abstract-entity.js";
|
|
1
2
|
import { type EnvironmentDependencies } from "../environment/environment.js";
|
|
2
3
|
import type { ILogger } from "../logger/logger.js";
|
|
3
4
|
import type { RepositoryIndex, RepositoryScans } from "./types/query.interface.js";
|
|
@@ -27,6 +28,7 @@ export declare const states: WeakMap<object, RepositoryState>;
|
|
|
27
28
|
export declare function findState(repository: unknown): RepositoryState | undefined;
|
|
28
29
|
/** The state of the repository class of `repository`, or of a class it extends. */
|
|
29
30
|
export declare function stateOf(repository: object): RepositoryState;
|
|
31
|
+
export { SCHEMA_VERSION_KEY };
|
|
30
32
|
/** The context of the provider, with the environment read on first use. */
|
|
31
33
|
export declare function contextOf(state: RepositoryState): RepositoryContext;
|
|
32
34
|
/** What an application reads from a repository, without reaching its state. */
|
|
@@ -4,6 +4,7 @@ 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
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[];
|
|
@@ -154,6 +155,12 @@ export interface ProviderIncrementOptions {
|
|
|
154
155
|
*/
|
|
155
156
|
readonly now?: string;
|
|
156
157
|
}
|
|
158
|
+
/** `RepositoryContext.schema`. */
|
|
159
|
+
export interface RepositorySchema {
|
|
160
|
+
readonly version: number;
|
|
161
|
+
readonly key: string;
|
|
162
|
+
readonly previousNames: readonly string[];
|
|
163
|
+
}
|
|
157
164
|
/** What a provider gets with every call: whose entities, and the environment. */
|
|
158
165
|
export interface RepositoryContext<Environment = unknown> {
|
|
159
166
|
/** The entity class; name a table after `recordNameOf(entity)`, not `entity.name`. */
|
|
@@ -180,6 +187,13 @@ export interface RepositoryContext<Environment = unknown> {
|
|
|
180
187
|
readonly expiry?: {
|
|
181
188
|
readonly key: string;
|
|
182
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;
|
|
183
197
|
/** The variables of the `environmentDependencies` of the repository. */
|
|
184
198
|
readonly env: Environment;
|
|
185
199
|
}
|
|
@@ -189,6 +203,14 @@ export interface RepositoryContext<Environment = unknown> {
|
|
|
189
203
|
* `RepositoryUniqueViolation`; `update` and `remove` throw
|
|
190
204
|
* `RepositoryEntityNotFound` for an unknown id.
|
|
191
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
|
+
};
|
|
192
214
|
export interface IRepositoryProvider<Environment = unknown> {
|
|
193
215
|
findOne(context: RepositoryContext<Environment>, query?: ProviderQuery): Promise<RepositoryData | undefined>;
|
|
194
216
|
/** `query.after` is only given to a provider with `capabilities.keyset`. */
|
|
@@ -218,5 +240,16 @@ export interface IRepositoryProvider<Environment = unknown> {
|
|
|
218
240
|
transaction?<Result>(run: (provider: IRepositoryProvider<Environment>) => Promise<Result>): Promise<Result>;
|
|
219
241
|
/** Releases what the provider holds (connections, files); `app.stop()` calls it. */
|
|
220
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>;
|
|
221
254
|
}
|
|
222
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
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { EntityUpgrades } from "../entity/types/entity.interface.js";
|
|
2
|
+
import type { RepositoryState } from "./state.js";
|
|
3
|
+
import type { RepositoryData } from "./types/repository.interface.js";
|
|
4
|
+
/** What happens to the value of one key of a collection. */
|
|
5
|
+
type FieldStep = {
|
|
6
|
+
readonly kind: "property";
|
|
7
|
+
readonly fill?: unknown;
|
|
8
|
+
readonly renamedFrom?: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: "collection";
|
|
11
|
+
readonly fields: CollectionSteps;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: "list";
|
|
14
|
+
readonly item: FieldStep;
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: "opaque";
|
|
17
|
+
};
|
|
18
|
+
/** The steps of the keys of a blueprint, and whether any of them does something. */
|
|
19
|
+
interface CollectionSteps {
|
|
20
|
+
readonly fields: ReadonlyMap<string, FieldStep>;
|
|
21
|
+
}
|
|
22
|
+
/** How the rows of an entity are brought up to its current shape. */
|
|
23
|
+
export interface UpcastPlan {
|
|
24
|
+
readonly versioned: boolean;
|
|
25
|
+
readonly version: number;
|
|
26
|
+
/** Whether the entity lists `previousNames`: its moved rows may hold old keys. */
|
|
27
|
+
readonly renamed: boolean;
|
|
28
|
+
readonly upgrade: EntityUpgrades;
|
|
29
|
+
readonly fields: CollectionSteps;
|
|
30
|
+
}
|
|
31
|
+
/** What `upcast` did to a row, for `app.migrate`. */
|
|
32
|
+
export interface UpcastReport {
|
|
33
|
+
/** The version the row was stored at (1 without the version key). */
|
|
34
|
+
from: number;
|
|
35
|
+
/** Paths of the keys moved from their `renamedFrom`. */
|
|
36
|
+
readonly renamed: string[];
|
|
37
|
+
/** Paths of the keys given their `fill`. */
|
|
38
|
+
readonly filled: string[];
|
|
39
|
+
/** Paths of the keys the blueprint no longer declares. */
|
|
40
|
+
readonly removed: string[];
|
|
41
|
+
}
|
|
42
|
+
/** The plan of `entity`, even when it changes nothing but the keys it no longer declares. */
|
|
43
|
+
export declare function fullUpcastPlanOf(entity: object): UpcastPlan;
|
|
44
|
+
/**
|
|
45
|
+
* `data`, a row of an entity of `plan` as a provider gave it, in the current
|
|
46
|
+
* shape: without the version key, through each `upgrade` step from the
|
|
47
|
+
* version after the stored one up to `schemaVersion`, then the automatic
|
|
48
|
+
* steps of the current blueprint (`renamedFrom`, `fill`, and, with `drop`,
|
|
49
|
+
* leaving out keys it no longer declares). The steps run first because each
|
|
50
|
+
* is written against the shape of the version before it, which the
|
|
51
|
+
* automatic steps would already have changed. Throws
|
|
52
|
+
* `InvalidStoredSchemaVersion` for a version that is not a positive integer
|
|
53
|
+
* or is above `schemaVersion`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function upcast(source: string, plan: UpcastPlan, data: RepositoryData, drop?: boolean, report?: UpcastReport): RepositoryData;
|
|
56
|
+
/**
|
|
57
|
+
* `rows` of `state`'s entity in its current shape (see `upcast`); `rows`
|
|
58
|
+
* itself when the entity needs nothing. Throws `InvalidRepositoryData`
|
|
59
|
+
* caused by `InvalidStoredSchemaVersion` for a row it cannot bring up.
|
|
60
|
+
*/
|
|
61
|
+
export declare function upcastRows(state: RepositoryState, rows: readonly RepositoryData[]): readonly RepositoryData[];
|
|
62
|
+
/** `data` with the version key of `state`'s entity, when it declares `schemaVersion`. */
|
|
63
|
+
export declare function withSchemaVersion(entity: object, data: RepositoryData): RepositoryData;
|
|
64
|
+
export {};
|
|
@@ -5,6 +5,34 @@ export declare function describeError(error: {
|
|
|
5
5
|
instancePath: string;
|
|
6
6
|
message: string;
|
|
7
7
|
}): string;
|
|
8
|
+
/** One TypeBox error, as `Errors` gives it. */
|
|
9
|
+
export interface SchemaError {
|
|
10
|
+
readonly keyword: string;
|
|
11
|
+
readonly schemaPath: string;
|
|
12
|
+
readonly instancePath: string;
|
|
13
|
+
readonly params: object;
|
|
14
|
+
readonly message: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The errors of `value`, which `schema` rejects. For every schema but one
|
|
18
|
+
* they are what `schema.type.Errors(value)` gives: same errors, same order,
|
|
19
|
+
* same messages. The one is a discriminated union given an object, told by
|
|
20
|
+
* the variant its key names (see `errorsOfVariant`). TypeBox is only asked
|
|
21
|
+
* about a schema with a keyword that has no explanation here (`explainerOf`).
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export declare function errorsOfSchema(schema: ISchema<TSchema>, value: unknown): readonly SchemaError[];
|
|
25
|
+
/** The two checks of a schema, as functions. */
|
|
26
|
+
export interface SchemaChecks {
|
|
27
|
+
match(value: unknown): boolean;
|
|
28
|
+
matchShape(value: unknown): boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `schema.match` and `schema.matchShape`, compiled now, for who checks many
|
|
32
|
+
* values with one schema; those of a schema not made by `schemaOf` call it.
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export declare function checksOfSchema(schema: ISchema<TSchema>): SchemaChecks;
|
|
8
36
|
/** A schema class declared with `schemaOf`. */
|
|
9
37
|
export interface SchemaClass<SchemaType extends TSchema> {
|
|
10
38
|
new (type?: SchemaType): ISchema<SchemaType>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/** Brands blueprints, lists and records, so none is told apart by its shape. */
|
|
1
|
+
/** Brands blueprints, lists, variants and records, so none is told apart by its shape. */
|
|
2
2
|
export declare const Kind: unique symbol;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AnyBlueprint } from "../blueprint/types/blueprint.interface.js";
|
|
2
|
+
import type { AccessDeclaration } from "./types/access.interface.js";
|
|
3
|
+
/** Whether `access` lets anyone in. */
|
|
4
|
+
export declare function isPublicAccess(access: AccessDeclaration): access is {
|
|
5
|
+
readonly public: true;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The frozen declaration of `access` for a use case whose input is
|
|
9
|
+
* `blueprint`, or the reason it is not valid. The keys of `context` are
|
|
10
|
+
* checked where the context is known: when the use case becomes an operation.
|
|
11
|
+
*/
|
|
12
|
+
export declare function accessDeclarationOf(access: unknown, blueprint: AnyBlueprint): AccessDeclaration | string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AccessDeclaration } from "./types/access.interface.js";
|
|
2
|
+
import type { RouteSettings } from "./types/route.interface.js";
|
|
3
|
+
/** What a decorator of this module is given besides the class: nothing it reads. */
|
|
4
|
+
type ClassContext = {
|
|
5
|
+
readonly kind: string;
|
|
6
|
+
};
|
|
7
|
+
/** A class decorator for use cases, in the standard form and in the legacy one of TypeScript. */
|
|
8
|
+
export type UseCaseDecorator = <UseCase extends abstract new (...args: any[]) => unknown>(value: UseCase, context?: ClassContext) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Says where a use case is served over HTTP, on the class itself:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* @Route("GET", "/posts/{slug}", { query: ["page"], cookie: { session: "sid" } })
|
|
14
|
+
* class ListPosts extends useCaseOf({ name: "ListPosts", blueprint, response }) {}
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* The same as the `route` setting of `useCaseOf` (see `RouteDeclaration`), so
|
|
18
|
+
* `describe()`, the sinks and `afora.json` see one declaration. It names no
|
|
19
|
+
* server: the sink of an output does. Throws `InvalidUseCaseDeclaration` on
|
|
20
|
+
* a class that is not a use case, on one that already has another route
|
|
21
|
+
* (the same one again is accepted), and for a route that does not fit the
|
|
22
|
+
* input of the use case.
|
|
23
|
+
*/
|
|
24
|
+
export declare function Route(method: RouteSettings["method"], path?: string, options?: Omit<RouteSettings, "method" | "path">): UseCaseDecorator;
|
|
25
|
+
export declare function Route(method: RouteSettings["method"], options?: Omit<RouteSettings, "method" | "path">): UseCaseDecorator;
|
|
26
|
+
/**
|
|
27
|
+
* Says who may call a use case, on the class itself:
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* @Access({ scopes: ["posts:write"], context: { userId: "subject" } })
|
|
31
|
+
* class PublishPost extends useCaseOf({ name: "PublishPost", blueprint, response, context }) {}
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* The same as the `access` setting of `useCaseOf` (see `AccessDeclaration`).
|
|
35
|
+
* Throws `InvalidUseCaseDeclaration` on a class that is not a use case, on
|
|
36
|
+
* one that already declares another access (the same one again is accepted),
|
|
37
|
+
* and for a declaration that does not fit the input of the use case.
|
|
38
|
+
*/
|
|
39
|
+
export declare function Access(access: AccessDeclaration): UseCaseDecorator;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AnyBlueprint } from "../blueprint/types/blueprint.interface.js";
|
|
2
|
+
import type { OpenAPISchema } from "../openapi/types/openapi.interface.js";
|
|
3
|
+
import type { RouteDeclaration } from "./types/route.interface.js";
|
|
4
|
+
/** A header name (an HTTP token), which a cookie name is too. */
|
|
5
|
+
export declare const HEADER_NAME: RegExp;
|
|
6
|
+
/** A path such as `/entries/{slug}`: `{name}` only as a whole segment, no `:`, `*`, query or spaces. */
|
|
7
|
+
export declare function isRoutePath(value: unknown, parameters?: boolean): value is string;
|
|
8
|
+
/** The names of the `{name}` segments of `path`, in order. */
|
|
9
|
+
export declare function parametersOfPath(path: string): string[];
|
|
10
|
+
/** Whether a JSON Schema describes a value that is neither an object nor a list. */
|
|
11
|
+
export declare function isScalarSchema(schema: OpenAPISchema): boolean;
|
|
12
|
+
/** Whether `name` is a key of `blueprint` that is a scalar property (no object, no list). */
|
|
13
|
+
export declare function isScalarKey(blueprint: AnyBlueprint, name: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The frozen, normalized declaration of `route` for a use case whose input is
|
|
16
|
+
* `blueprint` (the method in lower case), or the reason it is not valid.
|
|
17
|
+
*/
|
|
18
|
+
export declare function routeDeclarationOf(route: unknown, blueprint: AnyBlueprint, response: AnyBlueprint): RouteDeclaration | string;
|