@afora/core 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -30
- package/dist/adapters.js +31 -55
- package/dist/core/application/application.d.ts +30 -0
- package/dist/core/application/application.js +1291 -0
- package/dist/core/application/exceptions.js +85 -0
- package/dist/core/application/migrate.js +67 -0
- package/dist/core/application/providers.js +108 -0
- package/dist/core/application/registry.js +23 -0
- package/dist/core/application/schema.js +398 -0
- package/dist/core/application/start.js +162 -0
- package/dist/core/application/types/application.interface.d.ts +23 -3
- package/dist/core/auth/jwt.js +200 -0
- package/dist/core/auth/types/jwt.interface.d.ts +10 -2
- package/dist/core/blueprint/blueprint-plugin.js +7 -0
- package/dist/core/blueprint/blueprint.js +146 -0
- package/dist/core/blueprint/callable.js +13 -0
- package/dist/core/blueprint/exceptions.js +20 -0
- package/dist/core/blueprint/is-blueprint.js +4 -0
- package/dist/core/blueprint/list.js +19 -0
- package/dist/core/blueprint/optional.js +69 -0
- package/dist/core/blueprint/record-field.js +14 -0
- package/dist/core/blueprint/slice.js +127 -0
- package/dist/core/blueprint/types/blueprint.interface.d.ts +16 -0
- package/dist/core/blueprint/variant.js +72 -0
- package/dist/core/command/command.js +428 -0
- package/dist/core/command/exceptions.js +30 -0
- package/dist/core/command/types/command.interface.d.ts +10 -1
- package/dist/core/entity/abstract-entity.d.ts +19 -0
- package/dist/core/entity/abstract-entity.js +181 -0
- package/dist/core/entity/entity.js +209 -0
- package/dist/core/entity/event.d.ts +39 -0
- package/dist/core/entity/event.js +80 -0
- package/dist/core/entity/exceptions.js +27 -0
- package/dist/core/entity/trust.js +7 -0
- package/dist/core/entity/version.js +13 -0
- package/dist/core/environment/environment.d.ts +26 -3
- package/dist/core/environment/environment.js +71 -0
- package/dist/core/event-policy/event-policy.js +76 -0
- package/dist/core/event-policy/exceptions.js +12 -0
- package/dist/core/event-policy/publish.js +98 -0
- package/dist/core/event-policy/publishers.js +18 -0
- package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
- package/dist/core/exception/enums/exception-layers.js +15 -0
- package/dist/core/exception/exception.d.ts +15 -0
- package/dist/core/exception/exception.js +128 -0
- package/dist/core/exception/exported.js +49 -0
- package/dist/core/exception/guard.d.ts +20 -1
- package/dist/core/exception/guard.js +16 -0
- package/dist/core/exception/types/exception.interface.d.ts +13 -0
- package/dist/core/json.js +19 -0
- package/dist/core/logger/exceptions.js +7 -0
- package/dist/core/logger/logger.d.ts +14 -1
- package/dist/core/logger/logger.js +178 -0
- package/dist/core/openapi/exceptions.js +7 -0
- package/dist/core/openapi/openapi.js +132 -0
- package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
- package/dist/core/output/access.js +66 -0
- package/dist/core/output/coerce.js +38 -0
- package/dist/core/output/exceptions.js +22 -0
- package/dist/{index-y280ew0k.js → core/output/http.js} +35 -80
- package/dist/core/output/openapi-sink.d.ts +10 -2
- package/dist/{index-hg1z2s2v.js → core/output/openapi-sink.js} +60 -544
- package/dist/core/output/output.js +192 -0
- package/dist/core/output/problem.js +30 -0
- package/dist/core/output/select.js +28 -0
- package/dist/core/output/stage.js +111 -0
- package/dist/core/output/status.js +52 -0
- package/dist/core/output/types/http.interface.d.ts +5 -1
- package/dist/core/output/types/output.interface.d.ts +24 -0
- package/dist/core/parse/issue.js +31 -0
- package/dist/core/parse/parse.d.ts +8 -1
- package/dist/core/parse/parse.js +323 -0
- package/dist/core/parse/strip.js +48 -0
- package/dist/core/property/abstract-property.js +37 -0
- package/dist/core/property/define-property.js +209 -0
- package/dist/core/property/enums/property-states.js +8 -0
- package/dist/core/property/exceptions.js +12 -0
- package/dist/core/property/input.js +10 -0
- package/dist/core/property/is-property.js +3 -0
- package/dist/core/property/lineage.js +8 -0
- package/dist/core/property/property.d.ts +27 -0
- package/dist/core/property/property.js +46 -0
- package/dist/core/property/refine.js +103 -0
- package/dist/core/record/abstract-record.d.ts +2 -1
- package/dist/core/record/abstract-record.js +307 -0
- package/dist/core/record/exceptions.js +49 -0
- package/dist/core/record/marks.js +7 -0
- package/dist/core/record/mask.js +60 -0
- package/dist/core/record/name.js +23 -0
- package/dist/core/record/plugin-catalog.js +115 -0
- package/dist/core/record/plugin.js +198 -0
- package/dist/core/record/record.js +117 -0
- package/dist/core/record/types/record.interface.d.ts +5 -0
- package/dist/core/repository/cursor.js +83 -0
- package/dist/core/repository/exceptions.js +72 -0
- package/dist/core/repository/in-memory.js +196 -0
- package/dist/core/repository/migrate.js +254 -0
- package/dist/core/repository/plan.js +27 -0
- package/dist/core/repository/provider.js +29 -0
- package/dist/core/repository/query.js +153 -0
- package/dist/core/repository/references.js +196 -0
- package/dist/core/repository/repository.d.ts +22 -2
- package/dist/core/repository/repository.js +720 -0
- package/dist/core/repository/state.js +138 -0
- package/dist/core/repository/types/query.interface.d.ts +13 -0
- package/dist/core/repository/types/repository.interface.d.ts +20 -8
- package/dist/core/repository/types/schema.interface.d.ts +8 -2
- package/dist/core/repository/upcast.js +179 -0
- package/dist/core/schema/schema.d.ts +9 -0
- package/dist/core/schema/schema.js +542 -0
- package/dist/core/standard/standard-schema.js +1 -0
- package/dist/core/standard/standard.js +23 -0
- package/dist/core/symbols/demo.d.ts +4 -0
- package/dist/core/symbols/demo.js +1 -0
- package/dist/core/symbols/kind.js +1 -0
- package/dist/core/symbols/meta.js +1 -0
- package/dist/core/symbols/state.d.ts +5 -0
- package/dist/core/symbols/state.js +1 -0
- package/dist/core/tracing/exceptions.d.ts +4 -0
- package/dist/core/tracing/exceptions.js +7 -0
- package/dist/core/tracing/tracing.d.ts +72 -0
- package/dist/core/tracing/tracing.js +101 -0
- package/dist/core/use-case/access.js +56 -0
- package/dist/core/use-case/decorators.js +65 -0
- package/dist/core/use-case/exceptions.d.ts +5 -1
- package/dist/core/use-case/exceptions.js +48 -0
- package/dist/core/use-case/plugin-catalog.js +68 -0
- package/dist/core/use-case/plugin.js +133 -0
- package/dist/core/use-case/route.js +200 -0
- package/dist/core/use-case/scope.d.ts +5 -0
- package/dist/core/use-case/scope.js +4 -0
- package/dist/core/use-case/steps/describe.d.ts +5 -1
- package/dist/core/use-case/steps/describe.js +24 -0
- package/dist/core/use-case/steps/references.js +105 -0
- package/dist/core/use-case/steps/run.js +139 -0
- package/dist/core/use-case/steps/types/check.interface.d.ts +27 -8
- package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
- package/dist/core/use-case/steps/validate.js +368 -0
- package/dist/core/use-case/target.js +100 -0
- package/dist/core/use-case/types/route.interface.d.ts +25 -5
- package/dist/core/use-case/use-case.d.ts +77 -2
- package/dist/core/use-case/use-case.js +493 -0
- package/dist/experimental.d.ts +5 -0
- package/dist/experimental.js +18 -161
- package/dist/index.d.ts +7 -5
- package/dist/index.js +213 -367
- package/dist/std/blueprint-plugin.js +42 -0
- package/dist/std/blueprint.d.ts +21 -0
- package/dist/std/blueprint.js +3 -0
- package/dist/std/command.js +3 -0
- package/dist/std/entity.d.ts +20 -1
- package/dist/std/entity.js +30 -0
- package/dist/std/event-policies.js +3 -0
- package/dist/std/properties/contact.d.ts +3 -0
- package/dist/std/properties/contact.js +29 -0
- package/dist/std/properties/enum.js +26 -0
- package/dist/std/properties/exceptions.js +14 -0
- package/dist/std/properties/headless.js +45 -0
- package/dist/std/properties/identity.js +28 -0
- package/dist/std/properties/json.js +161 -0
- package/dist/std/properties/password.js +79 -0
- package/dist/std/properties/primitives.d.ts +4 -0
- package/dist/std/properties/primitives.js +28 -0
- package/dist/std/properties/time.js +55 -0
- package/dist/std/record-plugins.js +73 -0
- package/dist/std/record.d.ts +20 -1
- package/dist/std/record.js +3 -0
- package/dist/std/resource.js +39 -0
- package/dist/std/runtime.js +193 -0
- package/dist/std/stages.js +4 -0
- package/dist/std/use-cases/auth.js +193 -0
- package/dist/std/use-cases/crud.js +261 -0
- package/dist/std/use-cases/index.js +12 -0
- package/dist/std/use-cases/lifecycle.js +97 -0
- package/dist/std/use-cases/shared.js +232 -0
- package/dist/std/use-cases/types.d.ts +5 -1
- package/dist/std/use-cases/types.js +0 -0
- package/dist/testing/http-provider-contract.js +344 -0
- package/dist/testing/index.js +3 -1435
- package/dist/testing/memory-publisher.js +12 -0
- package/dist/testing/provider-contract.d.ts +12 -3
- package/dist/testing/provider-contract.js +1071 -0
- package/package.json +10 -3
- package/dist/index-3aae5jfq.js +0 -2997
- package/dist/index-ny3as79r.js +0 -4567
- package/dist/index-xegv6a1p.js +0 -4291
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { isCommand } from "../command/command.js";
|
|
2
|
+
import { Kind } from "../symbols/kind.js";
|
|
3
|
+
import { accessGuardOf } from "./access.js";
|
|
4
|
+
import { DuplicateOperation, InvalidOutputStage } from "./exceptions.js";
|
|
5
|
+
import { isStage } from "./stage.js";
|
|
6
|
+
function isApplicationLike(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && value[Kind] === "application";
|
|
8
|
+
}
|
|
9
|
+
function invalid(source, reason) {
|
|
10
|
+
throw new InvalidOutputStage({ source, details: { reason } });
|
|
11
|
+
}
|
|
12
|
+
const PATCHABLE = ["meta", "tags", "description", "deprecated", "invoke"];
|
|
13
|
+
export function operationOf(operation, patch) {
|
|
14
|
+
const source = operation?.id ?? "operationOf";
|
|
15
|
+
if (operation?.kind !== "operation" || operation.version !== 1) {
|
|
16
|
+
invalid(source, "Expected an operation descriptor.");
|
|
17
|
+
}
|
|
18
|
+
if (typeof patch !== "object" || patch === null || Array.isArray(patch)) {
|
|
19
|
+
invalid(source, "Expected the patch to be an object.");
|
|
20
|
+
}
|
|
21
|
+
for (const key of Reflect.ownKeys(patch)) {
|
|
22
|
+
if (typeof key !== "string" || !PATCHABLE.includes(key)) {
|
|
23
|
+
invalid(source, `"${String(key)}" cannot be patched; only ${PATCHABLE.join(", ")} can.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const { meta, tags, description, deprecated, invoke } = patch;
|
|
27
|
+
if ("meta" in patch && (typeof meta !== "object" || meta === null || Array.isArray(meta))) {
|
|
28
|
+
invalid(source, "Expected meta to be an object.");
|
|
29
|
+
}
|
|
30
|
+
if ("tags" in patch && (!Array.isArray(tags) || tags.some((tag) => typeof tag !== "string"))) {
|
|
31
|
+
invalid(source, "Expected tags to be a list of strings.");
|
|
32
|
+
}
|
|
33
|
+
if ("description" in patch && description !== undefined && typeof description !== "string") {
|
|
34
|
+
invalid(source, "Expected description to be a string.");
|
|
35
|
+
}
|
|
36
|
+
if ("deprecated" in patch && typeof deprecated !== "boolean") {
|
|
37
|
+
invalid(source, "Expected deprecated to be a boolean.");
|
|
38
|
+
}
|
|
39
|
+
if ("invoke" in patch && typeof invoke !== "function") {
|
|
40
|
+
invalid(source, "Expected invoke to be a function.");
|
|
41
|
+
}
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
...operation,
|
|
44
|
+
..."meta" in patch ? { meta: Object.freeze({ ...meta }) } : {},
|
|
45
|
+
..."tags" in patch ? { tags: Object.freeze([...tags ?? []]) } : {},
|
|
46
|
+
..."description" in patch ? { description } : {},
|
|
47
|
+
..."deprecated" in patch ? { deprecated } : {},
|
|
48
|
+
..."invoke" in patch ? { invoke } : {}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function operationsOfCommand(command) {
|
|
52
|
+
return command.operations.map((name) => {
|
|
53
|
+
const UseCase = command.useCases[name];
|
|
54
|
+
const context = command.context ?? UseCase.context;
|
|
55
|
+
const description = UseCase.description ?? command.description;
|
|
56
|
+
const id = `${command.commandName}.${name}`;
|
|
57
|
+
const run = command.run;
|
|
58
|
+
const guard = UseCase.access === undefined ? undefined : accessGuardOf(id, UseCase.access, context);
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
kind: "operation",
|
|
61
|
+
version: 1,
|
|
62
|
+
id,
|
|
63
|
+
command: command.commandName,
|
|
64
|
+
name,
|
|
65
|
+
useCase: UseCase.useCaseName,
|
|
66
|
+
...description === undefined ? {} : { description },
|
|
67
|
+
tags: Object.freeze([
|
|
68
|
+
...new Set([...command.tags, ...UseCase.tags])
|
|
69
|
+
]),
|
|
70
|
+
deprecated: UseCase.deprecated,
|
|
71
|
+
input: UseCase.blueprint,
|
|
72
|
+
response: UseCase.response,
|
|
73
|
+
...context ? { context } : {},
|
|
74
|
+
errors: UseCase.errors,
|
|
75
|
+
...UseCase.route === undefined ? {} : { route: UseCase.route },
|
|
76
|
+
...UseCase.access === undefined ? {} : { access: UseCase.access },
|
|
77
|
+
meta: Object.freeze({}),
|
|
78
|
+
invoke: guard ? async (input, options) => run(name, input, guard(input, options)) : (input, options) => run(name, input, options && "principal" in options ? { context: options.context } : options)
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function statusesOf(name, statuses) {
|
|
83
|
+
if (statuses === undefined)
|
|
84
|
+
return Object.freeze({});
|
|
85
|
+
if (typeof statuses !== "object" || statuses === null || Array.isArray(statuses)) {
|
|
86
|
+
invalid(name, "Expected statuses to be a map of exception names to statuses.");
|
|
87
|
+
}
|
|
88
|
+
for (const [exception, status] of Object.entries(statuses)) {
|
|
89
|
+
if (!exception || typeof status !== "number" || !Number.isInteger(status) || status < 400 || status > 599) {
|
|
90
|
+
invalid(name, `Expected the status of "${exception}" to be an integer between 400 and 599.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return Object.freeze({ ...statuses });
|
|
94
|
+
}
|
|
95
|
+
function checkPipe(name, pipe) {
|
|
96
|
+
if (!Array.isArray(pipe))
|
|
97
|
+
invalid(name, "Expected pipe to be a list of stages.");
|
|
98
|
+
const stages = pipe;
|
|
99
|
+
stages.forEach((stage, index) => {
|
|
100
|
+
if (!isStage(stage)) {
|
|
101
|
+
invalid(name, `Expected pipe[${index}] to be a transform or sink stage.`);
|
|
102
|
+
}
|
|
103
|
+
if (stage.kind === "sink" && index !== stages.length - 1) {
|
|
104
|
+
invalid(name, `The sink at pipe[${index}] must be the last stage.`);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return Object.freeze([...stages]);
|
|
108
|
+
}
|
|
109
|
+
function transformed(name, operations, transforms, context) {
|
|
110
|
+
let current = operations;
|
|
111
|
+
for (const stage of transforms) {
|
|
112
|
+
if (stage.kind !== "transform")
|
|
113
|
+
continue;
|
|
114
|
+
const next = [];
|
|
115
|
+
for (const operation of current) {
|
|
116
|
+
const result = stage.apply(operation, context);
|
|
117
|
+
if (result === null)
|
|
118
|
+
continue;
|
|
119
|
+
if (typeof result !== "object" || result?.kind !== "operation" || result.id !== operation.id) {
|
|
120
|
+
invalid(name, `A transform must return the operation "${operation.id}" or null.`);
|
|
121
|
+
}
|
|
122
|
+
next.push(result);
|
|
123
|
+
}
|
|
124
|
+
current = next;
|
|
125
|
+
}
|
|
126
|
+
return Object.freeze([...current]);
|
|
127
|
+
}
|
|
128
|
+
function make(name, commands, operations, stages, statuses) {
|
|
129
|
+
const context = Object.freeze({
|
|
130
|
+
output: Object.freeze({ name, statuses })
|
|
131
|
+
});
|
|
132
|
+
let built;
|
|
133
|
+
const output = {
|
|
134
|
+
[Kind]: "output",
|
|
135
|
+
outputName: name,
|
|
136
|
+
commands,
|
|
137
|
+
operations,
|
|
138
|
+
stages,
|
|
139
|
+
statuses,
|
|
140
|
+
build() {
|
|
141
|
+
if (built)
|
|
142
|
+
return built.value;
|
|
143
|
+
const sink = stages.at(-1);
|
|
144
|
+
if (sink?.kind !== "sink") {
|
|
145
|
+
invalid(name || "output", "The pipe has no sink to build.");
|
|
146
|
+
}
|
|
147
|
+
built = { value: sink.build(operations, context) };
|
|
148
|
+
return built.value;
|
|
149
|
+
},
|
|
150
|
+
pipe(...more) {
|
|
151
|
+
const next = checkPipe(name || "output", more);
|
|
152
|
+
const last = next.at(-1);
|
|
153
|
+
if (stages.at(-1)?.kind === "sink") {
|
|
154
|
+
invalid(name || "output", "Cannot add stages after the sink.");
|
|
155
|
+
}
|
|
156
|
+
const ops = transformed(name, operations, next, context);
|
|
157
|
+
if (last?.kind === "sink") {
|
|
158
|
+
return last.build(ops, context);
|
|
159
|
+
}
|
|
160
|
+
return make("", commands, ops, Object.freeze([...stages, ...next]), statuses);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
return Object.freeze(output);
|
|
164
|
+
}
|
|
165
|
+
export function outputOf(settings) {
|
|
166
|
+
const { name, commands } = settings ?? {};
|
|
167
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
168
|
+
invalid("outputOf", "Expected name to be a non-empty string.");
|
|
169
|
+
}
|
|
170
|
+
const sources = Array.isArray(commands) ? commands : [commands];
|
|
171
|
+
if (!Array.isArray(commands) && isCommand(commands) || sources.some((entry) => !isCommand(entry) && !isApplicationLike(entry))) {
|
|
172
|
+
invalid(name, "Expected commands to be a list of commands made by commandOf or applications.");
|
|
173
|
+
}
|
|
174
|
+
const flat = [
|
|
175
|
+
...new Set(sources.flatMap((entry) => isCommand(entry) ? [entry] : Object.values(entry.commands)))
|
|
176
|
+
];
|
|
177
|
+
const statuses = statusesOf(name, settings.statuses);
|
|
178
|
+
const stages = checkPipe(name, settings.pipe ?? []);
|
|
179
|
+
const all = flat.flatMap(operationsOfCommand);
|
|
180
|
+
const ids = new Set;
|
|
181
|
+
for (const { id } of all) {
|
|
182
|
+
if (ids.has(id))
|
|
183
|
+
throw new DuplicateOperation({ source: name, details: { id } });
|
|
184
|
+
ids.add(id);
|
|
185
|
+
}
|
|
186
|
+
const context = { output: { name, statuses } };
|
|
187
|
+
const operations = transformed(name, all, stages, context);
|
|
188
|
+
return make(name, Object.freeze(flat), operations, stages, statuses);
|
|
189
|
+
}
|
|
190
|
+
export function isOutput(value) {
|
|
191
|
+
return typeof value === "object" && value !== null && value[Kind] === "output";
|
|
192
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Exception } from "../exception/exception.js";
|
|
2
|
+
import { raisedOf } from "./status.js";
|
|
3
|
+
export const PROBLEM_MEDIA_TYPE = "application/problem+json";
|
|
4
|
+
const INTERNAL = "Internal error.";
|
|
5
|
+
export function problemOf(error, options) {
|
|
6
|
+
const { status, debug = false } = options;
|
|
7
|
+
const exception = error instanceof Exception ? error : undefined;
|
|
8
|
+
const title = exception?.name ?? "UnexpectedError";
|
|
9
|
+
const message = exception?.message ?? (error instanceof Error ? error.message : String(error));
|
|
10
|
+
const raised = exception && raisedOf(exception);
|
|
11
|
+
const rawIssues = raised?.details?.issues;
|
|
12
|
+
const issues = status === 400 && Array.isArray(rawIssues) ? rawIssues.map((issue) => ({
|
|
13
|
+
path: [...issue.path],
|
|
14
|
+
code: issue.code,
|
|
15
|
+
message: issue.message
|
|
16
|
+
})) : undefined;
|
|
17
|
+
return {
|
|
18
|
+
type: `urn:afora:exception:${title}`,
|
|
19
|
+
title,
|
|
20
|
+
status,
|
|
21
|
+
detail: status >= 500 && !debug ? INTERNAL : message,
|
|
22
|
+
...raised ? { layer: raised.layer } : {},
|
|
23
|
+
...issues ? { issues } : {},
|
|
24
|
+
...debug ? { trace: exception ? exception.toString() : String(error) } : {}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function outputValueOf(value) {
|
|
28
|
+
const text = JSON.stringify(value);
|
|
29
|
+
return text === undefined ? undefined : JSON.parse(text);
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { transformOf } from "./stage.js";
|
|
2
|
+
const LISTS = ["tags", "operations", "exclude"];
|
|
3
|
+
export const select = transformOf({
|
|
4
|
+
name: "select",
|
|
5
|
+
validate: (options) => {
|
|
6
|
+
for (const key of Object.keys(options)) {
|
|
7
|
+
if (!LISTS.includes(key)) {
|
|
8
|
+
return `select has no option "${key}".`;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
for (const key of LISTS) {
|
|
12
|
+
const list = options[key];
|
|
13
|
+
if (list !== undefined && (!Array.isArray(list) || list.some((item) => typeof item !== "string"))) {
|
|
14
|
+
return `Expected select.${key} to be a list of strings.`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
},
|
|
19
|
+
apply: (operation, { tags, operations, exclude }) => {
|
|
20
|
+
if (tags && !tags.some((tag) => operation.tags.includes(tag)))
|
|
21
|
+
return null;
|
|
22
|
+
if (operations && !operations.includes(operation.id))
|
|
23
|
+
return null;
|
|
24
|
+
if (exclude?.includes(operation.id))
|
|
25
|
+
return null;
|
|
26
|
+
return operation;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { frozenJson } from "../json.js";
|
|
2
|
+
import { Kind } from "../symbols/kind.js";
|
|
3
|
+
import { InvalidOutputStage } from "./exceptions.js";
|
|
4
|
+
const descriptions = new WeakMap;
|
|
5
|
+
const factories = new WeakMap;
|
|
6
|
+
const opaque = Object.freeze({ kind: "opaque" });
|
|
7
|
+
function invalid(source, reason) {
|
|
8
|
+
throw new InvalidOutputStage({ source, details: { reason } });
|
|
9
|
+
}
|
|
10
|
+
function optionsOf(name, given, validate) {
|
|
11
|
+
const options = frozenJson(given ?? {}, "Stage options", (reason) => invalid(name, reason));
|
|
12
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
13
|
+
invalid(name, "Stage options must be an object.");
|
|
14
|
+
}
|
|
15
|
+
const reason = validate?.(options);
|
|
16
|
+
if (reason !== undefined)
|
|
17
|
+
invalid(name, reason);
|
|
18
|
+
return options;
|
|
19
|
+
}
|
|
20
|
+
function checkName(source, name) {
|
|
21
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
22
|
+
invalid(source, "Expected the stage name to be a non-empty string.");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function transformOf(settings) {
|
|
26
|
+
checkName("transformOf", settings?.name);
|
|
27
|
+
if (typeof settings.apply !== "function") {
|
|
28
|
+
invalid(settings.name, "Expected apply to be a function.");
|
|
29
|
+
}
|
|
30
|
+
const { name, apply, validate } = settings;
|
|
31
|
+
const factory = (given) => {
|
|
32
|
+
const options = optionsOf(name, given, validate);
|
|
33
|
+
const stage = Object.freeze({
|
|
34
|
+
kind: "transform",
|
|
35
|
+
name,
|
|
36
|
+
options,
|
|
37
|
+
apply: (operation, context) => apply(operation, options, context)
|
|
38
|
+
});
|
|
39
|
+
descriptions.set(stage, Object.freeze({
|
|
40
|
+
kind: "transform",
|
|
41
|
+
name,
|
|
42
|
+
options
|
|
43
|
+
}));
|
|
44
|
+
return stage;
|
|
45
|
+
};
|
|
46
|
+
factories.set(factory, { kind: "transform", name });
|
|
47
|
+
return factory;
|
|
48
|
+
}
|
|
49
|
+
export function sinkOf(settings) {
|
|
50
|
+
checkName("sinkOf", settings?.name);
|
|
51
|
+
if (typeof settings.build !== "function") {
|
|
52
|
+
invalid(settings.name, "Expected build to be a function.");
|
|
53
|
+
}
|
|
54
|
+
const { name, build, validate } = settings;
|
|
55
|
+
const factory = (given, hooks) => {
|
|
56
|
+
const options = optionsOf(name, given, validate);
|
|
57
|
+
if (hooks !== undefined && (typeof hooks !== "object" || hooks === null || Array.isArray(hooks))) {
|
|
58
|
+
invalid(name, "Stage hooks must be an object.");
|
|
59
|
+
}
|
|
60
|
+
const stage = Object.freeze({
|
|
61
|
+
kind: "sink",
|
|
62
|
+
name,
|
|
63
|
+
options,
|
|
64
|
+
build: (operations, context) => build(operations, options, context, hooks)
|
|
65
|
+
});
|
|
66
|
+
descriptions.set(stage, Object.freeze({
|
|
67
|
+
kind: "sink",
|
|
68
|
+
name,
|
|
69
|
+
options
|
|
70
|
+
}));
|
|
71
|
+
return stage;
|
|
72
|
+
};
|
|
73
|
+
factories.set(factory, { kind: "sink", name });
|
|
74
|
+
return factory;
|
|
75
|
+
}
|
|
76
|
+
export function stageDescriptionOf(stage) {
|
|
77
|
+
return descriptions.get(stage) ?? opaque;
|
|
78
|
+
}
|
|
79
|
+
export function stageKindOf(factory) {
|
|
80
|
+
return factories.get(factory)?.kind;
|
|
81
|
+
}
|
|
82
|
+
export function isStage(value) {
|
|
83
|
+
if (typeof value !== "object" || value === null)
|
|
84
|
+
return false;
|
|
85
|
+
const { kind, apply, build } = value;
|
|
86
|
+
return kind === "transform" && typeof apply === "function" || kind === "sink" && typeof build === "function";
|
|
87
|
+
}
|
|
88
|
+
export function stageCatalogOf(...stages) {
|
|
89
|
+
const byName = {};
|
|
90
|
+
for (const factory of stages) {
|
|
91
|
+
const known = factories.get(factory);
|
|
92
|
+
if (!known) {
|
|
93
|
+
invalid("catalog", "A catalog takes factories made by `transformOf` or `sinkOf`.");
|
|
94
|
+
}
|
|
95
|
+
if (Object.hasOwn(byName, known.name)) {
|
|
96
|
+
invalid(known.name, "The catalog has another stage with its name.");
|
|
97
|
+
}
|
|
98
|
+
byName[known.name] = factory;
|
|
99
|
+
}
|
|
100
|
+
return Object.freeze({
|
|
101
|
+
[Kind]: "stage-catalog",
|
|
102
|
+
stages: Object.freeze(byName),
|
|
103
|
+
list: Object.freeze([...stages]),
|
|
104
|
+
resolve(name, options) {
|
|
105
|
+
const factory = Object.hasOwn(byName, name) ? byName[name] : undefined;
|
|
106
|
+
if (!factory)
|
|
107
|
+
invalid("catalog", `Unknown stage "${name}".`);
|
|
108
|
+
return factory(options);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Exception } from "../exception/exception.js";
|
|
2
|
+
const INVALID_INPUT = new Set([
|
|
3
|
+
"InvalidUseCaseInput",
|
|
4
|
+
"InvalidUseCaseContext",
|
|
5
|
+
"InvalidCommandContext"
|
|
6
|
+
]);
|
|
7
|
+
const CONFIGURATION = new Set(["InvalidCommandEnvironment"]);
|
|
8
|
+
const NOT_FOUND = new Set([
|
|
9
|
+
"RepositoryEntityNotFound",
|
|
10
|
+
"UnknownCommandOperation",
|
|
11
|
+
"UseCaseEntityNotFound"
|
|
12
|
+
]);
|
|
13
|
+
const CONFLICT = new Set([
|
|
14
|
+
"RepositoryUniqueViolation",
|
|
15
|
+
"RepositoryConcurrencyConflict",
|
|
16
|
+
"UseCaseEntityConflict"
|
|
17
|
+
]);
|
|
18
|
+
export function raisedOf(error) {
|
|
19
|
+
let raised = error;
|
|
20
|
+
for (let current = error.cause;current instanceof Exception; current = current.cause) {
|
|
21
|
+
if (current.name === error.name)
|
|
22
|
+
raised = current;
|
|
23
|
+
}
|
|
24
|
+
return raised;
|
|
25
|
+
}
|
|
26
|
+
export function statusOf(error, operation, statuses) {
|
|
27
|
+
if (!(error instanceof Exception))
|
|
28
|
+
return 500;
|
|
29
|
+
const { name } = error;
|
|
30
|
+
if (statuses && Object.hasOwn(statuses, name))
|
|
31
|
+
return statuses[name];
|
|
32
|
+
if (INVALID_INPUT.has(name))
|
|
33
|
+
return 400;
|
|
34
|
+
if (CONFIGURATION.has(name))
|
|
35
|
+
return 500;
|
|
36
|
+
if (Array.isArray(raisedOf(error).details?.issues)) {
|
|
37
|
+
return 400;
|
|
38
|
+
}
|
|
39
|
+
if (NOT_FOUND.has(name))
|
|
40
|
+
return 404;
|
|
41
|
+
if (CONFLICT.has(name))
|
|
42
|
+
return 409;
|
|
43
|
+
if (name === "UseCaseInvalidCredentials" || name === "Unauthenticated") {
|
|
44
|
+
return 401;
|
|
45
|
+
}
|
|
46
|
+
if (name === "Forbidden")
|
|
47
|
+
return 403;
|
|
48
|
+
if (operation?.errors.some((Declared) => error.is(Declared) || Declared.name === name)) {
|
|
49
|
+
return 422;
|
|
50
|
+
}
|
|
51
|
+
return 500;
|
|
52
|
+
}
|
|
@@ -15,7 +15,11 @@ export interface HttpOptions extends RouteOptions {
|
|
|
15
15
|
readonly info: OpenAPISinkOptions["info"];
|
|
16
16
|
readonly servers?: OpenAPISinkOptions["servers"];
|
|
17
17
|
};
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Adds the `trace` to problems and the real `detail` to 5xx ones.
|
|
20
|
+
*
|
|
21
|
+
* @default false
|
|
22
|
+
*/
|
|
19
23
|
readonly debug?: boolean;
|
|
20
24
|
/**
|
|
21
25
|
* The context of an operation, by context key: from request headers
|
|
@@ -100,6 +100,11 @@ export type StageDescription = {
|
|
|
100
100
|
} | {
|
|
101
101
|
readonly kind: "opaque";
|
|
102
102
|
};
|
|
103
|
+
/**
|
|
104
|
+
* What `transformOf` takes: the `name` of the stage, the `apply` function that
|
|
105
|
+
* changes each operation (or removes it) and an optional `validate` of its
|
|
106
|
+
* options.
|
|
107
|
+
*/
|
|
103
108
|
export interface TransformSettings<Options extends object> {
|
|
104
109
|
readonly name: string;
|
|
105
110
|
/** Returns the operation, a changed copy, or `null` to remove it. */
|
|
@@ -107,6 +112,11 @@ export interface TransformSettings<Options extends object> {
|
|
|
107
112
|
/** Why `options` cannot be used, or `undefined`. Checked when the stage is made. */
|
|
108
113
|
readonly validate?: (options: Options) => string | undefined;
|
|
109
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* What `sinkOf` takes: the `name` of the stage, the `build` function that turns
|
|
117
|
+
* the operations into the result of the output (a server, a document...) and an
|
|
118
|
+
* optional `validate` of its options.
|
|
119
|
+
*/
|
|
110
120
|
export interface SinkSettings<Result, Options extends object, Hooks = never> {
|
|
111
121
|
readonly name: string;
|
|
112
122
|
/**
|
|
@@ -120,6 +130,11 @@ export interface SinkSettings<Result, Options extends object, Hooks = never> {
|
|
|
120
130
|
}
|
|
121
131
|
/** Makes a stage from JSON options. */
|
|
122
132
|
export type TransformFactory<Options extends object = StageOptions> = (options?: Options) => TransformStage<Options>;
|
|
133
|
+
/**
|
|
134
|
+
* Makes a sink stage from JSON `options` and, second, `hooks`: what cannot be
|
|
135
|
+
* JSON (functions, an instance of a server), which the description of the stage
|
|
136
|
+
* leaves out. What `sinkOf` returns.
|
|
137
|
+
*/
|
|
123
138
|
export type SinkFactory<Result = unknown, Options extends object = StageOptions, Hooks = never> = (options?: Options, hooks?: Hooks) => SinkStage<Result, Options>;
|
|
124
139
|
/** Any stage factory. */
|
|
125
140
|
export type StageFactory = TransformFactory<any> | SinkFactory<any, any>;
|
|
@@ -143,6 +158,11 @@ export interface OutputApplication {
|
|
|
143
158
|
}
|
|
144
159
|
/** What an output takes its commands from. */
|
|
145
160
|
export type OutputSource = AnyCommand | OutputApplication;
|
|
161
|
+
/**
|
|
162
|
+
* What `outputOf` takes: the `name` of the output, the `commands` it exposes
|
|
163
|
+
* (commands, applications or a mix), the `statuses` by exception name and the
|
|
164
|
+
* `pipe` of stages that transform its operations and end in a sink.
|
|
165
|
+
*/
|
|
146
166
|
export interface OutputSettings<Pipe extends readonly Stage[]> {
|
|
147
167
|
readonly name: string;
|
|
148
168
|
/** The commands the output exposes: commands, applications (all their commands) or a mix. */
|
|
@@ -184,6 +204,10 @@ export interface Problem {
|
|
|
184
204
|
/** Only with `debug`. */
|
|
185
205
|
readonly trace?: string;
|
|
186
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* What `problemOf` takes: the HTTP `status` of the problem and `debug`, which
|
|
209
|
+
* adds the `trace` and the real `detail` of a 5xx.
|
|
210
|
+
*/
|
|
187
211
|
export interface ProblemOptions {
|
|
188
212
|
readonly status: number;
|
|
189
213
|
readonly debug?: boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const REJECTED = Symbol("rejected");
|
|
2
|
+
|
|
3
|
+
export class LazyIssue {
|
|
4
|
+
message;
|
|
5
|
+
path;
|
|
6
|
+
code;
|
|
7
|
+
source;
|
|
8
|
+
#origin;
|
|
9
|
+
constructor(message, path, code, source, origin) {
|
|
10
|
+
this.message = message;
|
|
11
|
+
this.path = path;
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.source = source;
|
|
14
|
+
this.#origin = origin;
|
|
15
|
+
}
|
|
16
|
+
get cause() {
|
|
17
|
+
return this.#origin.exception();
|
|
18
|
+
}
|
|
19
|
+
at(path) {
|
|
20
|
+
return new LazyIssue(this.message, path, this.code, this.source, this.#origin);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function movedIssue(issue, path) {
|
|
24
|
+
if (issue instanceof LazyIssue)
|
|
25
|
+
return issue.at(path);
|
|
26
|
+
const moved = { ...issue, path };
|
|
27
|
+
const cause = Object.getOwnPropertyDescriptor(issue, "cause");
|
|
28
|
+
if (cause && !cause.enumerable)
|
|
29
|
+
Object.defineProperty(moved, "cause", cause);
|
|
30
|
+
return moved;
|
|
31
|
+
}
|
|
@@ -18,6 +18,13 @@ export declare function isPlainObject(value: unknown): value is Record<string, u
|
|
|
18
18
|
* is parsing it, e.g. a record name.
|
|
19
19
|
*/
|
|
20
20
|
export declare function parseCollection(properties: IBlueprintCollection, value: unknown, path: Path, source: string): ParseResult<Readonly<Record<string, unknown>>>;
|
|
21
|
+
/**
|
|
22
|
+
* Makes, once the rest of a collection is known to be valid, the values the
|
|
23
|
+
* caller left out for it (`undefined` in `handled`); it returns the keys it
|
|
24
|
+
* made that still have to be checked.
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export type Complete = (handled: Record<string, unknown>) => readonly string[] | undefined;
|
|
21
28
|
/**
|
|
22
29
|
* `parseCollection` at the root, as a function of the value alone: what it
|
|
23
30
|
* needs of `properties` is looked up on the first value and kept, instead of
|
|
@@ -26,7 +33,7 @@ export declare function parseCollection(properties: IBlueprintCollection, value:
|
|
|
26
33
|
* unchecked: for what the caller made itself and knows to be valid.
|
|
27
34
|
* @internal
|
|
28
35
|
*/
|
|
29
|
-
export declare function collectionParserOf(properties: IBlueprintCollection, source: string): (value: unknown, vouched?: readonly string[]) => ParseResult<Readonly<Record<string, unknown>>>;
|
|
36
|
+
export declare function collectionParserOf(properties: IBlueprintCollection, source: string): (value: unknown, vouched?: readonly string[], complete?: Complete) => ParseResult<Readonly<Record<string, unknown>>>;
|
|
30
37
|
/**
|
|
31
38
|
* `value` handled by `field`, or every issue found: a property runs its
|
|
32
39
|
* handlers, a blueprint each of its fields, a list each of its items, and a
|