@afora/core 0.0.9 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -189
- package/dist/adapters.js +31 -55
- package/dist/core/application/application.d.ts +30 -0
- package/dist/core/application/application.js +1291 -0
- package/dist/core/application/exceptions.js +85 -0
- package/dist/core/application/migrate.js +67 -0
- package/dist/core/application/providers.js +108 -0
- package/dist/core/application/registry.js +23 -0
- package/dist/core/application/schema.js +398 -0
- package/dist/core/application/start.js +162 -0
- package/dist/core/application/types/application.interface.d.ts +23 -3
- package/dist/core/auth/jwt.js +200 -0
- package/dist/core/auth/types/jwt.interface.d.ts +10 -2
- package/dist/core/blueprint/blueprint-plugin.js +7 -0
- package/dist/core/blueprint/blueprint.js +146 -0
- package/dist/core/blueprint/callable.js +13 -0
- package/dist/core/blueprint/exceptions.js +20 -0
- package/dist/core/blueprint/is-blueprint.js +4 -0
- package/dist/core/blueprint/list.js +19 -0
- package/dist/core/blueprint/optional.js +69 -0
- package/dist/core/blueprint/record-field.js +14 -0
- package/dist/core/blueprint/slice.js +127 -0
- package/dist/core/blueprint/types/blueprint.interface.d.ts +16 -0
- package/dist/core/blueprint/variant.js +72 -0
- package/dist/core/command/command.js +428 -0
- package/dist/core/command/exceptions.js +30 -0
- package/dist/core/command/types/command.interface.d.ts +10 -1
- package/dist/core/entity/abstract-entity.d.ts +19 -0
- package/dist/core/entity/abstract-entity.js +181 -0
- package/dist/core/entity/entity.js +209 -0
- package/dist/core/entity/event.d.ts +39 -0
- package/dist/core/entity/event.js +80 -0
- package/dist/core/entity/exceptions.js +27 -0
- package/dist/core/entity/trust.js +7 -0
- package/dist/core/entity/version.js +13 -0
- package/dist/core/environment/environment.d.ts +26 -3
- package/dist/core/environment/environment.js +71 -0
- package/dist/core/event-policy/event-policy.js +76 -0
- package/dist/core/event-policy/exceptions.js +12 -0
- package/dist/core/event-policy/publish.js +98 -0
- package/dist/core/event-policy/publishers.js +18 -0
- package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
- package/dist/core/exception/enums/exception-layers.js +15 -0
- package/dist/core/exception/exception.d.ts +15 -0
- package/dist/core/exception/exception.js +128 -0
- package/dist/core/exception/exported.js +49 -0
- package/dist/core/exception/guard.d.ts +20 -1
- package/dist/core/exception/guard.js +16 -0
- package/dist/core/exception/types/exception.interface.d.ts +13 -0
- package/dist/core/json.js +19 -0
- package/dist/core/logger/exceptions.js +7 -0
- package/dist/core/logger/logger.d.ts +14 -1
- package/dist/core/logger/logger.js +178 -0
- package/dist/core/openapi/exceptions.js +7 -0
- package/dist/core/openapi/openapi.js +132 -0
- package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
- package/dist/core/output/access.js +66 -0
- package/dist/core/output/coerce.js +38 -0
- package/dist/core/output/exceptions.js +22 -0
- package/dist/{index-y280ew0k.js → core/output/http.js} +35 -80
- package/dist/core/output/openapi-sink.d.ts +10 -2
- package/dist/{index-hg1z2s2v.js → core/output/openapi-sink.js} +60 -544
- package/dist/core/output/output.js +192 -0
- package/dist/core/output/problem.js +30 -0
- package/dist/core/output/select.js +28 -0
- package/dist/core/output/stage.js +111 -0
- package/dist/core/output/status.js +52 -0
- package/dist/core/output/types/http.interface.d.ts +5 -1
- package/dist/core/output/types/output.interface.d.ts +24 -0
- package/dist/core/parse/issue.js +31 -0
- package/dist/core/parse/parse.d.ts +8 -1
- package/dist/core/parse/parse.js +323 -0
- package/dist/core/parse/strip.js +48 -0
- package/dist/core/property/abstract-property.js +37 -0
- package/dist/core/property/define-property.js +209 -0
- package/dist/core/property/enums/property-states.js +8 -0
- package/dist/core/property/exceptions.js +12 -0
- package/dist/core/property/input.js +10 -0
- package/dist/core/property/is-property.js +3 -0
- package/dist/core/property/lineage.js +8 -0
- package/dist/core/property/property.d.ts +27 -0
- package/dist/core/property/property.js +46 -0
- package/dist/core/property/refine.js +103 -0
- package/dist/core/record/abstract-record.d.ts +2 -1
- package/dist/core/record/abstract-record.js +307 -0
- package/dist/core/record/exceptions.js +49 -0
- package/dist/core/record/marks.js +7 -0
- package/dist/core/record/mask.js +60 -0
- package/dist/core/record/name.js +23 -0
- package/dist/core/record/plugin-catalog.js +115 -0
- package/dist/core/record/plugin.js +198 -0
- package/dist/core/record/record.js +117 -0
- package/dist/core/record/types/record.interface.d.ts +5 -0
- package/dist/core/repository/cursor.js +83 -0
- package/dist/core/repository/exceptions.js +72 -0
- package/dist/core/repository/in-memory.js +196 -0
- package/dist/core/repository/migrate.js +254 -0
- package/dist/core/repository/plan.js +27 -0
- package/dist/core/repository/provider.js +29 -0
- package/dist/core/repository/query.js +153 -0
- package/dist/core/repository/references.js +196 -0
- package/dist/core/repository/repository.d.ts +22 -2
- package/dist/core/repository/repository.js +720 -0
- package/dist/core/repository/state.js +138 -0
- package/dist/core/repository/types/query.interface.d.ts +13 -0
- package/dist/core/repository/types/repository.interface.d.ts +20 -8
- package/dist/core/repository/types/schema.interface.d.ts +8 -2
- package/dist/core/repository/upcast.js +179 -0
- package/dist/core/schema/schema.d.ts +9 -0
- package/dist/core/schema/schema.js +542 -0
- package/dist/core/standard/standard-schema.js +1 -0
- package/dist/core/standard/standard.js +23 -0
- package/dist/core/symbols/demo.d.ts +4 -0
- package/dist/core/symbols/demo.js +1 -0
- package/dist/core/symbols/kind.js +1 -0
- package/dist/core/symbols/meta.js +1 -0
- package/dist/core/symbols/state.d.ts +5 -0
- package/dist/core/symbols/state.js +1 -0
- package/dist/core/tracing/exceptions.d.ts +4 -0
- package/dist/core/tracing/exceptions.js +7 -0
- package/dist/core/tracing/tracing.d.ts +72 -0
- package/dist/core/tracing/tracing.js +101 -0
- package/dist/core/use-case/access.js +56 -0
- package/dist/core/use-case/decorators.js +65 -0
- package/dist/core/use-case/exceptions.d.ts +5 -1
- package/dist/core/use-case/exceptions.js +48 -0
- package/dist/core/use-case/plugin-catalog.js +68 -0
- package/dist/core/use-case/plugin.js +133 -0
- package/dist/core/use-case/route.js +200 -0
- package/dist/core/use-case/scope.d.ts +5 -0
- package/dist/core/use-case/scope.js +4 -0
- package/dist/core/use-case/steps/describe.d.ts +5 -1
- package/dist/core/use-case/steps/describe.js +24 -0
- package/dist/core/use-case/steps/references.js +105 -0
- package/dist/core/use-case/steps/run.js +139 -0
- package/dist/core/use-case/steps/types/check.interface.d.ts +27 -8
- package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
- package/dist/core/use-case/steps/validate.js +368 -0
- package/dist/core/use-case/target.js +100 -0
- package/dist/core/use-case/types/route.interface.d.ts +25 -5
- package/dist/core/use-case/use-case.d.ts +77 -2
- package/dist/core/use-case/use-case.js +493 -0
- package/dist/experimental.d.ts +5 -0
- package/dist/experimental.js +18 -161
- package/dist/index.d.ts +7 -5
- package/dist/index.js +213 -367
- package/dist/std/blueprint-plugin.js +42 -0
- package/dist/std/blueprint.d.ts +21 -0
- package/dist/std/blueprint.js +3 -0
- package/dist/std/command.js +3 -0
- package/dist/std/entity.d.ts +20 -1
- package/dist/std/entity.js +30 -0
- package/dist/std/event-policies.js +3 -0
- package/dist/std/properties/contact.d.ts +3 -0
- package/dist/std/properties/contact.js +29 -0
- package/dist/std/properties/enum.js +26 -0
- package/dist/std/properties/exceptions.js +14 -0
- package/dist/std/properties/headless.js +45 -0
- package/dist/std/properties/identity.js +28 -0
- package/dist/std/properties/json.js +161 -0
- package/dist/std/properties/password.js +79 -0
- package/dist/std/properties/primitives.d.ts +4 -0
- package/dist/std/properties/primitives.js +28 -0
- package/dist/std/properties/time.js +55 -0
- package/dist/std/record-plugins.js +73 -0
- package/dist/std/record.d.ts +20 -1
- package/dist/std/record.js +3 -0
- package/dist/std/resource.js +39 -0
- package/dist/std/runtime.js +193 -0
- package/dist/std/stages.js +4 -0
- package/dist/std/use-cases/auth.js +193 -0
- package/dist/std/use-cases/crud.js +261 -0
- package/dist/std/use-cases/index.js +12 -0
- package/dist/std/use-cases/lifecycle.js +97 -0
- package/dist/std/use-cases/shared.js +232 -0
- package/dist/std/use-cases/types.d.ts +5 -1
- package/dist/std/use-cases/types.js +0 -0
- package/dist/testing/http-provider-contract.js +344 -0
- package/dist/testing/index.js +3 -1435
- package/dist/testing/memory-publisher.js +12 -0
- package/dist/testing/provider-contract.d.ts +12 -3
- package/dist/testing/provider-contract.js +1071 -0
- package/package.json +17 -5
- package/dist/index-3aae5jfq.js +0 -2997
- package/dist/index-ny3as79r.js +0 -4567
- package/dist/index-xegv6a1p.js +0 -4291
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { isBlueprint } from "../blueprint/is-blueprint.js";
|
|
2
|
+
import { isBlueprintList } from "../blueprint/list.js";
|
|
3
|
+
import { isRecordClass, recordFieldOf } from "../blueprint/record-field.js";
|
|
4
|
+
import { isBlueprintVariant } from "../blueprint/variant.js";
|
|
5
|
+
import { identityKeysOf } from "../entity/abstract-entity.js";
|
|
6
|
+
import { isEntityClass } from "../entity/entity.js";
|
|
7
|
+
import { isProperty } from "../property/is-property.js";
|
|
8
|
+
import { rawTypeOf } from "../schema/schema.js";
|
|
9
|
+
import { InvalidOpenAPIModel } from "./exceptions.js";
|
|
10
|
+
const NO_MARKS = { writeOnly: [], readOnly: [] };
|
|
11
|
+
function isField(value) {
|
|
12
|
+
return isProperty(value) || isBlueprint(value) || isBlueprintList(value) || isBlueprintVariant(value) || recordFieldOf(value) !== undefined;
|
|
13
|
+
}
|
|
14
|
+
function isOptional(field) {
|
|
15
|
+
if (isProperty(field))
|
|
16
|
+
return field.type.optional === true;
|
|
17
|
+
if (isBlueprint(field) || isBlueprintList(field) || isBlueprintVariant(field)) {
|
|
18
|
+
return field.optional;
|
|
19
|
+
}
|
|
20
|
+
return recordFieldOf(field)?.optional ?? false;
|
|
21
|
+
}
|
|
22
|
+
export function plainOf(value, nested = false) {
|
|
23
|
+
if (Array.isArray(value))
|
|
24
|
+
return value.map((item) => plainOf(item, true));
|
|
25
|
+
if (typeof value !== "object" || value === null)
|
|
26
|
+
return value;
|
|
27
|
+
const id = value.$id;
|
|
28
|
+
if (nested && typeof id === "string")
|
|
29
|
+
return { $ref: id };
|
|
30
|
+
const plain = {};
|
|
31
|
+
for (const [key, item] of Object.entries(value)) {
|
|
32
|
+
if (!key.startsWith("~"))
|
|
33
|
+
plain[key] = plainOf(item, true);
|
|
34
|
+
}
|
|
35
|
+
return plain;
|
|
36
|
+
}
|
|
37
|
+
function objectOf(properties, scope, marks) {
|
|
38
|
+
const shape = {};
|
|
39
|
+
const required = [];
|
|
40
|
+
for (const [key, field] of Object.entries(properties)) {
|
|
41
|
+
const schema = schemaOf(field, scope);
|
|
42
|
+
if (marks.readOnly.includes(key))
|
|
43
|
+
schema.readOnly = true;
|
|
44
|
+
if (marks.writeOnly.includes(key))
|
|
45
|
+
schema.writeOnly = true;
|
|
46
|
+
shape[key] = schema;
|
|
47
|
+
if (!isOptional(field))
|
|
48
|
+
required.push(key);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: shape,
|
|
53
|
+
...required.length > 0 ? { required } : {},
|
|
54
|
+
additionalProperties: false
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function recordSchemaOf(record, scope) {
|
|
58
|
+
const { sensitive = [] } = record.options ?? {};
|
|
59
|
+
return objectOf(record.blueprint.properties, scope, {
|
|
60
|
+
writeOnly: sensitive,
|
|
61
|
+
readOnly: isEntityClass(record) ? identityKeysOf(record) : []
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function schemaOf(field, scope) {
|
|
65
|
+
if (isBlueprintList(field)) {
|
|
66
|
+
const { item, minItems, maxItems } = field;
|
|
67
|
+
return {
|
|
68
|
+
type: "array",
|
|
69
|
+
items: schemaOf(item, scope),
|
|
70
|
+
...minItems === undefined ? {} : { minItems },
|
|
71
|
+
...maxItems === undefined ? {} : { maxItems }
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (isBlueprint(field))
|
|
75
|
+
return objectOf(field.properties, scope, NO_MARKS);
|
|
76
|
+
if (isBlueprintVariant(field)) {
|
|
77
|
+
const variants = field.variants;
|
|
78
|
+
return {
|
|
79
|
+
oneOf: Object.entries(variants).map(([tag, variant]) => {
|
|
80
|
+
const object = isRecordClass(variant) ? recordSchemaOf(variant, scope) : objectOf(variant.properties, scope, NO_MARKS);
|
|
81
|
+
return {
|
|
82
|
+
...object,
|
|
83
|
+
properties: { [field.key]: { const: tag }, ...object.properties },
|
|
84
|
+
required: [field.key, ...object.required ?? []]
|
|
85
|
+
};
|
|
86
|
+
}),
|
|
87
|
+
discriminator: { propertyName: field.key }
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const record = recordFieldOf(field);
|
|
91
|
+
if (record) {
|
|
92
|
+
const name = scope.names.get(record.record);
|
|
93
|
+
return name === undefined ? recordSchemaOf(record.record, scope) : { $ref: `${scope.prefix}${name}` };
|
|
94
|
+
}
|
|
95
|
+
if (!isProperty(field)) {
|
|
96
|
+
throw new InvalidOpenAPIModel({
|
|
97
|
+
source: "openapi",
|
|
98
|
+
details: { name: String(field) }
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const schema = plainOf(rawTypeOf(field.type.schema));
|
|
102
|
+
if (field.type.sensitive)
|
|
103
|
+
schema.writeOnly = true;
|
|
104
|
+
return schema;
|
|
105
|
+
}
|
|
106
|
+
export function toOpenAPI(modelOrModels, { refPrefix = "#/components/schemas/" } = {}) {
|
|
107
|
+
if (isField(modelOrModels)) {
|
|
108
|
+
return schemaOf(modelOrModels, { names: new Map, prefix: refPrefix });
|
|
109
|
+
}
|
|
110
|
+
if (typeof modelOrModels !== "object" || modelOrModels === null) {
|
|
111
|
+
throw new InvalidOpenAPIModel({
|
|
112
|
+
source: "openapi",
|
|
113
|
+
details: { name: String(modelOrModels) }
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const names = new Map;
|
|
117
|
+
for (const [name, model] of Object.entries(modelOrModels)) {
|
|
118
|
+
if (!isField(model)) {
|
|
119
|
+
throw new InvalidOpenAPIModel({ source: "openapi", details: { name } });
|
|
120
|
+
}
|
|
121
|
+
const record = recordFieldOf(model);
|
|
122
|
+
if (record && !names.has(record.record))
|
|
123
|
+
names.set(record.record, name);
|
|
124
|
+
}
|
|
125
|
+
const scope = { names, prefix: refPrefix };
|
|
126
|
+
const schemas = {};
|
|
127
|
+
for (const [name, model] of Object.entries(modelOrModels)) {
|
|
128
|
+
const record = recordFieldOf(model);
|
|
129
|
+
schemas[name] = record && names.get(record.record) === name ? recordSchemaOf(record.record, scope) : schemaOf(model, scope);
|
|
130
|
+
}
|
|
131
|
+
return { schemas };
|
|
132
|
+
}
|
|
@@ -28,10 +28,16 @@ export interface OpenAPIComponents<Name extends PropertyKey = string> {
|
|
|
28
28
|
readonly [Key in Name]: OpenAPISchema;
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* What `toOpenAPI` takes after the models: how a record named among them is
|
|
33
|
+
* referenced.
|
|
34
|
+
*/
|
|
31
35
|
export interface OpenAPIOptions {
|
|
32
36
|
/**
|
|
33
37
|
* Where a record named among the models is referenced from: `$ref` is this
|
|
34
|
-
* prefix and its name.
|
|
38
|
+
* prefix and its name.
|
|
39
|
+
*
|
|
40
|
+
* @default "#/components/schemas/"
|
|
35
41
|
*/
|
|
36
42
|
readonly refPrefix?: string;
|
|
37
43
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { isPublicAccess } from "../use-case/access.js";
|
|
2
|
+
import { Forbidden, InvalidOutputStage, Unauthenticated } from "./exceptions.js";
|
|
3
|
+
function readerOf(path) {
|
|
4
|
+
if (path === "subject")
|
|
5
|
+
return (principal) => principal.subject;
|
|
6
|
+
if (path === "scopes")
|
|
7
|
+
return (principal) => [...principal.scopes];
|
|
8
|
+
const claim = path.slice("claims.".length);
|
|
9
|
+
return (principal) => principal.claims && Object.hasOwn(principal.claims, claim) ? principal.claims[claim] : undefined;
|
|
10
|
+
}
|
|
11
|
+
export function accessGuardOf(id, access, context) {
|
|
12
|
+
if (isPublicAccess(access)) {
|
|
13
|
+
return (_input, options) => options === undefined ? undefined : { context: options.context };
|
|
14
|
+
}
|
|
15
|
+
const scopes = access.scopes ?? [];
|
|
16
|
+
const owner = access.owner;
|
|
17
|
+
const ownerOf = owner ? readerOf(owner.claim === undefined ? "subject" : `claims.${owner.claim}`) : undefined;
|
|
18
|
+
const fills = Object.entries(access.context ?? {}).map(([key, path]) => [key, readerOf(path)]);
|
|
19
|
+
for (const [key] of fills) {
|
|
20
|
+
if (!context || !Object.hasOwn(context.properties, key)) {
|
|
21
|
+
throw new InvalidOutputStage({
|
|
22
|
+
source: id,
|
|
23
|
+
details: {
|
|
24
|
+
reason: `The access of ${id} fills the context key "${key}", which the context of the operation does not declare.`
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return (input, options) => {
|
|
30
|
+
const principal = options?.principal;
|
|
31
|
+
if (principal === undefined || principal === null || typeof principal.subject !== "string" || !Array.isArray(principal.scopes)) {
|
|
32
|
+
throw new Unauthenticated({ source: id });
|
|
33
|
+
}
|
|
34
|
+
if (scopes.length > 0) {
|
|
35
|
+
const missing = scopes.filter((scope) => !principal.scopes.includes(scope));
|
|
36
|
+
if (missing.length > 0) {
|
|
37
|
+
throw new Forbidden({
|
|
38
|
+
source: id,
|
|
39
|
+
details: { reason: "scope", scopes: missing }
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (owner && ownerOf) {
|
|
44
|
+
const expected = ownerOf(principal);
|
|
45
|
+
const given = typeof input === "object" && input !== null ? input[owner.input] : undefined;
|
|
46
|
+
if (given === undefined || given === null || expected === undefined || expected === null || String(given) !== String(expected)) {
|
|
47
|
+
throw new Forbidden({ source: id, details: { reason: "owner" } });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (fills.length === 0) {
|
|
51
|
+
return options?.context === undefined ? undefined : { context: options.context };
|
|
52
|
+
}
|
|
53
|
+
const given = options?.context;
|
|
54
|
+
const filled = {
|
|
55
|
+
...typeof given === "object" && given !== null ? given : {}
|
|
56
|
+
};
|
|
57
|
+
for (const [key, read] of fills) {
|
|
58
|
+
const value = read(principal);
|
|
59
|
+
if (value === undefined)
|
|
60
|
+
delete filled[key];
|
|
61
|
+
else
|
|
62
|
+
filled[key] = value;
|
|
63
|
+
}
|
|
64
|
+
return { context: filled };
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { toOpenAPI } from "../openapi/openapi.js";
|
|
2
|
+
const scalarTypes = new WeakMap;
|
|
3
|
+
function scalarTypesOf(blueprint) {
|
|
4
|
+
let types = scalarTypes.get(blueprint);
|
|
5
|
+
if (!types) {
|
|
6
|
+
const schema = toOpenAPI(blueprint);
|
|
7
|
+
const found = {};
|
|
8
|
+
for (const [key, property] of Object.entries(schema.properties ?? {})) {
|
|
9
|
+
found[key] = [property.type ?? []].flat();
|
|
10
|
+
}
|
|
11
|
+
types = Object.freeze(found);
|
|
12
|
+
scalarTypes.set(blueprint, types);
|
|
13
|
+
}
|
|
14
|
+
return types;
|
|
15
|
+
}
|
|
16
|
+
function coerce(text, types) {
|
|
17
|
+
if (!types || types.includes("string"))
|
|
18
|
+
return text;
|
|
19
|
+
const trimmed = text.trim();
|
|
20
|
+
if (types.includes("boolean") && (trimmed === "true" || trimmed === "false")) {
|
|
21
|
+
return trimmed === "true";
|
|
22
|
+
}
|
|
23
|
+
if (types.includes("integer") || types.includes("number")) {
|
|
24
|
+
const number = trimmed === "" ? Number.NaN : Number(trimmed);
|
|
25
|
+
if (Number.isFinite(number))
|
|
26
|
+
return number;
|
|
27
|
+
}
|
|
28
|
+
return text;
|
|
29
|
+
}
|
|
30
|
+
export function coerceTextValues(blueprint, texts) {
|
|
31
|
+
const types = scalarTypesOf(blueprint);
|
|
32
|
+
const out = {};
|
|
33
|
+
for (const [key, value] of Object.entries(texts)) {
|
|
34
|
+
if (value !== undefined)
|
|
35
|
+
out[key] = coerce(value, types[key]);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ExceptionLayers } from "../exception/enums/exception-layers.js";
|
|
2
|
+
import { exceptionOf } from "../exception/exception.js";
|
|
3
|
+
export const InvalidOutputStage = exceptionOf({
|
|
4
|
+
layer: ExceptionLayers.Output,
|
|
5
|
+
name: "InvalidOutputStage",
|
|
6
|
+
message: ({ reason }) => reason
|
|
7
|
+
});
|
|
8
|
+
export const DuplicateOperation = exceptionOf({
|
|
9
|
+
layer: ExceptionLayers.Output,
|
|
10
|
+
name: "DuplicateOperation",
|
|
11
|
+
message: ({ id }) => `The operation "${id}" is declared twice.`
|
|
12
|
+
});
|
|
13
|
+
export const Unauthenticated = exceptionOf({
|
|
14
|
+
layer: ExceptionLayers.Output,
|
|
15
|
+
name: "Unauthenticated",
|
|
16
|
+
message: "Authentication is required."
|
|
17
|
+
});
|
|
18
|
+
export const Forbidden = exceptionOf({
|
|
19
|
+
layer: ExceptionLayers.Output,
|
|
20
|
+
name: "Forbidden",
|
|
21
|
+
message: (_details) => "The caller may not do this."
|
|
22
|
+
});
|
|
@@ -1,61 +1,17 @@
|
|
|
1
|
+
import { jwtOf, jwtOptionsIssue } from "../auth/jwt.js";
|
|
2
|
+
import { Exception } from "../exception/exception.js";
|
|
3
|
+
import { HEADER_NAME } from "../use-case/route.js";
|
|
4
|
+
import { coerceTextValues } from "./coerce.js";
|
|
5
|
+
import { InvalidOutputStage } from "./exceptions.js";
|
|
1
6
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
openapi2
|
|
11
|
-
} from "./index-hg1z2s2v.js";
|
|
12
|
-
import {
|
|
13
|
-
Exception2,
|
|
14
|
-
toOpenAPI2,
|
|
15
|
-
HEADER_NAME,
|
|
16
|
-
InvalidOutputStage2,
|
|
17
|
-
jwtOptionsIssue,
|
|
18
|
-
jwtOf2
|
|
19
|
-
} from "./index-xegv6a1p.js";
|
|
20
|
-
// src/core/output/coerce.ts
|
|
21
|
-
var scalarTypes = new WeakMap;
|
|
22
|
-
function scalarTypesOf(blueprint) {
|
|
23
|
-
let types = scalarTypes.get(blueprint);
|
|
24
|
-
if (!types) {
|
|
25
|
-
const schema = toOpenAPI2(blueprint);
|
|
26
|
-
const found = {};
|
|
27
|
-
for (const [key, property] of Object.entries(schema.properties ?? {})) {
|
|
28
|
-
found[key] = [property.type ?? []].flat();
|
|
29
|
-
}
|
|
30
|
-
types = Object.freeze(found);
|
|
31
|
-
scalarTypes.set(blueprint, types);
|
|
32
|
-
}
|
|
33
|
-
return types;
|
|
34
|
-
}
|
|
35
|
-
function coerce(text, types) {
|
|
36
|
-
if (!types || types.includes("string"))
|
|
37
|
-
return text;
|
|
38
|
-
const trimmed = text.trim();
|
|
39
|
-
if (types.includes("boolean") && (trimmed === "true" || trimmed === "false")) {
|
|
40
|
-
return trimmed === "true";
|
|
41
|
-
}
|
|
42
|
-
if (types.includes("integer") || types.includes("number")) {
|
|
43
|
-
const number = trimmed === "" ? Number.NaN : Number(trimmed);
|
|
44
|
-
if (Number.isFinite(number))
|
|
45
|
-
return number;
|
|
46
|
-
}
|
|
47
|
-
return text;
|
|
48
|
-
}
|
|
49
|
-
function coerceTextValues2(blueprint, texts) {
|
|
50
|
-
const types = scalarTypesOf(blueprint);
|
|
51
|
-
const out = {};
|
|
52
|
-
for (const [key, value] of Object.entries(texts)) {
|
|
53
|
-
if (value !== undefined)
|
|
54
|
-
out[key] = coerce(value, types[key]);
|
|
55
|
-
}
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
|
-
// src/core/output/http.ts
|
|
7
|
+
openapi,
|
|
8
|
+
operationRoutes,
|
|
9
|
+
ROUTE_OPTION_KEYS,
|
|
10
|
+
requestIdHeaderOf,
|
|
11
|
+
routeOptionsIssue
|
|
12
|
+
} from "./openapi-sink.js";
|
|
13
|
+
import { outputValueOf, PROBLEM_MEDIA_TYPE, problemOf } from "./problem.js";
|
|
14
|
+
import { statusOf } from "./status.js";
|
|
59
15
|
function isObject(value) {
|
|
60
16
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
61
17
|
}
|
|
@@ -66,16 +22,16 @@ function onlyKeys(value, allowed, where) {
|
|
|
66
22
|
}
|
|
67
23
|
return;
|
|
68
24
|
}
|
|
69
|
-
|
|
70
|
-
...
|
|
25
|
+
export const HTTP_OPTION_KEYS = [
|
|
26
|
+
...ROUTE_OPTION_KEYS,
|
|
71
27
|
"docs",
|
|
72
28
|
"debug",
|
|
73
29
|
"context",
|
|
74
30
|
"auth"
|
|
75
31
|
];
|
|
76
|
-
function
|
|
32
|
+
export function httpOptionsIssue(options, where) {
|
|
77
33
|
const given = options;
|
|
78
|
-
const routing =
|
|
34
|
+
const routing = routeOptionsIssue(options, where);
|
|
79
35
|
if (routing)
|
|
80
36
|
return routing;
|
|
81
37
|
if (given.debug !== undefined && typeof given.debug !== "boolean") {
|
|
@@ -109,7 +65,7 @@ function httpOptionsIssue2(options, where) {
|
|
|
109
65
|
return `Expected ${where}.docs.path to be a path such as "/openapi.json".`;
|
|
110
66
|
}
|
|
111
67
|
try {
|
|
112
|
-
|
|
68
|
+
openapi({
|
|
113
69
|
info: docs.info,
|
|
114
70
|
...docs.servers ? { servers: docs.servers } : {}
|
|
115
71
|
});
|
|
@@ -151,9 +107,9 @@ function httpOptionsIssue2(options, where) {
|
|
|
151
107
|
}
|
|
152
108
|
return;
|
|
153
109
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
110
|
+
const REQUEST_ID = /^[\x21-\x7e]{1,200}$/;
|
|
111
|
+
const JSON_TYPE = "application/json";
|
|
112
|
+
const NO_COOKIES = Object.freeze([]);
|
|
157
113
|
function cookiesOf(header) {
|
|
158
114
|
const cookies = new Map;
|
|
159
115
|
if (!header)
|
|
@@ -190,28 +146,28 @@ function cookieWriterOf(cookie) {
|
|
|
190
146
|
const lasting = cookie.maxAge === undefined ? "" : `; Max-Age=${cookie.maxAge}`;
|
|
191
147
|
return (value) => value === "" ? `${cookie.name}=; ${attributes}; Max-Age=0` : `${cookie.name}=${encodeURIComponent(value)}; ${attributes}${lasting}`;
|
|
192
148
|
}
|
|
193
|
-
function
|
|
149
|
+
export function compileRoutes(operations, options, context, settings) {
|
|
194
150
|
const { source } = settings;
|
|
195
151
|
const { output } = context;
|
|
196
152
|
let authenticator = settings.authenticator;
|
|
197
153
|
if (!authenticator && options.auth) {
|
|
198
154
|
const { header, cookie, ...jwt } = options.auth.jwt;
|
|
199
|
-
authenticator =
|
|
155
|
+
authenticator = jwtOf(jwt).authenticator({
|
|
200
156
|
...header === undefined ? {} : { header },
|
|
201
157
|
...cookie === undefined ? {} : { cookie }
|
|
202
158
|
});
|
|
203
159
|
}
|
|
204
|
-
const routes =
|
|
160
|
+
const routes = operationRoutes(operations, options, source);
|
|
205
161
|
const debug = options.debug === true;
|
|
206
162
|
const contextHeaders = options.context?.headers ?? {};
|
|
207
163
|
const contextVariables = options.context?.variables ?? {};
|
|
208
|
-
const requestIdHeader =
|
|
164
|
+
const requestIdHeader = requestIdHeaderOf(options);
|
|
209
165
|
const idOf = (request) => {
|
|
210
166
|
const sent = request?.header(requestIdHeader);
|
|
211
167
|
return sent !== undefined && REQUEST_ID.test(sent) ? sent : crypto.randomUUID();
|
|
212
168
|
};
|
|
213
169
|
const headersOf = (headers, request) => requestIdHeader === undefined ? headers : { ...headers, [requestIdHeader]: idOf(request) };
|
|
214
|
-
const problemHeaders = Object.freeze({ "content-type":
|
|
170
|
+
const problemHeaders = Object.freeze({ "content-type": PROBLEM_MEDIA_TYPE });
|
|
215
171
|
const problem = (body, status, request) => ({
|
|
216
172
|
status,
|
|
217
173
|
headers: headersOf(problemHeaders, request),
|
|
@@ -219,8 +175,8 @@ function compileRoutes2(operations, options, context, settings) {
|
|
|
219
175
|
body: JSON.stringify(body)
|
|
220
176
|
});
|
|
221
177
|
const fail = (error, operation, request, fallback) => {
|
|
222
|
-
const status = fallback !== undefined && !(error instanceof
|
|
223
|
-
return problem(
|
|
178
|
+
const status = fallback !== undefined && !(error instanceof Exception && Object.hasOwn(output.statuses, error.name)) ? fallback : statusOf(error, operation, output.statuses);
|
|
179
|
+
return problem(problemOf(error, { status, debug }), status, request);
|
|
224
180
|
};
|
|
225
181
|
const failure = (error, given) => fail(error, given?.operation, given?.request, given?.status);
|
|
226
182
|
const compiled = operations.map((operation) => {
|
|
@@ -268,7 +224,7 @@ function compileRoutes2(operations, options, context, settings) {
|
|
|
268
224
|
}, 400, request);
|
|
269
225
|
}
|
|
270
226
|
} else {
|
|
271
|
-
input =
|
|
227
|
+
input = coerceTextValues(operation.input, request.query());
|
|
272
228
|
}
|
|
273
229
|
if (placed && isObject(input)) {
|
|
274
230
|
const texts = {};
|
|
@@ -287,7 +243,7 @@ function compileRoutes2(operations, options, context, settings) {
|
|
|
287
243
|
}
|
|
288
244
|
for (const name of parameters)
|
|
289
245
|
texts[name] = request.param(name);
|
|
290
|
-
input = { ...input, ...
|
|
246
|
+
input = { ...input, ...coerceTextValues(operation.input, texts) };
|
|
291
247
|
}
|
|
292
248
|
let call;
|
|
293
249
|
if (hasContext) {
|
|
@@ -295,7 +251,7 @@ function compileRoutes2(operations, options, context, settings) {
|
|
|
295
251
|
for (const [key, name] of contextHeaderKeys) {
|
|
296
252
|
raw[key] = request.header(name);
|
|
297
253
|
}
|
|
298
|
-
const values =
|
|
254
|
+
const values = coerceTextValues(operation.context, raw);
|
|
299
255
|
for (const [key, name] of contextVariableKeys) {
|
|
300
256
|
const value = request.variable?.(name);
|
|
301
257
|
if (value !== undefined)
|
|
@@ -313,7 +269,7 @@ function compileRoutes2(operations, options, context, settings) {
|
|
|
313
269
|
body: status === 204 ? null : JSON.stringify(response) ?? "null"
|
|
314
270
|
};
|
|
315
271
|
}
|
|
316
|
-
const value =
|
|
272
|
+
const value = outputValueOf(response);
|
|
317
273
|
const sent = [];
|
|
318
274
|
if (isObject(value)) {
|
|
319
275
|
for (const [key, write] of setCookies) {
|
|
@@ -340,7 +296,7 @@ function compileRoutes2(operations, options, context, settings) {
|
|
|
340
296
|
if (options.docs) {
|
|
341
297
|
const { path, info, servers } = options.docs;
|
|
342
298
|
if (compiled.some((route) => route.method === "get" && route.path === path)) {
|
|
343
|
-
throw new
|
|
299
|
+
throw new InvalidOutputStage({
|
|
344
300
|
source,
|
|
345
301
|
details: {
|
|
346
302
|
reason: `${source}.docs.path "${path}" collides with a GET operation route.`
|
|
@@ -348,7 +304,7 @@ function compileRoutes2(operations, options, context, settings) {
|
|
|
348
304
|
});
|
|
349
305
|
}
|
|
350
306
|
const routing = {};
|
|
351
|
-
for (const key of
|
|
307
|
+
for (const key of ROUTE_OPTION_KEYS) {
|
|
352
308
|
if (options[key] !== undefined)
|
|
353
309
|
routing[key] = options[key];
|
|
354
310
|
}
|
|
@@ -364,7 +320,7 @@ function compileRoutes2(operations, options, context, settings) {
|
|
|
364
320
|
}
|
|
365
321
|
};
|
|
366
322
|
}
|
|
367
|
-
const document =
|
|
323
|
+
const document = openapi({
|
|
368
324
|
info,
|
|
369
325
|
...servers ? { servers } : {},
|
|
370
326
|
...routing
|
|
@@ -393,4 +349,3 @@ function compileRoutes2(operations, options, context, settings) {
|
|
|
393
349
|
failure
|
|
394
350
|
});
|
|
395
351
|
}
|
|
396
|
-
export { coerceTextValues2, HTTP_OPTION_KEYS2, httpOptionsIssue2, compileRoutes2 };
|
|
@@ -45,7 +45,11 @@ export interface SecurityRequirement {
|
|
|
45
45
|
* sinks that serve HTTP, so the served routes and the document agree.
|
|
46
46
|
*/
|
|
47
47
|
export interface RouteOptions {
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Prefix of every path, e.g. `"/api"`.
|
|
50
|
+
*
|
|
51
|
+
* @default ""
|
|
52
|
+
*/
|
|
49
53
|
readonly basePath?: string;
|
|
50
54
|
/**
|
|
51
55
|
* Method, path and success status by operation id, e.g.
|
|
@@ -56,7 +60,11 @@ export interface RouteOptions {
|
|
|
56
60
|
readonly [operationId: string]: {
|
|
57
61
|
readonly method?: OpenAPIMethod;
|
|
58
62
|
readonly path?: string;
|
|
59
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* The status of a successful response.
|
|
65
|
+
*
|
|
66
|
+
* @default 200
|
|
67
|
+
*/
|
|
60
68
|
readonly status?: RouteStatus;
|
|
61
69
|
};
|
|
62
70
|
};
|