@afora/core 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -25
- 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/start.d.ts +7 -0
- package/dist/core/application/types/application.interface.d.ts +73 -2
- 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/command/types/command.interface.d.ts +7 -0
- package/dist/core/entity/abstract-entity.d.ts +33 -1
- package/dist/core/entity/entity.d.ts +1 -1
- package/dist/core/entity/event.d.ts +13 -2
- package/dist/core/entity/exceptions.d.ts +17 -0
- package/dist/core/entity/trust.d.ts +24 -0
- package/dist/core/entity/types/entity.interface.d.ts +96 -2
- package/dist/core/entity/version.d.ts +13 -0
- package/dist/core/event-policy/event-policy.d.ts +25 -0
- package/dist/core/event-policy/exceptions.d.ts +11 -0
- package/dist/core/event-policy/publish.d.ts +17 -0
- package/dist/core/event-policy/publishers.d.ts +18 -0
- package/dist/core/event-policy/types/event-policy.interface.d.ts +72 -0
- package/dist/core/output/access.d.ts +16 -0
- package/dist/core/output/coerce.d.ts +9 -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 +160 -39
- package/dist/core/output/stage.d.ts +7 -2
- package/dist/core/output/status.d.ts +2 -2
- 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 +19 -0
- package/dist/core/parse/strip.d.ts +13 -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 +43 -0
- package/dist/core/repository/in-memory.d.ts +16 -3
- package/dist/core/repository/migrate.d.ts +20 -0
- package/dist/core/repository/plan.d.ts +20 -0
- package/dist/core/repository/provider.d.ts +1 -1
- package/dist/core/repository/query.d.ts +9 -2
- package/dist/core/repository/repository.d.ts +40 -8
- package/dist/core/repository/state.d.ts +32 -2
- package/dist/core/repository/types/query.interface.d.ts +106 -5
- package/dist/core/repository/types/repository.interface.d.ts +139 -7
- 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 +64 -26
- 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 +55 -48
- package/dist/index.js +141 -169
- package/dist/std/blueprint-plugin.d.ts +6 -2
- package/dist/std/blueprint.d.ts +3 -0
- package/dist/std/entity.d.ts +2 -1
- package/dist/std/event-policies.d.ts +2 -0
- package/dist/std/properties/exceptions.d.ts +11 -0
- package/dist/std/properties/json.d.ts +54 -0
- package/dist/std/properties/password.d.ts +0 -2
- package/dist/std/properties/time.d.ts +4 -0
- 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/crud.d.ts +6 -4
- package/dist/std/use-cases/index.d.ts +1 -1
- package/dist/std/use-cases/shared.d.ts +34 -6
- package/dist/std/use-cases/types.d.ts +197 -22
- package/dist/testing/http-provider-contract.d.ts +32 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +839 -3
- package/dist/testing/memory-publisher.d.ts +16 -0
- package/dist/testing/provider-contract.d.ts +31 -1
- package/package.json +17 -3
- package/dist/index-q6hggpzw.js +0 -6667
- package/dist/index-r78dsvtc.js +0 -1721
|
@@ -0,0 +1,4567 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__export,
|
|
3
|
+
isProperty,
|
|
4
|
+
schemaOf,
|
|
5
|
+
recordPropertyLineage,
|
|
6
|
+
propertyLineageOf2,
|
|
7
|
+
fieldOf,
|
|
8
|
+
Kind2,
|
|
9
|
+
isBlueprint,
|
|
10
|
+
isRecordClass,
|
|
11
|
+
recordFieldOf,
|
|
12
|
+
Exception2,
|
|
13
|
+
isDeclaredException,
|
|
14
|
+
declaredMessageOf,
|
|
15
|
+
exceptionOf2,
|
|
16
|
+
exports_exceptions3,
|
|
17
|
+
variantOfValue,
|
|
18
|
+
isBlueprintVariant,
|
|
19
|
+
InvalidPropertyValue2,
|
|
20
|
+
exports_exceptions4,
|
|
21
|
+
recordNameOf2,
|
|
22
|
+
declaredRecordNameOf,
|
|
23
|
+
fieldsOf,
|
|
24
|
+
isPlainObject,
|
|
25
|
+
listOf2,
|
|
26
|
+
isBlueprintList,
|
|
27
|
+
InvalidEnvironment2,
|
|
28
|
+
loadEnvironments,
|
|
29
|
+
isEnvironment,
|
|
30
|
+
exports_environment,
|
|
31
|
+
InvalidApplicationDeclaration2,
|
|
32
|
+
DuplicateApplicationDeclaration2,
|
|
33
|
+
ApplicationConflict2,
|
|
34
|
+
UnregisteredApplicationReference2,
|
|
35
|
+
UnregisteredRepositoryDependency2,
|
|
36
|
+
UnregisteredUseCaseDependency2,
|
|
37
|
+
ApplicationNotStarted2,
|
|
38
|
+
ProviderNotBound2,
|
|
39
|
+
ApplicationAlreadyStarted2,
|
|
40
|
+
SchemaLockMismatch2,
|
|
41
|
+
InvalidSchemaMigration2,
|
|
42
|
+
InvalidApplicationEnvironment2,
|
|
43
|
+
exports_exceptions5,
|
|
44
|
+
startedApplication,
|
|
45
|
+
setStartedApplication,
|
|
46
|
+
exports_exceptions6,
|
|
47
|
+
logProviderNameOf,
|
|
48
|
+
checkLogSettings,
|
|
49
|
+
silentLogger,
|
|
50
|
+
applicationLogger,
|
|
51
|
+
isLoggerDeclaration,
|
|
52
|
+
ApplicationLoggerKey,
|
|
53
|
+
resolveLogger,
|
|
54
|
+
clock,
|
|
55
|
+
elapsed,
|
|
56
|
+
logFailure,
|
|
57
|
+
writeLog,
|
|
58
|
+
InvalidRecordPlugin2,
|
|
59
|
+
UnknownRecordPlugin2,
|
|
60
|
+
IncompatibleRecordPlugin2,
|
|
61
|
+
exports_exceptions7,
|
|
62
|
+
propertyOf2,
|
|
63
|
+
optionalOf2,
|
|
64
|
+
blueprintBuilderOf2,
|
|
65
|
+
isVersion,
|
|
66
|
+
satisfies,
|
|
67
|
+
pluginsOf2,
|
|
68
|
+
isRecordPlugin,
|
|
69
|
+
recordPluginOf2,
|
|
70
|
+
recordBuilderOf2,
|
|
71
|
+
recordOf3,
|
|
72
|
+
exports_exceptions8,
|
|
73
|
+
vouching,
|
|
74
|
+
entityBuilderOf2,
|
|
75
|
+
isEntityClass2,
|
|
76
|
+
eventHasHandler,
|
|
77
|
+
lifecycleEventsOf,
|
|
78
|
+
EntitySettings2,
|
|
79
|
+
SCHEMA_VERSION_KEY2,
|
|
80
|
+
isVersionedEntityClass,
|
|
81
|
+
identityKeysOf,
|
|
82
|
+
InvalidRepositoryDeclaration2,
|
|
83
|
+
InvalidRepositoryData2,
|
|
84
|
+
RepositoryConcurrencyConflict2,
|
|
85
|
+
exports_exceptions9,
|
|
86
|
+
findState,
|
|
87
|
+
contextOf,
|
|
88
|
+
inspectRepository,
|
|
89
|
+
bindRepository,
|
|
90
|
+
providerOfRepository,
|
|
91
|
+
exports_exceptions10,
|
|
92
|
+
plainOf,
|
|
93
|
+
routeDeclarationOf,
|
|
94
|
+
InvalidOutputStage2,
|
|
95
|
+
DuplicateOperation2,
|
|
96
|
+
Unauthenticated2,
|
|
97
|
+
Forbidden2,
|
|
98
|
+
exports_exceptions11,
|
|
99
|
+
transformOf2,
|
|
100
|
+
stageDescriptionOf2,
|
|
101
|
+
isStage,
|
|
102
|
+
stageCatalogOf2,
|
|
103
|
+
jwtOptionsIssue,
|
|
104
|
+
jwtOf2
|
|
105
|
+
} from "./index-xegv6a1p.js";
|
|
106
|
+
import {
|
|
107
|
+
PublisherNotBound2,
|
|
108
|
+
exports_exceptions,
|
|
109
|
+
guard2,
|
|
110
|
+
ScopeKey,
|
|
111
|
+
providerDescriptionOf2,
|
|
112
|
+
fullUpcastPlanOf,
|
|
113
|
+
upcast,
|
|
114
|
+
resolveReferences,
|
|
115
|
+
isRepository,
|
|
116
|
+
InvalidUseCasePlugin2,
|
|
117
|
+
UnknownUseCasePlugin2,
|
|
118
|
+
IncompatibleUseCasePlugin2,
|
|
119
|
+
UseCaseEntityNotFound2,
|
|
120
|
+
UseCaseEntityConflict2,
|
|
121
|
+
UseCaseInvalidCredentials2,
|
|
122
|
+
exports_exceptions2,
|
|
123
|
+
isPlain,
|
|
124
|
+
PropertyInput2,
|
|
125
|
+
isPublicAccess,
|
|
126
|
+
accessDeclarationOf,
|
|
127
|
+
interpretedStepsOf,
|
|
128
|
+
InvalidUseCaseDeclaration2,
|
|
129
|
+
isUseCaseClass2,
|
|
130
|
+
exports_use_case,
|
|
131
|
+
isUseCasePlugin2,
|
|
132
|
+
useCaseOriginOf2,
|
|
133
|
+
useCasePluginOf2,
|
|
134
|
+
eventPolicyOf2,
|
|
135
|
+
eventPolicyDescriptionOf2,
|
|
136
|
+
eventPolicyPublishersOf,
|
|
137
|
+
eventPolicyCatalogOf2
|
|
138
|
+
} from "./index-3aae5jfq.js";
|
|
139
|
+
import {
|
|
140
|
+
schemaLockOf2,
|
|
141
|
+
diffSchemas2,
|
|
142
|
+
schemaLockProblemsOf2,
|
|
143
|
+
outputValueOf2,
|
|
144
|
+
openapi2
|
|
145
|
+
} from "./index-hg1z2s2v.js";
|
|
146
|
+
|
|
147
|
+
// src/core/command/exceptions.ts
|
|
148
|
+
var exports_exceptions12 = {};
|
|
149
|
+
__export(exports_exceptions12, {
|
|
150
|
+
CommandEventFailed: () => CommandEventFailed2,
|
|
151
|
+
InvalidCommandContext: () => InvalidCommandContext2,
|
|
152
|
+
InvalidCommandDeclaration: () => InvalidCommandDeclaration2,
|
|
153
|
+
InvalidCommandEnvironment: () => InvalidCommandEnvironment2,
|
|
154
|
+
UnknownCommandOperation: () => UnknownCommandOperation2
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// src/core/event-policy/publishers.ts
|
|
158
|
+
var ApplicationPublishersKey = Symbol("afora.application.publishers");
|
|
159
|
+
var boundPublishers = new WeakMap;
|
|
160
|
+
function bindPublishers(command, publishers) {
|
|
161
|
+
boundPublishers.set(command, publishers);
|
|
162
|
+
}
|
|
163
|
+
function isEventPublisher(value) {
|
|
164
|
+
return typeof value === "object" && value !== null && typeof value.publish === "function";
|
|
165
|
+
}
|
|
166
|
+
function resolvePublisher(command, name, source) {
|
|
167
|
+
const publishers = boundPublishers.get(command) ?? startedApplication()?.[ApplicationPublishersKey];
|
|
168
|
+
const publisher = publishers && Object.hasOwn(publishers, name) ? publishers[name] : undefined;
|
|
169
|
+
if (!publisher) {
|
|
170
|
+
throw new PublisherNotBound2({ source, details: { publisher: name } });
|
|
171
|
+
}
|
|
172
|
+
return publisher;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/core/use-case/plugin-catalog.ts
|
|
176
|
+
function isUseCaseCatalog2(value) {
|
|
177
|
+
return typeof value === "object" && value !== null && value[Kind2] === "use-case-catalog";
|
|
178
|
+
}
|
|
179
|
+
function useCaseCatalogOf2(...plugins) {
|
|
180
|
+
const byName = {};
|
|
181
|
+
const context = {};
|
|
182
|
+
for (const plugin of plugins) {
|
|
183
|
+
if (!isUseCasePlugin2(plugin)) {
|
|
184
|
+
throw new InvalidUseCasePlugin2({
|
|
185
|
+
source: "catalog",
|
|
186
|
+
details: {
|
|
187
|
+
reason: "a catalog takes plugins made by `useCasePluginOf`"
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (Object.hasOwn(byName, plugin.name)) {
|
|
192
|
+
throw new InvalidUseCasePlugin2({
|
|
193
|
+
source: plugin.name,
|
|
194
|
+
details: { reason: "the catalog has another plugin with its name" }
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
byName[plugin.name] = plugin;
|
|
198
|
+
context[plugin.name] = plugin.make;
|
|
199
|
+
}
|
|
200
|
+
const find = (name) => {
|
|
201
|
+
const plugin = Object.hasOwn(byName, name) ? byName[name] : undefined;
|
|
202
|
+
if (!plugin) {
|
|
203
|
+
throw new UnknownUseCasePlugin2({ source: "catalog", details: { name } });
|
|
204
|
+
}
|
|
205
|
+
return plugin;
|
|
206
|
+
};
|
|
207
|
+
return Object.freeze({
|
|
208
|
+
[Kind2]: "use-case-catalog",
|
|
209
|
+
plugins: Object.freeze(byName),
|
|
210
|
+
list: Object.freeze([...plugins]),
|
|
211
|
+
context: Object.freeze(context),
|
|
212
|
+
resolve(reference) {
|
|
213
|
+
const at = typeof reference === "string" ? reference.lastIndexOf("@") : -1;
|
|
214
|
+
if (at <= 0)
|
|
215
|
+
return find(String(reference));
|
|
216
|
+
const plugin = find(reference.slice(0, at));
|
|
217
|
+
const version = reference.slice(at + 1);
|
|
218
|
+
if (!isVersion(version)) {
|
|
219
|
+
throw new IncompatibleUseCasePlugin2({
|
|
220
|
+
source: plugin.name,
|
|
221
|
+
details: { reason: `"${version}" is not a semantic version` }
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (!satisfies(plugin.version, version)) {
|
|
225
|
+
throw new IncompatibleUseCasePlugin2({
|
|
226
|
+
source: plugin.name,
|
|
227
|
+
details: {
|
|
228
|
+
reason: `version ${plugin.version} does not satisfy ${version}`
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return plugin;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/core/command/exceptions.ts
|
|
238
|
+
var InvalidCommandDeclaration2 = exceptionOf2({
|
|
239
|
+
layer: "command" /* Command */,
|
|
240
|
+
name: "InvalidCommandDeclaration",
|
|
241
|
+
message: ({ reason }) => reason
|
|
242
|
+
});
|
|
243
|
+
var UnknownCommandOperation2 = exceptionOf2({
|
|
244
|
+
layer: "command" /* Command */,
|
|
245
|
+
name: "UnknownCommandOperation",
|
|
246
|
+
message: ({ operation }) => `Unknown command operation "${operation}".`
|
|
247
|
+
});
|
|
248
|
+
var InvalidCommandContext2 = exceptionOf2({
|
|
249
|
+
layer: "command" /* Command */,
|
|
250
|
+
name: "InvalidCommandContext",
|
|
251
|
+
message: (_details) => "Invalid Command context."
|
|
252
|
+
});
|
|
253
|
+
var InvalidCommandEnvironment2 = exceptionOf2({
|
|
254
|
+
layer: "command" /* Command */,
|
|
255
|
+
name: "InvalidCommandEnvironment",
|
|
256
|
+
message: (_details) => "Invalid Command environment."
|
|
257
|
+
});
|
|
258
|
+
var CommandEventFailed2 = exceptionOf2({
|
|
259
|
+
layer: "command" /* Command */,
|
|
260
|
+
name: "CommandEventFailed",
|
|
261
|
+
message: ({
|
|
262
|
+
operation,
|
|
263
|
+
event
|
|
264
|
+
}) => event === undefined ? `The event handler of "${operation}" failed after the call succeeded.` : `The event "${event}" of "${operation}" failed after the call succeeded.`
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// src/core/command/command.ts
|
|
268
|
+
var verifiers = new WeakMap;
|
|
269
|
+
function verifyCommand(command) {
|
|
270
|
+
verifiers.get(command)?.();
|
|
271
|
+
}
|
|
272
|
+
function commandTransactionProvider(command) {
|
|
273
|
+
return verifiers.get(command)?.();
|
|
274
|
+
}
|
|
275
|
+
var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
276
|
+
function repositoryProvidersOf(useCases, declared) {
|
|
277
|
+
const providers = new Map;
|
|
278
|
+
const seen = new Set;
|
|
279
|
+
const add = (state) => {
|
|
280
|
+
if (declared && !state.explicitProvider)
|
|
281
|
+
return;
|
|
282
|
+
const { provider, name } = state;
|
|
283
|
+
const names = providers.get(provider) ?? [];
|
|
284
|
+
if (!names.includes(name))
|
|
285
|
+
names.push(name);
|
|
286
|
+
providers.set(provider, names);
|
|
287
|
+
};
|
|
288
|
+
const visit = (UseCase) => {
|
|
289
|
+
if (seen.has(UseCase))
|
|
290
|
+
return;
|
|
291
|
+
seen.add(UseCase);
|
|
292
|
+
for (const dependency of Object.values(UseCase.dependencies)) {
|
|
293
|
+
if (isUseCaseClass2(dependency)) {
|
|
294
|
+
visit(dependency);
|
|
295
|
+
} else if (isRepository(dependency)) {
|
|
296
|
+
const state = findState(dependency);
|
|
297
|
+
if (!state)
|
|
298
|
+
continue;
|
|
299
|
+
add(state);
|
|
300
|
+
for (const related of state.relations.values())
|
|
301
|
+
add(related);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
for (const UseCase of Object.values(useCases))
|
|
306
|
+
visit(UseCase);
|
|
307
|
+
return providers;
|
|
308
|
+
}
|
|
309
|
+
function transactionProviderOf(name, useCases, declared = false) {
|
|
310
|
+
const providers = repositoryProvidersOf(useCases, declared);
|
|
311
|
+
const invalid = (reason, repositories) => {
|
|
312
|
+
throw new InvalidCommandDeclaration2({
|
|
313
|
+
source: name,
|
|
314
|
+
details: { reason, repositories }
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
if (providers.size > 1) {
|
|
318
|
+
const repositories = [...providers.values()].flat();
|
|
319
|
+
invalid(`A transaction needs every repository to share one provider; found ${providers.size} (${repositories.join(", ")}).`, repositories);
|
|
320
|
+
}
|
|
321
|
+
const [entry] = providers;
|
|
322
|
+
if (!entry)
|
|
323
|
+
return;
|
|
324
|
+
const [provider, repositories] = entry;
|
|
325
|
+
if (typeof provider.transaction !== "function" || provider.capabilities?.transactions === false) {
|
|
326
|
+
invalid(`The provider of ${repositories.join(", ")} does not support transactions.`, repositories);
|
|
327
|
+
}
|
|
328
|
+
return provider;
|
|
329
|
+
}
|
|
330
|
+
function narrowed(UseCase, context) {
|
|
331
|
+
if (!UseCase.context || typeof context !== "object" || context === null) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const picked = {};
|
|
335
|
+
for (const key of Object.keys(UseCase.context.properties)) {
|
|
336
|
+
if (Object.hasOwn(context, key)) {
|
|
337
|
+
picked[key] = context[key];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return picked;
|
|
341
|
+
}
|
|
342
|
+
function callEnvironment(source, environments, given) {
|
|
343
|
+
if (typeof given !== "object" || given === null || Array.isArray(given)) {
|
|
344
|
+
throw new InvalidCommandEnvironment2({
|
|
345
|
+
source,
|
|
346
|
+
details: { keys: [], issues: [] }
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
const values = given;
|
|
350
|
+
const unknownKeys = Object.keys(values).filter((key) => !environments.some((environment) => Object.hasOwn(environment.blueprint.properties, key)));
|
|
351
|
+
if (unknownKeys.length > 0) {
|
|
352
|
+
throw new InvalidCommandEnvironment2({
|
|
353
|
+
source,
|
|
354
|
+
details: { keys: unknownKeys, issues: [] }
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const byEnvironment = new Map;
|
|
358
|
+
const merged = {};
|
|
359
|
+
for (const environment of environments) {
|
|
360
|
+
const keys = Object.keys(environment.blueprint.properties);
|
|
361
|
+
const picked = {};
|
|
362
|
+
for (const key of keys) {
|
|
363
|
+
if (Object.hasOwn(values, key))
|
|
364
|
+
picked[key] = values[key];
|
|
365
|
+
}
|
|
366
|
+
const complete = keys.every((key) => Object.hasOwn(picked, key));
|
|
367
|
+
const base = Object.keys(picked).length === 0 || !complete ? environment.load() : {};
|
|
368
|
+
if (Object.keys(picked).length === 0) {
|
|
369
|
+
Object.assign(merged, base);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const parsed = environment.blueprint.safeParse({ ...base, ...picked });
|
|
373
|
+
if (parsed.issues) {
|
|
374
|
+
throw new InvalidCommandEnvironment2({
|
|
375
|
+
source,
|
|
376
|
+
details: {
|
|
377
|
+
keys: [
|
|
378
|
+
...new Set(parsed.issues.map((issue) => String(issue.path[0] ?? "")))
|
|
379
|
+
],
|
|
380
|
+
issues: parsed.issues
|
|
381
|
+
},
|
|
382
|
+
cause: parsed.issues.find((issue) => issue.cause)?.cause
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
const value = Object.freeze(parsed.value);
|
|
386
|
+
byEnvironment.set(environment, value);
|
|
387
|
+
Object.assign(merged, value);
|
|
388
|
+
}
|
|
389
|
+
return { byEnvironment, merged: Object.freeze(merged) };
|
|
390
|
+
}
|
|
391
|
+
function declareCommand(given, generics) {
|
|
392
|
+
const { name, description, context, onEventError } = given;
|
|
393
|
+
const source = typeof name === "string" ? name : "commandOf";
|
|
394
|
+
const picked = given.useCases;
|
|
395
|
+
const useCases = typeof picked === "function" && !isUseCaseClass2(picked) ? guard2({ layer: "command" /* Command */, source }, () => picked(generics)) : picked;
|
|
396
|
+
const settings = given;
|
|
397
|
+
const invalid = (reason) => {
|
|
398
|
+
throw new InvalidCommandDeclaration2({ source, details: { reason } });
|
|
399
|
+
};
|
|
400
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
401
|
+
invalid("Expected name to be a non-empty string.");
|
|
402
|
+
}
|
|
403
|
+
if (description !== undefined && typeof description !== "string") {
|
|
404
|
+
invalid("Expected description to be a string.");
|
|
405
|
+
}
|
|
406
|
+
const tags = settings.tags === undefined ? [] : settings.tags;
|
|
407
|
+
if (!Array.isArray(tags) || tags.some((tag) => typeof tag !== "string") || new Set(tags).size !== tags.length) {
|
|
408
|
+
invalid("Expected tags to be distinct strings.");
|
|
409
|
+
}
|
|
410
|
+
if (context !== undefined && (!isBlueprint(context) || context.optional)) {
|
|
411
|
+
invalid("Expected context to be a non-optional blueprint.");
|
|
412
|
+
}
|
|
413
|
+
if (typeof useCases !== "object" || useCases === null || Array.isArray(useCases) || Reflect.ownKeys(useCases).length === 0) {
|
|
414
|
+
invalid("Expected useCases to be a non-empty object of use case classes, or a function returning one.");
|
|
415
|
+
}
|
|
416
|
+
for (const key of Reflect.ownKeys(useCases)) {
|
|
417
|
+
if (typeof key !== "string" || !IDENTIFIER.test(key) || !isUseCaseClass2(useCases[key])) {
|
|
418
|
+
invalid("Expected the operations to be identifiers holding use case classes.");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const transaction = settings.transaction === undefined ? false : settings.transaction;
|
|
422
|
+
if (typeof transaction !== "boolean") {
|
|
423
|
+
invalid("Expected transaction to be a boolean.");
|
|
424
|
+
}
|
|
425
|
+
const declaredEnvironments = settings.environmentDependencies;
|
|
426
|
+
const environments = Object.freeze(declaredEnvironments === undefined ? [] : isEnvironment(declaredEnvironments) ? [declaredEnvironments] : Array.isArray(declaredEnvironments) ? [...declaredEnvironments] : [null]);
|
|
427
|
+
if (!environments.every(isEnvironment) || new Set(environments).size !== environments.length) {
|
|
428
|
+
invalid("Expected environmentDependencies to be distinct environments made by environmentOf.");
|
|
429
|
+
}
|
|
430
|
+
const events = settings.events === undefined ? "after-commit" : settings.events;
|
|
431
|
+
if (events !== "after-commit" && events !== "manual" && typeof events !== "function") {
|
|
432
|
+
invalid('Expected events to be "after-commit", "manual" or a function.');
|
|
433
|
+
}
|
|
434
|
+
if (onEventError !== undefined) {
|
|
435
|
+
if (typeof onEventError !== "function") {
|
|
436
|
+
invalid("Expected onEventError to be a function.");
|
|
437
|
+
}
|
|
438
|
+
if (events !== "after-commit") {
|
|
439
|
+
invalid('onEventError only applies to the "after-commit" policy.');
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (transaction)
|
|
443
|
+
transactionProviderOf(source, useCases, true);
|
|
444
|
+
let resolved;
|
|
445
|
+
const providerOfCall = () => {
|
|
446
|
+
if (!transaction)
|
|
447
|
+
return;
|
|
448
|
+
resolved ??= { provider: transactionProviderOf(source, useCases) };
|
|
449
|
+
return resolved.provider;
|
|
450
|
+
};
|
|
451
|
+
const operations = Object.freeze(Object.keys(useCases));
|
|
452
|
+
const frozenUseCases = Object.freeze({ ...useCases });
|
|
453
|
+
const perform = async (op, input, options) => {
|
|
454
|
+
if (typeof op !== "string" || !Object.hasOwn(frozenUseCases, op)) {
|
|
455
|
+
throw new UnknownCommandOperation2({
|
|
456
|
+
source,
|
|
457
|
+
details: { operation: String(op) }
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
const where = `${source}.${op}`;
|
|
461
|
+
const UseCase = frozenUseCases[op];
|
|
462
|
+
let callContext = options?.context;
|
|
463
|
+
let given = callContext;
|
|
464
|
+
if (context) {
|
|
465
|
+
const checked = context.safeParse(callContext);
|
|
466
|
+
if (checked.issues) {
|
|
467
|
+
throw new InvalidCommandContext2({
|
|
468
|
+
source: where,
|
|
469
|
+
details: { issues: checked.issues },
|
|
470
|
+
cause: checked.issues.find((issue) => issue.cause)?.cause
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
callContext = checked.value;
|
|
474
|
+
given = narrowed(UseCase, callContext);
|
|
475
|
+
}
|
|
476
|
+
const overrides = options?.env === undefined ? undefined : callEnvironment(where, environments, options.env);
|
|
477
|
+
const logger = resolveLogger(undefined, UseCase);
|
|
478
|
+
const withEnvironments = (scope) => {
|
|
479
|
+
const environmentsOfCall = overrides === undefined || overrides.byEnvironment.size === 0 ? scope?.environments : overrides.byEnvironment;
|
|
480
|
+
if (!environmentsOfCall && !logger)
|
|
481
|
+
return scope;
|
|
482
|
+
return {
|
|
483
|
+
providers: scope?.providers ?? new Map,
|
|
484
|
+
...environmentsOfCall ? { environments: environmentsOfCall } : {},
|
|
485
|
+
...logger ? { logger } : {}
|
|
486
|
+
};
|
|
487
|
+
};
|
|
488
|
+
const run = async (declared) => {
|
|
489
|
+
const scope = withEnvironments(declared);
|
|
490
|
+
const useCase = new UseCase(input, { context: given, [ScopeKey]: scope });
|
|
491
|
+
const value = await useCase.execute();
|
|
492
|
+
return { value, events: useCase.pullEvents() };
|
|
493
|
+
};
|
|
494
|
+
const provider = providerOfCall();
|
|
495
|
+
const started = clock();
|
|
496
|
+
const outcome = await guard2({ layer: "command" /* Command */, source: where }, () => provider?.transaction ? provider.transaction((scoped) => run({ providers: new Map([[provider, scoped]]) })) : run(undefined)).catch((error) => {
|
|
497
|
+
logFailure(logger, "command-failed", { command: where, ms: elapsed(started) }, error);
|
|
498
|
+
throw error;
|
|
499
|
+
});
|
|
500
|
+
writeLog(logger, "debug", "command", {
|
|
501
|
+
command: where,
|
|
502
|
+
ms: elapsed(started)
|
|
503
|
+
});
|
|
504
|
+
return {
|
|
505
|
+
...outcome,
|
|
506
|
+
context: callContext,
|
|
507
|
+
env: () => overrides?.merged ?? loadEnvironments(environments),
|
|
508
|
+
logger: logger ?? silentLogger
|
|
509
|
+
};
|
|
510
|
+
};
|
|
511
|
+
const command = {
|
|
512
|
+
[Kind2]: "command",
|
|
513
|
+
commandName: name,
|
|
514
|
+
description,
|
|
515
|
+
tags: Object.freeze([...tags]),
|
|
516
|
+
context,
|
|
517
|
+
useCases: frozenUseCases,
|
|
518
|
+
transaction,
|
|
519
|
+
environments,
|
|
520
|
+
get env() {
|
|
521
|
+
return loadEnvironments(environments);
|
|
522
|
+
},
|
|
523
|
+
events,
|
|
524
|
+
onEventError,
|
|
525
|
+
operations,
|
|
526
|
+
async execute(op, input, options) {
|
|
527
|
+
const { value, events } = await perform(op, input, options);
|
|
528
|
+
return { value, events };
|
|
529
|
+
},
|
|
530
|
+
async run(op, input, options) {
|
|
531
|
+
const {
|
|
532
|
+
value,
|
|
533
|
+
events: raised,
|
|
534
|
+
context: callContext,
|
|
535
|
+
env,
|
|
536
|
+
logger
|
|
537
|
+
} = await perform(op, input, options);
|
|
538
|
+
await applyPolicy(command, events, onEventError, {
|
|
539
|
+
operation: op,
|
|
540
|
+
raised,
|
|
541
|
+
context: callContext,
|
|
542
|
+
env,
|
|
543
|
+
logger
|
|
544
|
+
});
|
|
545
|
+
return value;
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
Object.freeze(command);
|
|
549
|
+
verifiers.set(command, providerOfCall);
|
|
550
|
+
return command;
|
|
551
|
+
}
|
|
552
|
+
function commandBuilderOf2(...catalogs) {
|
|
553
|
+
if (!catalogs.every(isUseCaseCatalog2)) {
|
|
554
|
+
throw new InvalidCommandDeclaration2({
|
|
555
|
+
source: "commandBuilderOf",
|
|
556
|
+
details: {
|
|
557
|
+
reason: "Expected catalogs made by useCaseCatalogOf."
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
const generics = Object.freeze(Object.assign({}, ...catalogs.map((catalog) => catalog.context)));
|
|
562
|
+
return (settings) => declareCommand(settings, generics);
|
|
563
|
+
}
|
|
564
|
+
var commandOf = commandBuilderOf2();
|
|
565
|
+
async function applyPolicy(command, policy, onEventError, call) {
|
|
566
|
+
const { operation, raised } = call;
|
|
567
|
+
const source = `${command.commandName}.${operation}`;
|
|
568
|
+
if (policy === "manual")
|
|
569
|
+
return;
|
|
570
|
+
const info = {
|
|
571
|
+
command,
|
|
572
|
+
operation,
|
|
573
|
+
context: call.context,
|
|
574
|
+
get env() {
|
|
575
|
+
return call.env();
|
|
576
|
+
},
|
|
577
|
+
logger: call.logger,
|
|
578
|
+
publisher: (name) => resolvePublisher(command, name, source)
|
|
579
|
+
};
|
|
580
|
+
if (typeof policy === "function") {
|
|
581
|
+
try {
|
|
582
|
+
await policy(raised, info);
|
|
583
|
+
} catch (cause) {
|
|
584
|
+
if (cause instanceof Exception2 && cause.name === "CommandEventFailed") {
|
|
585
|
+
throw cause;
|
|
586
|
+
}
|
|
587
|
+
throw new CommandEventFailed2({
|
|
588
|
+
source,
|
|
589
|
+
cause,
|
|
590
|
+
details: { operation, handled: 0, remaining: raised.length }
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
for (const [index, event] of raised.entries()) {
|
|
596
|
+
try {
|
|
597
|
+
await event.handle({ logger: call.logger });
|
|
598
|
+
} catch (cause) {
|
|
599
|
+
if (!onEventError) {
|
|
600
|
+
throw new CommandEventFailed2({
|
|
601
|
+
source,
|
|
602
|
+
cause,
|
|
603
|
+
details: {
|
|
604
|
+
operation,
|
|
605
|
+
event: event.name,
|
|
606
|
+
handled: index,
|
|
607
|
+
remaining: raised.length - index - 1
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
await onEventError(cause, event, info);
|
|
613
|
+
} catch (failure) {
|
|
614
|
+
throw new CommandEventFailed2({
|
|
615
|
+
source,
|
|
616
|
+
cause: failure,
|
|
617
|
+
details: {
|
|
618
|
+
operation,
|
|
619
|
+
event: event.name,
|
|
620
|
+
handled: index,
|
|
621
|
+
remaining: raised.length - index - 1
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function isCommand2(value) {
|
|
629
|
+
return typeof value === "object" && value !== null && value[Kind2] === "command";
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/core/exception/exported.ts
|
|
633
|
+
var exported = new WeakSet;
|
|
634
|
+
var collected = false;
|
|
635
|
+
function markExported(...classes) {
|
|
636
|
+
for (const Declared of classes) {
|
|
637
|
+
if (isDeclaredException(Declared))
|
|
638
|
+
exported.add(Declared);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function isExportedException(Declared) {
|
|
642
|
+
if (!collected) {
|
|
643
|
+
collected = true;
|
|
644
|
+
for (const module of [
|
|
645
|
+
exports_exceptions5,
|
|
646
|
+
exports_exceptions3,
|
|
647
|
+
exports_exceptions12,
|
|
648
|
+
exports_exceptions8,
|
|
649
|
+
exports_environment,
|
|
650
|
+
exports_exceptions,
|
|
651
|
+
exports_exceptions6,
|
|
652
|
+
exports_exceptions10,
|
|
653
|
+
exports_exceptions11,
|
|
654
|
+
exports_exceptions4,
|
|
655
|
+
exports_exceptions7,
|
|
656
|
+
exports_exceptions9,
|
|
657
|
+
exports_use_case,
|
|
658
|
+
exports_exceptions2
|
|
659
|
+
]) {
|
|
660
|
+
markExported(...Object.values(module));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return typeof Declared === "function" && exported.has(Declared);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/core/output/access.ts
|
|
667
|
+
function readerOf(path) {
|
|
668
|
+
if (path === "subject")
|
|
669
|
+
return (principal) => principal.subject;
|
|
670
|
+
if (path === "scopes")
|
|
671
|
+
return (principal) => [...principal.scopes];
|
|
672
|
+
const claim = path.slice("claims.".length);
|
|
673
|
+
return (principal) => principal.claims && Object.hasOwn(principal.claims, claim) ? principal.claims[claim] : undefined;
|
|
674
|
+
}
|
|
675
|
+
function accessGuardOf(id, access, context) {
|
|
676
|
+
if (isPublicAccess(access)) {
|
|
677
|
+
return (_input, options) => options === undefined ? undefined : { context: options.context };
|
|
678
|
+
}
|
|
679
|
+
const scopes = access.scopes ?? [];
|
|
680
|
+
const owner = access.owner;
|
|
681
|
+
const ownerOf = owner ? readerOf(owner.claim === undefined ? "subject" : `claims.${owner.claim}`) : undefined;
|
|
682
|
+
const fills = Object.entries(access.context ?? {}).map(([key, path]) => [key, readerOf(path)]);
|
|
683
|
+
for (const [key] of fills) {
|
|
684
|
+
if (!context || !Object.hasOwn(context.properties, key)) {
|
|
685
|
+
throw new InvalidOutputStage2({
|
|
686
|
+
source: id,
|
|
687
|
+
details: {
|
|
688
|
+
reason: `The access of ${id} fills the context key "${key}", which the context of the operation does not declare.`
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return (input, options) => {
|
|
694
|
+
const principal = options?.principal;
|
|
695
|
+
if (principal === undefined || principal === null || typeof principal.subject !== "string" || !Array.isArray(principal.scopes)) {
|
|
696
|
+
throw new Unauthenticated2({ source: id });
|
|
697
|
+
}
|
|
698
|
+
if (scopes.length > 0) {
|
|
699
|
+
const missing = scopes.filter((scope) => !principal.scopes.includes(scope));
|
|
700
|
+
if (missing.length > 0) {
|
|
701
|
+
throw new Forbidden2({
|
|
702
|
+
source: id,
|
|
703
|
+
details: { reason: "scope", scopes: missing }
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (owner && ownerOf) {
|
|
708
|
+
const expected = ownerOf(principal);
|
|
709
|
+
const given = typeof input === "object" && input !== null ? input[owner.input] : undefined;
|
|
710
|
+
if (given === undefined || given === null || expected === undefined || expected === null || String(given) !== String(expected)) {
|
|
711
|
+
throw new Forbidden2({ source: id, details: { reason: "owner" } });
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (fills.length === 0) {
|
|
715
|
+
return options?.context === undefined ? undefined : { context: options.context };
|
|
716
|
+
}
|
|
717
|
+
const given = options?.context;
|
|
718
|
+
const filled = {
|
|
719
|
+
...typeof given === "object" && given !== null ? given : {}
|
|
720
|
+
};
|
|
721
|
+
for (const [key, read] of fills) {
|
|
722
|
+
const value = read(principal);
|
|
723
|
+
if (value === undefined)
|
|
724
|
+
delete filled[key];
|
|
725
|
+
else
|
|
726
|
+
filled[key] = value;
|
|
727
|
+
}
|
|
728
|
+
return { context: filled };
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/core/output/output.ts
|
|
733
|
+
function isApplicationLike(value) {
|
|
734
|
+
return typeof value === "object" && value !== null && value[Kind2] === "application";
|
|
735
|
+
}
|
|
736
|
+
function invalid(source, reason) {
|
|
737
|
+
throw new InvalidOutputStage2({ source, details: { reason } });
|
|
738
|
+
}
|
|
739
|
+
var PATCHABLE = ["meta", "tags", "description", "deprecated", "invoke"];
|
|
740
|
+
function operationOf2(operation, patch) {
|
|
741
|
+
const source = operation?.id ?? "operationOf";
|
|
742
|
+
if (operation?.kind !== "operation" || operation.version !== 1) {
|
|
743
|
+
invalid(source, "Expected an operation descriptor.");
|
|
744
|
+
}
|
|
745
|
+
if (typeof patch !== "object" || patch === null || Array.isArray(patch)) {
|
|
746
|
+
invalid(source, "Expected the patch to be an object.");
|
|
747
|
+
}
|
|
748
|
+
for (const key of Reflect.ownKeys(patch)) {
|
|
749
|
+
if (typeof key !== "string" || !PATCHABLE.includes(key)) {
|
|
750
|
+
invalid(source, `"${String(key)}" cannot be patched; only ${PATCHABLE.join(", ")} can.`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const { meta, tags, description, deprecated, invoke } = patch;
|
|
754
|
+
if ("meta" in patch && (typeof meta !== "object" || meta === null || Array.isArray(meta))) {
|
|
755
|
+
invalid(source, "Expected meta to be an object.");
|
|
756
|
+
}
|
|
757
|
+
if ("tags" in patch && (!Array.isArray(tags) || tags.some((tag) => typeof tag !== "string"))) {
|
|
758
|
+
invalid(source, "Expected tags to be a list of strings.");
|
|
759
|
+
}
|
|
760
|
+
if ("description" in patch && description !== undefined && typeof description !== "string") {
|
|
761
|
+
invalid(source, "Expected description to be a string.");
|
|
762
|
+
}
|
|
763
|
+
if ("deprecated" in patch && typeof deprecated !== "boolean") {
|
|
764
|
+
invalid(source, "Expected deprecated to be a boolean.");
|
|
765
|
+
}
|
|
766
|
+
if ("invoke" in patch && typeof invoke !== "function") {
|
|
767
|
+
invalid(source, "Expected invoke to be a function.");
|
|
768
|
+
}
|
|
769
|
+
return Object.freeze({
|
|
770
|
+
...operation,
|
|
771
|
+
..."meta" in patch ? { meta: Object.freeze({ ...meta }) } : {},
|
|
772
|
+
..."tags" in patch ? { tags: Object.freeze([...tags ?? []]) } : {},
|
|
773
|
+
..."description" in patch ? { description } : {},
|
|
774
|
+
..."deprecated" in patch ? { deprecated } : {},
|
|
775
|
+
..."invoke" in patch ? { invoke } : {}
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
function operationsOfCommand(command) {
|
|
779
|
+
return command.operations.map((name) => {
|
|
780
|
+
const UseCase = command.useCases[name];
|
|
781
|
+
const context = command.context ?? UseCase.context;
|
|
782
|
+
const description = UseCase.description ?? command.description;
|
|
783
|
+
const id = `${command.commandName}.${name}`;
|
|
784
|
+
const run = command.run;
|
|
785
|
+
const guard = UseCase.access === undefined ? undefined : accessGuardOf(id, UseCase.access, context);
|
|
786
|
+
return Object.freeze({
|
|
787
|
+
kind: "operation",
|
|
788
|
+
version: 1,
|
|
789
|
+
id,
|
|
790
|
+
command: command.commandName,
|
|
791
|
+
name,
|
|
792
|
+
useCase: UseCase.useCaseName,
|
|
793
|
+
...description === undefined ? {} : { description },
|
|
794
|
+
tags: Object.freeze([
|
|
795
|
+
...new Set([...command.tags, ...UseCase.tags])
|
|
796
|
+
]),
|
|
797
|
+
deprecated: UseCase.deprecated,
|
|
798
|
+
input: UseCase.blueprint,
|
|
799
|
+
response: UseCase.response,
|
|
800
|
+
...context ? { context } : {},
|
|
801
|
+
errors: UseCase.errors,
|
|
802
|
+
...UseCase.route === undefined ? {} : { route: UseCase.route },
|
|
803
|
+
...UseCase.access === undefined ? {} : { access: UseCase.access },
|
|
804
|
+
meta: Object.freeze({}),
|
|
805
|
+
invoke: guard ? async (input, options) => run(name, input, guard(input, options)) : (input, options) => run(name, input, options && "principal" in options ? { context: options.context } : options)
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
function statusesOf(name, statuses) {
|
|
810
|
+
if (statuses === undefined)
|
|
811
|
+
return Object.freeze({});
|
|
812
|
+
if (typeof statuses !== "object" || statuses === null || Array.isArray(statuses)) {
|
|
813
|
+
invalid(name, "Expected statuses to be a map of exception names to statuses.");
|
|
814
|
+
}
|
|
815
|
+
for (const [exception, status] of Object.entries(statuses)) {
|
|
816
|
+
if (!exception || typeof status !== "number" || !Number.isInteger(status) || status < 400 || status > 599) {
|
|
817
|
+
invalid(name, `Expected the status of "${exception}" to be an integer between 400 and 599.`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return Object.freeze({ ...statuses });
|
|
821
|
+
}
|
|
822
|
+
function checkPipe(name, pipe) {
|
|
823
|
+
if (!Array.isArray(pipe))
|
|
824
|
+
invalid(name, "Expected pipe to be a list of stages.");
|
|
825
|
+
const stages = pipe;
|
|
826
|
+
stages.forEach((stage, index) => {
|
|
827
|
+
if (!isStage(stage)) {
|
|
828
|
+
invalid(name, `Expected pipe[${index}] to be a transform or sink stage.`);
|
|
829
|
+
}
|
|
830
|
+
if (stage.kind === "sink" && index !== stages.length - 1) {
|
|
831
|
+
invalid(name, `The sink at pipe[${index}] must be the last stage.`);
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
return Object.freeze([...stages]);
|
|
835
|
+
}
|
|
836
|
+
function transformed(name, operations, transforms, context) {
|
|
837
|
+
let current = operations;
|
|
838
|
+
for (const stage of transforms) {
|
|
839
|
+
if (stage.kind !== "transform")
|
|
840
|
+
continue;
|
|
841
|
+
const next = [];
|
|
842
|
+
for (const operation of current) {
|
|
843
|
+
const result = stage.apply(operation, context);
|
|
844
|
+
if (result === null)
|
|
845
|
+
continue;
|
|
846
|
+
if (typeof result !== "object" || result?.kind !== "operation" || result.id !== operation.id) {
|
|
847
|
+
invalid(name, `A transform must return the operation "${operation.id}" or null.`);
|
|
848
|
+
}
|
|
849
|
+
next.push(result);
|
|
850
|
+
}
|
|
851
|
+
current = next;
|
|
852
|
+
}
|
|
853
|
+
return Object.freeze([...current]);
|
|
854
|
+
}
|
|
855
|
+
function make(name, commands, operations, stages, statuses) {
|
|
856
|
+
const context = Object.freeze({
|
|
857
|
+
output: Object.freeze({ name, statuses })
|
|
858
|
+
});
|
|
859
|
+
let built;
|
|
860
|
+
const output = {
|
|
861
|
+
[Kind2]: "output",
|
|
862
|
+
outputName: name,
|
|
863
|
+
commands,
|
|
864
|
+
operations,
|
|
865
|
+
stages,
|
|
866
|
+
statuses,
|
|
867
|
+
build() {
|
|
868
|
+
if (built)
|
|
869
|
+
return built.value;
|
|
870
|
+
const sink = stages.at(-1);
|
|
871
|
+
if (sink?.kind !== "sink") {
|
|
872
|
+
invalid(name || "output", "The pipe has no sink to build.");
|
|
873
|
+
}
|
|
874
|
+
built = { value: sink.build(operations, context) };
|
|
875
|
+
return built.value;
|
|
876
|
+
},
|
|
877
|
+
pipe(...more) {
|
|
878
|
+
const next = checkPipe(name || "output", more);
|
|
879
|
+
const last = next.at(-1);
|
|
880
|
+
if (stages.at(-1)?.kind === "sink") {
|
|
881
|
+
invalid(name || "output", "Cannot add stages after the sink.");
|
|
882
|
+
}
|
|
883
|
+
const ops = transformed(name, operations, next, context);
|
|
884
|
+
if (last?.kind === "sink") {
|
|
885
|
+
return last.build(ops, context);
|
|
886
|
+
}
|
|
887
|
+
return make("", commands, ops, Object.freeze([...stages, ...next]), statuses);
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
return Object.freeze(output);
|
|
891
|
+
}
|
|
892
|
+
function outputOf2(settings) {
|
|
893
|
+
const { name, commands } = settings ?? {};
|
|
894
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
895
|
+
invalid("outputOf", "Expected name to be a non-empty string.");
|
|
896
|
+
}
|
|
897
|
+
const sources = Array.isArray(commands) ? commands : [commands];
|
|
898
|
+
if (!Array.isArray(commands) && isCommand2(commands) || sources.some((entry) => !isCommand2(entry) && !isApplicationLike(entry))) {
|
|
899
|
+
invalid(name, "Expected commands to be a list of commands made by commandOf or applications.");
|
|
900
|
+
}
|
|
901
|
+
const flat = [
|
|
902
|
+
...new Set(sources.flatMap((entry) => isCommand2(entry) ? [entry] : Object.values(entry.commands)))
|
|
903
|
+
];
|
|
904
|
+
const statuses = statusesOf(name, settings.statuses);
|
|
905
|
+
const stages = checkPipe(name, settings.pipe ?? []);
|
|
906
|
+
const all = flat.flatMap(operationsOfCommand);
|
|
907
|
+
const ids = new Set;
|
|
908
|
+
for (const { id } of all) {
|
|
909
|
+
if (ids.has(id))
|
|
910
|
+
throw new DuplicateOperation2({ source: name, details: { id } });
|
|
911
|
+
ids.add(id);
|
|
912
|
+
}
|
|
913
|
+
const context = { output: { name, statuses } };
|
|
914
|
+
const operations = transformed(name, all, stages, context);
|
|
915
|
+
return make(name, Object.freeze(flat), operations, stages, statuses);
|
|
916
|
+
}
|
|
917
|
+
function isOutput2(value) {
|
|
918
|
+
return typeof value === "object" && value !== null && value[Kind2] === "output";
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/core/use-case/steps/describe.ts
|
|
922
|
+
function cloneData(value) {
|
|
923
|
+
if (Array.isArray(value))
|
|
924
|
+
return value.map(cloneData);
|
|
925
|
+
if (isPlain(value)) {
|
|
926
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneData(item)]));
|
|
927
|
+
}
|
|
928
|
+
return value;
|
|
929
|
+
}
|
|
930
|
+
function describeSteps(steps, names) {
|
|
931
|
+
return steps.map((step) => {
|
|
932
|
+
const copy = cloneData({ ...step });
|
|
933
|
+
if (Object.hasOwn(step, "build"))
|
|
934
|
+
copy.build = names.record(step.build);
|
|
935
|
+
if (Object.hasOwn(step, "ensure"))
|
|
936
|
+
copy.error = names.error(step.error);
|
|
937
|
+
if (Object.hasOwn(step, "run"))
|
|
938
|
+
copy.fn = { kind: "opaque" };
|
|
939
|
+
return copy;
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// src/core/repository/migrate.ts
|
|
944
|
+
var aliases = new WeakMap;
|
|
945
|
+
function aliasOf(entity, name) {
|
|
946
|
+
let byName = aliases.get(entity);
|
|
947
|
+
if (!byName) {
|
|
948
|
+
byName = new Map;
|
|
949
|
+
aliases.set(entity, byName);
|
|
950
|
+
}
|
|
951
|
+
let alias = byName.get(name);
|
|
952
|
+
if (!alias) {
|
|
953
|
+
const Entity = entity;
|
|
954
|
+
alias = class extends Entity {
|
|
955
|
+
};
|
|
956
|
+
Object.defineProperty(alias, "name", { value: name });
|
|
957
|
+
Object.defineProperty(alias, "recordName", { value: name });
|
|
958
|
+
byName.set(name, alias);
|
|
959
|
+
}
|
|
960
|
+
return alias;
|
|
961
|
+
}
|
|
962
|
+
function previousContextOf(context, name) {
|
|
963
|
+
return Object.freeze({
|
|
964
|
+
...context,
|
|
965
|
+
entity: aliasOf(context.entity, name),
|
|
966
|
+
name: `${name}Repository`
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
async function walk(provider, context, pageSize, visit) {
|
|
970
|
+
const keyset = provider.capabilities?.keyset === true;
|
|
971
|
+
let after;
|
|
972
|
+
let offset = 0;
|
|
973
|
+
for (;; ) {
|
|
974
|
+
const query = {
|
|
975
|
+
orderBy: [{ key: "id", direction: "asc" }],
|
|
976
|
+
withDeleted: true,
|
|
977
|
+
...context.expiry === undefined ? {} : { withExpired: true },
|
|
978
|
+
limit: pageSize,
|
|
979
|
+
...keyset ? after === undefined ? {} : { after } : offset === 0 ? {} : { offset }
|
|
980
|
+
};
|
|
981
|
+
const rows = await provider.findMany(context, query);
|
|
982
|
+
if (rows.length === 0)
|
|
983
|
+
return;
|
|
984
|
+
const removed = await visit(rows);
|
|
985
|
+
after = [rows.at(-1).id];
|
|
986
|
+
offset += rows.length - removed;
|
|
987
|
+
if (rows.length < pageSize)
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
function issuesOf(source, error) {
|
|
992
|
+
const issues = error?.details?.issues;
|
|
993
|
+
if (Array.isArray(issues))
|
|
994
|
+
return issues;
|
|
995
|
+
const cause = error instanceof Exception2 && error.is(InvalidRepositoryData2) ? error.cause ?? error : error;
|
|
996
|
+
return [
|
|
997
|
+
{
|
|
998
|
+
code: "invalid_value",
|
|
999
|
+
path: [],
|
|
1000
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
1001
|
+
source,
|
|
1002
|
+
...cause instanceof Exception2 ? { cause } : {}
|
|
1003
|
+
}
|
|
1004
|
+
];
|
|
1005
|
+
}
|
|
1006
|
+
async function upcastPage(state, plan, rows) {
|
|
1007
|
+
const brought = rows.map((row) => {
|
|
1008
|
+
const report = {
|
|
1009
|
+
from: 1,
|
|
1010
|
+
renamed: [],
|
|
1011
|
+
filled: [],
|
|
1012
|
+
removed: []
|
|
1013
|
+
};
|
|
1014
|
+
try {
|
|
1015
|
+
return {
|
|
1016
|
+
row,
|
|
1017
|
+
current: upcast(state.name, plan, row, true, report),
|
|
1018
|
+
report
|
|
1019
|
+
};
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
return { row, invalid: issuesOf(state.name, error) };
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
const valid = brought.filter((item) => ("current" in item));
|
|
1025
|
+
const parse = (data) => state.entity.safeParse(data);
|
|
1026
|
+
const resolvedOf = async (items) => resolveReferences(state, items.map((item) => item.current), new Map);
|
|
1027
|
+
let resolved;
|
|
1028
|
+
try {
|
|
1029
|
+
resolved = await resolvedOf(valid);
|
|
1030
|
+
} catch {
|
|
1031
|
+
resolved = await Promise.all(valid.map(async (item) => {
|
|
1032
|
+
try {
|
|
1033
|
+
return (await resolvedOf([item]))[0];
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
return issuesOf(state.name, error);
|
|
1036
|
+
}
|
|
1037
|
+
}));
|
|
1038
|
+
}
|
|
1039
|
+
const checked = new Map;
|
|
1040
|
+
valid.forEach((item, index) => {
|
|
1041
|
+
const data = resolved[index];
|
|
1042
|
+
if (Array.isArray(data)) {
|
|
1043
|
+
checked.set(item.row, data);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const parsed = parse(data);
|
|
1047
|
+
if (parsed.issues)
|
|
1048
|
+
checked.set(item.row, parsed.issues);
|
|
1049
|
+
});
|
|
1050
|
+
return brought.map((item) => {
|
|
1051
|
+
const issues = checked.get(item.row);
|
|
1052
|
+
return issues === undefined ? item : { row: item.row, invalid: issues };
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
function writtenOf(state, plan, item, destructive) {
|
|
1056
|
+
const data = destructive ? item.current : upcast(state.name, plan, item.row, false);
|
|
1057
|
+
return plan.versioned ? { ...data, [SCHEMA_VERSION_KEY2]: plan.version } : data;
|
|
1058
|
+
}
|
|
1059
|
+
async function migrateRepository(state, options) {
|
|
1060
|
+
const { mode, destructive, pageSize } = options;
|
|
1061
|
+
const apply = mode === "apply";
|
|
1062
|
+
const { provider } = state;
|
|
1063
|
+
const context = contextOf(state);
|
|
1064
|
+
const plan = fullUpcastPlanOf(state.entity);
|
|
1065
|
+
const versioned = isVersionedEntityClass(state.entity) && provider.capabilities?.versioning === true;
|
|
1066
|
+
const own = recordNameOf2(state.entity);
|
|
1067
|
+
const previousNames = (state.entity[EntitySettings2]?.previousNames ?? []).filter((previous) => previous !== own && `${previous}Repository` !== context.name);
|
|
1068
|
+
const needs = ["findMany"];
|
|
1069
|
+
if (apply)
|
|
1070
|
+
needs.push("update");
|
|
1071
|
+
if (apply && previousNames.length > 0)
|
|
1072
|
+
needs.push("insert");
|
|
1073
|
+
if (apply && destructive && previousNames.length > 0)
|
|
1074
|
+
needs.push("remove");
|
|
1075
|
+
for (const need of needs) {
|
|
1076
|
+
if (typeof provider[need] !== "function") {
|
|
1077
|
+
throw new InvalidRepositoryDeclaration2({
|
|
1078
|
+
source: state.name,
|
|
1079
|
+
details: { reason: `The provider has no "${need}" to migrate.` }
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const tally = {
|
|
1084
|
+
scanned: 0,
|
|
1085
|
+
upToDate: 0,
|
|
1086
|
+
upgraded: 0,
|
|
1087
|
+
removalPending: 0,
|
|
1088
|
+
movedFromPreviousNames: 0,
|
|
1089
|
+
invalid: [],
|
|
1090
|
+
removedKeys: new Set
|
|
1091
|
+
};
|
|
1092
|
+
if (apply) {
|
|
1093
|
+
const migrated = await provider.migrate?.(context, options.plan);
|
|
1094
|
+
if (typeof migrated === "object" && migrated !== null && migrated.deferred === true) {
|
|
1095
|
+
return { ...reportOf(tally), deferred: true };
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
await walk(provider, context, pageSize, async (rows) => {
|
|
1099
|
+
for (const item of await upcastPage(state, plan, rows)) {
|
|
1100
|
+
tally.scanned += 1;
|
|
1101
|
+
if ("invalid" in item) {
|
|
1102
|
+
tally.invalid.push({ id: String(item.row.id), issues: item.invalid });
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
const { report } = item;
|
|
1106
|
+
for (const key of report.removed)
|
|
1107
|
+
tally.removedKeys.add(key);
|
|
1108
|
+
const pending = plan.versioned && report.from < plan.version || report.renamed.length > 0 || report.filled.length > 0;
|
|
1109
|
+
const removal = report.removed.length > 0;
|
|
1110
|
+
if (pending || removal && destructive) {
|
|
1111
|
+
tally.upgraded += 1;
|
|
1112
|
+
if (apply) {
|
|
1113
|
+
const expected = item.row.version;
|
|
1114
|
+
try {
|
|
1115
|
+
await provider.update(context, writtenOf(state, plan, item, destructive), versioned && typeof expected === "number" ? { expectedVersion: expected } : undefined);
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
if (!(error instanceof Exception2) || !error.is(RepositoryConcurrencyConflict2)) {
|
|
1118
|
+
throw error;
|
|
1119
|
+
}
|
|
1120
|
+
tally.upgraded -= 1;
|
|
1121
|
+
tally.upToDate += 1;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
} else if (removal) {
|
|
1125
|
+
tally.removalPending += 1;
|
|
1126
|
+
} else {
|
|
1127
|
+
tally.upToDate += 1;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return 0;
|
|
1131
|
+
});
|
|
1132
|
+
for (const previous of previousNames) {
|
|
1133
|
+
const old = previousContextOf(context, previous);
|
|
1134
|
+
await walk(provider, old, pageSize, async (rows) => {
|
|
1135
|
+
let removed = 0;
|
|
1136
|
+
const moved = new Set((await provider.findMany(context, {
|
|
1137
|
+
where: { id: { in: rows.map((row) => row.id) } },
|
|
1138
|
+
withDeleted: true,
|
|
1139
|
+
...context.expiry === undefined ? {} : { withExpired: true }
|
|
1140
|
+
})).map((row) => String(row.id)));
|
|
1141
|
+
for (const item of await upcastPage(state, plan, rows)) {
|
|
1142
|
+
tally.scanned += 1;
|
|
1143
|
+
if ("invalid" in item) {
|
|
1144
|
+
tally.invalid.push({
|
|
1145
|
+
id: String(item.row.id),
|
|
1146
|
+
issues: item.invalid
|
|
1147
|
+
});
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
for (const key of item.report.removed)
|
|
1151
|
+
tally.removedKeys.add(key);
|
|
1152
|
+
const id = String(item.row.id);
|
|
1153
|
+
if (!moved.has(id)) {
|
|
1154
|
+
tally.movedFromPreviousNames += 1;
|
|
1155
|
+
if (apply) {
|
|
1156
|
+
await provider.insert(context, writtenOf(state, plan, item, destructive));
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
if (apply && destructive) {
|
|
1160
|
+
await provider.remove(old, id);
|
|
1161
|
+
removed += 1;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return removed;
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
return { ...reportOf(tally), deferred: false };
|
|
1168
|
+
}
|
|
1169
|
+
function reportOf(tally) {
|
|
1170
|
+
return {
|
|
1171
|
+
scanned: tally.scanned,
|
|
1172
|
+
upToDate: tally.upToDate,
|
|
1173
|
+
upgraded: tally.upgraded,
|
|
1174
|
+
removalPending: tally.removalPending,
|
|
1175
|
+
invalid: tally.invalid,
|
|
1176
|
+
movedFromPreviousNames: tally.movedFromPreviousNames,
|
|
1177
|
+
removedKeys: [...tally.removedKeys].sort()
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// src/core/application/migrate.ts
|
|
1182
|
+
async function migrateApplication(source, options) {
|
|
1183
|
+
const fail = (reason) => {
|
|
1184
|
+
throw new InvalidSchemaMigration2({
|
|
1185
|
+
source: source.name,
|
|
1186
|
+
details: { reason }
|
|
1187
|
+
});
|
|
1188
|
+
};
|
|
1189
|
+
if (typeof options !== "object" || options === null) {
|
|
1190
|
+
fail('Expected the options of migrate, e.g. { mode: "check" }.');
|
|
1191
|
+
}
|
|
1192
|
+
const { mode, destructive = false, pageSize = 100, lock } = options;
|
|
1193
|
+
if (mode !== "check" && mode !== "apply") {
|
|
1194
|
+
fail('Expected mode to be "check" or "apply".');
|
|
1195
|
+
}
|
|
1196
|
+
if (typeof destructive !== "boolean") {
|
|
1197
|
+
fail("Expected destructive to be a boolean.");
|
|
1198
|
+
}
|
|
1199
|
+
if (!Number.isSafeInteger(pageSize) || pageSize < 1) {
|
|
1200
|
+
fail("Expected pageSize to be a positive integer.");
|
|
1201
|
+
}
|
|
1202
|
+
if (lock !== undefined && !isLock(lock)) {
|
|
1203
|
+
fail("Expected lock to be a schema lock ({ lockVersion: 1, entities }).");
|
|
1204
|
+
}
|
|
1205
|
+
const byEntity = new Map;
|
|
1206
|
+
for (const repository of source.repositories) {
|
|
1207
|
+
const state = findState(repository);
|
|
1208
|
+
if (state && !byEntity.has(state.entity))
|
|
1209
|
+
byEntity.set(state.entity, state);
|
|
1210
|
+
}
|
|
1211
|
+
const names = options.entities ?? [...source.entities].filter(([, entity]) => byEntity.has(entity)).map(([name]) => name);
|
|
1212
|
+
if (!Array.isArray(names))
|
|
1213
|
+
fail("Expected entities to be a list of names.");
|
|
1214
|
+
const targets = [];
|
|
1215
|
+
for (const name of names) {
|
|
1216
|
+
const entity = source.entities.get(name);
|
|
1217
|
+
if (entity === undefined) {
|
|
1218
|
+
fail(`"${name}" is not an entity of the application.`);
|
|
1219
|
+
}
|
|
1220
|
+
const state = byEntity.get(entity);
|
|
1221
|
+
if (state === undefined) {
|
|
1222
|
+
fail(`The entity "${name}" has no repository to migrate it through.`);
|
|
1223
|
+
}
|
|
1224
|
+
targets.push([name, state]);
|
|
1225
|
+
}
|
|
1226
|
+
const changes = lock === undefined ? [] : diffSchemas2(lock, schemaLockOf2(source.describe()));
|
|
1227
|
+
const entities = {};
|
|
1228
|
+
for (const [name, state] of targets) {
|
|
1229
|
+
entities[name] = await migrateRepository(state, {
|
|
1230
|
+
mode,
|
|
1231
|
+
destructive,
|
|
1232
|
+
pageSize,
|
|
1233
|
+
plan: Object.freeze({
|
|
1234
|
+
entity: name,
|
|
1235
|
+
changes: changes.filter((change) => change.entity === name),
|
|
1236
|
+
destructive
|
|
1237
|
+
})
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
return { mode, destructive, entities };
|
|
1241
|
+
}
|
|
1242
|
+
function isLock(value) {
|
|
1243
|
+
return typeof value === "object" && value !== null && value.lockVersion === 1 && typeof value.entities === "object" && value.entities !== null;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// src/core/application/start.ts
|
|
1247
|
+
function providerFrom2(environmentDependencies, create) {
|
|
1248
|
+
if (typeof environmentDependencies === "function" && create === undefined) {
|
|
1249
|
+
return Object.freeze({
|
|
1250
|
+
[Kind2]: "provider-factory",
|
|
1251
|
+
environments: Object.freeze([]),
|
|
1252
|
+
create: environmentDependencies,
|
|
1253
|
+
withContext: true
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
const list = isEnvironment(environmentDependencies) ? [environmentDependencies] : Array.isArray(environmentDependencies) ? [...environmentDependencies] : [null];
|
|
1257
|
+
if (!list.every(isEnvironment) || new Set(list).size !== list.length || typeof create !== "function") {
|
|
1258
|
+
throw new InvalidApplicationDeclaration2({
|
|
1259
|
+
source: "providerFrom",
|
|
1260
|
+
details: {
|
|
1261
|
+
reason: "Expected distinct environments made by environmentOf and a create function."
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
return Object.freeze({
|
|
1266
|
+
[Kind2]: "provider-factory",
|
|
1267
|
+
environments: Object.freeze(list),
|
|
1268
|
+
create,
|
|
1269
|
+
withContext: false
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
function logFrom2(create) {
|
|
1273
|
+
if (typeof create !== "function") {
|
|
1274
|
+
throw new InvalidApplicationDeclaration2({
|
|
1275
|
+
source: "logFrom",
|
|
1276
|
+
details: { reason: "Expected logFrom to take a create function." }
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
return Object.freeze({ [Kind2]: "log-factory", create });
|
|
1280
|
+
}
|
|
1281
|
+
function isLogFactory(value) {
|
|
1282
|
+
return typeof value === "object" && value !== null && value[Kind2] === "log-factory";
|
|
1283
|
+
}
|
|
1284
|
+
function isProviderFactory2(value) {
|
|
1285
|
+
return typeof value === "object" && value !== null && value[Kind2] === "provider-factory";
|
|
1286
|
+
}
|
|
1287
|
+
var deferred = new WeakMap;
|
|
1288
|
+
var METHODS = [
|
|
1289
|
+
"findOne",
|
|
1290
|
+
"findMany",
|
|
1291
|
+
"insert",
|
|
1292
|
+
"update",
|
|
1293
|
+
"remove",
|
|
1294
|
+
"count"
|
|
1295
|
+
];
|
|
1296
|
+
function deferredProvider(factory, application) {
|
|
1297
|
+
const state = {
|
|
1298
|
+
factory,
|
|
1299
|
+
application,
|
|
1300
|
+
provider: undefined,
|
|
1301
|
+
starting: undefined
|
|
1302
|
+
};
|
|
1303
|
+
const real = (method) => {
|
|
1304
|
+
const { provider } = state;
|
|
1305
|
+
if (!provider) {
|
|
1306
|
+
throw new ApplicationNotStarted2({
|
|
1307
|
+
source: application,
|
|
1308
|
+
details: { application }
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
if (typeof provider[method] !== "function") {
|
|
1312
|
+
throw new InvalidApplicationDeclaration2({
|
|
1313
|
+
source: application,
|
|
1314
|
+
details: {
|
|
1315
|
+
reason: `The provider made on start has no "${method}".`
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
return provider;
|
|
1320
|
+
};
|
|
1321
|
+
const proxy = {};
|
|
1322
|
+
for (const method of [...METHODS, "increment"]) {
|
|
1323
|
+
proxy[method] = (...args) => real(method)[method]?.(...args);
|
|
1324
|
+
}
|
|
1325
|
+
proxy.transaction = (run) => real("transaction").transaction(run);
|
|
1326
|
+
proxy.migrate = async (...args) => {
|
|
1327
|
+
const { provider } = state;
|
|
1328
|
+
if (!provider)
|
|
1329
|
+
real("migrate");
|
|
1330
|
+
return provider?.migrate?.(...args);
|
|
1331
|
+
};
|
|
1332
|
+
Object.defineProperty(proxy, "capabilities", {
|
|
1333
|
+
get: () => state.provider?.capabilities,
|
|
1334
|
+
enumerable: true
|
|
1335
|
+
});
|
|
1336
|
+
Object.freeze(proxy);
|
|
1337
|
+
deferred.set(proxy, state);
|
|
1338
|
+
return proxy;
|
|
1339
|
+
}
|
|
1340
|
+
function isDeferredProvider(provider) {
|
|
1341
|
+
return typeof provider === "object" && provider !== null && deferred.has(provider);
|
|
1342
|
+
}
|
|
1343
|
+
function deferredEnvironments(provider) {
|
|
1344
|
+
return (typeof provider === "object" && provider !== null ? deferred.get(provider)?.factory.environments : undefined) ?? [];
|
|
1345
|
+
}
|
|
1346
|
+
function startDeferred(provider, context) {
|
|
1347
|
+
const state = deferred.get(provider);
|
|
1348
|
+
if (!state)
|
|
1349
|
+
return;
|
|
1350
|
+
state.starting ??= (async () => {
|
|
1351
|
+
const made = await state.factory.create(state.factory.withContext ? context : loadEnvironments(state.factory.environments));
|
|
1352
|
+
if (typeof made !== "object" || made === null) {
|
|
1353
|
+
state.starting = undefined;
|
|
1354
|
+
throw new InvalidApplicationDeclaration2({
|
|
1355
|
+
source: state.application,
|
|
1356
|
+
details: {
|
|
1357
|
+
reason: "Expected providerFrom's create to give a repository provider."
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
state.provider = made;
|
|
1362
|
+
return made;
|
|
1363
|
+
})();
|
|
1364
|
+
return state.starting;
|
|
1365
|
+
}
|
|
1366
|
+
async function stopProvider(provider) {
|
|
1367
|
+
const state = deferred.get(provider);
|
|
1368
|
+
if (!state) {
|
|
1369
|
+
await provider.close?.();
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const made = state.provider;
|
|
1373
|
+
state.provider = undefined;
|
|
1374
|
+
state.starting = undefined;
|
|
1375
|
+
await made?.close?.();
|
|
1376
|
+
}
|
|
1377
|
+
function checkTransactions(source, provider, commands) {
|
|
1378
|
+
if (commands.length === 0)
|
|
1379
|
+
return;
|
|
1380
|
+
if (typeof provider.transaction !== "function" || provider.capabilities?.transactions === false) {
|
|
1381
|
+
throw new InvalidCommandDeclaration2({
|
|
1382
|
+
source,
|
|
1383
|
+
details: {
|
|
1384
|
+
reason: `The provider made on start does not support transactions, which ${commands.join(", ")} declare.`
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
function checkCapabilities(source, provider, repositories) {
|
|
1390
|
+
if (repositories.length === 0 || provider.capabilities?.keyset === true)
|
|
1391
|
+
return;
|
|
1392
|
+
throw new InvalidApplicationDeclaration2({
|
|
1393
|
+
source,
|
|
1394
|
+
details: {
|
|
1395
|
+
reason: `The provider made on start does not read by cursor (capabilities.keyset), which the findCursor of ${repositories.join(", ")} needs.`
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// src/core/application/providers.ts
|
|
1401
|
+
var slots = new WeakMap;
|
|
1402
|
+
var storages = new WeakMap;
|
|
1403
|
+
var METHODS2 = [
|
|
1404
|
+
"findOne",
|
|
1405
|
+
"findMany",
|
|
1406
|
+
"insert",
|
|
1407
|
+
"update",
|
|
1408
|
+
"remove",
|
|
1409
|
+
"count"
|
|
1410
|
+
];
|
|
1411
|
+
var KEY = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
1412
|
+
var RESERVED = new Set(["inMemory", "__proto__", "constructor", "prototype"]);
|
|
1413
|
+
function invalid2(reason) {
|
|
1414
|
+
throw new InvalidApplicationDeclaration2({
|
|
1415
|
+
source: "providersOf",
|
|
1416
|
+
details: { reason }
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
function isProviderObject(value) {
|
|
1420
|
+
return typeof value === "object" && value !== null && !isProviderFactory2(value) && METHODS2.some((method) => typeof value[method] === "function");
|
|
1421
|
+
}
|
|
1422
|
+
function slotOf(key, storage, value) {
|
|
1423
|
+
const slot = {
|
|
1424
|
+
[Kind2]: "provider-slot",
|
|
1425
|
+
key
|
|
1426
|
+
};
|
|
1427
|
+
if (isProviderFactory2(value)) {
|
|
1428
|
+
const unbound = () => {
|
|
1429
|
+
throw new ProviderNotBound2({ source: key, details: { provider: key } });
|
|
1430
|
+
};
|
|
1431
|
+
for (const method of METHODS2)
|
|
1432
|
+
slot[method] = unbound;
|
|
1433
|
+
slot.transaction = unbound;
|
|
1434
|
+
slot.increment = unbound;
|
|
1435
|
+
Object.defineProperty(slot, "capabilities", {
|
|
1436
|
+
get: () => {
|
|
1437
|
+
return;
|
|
1438
|
+
},
|
|
1439
|
+
enumerable: true
|
|
1440
|
+
});
|
|
1441
|
+
} else {
|
|
1442
|
+
const target = value;
|
|
1443
|
+
for (const method of [
|
|
1444
|
+
...METHODS2,
|
|
1445
|
+
"increment",
|
|
1446
|
+
"transaction",
|
|
1447
|
+
"close",
|
|
1448
|
+
"migrate"
|
|
1449
|
+
]) {
|
|
1450
|
+
if (typeof target[method] === "function") {
|
|
1451
|
+
slot[method] = (...args) => target[method]?.(...args);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
Object.defineProperty(slot, "capabilities", {
|
|
1455
|
+
get: () => value.capabilities,
|
|
1456
|
+
enumerable: true
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
Object.freeze(slot);
|
|
1460
|
+
slots.set(slot, { key, storage, value });
|
|
1461
|
+
return slot;
|
|
1462
|
+
}
|
|
1463
|
+
function providersOf2(entries) {
|
|
1464
|
+
if (typeof entries !== "object" || entries === null || Array.isArray(entries) || Object.keys(entries).length === 0) {
|
|
1465
|
+
invalid2("Expected an object of providers by key.");
|
|
1466
|
+
}
|
|
1467
|
+
const storage = Object.create(null);
|
|
1468
|
+
storage[Kind2] = "provider-storage";
|
|
1469
|
+
const seen = new Map;
|
|
1470
|
+
const list = [];
|
|
1471
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
1472
|
+
if (!KEY.test(key) || RESERVED.has(key)) {
|
|
1473
|
+
invalid2(key === "inMemory" ? `"inMemory" is the built-in provider; choose another key.` : `Expected "${key}" to be an identifier that is not reserved.`);
|
|
1474
|
+
}
|
|
1475
|
+
if (isProviderSlot(value)) {
|
|
1476
|
+
invalid2(`"${key}" holds a provider of another storage; a storage holds providers, not slots.`);
|
|
1477
|
+
}
|
|
1478
|
+
if (!isProviderFactory2(value) && !isProviderObject(value)) {
|
|
1479
|
+
invalid2(`Expected "${key}" to be a repository provider or a factory of providerFrom.`);
|
|
1480
|
+
}
|
|
1481
|
+
const other = seen.get(value);
|
|
1482
|
+
if (other !== undefined) {
|
|
1483
|
+
invalid2(`"${other}" and "${key}" hold the same provider.`);
|
|
1484
|
+
}
|
|
1485
|
+
seen.set(value, key);
|
|
1486
|
+
const slot = slotOf(key, storage, value);
|
|
1487
|
+
storage[key] = slot;
|
|
1488
|
+
list.push(slot);
|
|
1489
|
+
}
|
|
1490
|
+
Object.freeze(storage);
|
|
1491
|
+
storages.set(storage, Object.freeze(list));
|
|
1492
|
+
return storage;
|
|
1493
|
+
}
|
|
1494
|
+
function isProviderSlot(value) {
|
|
1495
|
+
return typeof value === "object" && value !== null && slots.has(value);
|
|
1496
|
+
}
|
|
1497
|
+
function isProviderStorage(value) {
|
|
1498
|
+
return typeof value === "object" && value !== null && storages.has(value);
|
|
1499
|
+
}
|
|
1500
|
+
function slotsOf(storage) {
|
|
1501
|
+
return storages.get(storage) ?? [];
|
|
1502
|
+
}
|
|
1503
|
+
function slotEntry(slot) {
|
|
1504
|
+
return slots.get(slot);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// src/core/application/application.ts
|
|
1508
|
+
var KEYS = [
|
|
1509
|
+
"properties",
|
|
1510
|
+
"blueprints",
|
|
1511
|
+
"records",
|
|
1512
|
+
"entities",
|
|
1513
|
+
"environments",
|
|
1514
|
+
"repositories",
|
|
1515
|
+
"useCases",
|
|
1516
|
+
"commands",
|
|
1517
|
+
"outputs",
|
|
1518
|
+
"exceptions"
|
|
1519
|
+
];
|
|
1520
|
+
var internals = new WeakMap;
|
|
1521
|
+
var logSettings = new WeakMap;
|
|
1522
|
+
var namedProviders = new WeakMap;
|
|
1523
|
+
var defaultSlots = new WeakMap;
|
|
1524
|
+
function referenceOf(slot) {
|
|
1525
|
+
return { kind: "ref", key: slotEntry(slot).key };
|
|
1526
|
+
}
|
|
1527
|
+
var KIND_OF = {
|
|
1528
|
+
properties: "property",
|
|
1529
|
+
blueprints: "blueprint",
|
|
1530
|
+
records: "record",
|
|
1531
|
+
entities: "entity",
|
|
1532
|
+
environments: "environment",
|
|
1533
|
+
repositories: "repository",
|
|
1534
|
+
useCases: "use case",
|
|
1535
|
+
commands: "command",
|
|
1536
|
+
outputs: "output",
|
|
1537
|
+
exceptions: "exception"
|
|
1538
|
+
};
|
|
1539
|
+
function isApplication(value) {
|
|
1540
|
+
return typeof value === "object" && value !== null && value[Kind2] === "application";
|
|
1541
|
+
}
|
|
1542
|
+
function invalid3(source, reason) {
|
|
1543
|
+
throw new InvalidApplicationDeclaration2({ source, details: { reason } });
|
|
1544
|
+
}
|
|
1545
|
+
var publisherOrigins = new WeakMap;
|
|
1546
|
+
function publishersOf(name, declared, extended) {
|
|
1547
|
+
const merged = {};
|
|
1548
|
+
const origins = {};
|
|
1549
|
+
for (const other of extended) {
|
|
1550
|
+
Object.assign(merged, other[ApplicationPublishersKey]);
|
|
1551
|
+
Object.assign(origins, publisherOrigins.get(other));
|
|
1552
|
+
}
|
|
1553
|
+
if (declared !== undefined) {
|
|
1554
|
+
if (typeof declared !== "object" || declared === null || Array.isArray(declared)) {
|
|
1555
|
+
invalid3(name, "Expected `publishers` to be an object of publishers by key.");
|
|
1556
|
+
}
|
|
1557
|
+
for (const [key, publisher] of Object.entries(declared)) {
|
|
1558
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(key)) {
|
|
1559
|
+
invalid3(name, `Expected the publisher key "${key}" to be an identifier.`);
|
|
1560
|
+
}
|
|
1561
|
+
if (!isEventPublisher(publisher)) {
|
|
1562
|
+
invalid3(name, `Expected the publisher "${key}" to be an object with a publish(topic, event) method.`);
|
|
1563
|
+
}
|
|
1564
|
+
merged[key] = publisher;
|
|
1565
|
+
origins[key] = name;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
return { publishers: Object.freeze(merged), origins };
|
|
1569
|
+
}
|
|
1570
|
+
function isOfKind(collection, value) {
|
|
1571
|
+
switch (collection) {
|
|
1572
|
+
case "properties":
|
|
1573
|
+
return isProperty(value);
|
|
1574
|
+
case "blueprints":
|
|
1575
|
+
return isBlueprint(value);
|
|
1576
|
+
case "records":
|
|
1577
|
+
return isRecordClass(value) && !isEntityClass2(value);
|
|
1578
|
+
case "entities":
|
|
1579
|
+
return isEntityClass2(value);
|
|
1580
|
+
case "environments":
|
|
1581
|
+
return isEnvironment(value);
|
|
1582
|
+
case "repositories":
|
|
1583
|
+
return inspectRepository(value) !== undefined;
|
|
1584
|
+
case "useCases":
|
|
1585
|
+
return isUseCaseClass2(value);
|
|
1586
|
+
case "commands":
|
|
1587
|
+
return isCommand2(value);
|
|
1588
|
+
case "outputs":
|
|
1589
|
+
return isOutput2(value);
|
|
1590
|
+
case "exceptions":
|
|
1591
|
+
return isDeclaredException(value);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
function keyOf(collection, value) {
|
|
1595
|
+
switch (collection) {
|
|
1596
|
+
case "properties":
|
|
1597
|
+
return fieldOf(value);
|
|
1598
|
+
case "blueprints":
|
|
1599
|
+
return value.properties;
|
|
1600
|
+
case "repositories":
|
|
1601
|
+
return inspectRepository(value).state;
|
|
1602
|
+
default:
|
|
1603
|
+
return value;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
function emptyRegistry() {
|
|
1607
|
+
return Object.fromEntries(KEYS.map((key) => [key, new Map]));
|
|
1608
|
+
}
|
|
1609
|
+
function resolve(settings) {
|
|
1610
|
+
const registry = emptyRegistry();
|
|
1611
|
+
const byKey = new Map;
|
|
1612
|
+
const source = settings.name;
|
|
1613
|
+
const add = (entry) => {
|
|
1614
|
+
const same = registry[entry.collection].get(entry.name);
|
|
1615
|
+
if (same) {
|
|
1616
|
+
if (same.key === entry.key)
|
|
1617
|
+
return;
|
|
1618
|
+
conflict(`${KIND_OF[entry.collection]} "${entry.name}" is declared as different objects by "${same.from}" and by "${entry.from}".`, same, entry);
|
|
1619
|
+
}
|
|
1620
|
+
const other = byKey.get(entry.key);
|
|
1621
|
+
if (other) {
|
|
1622
|
+
if (other.from === entry.from) {
|
|
1623
|
+
throw new DuplicateApplicationDeclaration2({
|
|
1624
|
+
source,
|
|
1625
|
+
details: {
|
|
1626
|
+
first: `${KIND_OF[other.collection]} "${other.name}"`,
|
|
1627
|
+
second: `${KIND_OF[entry.collection]} "${entry.name}"`
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
conflict(`The same ${KIND_OF[entry.collection]} is ${KIND_OF[other.collection]} "${other.name}" in "${other.from}" and ${KIND_OF[entry.collection]} "${entry.name}" in "${entry.from}".`, other, entry);
|
|
1632
|
+
}
|
|
1633
|
+
registry[entry.collection].set(entry.name, entry);
|
|
1634
|
+
byKey.set(entry.key, entry);
|
|
1635
|
+
};
|
|
1636
|
+
const conflict = (reason, first, second) => {
|
|
1637
|
+
throw new ApplicationConflict2({
|
|
1638
|
+
source,
|
|
1639
|
+
details: {
|
|
1640
|
+
reason,
|
|
1641
|
+
origins: [first.from, second.from],
|
|
1642
|
+
declarations: [
|
|
1643
|
+
`${KIND_OF[first.collection]} ${first.name}`,
|
|
1644
|
+
`${KIND_OF[second.collection]} ${second.name}`
|
|
1645
|
+
]
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
};
|
|
1649
|
+
for (const other of settings.extends ?? []) {
|
|
1650
|
+
const resolved = internals.get(other);
|
|
1651
|
+
if (!resolved)
|
|
1652
|
+
invalid3(source, "Expected `extends` to hold applications.");
|
|
1653
|
+
for (const key of KEYS) {
|
|
1654
|
+
for (const entry of resolved[key].values())
|
|
1655
|
+
add(entry);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
for (const collection of KEYS) {
|
|
1659
|
+
const declared = settings[collection];
|
|
1660
|
+
if (declared === undefined)
|
|
1661
|
+
continue;
|
|
1662
|
+
if (typeof declared !== "object" || declared === null || Array.isArray(declared)) {
|
|
1663
|
+
invalid3(source, `Expected ${collection} to be an object of declarations.`);
|
|
1664
|
+
}
|
|
1665
|
+
for (const [name, value] of Object.entries(declared)) {
|
|
1666
|
+
if (!isOfKind(collection, value)) {
|
|
1667
|
+
invalid3(source, `${collection}.${name} is not a ${KIND_OF[collection]} (${collection} takes ${KIND_OF[collection]} declarations).`);
|
|
1668
|
+
}
|
|
1669
|
+
const declaredName = collection === "useCases" ? value.useCaseName : collection === "commands" ? value.commandName : collection === "outputs" ? value.outputName : collection === "exceptions" ? value.name : collection === "records" || collection === "entities" ? declaredRecordNameOf(value) ?? name : name;
|
|
1670
|
+
if (declaredName !== name) {
|
|
1671
|
+
invalid3(source, `${collection}.${name} must be keyed by its name, "${declaredName}".`);
|
|
1672
|
+
}
|
|
1673
|
+
add({
|
|
1674
|
+
collection,
|
|
1675
|
+
name,
|
|
1676
|
+
value,
|
|
1677
|
+
key: keyOf(collection, value),
|
|
1678
|
+
from: source
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
for (const entry of [...registry.commands.values()]) {
|
|
1683
|
+
for (const UseCase of Object.values(entry.value.useCases)) {
|
|
1684
|
+
if (useCaseOriginOf2(UseCase) === undefined || byKey.has(UseCase)) {
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
add({
|
|
1688
|
+
collection: "useCases",
|
|
1689
|
+
name: UseCase.useCaseName,
|
|
1690
|
+
value: UseCase,
|
|
1691
|
+
key: UseCase,
|
|
1692
|
+
from: entry.from
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
return registry;
|
|
1697
|
+
}
|
|
1698
|
+
function recordsIn(properties) {
|
|
1699
|
+
const found = new Set;
|
|
1700
|
+
const visit = (field) => {
|
|
1701
|
+
if (isBlueprintList(field))
|
|
1702
|
+
visit(field.item);
|
|
1703
|
+
else if (isBlueprint(field))
|
|
1704
|
+
walk(field.properties);
|
|
1705
|
+
else if (isBlueprintVariant(field)) {
|
|
1706
|
+
for (const variant of Object.values(field.variants)) {
|
|
1707
|
+
visit(variant);
|
|
1708
|
+
}
|
|
1709
|
+
} else {
|
|
1710
|
+
const record = recordFieldOf(field)?.record;
|
|
1711
|
+
if (record)
|
|
1712
|
+
found.add(record);
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
const walk = (collection) => {
|
|
1716
|
+
for (const field of Object.values(collection))
|
|
1717
|
+
visit(field);
|
|
1718
|
+
};
|
|
1719
|
+
walk(properties);
|
|
1720
|
+
return found;
|
|
1721
|
+
}
|
|
1722
|
+
function validate(source, registry) {
|
|
1723
|
+
const registered = new Set([
|
|
1724
|
+
...[...registry.records.values()].map((entry) => entry.key),
|
|
1725
|
+
...[...registry.entities.values()].map((entry) => entry.key)
|
|
1726
|
+
]);
|
|
1727
|
+
const holders = [
|
|
1728
|
+
["blueprints", "properties"],
|
|
1729
|
+
["records", "blueprint.properties"],
|
|
1730
|
+
["entities", "blueprint.properties"]
|
|
1731
|
+
];
|
|
1732
|
+
for (const [collection] of holders) {
|
|
1733
|
+
for (const entry of registry[collection].values()) {
|
|
1734
|
+
const properties = collection === "blueprints" ? entry.value.properties : entry.value.blueprint.properties;
|
|
1735
|
+
for (const record of recordsIn(properties)) {
|
|
1736
|
+
if (registered.has(record))
|
|
1737
|
+
continue;
|
|
1738
|
+
throw new UnregisteredApplicationReference2({
|
|
1739
|
+
source,
|
|
1740
|
+
details: {
|
|
1741
|
+
holder: `${KIND_OF[collection]} "${entry.name}"`,
|
|
1742
|
+
reference: `${isEntityClass2(record) ? "entity" : "record"} "${recordNameOf2(record)}"`
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
const repositories = new Set([...registry.repositories.values()].map((entry) => entry.key));
|
|
1749
|
+
const environments = new Set([...registry.environments.values()].map((entry) => entry.key));
|
|
1750
|
+
const unregistered = (repository, dependency) => {
|
|
1751
|
+
throw new UnregisteredRepositoryDependency2({
|
|
1752
|
+
source,
|
|
1753
|
+
details: { repository, dependency }
|
|
1754
|
+
});
|
|
1755
|
+
};
|
|
1756
|
+
for (const entry of registry.repositories.values()) {
|
|
1757
|
+
const info = inspectRepository(entry.value);
|
|
1758
|
+
if (!registered.has(info.entity)) {
|
|
1759
|
+
unregistered(entry.name, `entity "${info.entity.name}"`);
|
|
1760
|
+
}
|
|
1761
|
+
for (const relation of info.relations) {
|
|
1762
|
+
if (!repositories.has(relation)) {
|
|
1763
|
+
unregistered(entry.name, `the repository of "${recordNameOf2(relation.entity)}"`);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
for (const environment of info.environments) {
|
|
1767
|
+
if (!environments.has(environment)) {
|
|
1768
|
+
unregistered(entry.name, "an environment");
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
function validateCommands(source, registry) {
|
|
1774
|
+
const useCases = new Set([...registry.useCases.values()].map((entry) => entry.key));
|
|
1775
|
+
const environments = new Set([...registry.environments.values()].map((entry) => entry.key));
|
|
1776
|
+
for (const entry of registry.commands.values()) {
|
|
1777
|
+
const command = entry.value;
|
|
1778
|
+
for (const environment of command.environments) {
|
|
1779
|
+
if (environments.has(environment))
|
|
1780
|
+
continue;
|
|
1781
|
+
throw new UnregisteredApplicationReference2({
|
|
1782
|
+
source,
|
|
1783
|
+
details: {
|
|
1784
|
+
holder: `command "${entry.name}"`,
|
|
1785
|
+
reference: "an environment",
|
|
1786
|
+
command: entry.name
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
for (const UseCase of Object.values(command.useCases)) {
|
|
1791
|
+
if (useCases.has(UseCase))
|
|
1792
|
+
continue;
|
|
1793
|
+
throw new UnregisteredApplicationReference2({
|
|
1794
|
+
source,
|
|
1795
|
+
details: {
|
|
1796
|
+
holder: `command "${entry.name}"`,
|
|
1797
|
+
reference: `use case "${UseCase.useCaseName}"`,
|
|
1798
|
+
command: entry.name,
|
|
1799
|
+
useCase: UseCase.useCaseName
|
|
1800
|
+
}
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
function validateOutputs(source, registry) {
|
|
1806
|
+
const commands = new Set([...registry.commands.values()].map((entry) => entry.key));
|
|
1807
|
+
for (const entry of registry.outputs.values()) {
|
|
1808
|
+
const output = entry.value;
|
|
1809
|
+
if (!output.outputName) {
|
|
1810
|
+
invalid3(source, `outputs.${entry.name} has no name (an output made by pipe cannot be registered).`);
|
|
1811
|
+
}
|
|
1812
|
+
for (const command of output.commands) {
|
|
1813
|
+
if (commands.has(command))
|
|
1814
|
+
continue;
|
|
1815
|
+
throw new UnregisteredApplicationReference2({
|
|
1816
|
+
source,
|
|
1817
|
+
details: {
|
|
1818
|
+
holder: `output "${entry.name}"`,
|
|
1819
|
+
reference: `command "${command.commandName}"`,
|
|
1820
|
+
output: entry.name,
|
|
1821
|
+
command: command.commandName
|
|
1822
|
+
}
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
function validateUseCases(source, registry) {
|
|
1828
|
+
const registered = new Set([
|
|
1829
|
+
...[...registry.records.values()].map((entry) => entry.key),
|
|
1830
|
+
...[...registry.entities.values()].map((entry) => entry.key)
|
|
1831
|
+
]);
|
|
1832
|
+
const known = (collection) => new Set([...registry[collection].values()].map((entry) => entry.key));
|
|
1833
|
+
const useCases = known("useCases");
|
|
1834
|
+
const repositories = known("repositories");
|
|
1835
|
+
const environments = known("environments");
|
|
1836
|
+
const exceptions = known("exceptions");
|
|
1837
|
+
for (const entry of registry.useCases.values()) {
|
|
1838
|
+
const UseCase = entry.value;
|
|
1839
|
+
for (const [key, dependency] of Object.entries(UseCase.dependencies)) {
|
|
1840
|
+
const found = isLoggerDeclaration(dependency) ? true : isUseCaseClass2(dependency) ? useCases.has(dependency) : isEnvironment(dependency) ? environments.has(dependency) : repositories.has(inspectRepository(dependency).state);
|
|
1841
|
+
if (!found) {
|
|
1842
|
+
throw new UnregisteredUseCaseDependency2({
|
|
1843
|
+
source,
|
|
1844
|
+
details: { useCase: `"${entry.name}"`, dependency: `"${key}"` }
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
for (const [part, blueprint] of [
|
|
1849
|
+
["blueprint", UseCase.blueprint],
|
|
1850
|
+
["response", UseCase.response],
|
|
1851
|
+
["context", UseCase.context]
|
|
1852
|
+
]) {
|
|
1853
|
+
if (blueprint === undefined)
|
|
1854
|
+
continue;
|
|
1855
|
+
for (const record of recordsIn(blueprint.properties)) {
|
|
1856
|
+
if (registered.has(record))
|
|
1857
|
+
continue;
|
|
1858
|
+
throw new UnregisteredApplicationReference2({
|
|
1859
|
+
source,
|
|
1860
|
+
details: {
|
|
1861
|
+
holder: `the ${part} of use case "${entry.name}"`,
|
|
1862
|
+
reference: `${isEntityClass2(record) ? "entity" : "record"} "${recordNameOf2(record)}"`
|
|
1863
|
+
}
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
for (const Thrown of UseCase.errors) {
|
|
1868
|
+
if (exceptions.has(Thrown) || isExportedException(Thrown))
|
|
1869
|
+
continue;
|
|
1870
|
+
throw new UnregisteredApplicationReference2({
|
|
1871
|
+
source,
|
|
1872
|
+
details: {
|
|
1873
|
+
holder: `use case "${entry.name}"`,
|
|
1874
|
+
reference: `exception "${Thrown.name}"`
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
for (const [index, step] of (interpretedStepsOf(UseCase) ?? []).entries()) {
|
|
1879
|
+
const holder = `step ${index} of use case "${entry.name}"`;
|
|
1880
|
+
if (step.build !== undefined && !registered.has(step.build)) {
|
|
1881
|
+
throw new UnregisteredApplicationReference2({
|
|
1882
|
+
source,
|
|
1883
|
+
details: {
|
|
1884
|
+
holder,
|
|
1885
|
+
reference: `${isEntityClass2(step.build) ? "entity" : "record"} "${recordNameOf2(step.build)}"`
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
const Thrown = step.error;
|
|
1890
|
+
if (Thrown !== undefined && !exceptions.has(Thrown) && !isExportedException(Thrown)) {
|
|
1891
|
+
throw new UnregisteredApplicationReference2({
|
|
1892
|
+
source,
|
|
1893
|
+
details: { holder, reference: `exception "${Thrown.name}"` }
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
function jsonOf(value) {
|
|
1900
|
+
return JSON.parse(JSON.stringify(value, (_key, item) => item instanceof RegExp ? item.source : item));
|
|
1901
|
+
}
|
|
1902
|
+
function referencesOf(registry, collection) {
|
|
1903
|
+
return new Map([...registry[collection].values()].map((entry) => [entry.key, entry.name]));
|
|
1904
|
+
}
|
|
1905
|
+
function propertyReferenceOf(Property, scope) {
|
|
1906
|
+
const ref = scope.properties.get(fieldOf(Property));
|
|
1907
|
+
return ref === undefined ? propertyDescriptionOf(Property, scope) : { ref };
|
|
1908
|
+
}
|
|
1909
|
+
function lineageDescriptionOf(lineage, scope) {
|
|
1910
|
+
switch (lineage.kind) {
|
|
1911
|
+
case "refined":
|
|
1912
|
+
return {
|
|
1913
|
+
kind: "refined",
|
|
1914
|
+
base: propertyReferenceOf(lineage.base, scope),
|
|
1915
|
+
options: jsonOf(lineage.options)
|
|
1916
|
+
};
|
|
1917
|
+
case "optional":
|
|
1918
|
+
return {
|
|
1919
|
+
kind: "optional",
|
|
1920
|
+
base: propertyReferenceOf(lineage.base, scope)
|
|
1921
|
+
};
|
|
1922
|
+
case "factory":
|
|
1923
|
+
return {
|
|
1924
|
+
kind: "factory",
|
|
1925
|
+
factory: lineage.factory,
|
|
1926
|
+
args: jsonOf(lineage.args)
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
function propertyDescriptionOf(Property, scope) {
|
|
1931
|
+
const { type } = Property;
|
|
1932
|
+
const lineage = propertyLineageOf2(Property);
|
|
1933
|
+
return {
|
|
1934
|
+
name: type.name,
|
|
1935
|
+
optional: type.optional === true,
|
|
1936
|
+
sensitive: type.sensitive === true,
|
|
1937
|
+
unique: type.unique === true,
|
|
1938
|
+
schema: plainOf(type.schema.type.Type()),
|
|
1939
|
+
...lineage ? { lineage: lineageDescriptionOf(lineage, scope) } : {}
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
function fieldDescriptionOf(field, scope) {
|
|
1943
|
+
if (isProperty(field)) {
|
|
1944
|
+
const real = fieldOf(field);
|
|
1945
|
+
const ref = scope.properties.get(real);
|
|
1946
|
+
return ref === undefined ? { kind: "property", ...propertyDescriptionOf(real, scope) } : { kind: "property", ref, optional: real.type.optional === true };
|
|
1947
|
+
}
|
|
1948
|
+
if (isBlueprintList(field)) {
|
|
1949
|
+
return {
|
|
1950
|
+
kind: "list",
|
|
1951
|
+
item: fieldDescriptionOf(field.item, scope),
|
|
1952
|
+
optional: field.optional,
|
|
1953
|
+
...field.minItems === undefined ? {} : { minItems: field.minItems },
|
|
1954
|
+
...field.maxItems === undefined ? {} : { maxItems: field.maxItems }
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
if (isBlueprint(field)) {
|
|
1958
|
+
const ref = scope.blueprints.get(field.properties);
|
|
1959
|
+
return ref === undefined ? {
|
|
1960
|
+
kind: "blueprint",
|
|
1961
|
+
optional: field.optional,
|
|
1962
|
+
fields: fieldsDescriptionOf(field.properties, scope)
|
|
1963
|
+
} : { kind: "blueprint", ref, optional: field.optional };
|
|
1964
|
+
}
|
|
1965
|
+
if (isBlueprintVariant(field)) {
|
|
1966
|
+
const variants = field.variants;
|
|
1967
|
+
return {
|
|
1968
|
+
kind: "variant",
|
|
1969
|
+
key: field.key,
|
|
1970
|
+
optional: field.optional,
|
|
1971
|
+
variants: Object.fromEntries(Object.entries(variants).map(([tag, variant]) => [
|
|
1972
|
+
tag,
|
|
1973
|
+
fieldDescriptionOf(variant, scope)
|
|
1974
|
+
]))
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
const record = recordFieldOf(field);
|
|
1978
|
+
if (record) {
|
|
1979
|
+
const entity = scope.entities.get(record.record);
|
|
1980
|
+
const ref = entity ?? scope.records.get(record.record);
|
|
1981
|
+
return {
|
|
1982
|
+
kind: entity === undefined ? "record" : "entity",
|
|
1983
|
+
ref: ref ?? recordNameOf2(record.record),
|
|
1984
|
+
optional: record.optional
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
return {
|
|
1988
|
+
kind: "property",
|
|
1989
|
+
name: "unknown",
|
|
1990
|
+
optional: false,
|
|
1991
|
+
sensitive: false,
|
|
1992
|
+
unique: false,
|
|
1993
|
+
schema: {}
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
function fieldsDescriptionOf(properties, scope, skip = new Set) {
|
|
1997
|
+
return Object.fromEntries(Object.entries(properties).filter(([key]) => !skip.has(key)).map(([key, field]) => [key, fieldDescriptionOf(field, scope)]));
|
|
1998
|
+
}
|
|
1999
|
+
function recordDescriptionOf(entry, scope) {
|
|
2000
|
+
const Record = entry.value;
|
|
2001
|
+
const plugins = pluginsOf2(Record).map((plugin) => plugin.toJSON());
|
|
2002
|
+
const skip = new Set(plugins.flatMap((plugin) => plugin.keys));
|
|
2003
|
+
const settings = entry.collection === "entities" ? Record[EntitySettings2] : undefined;
|
|
2004
|
+
const expires = settings?.expires;
|
|
2005
|
+
const upgrade = Object.keys(settings?.upgrade ?? {}).map(Number).sort((a, b) => a - b);
|
|
2006
|
+
if (entry.collection === "entities") {
|
|
2007
|
+
for (const key of identityKeysOf(Record))
|
|
2008
|
+
skip.add(key);
|
|
2009
|
+
}
|
|
2010
|
+
const own = (keys, flag) => keys.filter((key) => {
|
|
2011
|
+
if (skip.has(key))
|
|
2012
|
+
return false;
|
|
2013
|
+
const field = Record.blueprint.properties[key];
|
|
2014
|
+
return !(flag && isProperty(field) && field.type[flag] === true);
|
|
2015
|
+
});
|
|
2016
|
+
const lifecycle = entry.collection === "entities" ? lifecycleEventsOf(Record) : {};
|
|
2017
|
+
const events = Object.fromEntries(Object.entries(lifecycle).map(([cycle, list]) => [
|
|
2018
|
+
cycle,
|
|
2019
|
+
list.map((event) => ({
|
|
2020
|
+
name: event.name,
|
|
2021
|
+
handler: eventHasHandler(event)
|
|
2022
|
+
}))
|
|
2023
|
+
]));
|
|
2024
|
+
return {
|
|
2025
|
+
from: entry.from,
|
|
2026
|
+
fields: fieldsDescriptionOf(Record.blueprint.properties, scope, skip),
|
|
2027
|
+
options: {
|
|
2028
|
+
sensitive: own(Record.options.sensitive, "sensitive"),
|
|
2029
|
+
unique: own(Record.options.unique, "unique"),
|
|
2030
|
+
change: own(Record.options.change)
|
|
2031
|
+
},
|
|
2032
|
+
plugins,
|
|
2033
|
+
...Object.keys(events).length > 0 ? { events } : {},
|
|
2034
|
+
...expires === undefined ? {} : { expires },
|
|
2035
|
+
...entry.collection === "entities" && isVersionedEntityClass(Record) ? { versioned: true } : {},
|
|
2036
|
+
...settings?.schemaVersioned ? { schemaVersion: settings.schemaVersion } : {},
|
|
2037
|
+
...upgrade.length > 0 ? { upgrade } : {},
|
|
2038
|
+
...settings !== undefined && settings.previousNames.length > 0 ? { previousNames: [...settings.previousNames] } : {}
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
function describeRegistry(name, extended, registry, provider, named, log, publishers) {
|
|
2042
|
+
const scope = {
|
|
2043
|
+
properties: referencesOf(registry, "properties"),
|
|
2044
|
+
blueprints: referencesOf(registry, "blueprints"),
|
|
2045
|
+
records: referencesOf(registry, "records"),
|
|
2046
|
+
entities: referencesOf(registry, "entities")
|
|
2047
|
+
};
|
|
2048
|
+
const repositories = referencesOf(registry, "repositories");
|
|
2049
|
+
const environmentRefs = referencesOf(registry, "environments");
|
|
2050
|
+
const useCaseRefs = referencesOf(registry, "useCases");
|
|
2051
|
+
const exceptionRefs = referencesOf(registry, "exceptions");
|
|
2052
|
+
const describeImplementation = (UseCase) => {
|
|
2053
|
+
const steps = interpretedStepsOf(UseCase);
|
|
2054
|
+
return steps === undefined ? { kind: "opaque" } : {
|
|
2055
|
+
kind: "steps",
|
|
2056
|
+
steps: describeSteps(steps, {
|
|
2057
|
+
record: (Class) => scope.entities.get(Class) ?? scope.records.get(Class),
|
|
2058
|
+
error: (Class) => {
|
|
2059
|
+
const ref = exceptionRefs.get(Class);
|
|
2060
|
+
return ref === undefined ? { core: Class.name } : ref;
|
|
2061
|
+
}
|
|
2062
|
+
})
|
|
2063
|
+
};
|
|
2064
|
+
};
|
|
2065
|
+
const describeOption = (value) => {
|
|
2066
|
+
if (isLoggerDeclaration(value))
|
|
2067
|
+
return "Logger";
|
|
2068
|
+
if (isUseCaseClass2(value))
|
|
2069
|
+
return useCaseRefs.get(value);
|
|
2070
|
+
if (isEnvironment(value))
|
|
2071
|
+
return environmentRefs.get(value);
|
|
2072
|
+
if (isRepository(value)) {
|
|
2073
|
+
return repositories.get(inspectRepository(value).state);
|
|
2074
|
+
}
|
|
2075
|
+
if (typeof value === "function") {
|
|
2076
|
+
if (isDeclaredException(value)) {
|
|
2077
|
+
return exceptionRefs.get(value) ?? { core: value.name };
|
|
2078
|
+
}
|
|
2079
|
+
return scope.entities.get(value) ?? scope.records.get(value) ?? { kind: "opaque" };
|
|
2080
|
+
}
|
|
2081
|
+
if (Array.isArray(value))
|
|
2082
|
+
return value.map(describeOption);
|
|
2083
|
+
if (typeof value === "object" && value !== null) {
|
|
2084
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).map(([key, item]) => [key, describeOption(item)]));
|
|
2085
|
+
}
|
|
2086
|
+
return value;
|
|
2087
|
+
};
|
|
2088
|
+
const describeGeneric = (UseCase) => {
|
|
2089
|
+
const origin = useCaseOriginOf2(UseCase);
|
|
2090
|
+
if (!origin || interpretedStepsOf(UseCase) === undefined)
|
|
2091
|
+
return {};
|
|
2092
|
+
const repository = repositories.get(inspectRepository(origin.repository).state);
|
|
2093
|
+
if (repository === undefined)
|
|
2094
|
+
return {};
|
|
2095
|
+
return {
|
|
2096
|
+
generic: {
|
|
2097
|
+
name: origin.plugin.name,
|
|
2098
|
+
version: origin.plugin.version,
|
|
2099
|
+
repository,
|
|
2100
|
+
options: describeOption(origin.options)
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
};
|
|
2104
|
+
const refsOf = (blueprints) => {
|
|
2105
|
+
const refs = {};
|
|
2106
|
+
for (const [key, blueprint] of Object.entries(blueprints)) {
|
|
2107
|
+
const ref = blueprint === undefined ? undefined : scope.blueprints.get(blueprint.properties);
|
|
2108
|
+
if (ref !== undefined)
|
|
2109
|
+
refs[key] = ref;
|
|
2110
|
+
}
|
|
2111
|
+
return Object.keys(refs).length === 0 ? {} : { refs };
|
|
2112
|
+
};
|
|
2113
|
+
const each = (collection, describe) => Object.fromEntries([...registry[collection].values()].map((entry) => [
|
|
2114
|
+
entry.name,
|
|
2115
|
+
describe(entry)
|
|
2116
|
+
]));
|
|
2117
|
+
return {
|
|
2118
|
+
name,
|
|
2119
|
+
extends: extended.map((other) => other.name),
|
|
2120
|
+
...provider === undefined ? {} : { provider },
|
|
2121
|
+
...named.size === 0 ? {} : {
|
|
2122
|
+
providers: Object.fromEntries([...named].map(([key, { slot, from }]) => {
|
|
2123
|
+
const { value } = slotEntry(slot);
|
|
2124
|
+
return [
|
|
2125
|
+
key,
|
|
2126
|
+
{
|
|
2127
|
+
from,
|
|
2128
|
+
...isProviderFactory2(value) ? { kind: "opaque" } : providerDescriptionOf2(value)
|
|
2129
|
+
}
|
|
2130
|
+
];
|
|
2131
|
+
}))
|
|
2132
|
+
},
|
|
2133
|
+
...log === undefined ? {} : log === "opaque" ? { log: { kind: "opaque" } } : {
|
|
2134
|
+
log: {
|
|
2135
|
+
level: log.level,
|
|
2136
|
+
providers: log.providers.map((logProvider) => {
|
|
2137
|
+
const providerName = logProviderNameOf(logProvider);
|
|
2138
|
+
return providerName === undefined ? { kind: "opaque" } : { kind: "log-provider", name: providerName };
|
|
2139
|
+
})
|
|
2140
|
+
}
|
|
2141
|
+
},
|
|
2142
|
+
...Object.keys(publishers).length === 0 ? {} : {
|
|
2143
|
+
publishers: Object.fromEntries(Object.entries(publishers).map(([key, from]) => [key, { from }]))
|
|
2144
|
+
},
|
|
2145
|
+
properties: each("properties", (entry) => ({
|
|
2146
|
+
from: entry.from,
|
|
2147
|
+
...propertyDescriptionOf(entry.value, scope)
|
|
2148
|
+
})),
|
|
2149
|
+
blueprints: each("blueprints", (entry) => ({
|
|
2150
|
+
from: entry.from,
|
|
2151
|
+
fields: fieldsDescriptionOf(entry.value.properties, scope)
|
|
2152
|
+
})),
|
|
2153
|
+
records: each("records", (entry) => recordDescriptionOf(entry, scope)),
|
|
2154
|
+
entities: each("entities", (entry) => recordDescriptionOf(entry, scope)),
|
|
2155
|
+
environments: each("environments", (entry) => ({
|
|
2156
|
+
from: entry.from,
|
|
2157
|
+
fields: fieldsDescriptionOf(entry.value.blueprint.properties, scope)
|
|
2158
|
+
})),
|
|
2159
|
+
repositories: each("repositories", (entry) => {
|
|
2160
|
+
const info = inspectRepository(entry.value);
|
|
2161
|
+
return {
|
|
2162
|
+
from: entry.from,
|
|
2163
|
+
entity: scope.entities.get(info.entity),
|
|
2164
|
+
actions: info.actions,
|
|
2165
|
+
deletion: info.deletion,
|
|
2166
|
+
indexes: info.indexes,
|
|
2167
|
+
scans: info.scans,
|
|
2168
|
+
relations: info.relations.map((relation) => repositories.get(relation)),
|
|
2169
|
+
environments: info.environments.map((environment) => environmentRefs.get(environment)),
|
|
2170
|
+
...info.provider === undefined ? {} : {
|
|
2171
|
+
provider: isProviderSlot(info.provider) ? referenceOf(info.provider) : providerDescriptionOf2(info.provider)
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
}),
|
|
2175
|
+
useCases: each("useCases", (entry) => {
|
|
2176
|
+
const UseCase = entry.value;
|
|
2177
|
+
return {
|
|
2178
|
+
from: entry.from,
|
|
2179
|
+
name: UseCase.useCaseName,
|
|
2180
|
+
...UseCase.description === undefined ? {} : { description: UseCase.description },
|
|
2181
|
+
tags: UseCase.tags,
|
|
2182
|
+
deprecated: UseCase.deprecated,
|
|
2183
|
+
...UseCase.route === undefined ? {} : { route: UseCase.route },
|
|
2184
|
+
...UseCase.access === undefined ? {} : { access: UseCase.access },
|
|
2185
|
+
input: fieldsDescriptionOf(UseCase.blueprint.properties, scope),
|
|
2186
|
+
response: fieldsDescriptionOf(UseCase.response.properties, scope),
|
|
2187
|
+
...UseCase.context === undefined ? {} : {
|
|
2188
|
+
context: fieldsDescriptionOf(UseCase.context.properties, scope)
|
|
2189
|
+
},
|
|
2190
|
+
...refsOf({
|
|
2191
|
+
input: UseCase.blueprint,
|
|
2192
|
+
response: UseCase.response,
|
|
2193
|
+
context: UseCase.context
|
|
2194
|
+
}),
|
|
2195
|
+
errors: UseCase.errors.map((Thrown) => {
|
|
2196
|
+
const ref = exceptionRefs.get(Thrown);
|
|
2197
|
+
return ref === undefined ? { kind: "core", name: Thrown.name } : { kind: "exception", ref };
|
|
2198
|
+
}),
|
|
2199
|
+
dependencies: Object.fromEntries(Object.entries(UseCase.dependencies).map(([key, dependency]) => [
|
|
2200
|
+
key,
|
|
2201
|
+
isLoggerDeclaration(dependency) ? { kind: "logger", ref: "Logger" } : isUseCaseClass2(dependency) ? {
|
|
2202
|
+
kind: "use-case",
|
|
2203
|
+
ref: useCaseRefs.get(dependency)
|
|
2204
|
+
} : isEnvironment(dependency) ? {
|
|
2205
|
+
kind: "environment",
|
|
2206
|
+
ref: environmentRefs.get(dependency)
|
|
2207
|
+
} : {
|
|
2208
|
+
kind: "repository",
|
|
2209
|
+
ref: repositories.get(inspectRepository(dependency).state)
|
|
2210
|
+
}
|
|
2211
|
+
])),
|
|
2212
|
+
implementation: describeImplementation(UseCase),
|
|
2213
|
+
...describeGeneric(UseCase)
|
|
2214
|
+
};
|
|
2215
|
+
}),
|
|
2216
|
+
commands: each("commands", (entry) => {
|
|
2217
|
+
const command = entry.value;
|
|
2218
|
+
return {
|
|
2219
|
+
from: entry.from,
|
|
2220
|
+
name: command.commandName,
|
|
2221
|
+
...command.description === undefined ? {} : { description: command.description },
|
|
2222
|
+
tags: command.tags,
|
|
2223
|
+
...command.context === undefined ? {} : {
|
|
2224
|
+
context: fieldsDescriptionOf(command.context.properties, scope)
|
|
2225
|
+
},
|
|
2226
|
+
...refsOf({ context: command.context }),
|
|
2227
|
+
operations: Object.fromEntries(Object.entries(command.useCases).map(([op, UseCase]) => [
|
|
2228
|
+
op,
|
|
2229
|
+
{ ref: useCaseRefs.get(UseCase) }
|
|
2230
|
+
])),
|
|
2231
|
+
transaction: command.transaction,
|
|
2232
|
+
...command.environments.length === 0 ? {} : {
|
|
2233
|
+
environments: command.environments.map((environment) => environmentRefs.get(environment))
|
|
2234
|
+
},
|
|
2235
|
+
events: typeof command.events === "function" ? eventPolicyDescriptionOf2(command.events) ?? { kind: "opaque" } : command.events,
|
|
2236
|
+
...command.onEventError === undefined ? {} : { onEventError: { kind: "opaque" } }
|
|
2237
|
+
};
|
|
2238
|
+
}),
|
|
2239
|
+
outputs: each("outputs", (entry) => {
|
|
2240
|
+
const output = entry.value;
|
|
2241
|
+
return {
|
|
2242
|
+
from: entry.from,
|
|
2243
|
+
name: output.outputName,
|
|
2244
|
+
commands: output.commands.map((command) => command.commandName),
|
|
2245
|
+
statuses: output.statuses,
|
|
2246
|
+
pipe: output.stages.map((stage) => stageDescriptionOf2(stage))
|
|
2247
|
+
};
|
|
2248
|
+
}),
|
|
2249
|
+
exceptions: each("exceptions", (entry) => {
|
|
2250
|
+
const Declared = entry.value;
|
|
2251
|
+
const message = declaredMessageOf(Declared);
|
|
2252
|
+
return {
|
|
2253
|
+
from: entry.from,
|
|
2254
|
+
layer: Declared.layer,
|
|
2255
|
+
...message === undefined ? {} : { message }
|
|
2256
|
+
};
|
|
2257
|
+
})
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
function applicationOf2(settings) {
|
|
2261
|
+
const { name } = settings ?? {};
|
|
2262
|
+
if (typeof name !== "string" || name === "") {
|
|
2263
|
+
invalid3("applicationOf", "Expected a name.");
|
|
2264
|
+
}
|
|
2265
|
+
const extended = settings.extends ?? [];
|
|
2266
|
+
if (!Array.isArray(extended) || !extended.every(isApplication)) {
|
|
2267
|
+
invalid3(name, "Expected `extends` to be a list of applications.");
|
|
2268
|
+
}
|
|
2269
|
+
const registry = resolve(settings);
|
|
2270
|
+
validate(name, registry);
|
|
2271
|
+
validateUseCases(name, registry);
|
|
2272
|
+
validateCommands(name, registry);
|
|
2273
|
+
validateOutputs(name, registry);
|
|
2274
|
+
if (settings.providers !== undefined && !isProviderStorage(settings.providers)) {
|
|
2275
|
+
invalid3(name, "Expected `providers` to be a storage made by providersOf.");
|
|
2276
|
+
}
|
|
2277
|
+
const named = new Map;
|
|
2278
|
+
const addNamed = (entry) => {
|
|
2279
|
+
const { key } = slotEntry(entry.slot);
|
|
2280
|
+
const current = named.get(key);
|
|
2281
|
+
if (current !== undefined && current.slot !== entry.slot) {
|
|
2282
|
+
throw new ApplicationConflict2({
|
|
2283
|
+
source: name,
|
|
2284
|
+
details: {
|
|
2285
|
+
reason: `The provider "${key}" is declared by different storages, in "${current.from}" and "${entry.from}".`,
|
|
2286
|
+
origins: [current.from, entry.from],
|
|
2287
|
+
declarations: [`provider ${key}`, `provider ${key}`]
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
named.set(key, entry);
|
|
2292
|
+
};
|
|
2293
|
+
for (const other of extended) {
|
|
2294
|
+
for (const entry of namedProviders.get(other)?.values() ?? []) {
|
|
2295
|
+
addNamed(entry);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
for (const slot of settings.providers ? slotsOf(settings.providers) : []) {
|
|
2299
|
+
const inherited = named.get(slotEntry(slot).key);
|
|
2300
|
+
if (inherited?.slot === slot)
|
|
2301
|
+
continue;
|
|
2302
|
+
const { value } = slotEntry(slot);
|
|
2303
|
+
addNamed({
|
|
2304
|
+
slot,
|
|
2305
|
+
from: name,
|
|
2306
|
+
instance: isProviderFactory2(value) ? deferredProvider(value, name) : value
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
const instances = new Map([...named.values()].map((entry) => [entry.slot, entry.instance]));
|
|
2310
|
+
const instanceOf = (slot, holder) => {
|
|
2311
|
+
const instance = instances.get(slot);
|
|
2312
|
+
if (instance === undefined) {
|
|
2313
|
+
throw new UnregisteredApplicationReference2({
|
|
2314
|
+
source: name,
|
|
2315
|
+
details: {
|
|
2316
|
+
holder,
|
|
2317
|
+
reference: `provider "${slotEntry(slot).key}"`
|
|
2318
|
+
}
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
return instance;
|
|
2322
|
+
};
|
|
2323
|
+
for (const entry of registry.repositories.values()) {
|
|
2324
|
+
const declared = inspectRepository(entry.value).provider;
|
|
2325
|
+
if (isProviderSlot(declared)) {
|
|
2326
|
+
instanceOf(declared, `repository "${entry.name}"`);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
const inheritedFrom = settings.provider === undefined ? [...extended].reverse().find((other) => other.provider) : undefined;
|
|
2330
|
+
const declaredProvider = settings.provider ?? inheritedFrom?.provider;
|
|
2331
|
+
if (declaredProvider !== undefined && typeof declaredProvider !== "object") {
|
|
2332
|
+
invalid3(name, "Expected `provider` to be a repository provider.");
|
|
2333
|
+
}
|
|
2334
|
+
const defaultSlot = isProviderSlot(declaredProvider) ? declaredProvider : inheritedFrom === undefined ? undefined : defaultSlots.get(inheritedFrom);
|
|
2335
|
+
const provider = defaultSlot !== undefined ? instanceOf(defaultSlot, "the application") : isProviderFactory2(declaredProvider) ? deferredProvider(declaredProvider, name) : declaredProvider;
|
|
2336
|
+
const allProviders = [
|
|
2337
|
+
...new Set([provider, ...instances.values()].filter((value) => value !== undefined))
|
|
2338
|
+
];
|
|
2339
|
+
{
|
|
2340
|
+
const registered = new Set([...registry.environments.values()].map((entry) => entry.key));
|
|
2341
|
+
const holderOf = (instance) => {
|
|
2342
|
+
const entry = [...named.values()].find((candidate) => candidate.instance === instance);
|
|
2343
|
+
return entry === undefined || instance === provider ? "the provider" : `provider "${slotEntry(entry.slot).key}"`;
|
|
2344
|
+
};
|
|
2345
|
+
for (const instance of allProviders) {
|
|
2346
|
+
for (const environment of deferredEnvironments(instance)) {
|
|
2347
|
+
if (registered.has(environment))
|
|
2348
|
+
continue;
|
|
2349
|
+
throw new UnregisteredApplicationReference2({
|
|
2350
|
+
source: name,
|
|
2351
|
+
details: {
|
|
2352
|
+
holder: holderOf(instance),
|
|
2353
|
+
reference: "an environment"
|
|
2354
|
+
}
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
const declaredLog = settings.log ?? [...extended].reverse().map((other) => logSettings.get(other)).find((log) => log !== undefined);
|
|
2360
|
+
const log = declaredLog === undefined ? undefined : applicationLogger(name);
|
|
2361
|
+
if (declaredLog !== undefined && !isLogFactory(declaredLog)) {
|
|
2362
|
+
log?.set(checkLogSettings(declaredLog));
|
|
2363
|
+
}
|
|
2364
|
+
const logger = log?.logger;
|
|
2365
|
+
const { publishers, origins } = publishersOf(name, settings.publishers, extended);
|
|
2366
|
+
const hasPublishers = Object.keys(publishers).length > 0;
|
|
2367
|
+
for (const entry of registry.commands.values()) {
|
|
2368
|
+
for (const key of eventPolicyPublishersOf(entry.value.events)) {
|
|
2369
|
+
if (!Object.hasOwn(publishers, key)) {
|
|
2370
|
+
throw new UnregisteredApplicationReference2({
|
|
2371
|
+
source: name,
|
|
2372
|
+
details: {
|
|
2373
|
+
holder: `command "${entry.name}"`,
|
|
2374
|
+
reference: `publisher "${key}"`
|
|
2375
|
+
}
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
const bindsProviders = provider !== undefined || instances.size > 0;
|
|
2381
|
+
const rebinds = bindsProviders || logger !== undefined || hasPublishers;
|
|
2382
|
+
const collection = (key) => Object.freeze(Object.fromEntries([...registry[key].values()].map((entry) => [entry.name, entry.value])));
|
|
2383
|
+
const bound = new Map;
|
|
2384
|
+
const boundByState = new Map;
|
|
2385
|
+
const repositories = {};
|
|
2386
|
+
for (const entry of registry.repositories.values()) {
|
|
2387
|
+
const repository = bindsProviders ? bindRepository(entry.value, provider, instances, {
|
|
2388
|
+
application: name,
|
|
2389
|
+
key: entry.name,
|
|
2390
|
+
logger
|
|
2391
|
+
}) : entry.value;
|
|
2392
|
+
bound.set(entry.value, repository);
|
|
2393
|
+
boundByState.set(entry.key, repository);
|
|
2394
|
+
repositories[entry.name] = repository;
|
|
2395
|
+
}
|
|
2396
|
+
const boundUseCases = new Map;
|
|
2397
|
+
const bindUseCase = (UseCase) => {
|
|
2398
|
+
if (!rebinds)
|
|
2399
|
+
return UseCase;
|
|
2400
|
+
const done = boundUseCases.get(UseCase);
|
|
2401
|
+
if (done)
|
|
2402
|
+
return done;
|
|
2403
|
+
const Declared = UseCase;
|
|
2404
|
+
|
|
2405
|
+
class Bound extends Declared {
|
|
2406
|
+
}
|
|
2407
|
+
Object.defineProperty(Bound, "name", { value: Declared.name });
|
|
2408
|
+
boundUseCases.set(UseCase, Bound);
|
|
2409
|
+
const dependencies = Object.fromEntries(Object.entries(UseCase.dependencies).map(([key, dependency]) => [
|
|
2410
|
+
key,
|
|
2411
|
+
isUseCaseClass2(dependency) ? bindUseCase(dependency) : isEnvironment(dependency) || isLoggerDeclaration(dependency) ? dependency : boundByState.get(inspectRepository(dependency).state) ?? dependency
|
|
2412
|
+
]));
|
|
2413
|
+
Object.defineProperty(Bound, "dependencies", {
|
|
2414
|
+
value: Object.freeze(dependencies)
|
|
2415
|
+
});
|
|
2416
|
+
if (logger)
|
|
2417
|
+
Object.defineProperty(Bound, ApplicationLoggerKey, { value: logger });
|
|
2418
|
+
return Bound;
|
|
2419
|
+
};
|
|
2420
|
+
const useCases = {};
|
|
2421
|
+
const boundUseCaseOf = new Map;
|
|
2422
|
+
for (const entry of registry.useCases.values()) {
|
|
2423
|
+
const bound = bindUseCase(entry.value);
|
|
2424
|
+
boundUseCaseOf.set(entry.value, bound);
|
|
2425
|
+
useCases[entry.name] = bound;
|
|
2426
|
+
}
|
|
2427
|
+
const commands = {};
|
|
2428
|
+
const boundCommandOf = new Map;
|
|
2429
|
+
for (const entry of registry.commands.values()) {
|
|
2430
|
+
const declared = entry.value;
|
|
2431
|
+
const rebuilt = !rebinds ? declared : commandOf({
|
|
2432
|
+
name: declared.commandName,
|
|
2433
|
+
description: declared.description,
|
|
2434
|
+
tags: declared.tags,
|
|
2435
|
+
context: declared.context,
|
|
2436
|
+
useCases: Object.fromEntries(Object.entries(declared.useCases).map(([op, UseCase]) => [
|
|
2437
|
+
op,
|
|
2438
|
+
boundUseCaseOf.get(UseCase)
|
|
2439
|
+
])),
|
|
2440
|
+
transaction: declared.transaction,
|
|
2441
|
+
environmentDependencies: declared.environments,
|
|
2442
|
+
events: declared.events,
|
|
2443
|
+
onEventError: declared.onEventError
|
|
2444
|
+
});
|
|
2445
|
+
if (bindsProviders)
|
|
2446
|
+
verifyCommand(rebuilt);
|
|
2447
|
+
if (hasPublishers)
|
|
2448
|
+
bindPublishers(rebuilt, publishers);
|
|
2449
|
+
boundCommandOf.set(entry.value, rebuilt);
|
|
2450
|
+
commands[entry.name] = rebuilt;
|
|
2451
|
+
}
|
|
2452
|
+
const outputs = {};
|
|
2453
|
+
const boundOutputOf = new Map;
|
|
2454
|
+
for (const entry of registry.outputs.values()) {
|
|
2455
|
+
const declared = entry.value;
|
|
2456
|
+
const rebuilt = !rebinds ? declared : outputOf2({
|
|
2457
|
+
name: declared.outputName,
|
|
2458
|
+
commands: declared.commands.map((command) => boundCommandOf.get(command)),
|
|
2459
|
+
statuses: declared.statuses,
|
|
2460
|
+
pipe: declared.stages
|
|
2461
|
+
});
|
|
2462
|
+
boundOutputOf.set(entry.value, rebuilt);
|
|
2463
|
+
outputs[entry.name] = rebuilt;
|
|
2464
|
+
}
|
|
2465
|
+
const environmentEntries = [...registry.environments.values()];
|
|
2466
|
+
const registeredEnvironments = new Set(environmentEntries.map((entry) => entry.value));
|
|
2467
|
+
const context = Object.freeze({
|
|
2468
|
+
name,
|
|
2469
|
+
env(environment) {
|
|
2470
|
+
if (!registeredEnvironments.has(environment)) {
|
|
2471
|
+
throw new UnregisteredApplicationReference2({
|
|
2472
|
+
source: name,
|
|
2473
|
+
details: { holder: "the context", reference: "an environment" }
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
return environment.load();
|
|
2477
|
+
},
|
|
2478
|
+
logger: logger ?? silentLogger
|
|
2479
|
+
});
|
|
2480
|
+
let starting;
|
|
2481
|
+
let started = false;
|
|
2482
|
+
const start = async (lock) => {
|
|
2483
|
+
if (lock !== undefined) {
|
|
2484
|
+
if (!isLock(lock)) {
|
|
2485
|
+
invalid3(name, "Expected lock to be a schema lock ({ lockVersion: 1, entities }).");
|
|
2486
|
+
}
|
|
2487
|
+
const problems = schemaLockProblemsOf2(lock, schemaLockOf2(application.describe()));
|
|
2488
|
+
if (problems.length > 0) {
|
|
2489
|
+
throw new SchemaLockMismatch2({ source: name, details: { problems } });
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
const failures = {};
|
|
2493
|
+
let cause;
|
|
2494
|
+
for (const entry of environmentEntries) {
|
|
2495
|
+
try {
|
|
2496
|
+
entry.value.load();
|
|
2497
|
+
} catch (error) {
|
|
2498
|
+
if (!(error instanceof Exception2) || !error.is(InvalidEnvironment2)) {
|
|
2499
|
+
throw error;
|
|
2500
|
+
}
|
|
2501
|
+
cause ??= error;
|
|
2502
|
+
failures[entry.name] = [
|
|
2503
|
+
...error.details?.keys ?? []
|
|
2504
|
+
];
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
if (Object.keys(failures).length > 0) {
|
|
2508
|
+
throw new InvalidApplicationEnvironment2({
|
|
2509
|
+
source: name,
|
|
2510
|
+
details: { environments: failures },
|
|
2511
|
+
cause
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
if (isLogFactory(declaredLog)) {
|
|
2515
|
+
log?.set(checkLogSettings(await declaredLog.create(context)));
|
|
2516
|
+
}
|
|
2517
|
+
for (const instance of allProviders) {
|
|
2518
|
+
if (!isDeferredProvider(instance))
|
|
2519
|
+
continue;
|
|
2520
|
+
const made = await startDeferred(instance, context);
|
|
2521
|
+
checkTransactions(name, made, Object.entries(commands).filter(([, command]) => command.transaction && commandTransactionProvider(command) === instance).map(([key]) => key));
|
|
2522
|
+
checkCapabilities(name, made, Object.entries(repositories).filter(([, repository]) => providerOfRepository(repository) === instance && inspectRepository(repository).actions.includes("findCursor")).map(([key]) => key));
|
|
2523
|
+
}
|
|
2524
|
+
started = true;
|
|
2525
|
+
};
|
|
2526
|
+
const application = Object.freeze({
|
|
2527
|
+
[Kind2]: "application",
|
|
2528
|
+
name,
|
|
2529
|
+
extends: Object.freeze([...extended]),
|
|
2530
|
+
provider,
|
|
2531
|
+
logger: logger ?? silentLogger,
|
|
2532
|
+
context,
|
|
2533
|
+
...logger ? { [ApplicationLoggerKey]: logger } : {},
|
|
2534
|
+
...hasPublishers ? { [ApplicationPublishersKey]: publishers } : {},
|
|
2535
|
+
properties: collection("properties"),
|
|
2536
|
+
blueprints: collection("blueprints"),
|
|
2537
|
+
records: collection("records"),
|
|
2538
|
+
entities: collection("entities"),
|
|
2539
|
+
environments: collection("environments"),
|
|
2540
|
+
repositories: Object.freeze(repositories),
|
|
2541
|
+
useCases: Object.freeze(useCases),
|
|
2542
|
+
commands: Object.freeze(commands),
|
|
2543
|
+
outputs: Object.freeze(outputs),
|
|
2544
|
+
exceptions: collection("exceptions"),
|
|
2545
|
+
useCase(useCase) {
|
|
2546
|
+
if (!boundUseCaseOf.has(useCase)) {
|
|
2547
|
+
invalid3(name, "Expected a use case registered in the application.");
|
|
2548
|
+
}
|
|
2549
|
+
return boundUseCaseOf.get(useCase);
|
|
2550
|
+
},
|
|
2551
|
+
async run(useCase, input = {}, options) {
|
|
2552
|
+
if (!boundUseCaseOf.has(useCase)) {
|
|
2553
|
+
invalid3(name, "Expected a use case registered in the application.");
|
|
2554
|
+
}
|
|
2555
|
+
const Bound = boundUseCaseOf.get(useCase);
|
|
2556
|
+
const instance = options?.context === undefined ? new Bound(input) : new Bound(input, { context: options.context });
|
|
2557
|
+
return instance.execute(options?.dependencies);
|
|
2558
|
+
},
|
|
2559
|
+
command(command) {
|
|
2560
|
+
if (!boundCommandOf.has(command)) {
|
|
2561
|
+
invalid3(name, "Expected a command registered in the application.");
|
|
2562
|
+
}
|
|
2563
|
+
return boundCommandOf.get(command);
|
|
2564
|
+
},
|
|
2565
|
+
output(output) {
|
|
2566
|
+
if (!boundOutputOf.has(output)) {
|
|
2567
|
+
invalid3(name, "Expected an output registered in the application.");
|
|
2568
|
+
}
|
|
2569
|
+
return boundOutputOf.get(output);
|
|
2570
|
+
},
|
|
2571
|
+
repository(repository) {
|
|
2572
|
+
if (!bound.has(repository)) {
|
|
2573
|
+
invalid3(name, "Expected a repository registered in the application.");
|
|
2574
|
+
}
|
|
2575
|
+
return bound.get(repository);
|
|
2576
|
+
},
|
|
2577
|
+
extend(...others) {
|
|
2578
|
+
return applicationOf2({
|
|
2579
|
+
name,
|
|
2580
|
+
extends: [application, ...others],
|
|
2581
|
+
...provider === undefined ? {} : { provider: defaultSlot ?? provider },
|
|
2582
|
+
...declaredLog === undefined ? {} : { log: declaredLog }
|
|
2583
|
+
});
|
|
2584
|
+
},
|
|
2585
|
+
describe: () => describeRegistry(name, extended, registry, defaultSlot !== undefined ? referenceOf(defaultSlot) : provider === undefined ? undefined : providerDescriptionOf2(provider), named, isLogFactory(declaredLog) ? "opaque" : log?.settings, origins),
|
|
2586
|
+
get started() {
|
|
2587
|
+
return started;
|
|
2588
|
+
},
|
|
2589
|
+
migrate(options) {
|
|
2590
|
+
return migrateApplication({
|
|
2591
|
+
name,
|
|
2592
|
+
entities: new Map([...registry.entities.values()].map((entry) => [
|
|
2593
|
+
entry.name,
|
|
2594
|
+
entry.value
|
|
2595
|
+
])),
|
|
2596
|
+
repositories: Object.values(repositories),
|
|
2597
|
+
describe: () => application.describe()
|
|
2598
|
+
}, options);
|
|
2599
|
+
},
|
|
2600
|
+
start(options = {}) {
|
|
2601
|
+
const { replace = false, register = true, lock } = options ?? {};
|
|
2602
|
+
starting ??= (async () => {
|
|
2603
|
+
if (register) {
|
|
2604
|
+
const current = startedApplication();
|
|
2605
|
+
if (current !== undefined && current !== application) {
|
|
2606
|
+
if (!replace) {
|
|
2607
|
+
throw new ApplicationAlreadyStarted2({
|
|
2608
|
+
source: name,
|
|
2609
|
+
details: { current: current.name, attempted: name }
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
await current.stop();
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
await start(lock);
|
|
2616
|
+
if (register)
|
|
2617
|
+
setStartedApplication(application);
|
|
2618
|
+
})().then(() => application, async (error) => {
|
|
2619
|
+
starting = undefined;
|
|
2620
|
+
for (const instance of allProviders) {
|
|
2621
|
+
if (isDeferredProvider(instance))
|
|
2622
|
+
await stopProvider(instance);
|
|
2623
|
+
}
|
|
2624
|
+
if (isLogFactory(declaredLog))
|
|
2625
|
+
log?.set(undefined);
|
|
2626
|
+
throw error;
|
|
2627
|
+
});
|
|
2628
|
+
return starting;
|
|
2629
|
+
},
|
|
2630
|
+
async stop() {
|
|
2631
|
+
const wasStarting = starting;
|
|
2632
|
+
starting = undefined;
|
|
2633
|
+
started = false;
|
|
2634
|
+
if (wasStarting)
|
|
2635
|
+
await wasStarting.catch(() => {
|
|
2636
|
+
return;
|
|
2637
|
+
});
|
|
2638
|
+
if (startedApplication() === application)
|
|
2639
|
+
setStartedApplication(undefined);
|
|
2640
|
+
for (const instance of allProviders)
|
|
2641
|
+
await stopProvider(instance);
|
|
2642
|
+
await log?.flush();
|
|
2643
|
+
if (isLogFactory(declaredLog))
|
|
2644
|
+
log?.set(undefined);
|
|
2645
|
+
},
|
|
2646
|
+
get(declaration) {
|
|
2647
|
+
if (isEnvironment(declaration)) {
|
|
2648
|
+
if (!registeredEnvironments.has(declaration)) {
|
|
2649
|
+
invalid3(name, "Expected an environment registered in the application.");
|
|
2650
|
+
}
|
|
2651
|
+
return declaration.load();
|
|
2652
|
+
}
|
|
2653
|
+
if (isCommand2(declaration))
|
|
2654
|
+
return application.command(declaration);
|
|
2655
|
+
if (isOutput2(declaration))
|
|
2656
|
+
return application.output(declaration);
|
|
2657
|
+
if (isUseCaseClass2(declaration))
|
|
2658
|
+
return application.useCase(declaration);
|
|
2659
|
+
if (isRepository(declaration)) {
|
|
2660
|
+
return application.repository(declaration);
|
|
2661
|
+
}
|
|
2662
|
+
if (isProviderSlot(declaration)) {
|
|
2663
|
+
const instance = instances.get(declaration);
|
|
2664
|
+
if (instance === undefined) {
|
|
2665
|
+
invalid3(name, "Expected a provider of a storage registered in the application.");
|
|
2666
|
+
}
|
|
2667
|
+
return instance;
|
|
2668
|
+
}
|
|
2669
|
+
return invalid3(name, "Expected a repository, use case, command, output, environment or provider registered in the application.");
|
|
2670
|
+
}
|
|
2671
|
+
});
|
|
2672
|
+
internals.set(application, registry);
|
|
2673
|
+
namedProviders.set(application, named);
|
|
2674
|
+
if (defaultSlot !== undefined)
|
|
2675
|
+
defaultSlots.set(application, defaultSlot);
|
|
2676
|
+
if (declaredLog !== undefined)
|
|
2677
|
+
logSettings.set(application, declaredLog);
|
|
2678
|
+
publisherOrigins.set(application, Object.freeze(origins));
|
|
2679
|
+
return application;
|
|
2680
|
+
}
|
|
2681
|
+
// src/core/blueprint/blueprint-plugin.ts
|
|
2682
|
+
function blueprintPluginOf2(name, properties, factories = {}) {
|
|
2683
|
+
return Object.freeze({
|
|
2684
|
+
name,
|
|
2685
|
+
properties: Object.freeze({ ...properties }),
|
|
2686
|
+
factories: Object.freeze({ ...factories })
|
|
2687
|
+
});
|
|
2688
|
+
}
|
|
2689
|
+
// src/core/event-policy/publish.ts
|
|
2690
|
+
var KEYS2 = ["publisher", "topic", "events", "handle", "onPublishError"];
|
|
2691
|
+
var IDENTIFIER2 = /^[A-Za-z_$][\w$]*$/;
|
|
2692
|
+
var PLACEHOLDER = /\{(event|command|operation|id)\}/g;
|
|
2693
|
+
function validate2(options) {
|
|
2694
|
+
const given = options;
|
|
2695
|
+
for (const key of Object.keys(given)) {
|
|
2696
|
+
if (!KEYS2.includes(key))
|
|
2697
|
+
return `publishEvents has no option "${key}".`;
|
|
2698
|
+
}
|
|
2699
|
+
const { publisher, topic, events, handle, onPublishError } = given;
|
|
2700
|
+
if (typeof publisher !== "string" || !IDENTIFIER2.test(publisher)) {
|
|
2701
|
+
return "Expected publishEvents.publisher to be the key of a publisher of the application.";
|
|
2702
|
+
}
|
|
2703
|
+
if (typeof topic !== "string" || topic === "") {
|
|
2704
|
+
return 'Expected publishEvents.topic to be a non-empty string, such as "posts/{id}".';
|
|
2705
|
+
}
|
|
2706
|
+
const unknown = topic.replaceAll(PLACEHOLDER, "").match(/\{[^}]*\}/);
|
|
2707
|
+
if (unknown) {
|
|
2708
|
+
return `publishEvents.topic has an unknown placeholder ${unknown[0]}: use {event}, {command}, {operation} or {id}.`;
|
|
2709
|
+
}
|
|
2710
|
+
if (events !== undefined && (!Array.isArray(events) || events.some((name) => typeof name !== "string" || name === ""))) {
|
|
2711
|
+
return "Expected publishEvents.events to be a list of event names.";
|
|
2712
|
+
}
|
|
2713
|
+
if (handle !== undefined && typeof handle !== "boolean") {
|
|
2714
|
+
return "Expected publishEvents.handle to be a boolean.";
|
|
2715
|
+
}
|
|
2716
|
+
if (onPublishError !== undefined && onPublishError !== "log" && onPublishError !== "throw") {
|
|
2717
|
+
return 'Expected publishEvents.onPublishError to be "log" or "throw".';
|
|
2718
|
+
}
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
function entityJson(event) {
|
|
2722
|
+
const text = JSON.stringify(event.entity);
|
|
2723
|
+
return text === undefined ? null : JSON.parse(text);
|
|
2724
|
+
}
|
|
2725
|
+
var publishEvents2 = eventPolicyOf2({
|
|
2726
|
+
name: "publishEvents",
|
|
2727
|
+
validate: validate2,
|
|
2728
|
+
publishers: (options) => [options.publisher],
|
|
2729
|
+
handle: async (events, info, options) => {
|
|
2730
|
+
const { command, operation, logger } = info;
|
|
2731
|
+
const commandName = command.commandName;
|
|
2732
|
+
const source = `${commandName}.${operation}`;
|
|
2733
|
+
const only = options.events ? new Set(options.events) : undefined;
|
|
2734
|
+
const failed = (cause, event, index) => new CommandEventFailed2({
|
|
2735
|
+
source,
|
|
2736
|
+
cause,
|
|
2737
|
+
details: {
|
|
2738
|
+
operation,
|
|
2739
|
+
event: event.name,
|
|
2740
|
+
handled: index,
|
|
2741
|
+
remaining: events.length - index - 1
|
|
2742
|
+
}
|
|
2743
|
+
});
|
|
2744
|
+
for (const [index, event] of events.entries()) {
|
|
2745
|
+
if (options.handle !== false) {
|
|
2746
|
+
try {
|
|
2747
|
+
await event.handle({ logger });
|
|
2748
|
+
} catch (cause) {
|
|
2749
|
+
throw failed(cause, event, index);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
if (only && !only.has(event.name))
|
|
2753
|
+
continue;
|
|
2754
|
+
let publisher;
|
|
2755
|
+
try {
|
|
2756
|
+
publisher = info.publisher(options.publisher);
|
|
2757
|
+
} catch (cause) {
|
|
2758
|
+
throw failed(cause, event, index);
|
|
2759
|
+
}
|
|
2760
|
+
const entity = entityJson(event);
|
|
2761
|
+
const id = entity?.id;
|
|
2762
|
+
const topic = options.topic.replaceAll(PLACEHOLDER, (_, key) => key === "event" ? event.name : key === "command" ? commandName : key === "operation" ? operation : id === undefined ? "" : String(id));
|
|
2763
|
+
const published = Object.freeze({
|
|
2764
|
+
name: event.name,
|
|
2765
|
+
command: commandName,
|
|
2766
|
+
operation,
|
|
2767
|
+
entity
|
|
2768
|
+
});
|
|
2769
|
+
try {
|
|
2770
|
+
await publisher.publish(topic, published);
|
|
2771
|
+
} catch (cause) {
|
|
2772
|
+
if (options.onPublishError === "throw")
|
|
2773
|
+
throw failed(cause, event, index);
|
|
2774
|
+
logFailure(logger, "event-publish-failed", {
|
|
2775
|
+
command: commandName,
|
|
2776
|
+
operation,
|
|
2777
|
+
event: event.name,
|
|
2778
|
+
publisher: options.publisher,
|
|
2779
|
+
topic
|
|
2780
|
+
}, cause);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
});
|
|
2785
|
+
// src/core/output/select.ts
|
|
2786
|
+
var LISTS = ["tags", "operations", "exclude"];
|
|
2787
|
+
var select2 = transformOf2({
|
|
2788
|
+
name: "select",
|
|
2789
|
+
validate: (options) => {
|
|
2790
|
+
for (const key of Object.keys(options)) {
|
|
2791
|
+
if (!LISTS.includes(key)) {
|
|
2792
|
+
return `select has no option "${key}".`;
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
for (const key of LISTS) {
|
|
2796
|
+
const list = options[key];
|
|
2797
|
+
if (list !== undefined && (!Array.isArray(list) || list.some((item) => typeof item !== "string"))) {
|
|
2798
|
+
return `Expected select.${key} to be a list of strings.`;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
return;
|
|
2802
|
+
},
|
|
2803
|
+
apply: (operation, { tags, operations, exclude }) => {
|
|
2804
|
+
if (tags && !tags.some((tag) => operation.tags.includes(tag)))
|
|
2805
|
+
return null;
|
|
2806
|
+
if (operations && !operations.includes(operation.id))
|
|
2807
|
+
return null;
|
|
2808
|
+
if (exclude?.includes(operation.id))
|
|
2809
|
+
return null;
|
|
2810
|
+
return operation;
|
|
2811
|
+
}
|
|
2812
|
+
});
|
|
2813
|
+
// src/core/parse/strip.ts
|
|
2814
|
+
function collectionOf(properties, value) {
|
|
2815
|
+
if (!isPlainObject(value))
|
|
2816
|
+
return value;
|
|
2817
|
+
const kept = {};
|
|
2818
|
+
for (const [name, field] of fieldsOf(properties)) {
|
|
2819
|
+
if (!Object.hasOwn(value, name))
|
|
2820
|
+
continue;
|
|
2821
|
+
const item = fieldOf2(field, value[name]);
|
|
2822
|
+
if (item === undefined)
|
|
2823
|
+
continue;
|
|
2824
|
+
Object.defineProperty(kept, name, {
|
|
2825
|
+
value: item,
|
|
2826
|
+
enumerable: true,
|
|
2827
|
+
writable: true,
|
|
2828
|
+
configurable: true
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
return kept;
|
|
2832
|
+
}
|
|
2833
|
+
function fieldOf2(field, value) {
|
|
2834
|
+
if (isBlueprintList(field)) {
|
|
2835
|
+
return Array.isArray(value) ? value.map((item) => fieldOf2(field.item, item)) : value;
|
|
2836
|
+
}
|
|
2837
|
+
if (isBlueprint(field))
|
|
2838
|
+
return collectionOf(field.properties, value);
|
|
2839
|
+
if (isBlueprintVariant(field)) {
|
|
2840
|
+
const variant = variantOfValue(field, value);
|
|
2841
|
+
if (!variant || !isPlainObject(value))
|
|
2842
|
+
return value;
|
|
2843
|
+
return {
|
|
2844
|
+
[field.key]: value[field.key],
|
|
2845
|
+
...collectionOf(variant.properties, value)
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
const record = recordFieldOf(field);
|
|
2849
|
+
if (record) {
|
|
2850
|
+
return collectionOf(record.record.blueprint.properties, value);
|
|
2851
|
+
}
|
|
2852
|
+
return value;
|
|
2853
|
+
}
|
|
2854
|
+
function stripUndeclared2(shape, value) {
|
|
2855
|
+
return fieldOf2(shape, value);
|
|
2856
|
+
}
|
|
2857
|
+
// src/core/record/plugin-catalog.ts
|
|
2858
|
+
var LISTS2 = ["keys", "sensitive", "unique", "change", "members"];
|
|
2859
|
+
function isDescriptor(value) {
|
|
2860
|
+
if (typeof value !== "object" || value === null)
|
|
2861
|
+
return false;
|
|
2862
|
+
const descriptor = value;
|
|
2863
|
+
const isList = (list) => Array.isArray(list) && list.every((item) => typeof item === "string");
|
|
2864
|
+
return descriptor.kind === "record-plugin" && typeof descriptor.name === "string" && isVersion(descriptor.version) && LISTS2.every((list) => isList(descriptor[list])) && Array.isArray(descriptor.requires) && descriptor.requires.every((required) => typeof required === "object" && required !== null && typeof required.name === "string" && isVersion(required.version));
|
|
2865
|
+
}
|
|
2866
|
+
function mismatchOf(plugin, descriptor) {
|
|
2867
|
+
if (!satisfies(plugin.version, descriptor.version)) {
|
|
2868
|
+
return `version ${plugin.version} does not satisfy ${descriptor.version}`;
|
|
2869
|
+
}
|
|
2870
|
+
const current = {
|
|
2871
|
+
keys: plugin.keys,
|
|
2872
|
+
sensitive: plugin.options.sensitive,
|
|
2873
|
+
unique: plugin.options.unique,
|
|
2874
|
+
change: plugin.options.change,
|
|
2875
|
+
members: plugin.members
|
|
2876
|
+
};
|
|
2877
|
+
for (const list of LISTS2) {
|
|
2878
|
+
const missing = descriptor[list].find((item) => !current[list].includes(item));
|
|
2879
|
+
if (missing !== undefined)
|
|
2880
|
+
return `"${missing}" is not in its ${list}`;
|
|
2881
|
+
}
|
|
2882
|
+
for (const required of descriptor.requires) {
|
|
2883
|
+
const found = plugin.requires.find(({ name }) => name === required.name);
|
|
2884
|
+
if (!found)
|
|
2885
|
+
return `it does not require "${required.name}"`;
|
|
2886
|
+
if (!satisfies(found.version, required.version)) {
|
|
2887
|
+
return `its "${required.name}" ${found.version} does not satisfy ${required.version}`;
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
function recordPluginCatalogOf2(...plugins) {
|
|
2893
|
+
const byName = {};
|
|
2894
|
+
for (const plugin of plugins) {
|
|
2895
|
+
if (!isRecordPlugin(plugin)) {
|
|
2896
|
+
throw new InvalidRecordPlugin2({
|
|
2897
|
+
source: "catalog",
|
|
2898
|
+
details: {
|
|
2899
|
+
reason: "a catalog takes plugins made by `recordPluginOf`"
|
|
2900
|
+
}
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
if (Object.hasOwn(byName, plugin.name)) {
|
|
2904
|
+
throw new InvalidRecordPlugin2({
|
|
2905
|
+
source: plugin.name,
|
|
2906
|
+
details: { reason: "the catalog has another plugin with its name" }
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
byName[plugin.name] = plugin;
|
|
2910
|
+
}
|
|
2911
|
+
const find = (name) => {
|
|
2912
|
+
const plugin = Object.hasOwn(byName, name) ? byName[name] : undefined;
|
|
2913
|
+
if (!plugin) {
|
|
2914
|
+
throw new UnknownRecordPlugin2({ source: "catalog", details: { name } });
|
|
2915
|
+
}
|
|
2916
|
+
return plugin;
|
|
2917
|
+
};
|
|
2918
|
+
const resolve = (reference) => {
|
|
2919
|
+
if (typeof reference === "string") {
|
|
2920
|
+
const at = reference.lastIndexOf("@");
|
|
2921
|
+
if (at <= 0)
|
|
2922
|
+
return find(reference);
|
|
2923
|
+
const plugin = find(reference.slice(0, at));
|
|
2924
|
+
const version = reference.slice(at + 1);
|
|
2925
|
+
if (!isVersion(version)) {
|
|
2926
|
+
throw new IncompatibleRecordPlugin2({
|
|
2927
|
+
source: plugin.name,
|
|
2928
|
+
details: { reason: `"${version}" is not a semantic version` }
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
if (!satisfies(plugin.version, version)) {
|
|
2932
|
+
throw new IncompatibleRecordPlugin2({
|
|
2933
|
+
source: plugin.name,
|
|
2934
|
+
details: {
|
|
2935
|
+
reason: `version ${plugin.version} does not satisfy ${version}`
|
|
2936
|
+
}
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
return plugin;
|
|
2940
|
+
}
|
|
2941
|
+
if (!isDescriptor(reference)) {
|
|
2942
|
+
const name = reference?.name;
|
|
2943
|
+
throw new IncompatibleRecordPlugin2({
|
|
2944
|
+
source: typeof name === "string" ? name : "unknown",
|
|
2945
|
+
details: { reason: "it is not a record plugin descriptor" }
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
const plugin = find(reference.name);
|
|
2949
|
+
const reason = mismatchOf(plugin, reference);
|
|
2950
|
+
if (reason !== undefined) {
|
|
2951
|
+
throw new IncompatibleRecordPlugin2({
|
|
2952
|
+
source: plugin.name,
|
|
2953
|
+
details: { reason }
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
return plugin;
|
|
2957
|
+
};
|
|
2958
|
+
return Object.freeze({
|
|
2959
|
+
[Kind2]: "record-plugin-catalog",
|
|
2960
|
+
plugins: Object.freeze(byName),
|
|
2961
|
+
list: Object.freeze([...plugins]),
|
|
2962
|
+
resolve,
|
|
2963
|
+
resolveAll: (references) => Object.freeze(references.map(resolve))
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
// src/core/use-case/decorators.ts
|
|
2967
|
+
function sameData(a, b) {
|
|
2968
|
+
if (a === b)
|
|
2969
|
+
return true;
|
|
2970
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null || Array.isArray(a) !== Array.isArray(b)) {
|
|
2971
|
+
return false;
|
|
2972
|
+
}
|
|
2973
|
+
const left = Object.keys(a);
|
|
2974
|
+
return left.length === Object.keys(b).length && left.every((key) => Object.hasOwn(b, key) && sameData(a[key], b[key]));
|
|
2975
|
+
}
|
|
2976
|
+
function decoratedUseCase(value, decorator) {
|
|
2977
|
+
if (typeof value !== "function" || value[Kind2] !== "use-case") {
|
|
2978
|
+
throw new InvalidUseCaseDeclaration2({
|
|
2979
|
+
source: decorator,
|
|
2980
|
+
details: {
|
|
2981
|
+
reason: `${decorator} decorates a class that extends useCaseOf(...).`
|
|
2982
|
+
}
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
return value;
|
|
2986
|
+
}
|
|
2987
|
+
function Route2(method, pathOrOptions, options) {
|
|
2988
|
+
const settings = typeof pathOrOptions === "string" ? { ...options, method, path: pathOrOptions } : { ...pathOrOptions, method };
|
|
2989
|
+
return (value) => {
|
|
2990
|
+
const UseCase = decoratedUseCase(value, "@Route");
|
|
2991
|
+
const invalid = (reason) => {
|
|
2992
|
+
throw new InvalidUseCaseDeclaration2({
|
|
2993
|
+
source: UseCase.useCaseName,
|
|
2994
|
+
details: { reason }
|
|
2995
|
+
});
|
|
2996
|
+
};
|
|
2997
|
+
const declared = routeDeclarationOf(settings, UseCase.blueprint, UseCase.response);
|
|
2998
|
+
if (typeof declared === "string")
|
|
2999
|
+
return invalid(declared);
|
|
3000
|
+
if (UseCase.route !== undefined) {
|
|
3001
|
+
if (sameData(UseCase.route, declared))
|
|
3002
|
+
return;
|
|
3003
|
+
invalid("The use case already has another route: declare it once, with the route setting or with @Route.");
|
|
3004
|
+
}
|
|
3005
|
+
Object.defineProperty(UseCase, "route", { value: declared });
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
function Access2(access) {
|
|
3009
|
+
return (value) => {
|
|
3010
|
+
const UseCase = decoratedUseCase(value, "@Access");
|
|
3011
|
+
const invalid = (reason) => {
|
|
3012
|
+
throw new InvalidUseCaseDeclaration2({
|
|
3013
|
+
source: UseCase.useCaseName,
|
|
3014
|
+
details: { reason }
|
|
3015
|
+
});
|
|
3016
|
+
};
|
|
3017
|
+
const declared = accessDeclarationOf(access, UseCase.blueprint);
|
|
3018
|
+
if (typeof declared === "string")
|
|
3019
|
+
return invalid(declared);
|
|
3020
|
+
if (UseCase.access !== undefined) {
|
|
3021
|
+
if (sameData(UseCase.access, declared))
|
|
3022
|
+
return;
|
|
3023
|
+
invalid("The use case already declares another access: declare it once, with the access setting or with @Access.");
|
|
3024
|
+
}
|
|
3025
|
+
Object.defineProperty(UseCase, "access", { value: declared });
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
// src/std/properties/contact.ts
|
|
3029
|
+
import { Type } from "typebox";
|
|
3030
|
+
var Email2 = propertyOf2({
|
|
3031
|
+
name: "email",
|
|
3032
|
+
schema: Type.String({ format: "email", maxLength: 254 }),
|
|
3033
|
+
default: "demo@example.com",
|
|
3034
|
+
preHandle: (value) => value.trim().toLowerCase()
|
|
3035
|
+
});
|
|
3036
|
+
var Phone2 = propertyOf2({
|
|
3037
|
+
name: "phone",
|
|
3038
|
+
schema: Type.String({ pattern: "^\\+[1-9]\\d{1,14}$" }),
|
|
3039
|
+
default: "+5511999999999",
|
|
3040
|
+
preHandle: (value) => value.replace(/[\s().-]/g, "")
|
|
3041
|
+
});
|
|
3042
|
+
var Url2 = propertyOf2({
|
|
3043
|
+
name: "url",
|
|
3044
|
+
schema: Type.String({ format: "url", maxLength: 2048 }),
|
|
3045
|
+
default: "https://example.com",
|
|
3046
|
+
preHandle: (value) => value.trim()
|
|
3047
|
+
});
|
|
3048
|
+
var Ip2 = propertyOf2({
|
|
3049
|
+
name: "ip",
|
|
3050
|
+
schema: Type.Union([
|
|
3051
|
+
Type.String({ format: "ipv4" }),
|
|
3052
|
+
Type.String({ format: "ipv6" })
|
|
3053
|
+
]),
|
|
3054
|
+
default: "127.0.0.1",
|
|
3055
|
+
preHandle: (value) => value.trim()
|
|
3056
|
+
});
|
|
3057
|
+
|
|
3058
|
+
// src/std/properties/enum.ts
|
|
3059
|
+
import { Type as Type2 } from "typebox";
|
|
3060
|
+
function enumOf2(values, {
|
|
3061
|
+
name = "enum",
|
|
3062
|
+
optional,
|
|
3063
|
+
default: _default = values[0]
|
|
3064
|
+
} = {}) {
|
|
3065
|
+
const Enum = propertyOf2({
|
|
3066
|
+
name,
|
|
3067
|
+
schema: Type2.Enum(values),
|
|
3068
|
+
optional,
|
|
3069
|
+
default: _default
|
|
3070
|
+
});
|
|
3071
|
+
recordPropertyLineage(Enum, {
|
|
3072
|
+
kind: "factory",
|
|
3073
|
+
factory: "enumOf",
|
|
3074
|
+
args: Object.freeze({
|
|
3075
|
+
values: Object.freeze([...values]),
|
|
3076
|
+
...name === "enum" ? {} : { name },
|
|
3077
|
+
...optional ? { optional: true } : {},
|
|
3078
|
+
..._default === values[0] ? {} : { default: _default }
|
|
3079
|
+
})
|
|
3080
|
+
});
|
|
3081
|
+
return Enum;
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
// src/std/properties/headless.ts
|
|
3085
|
+
import { Type as Type3 } from "typebox";
|
|
3086
|
+
import { Meta } from "typebox/schema";
|
|
3087
|
+
var JSON_SCHEMA = Type3.Unsafe({
|
|
3088
|
+
type: "object",
|
|
3089
|
+
allOf: [Meta["https://json-schema.org/draft/2020-12/schema"]]
|
|
3090
|
+
});
|
|
3091
|
+
|
|
3092
|
+
class SchemaProperty2 extends propertyOf2({
|
|
3093
|
+
name: "schema",
|
|
3094
|
+
schema: JSON_SCHEMA,
|
|
3095
|
+
default: Type3.String()
|
|
3096
|
+
}) {
|
|
3097
|
+
compiled;
|
|
3098
|
+
static deserialize(json) {
|
|
3099
|
+
let value;
|
|
3100
|
+
try {
|
|
3101
|
+
value = JSON.parse(json);
|
|
3102
|
+
} catch (error) {
|
|
3103
|
+
throw new InvalidPropertyValue2({
|
|
3104
|
+
source: "schema",
|
|
3105
|
+
details: { errors: [error.message] }
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
return new SchemaProperty2(value);
|
|
3109
|
+
}
|
|
3110
|
+
serialize() {
|
|
3111
|
+
return JSON.stringify(this.value);
|
|
3112
|
+
}
|
|
3113
|
+
match(data) {
|
|
3114
|
+
return this.toSchema().match(data);
|
|
3115
|
+
}
|
|
3116
|
+
errors(data) {
|
|
3117
|
+
return this.toSchema().errors(data);
|
|
3118
|
+
}
|
|
3119
|
+
toSchema() {
|
|
3120
|
+
if (!this.compiled) {
|
|
3121
|
+
const Schema = schemaOf("schema", this.value);
|
|
3122
|
+
this.compiled = new Schema;
|
|
3123
|
+
}
|
|
3124
|
+
return this.compiled;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
// src/std/properties/identity.ts
|
|
3129
|
+
import slugify from "slugify";
|
|
3130
|
+
import { Type as Type4 } from "typebox";
|
|
3131
|
+
|
|
3132
|
+
// src/std/runtime.ts
|
|
3133
|
+
var PasswordHasherRequired2 = exceptionOf2({
|
|
3134
|
+
layer: "property" /* Property */,
|
|
3135
|
+
name: "PasswordHasherRequired",
|
|
3136
|
+
message: "No password hasher is available: this runtime has neither Bun.password nor native argon2 (node:crypto, Node >= 24.7). Call configure({ hasher }) with your own PasswordHasher."
|
|
3137
|
+
});
|
|
3138
|
+
var SyncVerifyUnsupported2 = exceptionOf2({
|
|
3139
|
+
layer: "property" /* Property */,
|
|
3140
|
+
name: "SyncVerifyUnsupported",
|
|
3141
|
+
message: "The configured password hasher has no verifySync. Use verify, or configure a hasher that provides verifySync."
|
|
3142
|
+
});
|
|
3143
|
+
var UnsupportedPasswordHash = exceptionOf2({
|
|
3144
|
+
layer: "property" /* Property */,
|
|
3145
|
+
name: "UnsupportedPasswordHash",
|
|
3146
|
+
message: ({ version }) => `The password hash uses argon2 version ${version}, but native argon2 (node:crypto) only derives version ${ARGON2_VERSION}. Verify it with Bun.password or configure({ hasher }).`
|
|
3147
|
+
});
|
|
3148
|
+
markExported(PasswordHasherRequired2, SyncVerifyUnsupported2);
|
|
3149
|
+
var ARGON2_VERSION = 19;
|
|
3150
|
+
var ARGON2_PARAMS = {
|
|
3151
|
+
memory: 65536,
|
|
3152
|
+
passes: 2,
|
|
3153
|
+
parallelism: 1,
|
|
3154
|
+
tagLength: 32,
|
|
3155
|
+
saltLength: 32,
|
|
3156
|
+
version: ARGON2_VERSION
|
|
3157
|
+
};
|
|
3158
|
+
var toBase64 = (bytes) => btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join("")).replace(/=+$/, "");
|
|
3159
|
+
var fromBase64 = (text) => Uint8Array.from(atob(text), (char) => char.charCodeAt(0));
|
|
3160
|
+
function encodePhc(salt, tag) {
|
|
3161
|
+
const { memory, passes, parallelism, version } = ARGON2_PARAMS;
|
|
3162
|
+
return `$argon2id$v=${version}$m=${memory},t=${passes},p=${parallelism}$${toBase64(salt)}$${toBase64(tag)}`;
|
|
3163
|
+
}
|
|
3164
|
+
var PHC = /^\$argon2id\$v=(\d+)\$m=(\d+),t=(\d+),p=(\d+)\$([A-Za-z0-9+/]+)\$([A-Za-z0-9+/]+)$/;
|
|
3165
|
+
function decodePhc(phc) {
|
|
3166
|
+
const match = PHC.exec(phc);
|
|
3167
|
+
if (!match)
|
|
3168
|
+
return;
|
|
3169
|
+
return {
|
|
3170
|
+
version: Number(match[1]),
|
|
3171
|
+
memory: Number(match[2]),
|
|
3172
|
+
passes: Number(match[3]),
|
|
3173
|
+
parallelism: Number(match[4]),
|
|
3174
|
+
salt: fromBase64(match[5]),
|
|
3175
|
+
tag: fromBase64(match[6])
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
function nodeHasherOf(crypto) {
|
|
3179
|
+
const { argon2, argon2Sync, randomBytes, timingSafeEqual } = crypto;
|
|
3180
|
+
if (!argon2 || !argon2Sync || !randomBytes || !timingSafeEqual) {
|
|
3181
|
+
return;
|
|
3182
|
+
}
|
|
3183
|
+
const derive = (parameters) => new Promise((resolve, reject) => {
|
|
3184
|
+
argon2("argon2id", parameters, (error, key) => error ? reject(error) : resolve(key));
|
|
3185
|
+
});
|
|
3186
|
+
const parametersOf = (plain, parsed) => ({
|
|
3187
|
+
message: plain,
|
|
3188
|
+
nonce: parsed.salt,
|
|
3189
|
+
parallelism: parsed.parallelism,
|
|
3190
|
+
tagLength: parsed.tag.length,
|
|
3191
|
+
memory: parsed.memory,
|
|
3192
|
+
passes: parsed.passes
|
|
3193
|
+
});
|
|
3194
|
+
const decode = (hash) => {
|
|
3195
|
+
const parsed = decodePhc(hash);
|
|
3196
|
+
if (parsed && parsed.version !== ARGON2_VERSION) {
|
|
3197
|
+
throw new UnsupportedPasswordHash({
|
|
3198
|
+
source: "password",
|
|
3199
|
+
details: { version: parsed.version }
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
return parsed;
|
|
3203
|
+
};
|
|
3204
|
+
const matches = (tag, expected) => tag.length === expected.length && timingSafeEqual(tag, expected);
|
|
3205
|
+
return {
|
|
3206
|
+
async hash(plain) {
|
|
3207
|
+
const salt = randomBytes(ARGON2_PARAMS.saltLength);
|
|
3208
|
+
const tag = await derive({
|
|
3209
|
+
message: plain,
|
|
3210
|
+
nonce: salt,
|
|
3211
|
+
parallelism: ARGON2_PARAMS.parallelism,
|
|
3212
|
+
tagLength: ARGON2_PARAMS.tagLength,
|
|
3213
|
+
memory: ARGON2_PARAMS.memory,
|
|
3214
|
+
passes: ARGON2_PARAMS.passes
|
|
3215
|
+
});
|
|
3216
|
+
return encodePhc(salt, tag);
|
|
3217
|
+
},
|
|
3218
|
+
async verify(plain, hash) {
|
|
3219
|
+
const parsed = decode(hash);
|
|
3220
|
+
if (!parsed)
|
|
3221
|
+
return false;
|
|
3222
|
+
return matches(await derive(parametersOf(plain, parsed)), parsed.tag);
|
|
3223
|
+
},
|
|
3224
|
+
verifySync(plain, hash) {
|
|
3225
|
+
const parsed = decode(hash);
|
|
3226
|
+
if (!parsed)
|
|
3227
|
+
return false;
|
|
3228
|
+
const tag = argon2Sync("argon2id", parametersOf(plain, parsed));
|
|
3229
|
+
return matches(tag, parsed.tag);
|
|
3230
|
+
}
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
function bunHasherOf(password) {
|
|
3234
|
+
return {
|
|
3235
|
+
hash: (plain) => password.hash(plain, { algorithm: "argon2id" }),
|
|
3236
|
+
verify: (plain, hash) => password.verify(plain, hash),
|
|
3237
|
+
verifySync: (plain, hash) => password.verifySync(plain, hash)
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
function missingHasher() {
|
|
3241
|
+
return {
|
|
3242
|
+
hash: () => Promise.reject(new PasswordHasherRequired2({ source: "password" })),
|
|
3243
|
+
verify: () => Promise.reject(new PasswordHasherRequired2({ source: "password" })),
|
|
3244
|
+
verifySync: () => {
|
|
3245
|
+
throw new PasswordHasherRequired2({ source: "password" });
|
|
3246
|
+
}
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
function defaultHasherOf(environment) {
|
|
3250
|
+
if (environment.bun?.password) {
|
|
3251
|
+
return bunHasherOf(environment.bun.password);
|
|
3252
|
+
}
|
|
3253
|
+
const node = environment.crypto && nodeHasherOf(environment.crypto);
|
|
3254
|
+
return node ?? missingHasher();
|
|
3255
|
+
}
|
|
3256
|
+
var lastTimestamp = 0;
|
|
3257
|
+
var sequence = 0;
|
|
3258
|
+
var HEX = Array.from({ length: 256 }, (_, byte) => byte.toString(16).padStart(2, "0"));
|
|
3259
|
+
var RANDOM_PER_ID = 10;
|
|
3260
|
+
var randomPool = new Uint8Array(RANDOM_PER_ID * 64);
|
|
3261
|
+
var randomAt = randomPool.length;
|
|
3262
|
+
function uuidv7(now = Date.now()) {
|
|
3263
|
+
if (randomAt === randomPool.length) {
|
|
3264
|
+
globalThis.crypto.getRandomValues(randomPool);
|
|
3265
|
+
randomAt = 0;
|
|
3266
|
+
}
|
|
3267
|
+
const at = randomAt;
|
|
3268
|
+
randomAt += RANDOM_PER_ID;
|
|
3269
|
+
const random = randomPool;
|
|
3270
|
+
const start = random[at + 8] << 8 | random[at + 9];
|
|
3271
|
+
if (now > lastTimestamp) {
|
|
3272
|
+
lastTimestamp = now;
|
|
3273
|
+
sequence = start & 2047;
|
|
3274
|
+
} else if (sequence < 4095) {
|
|
3275
|
+
sequence++;
|
|
3276
|
+
} else {
|
|
3277
|
+
lastTimestamp++;
|
|
3278
|
+
sequence = start & 2047;
|
|
3279
|
+
}
|
|
3280
|
+
const high = Math.floor(lastTimestamp / 2 ** 32);
|
|
3281
|
+
const low = lastTimestamp % 2 ** 32;
|
|
3282
|
+
const hex = (byte) => HEX[byte & 255];
|
|
3283
|
+
return hex(high >>> 8) + hex(high) + hex(low >>> 24) + hex(low >>> 16) + "-" + hex(low >>> 8) + hex(low) + "-" + hex(112 | sequence >>> 8) + hex(sequence) + "-" + hex(random[at] & 63 | 128) + hex(random[at + 1]) + "-" + hex(random[at + 2]) + hex(random[at + 3]) + hex(random[at + 4]) + hex(random[at + 5]) + hex(random[at + 6]) + hex(random[at + 7]);
|
|
3284
|
+
}
|
|
3285
|
+
var hasher;
|
|
3286
|
+
var defaultIdGenerator = () => uuidv7();
|
|
3287
|
+
var idGenerator = defaultIdGenerator;
|
|
3288
|
+
function configure2(services) {
|
|
3289
|
+
if (services.hasher)
|
|
3290
|
+
hasher = services.hasher;
|
|
3291
|
+
if (services.idGenerator)
|
|
3292
|
+
idGenerator = services.idGenerator;
|
|
3293
|
+
}
|
|
3294
|
+
function resetRuntime2() {
|
|
3295
|
+
hasher = undefined;
|
|
3296
|
+
idGenerator = defaultIdGenerator;
|
|
3297
|
+
lastTimestamp = 0;
|
|
3298
|
+
sequence = 0;
|
|
3299
|
+
}
|
|
3300
|
+
function nodeCryptoOf() {
|
|
3301
|
+
const process = globalThis.process;
|
|
3302
|
+
return process?.getBuiltinModule?.("node:crypto");
|
|
3303
|
+
}
|
|
3304
|
+
function currentHasher() {
|
|
3305
|
+
hasher ??= defaultHasherOf({
|
|
3306
|
+
bun: globalThis.Bun,
|
|
3307
|
+
crypto: nodeCryptoOf()
|
|
3308
|
+
});
|
|
3309
|
+
return hasher;
|
|
3310
|
+
}
|
|
3311
|
+
function makesDefaultIds() {
|
|
3312
|
+
return idGenerator === defaultIdGenerator;
|
|
3313
|
+
}
|
|
3314
|
+
function generateId() {
|
|
3315
|
+
return idGenerator();
|
|
3316
|
+
}
|
|
3317
|
+
function verifySyncWithCurrent(plain, hash) {
|
|
3318
|
+
const current = currentHasher();
|
|
3319
|
+
if (!current.verifySync)
|
|
3320
|
+
throw new SyncVerifyUnsupported2({ source: "password" });
|
|
3321
|
+
return current.verifySync(plain, hash);
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
// src/std/properties/identity.ts
|
|
3325
|
+
var UUID_V7 = "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$";
|
|
3326
|
+
|
|
3327
|
+
class Id2 extends propertyOf2({
|
|
3328
|
+
name: "id",
|
|
3329
|
+
schema: Type4.String({ pattern: UUID_V7 }),
|
|
3330
|
+
default: "01a173d2-051c-77bb-8166-a371e999573e",
|
|
3331
|
+
preHandle: (value) => value.toLowerCase()
|
|
3332
|
+
}) {
|
|
3333
|
+
static generate() {
|
|
3334
|
+
return new Id2(generateId());
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
class Slug2 extends propertyOf2({
|
|
3339
|
+
name: "slug",
|
|
3340
|
+
schema: Type4.String({
|
|
3341
|
+
pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
3342
|
+
maxLength: 255
|
|
3343
|
+
}),
|
|
3344
|
+
default: "demo-slug",
|
|
3345
|
+
unique: true,
|
|
3346
|
+
preHandle: (value) => slugify(value, { lower: true, strict: true, trim: true })
|
|
3347
|
+
}) {
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
// src/std/properties/json.ts
|
|
3351
|
+
import { Type as Type5 } from "typebox";
|
|
3352
|
+
import { Meta as Meta2 } from "typebox/schema";
|
|
3353
|
+
import Value from "typebox/value";
|
|
3354
|
+
|
|
3355
|
+
// src/std/properties/exceptions.ts
|
|
3356
|
+
var InvalidJsonData2 = exceptionOf2({
|
|
3357
|
+
layer: "property" /* Property */,
|
|
3358
|
+
name: "InvalidJsonData",
|
|
3359
|
+
message: ({ issues }, source) => `Invalid JSON data for "${source}": ${issues.map((issue) => `${issue.path.length === 0 ? "/" : `/${issue.path.join("/")}`}: ${issue.message}`).join("; ")}`
|
|
3360
|
+
});
|
|
3361
|
+
var InvalidJsonSchema2 = exceptionOf2({
|
|
3362
|
+
layer: "schema" /* Schema */,
|
|
3363
|
+
name: "InvalidJsonSchema",
|
|
3364
|
+
message: (_, source) => `The schema of "${source}" is not a JSON Schema.`
|
|
3365
|
+
});
|
|
3366
|
+
markExported(InvalidJsonData2, InvalidJsonSchema2);
|
|
3367
|
+
|
|
3368
|
+
// src/std/properties/json.ts
|
|
3369
|
+
function notJson(value, path = [], ancestors = []) {
|
|
3370
|
+
if (value === null)
|
|
3371
|
+
return;
|
|
3372
|
+
switch (typeof value) {
|
|
3373
|
+
case "string":
|
|
3374
|
+
case "boolean":
|
|
3375
|
+
return;
|
|
3376
|
+
case "number":
|
|
3377
|
+
return Number.isFinite(value) ? undefined : { path, message: `is not JSON (${value})` };
|
|
3378
|
+
case "object":
|
|
3379
|
+
break;
|
|
3380
|
+
default:
|
|
3381
|
+
return { path, message: `is not JSON (${typeof value})` };
|
|
3382
|
+
}
|
|
3383
|
+
if (ancestors.includes(value))
|
|
3384
|
+
return { path, message: "is circular" };
|
|
3385
|
+
const inner = [...ancestors, value];
|
|
3386
|
+
if (Array.isArray(value)) {
|
|
3387
|
+
for (let index = 0;index < value.length; index++) {
|
|
3388
|
+
const found = notJson(value[index], [...path, index], inner);
|
|
3389
|
+
if (found)
|
|
3390
|
+
return found;
|
|
3391
|
+
}
|
|
3392
|
+
return;
|
|
3393
|
+
}
|
|
3394
|
+
const prototype = Object.getPrototypeOf(value);
|
|
3395
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
3396
|
+
return {
|
|
3397
|
+
path,
|
|
3398
|
+
message: `is not JSON (an instance of ${prototype?.constructor?.name ?? "a class"})`
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
for (const [key, item] of Object.entries(value)) {
|
|
3402
|
+
const found = notJson(item, [...path, key], inner);
|
|
3403
|
+
if (found)
|
|
3404
|
+
return found;
|
|
3405
|
+
}
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
var JSON_SCHEMA2 = Type5.Unsafe({
|
|
3409
|
+
type: "object",
|
|
3410
|
+
allOf: [Meta2["https://json-schema.org/draft/2020-12/schema"]]
|
|
3411
|
+
});
|
|
3412
|
+
var checked = new WeakSet;
|
|
3413
|
+
function assertJsonSchema(schema, source) {
|
|
3414
|
+
if (typeof schema === "object" && schema !== null && checked.has(schema)) {
|
|
3415
|
+
return schema;
|
|
3416
|
+
}
|
|
3417
|
+
if (!Value.Check(JSON_SCHEMA2, schema)) {
|
|
3418
|
+
throw new InvalidJsonSchema2({ source });
|
|
3419
|
+
}
|
|
3420
|
+
checked.add(schema);
|
|
3421
|
+
return schema;
|
|
3422
|
+
}
|
|
3423
|
+
function pathOf(pointer) {
|
|
3424
|
+
if (pointer === "")
|
|
3425
|
+
return [];
|
|
3426
|
+
return pointer.slice(1).split("/").map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~")).map((segment) => /^(?:0|[1-9][0-9]*)$/.test(segment) ? +segment : segment);
|
|
3427
|
+
}
|
|
3428
|
+
function issuesOfErrors(errors, source) {
|
|
3429
|
+
const issues = [];
|
|
3430
|
+
const add = (path, code, message) => {
|
|
3431
|
+
issues.push({ path, code, message, source });
|
|
3432
|
+
};
|
|
3433
|
+
for (const error of errors) {
|
|
3434
|
+
const path = pathOf(error.instancePath);
|
|
3435
|
+
const keys = (name) => Array.isArray(error.params[name]) ? error.params[name] : [];
|
|
3436
|
+
if (error.keyword === "required") {
|
|
3437
|
+
for (const key of keys("requiredProperties")) {
|
|
3438
|
+
add([...path, key], "required", "is required");
|
|
3439
|
+
}
|
|
3440
|
+
} else if (error.keyword === "additionalProperties") {
|
|
3441
|
+
for (const key of keys("additionalProperties")) {
|
|
3442
|
+
add([...path, key], "undeclared_key", "is not declared");
|
|
3443
|
+
}
|
|
3444
|
+
} else if (!(error.keyword === "boolean" && error.schemaPath.endsWith("/additionalProperties"))) {
|
|
3445
|
+
add(path, error.keyword === "type" ? "invalid_type" : "invalid_value", error.message);
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
return issues;
|
|
3449
|
+
}
|
|
3450
|
+
function jsonSchemaIssues2(schema, value, { source = "json" } = {}) {
|
|
3451
|
+
const checkedSchema = assertJsonSchema(schema, source);
|
|
3452
|
+
const found = notJson(value);
|
|
3453
|
+
if (found) {
|
|
3454
|
+
return [
|
|
3455
|
+
{
|
|
3456
|
+
path: found.path,
|
|
3457
|
+
code: "invalid_value",
|
|
3458
|
+
message: found.message,
|
|
3459
|
+
source
|
|
3460
|
+
}
|
|
3461
|
+
];
|
|
3462
|
+
}
|
|
3463
|
+
if (Value.Check(checkedSchema, value))
|
|
3464
|
+
return [];
|
|
3465
|
+
return issuesOfErrors(Value.Errors(checkedSchema, value), source);
|
|
3466
|
+
}
|
|
3467
|
+
function checkJsonSchema2(schema, value, options = {}) {
|
|
3468
|
+
const issues = jsonSchemaIssues2(schema, value, options);
|
|
3469
|
+
if (issues.length > 0) {
|
|
3470
|
+
throw new InvalidJsonData2({
|
|
3471
|
+
source: options.source ?? "json",
|
|
3472
|
+
details: { issues }
|
|
3473
|
+
});
|
|
3474
|
+
}
|
|
3475
|
+
return value;
|
|
3476
|
+
}
|
|
3477
|
+
function demoOf(schema) {
|
|
3478
|
+
try {
|
|
3479
|
+
return Value.Create(schema);
|
|
3480
|
+
} catch {
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
function assertJson(value) {
|
|
3485
|
+
const found = notJson(value);
|
|
3486
|
+
if (found) {
|
|
3487
|
+
throw new Error(`${found.path.length === 0 ? "" : `/${found.path.join("/")}: `}${found.message}`);
|
|
3488
|
+
}
|
|
3489
|
+
return value;
|
|
3490
|
+
}
|
|
3491
|
+
var JsonProperty2 = propertyOf2({
|
|
3492
|
+
name: "json",
|
|
3493
|
+
schema: Type5.Unsafe({}),
|
|
3494
|
+
default: {},
|
|
3495
|
+
preHandle: assertJson
|
|
3496
|
+
});
|
|
3497
|
+
function jsonOf2({
|
|
3498
|
+
schema,
|
|
3499
|
+
name = "json",
|
|
3500
|
+
optional,
|
|
3501
|
+
default: _default
|
|
3502
|
+
}) {
|
|
3503
|
+
assertJsonSchema(schema, name);
|
|
3504
|
+
const made = _default ?? demoOf(schema);
|
|
3505
|
+
const Json = propertyOf2({
|
|
3506
|
+
name,
|
|
3507
|
+
schema,
|
|
3508
|
+
optional,
|
|
3509
|
+
...made !== undefined && Value.Check(schema, made) ? { default: made } : {},
|
|
3510
|
+
preHandle: assertJson
|
|
3511
|
+
});
|
|
3512
|
+
recordPropertyLineage(Json, {
|
|
3513
|
+
kind: "factory",
|
|
3514
|
+
factory: "jsonOf",
|
|
3515
|
+
args: Object.freeze({
|
|
3516
|
+
schema,
|
|
3517
|
+
...name === "json" ? {} : { name },
|
|
3518
|
+
...optional ? { optional: true } : {},
|
|
3519
|
+
..._default === undefined ? {} : { default: _default }
|
|
3520
|
+
})
|
|
3521
|
+
});
|
|
3522
|
+
return Json;
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
// src/std/properties/password.ts
|
|
3526
|
+
import { Type as Type6 } from "typebox";
|
|
3527
|
+
var PASSWORD_MIN_LENGTH = 7;
|
|
3528
|
+
var PASSWORD_MAX_LENGTH = 128;
|
|
3529
|
+
var PASSWORD_SPECIAL_CHARACTERS = "!@#$%^&*()-_=+[]{}|\\;:'\",.<>/?`~";
|
|
3530
|
+
var ARGON2_HASH = "^\\$argon2(id|i|d)\\$v=\\d+\\$m=\\d+,t=\\d+,p=\\d+\\$[A-Za-z0-9+/]+\\$[A-Za-z0-9+/]+$";
|
|
3531
|
+
var DEMO_HASH = "$argon2id$v=19$m=65536,t=2,p=1$YO5NNmxyK9+adO95GxWsAaUX4TULmT3IGhTYfpshXl0$/C89OCh7bhDY2cR2UkgvOV3dPlkghNLhv/DJsdopwMo";
|
|
3532
|
+
function passwordIssues(value) {
|
|
3533
|
+
const issues = [];
|
|
3534
|
+
if (value.length < PASSWORD_MIN_LENGTH) {
|
|
3535
|
+
issues.push(`must have at least ${PASSWORD_MIN_LENGTH} characters`);
|
|
3536
|
+
}
|
|
3537
|
+
if (value.length > PASSWORD_MAX_LENGTH) {
|
|
3538
|
+
issues.push(`must have at most ${PASSWORD_MAX_LENGTH} characters`);
|
|
3539
|
+
}
|
|
3540
|
+
if (!/[a-z]/.test(value))
|
|
3541
|
+
issues.push("must contain a lowercase letter");
|
|
3542
|
+
if (!/[A-Z]/.test(value))
|
|
3543
|
+
issues.push("must contain an uppercase letter");
|
|
3544
|
+
if (!/\d/.test(value))
|
|
3545
|
+
issues.push("must contain a number");
|
|
3546
|
+
if (![...value].some((char) => PASSWORD_SPECIAL_CHARACTERS.includes(char))) {
|
|
3547
|
+
issues.push(`must contain a special character (${PASSWORD_SPECIAL_CHARACTERS})`);
|
|
3548
|
+
}
|
|
3549
|
+
return issues;
|
|
3550
|
+
}
|
|
3551
|
+
var escaped = PASSWORD_SPECIAL_CHARACTERS.replace(/[\\\]^-]/g, "\\$&");
|
|
3552
|
+
|
|
3553
|
+
class PlainPassword2 extends propertyOf2({
|
|
3554
|
+
name: "plainPassword",
|
|
3555
|
+
schema: Type6.String({
|
|
3556
|
+
minLength: PASSWORD_MIN_LENGTH,
|
|
3557
|
+
maxLength: PASSWORD_MAX_LENGTH,
|
|
3558
|
+
pattern: `^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[${escaped}])`
|
|
3559
|
+
}),
|
|
3560
|
+
default: "Demo@123",
|
|
3561
|
+
sensitive: true
|
|
3562
|
+
}) {
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
class Password2 extends propertyOf2({
|
|
3566
|
+
name: "password",
|
|
3567
|
+
schema: Type6.String({ pattern: ARGON2_HASH }),
|
|
3568
|
+
default: DEMO_HASH,
|
|
3569
|
+
sensitive: true
|
|
3570
|
+
}) {
|
|
3571
|
+
static [PropertyInput2] = {
|
|
3572
|
+
accepts: PlainPassword2,
|
|
3573
|
+
convert: async (plain) => (await Password2.hash(plain)).value
|
|
3574
|
+
};
|
|
3575
|
+
static async hash(plain) {
|
|
3576
|
+
const issues = passwordIssues(plain);
|
|
3577
|
+
if (issues.length > 0) {
|
|
3578
|
+
throw new InvalidPropertyValue2({
|
|
3579
|
+
source: "password",
|
|
3580
|
+
details: { errors: issues }
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
return new Password2(await currentHasher().hash(plain));
|
|
3584
|
+
}
|
|
3585
|
+
verify(input) {
|
|
3586
|
+
const hash = this.value;
|
|
3587
|
+
if (hash === undefined)
|
|
3588
|
+
return Promise.resolve(false);
|
|
3589
|
+
return currentHasher().verify(input, hash);
|
|
3590
|
+
}
|
|
3591
|
+
verifySync(input) {
|
|
3592
|
+
const hash = this.value;
|
|
3593
|
+
if (hash === undefined)
|
|
3594
|
+
return false;
|
|
3595
|
+
return verifySyncWithCurrent(input, hash);
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
// src/std/properties/primitives.ts
|
|
3600
|
+
import { Type as Type7 } from "typebox";
|
|
3601
|
+
var StringProperty2 = propertyOf2({
|
|
3602
|
+
name: "string",
|
|
3603
|
+
schema: Type7.String(),
|
|
3604
|
+
default: "demo"
|
|
3605
|
+
});
|
|
3606
|
+
var Text2 = propertyOf2({
|
|
3607
|
+
name: "text",
|
|
3608
|
+
schema: Type7.String({ minLength: 1 }),
|
|
3609
|
+
default: "Demo",
|
|
3610
|
+
preHandle: (value) => value.trim()
|
|
3611
|
+
});
|
|
3612
|
+
var NumberProperty2 = propertyOf2({
|
|
3613
|
+
name: "number",
|
|
3614
|
+
schema: Type7.Number(),
|
|
3615
|
+
default: 0
|
|
3616
|
+
});
|
|
3617
|
+
var Integer2 = propertyOf2({
|
|
3618
|
+
name: "integer",
|
|
3619
|
+
schema: Type7.Integer(),
|
|
3620
|
+
default: 0
|
|
3621
|
+
});
|
|
3622
|
+
var BooleanProperty2 = propertyOf2({
|
|
3623
|
+
name: "boolean",
|
|
3624
|
+
schema: Type7.Boolean(),
|
|
3625
|
+
default: false
|
|
3626
|
+
});
|
|
3627
|
+
|
|
3628
|
+
// src/std/properties/time.ts
|
|
3629
|
+
import { Type as Type8 } from "typebox";
|
|
3630
|
+
var DEMO_DATE_TIME = "2026-01-01T00:00:00.000Z";
|
|
3631
|
+
function dateTimeOf({
|
|
3632
|
+
name,
|
|
3633
|
+
optional,
|
|
3634
|
+
default: _default
|
|
3635
|
+
}) {
|
|
3636
|
+
return class extends propertyOf2({
|
|
3637
|
+
name,
|
|
3638
|
+
schema: Type8.String({ format: "date-time" }),
|
|
3639
|
+
optional,
|
|
3640
|
+
default: _default
|
|
3641
|
+
}) {
|
|
3642
|
+
static now() {
|
|
3643
|
+
return new this(new Date().toISOString());
|
|
3644
|
+
}
|
|
3645
|
+
};
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
class DateTime2 extends dateTimeOf({
|
|
3649
|
+
name: "dateTime",
|
|
3650
|
+
default: DEMO_DATE_TIME
|
|
3651
|
+
}) {
|
|
3652
|
+
}
|
|
3653
|
+
var DateProperty2 = propertyOf2({
|
|
3654
|
+
name: "date",
|
|
3655
|
+
schema: Type8.String({ format: "date" }),
|
|
3656
|
+
default: "2026-01-01"
|
|
3657
|
+
});
|
|
3658
|
+
|
|
3659
|
+
class CreatedAt2 extends dateTimeOf({
|
|
3660
|
+
name: "createdAt",
|
|
3661
|
+
default: DEMO_DATE_TIME
|
|
3662
|
+
}) {
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
class UpdatedAt2 extends dateTimeOf({
|
|
3666
|
+
name: "updatedAt",
|
|
3667
|
+
optional: true,
|
|
3668
|
+
default: DEMO_DATE_TIME
|
|
3669
|
+
}) {
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
class DeletedAt2 extends dateTimeOf({
|
|
3673
|
+
name: "deletedAt",
|
|
3674
|
+
optional: true
|
|
3675
|
+
}) {
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
class ExpiresAt2 extends dateTimeOf({
|
|
3679
|
+
name: "expiresAt",
|
|
3680
|
+
default: DEMO_DATE_TIME
|
|
3681
|
+
}) {
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
// src/std/blueprint-plugin.ts
|
|
3685
|
+
var Std2 = blueprintPluginOf2("std", {
|
|
3686
|
+
id: Id2,
|
|
3687
|
+
slug: Slug2,
|
|
3688
|
+
email: Email2,
|
|
3689
|
+
phone: Phone2,
|
|
3690
|
+
url: Url2,
|
|
3691
|
+
ip: Ip2,
|
|
3692
|
+
password: Password2,
|
|
3693
|
+
string: StringProperty2,
|
|
3694
|
+
text: Text2,
|
|
3695
|
+
number: NumberProperty2,
|
|
3696
|
+
integer: Integer2,
|
|
3697
|
+
boolean: BooleanProperty2,
|
|
3698
|
+
date: DateProperty2,
|
|
3699
|
+
dateTime: DateTime2,
|
|
3700
|
+
createdAt: CreatedAt2,
|
|
3701
|
+
updatedAt: UpdatedAt2,
|
|
3702
|
+
deletedAt: DeletedAt2,
|
|
3703
|
+
schema: SchemaProperty2,
|
|
3704
|
+
json: JsonProperty2
|
|
3705
|
+
}, { enumOf: enumOf2, jsonOf: jsonOf2 });
|
|
3706
|
+
|
|
3707
|
+
// src/std/blueprint.ts
|
|
3708
|
+
var blueprintOf2 = blueprintBuilderOf2(Std2);
|
|
3709
|
+
// src/std/use-cases/shared.ts
|
|
3710
|
+
var fields = blueprintBuilderOf2();
|
|
3711
|
+
var IDENTIFIER3 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3712
|
+
var capitalize = (key) => `${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
3713
|
+
function literal(value) {
|
|
3714
|
+
if (typeof value === "string") {
|
|
3715
|
+
return value.startsWith("$") ? { literal: value } : value;
|
|
3716
|
+
}
|
|
3717
|
+
if (Array.isArray(value))
|
|
3718
|
+
return value.map(literal);
|
|
3719
|
+
if (isPlain(value)) {
|
|
3720
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, literal(item)]));
|
|
3721
|
+
}
|
|
3722
|
+
return value;
|
|
3723
|
+
}
|
|
3724
|
+
function checkOptions(target, options, allowed) {
|
|
3725
|
+
const known = [...COMMON, ...allowed];
|
|
3726
|
+
for (const key of Object.keys(options)) {
|
|
3727
|
+
if (!known.includes(key)) {
|
|
3728
|
+
target.unavailable(`it has no option "${key}" (options: ${known.join(", ")})`);
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
var COMMON = [
|
|
3733
|
+
"name",
|
|
3734
|
+
"description",
|
|
3735
|
+
"tags",
|
|
3736
|
+
"deprecated",
|
|
3737
|
+
"errors",
|
|
3738
|
+
"route",
|
|
3739
|
+
"access"
|
|
3740
|
+
];
|
|
3741
|
+
var HOOKS = ["before", "after", "dependencies", "throws"];
|
|
3742
|
+
function metaOf(target, options, verb) {
|
|
3743
|
+
const { name, description, tags, deprecated, route, access } = options;
|
|
3744
|
+
if (name !== undefined && (typeof name !== "string" || name === "")) {
|
|
3745
|
+
target.unavailable("the option name must be a non-empty string");
|
|
3746
|
+
}
|
|
3747
|
+
return {
|
|
3748
|
+
name: name ?? `${verb}${target.entityName}`,
|
|
3749
|
+
...description === undefined ? {} : { description },
|
|
3750
|
+
...tags === undefined ? {} : { tags },
|
|
3751
|
+
...deprecated === undefined ? {} : { deprecated },
|
|
3752
|
+
...route === undefined ? {} : { route },
|
|
3753
|
+
...access === undefined ? {} : { access }
|
|
3754
|
+
};
|
|
3755
|
+
}
|
|
3756
|
+
function asOf(target, options) {
|
|
3757
|
+
const { as = "item" } = options;
|
|
3758
|
+
if (typeof as !== "string" || !IDENTIFIER3.test(as)) {
|
|
3759
|
+
target.unavailable("the option as must be an identifier");
|
|
3760
|
+
}
|
|
3761
|
+
return as;
|
|
3762
|
+
}
|
|
3763
|
+
function errorOf(target, options, kind, fallback) {
|
|
3764
|
+
const { errors } = options;
|
|
3765
|
+
if (errors === undefined)
|
|
3766
|
+
return fallback;
|
|
3767
|
+
if (!isPlain(errors)) {
|
|
3768
|
+
return target.unavailable("the option errors must be an object of exception classes");
|
|
3769
|
+
}
|
|
3770
|
+
const given = errors[kind];
|
|
3771
|
+
if (given === undefined)
|
|
3772
|
+
return fallback;
|
|
3773
|
+
if (!isDeclaredException(given)) {
|
|
3774
|
+
return target.unavailable(`errors.${kind} must be a class made by exceptionOf`);
|
|
3775
|
+
}
|
|
3776
|
+
return given;
|
|
3777
|
+
}
|
|
3778
|
+
function keysOf(target, options, option) {
|
|
3779
|
+
const given = options[option];
|
|
3780
|
+
if (given === undefined)
|
|
3781
|
+
return [];
|
|
3782
|
+
if (!Array.isArray(given) || given.some((key) => typeof key !== "string")) {
|
|
3783
|
+
return target.unavailable(`the option ${option} must be a list of keys`);
|
|
3784
|
+
}
|
|
3785
|
+
return given.map((key) => target.field(key));
|
|
3786
|
+
}
|
|
3787
|
+
function valuesOf(target, options, option) {
|
|
3788
|
+
const given = options[option];
|
|
3789
|
+
if (given === undefined)
|
|
3790
|
+
return {};
|
|
3791
|
+
if (!isPlain(given)) {
|
|
3792
|
+
return target.unavailable(`the option ${option} must be an object`);
|
|
3793
|
+
}
|
|
3794
|
+
for (const key of Object.keys(given)) {
|
|
3795
|
+
if (target.field(key).identity) {
|
|
3796
|
+
target.unavailable(`${option}.${key} is an identity key`);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
return given;
|
|
3800
|
+
}
|
|
3801
|
+
function ownerOf(target, options) {
|
|
3802
|
+
const { owner } = options;
|
|
3803
|
+
if (owner === undefined)
|
|
3804
|
+
return;
|
|
3805
|
+
if (!isPlain(owner) || typeof owner.context !== "string" || !IDENTIFIER3.test(owner.context) || owner.key !== undefined && typeof owner.key !== "string") {
|
|
3806
|
+
return target.unavailable("the option owner must be { key?, context }, context naming a context key");
|
|
3807
|
+
}
|
|
3808
|
+
const plan = target.field(owner.key ?? "ownerId");
|
|
3809
|
+
if (plan.identity || plan.optional) {
|
|
3810
|
+
target.unavailable(`owner.key "${plan.key}" must be a required key`);
|
|
3811
|
+
}
|
|
3812
|
+
if (plan.kind === "entity" && plan.related) {
|
|
3813
|
+
return {
|
|
3814
|
+
plan,
|
|
3815
|
+
context: owner.context,
|
|
3816
|
+
field: plan.related.blueprint.properties.id,
|
|
3817
|
+
reference: `$item.${plan.key}.id`
|
|
3818
|
+
};
|
|
3819
|
+
}
|
|
3820
|
+
if (plan.kind !== "property") {
|
|
3821
|
+
target.unavailable(`owner.key "${plan.key}" must hold an id or an entity`);
|
|
3822
|
+
}
|
|
3823
|
+
return {
|
|
3824
|
+
plan,
|
|
3825
|
+
context: owner.context,
|
|
3826
|
+
field: plan.field,
|
|
3827
|
+
reference: `$item.${plan.key}`
|
|
3828
|
+
};
|
|
3829
|
+
}
|
|
3830
|
+
function contextOf2(owner) {
|
|
3831
|
+
return owner ? { context: fields(() => ({ [owner.context]: owner.field })) } : {};
|
|
3832
|
+
}
|
|
3833
|
+
function loadSteps(owner, notFound) {
|
|
3834
|
+
return [
|
|
3835
|
+
{ find: "repository.findById", args: ["$input.id"], as: "item" },
|
|
3836
|
+
{ ensure: "exists", value: "$item", error: notFound },
|
|
3837
|
+
...owner ? [
|
|
3838
|
+
{
|
|
3839
|
+
ensure: "equals",
|
|
3840
|
+
value: owner.reference,
|
|
3841
|
+
other: `$context.${owner.context}`,
|
|
3842
|
+
error: notFound
|
|
3843
|
+
}
|
|
3844
|
+
] : []
|
|
3845
|
+
];
|
|
3846
|
+
}
|
|
3847
|
+
function hooksOf(target, options) {
|
|
3848
|
+
const { before, after, dependencies = {}, throws = [] } = options;
|
|
3849
|
+
for (const [key, hook] of [
|
|
3850
|
+
["before", before],
|
|
3851
|
+
["after", after]
|
|
3852
|
+
]) {
|
|
3853
|
+
if (hook !== undefined && typeof hook !== "function") {
|
|
3854
|
+
target.unavailable(`the option ${key} must be a function`);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
if (!isPlain(dependencies) || Object.keys(dependencies).some((key) => !IDENTIFIER3.test(key))) {
|
|
3858
|
+
target.unavailable("the option dependencies must be an object of repositories, use cases or environments by name");
|
|
3859
|
+
}
|
|
3860
|
+
if (!Array.isArray(throws) || throws.some((error) => !isDeclaredException(error))) {
|
|
3861
|
+
target.unavailable("the option throws must be a list of classes made by exceptionOf");
|
|
3862
|
+
}
|
|
3863
|
+
const input = before === undefined ? "$input" : "$prepared";
|
|
3864
|
+
return {
|
|
3865
|
+
input,
|
|
3866
|
+
dependencies,
|
|
3867
|
+
throws,
|
|
3868
|
+
before: (entity) => before === undefined ? [] : [
|
|
3869
|
+
{
|
|
3870
|
+
run: "before",
|
|
3871
|
+
fn: async (given, ...rest) => {
|
|
3872
|
+
const { context, dependencies } = rest.at(-1);
|
|
3873
|
+
const replaced = await before({
|
|
3874
|
+
input: given,
|
|
3875
|
+
...entity === undefined ? {} : { entity: rest[0] },
|
|
3876
|
+
dependencies,
|
|
3877
|
+
context
|
|
3878
|
+
});
|
|
3879
|
+
return replaced === undefined ? given : replaced;
|
|
3880
|
+
},
|
|
3881
|
+
args: entity === undefined ? ["$input"] : ["$input", entity],
|
|
3882
|
+
as: "prepared"
|
|
3883
|
+
}
|
|
3884
|
+
],
|
|
3885
|
+
after: (entity) => after === undefined ? [] : [
|
|
3886
|
+
{
|
|
3887
|
+
run: "after",
|
|
3888
|
+
fn: async (written, given, { context, dependencies }) => {
|
|
3889
|
+
await after({
|
|
3890
|
+
entity: written,
|
|
3891
|
+
input: given,
|
|
3892
|
+
dependencies,
|
|
3893
|
+
context
|
|
3894
|
+
});
|
|
3895
|
+
},
|
|
3896
|
+
args: [entity, input]
|
|
3897
|
+
}
|
|
3898
|
+
]
|
|
3899
|
+
};
|
|
3900
|
+
}
|
|
3901
|
+
function joinDependencies(target, made, hooks) {
|
|
3902
|
+
for (const key of Object.keys(hooks.dependencies)) {
|
|
3903
|
+
if (Object.hasOwn(made, key)) {
|
|
3904
|
+
target.unavailable(`dependencies.${key} is a name the use case gives its own dependency`);
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
return { ...made, ...hooks.dependencies };
|
|
3908
|
+
}
|
|
3909
|
+
var idOf = (target) => target.field("id").field;
|
|
3910
|
+
var distinct = (...errors) => [...new Set(errors)];
|
|
3911
|
+
function entityUseCase(target, options, verb, allowed, steps, hooks = false) {
|
|
3912
|
+
checkOptions(target, options, [
|
|
3913
|
+
"as",
|
|
3914
|
+
"owner",
|
|
3915
|
+
...allowed,
|
|
3916
|
+
...hooks ? HOOKS : []
|
|
3917
|
+
]);
|
|
3918
|
+
const owner = ownerOf(target, options);
|
|
3919
|
+
const notFound = errorOf(target, options, "notFound", UseCaseEntityNotFound2);
|
|
3920
|
+
const hooked = hooksOf(target, options);
|
|
3921
|
+
const body = steps({ owner, input: hooked.input });
|
|
3922
|
+
return {
|
|
3923
|
+
...metaOf(target, options, verb),
|
|
3924
|
+
blueprint: fields(() => ({ id: idOf(target) })),
|
|
3925
|
+
response: fields(() => ({ [asOf(target, options)]: target.entity })),
|
|
3926
|
+
...contextOf2(owner),
|
|
3927
|
+
dependencies: joinDependencies(target, { repository: target.repository }, hooked),
|
|
3928
|
+
errors: distinct(notFound, ...hooked.throws),
|
|
3929
|
+
steps: [
|
|
3930
|
+
...loadSteps(owner, notFound),
|
|
3931
|
+
...hooked.before("$item"),
|
|
3932
|
+
...body.steps,
|
|
3933
|
+
...hooked.after(body.result),
|
|
3934
|
+
{ return: { [asOf(target, options)]: body.result } }
|
|
3935
|
+
]
|
|
3936
|
+
};
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
// src/std/use-cases/crud.ts
|
|
3940
|
+
function createCompilation(target, options, verb, checks = () => [], errors = []) {
|
|
3941
|
+
checkOptions(target, options, ["as", "owner", "omit", "values", ...HOOKS]);
|
|
3942
|
+
const owner = ownerOf(target, options);
|
|
3943
|
+
const hooks = hooksOf(target, options);
|
|
3944
|
+
const omitted = keysOf(target, options, "omit");
|
|
3945
|
+
const constants = valuesOf(target, options, "values");
|
|
3946
|
+
const notFound = errorOf(target, options, "notFound", UseCaseEntityNotFound2);
|
|
3947
|
+
const ownerError = errorOf(target, options, "owner", notFound);
|
|
3948
|
+
const input = {};
|
|
3949
|
+
const values = {};
|
|
3950
|
+
const dependencies = {
|
|
3951
|
+
repository: target.repository
|
|
3952
|
+
};
|
|
3953
|
+
const steps = [];
|
|
3954
|
+
const used = new Set([...errors, ...hooks.throws]);
|
|
3955
|
+
const take = (key, field) => {
|
|
3956
|
+
if (Object.hasOwn(input, key)) {
|
|
3957
|
+
target.unavailable(`two keys of the entity take the input "${key}"`);
|
|
3958
|
+
}
|
|
3959
|
+
input[key] = field;
|
|
3960
|
+
};
|
|
3961
|
+
const load = (key, reference, error) => {
|
|
3962
|
+
const dependency = `${key}Repository`;
|
|
3963
|
+
dependencies[dependency] = target.relation(key, ["findById"]);
|
|
3964
|
+
used.add(error);
|
|
3965
|
+
steps.push({
|
|
3966
|
+
find: `${dependency}.findById`,
|
|
3967
|
+
args: [reference],
|
|
3968
|
+
as: `${key}Found`
|
|
3969
|
+
}, { ensure: "exists", value: `$${key}Found`, error });
|
|
3970
|
+
return `$${key}Found`;
|
|
3971
|
+
};
|
|
3972
|
+
for (const plan of target.fields) {
|
|
3973
|
+
const { key } = plan;
|
|
3974
|
+
if (plan.identity)
|
|
3975
|
+
continue;
|
|
3976
|
+
if (Object.hasOwn(constants, key)) {
|
|
3977
|
+
values[key] = literal(constants[key]);
|
|
3978
|
+
continue;
|
|
3979
|
+
}
|
|
3980
|
+
if (owner?.plan.key === key) {
|
|
3981
|
+
values[key] = plan.kind === "entity" ? load(key, `$context.${owner.context}`, ownerError) : `$context.${owner.context}`;
|
|
3982
|
+
continue;
|
|
3983
|
+
}
|
|
3984
|
+
const unloadable = plan.kind === "entity-list" || plan.kind === "entity" && plan.optional;
|
|
3985
|
+
if (omitted.includes(plan) || unloadable) {
|
|
3986
|
+
if (!plan.optional) {
|
|
3987
|
+
target.unavailable(unloadable ? `"${key}" holds a list of entities, which the input cannot take; give it in values` : `"${key}" is required and cannot be omitted`);
|
|
3988
|
+
}
|
|
3989
|
+
continue;
|
|
3990
|
+
}
|
|
3991
|
+
take(plan.input.key, plan.input.field);
|
|
3992
|
+
const given = `${hooks.input}.${plan.input.key}`;
|
|
3993
|
+
if (plan.kind === "entity") {
|
|
3994
|
+
values[key] = load(key, given, notFound);
|
|
3995
|
+
} else if (plan.convert) {
|
|
3996
|
+
steps.push({
|
|
3997
|
+
run: `convert${capitalize(key)}`,
|
|
3998
|
+
fn: plan.convert,
|
|
3999
|
+
args: [given],
|
|
4000
|
+
as: `${key}Converted`
|
|
4001
|
+
});
|
|
4002
|
+
values[key] = `$${key}Converted`;
|
|
4003
|
+
} else {
|
|
4004
|
+
values[key] = given;
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
return {
|
|
4008
|
+
...metaOf(target, options, verb),
|
|
4009
|
+
blueprint: fields(() => input),
|
|
4010
|
+
response: fields(() => ({ [asOf(target, options)]: target.entity })),
|
|
4011
|
+
...contextOf2(owner),
|
|
4012
|
+
dependencies: joinDependencies(target, dependencies, hooks),
|
|
4013
|
+
errors: distinct(...used),
|
|
4014
|
+
steps: [
|
|
4015
|
+
...hooks.before(),
|
|
4016
|
+
...checks(hooks.input),
|
|
4017
|
+
...steps,
|
|
4018
|
+
{ build: target.entity, values, as: "built" },
|
|
4019
|
+
{ create: "repository", value: "$built", as: "item" },
|
|
4020
|
+
...hooks.after("$item"),
|
|
4021
|
+
{ return: { [asOf(target, options)]: "$item" } }
|
|
4022
|
+
]
|
|
4023
|
+
};
|
|
4024
|
+
}
|
|
4025
|
+
var create = useCasePluginOf2("create", {
|
|
4026
|
+
description: "Builds an entity from the input (its keys, less the identity; an entity it holds by id) and stores it.",
|
|
4027
|
+
requires: { actions: ["create"] },
|
|
4028
|
+
compile: (target, options) => createCompilation(target, options, "Create")
|
|
4029
|
+
});
|
|
4030
|
+
var get = useCasePluginOf2("get", {
|
|
4031
|
+
description: "Gives the entity of an id.",
|
|
4032
|
+
requires: { actions: ["findById"] },
|
|
4033
|
+
compile: (target, options) => entityUseCase(target, options, "Get", [], () => ({
|
|
4034
|
+
steps: [],
|
|
4035
|
+
result: "$item"
|
|
4036
|
+
}))
|
|
4037
|
+
});
|
|
4038
|
+
var list = useCasePluginOf2("list", {
|
|
4039
|
+
description: "Gives a page of entities: { items, total, limit, offset }.",
|
|
4040
|
+
requires: { actions: ["findPage"] },
|
|
4041
|
+
compile: (target, options) => {
|
|
4042
|
+
checkOptions(target, options, ["owner", "orderBy"]);
|
|
4043
|
+
const owner = ownerOf(target, options);
|
|
4044
|
+
const { orderBy = [{ key: "createdAt", direction: "desc" }] } = options;
|
|
4045
|
+
if (!Array.isArray(orderBy)) {
|
|
4046
|
+
target.unavailable("the option orderBy must be a list of orders");
|
|
4047
|
+
}
|
|
4048
|
+
for (const order of orderBy) {
|
|
4049
|
+
target.field(String(order?.key));
|
|
4050
|
+
}
|
|
4051
|
+
return {
|
|
4052
|
+
...metaOf(target, options, "List"),
|
|
4053
|
+
blueprint: fields(() => ({
|
|
4054
|
+
limit: optionalOf2(Integer2),
|
|
4055
|
+
offset: optionalOf2(Integer2)
|
|
4056
|
+
})),
|
|
4057
|
+
response: fields(() => ({
|
|
4058
|
+
items: listOf2(target.entity),
|
|
4059
|
+
total: Integer2,
|
|
4060
|
+
limit: Integer2,
|
|
4061
|
+
offset: Integer2
|
|
4062
|
+
})),
|
|
4063
|
+
...contextOf2(owner),
|
|
4064
|
+
dependencies: { repository: target.repository },
|
|
4065
|
+
steps: [
|
|
4066
|
+
{
|
|
4067
|
+
find: "repository.findPage",
|
|
4068
|
+
args: [
|
|
4069
|
+
{
|
|
4070
|
+
...owner ? {
|
|
4071
|
+
where: {
|
|
4072
|
+
[owner.plan.key]: `$context.${owner.context}`
|
|
4073
|
+
}
|
|
4074
|
+
} : {},
|
|
4075
|
+
orderBy: literal(orderBy),
|
|
4076
|
+
limit: "$input.limit",
|
|
4077
|
+
offset: "$input.offset"
|
|
4078
|
+
}
|
|
4079
|
+
],
|
|
4080
|
+
as: "page"
|
|
4081
|
+
},
|
|
4082
|
+
{
|
|
4083
|
+
return: {
|
|
4084
|
+
items: "$page.items",
|
|
4085
|
+
total: "$page.total",
|
|
4086
|
+
limit: "$page.limit",
|
|
4087
|
+
offset: "$page.offset"
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
]
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
4093
|
+
});
|
|
4094
|
+
var update = useCasePluginOf2("update", {
|
|
4095
|
+
description: "Changes the change keys the input gives, leaving the others as they are, and stores the entity.",
|
|
4096
|
+
requires: { actions: ["findById", "update"] },
|
|
4097
|
+
compile: (target, options) => {
|
|
4098
|
+
checkOptions(target, options, ["as", "owner", ...HOOKS]);
|
|
4099
|
+
const owner = ownerOf(target, options);
|
|
4100
|
+
const hooks = hooksOf(target, options);
|
|
4101
|
+
const notFound = errorOf(target, options, "notFound", UseCaseEntityNotFound2);
|
|
4102
|
+
const plans = target.changeKeys.map((key) => target.field(key)).filter((plan) => plan.kind !== "entity" && plan.kind !== "entity-list");
|
|
4103
|
+
if (plans.length === 0) {
|
|
4104
|
+
target.unavailable("the entity has no change key an input can take");
|
|
4105
|
+
}
|
|
4106
|
+
if (plans.some((plan) => plan.key === "id")) {
|
|
4107
|
+
target.unavailable('"id" cannot be a change key');
|
|
4108
|
+
}
|
|
4109
|
+
const steps = [];
|
|
4110
|
+
for (const plan of plans) {
|
|
4111
|
+
const given = `${hooks.input}.${plan.input.key}`;
|
|
4112
|
+
let value = given;
|
|
4113
|
+
if (plan.convert) {
|
|
4114
|
+
steps.push({
|
|
4115
|
+
run: `convert${capitalize(plan.key)}`,
|
|
4116
|
+
fn: plan.convert,
|
|
4117
|
+
args: [given],
|
|
4118
|
+
as: `${plan.key}Converted`
|
|
4119
|
+
});
|
|
4120
|
+
value = `$${plan.key}Converted`;
|
|
4121
|
+
}
|
|
4122
|
+
steps.push({ change: "$item", key: plan.key, value, ifPresent: true });
|
|
4123
|
+
}
|
|
4124
|
+
return {
|
|
4125
|
+
...metaOf(target, options, "Update"),
|
|
4126
|
+
blueprint: fields(() => ({
|
|
4127
|
+
id: idOf(target),
|
|
4128
|
+
...Object.fromEntries(plans.map((plan) => [plan.input.key, optionalOf2(plan.input.field)]))
|
|
4129
|
+
})),
|
|
4130
|
+
response: fields(() => ({ [asOf(target, options)]: target.entity })),
|
|
4131
|
+
...contextOf2(owner),
|
|
4132
|
+
dependencies: joinDependencies(target, { repository: target.repository }, hooks),
|
|
4133
|
+
errors: distinct(notFound, ...hooks.throws),
|
|
4134
|
+
steps: [
|
|
4135
|
+
...loadSteps(owner, notFound),
|
|
4136
|
+
...hooks.before("$item"),
|
|
4137
|
+
...steps,
|
|
4138
|
+
{ update: "repository", value: "$item", as: "saved" },
|
|
4139
|
+
...hooks.after("$saved"),
|
|
4140
|
+
{ return: { [asOf(target, options)]: "$saved" } }
|
|
4141
|
+
]
|
|
4142
|
+
};
|
|
4143
|
+
}
|
|
4144
|
+
});
|
|
4145
|
+
var remove = useCasePluginOf2("delete", {
|
|
4146
|
+
description: "Deletes the entity of an id, hard or soft as the repository declares, and gives it.",
|
|
4147
|
+
requires: { actions: ["findById", "delete"] },
|
|
4148
|
+
compile: (target, options) => entityUseCase(target, options, "Delete", [], () => ({
|
|
4149
|
+
steps: [{ delete: "repository", value: "$item", as: "deleted" }],
|
|
4150
|
+
result: "$deleted"
|
|
4151
|
+
}), true)
|
|
4152
|
+
});
|
|
4153
|
+
var restore = useCasePluginOf2("restore", {
|
|
4154
|
+
description: "Brings back the soft-deleted entity of an id; for a repository with soft deletion and the restore action.",
|
|
4155
|
+
requires: { actions: ["restore"] },
|
|
4156
|
+
compile: (target, options) => {
|
|
4157
|
+
checkOptions(target, options, ["as", ...HOOKS]);
|
|
4158
|
+
const hooks = hooksOf(target, options);
|
|
4159
|
+
return {
|
|
4160
|
+
...metaOf(target, options, "Restore"),
|
|
4161
|
+
blueprint: fields(() => ({ id: idOf(target) })),
|
|
4162
|
+
response: fields(() => ({ [asOf(target, options)]: target.entity })),
|
|
4163
|
+
dependencies: joinDependencies(target, { repository: target.repository }, hooks),
|
|
4164
|
+
errors: distinct(...hooks.throws),
|
|
4165
|
+
steps: [
|
|
4166
|
+
...hooks.before(),
|
|
4167
|
+
{ restore: "repository", value: `${hooks.input}.id`, as: "item" },
|
|
4168
|
+
...hooks.after("$item"),
|
|
4169
|
+
{ return: { [asOf(target, options)]: "$item" } }
|
|
4170
|
+
]
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
4173
|
+
});
|
|
4174
|
+
|
|
4175
|
+
// src/std/use-cases/auth.ts
|
|
4176
|
+
var TOKEN_KEYS = ["refresh", "scopes", "scopesFrom", "claims"];
|
|
4177
|
+
var isTexts = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
4178
|
+
function tokensOf(target, options) {
|
|
4179
|
+
const given = options.token;
|
|
4180
|
+
if (given === undefined)
|
|
4181
|
+
return;
|
|
4182
|
+
const bad = jwtOptionsIssue(given, "the option token", TOKEN_KEYS, true);
|
|
4183
|
+
if (bad)
|
|
4184
|
+
return target.unavailable(bad);
|
|
4185
|
+
const { refresh, scopes, scopesFrom, claims, ...jwtOptions } = given;
|
|
4186
|
+
if (refresh !== undefined && (typeof refresh !== "object" || refresh === null || Object.keys(refresh).join() !== "expiresIn" || !Number.isInteger(refresh.expiresIn) || refresh.expiresIn <= 0)) {
|
|
4187
|
+
return target.unavailable("the option token.refresh must be { expiresIn }, a whole number of seconds above 0");
|
|
4188
|
+
}
|
|
4189
|
+
if (scopes !== undefined && !isTexts(scopes)) {
|
|
4190
|
+
return target.unavailable("the option token.scopes must be a list of scopes");
|
|
4191
|
+
}
|
|
4192
|
+
if (scopesFrom !== undefined)
|
|
4193
|
+
target.field(scopesFrom);
|
|
4194
|
+
if (claims !== undefined && (typeof claims !== "object" || claims === null || Array.isArray(claims))) {
|
|
4195
|
+
return target.unavailable("the option token.claims must map claims to keys of the entity");
|
|
4196
|
+
}
|
|
4197
|
+
const claimKeys = Object.entries(claims ?? {});
|
|
4198
|
+
for (const [, key] of claimKeys)
|
|
4199
|
+
target.field(key);
|
|
4200
|
+
const jwt = jwtOf2(jwtOptions);
|
|
4201
|
+
const issue = async (entity) => {
|
|
4202
|
+
const held = scopesFrom === undefined ? [] : entity[scopesFrom];
|
|
4203
|
+
const subject = {
|
|
4204
|
+
subject: String(entity.id),
|
|
4205
|
+
scopes: [...new Set([...scopes ?? [], ...isTexts(held) ? held : []])],
|
|
4206
|
+
...claimKeys.length > 0 ? {
|
|
4207
|
+
claims: Object.fromEntries(claimKeys.map(([claim, key]) => [
|
|
4208
|
+
claim,
|
|
4209
|
+
outputValueOf2(entity[key])
|
|
4210
|
+
]))
|
|
4211
|
+
} : {}
|
|
4212
|
+
};
|
|
4213
|
+
return {
|
|
4214
|
+
token: await jwt.sign(subject),
|
|
4215
|
+
...refresh ? {
|
|
4216
|
+
refreshToken: await jwt.sign({ subject: subject.subject }, { expiresIn: refresh.expiresIn, use: "refresh" })
|
|
4217
|
+
} : {}
|
|
4218
|
+
};
|
|
4219
|
+
};
|
|
4220
|
+
const verifyRefresh = (token) => jwt.verify(token, { use: "refresh" });
|
|
4221
|
+
return { issue, verifyRefresh, refresh: refresh !== undefined };
|
|
4222
|
+
}
|
|
4223
|
+
var authenticatable = (actions) => ({
|
|
4224
|
+
requires: { actions, plugins: ["authenticatable"] }
|
|
4225
|
+
});
|
|
4226
|
+
var register = useCasePluginOf2("register", {
|
|
4227
|
+
description: "Stores an authenticatable entity built from the input, its password given plain and stored as a hash, unless the email is taken.",
|
|
4228
|
+
...authenticatable(["findByEmail", "create"]),
|
|
4229
|
+
compile: (target, options) => {
|
|
4230
|
+
const conflict = errorOf(target, options, "conflict", UseCaseEntityConflict2);
|
|
4231
|
+
return createCompilation(target, options, "Register", (input) => [
|
|
4232
|
+
{
|
|
4233
|
+
find: "repository.findByEmail",
|
|
4234
|
+
args: [`${input}.email`],
|
|
4235
|
+
as: "existing"
|
|
4236
|
+
},
|
|
4237
|
+
{ ensure: "absent", value: "$existing", error: conflict }
|
|
4238
|
+
], [conflict]);
|
|
4239
|
+
}
|
|
4240
|
+
});
|
|
4241
|
+
var login = useCasePluginOf2("login", {
|
|
4242
|
+
description: "Gives the authenticatable entity of an email when the password matches, with a token (and a refresh token) for it when the option token is given.",
|
|
4243
|
+
...authenticatable(["findByEmail"]),
|
|
4244
|
+
compile: (target, options) => {
|
|
4245
|
+
checkOptions(target, options, ["as", "token"]);
|
|
4246
|
+
const tokens = tokensOf(target, options);
|
|
4247
|
+
const invalid = errorOf(target, options, "invalid", UseCaseInvalidCredentials2);
|
|
4248
|
+
return {
|
|
4249
|
+
...metaOf(target, options, "Login"),
|
|
4250
|
+
blueprint: fields(() => ({
|
|
4251
|
+
email: target.field("email").field,
|
|
4252
|
+
password: StringProperty2
|
|
4253
|
+
})),
|
|
4254
|
+
response: fields(() => ({
|
|
4255
|
+
[asOf(target, options)]: target.entity,
|
|
4256
|
+
...tokens ? { token: StringProperty2 } : {},
|
|
4257
|
+
...tokens?.refresh ? { refreshToken: StringProperty2 } : {}
|
|
4258
|
+
})),
|
|
4259
|
+
dependencies: { repository: target.repository },
|
|
4260
|
+
errors: [invalid],
|
|
4261
|
+
steps: [
|
|
4262
|
+
{ find: "repository.findByEmail", args: ["$input.email"], as: "item" },
|
|
4263
|
+
{ ensure: "exists", value: "$item", error: invalid },
|
|
4264
|
+
{
|
|
4265
|
+
invoke: "$item",
|
|
4266
|
+
method: "verifyPassword",
|
|
4267
|
+
args: ["$input.password"],
|
|
4268
|
+
as: "matches"
|
|
4269
|
+
},
|
|
4270
|
+
{ ensure: "true", value: "$matches", error: invalid },
|
|
4271
|
+
...tokens ? [
|
|
4272
|
+
{
|
|
4273
|
+
run: "issueTokens",
|
|
4274
|
+
fn: tokens.issue,
|
|
4275
|
+
args: ["$item"],
|
|
4276
|
+
as: "tokens"
|
|
4277
|
+
}
|
|
4278
|
+
] : [],
|
|
4279
|
+
{
|
|
4280
|
+
return: {
|
|
4281
|
+
[asOf(target, options)]: "$item",
|
|
4282
|
+
...tokens ? { token: "$tokens.token" } : {},
|
|
4283
|
+
...tokens?.refresh ? { refreshToken: "$tokens.refreshToken" } : {}
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
]
|
|
4287
|
+
};
|
|
4288
|
+
}
|
|
4289
|
+
});
|
|
4290
|
+
var refresh = useCasePluginOf2("refresh", {
|
|
4291
|
+
description: "Exchanges a refresh token issued by login for a new token and a new refresh token, for the entity it names, if it still exists. Experimental.",
|
|
4292
|
+
requires: { actions: ["findById"] },
|
|
4293
|
+
compile: (target, options) => {
|
|
4294
|
+
checkOptions(target, options, ["token"]);
|
|
4295
|
+
const tokens = tokensOf(target, options);
|
|
4296
|
+
if (!tokens?.refresh) {
|
|
4297
|
+
return target.unavailable("it needs the option token with refresh, the same login was given");
|
|
4298
|
+
}
|
|
4299
|
+
const invalid = errorOf(target, options, "invalid", UseCaseInvalidCredentials2);
|
|
4300
|
+
return {
|
|
4301
|
+
...metaOf(target, options, "Refresh"),
|
|
4302
|
+
blueprint: fields(() => ({ refreshToken: StringProperty2 })),
|
|
4303
|
+
response: fields(() => ({
|
|
4304
|
+
token: StringProperty2,
|
|
4305
|
+
refreshToken: StringProperty2
|
|
4306
|
+
})),
|
|
4307
|
+
dependencies: { repository: target.repository },
|
|
4308
|
+
errors: [invalid],
|
|
4309
|
+
steps: [
|
|
4310
|
+
{
|
|
4311
|
+
run: "verifyRefreshToken",
|
|
4312
|
+
fn: tokens.verifyRefresh,
|
|
4313
|
+
args: ["$input.refreshToken"],
|
|
4314
|
+
as: "caller"
|
|
4315
|
+
},
|
|
4316
|
+
{
|
|
4317
|
+
find: "repository.findById",
|
|
4318
|
+
args: ["$caller.subject"],
|
|
4319
|
+
as: "item"
|
|
4320
|
+
},
|
|
4321
|
+
{ ensure: "exists", value: "$item", error: invalid },
|
|
4322
|
+
{
|
|
4323
|
+
run: "issueTokens",
|
|
4324
|
+
fn: tokens.issue,
|
|
4325
|
+
args: ["$item"],
|
|
4326
|
+
as: "tokens"
|
|
4327
|
+
},
|
|
4328
|
+
{
|
|
4329
|
+
return: {
|
|
4330
|
+
token: "$tokens.token",
|
|
4331
|
+
refreshToken: "$tokens.refreshToken"
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
]
|
|
4335
|
+
};
|
|
4336
|
+
}
|
|
4337
|
+
});
|
|
4338
|
+
var logout = useCasePluginOf2("logout", {
|
|
4339
|
+
description: "Gives an empty token and refresh token: a route whose setCookie sends them removes the cookies. Tokens are not stored, so one already issued stays valid until it expires. Experimental.",
|
|
4340
|
+
compile: (target, options) => {
|
|
4341
|
+
checkOptions(target, options, []);
|
|
4342
|
+
return {
|
|
4343
|
+
...metaOf(target, options, "Logout"),
|
|
4344
|
+
blueprint: fields(() => ({})),
|
|
4345
|
+
response: fields(() => ({
|
|
4346
|
+
token: StringProperty2,
|
|
4347
|
+
refreshToken: StringProperty2
|
|
4348
|
+
})),
|
|
4349
|
+
steps: [{ return: { token: "", refreshToken: "" } }]
|
|
4350
|
+
};
|
|
4351
|
+
}
|
|
4352
|
+
});
|
|
4353
|
+
|
|
4354
|
+
// src/std/use-cases/lifecycle.ts
|
|
4355
|
+
function invoking(target, options, verb, method) {
|
|
4356
|
+
const set = valuesOf(target, options, "set");
|
|
4357
|
+
const changes = Object.entries(set).map(([key, value]) => {
|
|
4358
|
+
if (!target.changeKeys.includes(key)) {
|
|
4359
|
+
target.unavailable(`set.${key} is not a change key of the entity`);
|
|
4360
|
+
}
|
|
4361
|
+
return { change: "$item", key, value: literal(value) };
|
|
4362
|
+
});
|
|
4363
|
+
return entityUseCase(target, options, verb, ["set"], () => ({
|
|
4364
|
+
steps: [
|
|
4365
|
+
...changes,
|
|
4366
|
+
{ invoke: "$item", method },
|
|
4367
|
+
{ update: "repository", value: "$item", as: "saved" }
|
|
4368
|
+
],
|
|
4369
|
+
result: "$saved"
|
|
4370
|
+
}), true);
|
|
4371
|
+
}
|
|
4372
|
+
var lifecycle = (plugin) => ({
|
|
4373
|
+
requires: { actions: ["findById", "update"], plugins: [plugin] }
|
|
4374
|
+
});
|
|
4375
|
+
var publish = useCasePluginOf2("publish", {
|
|
4376
|
+
description: "Publishes the publishable entity of an id and stores it.",
|
|
4377
|
+
...lifecycle("publishable"),
|
|
4378
|
+
compile: (target, options) => invoking(target, options, "Publish", "publish")
|
|
4379
|
+
});
|
|
4380
|
+
var unpublish = useCasePluginOf2("unpublish", {
|
|
4381
|
+
description: "Makes the publishable entity of an id a draft again and stores it.",
|
|
4382
|
+
...lifecycle("publishable"),
|
|
4383
|
+
compile: (target, options) => invoking(target, options, "Unpublish", "unpublish")
|
|
4384
|
+
});
|
|
4385
|
+
var archive = useCasePluginOf2("archive", {
|
|
4386
|
+
description: "Archives the archivable entity of an id and stores it.",
|
|
4387
|
+
...lifecycle("archivable"),
|
|
4388
|
+
compile: (target, options) => invoking(target, options, "Archive", "archive")
|
|
4389
|
+
});
|
|
4390
|
+
var unarchive = useCasePluginOf2("unarchive", {
|
|
4391
|
+
description: "Makes the archived entity of an id active again and stores it.",
|
|
4392
|
+
...lifecycle("archivable"),
|
|
4393
|
+
compile: (target, options) => invoking(target, options, "Unarchive", "restore")
|
|
4394
|
+
});
|
|
4395
|
+
function tagging(target, options, verb, method) {
|
|
4396
|
+
checkOptions(target, options, ["as", "owner", ...HOOKS]);
|
|
4397
|
+
const owner = ownerOf(target, options);
|
|
4398
|
+
const hooks = hooksOf(target, options);
|
|
4399
|
+
const notFound = errorOf(target, options, "notFound", UseCaseEntityNotFound2);
|
|
4400
|
+
const { item } = target.field("tags").field;
|
|
4401
|
+
return {
|
|
4402
|
+
...metaOf(target, options, verb),
|
|
4403
|
+
blueprint: fields(() => ({
|
|
4404
|
+
id: idOf(target),
|
|
4405
|
+
tags: listOf2(item, { minItems: 1 })
|
|
4406
|
+
})),
|
|
4407
|
+
response: fields(() => ({ [asOf(target, options)]: target.entity })),
|
|
4408
|
+
...contextOf2(owner),
|
|
4409
|
+
dependencies: joinDependencies(target, { repository: target.repository }, hooks),
|
|
4410
|
+
errors: distinct(notFound, ...hooks.throws),
|
|
4411
|
+
steps: [
|
|
4412
|
+
...loadSteps(owner, notFound),
|
|
4413
|
+
...hooks.before("$item"),
|
|
4414
|
+
{ invoke: "$item", method, args: `${hooks.input}.tags` },
|
|
4415
|
+
{ update: "repository", value: "$item", as: "saved" },
|
|
4416
|
+
...hooks.after("$saved"),
|
|
4417
|
+
{ return: { [asOf(target, options)]: "$saved" } }
|
|
4418
|
+
]
|
|
4419
|
+
};
|
|
4420
|
+
}
|
|
4421
|
+
var tag = useCasePluginOf2("tag", {
|
|
4422
|
+
description: "Adds tags to the taggable entity of an id and stores it.",
|
|
4423
|
+
...lifecycle("taggable"),
|
|
4424
|
+
compile: (target, options) => tagging(target, options, "Tag", "tag")
|
|
4425
|
+
});
|
|
4426
|
+
var untag = useCasePluginOf2("untag", {
|
|
4427
|
+
description: "Removes tags from the taggable entity of an id and stores it.",
|
|
4428
|
+
...lifecycle("taggable"),
|
|
4429
|
+
compile: (target, options) => tagging(target, options, "Untag", "untag")
|
|
4430
|
+
});
|
|
4431
|
+
|
|
4432
|
+
// src/std/use-cases/index.ts
|
|
4433
|
+
var StdUseCases2 = useCaseCatalogOf2(create, get, list, update, remove, restore, publish, unpublish, archive, unarchive, tag, untag, register, login, refresh, logout);
|
|
4434
|
+
|
|
4435
|
+
// src/std/command.ts
|
|
4436
|
+
var commandOf2 = commandBuilderOf2(StdUseCases2);
|
|
4437
|
+
// src/std/record-plugins.ts
|
|
4438
|
+
var now = () => new Date().toISOString();
|
|
4439
|
+
|
|
4440
|
+
class PublishableRecord extends recordOf3(blueprintOf2((ctx) => ({ publishedAt: ctx.optional.dateTime }))) {
|
|
4441
|
+
get isPublished() {
|
|
4442
|
+
return this.publishedAt !== undefined;
|
|
4443
|
+
}
|
|
4444
|
+
publish(at = now()) {
|
|
4445
|
+
return this.set("publishedAt", at);
|
|
4446
|
+
}
|
|
4447
|
+
unpublish() {
|
|
4448
|
+
return this.set("publishedAt", undefined);
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
var Publishable2 = recordPluginOf2("publishable", PublishableRecord, {
|
|
4452
|
+
description: "A publication date, with publish, unpublish and isPublished."
|
|
4453
|
+
});
|
|
4454
|
+
|
|
4455
|
+
class ArchivableRecord extends recordOf3(blueprintOf2((ctx) => ({ archivedAt: ctx.optional.dateTime }))) {
|
|
4456
|
+
get isArchived() {
|
|
4457
|
+
return this.archivedAt !== undefined;
|
|
4458
|
+
}
|
|
4459
|
+
archive(at = now()) {
|
|
4460
|
+
return this.set("archivedAt", at);
|
|
4461
|
+
}
|
|
4462
|
+
restore() {
|
|
4463
|
+
return this.set("archivedAt", undefined);
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
var Archivable2 = recordPluginOf2("archivable", ArchivableRecord, {
|
|
4467
|
+
description: "An archive date, with archive, restore and isArchived."
|
|
4468
|
+
});
|
|
4469
|
+
|
|
4470
|
+
class TaggableRecord extends recordOf3(blueprintOf2((ctx) => ({ tags: ctx.list.optional.string }))) {
|
|
4471
|
+
hasTag(tag) {
|
|
4472
|
+
return this.tags?.includes(tag) ?? false;
|
|
4473
|
+
}
|
|
4474
|
+
tag(...tags) {
|
|
4475
|
+
return this.set("tags", [...new Set([...this.tags ?? [], ...tags])]);
|
|
4476
|
+
}
|
|
4477
|
+
untag(...tags) {
|
|
4478
|
+
return this.set("tags", (this.tags ?? []).filter((tag) => !tags.includes(tag)));
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
var Taggable2 = recordPluginOf2("taggable", TaggableRecord, {
|
|
4482
|
+
description: "A list of tags, with tag, untag and hasTag."
|
|
4483
|
+
});
|
|
4484
|
+
|
|
4485
|
+
class OwnableRecord extends recordOf3(blueprintOf2((ctx) => ({ ownerId: ctx.id }))) {
|
|
4486
|
+
isOwnedBy(id) {
|
|
4487
|
+
return this.ownerId === id.toLowerCase();
|
|
4488
|
+
}
|
|
4489
|
+
transferTo(id) {
|
|
4490
|
+
return this.set("ownerId", id);
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
var Ownable2 = recordPluginOf2("ownable", OwnableRecord, {
|
|
4494
|
+
description: "The id of the owner, with isOwnedBy and transferTo."
|
|
4495
|
+
});
|
|
4496
|
+
|
|
4497
|
+
class AuthenticatableRecord extends recordOf3(blueprintOf2((ctx) => ({ email: ctx.email, password: ctx.password })), { unique: ["email"], change: ["password"] }) {
|
|
4498
|
+
verifyPassword(plain) {
|
|
4499
|
+
return new Password2(this.password).verify(plain);
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
var Authenticatable2 = recordPluginOf2("authenticatable", AuthenticatableRecord, {
|
|
4503
|
+
description: "A unique email and a sensitive password hash, with change.password and verifyPassword."
|
|
4504
|
+
});
|
|
4505
|
+
var StdRecordPlugins2 = recordPluginCatalogOf2(Publishable2, Archivable2, Taggable2, Ownable2, Authenticatable2);
|
|
4506
|
+
|
|
4507
|
+
// src/std/entity.ts
|
|
4508
|
+
var Identity = blueprintBuilderOf2()(() => ({
|
|
4509
|
+
id: Id2,
|
|
4510
|
+
createdAt: CreatedAt2,
|
|
4511
|
+
updatedAt: UpdatedAt2,
|
|
4512
|
+
deletedAt: DeletedAt2
|
|
4513
|
+
}));
|
|
4514
|
+
var entityOf2 = entityBuilderOf2(vouching({
|
|
4515
|
+
identity: Identity,
|
|
4516
|
+
generateId,
|
|
4517
|
+
now: () => new Date().toISOString(),
|
|
4518
|
+
expiresAt: ExpiresAt2,
|
|
4519
|
+
plugins: [StdRecordPlugins2]
|
|
4520
|
+
}, { id: makesDefaultIds, createdAt: () => true }));
|
|
4521
|
+
// src/std/event-policies.ts
|
|
4522
|
+
var StdEventPolicies2 = eventPolicyCatalogOf2(publishEvents2);
|
|
4523
|
+
// src/std/record.ts
|
|
4524
|
+
var recordOf2 = recordBuilderOf2(StdRecordPlugins2);
|
|
4525
|
+
// src/std/resource.ts
|
|
4526
|
+
var ACTIONS = [
|
|
4527
|
+
"create",
|
|
4528
|
+
"get",
|
|
4529
|
+
"list",
|
|
4530
|
+
"update",
|
|
4531
|
+
"delete"
|
|
4532
|
+
];
|
|
4533
|
+
function routeOf(action, path) {
|
|
4534
|
+
switch (action) {
|
|
4535
|
+
case "create":
|
|
4536
|
+
return { method: "post", path, status: 201 };
|
|
4537
|
+
case "list":
|
|
4538
|
+
return { method: "get", path };
|
|
4539
|
+
case "get":
|
|
4540
|
+
return { method: "get", path: `${path}/{id}` };
|
|
4541
|
+
case "update":
|
|
4542
|
+
return { method: "patch", path: `${path}/{id}` };
|
|
4543
|
+
default:
|
|
4544
|
+
return { method: "delete", path: `${path}/{id}` };
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
function resourceOf2(repository, options) {
|
|
4548
|
+
const actions = options.actions ?? ACTIONS;
|
|
4549
|
+
const { access } = options;
|
|
4550
|
+
const byAction = access !== undefined && Object.keys(access).some((key) => ACTIONS.includes(key));
|
|
4551
|
+
const accessOf = (action) => byAction ? access[action] : access;
|
|
4552
|
+
return (ctx) => {
|
|
4553
|
+
const useCases = {};
|
|
4554
|
+
for (const action of actions) {
|
|
4555
|
+
const declared = accessOf(action);
|
|
4556
|
+
const make = ctx[action];
|
|
4557
|
+
useCases[action] = make(repository, {
|
|
4558
|
+
route: routeOf(action, options.path),
|
|
4559
|
+
...declared === undefined ? {} : { access: declared }
|
|
4560
|
+
});
|
|
4561
|
+
}
|
|
4562
|
+
return useCases;
|
|
4563
|
+
};
|
|
4564
|
+
}
|
|
4565
|
+
// src/std/stages.ts
|
|
4566
|
+
var StdStages2 = stageCatalogOf2(select2, openapi2);
|
|
4567
|
+
export { isUseCaseCatalog2, useCaseCatalogOf2, InvalidCommandDeclaration2, UnknownCommandOperation2, InvalidCommandContext2, InvalidCommandEnvironment2, CommandEventFailed2, commandBuilderOf2, isCommand2, operationOf2, outputOf2, isOutput2, providerFrom2, logFrom2, isProviderFactory2, providersOf2, applicationOf2, blueprintPluginOf2, publishEvents2, select2, stripUndeclared2, recordPluginCatalogOf2, Route2, Access2, Email2, Phone2, Url2, Ip2, enumOf2, SchemaProperty2, PasswordHasherRequired2, SyncVerifyUnsupported2, configure2, resetRuntime2, Id2, Slug2, InvalidJsonData2, InvalidJsonSchema2, jsonSchemaIssues2, checkJsonSchema2, JsonProperty2, jsonOf2, PlainPassword2, Password2, StringProperty2, Text2, NumberProperty2, Integer2, BooleanProperty2, DateTime2, DateProperty2, CreatedAt2, UpdatedAt2, DeletedAt2, ExpiresAt2, Std2, blueprintOf2, StdUseCases2, commandOf2, Publishable2, Archivable2, Taggable2, Ownable2, Authenticatable2, StdRecordPlugins2, entityOf2, StdEventPolicies2, recordOf2, resourceOf2, StdStages2 };
|