@afora/core 0.0.8 → 0.0.11
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 +149 -23
- package/dist/adapters.d.ts +25 -0
- package/dist/adapters.js +32 -0
- package/dist/core/application/application.d.ts +30 -0
- package/dist/core/application/application.js +1291 -0
- package/dist/core/application/exceptions.d.ts +17 -0
- package/dist/core/application/exceptions.js +85 -0
- package/dist/core/application/migrate.d.ts +19 -0
- package/dist/core/application/migrate.js +67 -0
- package/dist/core/application/providers.js +108 -0
- package/dist/core/application/registry.js +23 -0
- package/dist/core/application/schema.d.ts +30 -0
- package/dist/core/application/schema.js +398 -0
- package/dist/core/application/start.js +162 -0
- package/dist/core/application/types/application.interface.d.ts +60 -3
- package/dist/core/auth/jwt.d.ts +30 -0
- package/dist/core/auth/jwt.js +200 -0
- package/dist/core/auth/types/jwt.interface.d.ts +74 -0
- package/dist/core/blueprint/blueprint-plugin.js +7 -0
- package/dist/core/blueprint/blueprint.js +146 -0
- package/dist/core/blueprint/callable.js +13 -0
- package/dist/core/blueprint/exceptions.d.ts +5 -0
- package/dist/core/blueprint/exceptions.js +20 -0
- package/dist/core/blueprint/is-blueprint.js +4 -0
- package/dist/core/blueprint/list.js +19 -0
- package/dist/core/blueprint/optional.d.ts +3 -1
- package/dist/core/blueprint/optional.js +69 -0
- package/dist/core/blueprint/record-field.js +14 -0
- package/dist/core/blueprint/slice.js +127 -0
- package/dist/core/blueprint/types/blueprint.interface.d.ts +95 -5
- package/dist/core/blueprint/variant.d.ts +32 -0
- package/dist/core/blueprint/variant.js +72 -0
- package/dist/core/command/command.js +428 -0
- package/dist/core/command/exceptions.js +30 -0
- package/dist/core/command/types/command.interface.d.ts +10 -1
- package/dist/core/entity/abstract-entity.d.ts +24 -0
- package/dist/core/entity/abstract-entity.js +181 -0
- package/dist/core/entity/entity.d.ts +1 -1
- package/dist/core/entity/entity.js +209 -0
- package/dist/core/entity/event.d.ts +39 -0
- package/dist/core/entity/event.js +80 -0
- package/dist/core/entity/exceptions.d.ts +9 -0
- package/dist/core/entity/exceptions.js +27 -0
- package/dist/core/entity/trust.d.ts +24 -0
- package/dist/core/entity/trust.js +7 -0
- package/dist/core/entity/types/entity.interface.d.ts +30 -0
- package/dist/core/entity/version.js +13 -0
- package/dist/core/environment/environment.d.ts +26 -3
- package/dist/core/environment/environment.js +71 -0
- package/dist/core/event-policy/event-policy.js +76 -0
- package/dist/core/event-policy/exceptions.js +12 -0
- package/dist/core/event-policy/publish.js +98 -0
- package/dist/core/event-policy/publishers.js +18 -0
- package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
- package/dist/core/exception/enums/exception-layers.js +15 -0
- package/dist/core/exception/exception.d.ts +15 -0
- package/dist/core/exception/exception.js +128 -0
- package/dist/core/exception/exported.js +49 -0
- package/dist/core/exception/guard.d.ts +20 -1
- package/dist/core/exception/guard.js +16 -0
- package/dist/core/exception/types/exception.interface.d.ts +13 -0
- package/dist/core/json.js +19 -0
- package/dist/core/logger/exceptions.js +7 -0
- package/dist/core/logger/logger.d.ts +14 -1
- package/dist/core/logger/logger.js +178 -0
- package/dist/core/openapi/exceptions.js +7 -0
- package/dist/core/openapi/openapi.js +132 -0
- package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
- package/dist/core/output/access.d.ts +16 -0
- package/dist/core/output/access.js +66 -0
- package/dist/core/output/coerce.js +38 -0
- package/dist/core/output/exceptions.d.ts +16 -0
- package/dist/core/output/exceptions.js +22 -0
- package/dist/core/output/http.d.ts +36 -0
- package/dist/core/output/http.js +351 -0
- package/dist/core/output/openapi-sink.d.ts +33 -4
- package/dist/core/output/openapi-sink.js +571 -0
- package/dist/core/output/output.js +192 -0
- package/dist/core/output/problem.js +30 -0
- package/dist/core/output/select.js +28 -0
- package/dist/core/output/stage.d.ts +7 -2
- package/dist/core/output/stage.js +111 -0
- package/dist/core/output/status.d.ts +1 -1
- package/dist/core/output/status.js +52 -0
- package/dist/core/output/types/http.interface.d.ts +134 -0
- package/dist/core/output/types/output.interface.d.ts +49 -5
- package/dist/core/parse/issue.d.ts +28 -3
- package/dist/core/parse/issue.js +31 -0
- package/dist/core/parse/parse.d.ts +16 -0
- package/dist/core/parse/parse.js +323 -0
- package/dist/core/parse/strip.js +48 -0
- package/dist/core/property/abstract-property.d.ts +9 -0
- package/dist/core/property/abstract-property.js +37 -0
- package/dist/core/property/define-property.d.ts +34 -6
- package/dist/core/property/define-property.js +209 -0
- package/dist/core/property/enums/property-states.js +8 -0
- package/dist/core/property/exceptions.js +12 -0
- package/dist/core/property/input.js +10 -0
- package/dist/core/property/is-property.js +3 -0
- package/dist/core/property/lineage.js +8 -0
- package/dist/core/property/property.d.ts +27 -0
- package/dist/core/property/property.js +46 -0
- package/dist/core/property/refine.js +103 -0
- 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 +8 -0
- package/dist/core/record/abstract-record.js +307 -0
- package/dist/core/record/exceptions.js +49 -0
- package/dist/core/record/marks.js +7 -0
- package/dist/core/record/mask.js +60 -0
- package/dist/core/record/name.js +23 -0
- package/dist/core/record/plugin-catalog.js +115 -0
- package/dist/core/record/plugin.js +198 -0
- package/dist/core/record/record.js +117 -0
- package/dist/core/record/types/record.interface.d.ts +17 -1
- package/dist/core/repository/cursor.js +83 -0
- package/dist/core/repository/exceptions.d.ts +13 -0
- package/dist/core/repository/exceptions.js +72 -0
- package/dist/core/repository/in-memory.d.ts +2 -2
- package/dist/core/repository/in-memory.js +196 -0
- package/dist/core/repository/migrate.d.ts +20 -0
- package/dist/core/repository/migrate.js +254 -0
- package/dist/core/repository/plan.js +27 -0
- package/dist/core/repository/provider.js +29 -0
- package/dist/core/repository/query.js +153 -0
- package/dist/core/repository/references.js +196 -0
- package/dist/core/repository/repository.d.ts +22 -2
- package/dist/core/repository/repository.js +720 -0
- package/dist/core/repository/state.d.ts +2 -0
- package/dist/core/repository/state.js +138 -0
- package/dist/core/repository/types/query.interface.d.ts +13 -0
- package/dist/core/repository/types/repository.interface.d.ts +50 -5
- package/dist/core/repository/types/schema.interface.d.ts +132 -0
- package/dist/core/repository/upcast.d.ts +64 -0
- package/dist/core/repository/upcast.js +179 -0
- package/dist/core/schema/schema.d.ts +37 -0
- package/dist/core/schema/schema.js +542 -0
- package/dist/core/standard/standard-schema.js +1 -0
- package/dist/core/standard/standard.js +23 -0
- package/dist/core/symbols/demo.d.ts +4 -0
- package/dist/core/symbols/demo.js +1 -0
- package/dist/core/symbols/kind.d.ts +1 -1
- package/dist/core/symbols/kind.js +1 -0
- package/dist/core/symbols/meta.js +1 -0
- package/dist/core/symbols/state.d.ts +5 -0
- package/dist/core/symbols/state.js +1 -0
- package/dist/core/tracing/exceptions.d.ts +4 -0
- package/dist/core/tracing/exceptions.js +7 -0
- package/dist/core/tracing/tracing.d.ts +72 -0
- package/dist/core/tracing/tracing.js +101 -0
- package/dist/core/use-case/access.d.ts +12 -0
- package/dist/core/use-case/access.js +56 -0
- package/dist/core/use-case/decorators.d.ts +40 -0
- package/dist/core/use-case/decorators.js +65 -0
- package/dist/core/use-case/exceptions.d.ts +5 -1
- package/dist/core/use-case/exceptions.js +48 -0
- package/dist/core/use-case/plugin-catalog.js +68 -0
- package/dist/core/use-case/plugin.js +133 -0
- package/dist/core/use-case/route.d.ts +18 -0
- package/dist/core/use-case/route.js +200 -0
- package/dist/core/use-case/scope.d.ts +5 -0
- package/dist/core/use-case/scope.js +4 -0
- package/dist/core/use-case/steps/describe.d.ts +5 -1
- package/dist/core/use-case/steps/describe.js +24 -0
- package/dist/core/use-case/steps/references.js +105 -0
- package/dist/core/use-case/steps/run.js +139 -0
- package/dist/core/use-case/steps/types/check.interface.d.ts +233 -0
- package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
- package/dist/core/use-case/steps/validate.js +368 -0
- package/dist/core/use-case/target.js +100 -0
- package/dist/core/use-case/types/access.interface.d.ts +39 -0
- package/dist/core/use-case/types/route.interface.d.ts +86 -0
- package/dist/core/use-case/use-case.d.ts +127 -2
- package/dist/core/use-case/use-case.js +493 -0
- package/dist/experimental.d.ts +18 -1
- package/dist/experimental.js +18 -123
- package/dist/index.d.ts +49 -51
- package/dist/index.js +213 -443
- package/dist/std/blueprint-plugin.js +42 -0
- package/dist/std/blueprint.d.ts +21 -0
- package/dist/std/blueprint.js +3 -0
- package/dist/std/command.js +3 -0
- package/dist/std/entity.d.ts +20 -1
- package/dist/std/entity.js +30 -0
- package/dist/std/event-policies.js +3 -0
- package/dist/std/properties/contact.d.ts +3 -0
- package/dist/std/properties/contact.js +29 -0
- package/dist/std/properties/enum.js +26 -0
- package/dist/std/properties/exceptions.js +14 -0
- package/dist/std/properties/headless.js +45 -0
- package/dist/std/properties/identity.js +28 -0
- package/dist/std/properties/json.js +161 -0
- package/dist/std/properties/password.d.ts +0 -2
- package/dist/std/properties/password.js +79 -0
- package/dist/std/properties/primitives.d.ts +4 -0
- package/dist/std/properties/primitives.js +28 -0
- package/dist/std/properties/time.js +55 -0
- package/dist/std/record-plugins.d.ts +2 -2
- package/dist/std/record-plugins.js +73 -0
- package/dist/std/record.d.ts +20 -1
- package/dist/std/record.js +3 -0
- package/dist/std/resource.d.ts +31 -0
- package/dist/std/resource.js +39 -0
- package/dist/std/runtime.d.ts +6 -0
- package/dist/std/runtime.js +193 -0
- package/dist/std/stages.js +4 -0
- package/dist/std/use-cases/auth.d.ts +5 -1
- package/dist/std/use-cases/auth.js +193 -0
- package/dist/std/use-cases/crud.js +261 -0
- package/dist/std/use-cases/index.d.ts +1 -1
- package/dist/std/use-cases/index.js +12 -0
- package/dist/std/use-cases/lifecycle.js +97 -0
- package/dist/std/use-cases/shared.d.ts +2 -2
- package/dist/std/use-cases/shared.js +232 -0
- package/dist/std/use-cases/types.d.ts +102 -2
- package/dist/std/use-cases/types.js +0 -0
- package/dist/testing/http-provider-contract.d.ts +32 -0
- package/dist/testing/http-provider-contract.js +344 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +3 -944
- package/dist/testing/memory-publisher.js +12 -0
- package/dist/testing/provider-contract.d.ts +26 -1
- package/dist/testing/provider-contract.js +1071 -0
- package/package.json +23 -3
- package/dist/index-qxfkyd6k.js +0 -1721
- package/dist/index-t5qwtyj5.js +0 -8194
|
@@ -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. */
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EntitySettings,
|
|
3
|
+
EntityUniqueKeys,
|
|
4
|
+
EXPIRES_AT,
|
|
5
|
+
SCHEMA_VERSION_KEY
|
|
6
|
+
} from "../entity/abstract-entity.js";
|
|
7
|
+
import {
|
|
8
|
+
loadEnvironments
|
|
9
|
+
} from "../environment/environment.js";
|
|
10
|
+
import { RepositoryUsedOutsideApplication } from "./exceptions.js";
|
|
11
|
+
export const states = new WeakMap;
|
|
12
|
+
export function findState(repository) {
|
|
13
|
+
if (typeof repository !== "object" || repository === null)
|
|
14
|
+
return;
|
|
15
|
+
for (let Class = repository.constructor;typeof Class === "function"; Class = Object.getPrototypeOf(Class)) {
|
|
16
|
+
const state = states.get(Class);
|
|
17
|
+
if (state)
|
|
18
|
+
return state;
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
export function stateOf(repository) {
|
|
23
|
+
const state = findState(repository);
|
|
24
|
+
if (!state)
|
|
25
|
+
throw new TypeError("Expected a repository made by repositoryOf");
|
|
26
|
+
return state;
|
|
27
|
+
}
|
|
28
|
+
function expiryKeyOf(entity) {
|
|
29
|
+
const expires = entity[EntitySettings]?.expires;
|
|
30
|
+
if (expires === undefined)
|
|
31
|
+
return;
|
|
32
|
+
return "at" in expires ? expires.at : EXPIRES_AT;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { SCHEMA_VERSION_KEY };
|
|
36
|
+
function schemaOf(entity) {
|
|
37
|
+
const settings = entity[EntitySettings];
|
|
38
|
+
if (!settings?.schemaVersioned)
|
|
39
|
+
return;
|
|
40
|
+
return Object.freeze({
|
|
41
|
+
version: settings.schemaVersion,
|
|
42
|
+
key: SCHEMA_VERSION_KEY,
|
|
43
|
+
previousNames: settings.previousNames
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function contextOf(state) {
|
|
47
|
+
if (state.context)
|
|
48
|
+
return state.context;
|
|
49
|
+
const key = expiryKeyOf(state.entity);
|
|
50
|
+
const schema = schemaOf(state.entity);
|
|
51
|
+
state.context = Object.freeze({
|
|
52
|
+
entity: state.entity,
|
|
53
|
+
name: state.name,
|
|
54
|
+
unique: state.entity[EntityUniqueKeys],
|
|
55
|
+
indexes: state.indexes,
|
|
56
|
+
...key === undefined ? {} : { expiry: Object.freeze({ key }) },
|
|
57
|
+
...schema === undefined ? {} : { schema },
|
|
58
|
+
env: loadEnvironments(state.environment)
|
|
59
|
+
});
|
|
60
|
+
return state.context;
|
|
61
|
+
}
|
|
62
|
+
export function inspectRepository(repository) {
|
|
63
|
+
const state = findState(repository);
|
|
64
|
+
if (!state)
|
|
65
|
+
return;
|
|
66
|
+
const { environment } = state;
|
|
67
|
+
return {
|
|
68
|
+
entity: state.entity,
|
|
69
|
+
actions: state.actions,
|
|
70
|
+
deletion: state.deletion,
|
|
71
|
+
indexes: state.indexes,
|
|
72
|
+
scans: state.scans,
|
|
73
|
+
explicitProvider: state.explicitProvider,
|
|
74
|
+
provider: state.explicitProvider ? state.provider : undefined,
|
|
75
|
+
environments: environment === undefined ? [] : Array.isArray(environment) ? environment : [environment],
|
|
76
|
+
relations: [...state.relations.values()].map(declarationOf),
|
|
77
|
+
state: declarationOf(state)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const origins = new WeakMap;
|
|
81
|
+
function declarationOf(state) {
|
|
82
|
+
return origins.get(state) ?? state;
|
|
83
|
+
}
|
|
84
|
+
const declared = new WeakMap;
|
|
85
|
+
export function declareRepository(state, repository) {
|
|
86
|
+
declared.set(state, repository);
|
|
87
|
+
}
|
|
88
|
+
export function relatedRepositoryOf(repository, Entity) {
|
|
89
|
+
const related = findState(repository)?.relations.get(Entity);
|
|
90
|
+
return related ? declared.get(declarationOf(related)) : undefined;
|
|
91
|
+
}
|
|
92
|
+
function bindState(state, provider, instances, logger, bound = new Map) {
|
|
93
|
+
const done = bound.get(state);
|
|
94
|
+
if (done)
|
|
95
|
+
return done;
|
|
96
|
+
const copy = {
|
|
97
|
+
...state,
|
|
98
|
+
logger,
|
|
99
|
+
provider: state.explicitProvider ? instances?.get(state.provider) ?? state.provider : provider ?? state.provider,
|
|
100
|
+
context: undefined,
|
|
101
|
+
relations: new Map([...state.relations].map(([target, related]) => [
|
|
102
|
+
target,
|
|
103
|
+
bindState(related, provider, instances, logger, bound)
|
|
104
|
+
]))
|
|
105
|
+
};
|
|
106
|
+
origins.set(copy, declarationOf(state));
|
|
107
|
+
bound.set(state, copy);
|
|
108
|
+
return copy;
|
|
109
|
+
}
|
|
110
|
+
const owners = new WeakMap;
|
|
111
|
+
export function assertUnbound(state) {
|
|
112
|
+
const owner = owners.get(state);
|
|
113
|
+
if (owner) {
|
|
114
|
+
throw new RepositoryUsedOutsideApplication({
|
|
115
|
+
source: state.name,
|
|
116
|
+
details: owner
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function bindRepository(repository, provider, instances, binding) {
|
|
121
|
+
const state = stateOf(repository);
|
|
122
|
+
const Declared = repository.constructor;
|
|
123
|
+
|
|
124
|
+
class Bound extends Declared {
|
|
125
|
+
}
|
|
126
|
+
Object.defineProperty(Bound, "name", { value: Declared.name });
|
|
127
|
+
states.set(Bound, bindState(state, provider, instances, binding?.logger));
|
|
128
|
+
if (binding) {
|
|
129
|
+
owners.set(declarationOf(state), {
|
|
130
|
+
application: binding.application,
|
|
131
|
+
repository: binding.key
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return Object.freeze(new Bound);
|
|
135
|
+
}
|
|
136
|
+
export function providerOfRepository(repository) {
|
|
137
|
+
return findState(repository)?.provider;
|
|
138
|
+
}
|
|
@@ -98,6 +98,7 @@ export interface RepositoryPlan {
|
|
|
98
98
|
* throw `InvalidRepositoryQuery` before the provider is called (`"reject"`).
|
|
99
99
|
*/
|
|
100
100
|
export type RepositoryScans = "allow" | "warn" | "reject";
|
|
101
|
+
/** The `explain` method of a repository, for the actions that list it. */
|
|
101
102
|
export interface RepositoryExplain<Entity> {
|
|
102
103
|
/**
|
|
103
104
|
* How the query would be read (`{ access, index? }`), without reading
|
|
@@ -105,6 +106,7 @@ export interface RepositoryExplain<Entity> {
|
|
|
105
106
|
*/
|
|
106
107
|
explain(query?: RepositoryQuery<Entity> | RepositoryCursorQuery<Entity>): Promise<RepositoryPlan>;
|
|
107
108
|
}
|
|
109
|
+
/** The `findCursor` method of a repository, for the actions that list it. */
|
|
108
110
|
export interface RepositoryFindCursor<Entity> {
|
|
109
111
|
/**
|
|
110
112
|
* A page of `limit` items (20 by default) after the cursor `after`. Unlike
|
|
@@ -145,6 +147,7 @@ export interface RepositoryWalkResult {
|
|
|
145
147
|
/** The items of those pages. */
|
|
146
148
|
readonly items: number;
|
|
147
149
|
}
|
|
150
|
+
/** The `forEachPage` method of a repository, for the actions that list it. */
|
|
148
151
|
export interface RepositoryForEachPage<Entity> {
|
|
149
152
|
/**
|
|
150
153
|
* Reads the pages of `query` by cursor, as `findCursor` does, and runs
|
|
@@ -155,12 +158,21 @@ export interface RepositoryForEachPage<Entity> {
|
|
|
155
158
|
*/
|
|
156
159
|
forEachPage(query: RepositoryCursorQuery<Entity>, fn: (items: EntityInstanceOf<Entity>[], page: RepositoryWalkPage) => unknown, options?: RepositoryWalkOptions): Promise<RepositoryWalkResult>;
|
|
157
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* The `findMany` method of a repository: the entities that match a query, all
|
|
163
|
+
* of them without one.
|
|
164
|
+
*/
|
|
158
165
|
export interface RepositoryFindMany<Entity> {
|
|
159
166
|
findMany(query?: RepositoryQuery<Entity>): Promise<EntityInstanceOf<Entity>[]>;
|
|
160
167
|
}
|
|
168
|
+
/** The `exists` method of a repository: whether any entity matches a query. */
|
|
161
169
|
export interface RepositoryExists<Entity> {
|
|
162
170
|
exists(query?: RepositoryQuery<Entity>): Promise<boolean>;
|
|
163
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* The `findPage` method of a repository: one page of the entities that match a
|
|
174
|
+
* query, with its total.
|
|
175
|
+
*/
|
|
164
176
|
export interface RepositoryFindPage<Entity> {
|
|
165
177
|
/** Defaults to limit 20 and offset 0. */
|
|
166
178
|
findPage(query?: RepositoryQuery<Entity>): Promise<RepositoryPage<Entity>>;
|
|
@@ -169,6 +181,7 @@ export interface RepositoryFindPage<Entity> {
|
|
|
169
181
|
export type ProviderOption = null | boolean | number | string | readonly ProviderOption[] | {
|
|
170
182
|
readonly [key: string]: ProviderOption;
|
|
171
183
|
};
|
|
184
|
+
/** The JSON options of a named provider, as its description keeps them. */
|
|
172
185
|
export type ProviderOptions = Readonly<Record<string, ProviderOption>>;
|
|
173
186
|
/** A named provider can be reconstructed; opaque providers require an import mapping. */
|
|
174
187
|
export type ProviderDescription = {
|
|
@@ -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[];
|
|
@@ -30,11 +31,20 @@ type ValueAt<Entity, Key extends string> = Key extends keyof EntityValueOf<Entit
|
|
|
30
31
|
export type QueryValueOf<Value> = Value extends readonly (infer Item)[] ? Item extends AbstractEntity<any> ? readonly (Item | string)[] : Value : Value extends AbstractEntity<any> ? Value | string : Value;
|
|
31
32
|
/** What `create` takes: an entity, or the input that builds one. */
|
|
32
33
|
export type RepositoryCreateInput<Entity> = EntityInstanceOf<Entity> | (Entity extends new (value: infer Input) => unknown ? Input : never);
|
|
33
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* `create`: stores the entity, or a new one built from the input, and gives it.
|
|
36
|
+
*
|
|
37
|
+
* @throws {@link RepositoryUniqueViolation} when another entity holds the same value of a unique key (`id` included).
|
|
38
|
+
*/
|
|
34
39
|
export interface RepositoryCreate<Entity> {
|
|
35
40
|
create(entity: RepositoryCreateInput<Entity>): Promise<EntityInstanceOf<Entity>>;
|
|
36
41
|
}
|
|
37
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* `update`: stores the current value of an entity it holds, and gives it.
|
|
44
|
+
*
|
|
45
|
+
* @throws {@link RepositoryUniqueViolation} when another entity holds the same value of a unique key.
|
|
46
|
+
* @throws {@link RepositoryEntityNotFound} when the repository holds no entity with its id.
|
|
47
|
+
*/
|
|
38
48
|
export interface RepositoryUpdate<Entity> {
|
|
39
49
|
update(entity: EntityInstanceOf<Entity>): Promise<EntityInstanceOf<Entity>>;
|
|
40
50
|
}
|
|
@@ -154,6 +164,12 @@ export interface ProviderIncrementOptions {
|
|
|
154
164
|
*/
|
|
155
165
|
readonly now?: string;
|
|
156
166
|
}
|
|
167
|
+
/** `RepositoryContext.schema`. */
|
|
168
|
+
export interface RepositorySchema {
|
|
169
|
+
readonly version: number;
|
|
170
|
+
readonly key: string;
|
|
171
|
+
readonly previousNames: readonly string[];
|
|
172
|
+
}
|
|
157
173
|
/** What a provider gets with every call: whose entities, and the environment. */
|
|
158
174
|
export interface RepositoryContext<Environment = unknown> {
|
|
159
175
|
/** The entity class; name a table after `recordNameOf(entity)`, not `entity.name`. */
|
|
@@ -180,14 +196,32 @@ export interface RepositoryContext<Environment = unknown> {
|
|
|
180
196
|
readonly expiry?: {
|
|
181
197
|
readonly key: string;
|
|
182
198
|
};
|
|
199
|
+
/**
|
|
200
|
+
* For an entity declared with `schemaVersion`: the current version, the
|
|
201
|
+
* key of each row holding it (`SCHEMA_VERSION_KEY`, which repositories
|
|
202
|
+
* write and strip, so a provider keeping it in a column of its own maps
|
|
203
|
+
* that key), and the names the entity had before.
|
|
204
|
+
*/
|
|
205
|
+
readonly schema?: RepositorySchema;
|
|
183
206
|
/** The variables of the `environmentDependencies` of the repository. */
|
|
184
207
|
readonly env: Environment;
|
|
185
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* What `IRepositoryProvider.migrate` gives when it did not apply the plan to
|
|
211
|
+
* the store (it wrote it to a file to be run later): `app.migrate` skips the
|
|
212
|
+
* backfill of the entity and reports it `deferred`.
|
|
213
|
+
*/
|
|
214
|
+
export type ProviderMigrationResult = {
|
|
215
|
+
readonly deferred: true;
|
|
216
|
+
};
|
|
186
217
|
/**
|
|
187
218
|
* How a repository reads and writes: plain values of its entity, by its
|
|
188
|
-
* context.
|
|
189
|
-
*
|
|
190
|
-
*
|
|
219
|
+
* context. Implement it to store entities somewhere else; `inMemoryProvider`
|
|
220
|
+
* is the default, and `providerContract` (from `@afora/core/testing`) checks
|
|
221
|
+
* an implementation.
|
|
222
|
+
*
|
|
223
|
+
* @throws {@link RepositoryUniqueViolation} from `insert` and `update`, when they would break `context.unique`.
|
|
224
|
+
* @throws {@link RepositoryEntityNotFound} from `update` and `remove`, for an unknown id.
|
|
191
225
|
*/
|
|
192
226
|
export interface IRepositoryProvider<Environment = unknown> {
|
|
193
227
|
findOne(context: RepositoryContext<Environment>, query?: ProviderQuery): Promise<RepositoryData | undefined>;
|
|
@@ -218,5 +252,16 @@ export interface IRepositoryProvider<Environment = unknown> {
|
|
|
218
252
|
transaction?<Result>(run: (provider: IRepositoryProvider<Environment>) => Promise<Result>): Promise<Result>;
|
|
219
253
|
/** Releases what the provider holds (connections, files); `app.stop()` calls it. */
|
|
220
254
|
close?(): void | Promise<void>;
|
|
255
|
+
/**
|
|
256
|
+
* Applies the structural part of `plan` (columns, tables) for a provider
|
|
257
|
+
* with a schema of its own, as SQL; `app.migrate({ mode: "apply" })` calls
|
|
258
|
+
* it before the backfill. Document stores leave it out: they rely on the
|
|
259
|
+
* upgrade on read and the backfill. Only destructive changes when
|
|
260
|
+
* `plan.destructive`. Gives `{ deferred: true }` when it did not apply
|
|
261
|
+
* the plan to the store (it wrote it to a file to be run later): the
|
|
262
|
+
* backfill of the entity is then skipped, as its rows cannot be written
|
|
263
|
+
* in the new shape yet.
|
|
264
|
+
*/
|
|
265
|
+
migrate?(context: RepositoryContext<Environment>, plan: SchemaPlan): Promise<void | ProviderMigrationResult>;
|
|
221
266
|
}
|
|
222
267
|
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
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.
|
|
77
|
+
*
|
|
78
|
+
* @default false
|
|
79
|
+
*/
|
|
80
|
+
readonly destructive?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Rows read per page.
|
|
83
|
+
*
|
|
84
|
+
* @default 100
|
|
85
|
+
*/
|
|
86
|
+
readonly pageSize?: number;
|
|
87
|
+
/**
|
|
88
|
+
* The committed lock (`afora.lock.json`): in `apply`, the changes of each
|
|
89
|
+
* entity from it to the code are the plan given to
|
|
90
|
+
* `IRepositoryProvider.migrate`. Without it the plan has no changes.
|
|
91
|
+
*/
|
|
92
|
+
readonly lock?: SchemaLock;
|
|
93
|
+
}
|
|
94
|
+
/** A row `app.migrate` cannot bring up to the current shape. */
|
|
95
|
+
export interface SchemaMigrationInvalidRow {
|
|
96
|
+
readonly id: string;
|
|
97
|
+
/** Why: the issues of the parse, or one for an error of the upgrade. */
|
|
98
|
+
readonly issues: readonly Issue[];
|
|
99
|
+
}
|
|
100
|
+
/** What `app.migrate` found and did for one entity. */
|
|
101
|
+
export interface EntityMigrationReport {
|
|
102
|
+
/** Rows read, under the current name and the previous ones. */
|
|
103
|
+
readonly scanned: number;
|
|
104
|
+
/** Rows that needed nothing. */
|
|
105
|
+
readonly upToDate: number;
|
|
106
|
+
/** Rows rewritten in the current shape (in `check`, that would be). */
|
|
107
|
+
readonly upgraded: number;
|
|
108
|
+
/**
|
|
109
|
+
* Rows whose only pending change is removing keys, left as they are
|
|
110
|
+
* without `destructive`.
|
|
111
|
+
*/
|
|
112
|
+
readonly removalPending: number;
|
|
113
|
+
/** Rows the entity rejects once brought up, left as they are. */
|
|
114
|
+
readonly invalid: readonly SchemaMigrationInvalidRow[];
|
|
115
|
+
/** Rows moved from a previous name (in `check`, that would be). */
|
|
116
|
+
readonly movedFromPreviousNames: number;
|
|
117
|
+
/** Paths of the stored keys the blueprint no longer declares, sorted. */
|
|
118
|
+
readonly removedKeys: readonly string[];
|
|
119
|
+
/**
|
|
120
|
+
* Whether `provider.migrate` deferred the plan (`{ deferred: true }`, it
|
|
121
|
+
* wrote it to be run later): the rows were then not walked, and every
|
|
122
|
+
* count is 0. Run `app.migrate` again once the plan is applied.
|
|
123
|
+
*/
|
|
124
|
+
readonly deferred: boolean;
|
|
125
|
+
}
|
|
126
|
+
/** What `app.migrate` gives. */
|
|
127
|
+
export interface SchemaMigrationReport {
|
|
128
|
+
readonly mode: "check" | "apply";
|
|
129
|
+
readonly destructive: boolean;
|
|
130
|
+
/** By entity name. */
|
|
131
|
+
readonly entities: Readonly<Record<string, EntityMigrationReport>>;
|
|
132
|
+
}
|
|
@@ -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 {};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { fieldOf } from "../blueprint/callable.js";
|
|
2
|
+
import { isBlueprint } from "../blueprint/is-blueprint.js";
|
|
3
|
+
import { isBlueprintList } from "../blueprint/list.js";
|
|
4
|
+
import { recordFieldOf } from "../blueprint/record-field.js";
|
|
5
|
+
import { EntitySettings, SCHEMA_VERSION_KEY } from "../entity/abstract-entity.js";
|
|
6
|
+
import { isEntityClass } from "../entity/entity.js";
|
|
7
|
+
import { isProperty } from "../property/is-property.js";
|
|
8
|
+
import {
|
|
9
|
+
InvalidRepositoryData,
|
|
10
|
+
InvalidStoredSchemaVersion
|
|
11
|
+
} from "./exceptions.js";
|
|
12
|
+
const isPlain = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
function stepOf(field) {
|
|
14
|
+
if (field === undefined)
|
|
15
|
+
return { kind: "opaque" };
|
|
16
|
+
if (isBlueprintList(field))
|
|
17
|
+
return { kind: "list", item: stepOf(field.item) };
|
|
18
|
+
if (isBlueprint(field)) {
|
|
19
|
+
return { kind: "collection", fields: stepsOf(field.properties) };
|
|
20
|
+
}
|
|
21
|
+
const record = recordFieldOf(field);
|
|
22
|
+
if (record) {
|
|
23
|
+
if (isEntityClass(record.record))
|
|
24
|
+
return { kind: "opaque" };
|
|
25
|
+
return {
|
|
26
|
+
kind: "collection",
|
|
27
|
+
fields: stepsOf(record.record.blueprint.properties)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const property = fieldOf(field);
|
|
31
|
+
if (!isProperty(property))
|
|
32
|
+
return { kind: "opaque" };
|
|
33
|
+
const { fill, renamedFrom } = property.type;
|
|
34
|
+
return {
|
|
35
|
+
kind: "property",
|
|
36
|
+
...fill === undefined ? {} : { fill },
|
|
37
|
+
...renamedFrom === undefined ? {} : { renamedFrom }
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function stepsOf(properties) {
|
|
41
|
+
return {
|
|
42
|
+
fields: new Map(Object.entries(properties).map(([key, field]) => [key, stepOf(field)]))
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function declaresSteps(steps) {
|
|
46
|
+
const visit = (step) => {
|
|
47
|
+
switch (step.kind) {
|
|
48
|
+
case "property":
|
|
49
|
+
return step.fill !== undefined || step.renamedFrom !== undefined;
|
|
50
|
+
case "collection":
|
|
51
|
+
return declaresSteps(step.fields);
|
|
52
|
+
case "list":
|
|
53
|
+
return visit(step.item);
|
|
54
|
+
case "opaque":
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
for (const step of steps.fields.values())
|
|
59
|
+
if (visit(step))
|
|
60
|
+
return true;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const plans = new WeakMap;
|
|
64
|
+
function upcastPlanOf(entity) {
|
|
65
|
+
const cached = plans.get(entity);
|
|
66
|
+
if (cached !== undefined)
|
|
67
|
+
return cached ?? undefined;
|
|
68
|
+
const plan = fullUpcastPlanOf(entity);
|
|
69
|
+
const needed = plan.versioned || plan.renamed || declaresSteps(plan.fields) ? plan : null;
|
|
70
|
+
plans.set(entity, needed);
|
|
71
|
+
return needed ?? undefined;
|
|
72
|
+
}
|
|
73
|
+
export function fullUpcastPlanOf(entity) {
|
|
74
|
+
const settings = entity[EntitySettings];
|
|
75
|
+
return {
|
|
76
|
+
versioned: settings?.schemaVersioned === true,
|
|
77
|
+
version: settings?.schemaVersion ?? 1,
|
|
78
|
+
renamed: (settings?.previousNames.length ?? 0) > 0,
|
|
79
|
+
upgrade: settings?.upgrade ?? {},
|
|
80
|
+
fields: stepsOf(entity.blueprint.properties)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const join = (path, key) => path ? `${path}.${key}` : key;
|
|
84
|
+
const fillOf = (value) => typeof value === "object" && value !== null ? structuredClone(value) : value;
|
|
85
|
+
function applySteps(steps, value, drop, path, report) {
|
|
86
|
+
const result = { ...value };
|
|
87
|
+
for (const [key, step] of steps.fields) {
|
|
88
|
+
if (step.kind !== "property" || step.renamedFrom === undefined)
|
|
89
|
+
continue;
|
|
90
|
+
const from = step.renamedFrom;
|
|
91
|
+
if (result[key] === undefined && Object.hasOwn(result, from)) {
|
|
92
|
+
result[key] = result[from];
|
|
93
|
+
delete result[from];
|
|
94
|
+
report?.renamed.push(join(path, key));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const [key, step] of steps.fields) {
|
|
98
|
+
const current = result[key];
|
|
99
|
+
if (current === undefined || current === null) {
|
|
100
|
+
if (step.kind === "property" && step.fill !== undefined && current === undefined) {
|
|
101
|
+
result[key] = fillOf(step.fill);
|
|
102
|
+
report?.filled.push(join(path, key));
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
result[key] = applyField(step, current, drop, join(path, key), report);
|
|
107
|
+
}
|
|
108
|
+
for (const key of Object.keys(result)) {
|
|
109
|
+
if (steps.fields.has(key))
|
|
110
|
+
continue;
|
|
111
|
+
if (result[key] !== undefined)
|
|
112
|
+
report?.removed.push(join(path, key));
|
|
113
|
+
if (drop)
|
|
114
|
+
delete result[key];
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
function applyField(step, value, drop, path, report) {
|
|
119
|
+
switch (step.kind) {
|
|
120
|
+
case "collection":
|
|
121
|
+
return isPlain(value) ? applySteps(step.fields, value, drop, path, report) : value;
|
|
122
|
+
case "list":
|
|
123
|
+
return Array.isArray(value) ? value.map((item) => item === undefined || item === null ? item : applyField(step.item, item, drop, `${path}[]`, report)) : value;
|
|
124
|
+
default:
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function storedVersionOf(data) {
|
|
129
|
+
return Object.hasOwn(data, SCHEMA_VERSION_KEY) ? data[SCHEMA_VERSION_KEY] : 1;
|
|
130
|
+
}
|
|
131
|
+
export function upcast(source, plan, data, drop = true, report) {
|
|
132
|
+
const stored = storedVersionOf(data);
|
|
133
|
+
if (typeof stored !== "number" || !Number.isSafeInteger(stored) || stored < 1 || stored > plan.version) {
|
|
134
|
+
throw new InvalidStoredSchemaVersion({
|
|
135
|
+
source,
|
|
136
|
+
details: { id: String(data.id), stored, current: plan.version }
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (report)
|
|
140
|
+
report.from = stored;
|
|
141
|
+
let value = { ...data };
|
|
142
|
+
delete value[SCHEMA_VERSION_KEY];
|
|
143
|
+
for (let version = stored + 1;version <= plan.version; version++) {
|
|
144
|
+
const step = plan.upgrade[version];
|
|
145
|
+
if (step === undefined)
|
|
146
|
+
continue;
|
|
147
|
+
const next = step(Object.freeze(value));
|
|
148
|
+
if (!isPlain(next)) {
|
|
149
|
+
throw new InvalidStoredSchemaVersion({
|
|
150
|
+
source,
|
|
151
|
+
details: {
|
|
152
|
+
id: String(data.id),
|
|
153
|
+
stored,
|
|
154
|
+
current: plan.version,
|
|
155
|
+
reason: `The upgrade step ${version} did not give an object.`
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
value = { ...next };
|
|
160
|
+
delete value[SCHEMA_VERSION_KEY];
|
|
161
|
+
}
|
|
162
|
+
return applySteps(plan.fields, value, drop, "", report);
|
|
163
|
+
}
|
|
164
|
+
export function upcastRows(state, rows) {
|
|
165
|
+
const plan = upcastPlanOf(state.entity);
|
|
166
|
+
if (plan === undefined)
|
|
167
|
+
return rows;
|
|
168
|
+
return rows.map((row) => {
|
|
169
|
+
try {
|
|
170
|
+
return upcast(state.name, plan, row);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw new InvalidRepositoryData({ source: state.name, cause: error });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
export function withSchemaVersion(entity, data) {
|
|
177
|
+
const plan = upcastPlanOf(entity);
|
|
178
|
+
return plan?.versioned ? { ...data, [SCHEMA_VERSION_KEY]: plan.version } : data;
|
|
179
|
+
}
|