@afora/core 0.0.9 → 0.0.12
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 +62 -189
- package/dist/adapters.js +31 -55
- package/dist/core/application/application.d.ts +30 -0
- package/dist/core/application/application.js +1291 -0
- package/dist/core/application/exceptions.js +85 -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.js +398 -0
- package/dist/core/application/start.js +162 -0
- package/dist/core/application/types/application.interface.d.ts +23 -3
- package/dist/core/auth/jwt.js +200 -0
- package/dist/core/auth/types/jwt.interface.d.ts +10 -2
- 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.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.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 +16 -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 +19 -0
- package/dist/core/entity/abstract-entity.js +181 -0
- 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.js +27 -0
- package/dist/core/entity/trust.js +7 -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.js +66 -0
- package/dist/core/output/coerce.js +38 -0
- package/dist/core/output/exceptions.js +22 -0
- package/dist/{index-y280ew0k.js → core/output/http.js} +35 -80
- package/dist/core/output/openapi-sink.d.ts +10 -2
- package/dist/{index-hg1z2s2v.js → core/output/openapi-sink.js} +60 -544
- 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.js +111 -0
- package/dist/core/output/status.js +52 -0
- package/dist/core/output/types/http.interface.d.ts +5 -1
- package/dist/core/output/types/output.interface.d.ts +24 -0
- package/dist/core/parse/issue.js +31 -0
- package/dist/core/parse/parse.d.ts +8 -1
- package/dist/core/parse/parse.js +323 -0
- package/dist/core/parse/strip.js +48 -0
- package/dist/core/property/abstract-property.js +37 -0
- 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/record/abstract-record.d.ts +2 -1
- 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 +5 -0
- package/dist/core/repository/cursor.js +83 -0
- package/dist/core/repository/exceptions.js +72 -0
- package/dist/core/repository/in-memory.js +196 -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.js +138 -0
- package/dist/core/repository/types/query.interface.d.ts +13 -0
- package/dist/core/repository/types/repository.interface.d.ts +20 -8
- package/dist/core/repository/types/schema.interface.d.ts +8 -2
- package/dist/core/repository/upcast.js +179 -0
- package/dist/core/schema/schema.d.ts +9 -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.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.js +56 -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.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 +27 -8
- 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/route.interface.d.ts +25 -5
- package/dist/core/use-case/use-case.d.ts +77 -2
- package/dist/core/use-case/use-case.js +493 -0
- package/dist/experimental.d.ts +5 -0
- package/dist/experimental.js +18 -161
- package/dist/index.d.ts +7 -5
- package/dist/index.js +213 -367
- 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.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.js +73 -0
- package/dist/std/record.d.ts +20 -1
- package/dist/std/record.js +3 -0
- package/dist/std/resource.js +39 -0
- package/dist/std/runtime.js +193 -0
- package/dist/std/stages.js +4 -0
- package/dist/std/use-cases/auth.js +193 -0
- package/dist/std/use-cases/crud.js +261 -0
- package/dist/std/use-cases/index.js +12 -0
- package/dist/std/use-cases/lifecycle.js +97 -0
- package/dist/std/use-cases/shared.js +232 -0
- package/dist/std/use-cases/types.d.ts +5 -1
- package/dist/std/use-cases/types.js +0 -0
- package/dist/testing/http-provider-contract.js +344 -0
- package/dist/testing/index.js +3 -1435
- package/dist/testing/memory-publisher.js +12 -0
- package/dist/testing/provider-contract.d.ts +12 -3
- package/dist/testing/provider-contract.js +1071 -0
- package/package.json +17 -5
- package/dist/index-3aae5jfq.js +0 -2997
- package/dist/index-ny3as79r.js +0 -4567
- package/dist/index-xegv6a1p.js +0 -4291
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { declaredRecordNameOf } from "../record/name.js";
|
|
2
|
+
import {
|
|
3
|
+
InvalidRepositoryDeclaration,
|
|
4
|
+
RepositoryConcurrencyConflict,
|
|
5
|
+
RepositoryEntityNotFound,
|
|
6
|
+
RepositoryUniqueViolation
|
|
7
|
+
} from "./exceptions.js";
|
|
8
|
+
import { providerOf } from "./provider.js";
|
|
9
|
+
import { matchesQuery, nowOf, sameValue, selectRows } from "./query.js";
|
|
10
|
+
const tableKeys = new WeakMap;
|
|
11
|
+
function tableKeyOf(entity) {
|
|
12
|
+
let key = tableKeys.get(entity);
|
|
13
|
+
if (key === undefined) {
|
|
14
|
+
key = declaredRecordNameOf(entity) ?? entity;
|
|
15
|
+
tableKeys.set(entity, key);
|
|
16
|
+
}
|
|
17
|
+
return key;
|
|
18
|
+
}
|
|
19
|
+
function idOf(data) {
|
|
20
|
+
return data.id;
|
|
21
|
+
}
|
|
22
|
+
function snapshotOf(tables) {
|
|
23
|
+
return new Map([...tables].map(([key, table]) => [key, new Map(table)]));
|
|
24
|
+
}
|
|
25
|
+
const copy = (data) => structuredClone(data);
|
|
26
|
+
function scopeOf(initial, root) {
|
|
27
|
+
let tables = initial;
|
|
28
|
+
let held;
|
|
29
|
+
let closed = false;
|
|
30
|
+
const tableOf = (context) => {
|
|
31
|
+
const key = tableKeyOf(context.entity);
|
|
32
|
+
let table = tables.get(key);
|
|
33
|
+
if (!table) {
|
|
34
|
+
table = new Map;
|
|
35
|
+
tables.set(key, table);
|
|
36
|
+
}
|
|
37
|
+
return table;
|
|
38
|
+
};
|
|
39
|
+
const whenIdle = async (work) => {
|
|
40
|
+
while (held)
|
|
41
|
+
await held;
|
|
42
|
+
if (closed) {
|
|
43
|
+
throw new InvalidRepositoryDeclaration({
|
|
44
|
+
source: "inMemory",
|
|
45
|
+
details: {
|
|
46
|
+
reason: "The transaction has ended; its provider cannot be used."
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return work();
|
|
51
|
+
};
|
|
52
|
+
const assertUnique = (context, data) => {
|
|
53
|
+
for (const stored of tableOf(context).values()) {
|
|
54
|
+
if (idOf(stored) === idOf(data))
|
|
55
|
+
continue;
|
|
56
|
+
const key = context.unique.find((key) => data[key] !== undefined && sameValue(stored[key], data[key]));
|
|
57
|
+
if (key !== undefined) {
|
|
58
|
+
throw new RepositoryUniqueViolation({
|
|
59
|
+
source: context.name,
|
|
60
|
+
details: { key }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const provider = {
|
|
66
|
+
capabilities: Object.freeze({
|
|
67
|
+
transactions: true,
|
|
68
|
+
keyset: true,
|
|
69
|
+
versioning: true,
|
|
70
|
+
atomic: true,
|
|
71
|
+
expiry: true
|
|
72
|
+
}),
|
|
73
|
+
findOne: (context, query = {}) => whenIdle(() => {
|
|
74
|
+
const [first] = selectRows(tableOf(context).values(), { ...query, limit: Math.min(query.limit ?? 1, 1) }, context.expiry?.key);
|
|
75
|
+
return first === undefined ? undefined : copy(first);
|
|
76
|
+
}),
|
|
77
|
+
findMany: (context, query = {}) => whenIdle(() => selectRows(tableOf(context).values(), query, context.expiry?.key).map(copy)),
|
|
78
|
+
insert: (context, data) => whenIdle(() => {
|
|
79
|
+
const table = tableOf(context);
|
|
80
|
+
if (table.has(idOf(data))) {
|
|
81
|
+
throw new RepositoryUniqueViolation({
|
|
82
|
+
source: context.name,
|
|
83
|
+
details: { key: "id" }
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
assertUnique(context, data);
|
|
87
|
+
table.set(idOf(data), copy(data));
|
|
88
|
+
}),
|
|
89
|
+
update: (context, data, options = {}) => whenIdle(() => {
|
|
90
|
+
const table = tableOf(context);
|
|
91
|
+
const stored = table.get(idOf(data));
|
|
92
|
+
if (!stored) {
|
|
93
|
+
throw new RepositoryEntityNotFound({
|
|
94
|
+
source: context.name,
|
|
95
|
+
details: { id: idOf(data) }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const { expectedVersion: expected } = options;
|
|
99
|
+
if (expected !== undefined && stored.version !== expected) {
|
|
100
|
+
throw new RepositoryConcurrencyConflict({
|
|
101
|
+
source: context.name,
|
|
102
|
+
details: {
|
|
103
|
+
id: idOf(data),
|
|
104
|
+
expected,
|
|
105
|
+
...typeof stored.version === "number" ? { actual: stored.version } : {}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
assertUnique(context, data);
|
|
110
|
+
table.set(idOf(data), copy(data));
|
|
111
|
+
}),
|
|
112
|
+
increment: (context, id, deltas, options = {}) => whenIdle(() => {
|
|
113
|
+
const table = tableOf(context);
|
|
114
|
+
const found = table.get(id);
|
|
115
|
+
const expiry = context.expiry?.key;
|
|
116
|
+
const expired = found !== undefined && expiry !== undefined && !matchesQuery(found, { withDeleted: true, now: options.now }, expiry);
|
|
117
|
+
const stored = expired ? undefined : found;
|
|
118
|
+
if (!stored || stored.deletedAt != null) {
|
|
119
|
+
if (!options.create) {
|
|
120
|
+
throw new RepositoryEntityNotFound({
|
|
121
|
+
source: context.name,
|
|
122
|
+
details: { id }
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (stored) {
|
|
126
|
+
throw new RepositoryUniqueViolation({
|
|
127
|
+
source: context.name,
|
|
128
|
+
details: { key: "id" }
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const created = { ...options.create, id };
|
|
132
|
+
assertUnique(context, created);
|
|
133
|
+
table.set(id, copy(created));
|
|
134
|
+
return copy(created);
|
|
135
|
+
}
|
|
136
|
+
const next = { ...stored, ...options.set };
|
|
137
|
+
for (const [key, delta] of Object.entries(deltas)) {
|
|
138
|
+
const value = stored[key];
|
|
139
|
+
next[key] = (typeof value === "number" ? value : 0) + delta;
|
|
140
|
+
}
|
|
141
|
+
table.set(id, copy(next));
|
|
142
|
+
return copy(next);
|
|
143
|
+
}),
|
|
144
|
+
remove: (context, id) => whenIdle(() => {
|
|
145
|
+
if (!tableOf(context).delete(id)) {
|
|
146
|
+
throw new RepositoryEntityNotFound({
|
|
147
|
+
source: context.name,
|
|
148
|
+
details: { id }
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}),
|
|
152
|
+
count: (context, query = {}) => whenIdle(() => {
|
|
153
|
+
const expiry = context.expiry?.key;
|
|
154
|
+
const now = expiry === undefined ? 0 : nowOf(query);
|
|
155
|
+
let total = 0;
|
|
156
|
+
for (const data of tableOf(context).values()) {
|
|
157
|
+
if (matchesQuery(data, query, expiry, now))
|
|
158
|
+
total++;
|
|
159
|
+
}
|
|
160
|
+
return total;
|
|
161
|
+
}),
|
|
162
|
+
async transaction(run) {
|
|
163
|
+
let release;
|
|
164
|
+
const child = await whenIdle(() => {
|
|
165
|
+
held = new Promise((resolve) => {
|
|
166
|
+
release = resolve;
|
|
167
|
+
});
|
|
168
|
+
return scopeOf(snapshotOf(tables), false);
|
|
169
|
+
});
|
|
170
|
+
try {
|
|
171
|
+
const result = await run(child.provider);
|
|
172
|
+
tables = child.close();
|
|
173
|
+
return result;
|
|
174
|
+
} finally {
|
|
175
|
+
child.close();
|
|
176
|
+
held = undefined;
|
|
177
|
+
release();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
return {
|
|
182
|
+
provider,
|
|
183
|
+
close() {
|
|
184
|
+
if (!root)
|
|
185
|
+
closed = true;
|
|
186
|
+
return tables;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
export function inMemoryProvider() {
|
|
191
|
+
return providerOf({
|
|
192
|
+
name: "inMemory",
|
|
193
|
+
options: {},
|
|
194
|
+
create: () => scopeOf(new Map, true).provider
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EntitySettings,
|
|
3
|
+
isVersionedEntityClass,
|
|
4
|
+
SCHEMA_VERSION_KEY
|
|
5
|
+
} from "../entity/abstract-entity.js";
|
|
6
|
+
import { Exception } from "../exception/exception.js";
|
|
7
|
+
import { recordNameOf } from "../record/name.js";
|
|
8
|
+
import {
|
|
9
|
+
InvalidRepositoryData,
|
|
10
|
+
InvalidRepositoryDeclaration,
|
|
11
|
+
RepositoryConcurrencyConflict
|
|
12
|
+
} from "./exceptions.js";
|
|
13
|
+
import { resolveReferences } from "./references.js";
|
|
14
|
+
import { contextOf } from "./state.js";
|
|
15
|
+
import {
|
|
16
|
+
fullUpcastPlanOf,
|
|
17
|
+
upcast
|
|
18
|
+
} from "./upcast.js";
|
|
19
|
+
const aliases = new WeakMap;
|
|
20
|
+
function aliasOf(entity, name) {
|
|
21
|
+
let byName = aliases.get(entity);
|
|
22
|
+
if (!byName) {
|
|
23
|
+
byName = new Map;
|
|
24
|
+
aliases.set(entity, byName);
|
|
25
|
+
}
|
|
26
|
+
let alias = byName.get(name);
|
|
27
|
+
if (!alias) {
|
|
28
|
+
const Entity = entity;
|
|
29
|
+
alias = class extends Entity {
|
|
30
|
+
};
|
|
31
|
+
Object.defineProperty(alias, "name", { value: name });
|
|
32
|
+
Object.defineProperty(alias, "recordName", { value: name });
|
|
33
|
+
byName.set(name, alias);
|
|
34
|
+
}
|
|
35
|
+
return alias;
|
|
36
|
+
}
|
|
37
|
+
function previousContextOf(context, name) {
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
...context,
|
|
40
|
+
entity: aliasOf(context.entity, name),
|
|
41
|
+
name: `${name}Repository`
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function walk(provider, context, pageSize, visit) {
|
|
45
|
+
const keyset = provider.capabilities?.keyset === true;
|
|
46
|
+
let after;
|
|
47
|
+
let offset = 0;
|
|
48
|
+
for (;; ) {
|
|
49
|
+
const query = {
|
|
50
|
+
orderBy: [{ key: "id", direction: "asc" }],
|
|
51
|
+
withDeleted: true,
|
|
52
|
+
...context.expiry === undefined ? {} : { withExpired: true },
|
|
53
|
+
limit: pageSize,
|
|
54
|
+
...keyset ? after === undefined ? {} : { after } : offset === 0 ? {} : { offset }
|
|
55
|
+
};
|
|
56
|
+
const rows = await provider.findMany(context, query);
|
|
57
|
+
if (rows.length === 0)
|
|
58
|
+
return;
|
|
59
|
+
const removed = await visit(rows);
|
|
60
|
+
after = [rows.at(-1).id];
|
|
61
|
+
offset += rows.length - removed;
|
|
62
|
+
if (rows.length < pageSize)
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function issuesOf(source, error) {
|
|
67
|
+
const issues = error?.details?.issues;
|
|
68
|
+
if (Array.isArray(issues))
|
|
69
|
+
return issues;
|
|
70
|
+
const cause = error instanceof Exception && error.is(InvalidRepositoryData) ? error.cause ?? error : error;
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
code: "invalid_value",
|
|
74
|
+
path: [],
|
|
75
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
76
|
+
source,
|
|
77
|
+
...cause instanceof Exception ? { cause } : {}
|
|
78
|
+
}
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
async function upcastPage(state, plan, rows) {
|
|
82
|
+
const brought = rows.map((row) => {
|
|
83
|
+
const report = {
|
|
84
|
+
from: 1,
|
|
85
|
+
renamed: [],
|
|
86
|
+
filled: [],
|
|
87
|
+
removed: []
|
|
88
|
+
};
|
|
89
|
+
try {
|
|
90
|
+
return {
|
|
91
|
+
row,
|
|
92
|
+
current: upcast(state.name, plan, row, true, report),
|
|
93
|
+
report
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return { row, invalid: issuesOf(state.name, error) };
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const valid = brought.filter((item) => ("current" in item));
|
|
100
|
+
const parse = (data) => state.entity.safeParse(data);
|
|
101
|
+
const resolvedOf = async (items) => resolveReferences(state, items.map((item) => item.current), new Map);
|
|
102
|
+
let resolved;
|
|
103
|
+
try {
|
|
104
|
+
resolved = await resolvedOf(valid);
|
|
105
|
+
} catch {
|
|
106
|
+
resolved = await Promise.all(valid.map(async (item) => {
|
|
107
|
+
try {
|
|
108
|
+
return (await resolvedOf([item]))[0];
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return issuesOf(state.name, error);
|
|
111
|
+
}
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
const checked = new Map;
|
|
115
|
+
valid.forEach((item, index) => {
|
|
116
|
+
const data = resolved[index];
|
|
117
|
+
if (Array.isArray(data)) {
|
|
118
|
+
checked.set(item.row, data);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const parsed = parse(data);
|
|
122
|
+
if (parsed.issues)
|
|
123
|
+
checked.set(item.row, parsed.issues);
|
|
124
|
+
});
|
|
125
|
+
return brought.map((item) => {
|
|
126
|
+
const issues = checked.get(item.row);
|
|
127
|
+
return issues === undefined ? item : { row: item.row, invalid: issues };
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function writtenOf(state, plan, item, destructive) {
|
|
131
|
+
const data = destructive ? item.current : upcast(state.name, plan, item.row, false);
|
|
132
|
+
return plan.versioned ? { ...data, [SCHEMA_VERSION_KEY]: plan.version } : data;
|
|
133
|
+
}
|
|
134
|
+
export async function migrateRepository(state, options) {
|
|
135
|
+
const { mode, destructive, pageSize } = options;
|
|
136
|
+
const apply = mode === "apply";
|
|
137
|
+
const { provider } = state;
|
|
138
|
+
const context = contextOf(state);
|
|
139
|
+
const plan = fullUpcastPlanOf(state.entity);
|
|
140
|
+
const versioned = isVersionedEntityClass(state.entity) && provider.capabilities?.versioning === true;
|
|
141
|
+
const own = recordNameOf(state.entity);
|
|
142
|
+
const previousNames = (state.entity[EntitySettings]?.previousNames ?? []).filter((previous) => previous !== own && `${previous}Repository` !== context.name);
|
|
143
|
+
const needs = ["findMany"];
|
|
144
|
+
if (apply)
|
|
145
|
+
needs.push("update");
|
|
146
|
+
if (apply && previousNames.length > 0)
|
|
147
|
+
needs.push("insert");
|
|
148
|
+
if (apply && destructive && previousNames.length > 0)
|
|
149
|
+
needs.push("remove");
|
|
150
|
+
for (const need of needs) {
|
|
151
|
+
if (typeof provider[need] !== "function") {
|
|
152
|
+
throw new InvalidRepositoryDeclaration({
|
|
153
|
+
source: state.name,
|
|
154
|
+
details: { reason: `The provider has no "${need}" to migrate.` }
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const tally = {
|
|
159
|
+
scanned: 0,
|
|
160
|
+
upToDate: 0,
|
|
161
|
+
upgraded: 0,
|
|
162
|
+
removalPending: 0,
|
|
163
|
+
movedFromPreviousNames: 0,
|
|
164
|
+
invalid: [],
|
|
165
|
+
removedKeys: new Set
|
|
166
|
+
};
|
|
167
|
+
if (apply) {
|
|
168
|
+
const migrated = await provider.migrate?.(context, options.plan);
|
|
169
|
+
if (typeof migrated === "object" && migrated !== null && migrated.deferred === true) {
|
|
170
|
+
return { ...reportOf(tally), deferred: true };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
await walk(provider, context, pageSize, async (rows) => {
|
|
174
|
+
for (const item of await upcastPage(state, plan, rows)) {
|
|
175
|
+
tally.scanned += 1;
|
|
176
|
+
if ("invalid" in item) {
|
|
177
|
+
tally.invalid.push({ id: String(item.row.id), issues: item.invalid });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const { report } = item;
|
|
181
|
+
for (const key of report.removed)
|
|
182
|
+
tally.removedKeys.add(key);
|
|
183
|
+
const pending = plan.versioned && report.from < plan.version || report.renamed.length > 0 || report.filled.length > 0;
|
|
184
|
+
const removal = report.removed.length > 0;
|
|
185
|
+
if (pending || removal && destructive) {
|
|
186
|
+
tally.upgraded += 1;
|
|
187
|
+
if (apply) {
|
|
188
|
+
const expected = item.row.version;
|
|
189
|
+
try {
|
|
190
|
+
await provider.update(context, writtenOf(state, plan, item, destructive), versioned && typeof expected === "number" ? { expectedVersion: expected } : undefined);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (!(error instanceof Exception) || !error.is(RepositoryConcurrencyConflict)) {
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
tally.upgraded -= 1;
|
|
196
|
+
tally.upToDate += 1;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} else if (removal) {
|
|
200
|
+
tally.removalPending += 1;
|
|
201
|
+
} else {
|
|
202
|
+
tally.upToDate += 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return 0;
|
|
206
|
+
});
|
|
207
|
+
for (const previous of previousNames) {
|
|
208
|
+
const old = previousContextOf(context, previous);
|
|
209
|
+
await walk(provider, old, pageSize, async (rows) => {
|
|
210
|
+
let removed = 0;
|
|
211
|
+
const moved = new Set((await provider.findMany(context, {
|
|
212
|
+
where: { id: { in: rows.map((row) => row.id) } },
|
|
213
|
+
withDeleted: true,
|
|
214
|
+
...context.expiry === undefined ? {} : { withExpired: true }
|
|
215
|
+
})).map((row) => String(row.id)));
|
|
216
|
+
for (const item of await upcastPage(state, plan, rows)) {
|
|
217
|
+
tally.scanned += 1;
|
|
218
|
+
if ("invalid" in item) {
|
|
219
|
+
tally.invalid.push({
|
|
220
|
+
id: String(item.row.id),
|
|
221
|
+
issues: item.invalid
|
|
222
|
+
});
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
for (const key of item.report.removed)
|
|
226
|
+
tally.removedKeys.add(key);
|
|
227
|
+
const id = String(item.row.id);
|
|
228
|
+
if (!moved.has(id)) {
|
|
229
|
+
tally.movedFromPreviousNames += 1;
|
|
230
|
+
if (apply) {
|
|
231
|
+
await provider.insert(context, writtenOf(state, plan, item, destructive));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (apply && destructive) {
|
|
235
|
+
await provider.remove(old, id);
|
|
236
|
+
removed += 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return removed;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return { ...reportOf(tally), deferred: false };
|
|
243
|
+
}
|
|
244
|
+
function reportOf(tally) {
|
|
245
|
+
return {
|
|
246
|
+
scanned: tally.scanned,
|
|
247
|
+
upToDate: tally.upToDate,
|
|
248
|
+
upgraded: tally.upgraded,
|
|
249
|
+
removalPending: tally.removalPending,
|
|
250
|
+
invalid: tally.invalid,
|
|
251
|
+
movedFromPreviousNames: tally.movedFromPreviousNames,
|
|
252
|
+
removedKeys: [...tally.removedKeys].sort()
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const ID = Object.freeze({ access: "id" });
|
|
2
|
+
const UNIQUE = Object.freeze({ access: "unique" });
|
|
3
|
+
const SCAN = Object.freeze({ access: "scan" });
|
|
4
|
+
function isMembership(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 1 && Array.isArray(value.in);
|
|
6
|
+
}
|
|
7
|
+
export function planQuery(repository, query = {}) {
|
|
8
|
+
const where = query.where ?? {};
|
|
9
|
+
const keys = Object.keys(where);
|
|
10
|
+
if (keys.includes("id"))
|
|
11
|
+
return ID;
|
|
12
|
+
if (keys.some((key) => repository.unique.includes(key)))
|
|
13
|
+
return UNIQUE;
|
|
14
|
+
const equal = new Set(keys.filter((key) => !isMembership(where[key])));
|
|
15
|
+
const first = query.orderBy?.[0]?.key;
|
|
16
|
+
let best;
|
|
17
|
+
for (const entry of Object.entries(repository.indexes ?? {})) {
|
|
18
|
+
const [, index] = entry;
|
|
19
|
+
if (!index.where.every((key) => equal.has(key)))
|
|
20
|
+
continue;
|
|
21
|
+
if (first !== undefined && index.orderBy !== first)
|
|
22
|
+
continue;
|
|
23
|
+
if (best === undefined || index.where.length > best[1].where.length)
|
|
24
|
+
best = entry;
|
|
25
|
+
}
|
|
26
|
+
return best === undefined ? SCAN : Object.freeze({ access: "index", index: best[0] });
|
|
27
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { frozenJson } from "../json.js";
|
|
2
|
+
import { InvalidRepositoryDeclaration } from "./exceptions.js";
|
|
3
|
+
const descriptions = new WeakMap;
|
|
4
|
+
const opaque = Object.freeze({ kind: "opaque" });
|
|
5
|
+
function invalid(reason) {
|
|
6
|
+
throw new InvalidRepositoryDeclaration({
|
|
7
|
+
source: "providerOf",
|
|
8
|
+
details: { reason }
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export function providerOf(settings) {
|
|
12
|
+
if (!settings || typeof settings.name !== "string" || settings.name.trim().length === 0 || typeof settings.create !== "function")
|
|
13
|
+
invalid("Expected a provider name and create function.");
|
|
14
|
+
const options = frozenJson(settings.options ?? {}, "Provider options", invalid);
|
|
15
|
+
if (options === null || typeof options !== "object" || Array.isArray(options))
|
|
16
|
+
invalid("Provider options must be an object.");
|
|
17
|
+
const provider = settings.create(options);
|
|
18
|
+
if (!provider || typeof provider !== "object" || ["findOne", "findMany", "insert", "update", "remove", "count"].some((key) => typeof provider[key] !== "function"))
|
|
19
|
+
invalid("Expected a repository provider from create.");
|
|
20
|
+
descriptions.set(provider, Object.freeze({
|
|
21
|
+
kind: "provider",
|
|
22
|
+
name: settings.name,
|
|
23
|
+
options
|
|
24
|
+
}));
|
|
25
|
+
return provider;
|
|
26
|
+
}
|
|
27
|
+
export function providerDescriptionOf(provider) {
|
|
28
|
+
return descriptions.get(provider) ?? opaque;
|
|
29
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { isBlueprint } from "../blueprint/is-blueprint.js";
|
|
2
|
+
import { isBlueprintList } from "../blueprint/list.js";
|
|
3
|
+
import { recordFieldOf } from "../blueprint/record-field.js";
|
|
4
|
+
import { isBlueprintVariant } from "../blueprint/variant.js";
|
|
5
|
+
import { InvalidRepositoryQuery } from "./exceptions.js";
|
|
6
|
+
function plain(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
8
|
+
}
|
|
9
|
+
function isMembership(value) {
|
|
10
|
+
return plain(value) && Object.keys(value).length === 1 && Array.isArray(value.in);
|
|
11
|
+
}
|
|
12
|
+
const QUERY_OPTIONS = [
|
|
13
|
+
"where",
|
|
14
|
+
"orderBy",
|
|
15
|
+
"limit",
|
|
16
|
+
"offset",
|
|
17
|
+
"withDeleted",
|
|
18
|
+
"withExpired"
|
|
19
|
+
];
|
|
20
|
+
export function prepareQuery(context, input = {}, map = (_key, value) => value, options = QUERY_OPTIONS) {
|
|
21
|
+
const fail = (reason) => {
|
|
22
|
+
throw new InvalidRepositoryQuery({
|
|
23
|
+
source: context.name,
|
|
24
|
+
details: { reason }
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
if (!plain(input))
|
|
28
|
+
return fail("Expected a query object.");
|
|
29
|
+
for (const key of Object.keys(input))
|
|
30
|
+
if (!options.includes(key))
|
|
31
|
+
fail(`Unknown query option "${key}".`);
|
|
32
|
+
const properties = context.entity.blueprint.properties;
|
|
33
|
+
const keyOf = (key) => {
|
|
34
|
+
if (!Object.hasOwn(properties, key))
|
|
35
|
+
fail(`Unknown query field "${key}".`);
|
|
36
|
+
};
|
|
37
|
+
for (const name of ["limit", "offset"]) {
|
|
38
|
+
const value = input[name];
|
|
39
|
+
if (value !== undefined && (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0))
|
|
40
|
+
fail(`Expected a nonnegative integer for ${name}.`);
|
|
41
|
+
}
|
|
42
|
+
if (input.withDeleted !== undefined && typeof input.withDeleted !== "boolean")
|
|
43
|
+
fail("Expected withDeleted to be boolean.");
|
|
44
|
+
if (input.withExpired !== undefined && typeof input.withExpired !== "boolean")
|
|
45
|
+
fail("Expected withExpired to be boolean.");
|
|
46
|
+
let where;
|
|
47
|
+
if (input.where !== undefined) {
|
|
48
|
+
if (!plain(input.where))
|
|
49
|
+
return fail("Expected where to be an object.");
|
|
50
|
+
where = Object.fromEntries(Object.entries(input.where).map(([key, value]) => {
|
|
51
|
+
keyOf(key);
|
|
52
|
+
return [
|
|
53
|
+
key,
|
|
54
|
+
isMembership(value) ? { in: value.in.map((item) => map(key, item)) } : map(key, value)
|
|
55
|
+
];
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
if (input.orderBy !== undefined) {
|
|
59
|
+
if (!Array.isArray(input.orderBy))
|
|
60
|
+
return fail("Expected orderBy to be an array.");
|
|
61
|
+
for (const order of input.orderBy) {
|
|
62
|
+
if (!plain(order) || typeof order.key !== "string" || Object.keys(order).some((key) => key !== "key" && key !== "direction"))
|
|
63
|
+
return fail("Expected an order with key and optional direction.");
|
|
64
|
+
keyOf(order.key);
|
|
65
|
+
if (order.direction !== undefined && order.direction !== "asc" && order.direction !== "desc")
|
|
66
|
+
fail("Expected order direction asc or desc.");
|
|
67
|
+
const field = properties[order.key];
|
|
68
|
+
if (isBlueprint(field) || isBlueprintList(field) || isBlueprintVariant(field) || recordFieldOf(field))
|
|
69
|
+
fail("Only scalar fields may be ordered.");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { ...input, ...where ? { where } : {} };
|
|
73
|
+
}
|
|
74
|
+
export function sameValue(a, b) {
|
|
75
|
+
if (a === b)
|
|
76
|
+
return true;
|
|
77
|
+
if (a instanceof Date || b instanceof Date)
|
|
78
|
+
return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
|
|
79
|
+
if (Array.isArray(a) || Array.isArray(b))
|
|
80
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, index) => sameValue(value, b[index]));
|
|
81
|
+
if (!plain(a) || !plain(b))
|
|
82
|
+
return false;
|
|
83
|
+
const keys = Object.keys(a);
|
|
84
|
+
return keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && sameValue(a[key], b[key]));
|
|
85
|
+
}
|
|
86
|
+
function isExpired(value, now) {
|
|
87
|
+
if (value === undefined || value === null)
|
|
88
|
+
return false;
|
|
89
|
+
const at = value instanceof Date ? value.getTime() : typeof value === "string" ? Date.parse(value) : Number.NaN;
|
|
90
|
+
return !Number.isNaN(at) && at <= now;
|
|
91
|
+
}
|
|
92
|
+
export function nowOf(query) {
|
|
93
|
+
const now = query.now === undefined ? Number.NaN : Date.parse(query.now);
|
|
94
|
+
return Number.isNaN(now) ? Date.now() : now;
|
|
95
|
+
}
|
|
96
|
+
export function matchesQuery(data, query, expiry, now = expiry === undefined ? 0 : nowOf(query)) {
|
|
97
|
+
return (query.withDeleted === true || data.deletedAt === undefined) && (expiry === undefined || query.withExpired === true || !isExpired(data[expiry], now)) && Object.entries(query.where ?? {}).every(([key, value]) => isMembership(value) ? value.in.some((candidate) => sameValue(data[key], candidate)) : sameValue(data[key], value));
|
|
98
|
+
}
|
|
99
|
+
function codePoints(text) {
|
|
100
|
+
return Array.from(text, (char) => char.codePointAt(0));
|
|
101
|
+
}
|
|
102
|
+
function rank(value) {
|
|
103
|
+
return value instanceof Date ? 1 : { number: 0, bigint: 0, string: 2, boolean: 3 }[typeof value] ?? 4;
|
|
104
|
+
}
|
|
105
|
+
function compareScalars(a, b) {
|
|
106
|
+
const [left, right] = [rank(a), rank(b)];
|
|
107
|
+
if (left !== right)
|
|
108
|
+
return left - right;
|
|
109
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
110
|
+
const [x, y] = [codePoints(a), codePoints(b)];
|
|
111
|
+
for (let index = 0;index < Math.min(x.length, y.length); index++) {
|
|
112
|
+
const difference = x[index] - y[index];
|
|
113
|
+
if (difference !== 0)
|
|
114
|
+
return difference;
|
|
115
|
+
}
|
|
116
|
+
return x.length - y.length;
|
|
117
|
+
}
|
|
118
|
+
const [x, y] = [
|
|
119
|
+
a instanceof Date ? a.getTime() : Number(a),
|
|
120
|
+
b instanceof Date ? b.getTime() : Number(b)
|
|
121
|
+
];
|
|
122
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
123
|
+
}
|
|
124
|
+
function compareRows(orders, a, b) {
|
|
125
|
+
for (const { key, direction } of orders) {
|
|
126
|
+
const [x, y] = [a[key], b[key]];
|
|
127
|
+
const [missingX, missingY] = [
|
|
128
|
+
x === undefined || x === null,
|
|
129
|
+
y === undefined || y === null
|
|
130
|
+
];
|
|
131
|
+
let result = missingX || missingY ? Number(!missingX) - Number(!missingY) : compareScalars(x, y);
|
|
132
|
+
if (direction === "desc")
|
|
133
|
+
result = -result;
|
|
134
|
+
if (result !== 0)
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
function orderRows(rows, query) {
|
|
140
|
+
const orders = query.orderBy ?? [];
|
|
141
|
+
return [...rows].sort((a, b) => compareRows(orders, a, b));
|
|
142
|
+
}
|
|
143
|
+
function rowsAfter(rows, orders, after) {
|
|
144
|
+
const position = Object.fromEntries(orders.map(({ key }, index) => [key, after[index]]));
|
|
145
|
+
return rows.filter((row) => compareRows(orders, row, position) > 0);
|
|
146
|
+
}
|
|
147
|
+
export function selectRows(rows, query, expiry) {
|
|
148
|
+
const now = expiry === undefined ? 0 : nowOf(query);
|
|
149
|
+
const ordered = orderRows([...rows].filter((data) => matchesQuery(data, query, expiry, now)), query);
|
|
150
|
+
const matched = query.after === undefined ? ordered : rowsAfter(ordered, query.orderBy ?? [], query.after);
|
|
151
|
+
const start = query.offset ?? 0;
|
|
152
|
+
return matched.slice(start, query.limit === undefined ? undefined : start + query.limit);
|
|
153
|
+
}
|