@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,1055 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isProperty,
|
|
3
|
+
isBlueprint,
|
|
4
|
+
recordFieldOf,
|
|
5
|
+
Exception2,
|
|
6
|
+
isBlueprintVariant,
|
|
7
|
+
recordNameOf2,
|
|
8
|
+
isBlueprintList,
|
|
9
|
+
toOpenAPI2,
|
|
10
|
+
HEADER_NAME,
|
|
11
|
+
isRoutePath,
|
|
12
|
+
parametersOfPath,
|
|
13
|
+
isScalarSchema,
|
|
14
|
+
isScalarKey,
|
|
15
|
+
InvalidOutputStage2,
|
|
16
|
+
sinkOf2
|
|
17
|
+
} from "./index-xegv6a1p.js";
|
|
18
|
+
|
|
19
|
+
// src/core/application/schema.ts
|
|
20
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
function sorted(value) {
|
|
22
|
+
if (Array.isArray(value))
|
|
23
|
+
return value.map(sorted);
|
|
24
|
+
if (!isObject(value))
|
|
25
|
+
return value;
|
|
26
|
+
return Object.fromEntries(Object.keys(value).filter((key) => !key.startsWith("~") && value[key] !== undefined).sort().map((key) => [key, sorted(value[key])]));
|
|
27
|
+
}
|
|
28
|
+
function evolutionOf(property, description, found = {}, depth = 0) {
|
|
29
|
+
if (property === undefined || depth > 64)
|
|
30
|
+
return found;
|
|
31
|
+
const resolved = "ref" in property ? description.properties[property.ref] : property;
|
|
32
|
+
const lineage = resolved?.lineage;
|
|
33
|
+
if (lineage === undefined || lineage.kind === "factory")
|
|
34
|
+
return found;
|
|
35
|
+
if (lineage.kind === "refined") {
|
|
36
|
+
const { fill, renamedFrom } = lineage.options;
|
|
37
|
+
if (!("fill" in found) && fill !== undefined)
|
|
38
|
+
found.fill = fill;
|
|
39
|
+
if (found.renamedFrom === undefined && typeof renamedFrom === "string") {
|
|
40
|
+
found.renamedFrom = renamedFrom;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return evolutionOf(lineage.base, description, found, depth + 1);
|
|
44
|
+
}
|
|
45
|
+
function fieldsOf(fields, description) {
|
|
46
|
+
if (fields === undefined)
|
|
47
|
+
return;
|
|
48
|
+
return Object.fromEntries(Object.keys(fields).sort().map((key) => [
|
|
49
|
+
key,
|
|
50
|
+
lockFieldOf(fields[key], description)
|
|
51
|
+
]));
|
|
52
|
+
}
|
|
53
|
+
function lockFieldOf(field, description) {
|
|
54
|
+
switch (field.kind) {
|
|
55
|
+
case "property": {
|
|
56
|
+
const property = "ref" in field ? description.properties[field.ref] : field;
|
|
57
|
+
const evolution = evolutionOf("ref" in field ? { ref: field.ref } : field, description);
|
|
58
|
+
return {
|
|
59
|
+
schema: sorted(property?.schema ?? {}),
|
|
60
|
+
optional: field.optional,
|
|
61
|
+
...property?.unique ? { unique: true } : {},
|
|
62
|
+
..."fill" in evolution ? { fill: sorted(evolution.fill) } : {},
|
|
63
|
+
...evolution.renamedFrom === undefined ? {} : { renamedFrom: evolution.renamedFrom }
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
case "list": {
|
|
67
|
+
const item = lockFieldOf(field.item, description);
|
|
68
|
+
return {
|
|
69
|
+
schema: sorted({
|
|
70
|
+
type: "array",
|
|
71
|
+
items: item.schema,
|
|
72
|
+
minItems: field.minItems,
|
|
73
|
+
maxItems: field.maxItems
|
|
74
|
+
}),
|
|
75
|
+
optional: field.optional,
|
|
76
|
+
...item.fields === undefined ? {} : { fields: item.fields }
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
case "blueprint": {
|
|
80
|
+
const fields = "ref" in field ? description.blueprints[field.ref]?.fields : field.fields;
|
|
81
|
+
return {
|
|
82
|
+
schema: { type: "object" },
|
|
83
|
+
optional: field.optional,
|
|
84
|
+
fields: fieldsOf(fields, description) ?? {}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
case "record": {
|
|
88
|
+
const fields = description.records[field.ref]?.fields;
|
|
89
|
+
return {
|
|
90
|
+
schema: { type: "object" },
|
|
91
|
+
optional: field.optional,
|
|
92
|
+
fields: fieldsOf(fields, description) ?? {}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
case "variant":
|
|
96
|
+
return {
|
|
97
|
+
schema: { discriminator: field.key, type: "object" },
|
|
98
|
+
optional: field.optional,
|
|
99
|
+
fields: Object.fromEntries(Object.keys(field.variants).sort().map((tag) => [
|
|
100
|
+
tag,
|
|
101
|
+
{
|
|
102
|
+
...lockFieldOf(field.variants[tag], description),
|
|
103
|
+
optional: true
|
|
104
|
+
}
|
|
105
|
+
]))
|
|
106
|
+
};
|
|
107
|
+
case "entity":
|
|
108
|
+
return {
|
|
109
|
+
schema: { entity: field.ref, type: "string" },
|
|
110
|
+
optional: field.optional
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function lockEntityOf(entity, description) {
|
|
115
|
+
const declared = fieldsOf(entity.fields, description) ?? {};
|
|
116
|
+
const unique = new Set(entity.options.unique.filter((key) => key !== "id"));
|
|
117
|
+
for (const [key, field] of Object.entries(declared)) {
|
|
118
|
+
if (field.unique)
|
|
119
|
+
unique.add(key);
|
|
120
|
+
}
|
|
121
|
+
const fields = Object.fromEntries(Object.entries(declared).map(([key, field]) => [
|
|
122
|
+
key,
|
|
123
|
+
unique.has(key) ? { ...field, unique: true } : field
|
|
124
|
+
]));
|
|
125
|
+
return {
|
|
126
|
+
schemaVersion: entity.schemaVersion ?? 1,
|
|
127
|
+
previousNames: [...entity.previousNames ?? []].sort(),
|
|
128
|
+
unique: [...unique].sort(),
|
|
129
|
+
fields,
|
|
130
|
+
...entity.upgrade === undefined || entity.upgrade.length === 0 ? {} : { upgrade: [...entity.upgrade].sort((a, b) => a - b) }
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function schemaLockOf2(description) {
|
|
134
|
+
return {
|
|
135
|
+
lockVersion: 1,
|
|
136
|
+
entities: sorted(Object.fromEntries(Object.keys(description.entities).map((name) => [
|
|
137
|
+
name,
|
|
138
|
+
lockEntityOf(description.entities[name], description)
|
|
139
|
+
])))
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
var METADATA = new Set([
|
|
143
|
+
"title",
|
|
144
|
+
"description",
|
|
145
|
+
"examples",
|
|
146
|
+
"default",
|
|
147
|
+
"deprecated",
|
|
148
|
+
"readOnly",
|
|
149
|
+
"writeOnly",
|
|
150
|
+
"$comment"
|
|
151
|
+
]);
|
|
152
|
+
var UPPER = new Set([
|
|
153
|
+
"maximum",
|
|
154
|
+
"exclusiveMaximum",
|
|
155
|
+
"maxLength",
|
|
156
|
+
"maxItems",
|
|
157
|
+
"maxProperties"
|
|
158
|
+
]);
|
|
159
|
+
var LOWER = new Set([
|
|
160
|
+
"minimum",
|
|
161
|
+
"exclusiveMinimum",
|
|
162
|
+
"minLength",
|
|
163
|
+
"minItems",
|
|
164
|
+
"minProperties"
|
|
165
|
+
]);
|
|
166
|
+
var NARROWING = new Set([
|
|
167
|
+
"pattern",
|
|
168
|
+
"format",
|
|
169
|
+
"multipleOf",
|
|
170
|
+
"allOf",
|
|
171
|
+
"const"
|
|
172
|
+
]);
|
|
173
|
+
var same = (a, b) => JSON.stringify(sorted(a)) === JSON.stringify(sorted(b));
|
|
174
|
+
function literalsOf(schema) {
|
|
175
|
+
if (Array.isArray(schema.enum)) {
|
|
176
|
+
return schema.enum.map((value) => JSON.stringify(value));
|
|
177
|
+
}
|
|
178
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.every((item) => isObject(item) && ("const" in item))) {
|
|
179
|
+
return schema.anyOf.map((item) => JSON.stringify(item.const));
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
function compareSchemas(previous, next) {
|
|
184
|
+
if (same(previous, next))
|
|
185
|
+
return "same";
|
|
186
|
+
const strip = (schema) => Object.fromEntries(Object.entries(schema).filter(([key]) => !METADATA.has(key)));
|
|
187
|
+
const [a, b] = [strip(previous), strip(next)];
|
|
188
|
+
if (same(a, b))
|
|
189
|
+
return "metadata";
|
|
190
|
+
if (!same(a.type, b.type) || !same(a.entity, b.entity))
|
|
191
|
+
return "type-changed";
|
|
192
|
+
const [before, after] = [literalsOf(a), literalsOf(b)];
|
|
193
|
+
let loosened = false;
|
|
194
|
+
let tightened = false;
|
|
195
|
+
if (before !== undefined || after !== undefined) {
|
|
196
|
+
if (before === undefined || after === undefined)
|
|
197
|
+
return "type-changed";
|
|
198
|
+
const kept = before.every((value) => after.includes(value));
|
|
199
|
+
const gained = after.some((value) => !before.includes(value));
|
|
200
|
+
if (!kept)
|
|
201
|
+
tightened = true;
|
|
202
|
+
if (gained)
|
|
203
|
+
loosened = true;
|
|
204
|
+
}
|
|
205
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
206
|
+
for (const key of keys) {
|
|
207
|
+
if (["type", "entity", "enum", "anyOf"].includes(key))
|
|
208
|
+
continue;
|
|
209
|
+
if (same(a[key], b[key]))
|
|
210
|
+
continue;
|
|
211
|
+
const [x, y] = [a[key], b[key]];
|
|
212
|
+
if (key === "items") {
|
|
213
|
+
const items = compareSchemas(isObject(x) ? x : {}, isObject(y) ? y : {});
|
|
214
|
+
if (items === "type-changed")
|
|
215
|
+
return "type-changed";
|
|
216
|
+
if (items === "tightened")
|
|
217
|
+
tightened = true;
|
|
218
|
+
if (items === "loosened")
|
|
219
|
+
loosened = true;
|
|
220
|
+
} else if (UPPER.has(key) || LOWER.has(key)) {
|
|
221
|
+
if (x === undefined)
|
|
222
|
+
tightened = true;
|
|
223
|
+
else if (y === undefined)
|
|
224
|
+
loosened = true;
|
|
225
|
+
else if (typeof x !== "number" || typeof y !== "number")
|
|
226
|
+
return "type-changed";
|
|
227
|
+
else if (UPPER.has(key) ? y > x : y < x)
|
|
228
|
+
loosened = true;
|
|
229
|
+
else
|
|
230
|
+
tightened = true;
|
|
231
|
+
} else if (NARROWING.has(key)) {
|
|
232
|
+
if (y === undefined)
|
|
233
|
+
loosened = true;
|
|
234
|
+
else
|
|
235
|
+
tightened = true;
|
|
236
|
+
} else {
|
|
237
|
+
return "type-changed";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (tightened)
|
|
241
|
+
return "tightened";
|
|
242
|
+
return loosened ? "loosened" : "metadata";
|
|
243
|
+
}
|
|
244
|
+
function push(context, change, klass, satisfied, extra = {}) {
|
|
245
|
+
context.changes.push({
|
|
246
|
+
entity: context.entity,
|
|
247
|
+
...extra.key === undefined ? {} : { key: extra.key },
|
|
248
|
+
change,
|
|
249
|
+
class: klass,
|
|
250
|
+
satisfied,
|
|
251
|
+
destructive: extra.destructive ?? false,
|
|
252
|
+
...extra.from === undefined ? {} : { from: extra.from },
|
|
253
|
+
...extra.detail === undefined ? {} : { detail: extra.detail }
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
var STEP = "an upgrade step (bump schemaVersion and add upgrade[n])";
|
|
257
|
+
function diffFields(context, previous, next, path) {
|
|
258
|
+
const join = (key) => path ? `${path}.${key}` : key;
|
|
259
|
+
const renamedFrom = new Map;
|
|
260
|
+
for (const [key, field] of Object.entries(next)) {
|
|
261
|
+
const from = field.renamedFrom;
|
|
262
|
+
if (from !== undefined && !(key in previous) && from in previous && !(from in next)) {
|
|
263
|
+
renamedFrom.set(key, from);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const moved = new Set(renamedFrom.values());
|
|
267
|
+
const keys = [...new Set([...Object.keys(previous), ...Object.keys(next)])].filter((key) => !moved.has(key)).sort();
|
|
268
|
+
for (const key of keys) {
|
|
269
|
+
const was = previous[key];
|
|
270
|
+
const is = next[key];
|
|
271
|
+
if (is === undefined) {
|
|
272
|
+
push(context, "removed", "automatic", true, {
|
|
273
|
+
key: join(key),
|
|
274
|
+
destructive: true,
|
|
275
|
+
detail: "Left out on read; a backfill removes it only with destructive: true. Rename it with renamedFrom to keep the data."
|
|
276
|
+
});
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const from = renamedFrom.get(key);
|
|
280
|
+
if (from !== undefined) {
|
|
281
|
+
push(context, "renamed", "declared", true, {
|
|
282
|
+
key: join(key),
|
|
283
|
+
from: join(from)
|
|
284
|
+
});
|
|
285
|
+
diffField(context, previous[from], is, join(key));
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (was === undefined) {
|
|
289
|
+
if (is.optional) {
|
|
290
|
+
push(context, "added-optional", "automatic", true, { key: join(key) });
|
|
291
|
+
} else {
|
|
292
|
+
const satisfied = "fill" in is || context.stepped;
|
|
293
|
+
push(context, "added-required", "declared", satisfied, {
|
|
294
|
+
key: join(key),
|
|
295
|
+
...satisfied ? {} : {
|
|
296
|
+
detail: `Needs a fill (.with({ fill })) or ${STEP} that sets it.`
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
diffField(context, was, is, join(key));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function diffField(context, was, is, key) {
|
|
306
|
+
const compared = compareSchemas(was.schema, is.schema);
|
|
307
|
+
if (compared === "type-changed") {
|
|
308
|
+
push(context, "type-changed", "declared", context.stepped, {
|
|
309
|
+
key,
|
|
310
|
+
...context.stepped ? {} : { detail: `Needs ${STEP} that converts the stored values.` }
|
|
311
|
+
});
|
|
312
|
+
} else if (compared === "tightened") {
|
|
313
|
+
const satisfied = context.stepped || "fill" in is;
|
|
314
|
+
push(context, "tightened", "declared", satisfied, {
|
|
315
|
+
key,
|
|
316
|
+
detail: satisfied ? "Run app.migrate({ mode: 'check' }) to list the rows that violate it." : `Needs ${STEP} or a fill; app.migrate({ mode: 'check' }) lists the rows that violate it.`
|
|
317
|
+
});
|
|
318
|
+
} else if (compared === "loosened") {
|
|
319
|
+
push(context, "loosened", "automatic", true, { key });
|
|
320
|
+
} else if (compared === "metadata") {
|
|
321
|
+
push(context, "metadata", "automatic", true, { key });
|
|
322
|
+
}
|
|
323
|
+
if (was.optional && !is.optional) {
|
|
324
|
+
const satisfied = context.stepped || "fill" in is;
|
|
325
|
+
push(context, "made-required", "declared", satisfied, {
|
|
326
|
+
key,
|
|
327
|
+
...satisfied ? {} : {
|
|
328
|
+
detail: `Needs a fill (.with({ fill })) or ${STEP} for the rows without it.`
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
} else if (!was.optional && is.optional) {
|
|
332
|
+
push(context, "made-optional", "automatic", true, { key });
|
|
333
|
+
}
|
|
334
|
+
if (!was.unique && is.unique) {
|
|
335
|
+
push(context, "unique-added", "check", true, {
|
|
336
|
+
key,
|
|
337
|
+
detail: "Run app.migrate({ mode: 'check' }) and look for duplicates before it is enforced."
|
|
338
|
+
});
|
|
339
|
+
} else if (was.unique && !is.unique) {
|
|
340
|
+
push(context, "unique-removed", "automatic", true, { key });
|
|
341
|
+
}
|
|
342
|
+
if (was.fields !== undefined || is.fields !== undefined) {
|
|
343
|
+
diffFields(context, was.fields ?? {}, is.fields ?? {}, key);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function diffEntity(entity, from, to, changes) {
|
|
347
|
+
const stepped = to.schemaVersion > from.schemaVersion && (to.upgrade ?? []).some((version) => version > from.schemaVersion && version <= to.schemaVersion);
|
|
348
|
+
const context = { entity, from, to, stepped, changes };
|
|
349
|
+
diffFields(context, from.fields, to.fields, "");
|
|
350
|
+
}
|
|
351
|
+
function diffSchemas2(lock, current) {
|
|
352
|
+
const changes = [];
|
|
353
|
+
const claimed = new Set;
|
|
354
|
+
for (const name of Object.keys(current.entities).sort()) {
|
|
355
|
+
const to = current.entities[name];
|
|
356
|
+
let from = lock.entities[name];
|
|
357
|
+
if (from === undefined) {
|
|
358
|
+
const previous = to.previousNames.find((old) => lock.entities[old] !== undefined && !current.entities[old]);
|
|
359
|
+
if (previous === undefined) {
|
|
360
|
+
changes.push({
|
|
361
|
+
entity: name,
|
|
362
|
+
change: "entity-added",
|
|
363
|
+
class: "automatic",
|
|
364
|
+
satisfied: true,
|
|
365
|
+
destructive: false
|
|
366
|
+
});
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
claimed.add(previous);
|
|
370
|
+
from = lock.entities[previous];
|
|
371
|
+
changes.push({
|
|
372
|
+
entity: name,
|
|
373
|
+
change: "entity-renamed",
|
|
374
|
+
class: "declared",
|
|
375
|
+
satisfied: true,
|
|
376
|
+
destructive: true,
|
|
377
|
+
from: previous,
|
|
378
|
+
detail: "app.migrate moves its rows; it removes them from the previous name only with destructive: true."
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
diffEntity(name, from, to, changes);
|
|
382
|
+
}
|
|
383
|
+
for (const name of Object.keys(lock.entities).sort()) {
|
|
384
|
+
if (current.entities[name] !== undefined || claimed.has(name))
|
|
385
|
+
continue;
|
|
386
|
+
changes.push({
|
|
387
|
+
entity: name,
|
|
388
|
+
change: "entity-removed",
|
|
389
|
+
class: "declared",
|
|
390
|
+
satisfied: false,
|
|
391
|
+
destructive: true,
|
|
392
|
+
detail: `No entity is named "${name}": list it in the previousNames of the entity that replaces it, or remove it from the lock if it is gone.`
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return changes;
|
|
396
|
+
}
|
|
397
|
+
function schemaLockProblemsOf2(lock, current) {
|
|
398
|
+
const problems = [];
|
|
399
|
+
const renamed = new Map;
|
|
400
|
+
for (const [name, entity] of Object.entries(current.entities)) {
|
|
401
|
+
for (const old of entity.previousNames) {
|
|
402
|
+
if (!renamed.has(old))
|
|
403
|
+
renamed.set(old, name);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
for (const name of Object.keys(lock.entities).sort()) {
|
|
407
|
+
const locked = lock.entities[name];
|
|
408
|
+
const now = current.entities[name] === undefined ? renamed.get(name) : name;
|
|
409
|
+
const entity = now === undefined ? undefined : current.entities[now];
|
|
410
|
+
if (entity === undefined) {
|
|
411
|
+
problems.push(`The lock holds the entity "${name}", which no entity is named or lists in previousNames.`);
|
|
412
|
+
} else if (entity.schemaVersion < locked.schemaVersion) {
|
|
413
|
+
problems.push(`The schemaVersion of "${now}" (${entity.schemaVersion}) is lower than the lock's (${locked.schemaVersion}).`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return problems;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/core/output/status.ts
|
|
420
|
+
var INVALID_INPUT = new Set([
|
|
421
|
+
"InvalidUseCaseInput",
|
|
422
|
+
"InvalidUseCaseContext",
|
|
423
|
+
"InvalidCommandContext"
|
|
424
|
+
]);
|
|
425
|
+
var CONFIGURATION = new Set(["InvalidCommandEnvironment"]);
|
|
426
|
+
var NOT_FOUND = new Set([
|
|
427
|
+
"RepositoryEntityNotFound",
|
|
428
|
+
"UnknownCommandOperation",
|
|
429
|
+
"UseCaseEntityNotFound"
|
|
430
|
+
]);
|
|
431
|
+
var CONFLICT = new Set([
|
|
432
|
+
"RepositoryUniqueViolation",
|
|
433
|
+
"RepositoryConcurrencyConflict",
|
|
434
|
+
"UseCaseEntityConflict"
|
|
435
|
+
]);
|
|
436
|
+
function raisedOf(error) {
|
|
437
|
+
let raised = error;
|
|
438
|
+
for (let current = error.cause;current instanceof Exception2; current = current.cause) {
|
|
439
|
+
if (current.name === error.name)
|
|
440
|
+
raised = current;
|
|
441
|
+
}
|
|
442
|
+
return raised;
|
|
443
|
+
}
|
|
444
|
+
function statusOf2(error, operation, statuses) {
|
|
445
|
+
if (!(error instanceof Exception2))
|
|
446
|
+
return 500;
|
|
447
|
+
const { name } = error;
|
|
448
|
+
if (statuses && Object.hasOwn(statuses, name))
|
|
449
|
+
return statuses[name];
|
|
450
|
+
if (INVALID_INPUT.has(name))
|
|
451
|
+
return 400;
|
|
452
|
+
if (CONFIGURATION.has(name))
|
|
453
|
+
return 500;
|
|
454
|
+
if (Array.isArray(raisedOf(error).details?.issues)) {
|
|
455
|
+
return 400;
|
|
456
|
+
}
|
|
457
|
+
if (NOT_FOUND.has(name))
|
|
458
|
+
return 404;
|
|
459
|
+
if (CONFLICT.has(name))
|
|
460
|
+
return 409;
|
|
461
|
+
if (name === "UseCaseInvalidCredentials" || name === "Unauthenticated") {
|
|
462
|
+
return 401;
|
|
463
|
+
}
|
|
464
|
+
if (name === "Forbidden")
|
|
465
|
+
return 403;
|
|
466
|
+
if (operation?.errors.some((Declared) => error.is(Declared) || Declared.name === name)) {
|
|
467
|
+
return 422;
|
|
468
|
+
}
|
|
469
|
+
return 500;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/core/output/problem.ts
|
|
473
|
+
var PROBLEM_MEDIA_TYPE2 = "application/problem+json";
|
|
474
|
+
var INTERNAL = "Internal error.";
|
|
475
|
+
function problemOf2(error, options) {
|
|
476
|
+
const { status, debug = false } = options;
|
|
477
|
+
const exception = error instanceof Exception2 ? error : undefined;
|
|
478
|
+
const title = exception?.name ?? "UnexpectedError";
|
|
479
|
+
const message = exception?.message ?? (error instanceof Error ? error.message : String(error));
|
|
480
|
+
const raised = exception && raisedOf(exception);
|
|
481
|
+
const rawIssues = raised?.details?.issues;
|
|
482
|
+
const issues = status === 400 && Array.isArray(rawIssues) ? rawIssues.map((issue) => ({
|
|
483
|
+
path: [...issue.path],
|
|
484
|
+
code: issue.code,
|
|
485
|
+
message: issue.message
|
|
486
|
+
})) : undefined;
|
|
487
|
+
return {
|
|
488
|
+
type: `urn:afora:exception:${title}`,
|
|
489
|
+
title,
|
|
490
|
+
status,
|
|
491
|
+
detail: status >= 500 && !debug ? INTERNAL : message,
|
|
492
|
+
...raised ? { layer: raised.layer } : {},
|
|
493
|
+
...issues ? { issues } : {},
|
|
494
|
+
...debug ? { trace: exception ? exception.toString() : String(error) } : {}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function outputValueOf2(value) {
|
|
498
|
+
const text = JSON.stringify(value);
|
|
499
|
+
return text === undefined ? undefined : JSON.parse(text);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/core/output/openapi-sink.ts
|
|
503
|
+
var METHODS = ["get", "post", "put", "patch", "delete"];
|
|
504
|
+
var SUCCESS_STATUSES = [200, 201, 202, 204];
|
|
505
|
+
var REQUEST_ID_HEADER = "x-request-id";
|
|
506
|
+
function isObject2(value) {
|
|
507
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
508
|
+
}
|
|
509
|
+
function onlyKeys(value, allowed, where) {
|
|
510
|
+
for (const key of Object.keys(value)) {
|
|
511
|
+
if (!allowed.includes(key))
|
|
512
|
+
return `${where} has no option "${key}".`;
|
|
513
|
+
}
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
var COMPONENT_NAME = /^[A-Za-z0-9._-]+$/;
|
|
517
|
+
function isHeaderValue(value) {
|
|
518
|
+
if (typeof value !== "string")
|
|
519
|
+
return false;
|
|
520
|
+
for (let i = 0;i < value.length; i++) {
|
|
521
|
+
const code = value.charCodeAt(i);
|
|
522
|
+
if (code < 32 && code !== 9 || code === 127)
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
function isScopes(value) {
|
|
528
|
+
return value === undefined || Array.isArray(value) && value.every((scope) => typeof scope === "string" && scope !== "");
|
|
529
|
+
}
|
|
530
|
+
var SCHEME_KEYS = {
|
|
531
|
+
http: ["scheme", "bearerFormat"],
|
|
532
|
+
apiKey: ["name", "in"],
|
|
533
|
+
oauth2: ["flows"],
|
|
534
|
+
openIdConnect: ["openIdConnectUrl"],
|
|
535
|
+
mutualTLS: []
|
|
536
|
+
};
|
|
537
|
+
function schemeIssue(scheme, where) {
|
|
538
|
+
if (!isObject2(scheme) || typeof scheme.type !== "string") {
|
|
539
|
+
return `Expected ${where} to be a security scheme with a "type".`;
|
|
540
|
+
}
|
|
541
|
+
const keys = Object.hasOwn(SCHEME_KEYS, scheme.type) ? SCHEME_KEYS[scheme.type] : undefined;
|
|
542
|
+
if (!keys) {
|
|
543
|
+
return `Expected ${where}.type to be one of ${Object.keys(SCHEME_KEYS).join(", ")}.`;
|
|
544
|
+
}
|
|
545
|
+
const bad = onlyKeys(scheme, ["type", "description", ...keys], where);
|
|
546
|
+
if (bad)
|
|
547
|
+
return bad;
|
|
548
|
+
if (scheme.description !== undefined && typeof scheme.description !== "string") {
|
|
549
|
+
return `Expected ${where}.description to be a string.`;
|
|
550
|
+
}
|
|
551
|
+
switch (scheme.type) {
|
|
552
|
+
case "http":
|
|
553
|
+
if (typeof scheme.scheme !== "string" || !scheme.scheme) {
|
|
554
|
+
return `Expected ${where}.scheme to be a scheme such as "bearer".`;
|
|
555
|
+
}
|
|
556
|
+
if (scheme.bearerFormat !== undefined && typeof scheme.bearerFormat !== "string") {
|
|
557
|
+
return `Expected ${where}.bearerFormat to be a string.`;
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
case "apiKey":
|
|
561
|
+
if (typeof scheme.name !== "string" || !scheme.name) {
|
|
562
|
+
return `Expected ${where}.name to be a non-empty string.`;
|
|
563
|
+
}
|
|
564
|
+
if (!["header", "query", "cookie"].includes(scheme.in)) {
|
|
565
|
+
return `Expected ${where}.in to be "header", "query" or "cookie".`;
|
|
566
|
+
}
|
|
567
|
+
return;
|
|
568
|
+
case "oauth2":
|
|
569
|
+
return isObject2(scheme.flows) ? undefined : `Expected ${where}.flows to be an object.`;
|
|
570
|
+
case "openIdConnect":
|
|
571
|
+
return typeof scheme.openIdConnectUrl === "string" && scheme.openIdConnectUrl ? undefined : `Expected ${where}.openIdConnectUrl to be a URL.`;
|
|
572
|
+
default:
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function requirementIssue(requirement, schemes, where) {
|
|
577
|
+
if (!isObject2(requirement) || onlyKeys(requirement, ["scheme", "scopes"], where) || typeof requirement.scheme !== "string" || !isScopes(requirement.scopes)) {
|
|
578
|
+
return `Expected ${where} to be { scheme, scopes? }.`;
|
|
579
|
+
}
|
|
580
|
+
if (!Object.hasOwn(schemes, requirement.scheme)) {
|
|
581
|
+
return `${where}.scheme names the unknown security scheme "${requirement.scheme}".`;
|
|
582
|
+
}
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
function securityIssue(security, where) {
|
|
586
|
+
if (!isObject2(security))
|
|
587
|
+
return `Expected ${where} to be an object.`;
|
|
588
|
+
const bad = onlyKeys(security, ["schemes", "operations", "default"], where);
|
|
589
|
+
if (bad)
|
|
590
|
+
return bad;
|
|
591
|
+
const { schemes, operations } = security;
|
|
592
|
+
if (!isObject2(schemes))
|
|
593
|
+
return `Expected ${where}.schemes to be an object.`;
|
|
594
|
+
for (const [name, scheme] of Object.entries(schemes)) {
|
|
595
|
+
if (!COMPONENT_NAME.test(name)) {
|
|
596
|
+
return `Expected the security scheme "${name}" to be named with letters, digits, ".", "_" or "-".`;
|
|
597
|
+
}
|
|
598
|
+
const schemeBad = schemeIssue(scheme, `${where}.schemes["${name}"]`);
|
|
599
|
+
if (schemeBad)
|
|
600
|
+
return schemeBad;
|
|
601
|
+
}
|
|
602
|
+
if (operations !== undefined) {
|
|
603
|
+
if (!isObject2(operations)) {
|
|
604
|
+
return `Expected ${where}.operations to be an object.`;
|
|
605
|
+
}
|
|
606
|
+
for (const [id, requirement] of Object.entries(operations)) {
|
|
607
|
+
if (requirement === false)
|
|
608
|
+
continue;
|
|
609
|
+
const requirementBad = requirementIssue(requirement, schemes, `${where}.operations["${id}"]`);
|
|
610
|
+
if (requirementBad)
|
|
611
|
+
return requirementBad;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (security.default !== undefined) {
|
|
615
|
+
return requirementIssue(security.default, schemes, `${where}.default`);
|
|
616
|
+
}
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
var ROUTE_OPTION_KEYS2 = [
|
|
620
|
+
"basePath",
|
|
621
|
+
"route",
|
|
622
|
+
"responses",
|
|
623
|
+
"requestId",
|
|
624
|
+
"security"
|
|
625
|
+
];
|
|
626
|
+
function routeOptionsIssue2(options, where) {
|
|
627
|
+
const { basePath, route, responses, requestId, security } = options;
|
|
628
|
+
if (basePath !== undefined && (typeof basePath !== "string" || basePath !== "" && !isRoutePath(basePath, false))) {
|
|
629
|
+
return `Expected ${where}.basePath to be "" or a path such as "/api".`;
|
|
630
|
+
}
|
|
631
|
+
if (route !== undefined) {
|
|
632
|
+
if (!isObject2(route))
|
|
633
|
+
return `Expected ${where}.route to be an object.`;
|
|
634
|
+
for (const [id, entry] of Object.entries(route)) {
|
|
635
|
+
if (!isObject2(entry) || onlyKeys(entry, ["method", "path", "status"], "route")) {
|
|
636
|
+
return `Expected ${where}.route["${id}"] to be { method?, path?, status? }.`;
|
|
637
|
+
}
|
|
638
|
+
if (entry.method !== undefined && !METHODS.includes(entry.method)) {
|
|
639
|
+
return `Expected ${where}.route["${id}"].method to be one of ${METHODS.join(", ")}.`;
|
|
640
|
+
}
|
|
641
|
+
if (entry.path !== undefined) {
|
|
642
|
+
if (!isRoutePath(entry.path)) {
|
|
643
|
+
return `Expected ${where}.route["${id}"].path to start with "/", with parameters only as whole "{name}" segments and no query.`;
|
|
644
|
+
}
|
|
645
|
+
const names = parametersOfPath(entry.path);
|
|
646
|
+
if (new Set(names).size !== names.length) {
|
|
647
|
+
return `Expected ${where}.route["${id}"].path to name each parameter once.`;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (entry.status !== undefined && !SUCCESS_STATUSES.includes(entry.status)) {
|
|
651
|
+
return `Expected ${where}.route["${id}"].status to be one of ${SUCCESS_STATUSES.join(", ")}.`;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (responses !== undefined) {
|
|
656
|
+
if (!isObject2(responses)) {
|
|
657
|
+
return `Expected ${where}.responses to be an object.`;
|
|
658
|
+
}
|
|
659
|
+
for (const [id, entry] of Object.entries(responses)) {
|
|
660
|
+
const at = `${where}.responses["${id}"]`;
|
|
661
|
+
if (!isObject2(entry) || onlyKeys(entry, ["headers"], at) || entry.headers !== undefined && !isObject2(entry.headers)) {
|
|
662
|
+
return `Expected ${at} to be { headers?: { name: value } }.`;
|
|
663
|
+
}
|
|
664
|
+
for (const [name, value] of Object.entries(entry.headers ?? {})) {
|
|
665
|
+
if (!HEADER_NAME.test(name)) {
|
|
666
|
+
return `Expected ${at}.headers to be named by header names, not "${name}".`;
|
|
667
|
+
}
|
|
668
|
+
if (name.toLowerCase() === "content-type") {
|
|
669
|
+
return `${at}.headers cannot set "content-type": the sink sets it.`;
|
|
670
|
+
}
|
|
671
|
+
if (!isHeaderValue(value)) {
|
|
672
|
+
return `Expected ${at}.headers["${name}"] to be text without line breaks.`;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (requestId !== undefined && requestId !== true && (typeof requestId !== "string" || !HEADER_NAME.test(requestId))) {
|
|
678
|
+
return `Expected ${where}.requestId to be true or a header name.`;
|
|
679
|
+
}
|
|
680
|
+
if (security !== undefined) {
|
|
681
|
+
return securityIssue(security, `${where}.security`);
|
|
682
|
+
}
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
function requestIdHeaderOf2(options) {
|
|
686
|
+
return options.requestId === true ? REQUEST_ID_HEADER : options.requestId ?? undefined;
|
|
687
|
+
}
|
|
688
|
+
function validate(options) {
|
|
689
|
+
const bad = onlyKeys(options, ["info", "servers", ...ROUTE_OPTION_KEYS2], "openapi");
|
|
690
|
+
if (bad)
|
|
691
|
+
return bad;
|
|
692
|
+
const { info, servers } = options;
|
|
693
|
+
if (!isObject2(info))
|
|
694
|
+
return "Expected openapi.info to be an object.";
|
|
695
|
+
const infoBad = onlyKeys(info, ["title", "version", "description"], "info");
|
|
696
|
+
if (infoBad)
|
|
697
|
+
return infoBad;
|
|
698
|
+
for (const key of ["title", "version"]) {
|
|
699
|
+
if (typeof info[key] !== "string" || !info[key]) {
|
|
700
|
+
return `Expected openapi.info.${key} to be a non-empty string.`;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (info.description !== undefined && typeof info.description !== "string") {
|
|
704
|
+
return "Expected openapi.info.description to be a string.";
|
|
705
|
+
}
|
|
706
|
+
if (servers !== undefined) {
|
|
707
|
+
if (!Array.isArray(servers))
|
|
708
|
+
return "Expected openapi.servers to be a list.";
|
|
709
|
+
for (const server of servers) {
|
|
710
|
+
if (!isObject2(server) || typeof server.url !== "string" || !server.url || server.description !== undefined && typeof server.description !== "string" || onlyKeys(server, ["url", "description"], "server")) {
|
|
711
|
+
return "Expected each openapi.servers entry to be { url, description? }.";
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return routeOptionsIssue2(options, "openapi");
|
|
716
|
+
}
|
|
717
|
+
function invalid(reason, source = "openapi") {
|
|
718
|
+
throw new InvalidOutputStage2({ source, details: { reason } });
|
|
719
|
+
}
|
|
720
|
+
function kebab(text) {
|
|
721
|
+
return text.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
722
|
+
}
|
|
723
|
+
function isScalarKey2(operation, name) {
|
|
724
|
+
return isScalarKey(operation.input, name);
|
|
725
|
+
}
|
|
726
|
+
function operationRoutes2(operations, options, source = "openapi") {
|
|
727
|
+
const base = options.basePath ?? "";
|
|
728
|
+
const route = options.route ?? {};
|
|
729
|
+
const responses = options.responses ?? {};
|
|
730
|
+
const ids = new Set(operations.map(({ id }) => id));
|
|
731
|
+
for (const [where, entries] of [
|
|
732
|
+
["route", route],
|
|
733
|
+
["responses", responses],
|
|
734
|
+
["security.operations", options.security?.operations ?? {}]
|
|
735
|
+
]) {
|
|
736
|
+
for (const id of Object.keys(entries)) {
|
|
737
|
+
if (!ids.has(id)) {
|
|
738
|
+
invalid(`${source}.${where} names the unknown operation "${id}".`, source);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const routes = new Map;
|
|
743
|
+
const taken = new Map;
|
|
744
|
+
for (const operation of operations) {
|
|
745
|
+
const chosen = route[operation.id] ?? {};
|
|
746
|
+
const declared = operation.route;
|
|
747
|
+
const method = chosen.method ?? declared?.method ?? "post";
|
|
748
|
+
const path = base + (chosen.path ?? declared?.path ?? `/${kebab(operation.command)}/${kebab(operation.name)}`);
|
|
749
|
+
const parameters = parametersOfPath(path);
|
|
750
|
+
const query = declared?.query ?? [];
|
|
751
|
+
const requestHeaders = declared?.headers ?? {};
|
|
752
|
+
const cookies = declared?.cookie ?? {};
|
|
753
|
+
for (const name of parameters) {
|
|
754
|
+
if (!isScalarKey2(operation, name)) {
|
|
755
|
+
invalid(`${source}.route["${operation.id}"].path has the parameter "{${name}}", which is not a scalar key of the input of ${operation.id}.`, source);
|
|
756
|
+
}
|
|
757
|
+
if (query.includes(name) || Object.hasOwn(requestHeaders, name) || Object.hasOwn(cookies, name)) {
|
|
758
|
+
invalid(`${source}.route["${operation.id}"].path has the parameter "{${name}}", which the route of ${operation.useCase} reads from another place.`, source);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const key = `${method} ${path.replace(/\{[^}]*\}/g, "{}")}`;
|
|
762
|
+
const other = taken.get(key);
|
|
763
|
+
if (other !== undefined) {
|
|
764
|
+
invalid(`Two operations are routed to ${method.toUpperCase()} ${path}: ${other} and ${operation.id}. Give one another route, on its use case or in ${source}.route["${operation.id}"].`, source);
|
|
765
|
+
}
|
|
766
|
+
taken.set(key, operation.id);
|
|
767
|
+
routes.set(operation.id, {
|
|
768
|
+
method,
|
|
769
|
+
path,
|
|
770
|
+
parameters,
|
|
771
|
+
status: chosen.status ?? declared?.status ?? 200,
|
|
772
|
+
headers: { ...responses[operation.id]?.headers ?? {} },
|
|
773
|
+
body: method !== "get" && method !== "delete",
|
|
774
|
+
query,
|
|
775
|
+
requestHeaders,
|
|
776
|
+
cookies,
|
|
777
|
+
setCookies: declared?.setCookie ?? {}
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
return routes;
|
|
781
|
+
}
|
|
782
|
+
function collectRecords(properties, found) {
|
|
783
|
+
const visit = (field) => {
|
|
784
|
+
if (isBlueprintList(field)) {
|
|
785
|
+
visit(field.item);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (isBlueprint(field)) {
|
|
789
|
+
collectRecords(field.properties, found);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (isBlueprintVariant(field)) {
|
|
793
|
+
for (const variant of Object.values(field.variants)) {
|
|
794
|
+
visit(variant);
|
|
795
|
+
}
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const record = recordFieldOf(field)?.record;
|
|
799
|
+
if (record && !found.has(record)) {
|
|
800
|
+
found.add(record);
|
|
801
|
+
collectRecords(record.blueprint.properties, found);
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
for (const field of Object.values(properties))
|
|
805
|
+
visit(field);
|
|
806
|
+
}
|
|
807
|
+
function problemSchema() {
|
|
808
|
+
return {
|
|
809
|
+
type: "object",
|
|
810
|
+
properties: {
|
|
811
|
+
type: { type: "string" },
|
|
812
|
+
title: { type: "string" },
|
|
813
|
+
status: { type: "integer" },
|
|
814
|
+
detail: { type: "string" },
|
|
815
|
+
layer: { type: "string" },
|
|
816
|
+
issues: {
|
|
817
|
+
type: "array",
|
|
818
|
+
items: {
|
|
819
|
+
type: "object",
|
|
820
|
+
properties: {
|
|
821
|
+
path: {
|
|
822
|
+
type: "array",
|
|
823
|
+
items: { type: ["string", "integer"] }
|
|
824
|
+
},
|
|
825
|
+
code: { type: "string" },
|
|
826
|
+
message: { type: "string" }
|
|
827
|
+
},
|
|
828
|
+
required: ["path", "code", "message"]
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
trace: { type: "string" }
|
|
832
|
+
},
|
|
833
|
+
required: ["type", "title", "status", "detail"]
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
var PROBLEM_REF = "#/components/schemas/Problem";
|
|
837
|
+
var problemContent = {
|
|
838
|
+
[PROBLEM_MEDIA_TYPE2]: { schema: { $ref: PROBLEM_REF } }
|
|
839
|
+
};
|
|
840
|
+
var STATUS_TEXT = {
|
|
841
|
+
400: "Invalid input or context.",
|
|
842
|
+
401: "Authentication is required.",
|
|
843
|
+
403: "The caller may not do this.",
|
|
844
|
+
404: "Not found.",
|
|
845
|
+
409: "Conflict.",
|
|
846
|
+
422: "Declared error of the operation.",
|
|
847
|
+
500: "Internal error."
|
|
848
|
+
};
|
|
849
|
+
var SUCCESS_TEXT = {
|
|
850
|
+
200: "Success.",
|
|
851
|
+
201: "Created.",
|
|
852
|
+
202: "Accepted.",
|
|
853
|
+
204: "No content."
|
|
854
|
+
};
|
|
855
|
+
function responsesOf(operation, statuses, response, route, requestIdHeader) {
|
|
856
|
+
const names = new Map([
|
|
857
|
+
[400, []],
|
|
858
|
+
[500, []]
|
|
859
|
+
]);
|
|
860
|
+
const access = operation.access;
|
|
861
|
+
if (access && access.public !== true) {
|
|
862
|
+
names.set(401, []);
|
|
863
|
+
if ((access.scopes?.length ?? 0) > 0 || access.owner)
|
|
864
|
+
names.set(403, []);
|
|
865
|
+
}
|
|
866
|
+
for (const Declared of operation.errors) {
|
|
867
|
+
const synthetic = new Exception2({
|
|
868
|
+
layer: "use-case" /* UseCase */,
|
|
869
|
+
name: Declared.name,
|
|
870
|
+
source: "openapi",
|
|
871
|
+
message: Declared.name
|
|
872
|
+
});
|
|
873
|
+
const status = statusOf2(synthetic, operation, statuses);
|
|
874
|
+
const list = names.get(status) ?? [];
|
|
875
|
+
if (!list.includes(Declared.name))
|
|
876
|
+
list.push(Declared.name);
|
|
877
|
+
names.set(status, list);
|
|
878
|
+
}
|
|
879
|
+
const idHeaders = requestIdHeader ? {
|
|
880
|
+
[requestIdHeader]: {
|
|
881
|
+
description: "The id of the request: the one it sent in this header, else a new one.",
|
|
882
|
+
schema: { type: "string" }
|
|
883
|
+
}
|
|
884
|
+
} : {};
|
|
885
|
+
const successHeaders = { ...idHeaders };
|
|
886
|
+
const setCookies = Object.values(route.setCookies);
|
|
887
|
+
if (setCookies.length > 0) {
|
|
888
|
+
successHeaders["set-cookie"] = {
|
|
889
|
+
description: `Sets ${setCookies.map(({ name }) => `\`${name}\``).join(", ")}.`,
|
|
890
|
+
schema: { type: "string" }
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
for (const [name, value] of Object.entries(route.headers)) {
|
|
894
|
+
successHeaders[name] = { schema: { type: "string", const: value } };
|
|
895
|
+
}
|
|
896
|
+
const withHeaders = (headers) => Object.keys(headers).length > 0 ? { headers } : {};
|
|
897
|
+
const responses = {
|
|
898
|
+
[String(route.status)]: {
|
|
899
|
+
description: SUCCESS_TEXT[route.status],
|
|
900
|
+
...withHeaders(successHeaders),
|
|
901
|
+
...route.status === 204 ? {} : { content: { "application/json": { schema: response } } }
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
for (const status of [...names.keys()].sort((a, b) => a - b)) {
|
|
905
|
+
const declared = names.get(status) ?? [];
|
|
906
|
+
const text = STATUS_TEXT[status] ?? `Error ${status}.`;
|
|
907
|
+
responses[String(status)] = {
|
|
908
|
+
description: declared.length > 0 ? `${text} Declared: ${declared.join(", ")}.` : text,
|
|
909
|
+
...withHeaders(idHeaders),
|
|
910
|
+
content: problemContent
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
return responses;
|
|
914
|
+
}
|
|
915
|
+
function parameterOf(name, property, where, required) {
|
|
916
|
+
const { description, ...rest } = property;
|
|
917
|
+
return {
|
|
918
|
+
name,
|
|
919
|
+
in: where,
|
|
920
|
+
required: where === "path" || required,
|
|
921
|
+
...typeof description === "string" ? { description } : {},
|
|
922
|
+
schema: rest
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
function queryParametersOf(operation, schema) {
|
|
926
|
+
const required = new Set(schema.required ?? []);
|
|
927
|
+
return Object.entries(schema.properties ?? {}).map(([name, property]) => {
|
|
928
|
+
if (!isProperty(operation.input.properties[name]) || !isScalarSchema(property)) {
|
|
929
|
+
invalid(`${operation.id} uses a get/delete route, whose input becomes query parameters, but "${name}" is not a scalar; route it with method "post".`);
|
|
930
|
+
}
|
|
931
|
+
return parameterOf(name, property, "query", required.has(name));
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
function withoutKeys(schema, keys) {
|
|
935
|
+
if (keys.length === 0)
|
|
936
|
+
return schema;
|
|
937
|
+
const properties = { ...schema.properties ?? {} };
|
|
938
|
+
for (const key of keys)
|
|
939
|
+
delete properties[key];
|
|
940
|
+
const required = (schema.required ?? []).filter((key) => !keys.includes(key));
|
|
941
|
+
const { required: _, ...rest } = schema;
|
|
942
|
+
return {
|
|
943
|
+
...rest,
|
|
944
|
+
properties,
|
|
945
|
+
...required.length > 0 ? { required } : {}
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function requirementOf(requirement) {
|
|
949
|
+
return { [requirement.scheme]: [...requirement.scopes ?? []] };
|
|
950
|
+
}
|
|
951
|
+
var openapi2 = sinkOf2({
|
|
952
|
+
name: "openapi",
|
|
953
|
+
validate,
|
|
954
|
+
build: (operations, options, { output }) => {
|
|
955
|
+
const routes = operationRoutes2(operations, options);
|
|
956
|
+
const requestIdHeader = requestIdHeaderOf2(options);
|
|
957
|
+
const security = options.security;
|
|
958
|
+
const records = new Set;
|
|
959
|
+
for (const operation of operations) {
|
|
960
|
+
collectRecords(operation.input.properties, records);
|
|
961
|
+
collectRecords(operation.response.properties, records);
|
|
962
|
+
}
|
|
963
|
+
const models = {};
|
|
964
|
+
const classes = new Map;
|
|
965
|
+
for (const record of records) {
|
|
966
|
+
const name = recordNameOf2(record);
|
|
967
|
+
if (!COMPONENT_NAME.test(name) || name === "Problem") {
|
|
968
|
+
invalid(`The record class "${name}" cannot be a component name; give the class a name that is not "Problem".`);
|
|
969
|
+
}
|
|
970
|
+
if (classes.has(name)) {
|
|
971
|
+
invalid(`Two record classes are named "${name}"; rename one.`);
|
|
972
|
+
}
|
|
973
|
+
classes.set(name, record);
|
|
974
|
+
models[name] = record;
|
|
975
|
+
}
|
|
976
|
+
for (const operation of operations) {
|
|
977
|
+
models[`${operation.id}#input`] = operation.input;
|
|
978
|
+
models[`${operation.id}#response`] = operation.response;
|
|
979
|
+
}
|
|
980
|
+
const { schemas } = toOpenAPI2(models);
|
|
981
|
+
const paths = {};
|
|
982
|
+
const operationIds = new Set;
|
|
983
|
+
const components = {};
|
|
984
|
+
for (const name of classes.keys())
|
|
985
|
+
components[name] = schemas[name];
|
|
986
|
+
components.Problem = problemSchema();
|
|
987
|
+
for (const operation of operations) {
|
|
988
|
+
const route = routes.get(operation.id);
|
|
989
|
+
const { method, path } = route;
|
|
990
|
+
const operationId = operation.id.replaceAll(".", "_");
|
|
991
|
+
if (operationIds.has(operationId)) {
|
|
992
|
+
invalid(`The operationId "${operationId}" is produced twice.`);
|
|
993
|
+
}
|
|
994
|
+
operationIds.add(operationId);
|
|
995
|
+
const slot = paths[path] ?? {};
|
|
996
|
+
paths[path] = slot;
|
|
997
|
+
const full = schemas[`${operation.id}#input`];
|
|
998
|
+
const required = new Set(full.required ?? []);
|
|
999
|
+
const hasBody = route.body;
|
|
1000
|
+
const input = withoutKeys(full, [
|
|
1001
|
+
...route.parameters,
|
|
1002
|
+
...hasBody ? route.query : [],
|
|
1003
|
+
...Object.keys(route.requestHeaders),
|
|
1004
|
+
...Object.keys(route.cookies)
|
|
1005
|
+
]);
|
|
1006
|
+
const response = withoutKeys(schemas[`${operation.id}#response`], Object.keys(route.setCookies));
|
|
1007
|
+
const hasInput = Object.keys(input.properties ?? {}).length > 0;
|
|
1008
|
+
const placed = (keys, where) => Object.entries(keys).map(([key, name]) => parameterOf(name, full.properties?.[key], where, required.has(key)));
|
|
1009
|
+
const parameters = [
|
|
1010
|
+
...route.parameters.map((name) => parameterOf(name, full.properties?.[name], "path", true)),
|
|
1011
|
+
...hasBody ? route.query.map((name) => parameterOf(name, full.properties?.[name], "query", required.has(name))) : queryParametersOf(operation, input),
|
|
1012
|
+
...placed(route.requestHeaders, "header"),
|
|
1013
|
+
...placed(route.cookies, "cookie")
|
|
1014
|
+
];
|
|
1015
|
+
const explicit = security?.operations?.[operation.id];
|
|
1016
|
+
const access = operation.access;
|
|
1017
|
+
const guarded = access !== undefined && access.public !== true;
|
|
1018
|
+
if (guarded && explicit === false) {
|
|
1019
|
+
invalid(`openapi.security.operations["${operation.id}"] is false, but ${operation.useCase} declares an access that the output enforces.`);
|
|
1020
|
+
}
|
|
1021
|
+
const schemeNames = Object.keys(security?.schemes ?? {});
|
|
1022
|
+
const scheme = security?.default?.scheme ?? (schemeNames.length === 1 ? schemeNames[0] : undefined);
|
|
1023
|
+
const requirement = explicit !== undefined ? explicit : guarded && scheme !== undefined ? { scheme, scopes: access.scopes ?? [] } : access?.public === true && security?.default ? false : undefined;
|
|
1024
|
+
slot[method] = {
|
|
1025
|
+
operationId,
|
|
1026
|
+
summary: operation.id,
|
|
1027
|
+
...operation.description ? { description: operation.description } : {},
|
|
1028
|
+
...operation.tags.length > 0 ? { tags: [...operation.tags] } : {},
|
|
1029
|
+
...operation.deprecated ? { deprecated: true } : {},
|
|
1030
|
+
...!hasBody || parameters.length > 0 ? { parameters } : {},
|
|
1031
|
+
...hasBody && hasInput ? {
|
|
1032
|
+
requestBody: {
|
|
1033
|
+
required: true,
|
|
1034
|
+
content: { "application/json": { schema: input } }
|
|
1035
|
+
}
|
|
1036
|
+
} : {},
|
|
1037
|
+
responses: responsesOf(operation, output.statuses, response, route, requestIdHeader),
|
|
1038
|
+
...requirement === false ? { security: [] } : requirement ? { security: [requirementOf(requirement)] } : {}
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
return {
|
|
1042
|
+
openapi: "3.1.0",
|
|
1043
|
+
info: options.info,
|
|
1044
|
+
...options.servers ? { servers: options.servers } : {},
|
|
1045
|
+
...security?.default ? { security: [requirementOf(security.default)] } : {},
|
|
1046
|
+
paths,
|
|
1047
|
+
components: {
|
|
1048
|
+
schemas: components,
|
|
1049
|
+
...security ? { securitySchemes: security.schemes } : {}
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
export { schemaLockOf2, diffSchemas2, schemaLockProblemsOf2, statusOf2, PROBLEM_MEDIA_TYPE2, problemOf2, outputValueOf2, ROUTE_OPTION_KEYS2, routeOptionsIssue2, requestIdHeaderOf2, operationRoutes2, openapi2 };
|