@afora/core 0.0.8 → 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 +149 -23
- package/dist/adapters.d.ts +25 -0
- package/dist/adapters.js +32 -0
- package/dist/core/application/application.d.ts +30 -0
- package/dist/core/application/application.js +1291 -0
- package/dist/core/application/exceptions.d.ts +17 -0
- package/dist/core/application/exceptions.js +85 -0
- package/dist/core/application/migrate.d.ts +19 -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.d.ts +30 -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 +60 -3
- package/dist/core/auth/jwt.d.ts +30 -0
- package/dist/core/auth/jwt.js +200 -0
- package/dist/core/auth/types/jwt.interface.d.ts +74 -0
- 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.d.ts +5 -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.d.ts +3 -1
- 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 +95 -5
- package/dist/core/blueprint/variant.d.ts +32 -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 +24 -0
- package/dist/core/entity/abstract-entity.js +181 -0
- package/dist/core/entity/entity.d.ts +1 -1
- 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.d.ts +9 -0
- package/dist/core/entity/exceptions.js +27 -0
- package/dist/core/entity/trust.d.ts +24 -0
- package/dist/core/entity/trust.js +7 -0
- package/dist/core/entity/types/entity.interface.d.ts +30 -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.d.ts +16 -0
- package/dist/core/output/access.js +66 -0
- package/dist/core/output/coerce.js +38 -0
- package/dist/core/output/exceptions.d.ts +16 -0
- package/dist/core/output/exceptions.js +22 -0
- package/dist/core/output/http.d.ts +36 -0
- package/dist/core/output/http.js +351 -0
- package/dist/core/output/openapi-sink.d.ts +33 -4
- package/dist/core/output/openapi-sink.js +571 -0
- 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.d.ts +7 -2
- package/dist/core/output/stage.js +111 -0
- package/dist/core/output/status.d.ts +1 -1
- package/dist/core/output/status.js +52 -0
- package/dist/core/output/types/http.interface.d.ts +134 -0
- package/dist/core/output/types/output.interface.d.ts +49 -5
- package/dist/core/parse/issue.d.ts +28 -3
- package/dist/core/parse/issue.js +31 -0
- package/dist/core/parse/parse.d.ts +16 -0
- package/dist/core/parse/parse.js +323 -0
- package/dist/core/parse/strip.js +48 -0
- package/dist/core/property/abstract-property.d.ts +9 -0
- package/dist/core/property/abstract-property.js +37 -0
- package/dist/core/property/define-property.d.ts +34 -6
- 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/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 +8 -0
- 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 +17 -1
- package/dist/core/repository/cursor.js +83 -0
- package/dist/core/repository/exceptions.d.ts +13 -0
- package/dist/core/repository/exceptions.js +72 -0
- package/dist/core/repository/in-memory.d.ts +2 -2
- package/dist/core/repository/in-memory.js +196 -0
- package/dist/core/repository/migrate.d.ts +20 -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.d.ts +2 -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 +50 -5
- package/dist/core/repository/types/schema.interface.d.ts +132 -0
- package/dist/core/repository/upcast.d.ts +64 -0
- package/dist/core/repository/upcast.js +179 -0
- package/dist/core/schema/schema.d.ts +37 -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.d.ts +1 -1
- 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.d.ts +12 -0
- package/dist/core/use-case/access.js +56 -0
- package/dist/core/use-case/decorators.d.ts +40 -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.d.ts +18 -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 +233 -0
- 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/access.interface.d.ts +39 -0
- package/dist/core/use-case/types/route.interface.d.ts +86 -0
- package/dist/core/use-case/use-case.d.ts +127 -2
- package/dist/core/use-case/use-case.js +493 -0
- package/dist/experimental.d.ts +18 -1
- package/dist/experimental.js +18 -123
- package/dist/index.d.ts +49 -51
- package/dist/index.js +213 -443
- 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.d.ts +0 -2
- 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.d.ts +2 -2
- 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.d.ts +31 -0
- package/dist/std/resource.js +39 -0
- package/dist/std/runtime.d.ts +6 -0
- package/dist/std/runtime.js +193 -0
- package/dist/std/stages.js +4 -0
- package/dist/std/use-cases/auth.d.ts +5 -1
- package/dist/std/use-cases/auth.js +193 -0
- package/dist/std/use-cases/crud.js +261 -0
- package/dist/std/use-cases/index.d.ts +1 -1
- package/dist/std/use-cases/index.js +12 -0
- package/dist/std/use-cases/lifecycle.js +97 -0
- package/dist/std/use-cases/shared.d.ts +2 -2
- package/dist/std/use-cases/shared.js +232 -0
- package/dist/std/use-cases/types.d.ts +102 -2
- package/dist/std/use-cases/types.js +0 -0
- package/dist/testing/http-provider-contract.d.ts +32 -0
- package/dist/testing/http-provider-contract.js +344 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +3 -944
- package/dist/testing/memory-publisher.js +12 -0
- package/dist/testing/provider-contract.d.ts +26 -1
- package/dist/testing/provider-contract.js +1071 -0
- package/package.json +23 -3
- package/dist/index-qxfkyd6k.js +0 -1721
- package/dist/index-t5qwtyj5.js +0 -8194
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Kind } from "../symbols/kind.js";
|
|
2
2
|
/** What a log line carries besides its event. */
|
|
3
3
|
export type LogFields = Readonly<Record<string, unknown>>;
|
|
4
|
+
/** The severity of a log line, from the least to the most severe. */
|
|
4
5
|
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
6
|
+
/**
|
|
7
|
+
* The {@link LogLevel}s in order of severity, from `"debug"` to `"error"`.
|
|
8
|
+
*/
|
|
5
9
|
export declare const LogLevels: readonly LogLevel[];
|
|
6
10
|
/** What logs: an application, a use case through its `Logger` dependency, your code. */
|
|
7
11
|
export interface ILogger {
|
|
@@ -31,7 +35,11 @@ export interface ILogProvider {
|
|
|
31
35
|
}
|
|
32
36
|
/** The `log` of an application. */
|
|
33
37
|
export interface LogSettings {
|
|
34
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Lines below it are dropped.
|
|
40
|
+
*
|
|
41
|
+
* @default "info"
|
|
42
|
+
*/
|
|
35
43
|
readonly level?: LogLevel;
|
|
36
44
|
readonly providers: readonly ILogProvider[];
|
|
37
45
|
}
|
|
@@ -79,6 +87,11 @@ export declare function applicationLogger(application: string): {
|
|
|
79
87
|
export interface LoggerDeclaration {
|
|
80
88
|
readonly [Kind]: "logger";
|
|
81
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* The dependency that gives a use case the logger of its application:
|
|
92
|
+
* `dependencies: { logger: Logger }` makes `this.dependencies.logger` an {@link
|
|
93
|
+
* ILogger}.
|
|
94
|
+
*/
|
|
82
95
|
export declare const Logger: LoggerDeclaration;
|
|
83
96
|
export declare function isLoggerDeclaration(value: unknown): value is LoggerDeclaration;
|
|
84
97
|
/** Whether `value` can stand for a logger (an override in `execute`). */
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { startedApplication } from "../application/registry.js";
|
|
2
|
+
import { Exception, UNEXPECTED_ERROR } from "../exception/exception.js";
|
|
3
|
+
import { Kind } from "../symbols/kind.js";
|
|
4
|
+
import { InvalidLogDeclaration } from "./exceptions.js";
|
|
5
|
+
export const LogLevels = Object.freeze([
|
|
6
|
+
"debug",
|
|
7
|
+
"info",
|
|
8
|
+
"warn",
|
|
9
|
+
"error"
|
|
10
|
+
]);
|
|
11
|
+
const names = new WeakMap;
|
|
12
|
+
const invalid = (reason) => {
|
|
13
|
+
throw new InvalidLogDeclaration({ source: "log", details: { reason } });
|
|
14
|
+
};
|
|
15
|
+
export function logProviderOf(settings) {
|
|
16
|
+
if (!settings || typeof settings.write !== "function") {
|
|
17
|
+
invalid("Expected a log provider with a write function.");
|
|
18
|
+
}
|
|
19
|
+
if (settings.flush !== undefined && typeof settings.flush !== "function") {
|
|
20
|
+
invalid("Expected flush to be a function.");
|
|
21
|
+
}
|
|
22
|
+
if (settings.name !== undefined && (typeof settings.name !== "string" || settings.name.trim() === "")) {
|
|
23
|
+
invalid("Expected the name of a log provider to be a non-empty string.");
|
|
24
|
+
}
|
|
25
|
+
const provider = Object.freeze({
|
|
26
|
+
write: settings.write,
|
|
27
|
+
...settings.flush ? { flush: settings.flush } : {}
|
|
28
|
+
});
|
|
29
|
+
if (settings.name)
|
|
30
|
+
names.set(provider, settings.name);
|
|
31
|
+
return provider;
|
|
32
|
+
}
|
|
33
|
+
export function logProviderNameOf(provider) {
|
|
34
|
+
return names.get(provider);
|
|
35
|
+
}
|
|
36
|
+
const fieldsText = (fields) => Object.keys(fields).length > 0 ? ` ${JSON.stringify(fields)}` : "";
|
|
37
|
+
export function consoleLogProvider() {
|
|
38
|
+
return logProviderOf({
|
|
39
|
+
name: "console",
|
|
40
|
+
write: ({ at, level, event, fields, application }) => {
|
|
41
|
+
const line = `${at} ${level.toUpperCase()} [${application}] ${event}${fieldsText(fields)}`;
|
|
42
|
+
(level === "debug" ? console.debug : level === "info" ? console.info : level === "warn" ? console.warn : console.error)(line);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function jsonLogProvider() {
|
|
47
|
+
return logProviderOf({
|
|
48
|
+
name: "json",
|
|
49
|
+
write: (entry) => {
|
|
50
|
+
console.log(JSON.stringify({
|
|
51
|
+
at: entry.at,
|
|
52
|
+
level: entry.level,
|
|
53
|
+
application: entry.application,
|
|
54
|
+
event: entry.event,
|
|
55
|
+
...entry.fields
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export function memoryLogProvider() {
|
|
61
|
+
const entries = [];
|
|
62
|
+
const provider = Object.freeze({
|
|
63
|
+
write: (entry) => {
|
|
64
|
+
entries.push(entry);
|
|
65
|
+
},
|
|
66
|
+
entries,
|
|
67
|
+
clear: () => {
|
|
68
|
+
entries.length = 0;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
names.set(provider, "memory");
|
|
72
|
+
return provider;
|
|
73
|
+
}
|
|
74
|
+
export function checkLogSettings(settings) {
|
|
75
|
+
if (typeof settings !== "object" || settings === null) {
|
|
76
|
+
return invalid("Expected `log` to be { level?, providers }.");
|
|
77
|
+
}
|
|
78
|
+
const { level = "info", providers } = settings;
|
|
79
|
+
if (!LogLevels.includes(level)) {
|
|
80
|
+
invalid(`Expected the log level to be one of ${LogLevels.join(", ")}.`);
|
|
81
|
+
}
|
|
82
|
+
if (!Array.isArray(providers) || providers.some((provider) => typeof provider !== "object" || provider === null || typeof provider.write !== "function")) {
|
|
83
|
+
invalid("Expected `log.providers` to be a list of log providers.");
|
|
84
|
+
}
|
|
85
|
+
return Object.freeze({ level, providers: Object.freeze([...providers]) });
|
|
86
|
+
}
|
|
87
|
+
export const silentLogger = Object.freeze({
|
|
88
|
+
debug: () => {},
|
|
89
|
+
info: () => {},
|
|
90
|
+
warn: () => {},
|
|
91
|
+
error: () => {}
|
|
92
|
+
});
|
|
93
|
+
const rank = (level) => LogLevels.indexOf(level);
|
|
94
|
+
export function applicationLogger(application) {
|
|
95
|
+
let current;
|
|
96
|
+
const write = (level) => (event, fields = {}) => {
|
|
97
|
+
if (!current || rank(level) < rank(current.level))
|
|
98
|
+
return;
|
|
99
|
+
const entry = Object.freeze({
|
|
100
|
+
at: new Date().toISOString(),
|
|
101
|
+
level,
|
|
102
|
+
event,
|
|
103
|
+
fields,
|
|
104
|
+
application
|
|
105
|
+
});
|
|
106
|
+
for (const provider of current.providers) {
|
|
107
|
+
try {
|
|
108
|
+
const written = provider.write(entry);
|
|
109
|
+
if (written && typeof written.catch === "function") {
|
|
110
|
+
written.catch(() => {});
|
|
111
|
+
}
|
|
112
|
+
} catch {}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const logger = Object.freeze({
|
|
116
|
+
debug: write("debug"),
|
|
117
|
+
info: write("info"),
|
|
118
|
+
warn: write("warn"),
|
|
119
|
+
error: write("error")
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
logger,
|
|
123
|
+
get settings() {
|
|
124
|
+
return current;
|
|
125
|
+
},
|
|
126
|
+
set(settings) {
|
|
127
|
+
current = settings;
|
|
128
|
+
},
|
|
129
|
+
async flush() {
|
|
130
|
+
await Promise.all((current?.providers ?? []).map((provider) => Promise.resolve().then(() => provider.flush?.()).catch(() => {})));
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export const Logger = Object.freeze({
|
|
135
|
+
[Kind]: "logger"
|
|
136
|
+
});
|
|
137
|
+
export function isLoggerDeclaration(value) {
|
|
138
|
+
return value === Logger;
|
|
139
|
+
}
|
|
140
|
+
export function isLogger(value) {
|
|
141
|
+
return typeof value === "object" && value !== null && ["debug", "info", "warn", "error"].every((level) => typeof value[level] === "function");
|
|
142
|
+
}
|
|
143
|
+
export const ApplicationLoggerKey = Symbol("afora.application.logger");
|
|
144
|
+
export function resolveLogger(scoped, owner) {
|
|
145
|
+
return scoped ?? owner?.[ApplicationLoggerKey] ?? startedApplication()?.[ApplicationLoggerKey];
|
|
146
|
+
}
|
|
147
|
+
export function clock() {
|
|
148
|
+
return globalThis.performance?.now() ?? Date.now();
|
|
149
|
+
}
|
|
150
|
+
export function elapsed(since) {
|
|
151
|
+
return Math.round(clock() - since);
|
|
152
|
+
}
|
|
153
|
+
const logged = new WeakSet;
|
|
154
|
+
export function logFailure(logger, event, fields, error) {
|
|
155
|
+
if (!logger)
|
|
156
|
+
return;
|
|
157
|
+
const exception = error instanceof Exception ? error : undefined;
|
|
158
|
+
const key = exception?.origin ?? error;
|
|
159
|
+
if (typeof key === "object" && key !== null) {
|
|
160
|
+
if (logged.has(key))
|
|
161
|
+
return;
|
|
162
|
+
logged.add(key);
|
|
163
|
+
}
|
|
164
|
+
const unexpected = !exception || exception.origin.name === UNEXPECTED_ERROR;
|
|
165
|
+
try {
|
|
166
|
+
logger[unexpected ? "error" : "warn"](event, {
|
|
167
|
+
...fields,
|
|
168
|
+
exception: exception ? exception.toJSON() : { message: error instanceof Error ? error.message : String(error) }
|
|
169
|
+
});
|
|
170
|
+
} catch {}
|
|
171
|
+
}
|
|
172
|
+
export function writeLog(logger, level, event, fields) {
|
|
173
|
+
if (!logger)
|
|
174
|
+
return;
|
|
175
|
+
try {
|
|
176
|
+
logger[level](event, fields);
|
|
177
|
+
} catch {}
|
|
178
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ExceptionLayers } from "../exception/enums/exception-layers.js";
|
|
2
|
+
import { exceptionOf } from "../exception/exception.js";
|
|
3
|
+
export const InvalidOpenAPIModel = exceptionOf({
|
|
4
|
+
layer: ExceptionLayers.Output,
|
|
5
|
+
name: "InvalidOpenAPIModel",
|
|
6
|
+
message: ({ name }) => `"${name}" is not a property, blueprint, list, record or entity.`
|
|
7
|
+
});
|
|
@@ -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,16 @@
|
|
|
1
|
+
import type { AnyBlueprint } from "../blueprint/types/blueprint.interface.js";
|
|
2
|
+
import type { AccessDeclaration } from "../use-case/types/access.interface.js";
|
|
3
|
+
import type { OperationCallOptions } from "./types/output.interface.js";
|
|
4
|
+
/** What `command.run` takes: the call options without the caller. */
|
|
5
|
+
type RunOptions = {
|
|
6
|
+
readonly context?: unknown;
|
|
7
|
+
} | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* The check of `access` for the operation `id`, made once: given the input
|
|
10
|
+
* and the options of a call, it throws `Unauthenticated` or `Forbidden`, or
|
|
11
|
+
* gives the options to run the command with (the context filled from the
|
|
12
|
+
* caller). Throws `InvalidOutputStage` when `access.context` names a key the
|
|
13
|
+
* `context` of the operation does not have.
|
|
14
|
+
*/
|
|
15
|
+
export declare function accessGuardOf(id: string, access: AccessDeclaration, context: AnyBlueprint | undefined): (input: unknown, options: OperationCallOptions | undefined) => RunOptions;
|
|
16
|
+
export {};
|
|
@@ -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
|
+
}
|
|
@@ -6,3 +6,19 @@ export declare const InvalidOutputStage: import("../../index.js").DeclaredExcept
|
|
|
6
6
|
export declare const DuplicateOperation: import("../../index.js").DeclaredExceptionClass<{
|
|
7
7
|
readonly id: string;
|
|
8
8
|
}>;
|
|
9
|
+
/**
|
|
10
|
+
* An operation that declares its `access` was called without an
|
|
11
|
+
* authenticated caller: no credentials, or ones that were not accepted. A
|
|
12
|
+
* 401.
|
|
13
|
+
*/
|
|
14
|
+
export declare const Unauthenticated: import("../../index.js").DeclaredExceptionClass<undefined>;
|
|
15
|
+
/**
|
|
16
|
+
* The caller is authenticated but may not call the operation: it lacks a
|
|
17
|
+
* scope the `access` of the operation asks for (`details.reason` is
|
|
18
|
+
* `"scope"`, with the missing `scopes`), or is not the owner (`"owner"`). A
|
|
19
|
+
* 403.
|
|
20
|
+
*/
|
|
21
|
+
export declare const Forbidden: import("../../index.js").DeclaredExceptionClass<{
|
|
22
|
+
readonly reason: "scope" | "owner";
|
|
23
|
+
readonly scopes?: readonly string[];
|
|
24
|
+
}>;
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CompileRoutesSettings, HttpOptions, RouteTable } from "./types/http.interface.js";
|
|
2
|
+
import type { OperationDescriptor, StageContext } from "./types/output.interface.js";
|
|
3
|
+
/** The option names of {@link HttpOptions}, for sinks that list their options. */
|
|
4
|
+
export declare const HTTP_OPTION_KEYS: readonly ["basePath", "route", "responses", "requestId", "security", "docs", "debug", "context", "auth"];
|
|
5
|
+
/**
|
|
6
|
+
* Why the {@link HttpOptions} are not valid, naming them `where.<option>`;
|
|
7
|
+
* `undefined` when they are. Other keys of `options` are not looked at.
|
|
8
|
+
*/
|
|
9
|
+
export declare function httpOptionsIssue(options: HttpOptions, where: string): string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* The routes of `operations`, compiled once, for a sink that serves HTTP on
|
|
12
|
+
* any server: the sink routes each `method` and `path` to `handle` and sends
|
|
13
|
+
* the result as is. Everything a request needs is decided here, so a call
|
|
14
|
+
* only reads, coerces and runs:
|
|
15
|
+
*
|
|
16
|
+
* - The input is the JSON body of a `post`, `put` or `patch` and the query
|
|
17
|
+
* of a `get` or `delete`, with the keys the route reads elsewhere over it:
|
|
18
|
+
* `{name}` segments of the path, and the query keys, headers and cookies
|
|
19
|
+
* the use case route declares. Text is coerced to the numbers and booleans
|
|
20
|
+
* the input takes; the operation validates it (a 400 problem with `issues`).
|
|
21
|
+
* - The context comes from `context.headers` (coerced) and
|
|
22
|
+
* `context.variables` (as is); a missing one leaves the key out.
|
|
23
|
+
* - An operation with an `access` other than `public` asks
|
|
24
|
+
* `settings.authenticator` who calls; without one, or without credentials,
|
|
25
|
+
* it is a 401.
|
|
26
|
+
* - Success is the route status with the response as JSON (sensitive values
|
|
27
|
+
* masked by `toJSON`), or no body for `204`, plus the `responses` headers.
|
|
28
|
+
* Errors are `application/problem+json`, with the status of `statusOf`
|
|
29
|
+
* (the output `statuses` first). `requestId` puts the request id on every
|
|
30
|
+
* result, `notFound` and `failure` included.
|
|
31
|
+
* - `docs.path` serves the `openapi` document of the same operations.
|
|
32
|
+
*
|
|
33
|
+
* Throws `InvalidOutputStage` (source `settings.source`) as `operationRoutes`
|
|
34
|
+
* does, and when `docs.path` is taken by a `get` route.
|
|
35
|
+
*/
|
|
36
|
+
export declare function compileRoutes(operations: readonly OperationDescriptor[], options: HttpOptions, context: StageContext, settings: CompileRoutesSettings): RouteTable;
|