@afora/core 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -30
- package/dist/adapters.js +31 -55
- package/dist/core/application/application.d.ts +30 -0
- package/dist/core/application/application.js +1291 -0
- package/dist/core/application/exceptions.js +85 -0
- package/dist/core/application/migrate.js +67 -0
- package/dist/core/application/providers.js +108 -0
- package/dist/core/application/registry.js +23 -0
- package/dist/core/application/schema.js +398 -0
- package/dist/core/application/start.js +162 -0
- package/dist/core/application/types/application.interface.d.ts +23 -3
- package/dist/core/auth/jwt.js +200 -0
- package/dist/core/auth/types/jwt.interface.d.ts +10 -2
- package/dist/core/blueprint/blueprint-plugin.js +7 -0
- package/dist/core/blueprint/blueprint.js +146 -0
- package/dist/core/blueprint/callable.js +13 -0
- package/dist/core/blueprint/exceptions.js +20 -0
- package/dist/core/blueprint/is-blueprint.js +4 -0
- package/dist/core/blueprint/list.js +19 -0
- package/dist/core/blueprint/optional.js +69 -0
- package/dist/core/blueprint/record-field.js +14 -0
- package/dist/core/blueprint/slice.js +127 -0
- package/dist/core/blueprint/types/blueprint.interface.d.ts +16 -0
- package/dist/core/blueprint/variant.js +72 -0
- package/dist/core/command/command.js +428 -0
- package/dist/core/command/exceptions.js +30 -0
- package/dist/core/command/types/command.interface.d.ts +10 -1
- package/dist/core/entity/abstract-entity.d.ts +19 -0
- package/dist/core/entity/abstract-entity.js +181 -0
- package/dist/core/entity/entity.js +209 -0
- package/dist/core/entity/event.d.ts +39 -0
- package/dist/core/entity/event.js +80 -0
- package/dist/core/entity/exceptions.js +27 -0
- package/dist/core/entity/trust.js +7 -0
- package/dist/core/entity/version.js +13 -0
- package/dist/core/environment/environment.d.ts +26 -3
- package/dist/core/environment/environment.js +71 -0
- package/dist/core/event-policy/event-policy.js +76 -0
- package/dist/core/event-policy/exceptions.js +12 -0
- package/dist/core/event-policy/publish.js +98 -0
- package/dist/core/event-policy/publishers.js +18 -0
- package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
- package/dist/core/exception/enums/exception-layers.js +15 -0
- package/dist/core/exception/exception.d.ts +15 -0
- package/dist/core/exception/exception.js +128 -0
- package/dist/core/exception/exported.js +49 -0
- package/dist/core/exception/guard.d.ts +20 -1
- package/dist/core/exception/guard.js +16 -0
- package/dist/core/exception/types/exception.interface.d.ts +13 -0
- package/dist/core/json.js +19 -0
- package/dist/core/logger/exceptions.js +7 -0
- package/dist/core/logger/logger.d.ts +14 -1
- package/dist/core/logger/logger.js +178 -0
- package/dist/core/openapi/exceptions.js +7 -0
- package/dist/core/openapi/openapi.js +132 -0
- package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
- package/dist/core/output/access.js +66 -0
- package/dist/core/output/coerce.js +38 -0
- package/dist/core/output/exceptions.js +22 -0
- package/dist/{index-y280ew0k.js → core/output/http.js} +35 -80
- package/dist/core/output/openapi-sink.d.ts +10 -2
- package/dist/{index-hg1z2s2v.js → core/output/openapi-sink.js} +60 -544
- package/dist/core/output/output.js +192 -0
- package/dist/core/output/problem.js +30 -0
- package/dist/core/output/select.js +28 -0
- package/dist/core/output/stage.js +111 -0
- package/dist/core/output/status.js +52 -0
- package/dist/core/output/types/http.interface.d.ts +5 -1
- package/dist/core/output/types/output.interface.d.ts +24 -0
- package/dist/core/parse/issue.js +31 -0
- package/dist/core/parse/parse.d.ts +8 -1
- package/dist/core/parse/parse.js +323 -0
- package/dist/core/parse/strip.js +48 -0
- package/dist/core/property/abstract-property.js +37 -0
- package/dist/core/property/define-property.js +209 -0
- package/dist/core/property/enums/property-states.js +8 -0
- package/dist/core/property/exceptions.js +12 -0
- package/dist/core/property/input.js +10 -0
- package/dist/core/property/is-property.js +3 -0
- package/dist/core/property/lineage.js +8 -0
- package/dist/core/property/property.d.ts +27 -0
- package/dist/core/property/property.js +46 -0
- package/dist/core/property/refine.js +103 -0
- package/dist/core/record/abstract-record.d.ts +2 -1
- package/dist/core/record/abstract-record.js +307 -0
- package/dist/core/record/exceptions.js +49 -0
- package/dist/core/record/marks.js +7 -0
- package/dist/core/record/mask.js +60 -0
- package/dist/core/record/name.js +23 -0
- package/dist/core/record/plugin-catalog.js +115 -0
- package/dist/core/record/plugin.js +198 -0
- package/dist/core/record/record.js +117 -0
- package/dist/core/record/types/record.interface.d.ts +5 -0
- package/dist/core/repository/cursor.js +83 -0
- package/dist/core/repository/exceptions.js +72 -0
- package/dist/core/repository/in-memory.js +196 -0
- package/dist/core/repository/migrate.js +254 -0
- package/dist/core/repository/plan.js +27 -0
- package/dist/core/repository/provider.js +29 -0
- package/dist/core/repository/query.js +153 -0
- package/dist/core/repository/references.js +196 -0
- package/dist/core/repository/repository.d.ts +22 -2
- package/dist/core/repository/repository.js +720 -0
- package/dist/core/repository/state.js +138 -0
- package/dist/core/repository/types/query.interface.d.ts +13 -0
- package/dist/core/repository/types/repository.interface.d.ts +20 -8
- package/dist/core/repository/types/schema.interface.d.ts +8 -2
- package/dist/core/repository/upcast.js +179 -0
- package/dist/core/schema/schema.d.ts +9 -0
- package/dist/core/schema/schema.js +542 -0
- package/dist/core/standard/standard-schema.js +1 -0
- package/dist/core/standard/standard.js +23 -0
- package/dist/core/symbols/demo.d.ts +4 -0
- package/dist/core/symbols/demo.js +1 -0
- package/dist/core/symbols/kind.js +1 -0
- package/dist/core/symbols/meta.js +1 -0
- package/dist/core/symbols/state.d.ts +5 -0
- package/dist/core/symbols/state.js +1 -0
- package/dist/core/tracing/exceptions.d.ts +4 -0
- package/dist/core/tracing/exceptions.js +7 -0
- package/dist/core/tracing/tracing.d.ts +72 -0
- package/dist/core/tracing/tracing.js +101 -0
- package/dist/core/use-case/access.js +56 -0
- package/dist/core/use-case/decorators.js +65 -0
- package/dist/core/use-case/exceptions.d.ts +5 -1
- package/dist/core/use-case/exceptions.js +48 -0
- package/dist/core/use-case/plugin-catalog.js +68 -0
- package/dist/core/use-case/plugin.js +133 -0
- package/dist/core/use-case/route.js +200 -0
- package/dist/core/use-case/scope.d.ts +5 -0
- package/dist/core/use-case/scope.js +4 -0
- package/dist/core/use-case/steps/describe.d.ts +5 -1
- package/dist/core/use-case/steps/describe.js +24 -0
- package/dist/core/use-case/steps/references.js +105 -0
- package/dist/core/use-case/steps/run.js +139 -0
- package/dist/core/use-case/steps/types/check.interface.d.ts +27 -8
- package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
- package/dist/core/use-case/steps/validate.js +368 -0
- package/dist/core/use-case/target.js +100 -0
- package/dist/core/use-case/types/route.interface.d.ts +25 -5
- package/dist/core/use-case/use-case.d.ts +77 -2
- package/dist/core/use-case/use-case.js +493 -0
- package/dist/experimental.d.ts +5 -0
- package/dist/experimental.js +18 -161
- package/dist/index.d.ts +7 -5
- package/dist/index.js +213 -367
- package/dist/std/blueprint-plugin.js +42 -0
- package/dist/std/blueprint.d.ts +21 -0
- package/dist/std/blueprint.js +3 -0
- package/dist/std/command.js +3 -0
- package/dist/std/entity.d.ts +20 -1
- package/dist/std/entity.js +30 -0
- package/dist/std/event-policies.js +3 -0
- package/dist/std/properties/contact.d.ts +3 -0
- package/dist/std/properties/contact.js +29 -0
- package/dist/std/properties/enum.js +26 -0
- package/dist/std/properties/exceptions.js +14 -0
- package/dist/std/properties/headless.js +45 -0
- package/dist/std/properties/identity.js +28 -0
- package/dist/std/properties/json.js +161 -0
- package/dist/std/properties/password.js +79 -0
- package/dist/std/properties/primitives.d.ts +4 -0
- package/dist/std/properties/primitives.js +28 -0
- package/dist/std/properties/time.js +55 -0
- package/dist/std/record-plugins.js +73 -0
- package/dist/std/record.d.ts +20 -1
- package/dist/std/record.js +3 -0
- package/dist/std/resource.js +39 -0
- package/dist/std/runtime.js +193 -0
- package/dist/std/stages.js +4 -0
- package/dist/std/use-cases/auth.js +193 -0
- package/dist/std/use-cases/crud.js +261 -0
- package/dist/std/use-cases/index.js +12 -0
- package/dist/std/use-cases/lifecycle.js +97 -0
- package/dist/std/use-cases/shared.js +232 -0
- package/dist/std/use-cases/types.d.ts +5 -1
- package/dist/std/use-cases/types.js +0 -0
- package/dist/testing/http-provider-contract.js +344 -0
- package/dist/testing/index.js +3 -1435
- package/dist/testing/memory-publisher.js +12 -0
- package/dist/testing/provider-contract.d.ts +12 -3
- package/dist/testing/provider-contract.js +1071 -0
- package/package.json +10 -3
- package/dist/index-3aae5jfq.js +0 -2997
- package/dist/index-ny3as79r.js +0 -4567
- package/dist/index-xegv6a1p.js +0 -4291
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { CommandEventFailed } from "../command/exceptions.js";
|
|
2
|
+
import { logFailure } from "../logger/logger.js";
|
|
3
|
+
import { eventPolicyOf } from "./event-policy.js";
|
|
4
|
+
const KEYS = ["publisher", "topic", "events", "handle", "onPublishError"];
|
|
5
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
6
|
+
const PLACEHOLDER = /\{(event|command|operation|id)\}/g;
|
|
7
|
+
function validate(options) {
|
|
8
|
+
const given = options;
|
|
9
|
+
for (const key of Object.keys(given)) {
|
|
10
|
+
if (!KEYS.includes(key))
|
|
11
|
+
return `publishEvents has no option "${key}".`;
|
|
12
|
+
}
|
|
13
|
+
const { publisher, topic, events, handle, onPublishError } = given;
|
|
14
|
+
if (typeof publisher !== "string" || !IDENTIFIER.test(publisher)) {
|
|
15
|
+
return "Expected publishEvents.publisher to be the key of a publisher of the application.";
|
|
16
|
+
}
|
|
17
|
+
if (typeof topic !== "string" || topic === "") {
|
|
18
|
+
return 'Expected publishEvents.topic to be a non-empty string, such as "posts/{id}".';
|
|
19
|
+
}
|
|
20
|
+
const unknown = topic.replaceAll(PLACEHOLDER, "").match(/\{[^}]*\}/);
|
|
21
|
+
if (unknown) {
|
|
22
|
+
return `publishEvents.topic has an unknown placeholder ${unknown[0]}: use {event}, {command}, {operation} or {id}.`;
|
|
23
|
+
}
|
|
24
|
+
if (events !== undefined && (!Array.isArray(events) || events.some((name) => typeof name !== "string" || name === ""))) {
|
|
25
|
+
return "Expected publishEvents.events to be a list of event names.";
|
|
26
|
+
}
|
|
27
|
+
if (handle !== undefined && typeof handle !== "boolean") {
|
|
28
|
+
return "Expected publishEvents.handle to be a boolean.";
|
|
29
|
+
}
|
|
30
|
+
if (onPublishError !== undefined && onPublishError !== "log" && onPublishError !== "throw") {
|
|
31
|
+
return 'Expected publishEvents.onPublishError to be "log" or "throw".';
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
function entityJson(event) {
|
|
36
|
+
const text = JSON.stringify(event.entity);
|
|
37
|
+
return text === undefined ? null : JSON.parse(text);
|
|
38
|
+
}
|
|
39
|
+
export const publishEvents = eventPolicyOf({
|
|
40
|
+
name: "publishEvents",
|
|
41
|
+
validate,
|
|
42
|
+
publishers: (options) => [options.publisher],
|
|
43
|
+
handle: async (events, info, options) => {
|
|
44
|
+
const { command, operation, logger } = info;
|
|
45
|
+
const commandName = command.commandName;
|
|
46
|
+
const source = `${commandName}.${operation}`;
|
|
47
|
+
const only = options.events ? new Set(options.events) : undefined;
|
|
48
|
+
const failed = (cause, event, index) => new CommandEventFailed({
|
|
49
|
+
source,
|
|
50
|
+
cause,
|
|
51
|
+
details: {
|
|
52
|
+
operation,
|
|
53
|
+
event: event.name,
|
|
54
|
+
handled: index,
|
|
55
|
+
remaining: events.length - index - 1
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
for (const [index, event] of events.entries()) {
|
|
59
|
+
if (options.handle !== false) {
|
|
60
|
+
try {
|
|
61
|
+
await event.handle({ logger });
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
throw failed(cause, event, index);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (only && !only.has(event.name))
|
|
67
|
+
continue;
|
|
68
|
+
let publisher;
|
|
69
|
+
try {
|
|
70
|
+
publisher = info.publisher(options.publisher);
|
|
71
|
+
} catch (cause) {
|
|
72
|
+
throw failed(cause, event, index);
|
|
73
|
+
}
|
|
74
|
+
const entity = entityJson(event);
|
|
75
|
+
const id = entity?.id;
|
|
76
|
+
const topic = options.topic.replaceAll(PLACEHOLDER, (_, key) => key === "event" ? event.name : key === "command" ? commandName : key === "operation" ? operation : id === undefined ? "" : String(id));
|
|
77
|
+
const published = Object.freeze({
|
|
78
|
+
name: event.name,
|
|
79
|
+
command: commandName,
|
|
80
|
+
operation,
|
|
81
|
+
entity
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
await publisher.publish(topic, published);
|
|
85
|
+
} catch (cause) {
|
|
86
|
+
if (options.onPublishError === "throw")
|
|
87
|
+
throw failed(cause, event, index);
|
|
88
|
+
logFailure(logger, "event-publish-failed", {
|
|
89
|
+
command: commandName,
|
|
90
|
+
operation,
|
|
91
|
+
event: event.name,
|
|
92
|
+
publisher: options.publisher,
|
|
93
|
+
topic
|
|
94
|
+
}, cause);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { startedApplication } from "../application/registry.js";
|
|
2
|
+
import { PublisherNotBound } from "./exceptions.js";
|
|
3
|
+
export const ApplicationPublishersKey = Symbol("afora.application.publishers");
|
|
4
|
+
const boundPublishers = new WeakMap;
|
|
5
|
+
export function bindPublishers(command, publishers) {
|
|
6
|
+
boundPublishers.set(command, publishers);
|
|
7
|
+
}
|
|
8
|
+
export function isEventPublisher(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && typeof value.publish === "function";
|
|
10
|
+
}
|
|
11
|
+
export function resolvePublisher(command, name, source) {
|
|
12
|
+
const publishers = boundPublishers.get(command) ?? startedApplication()?.[ApplicationPublishersKey];
|
|
13
|
+
const publisher = publishers && Object.hasOwn(publishers, name) ? publishers[name] : undefined;
|
|
14
|
+
if (!publisher) {
|
|
15
|
+
throw new PublisherNotBound({ source, details: { publisher: name } });
|
|
16
|
+
}
|
|
17
|
+
return publisher;
|
|
18
|
+
}
|
|
@@ -61,12 +61,18 @@ export interface PublishEventsOptions {
|
|
|
61
61
|
readonly topic: string;
|
|
62
62
|
/** The names of the events to publish; all of them by default. */
|
|
63
63
|
readonly events?: readonly string[];
|
|
64
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Whether each event runs its own handler first, as `"after-commit"` does.
|
|
66
|
+
*
|
|
67
|
+
* @default true
|
|
68
|
+
*/
|
|
65
69
|
readonly handle?: boolean;
|
|
66
70
|
/**
|
|
67
|
-
* What a failed publish does: `"log"`
|
|
71
|
+
* What a failed publish does: `"log"` writes an error to the
|
|
68
72
|
* logger and goes on, since the call already committed; `"throw"` makes
|
|
69
73
|
* the call fail with `CommandEventFailed`.
|
|
74
|
+
*
|
|
75
|
+
* @default "log"
|
|
70
76
|
*/
|
|
71
77
|
readonly onPublishError?: "log" | "throw";
|
|
72
78
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export var ExceptionLayers;
|
|
2
|
+
((ExceptionLayers) => {
|
|
3
|
+
ExceptionLayers["Schema"] = "schema";
|
|
4
|
+
ExceptionLayers["Property"] = "property";
|
|
5
|
+
ExceptionLayers["Blueprint"] = "blueprint";
|
|
6
|
+
ExceptionLayers["Record"] = "record";
|
|
7
|
+
ExceptionLayers["Entity"] = "entity";
|
|
8
|
+
ExceptionLayers["UseCase"] = "use-case";
|
|
9
|
+
ExceptionLayers["Repository"] = "repository";
|
|
10
|
+
ExceptionLayers["Environment"] = "environment";
|
|
11
|
+
ExceptionLayers["Event"] = "event";
|
|
12
|
+
ExceptionLayers["Command"] = "command";
|
|
13
|
+
ExceptionLayers["Application"] = "application";
|
|
14
|
+
ExceptionLayers["Output"] = "output";
|
|
15
|
+
})(ExceptionLayers ||= {});
|
|
@@ -64,5 +64,20 @@ export declare function declaredMessageOf(Declared: DeclaredExceptionClass<never
|
|
|
64
64
|
* Declares an exception of `layer`, e.g.
|
|
65
65
|
* `throw new PropertyRequired({ source: "email" })`. Its `details` are typed
|
|
66
66
|
* by the parameter of `message`, when it is a function.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* import { ExceptionLayers, exceptionOf } from "@afora/core";
|
|
71
|
+
*
|
|
72
|
+
* const UserNotFound = exceptionOf({
|
|
73
|
+
* layer: ExceptionLayers.UseCase,
|
|
74
|
+
* name: "UserNotFound",
|
|
75
|
+
* message: ({ id }: { id: string }) => `There is no user ${id}.`,
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* const error = new UserNotFound({ source: "FindUser", details: { id: "42" } });
|
|
79
|
+
* error.message; // "There is no user 42."
|
|
80
|
+
* error.layer; // ExceptionLayers.UseCase
|
|
81
|
+
* ```
|
|
67
82
|
*/
|
|
68
83
|
export declare function exceptionOf<Details = undefined>({ layer, name, message, }: ExceptionDeclaration<Details>): DeclaredExceptionClass<Details>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export const UNEXPECTED_ERROR = "UnexpectedError";
|
|
2
|
+
|
|
3
|
+
export class Exception extends Error {
|
|
4
|
+
name;
|
|
5
|
+
layer;
|
|
6
|
+
source;
|
|
7
|
+
details;
|
|
8
|
+
cause;
|
|
9
|
+
constructor({
|
|
10
|
+
layer,
|
|
11
|
+
name,
|
|
12
|
+
source,
|
|
13
|
+
message,
|
|
14
|
+
details,
|
|
15
|
+
cause
|
|
16
|
+
}) {
|
|
17
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
18
|
+
this.name = name;
|
|
19
|
+
this.layer = layer;
|
|
20
|
+
this.source = source;
|
|
21
|
+
this.details = details;
|
|
22
|
+
this.cause = cause;
|
|
23
|
+
}
|
|
24
|
+
static from(error, scope) {
|
|
25
|
+
if (error instanceof Exception)
|
|
26
|
+
return error;
|
|
27
|
+
return new Exception({
|
|
28
|
+
...scope,
|
|
29
|
+
name: UNEXPECTED_ERROR,
|
|
30
|
+
message: error instanceof Error ? error.message : String(error),
|
|
31
|
+
cause: error
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
get frame() {
|
|
35
|
+
const { layer, name, source, message, details } = this;
|
|
36
|
+
return { layer, name, source, message, details };
|
|
37
|
+
}
|
|
38
|
+
get trace() {
|
|
39
|
+
return this.chain().map((exception) => exception.frame).reverse();
|
|
40
|
+
}
|
|
41
|
+
get origin() {
|
|
42
|
+
return this.chain().at(-1);
|
|
43
|
+
}
|
|
44
|
+
find(target) {
|
|
45
|
+
return this.chain().find((exception) => typeof target === "string" ? exception.name === target : exception instanceof target);
|
|
46
|
+
}
|
|
47
|
+
is(target) {
|
|
48
|
+
return this.find(target) !== undefined;
|
|
49
|
+
}
|
|
50
|
+
passedThrough(layer) {
|
|
51
|
+
return this.chain().some((exception) => exception.layer === layer);
|
|
52
|
+
}
|
|
53
|
+
propagate({
|
|
54
|
+
layer,
|
|
55
|
+
source,
|
|
56
|
+
name = this.name,
|
|
57
|
+
message = this.message,
|
|
58
|
+
details
|
|
59
|
+
}) {
|
|
60
|
+
const keepsClass = name === this.name && details === undefined && isDeclaredInstance(this);
|
|
61
|
+
const init = {
|
|
62
|
+
layer,
|
|
63
|
+
source,
|
|
64
|
+
name,
|
|
65
|
+
message,
|
|
66
|
+
details: keepsClass ? this.details : details,
|
|
67
|
+
cause: this
|
|
68
|
+
};
|
|
69
|
+
return keepsClass ? Reflect.construct(Exception, [init], this.constructor) : new Exception(init);
|
|
70
|
+
}
|
|
71
|
+
toJSON() {
|
|
72
|
+
return { ...this.frame, trace: this.trace };
|
|
73
|
+
}
|
|
74
|
+
toString() {
|
|
75
|
+
return this.chain().map((exception, index) => `${index === 0 ? "" : " <- "}${exception.name} [${exception.layer} @ ${exception.source}]: ${exception.message}`).join(`
|
|
76
|
+
`);
|
|
77
|
+
}
|
|
78
|
+
chain() {
|
|
79
|
+
const chain = [];
|
|
80
|
+
for (let current = this;current instanceof Exception; current = current.cause) {
|
|
81
|
+
chain.push(current);
|
|
82
|
+
}
|
|
83
|
+
return chain;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const declarations = new WeakMap;
|
|
87
|
+
function isDeclaredInstance(exception) {
|
|
88
|
+
for (let current = exception.constructor;typeof current === "function"; current = Object.getPrototypeOf(current)) {
|
|
89
|
+
if (declarations.has(current))
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
export function isDeclaredException(value) {
|
|
95
|
+
return typeof value === "function" && declarations.has(value);
|
|
96
|
+
}
|
|
97
|
+
export function declaredMessageOf(Declared) {
|
|
98
|
+
const { message } = declarations.get(Declared) ?? {};
|
|
99
|
+
return typeof message === "string" ? message : undefined;
|
|
100
|
+
}
|
|
101
|
+
export function exceptionOf({
|
|
102
|
+
layer,
|
|
103
|
+
name,
|
|
104
|
+
message
|
|
105
|
+
}) {
|
|
106
|
+
|
|
107
|
+
class DeclaredException extends Exception {
|
|
108
|
+
static layer = layer;
|
|
109
|
+
constructor({
|
|
110
|
+
source,
|
|
111
|
+
details,
|
|
112
|
+
cause,
|
|
113
|
+
message: custom
|
|
114
|
+
}) {
|
|
115
|
+
super({
|
|
116
|
+
layer,
|
|
117
|
+
name,
|
|
118
|
+
source,
|
|
119
|
+
details,
|
|
120
|
+
cause,
|
|
121
|
+
message: custom ?? (typeof message === "function" ? message(details, source) : message)
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
Object.defineProperty(DeclaredException, "name", { value: name });
|
|
126
|
+
declarations.set(DeclaredException, { message });
|
|
127
|
+
return DeclaredException;
|
|
128
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as Application from "../application/exceptions.js";
|
|
2
|
+
import * as Blueprint from "../blueprint/exceptions.js";
|
|
3
|
+
import * as Command from "../command/exceptions.js";
|
|
4
|
+
import * as Entity from "../entity/exceptions.js";
|
|
5
|
+
import * as Environment from "../environment/environment.js";
|
|
6
|
+
import * as EventPolicy from "../event-policy/exceptions.js";
|
|
7
|
+
import * as Log from "../logger/exceptions.js";
|
|
8
|
+
import * as OpenAPI from "../openapi/exceptions.js";
|
|
9
|
+
import * as Output from "../output/exceptions.js";
|
|
10
|
+
import * as Property from "../property/exceptions.js";
|
|
11
|
+
import * as RecordKeys from "../record/exceptions.js";
|
|
12
|
+
import * as Repository from "../repository/exceptions.js";
|
|
13
|
+
import * as Tracing from "../tracing/exceptions.js";
|
|
14
|
+
import * as UseCaseSteps from "../use-case/exceptions.js";
|
|
15
|
+
import * as UseCase from "../use-case/use-case.js";
|
|
16
|
+
import { isDeclaredException } from "./exception.js";
|
|
17
|
+
const exported = new WeakSet;
|
|
18
|
+
let collected = false;
|
|
19
|
+
export function markExported(...classes) {
|
|
20
|
+
for (const Declared of classes) {
|
|
21
|
+
if (isDeclaredException(Declared))
|
|
22
|
+
exported.add(Declared);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function isExportedException(Declared) {
|
|
26
|
+
if (!collected) {
|
|
27
|
+
collected = true;
|
|
28
|
+
for (const module of [
|
|
29
|
+
Application,
|
|
30
|
+
Blueprint,
|
|
31
|
+
Command,
|
|
32
|
+
Entity,
|
|
33
|
+
Environment,
|
|
34
|
+
EventPolicy,
|
|
35
|
+
Log,
|
|
36
|
+
OpenAPI,
|
|
37
|
+
Output,
|
|
38
|
+
Property,
|
|
39
|
+
RecordKeys,
|
|
40
|
+
Repository,
|
|
41
|
+
Tracing,
|
|
42
|
+
UseCase,
|
|
43
|
+
UseCaseSteps
|
|
44
|
+
]) {
|
|
45
|
+
markExported(...Object.values(module));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return typeof Declared === "function" && exported.has(Declared);
|
|
49
|
+
}
|
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import { Exception } from "./exception.js";
|
|
2
2
|
import type { ExceptionScope } from "./types/exception.interface.js";
|
|
3
3
|
/**
|
|
4
|
-
* Handles what `run` throws, or rejects with, in `scope
|
|
4
|
+
* Handles what `run` throws, or rejects with, in `scope` (see {@link ExceptionScope}). Without `resolve`,
|
|
5
5
|
* or when it calls `pass`, the exception goes up to the next layer with its
|
|
6
6
|
* trace; otherwise `resolve` returns the value to use instead.
|
|
7
|
+
*
|
|
8
|
+
* @throws {@link Exception} what `run` throws or rejects with, in `scope` and with its trace, unless `resolve` gives a value instead.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { ExceptionLayers, guard } from "@afora/core";
|
|
13
|
+
*
|
|
14
|
+
* const scope = { layer: ExceptionLayers.UseCase, source: "LoadConfig" };
|
|
15
|
+
*
|
|
16
|
+
* // Without `resolve`, what `run` throws goes up to the next layer with its trace
|
|
17
|
+
* guard(scope, () => JSON.parse('{"port":3000}'));
|
|
18
|
+
*
|
|
19
|
+
* // With `resolve`, it can give a value instead, or call `pass` to go up
|
|
20
|
+
* const config = guard(
|
|
21
|
+
* scope,
|
|
22
|
+
* () => JSON.parse("{") as { port: number },
|
|
23
|
+
* () => ({ port: 8080 }),
|
|
24
|
+
* );
|
|
25
|
+
* ```
|
|
7
26
|
*/
|
|
8
27
|
export declare function guard<Result>(scope: ExceptionScope, run: () => Result, resolve?: (exception: Exception, pass: () => never) => Result | Awaited<Result>): Result;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Exception } from "./exception.js";
|
|
2
|
+
export function guard(scope, run, resolve) {
|
|
3
|
+
const handle = (error) => {
|
|
4
|
+
const exception = Exception.from(error, scope);
|
|
5
|
+
const pass = () => {
|
|
6
|
+
throw exception === error ? exception.propagate(scope) : exception;
|
|
7
|
+
};
|
|
8
|
+
return resolve ? resolve(exception, pass) : pass();
|
|
9
|
+
};
|
|
10
|
+
try {
|
|
11
|
+
const result = run();
|
|
12
|
+
return result instanceof Promise ? result.catch(handle) : result;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return handle(error);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -11,6 +11,10 @@ export interface ExceptionFrame<Details = unknown> extends ExceptionScope {
|
|
|
11
11
|
readonly message: string;
|
|
12
12
|
readonly details: Details;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* What the {@link Exception} constructor takes: the scope (`layer` and
|
|
16
|
+
* `source`), the `name` and `message`, optional `details` and the `cause`.
|
|
17
|
+
*/
|
|
14
18
|
export interface ExceptionInit<Details = unknown> extends ExceptionScope {
|
|
15
19
|
readonly name: string;
|
|
16
20
|
readonly message: string;
|
|
@@ -26,7 +30,16 @@ export interface ExceptionPropagation<Details = unknown> extends ExceptionScope
|
|
|
26
30
|
}
|
|
27
31
|
/** An exception class, abstract or declared with `exceptionOf`. */
|
|
28
32
|
export type ExceptionClass<Instance = any> = abstract new (...args: never[]) => Instance;
|
|
33
|
+
/**
|
|
34
|
+
* The `message` of an exception declared with {@link exceptionOf}: a string, or
|
|
35
|
+
* a function of the `details` and the `source` that gives it.
|
|
36
|
+
*/
|
|
29
37
|
export type ExceptionMessage<Details> = string | ((details: Details, source: string) => string);
|
|
38
|
+
/**
|
|
39
|
+
* What {@link exceptionOf} takes: the `layer` of the exception, its `name` and
|
|
40
|
+
* its `message`. `Details` is the type of the `details` it carries, taken from
|
|
41
|
+
* the parameter of `message` when that is a function.
|
|
42
|
+
*/
|
|
30
43
|
export interface ExceptionDeclaration<Details> {
|
|
31
44
|
readonly layer: ExceptionLayers;
|
|
32
45
|
readonly name: string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function frozenJson(value, subject, fail, seen = new Set) {
|
|
2
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
3
|
+
return value;
|
|
4
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
5
|
+
return value;
|
|
6
|
+
if (typeof value !== "object" || value === null || seen.has(value))
|
|
7
|
+
return fail(`${subject} must be finite, acyclic JSON values.`);
|
|
8
|
+
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
9
|
+
return fail(`${subject} must contain only plain objects and arrays.`);
|
|
10
|
+
if (Object.getOwnPropertySymbols(value).length > 0)
|
|
11
|
+
return fail(`${subject} cannot contain symbol keys.`);
|
|
12
|
+
seen.add(value);
|
|
13
|
+
const result = Array.isArray(value) ? value.map((item) => frozenJson(item, subject, fail, seen)) : Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
14
|
+
key,
|
|
15
|
+
frozenJson(item, subject, fail, seen)
|
|
16
|
+
]));
|
|
17
|
+
seen.delete(value);
|
|
18
|
+
return Object.freeze(result);
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ExceptionLayers } from "../exception/enums/exception-layers.js";
|
|
2
|
+
import { exceptionOf } from "../exception/exception.js";
|
|
3
|
+
export const InvalidLogDeclaration = exceptionOf({
|
|
4
|
+
layer: ExceptionLayers.Application,
|
|
5
|
+
name: "InvalidLogDeclaration",
|
|
6
|
+
message: ({ reason }) => reason
|
|
7
|
+
});
|
|
@@ -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
|
+
});
|