@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.
Files changed (186) hide show
  1. package/README.md +52 -30
  2. package/dist/adapters.js +31 -55
  3. package/dist/core/application/application.d.ts +30 -0
  4. package/dist/core/application/application.js +1291 -0
  5. package/dist/core/application/exceptions.js +85 -0
  6. package/dist/core/application/migrate.js +67 -0
  7. package/dist/core/application/providers.js +108 -0
  8. package/dist/core/application/registry.js +23 -0
  9. package/dist/core/application/schema.js +398 -0
  10. package/dist/core/application/start.js +162 -0
  11. package/dist/core/application/types/application.interface.d.ts +23 -3
  12. package/dist/core/auth/jwt.js +200 -0
  13. package/dist/core/auth/types/jwt.interface.d.ts +10 -2
  14. package/dist/core/blueprint/blueprint-plugin.js +7 -0
  15. package/dist/core/blueprint/blueprint.js +146 -0
  16. package/dist/core/blueprint/callable.js +13 -0
  17. package/dist/core/blueprint/exceptions.js +20 -0
  18. package/dist/core/blueprint/is-blueprint.js +4 -0
  19. package/dist/core/blueprint/list.js +19 -0
  20. package/dist/core/blueprint/optional.js +69 -0
  21. package/dist/core/blueprint/record-field.js +14 -0
  22. package/dist/core/blueprint/slice.js +127 -0
  23. package/dist/core/blueprint/types/blueprint.interface.d.ts +16 -0
  24. package/dist/core/blueprint/variant.js +72 -0
  25. package/dist/core/command/command.js +428 -0
  26. package/dist/core/command/exceptions.js +30 -0
  27. package/dist/core/command/types/command.interface.d.ts +10 -1
  28. package/dist/core/entity/abstract-entity.d.ts +19 -0
  29. package/dist/core/entity/abstract-entity.js +181 -0
  30. package/dist/core/entity/entity.js +209 -0
  31. package/dist/core/entity/event.d.ts +39 -0
  32. package/dist/core/entity/event.js +80 -0
  33. package/dist/core/entity/exceptions.js +27 -0
  34. package/dist/core/entity/trust.js +7 -0
  35. package/dist/core/entity/version.js +13 -0
  36. package/dist/core/environment/environment.d.ts +26 -3
  37. package/dist/core/environment/environment.js +71 -0
  38. package/dist/core/event-policy/event-policy.js +76 -0
  39. package/dist/core/event-policy/exceptions.js +12 -0
  40. package/dist/core/event-policy/publish.js +98 -0
  41. package/dist/core/event-policy/publishers.js +18 -0
  42. package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
  43. package/dist/core/exception/enums/exception-layers.js +15 -0
  44. package/dist/core/exception/exception.d.ts +15 -0
  45. package/dist/core/exception/exception.js +128 -0
  46. package/dist/core/exception/exported.js +49 -0
  47. package/dist/core/exception/guard.d.ts +20 -1
  48. package/dist/core/exception/guard.js +16 -0
  49. package/dist/core/exception/types/exception.interface.d.ts +13 -0
  50. package/dist/core/json.js +19 -0
  51. package/dist/core/logger/exceptions.js +7 -0
  52. package/dist/core/logger/logger.d.ts +14 -1
  53. package/dist/core/logger/logger.js +178 -0
  54. package/dist/core/openapi/exceptions.js +7 -0
  55. package/dist/core/openapi/openapi.js +132 -0
  56. package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
  57. package/dist/core/output/access.js +66 -0
  58. package/dist/core/output/coerce.js +38 -0
  59. package/dist/core/output/exceptions.js +22 -0
  60. package/dist/{index-y280ew0k.js → core/output/http.js} +35 -80
  61. package/dist/core/output/openapi-sink.d.ts +10 -2
  62. package/dist/{index-hg1z2s2v.js → core/output/openapi-sink.js} +60 -544
  63. package/dist/core/output/output.js +192 -0
  64. package/dist/core/output/problem.js +30 -0
  65. package/dist/core/output/select.js +28 -0
  66. package/dist/core/output/stage.js +111 -0
  67. package/dist/core/output/status.js +52 -0
  68. package/dist/core/output/types/http.interface.d.ts +5 -1
  69. package/dist/core/output/types/output.interface.d.ts +24 -0
  70. package/dist/core/parse/issue.js +31 -0
  71. package/dist/core/parse/parse.d.ts +8 -1
  72. package/dist/core/parse/parse.js +323 -0
  73. package/dist/core/parse/strip.js +48 -0
  74. package/dist/core/property/abstract-property.js +37 -0
  75. package/dist/core/property/define-property.js +209 -0
  76. package/dist/core/property/enums/property-states.js +8 -0
  77. package/dist/core/property/exceptions.js +12 -0
  78. package/dist/core/property/input.js +10 -0
  79. package/dist/core/property/is-property.js +3 -0
  80. package/dist/core/property/lineage.js +8 -0
  81. package/dist/core/property/property.d.ts +27 -0
  82. package/dist/core/property/property.js +46 -0
  83. package/dist/core/property/refine.js +103 -0
  84. package/dist/core/record/abstract-record.d.ts +2 -1
  85. package/dist/core/record/abstract-record.js +307 -0
  86. package/dist/core/record/exceptions.js +49 -0
  87. package/dist/core/record/marks.js +7 -0
  88. package/dist/core/record/mask.js +60 -0
  89. package/dist/core/record/name.js +23 -0
  90. package/dist/core/record/plugin-catalog.js +115 -0
  91. package/dist/core/record/plugin.js +198 -0
  92. package/dist/core/record/record.js +117 -0
  93. package/dist/core/record/types/record.interface.d.ts +5 -0
  94. package/dist/core/repository/cursor.js +83 -0
  95. package/dist/core/repository/exceptions.js +72 -0
  96. package/dist/core/repository/in-memory.js +196 -0
  97. package/dist/core/repository/migrate.js +254 -0
  98. package/dist/core/repository/plan.js +27 -0
  99. package/dist/core/repository/provider.js +29 -0
  100. package/dist/core/repository/query.js +153 -0
  101. package/dist/core/repository/references.js +196 -0
  102. package/dist/core/repository/repository.d.ts +22 -2
  103. package/dist/core/repository/repository.js +720 -0
  104. package/dist/core/repository/state.js +138 -0
  105. package/dist/core/repository/types/query.interface.d.ts +13 -0
  106. package/dist/core/repository/types/repository.interface.d.ts +20 -8
  107. package/dist/core/repository/types/schema.interface.d.ts +8 -2
  108. package/dist/core/repository/upcast.js +179 -0
  109. package/dist/core/schema/schema.d.ts +9 -0
  110. package/dist/core/schema/schema.js +542 -0
  111. package/dist/core/standard/standard-schema.js +1 -0
  112. package/dist/core/standard/standard.js +23 -0
  113. package/dist/core/symbols/demo.d.ts +4 -0
  114. package/dist/core/symbols/demo.js +1 -0
  115. package/dist/core/symbols/kind.js +1 -0
  116. package/dist/core/symbols/meta.js +1 -0
  117. package/dist/core/symbols/state.d.ts +5 -0
  118. package/dist/core/symbols/state.js +1 -0
  119. package/dist/core/tracing/exceptions.d.ts +4 -0
  120. package/dist/core/tracing/exceptions.js +7 -0
  121. package/dist/core/tracing/tracing.d.ts +72 -0
  122. package/dist/core/tracing/tracing.js +101 -0
  123. package/dist/core/use-case/access.js +56 -0
  124. package/dist/core/use-case/decorators.js +65 -0
  125. package/dist/core/use-case/exceptions.d.ts +5 -1
  126. package/dist/core/use-case/exceptions.js +48 -0
  127. package/dist/core/use-case/plugin-catalog.js +68 -0
  128. package/dist/core/use-case/plugin.js +133 -0
  129. package/dist/core/use-case/route.js +200 -0
  130. package/dist/core/use-case/scope.d.ts +5 -0
  131. package/dist/core/use-case/scope.js +4 -0
  132. package/dist/core/use-case/steps/describe.d.ts +5 -1
  133. package/dist/core/use-case/steps/describe.js +24 -0
  134. package/dist/core/use-case/steps/references.js +105 -0
  135. package/dist/core/use-case/steps/run.js +139 -0
  136. package/dist/core/use-case/steps/types/check.interface.d.ts +27 -8
  137. package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
  138. package/dist/core/use-case/steps/validate.js +368 -0
  139. package/dist/core/use-case/target.js +100 -0
  140. package/dist/core/use-case/types/route.interface.d.ts +25 -5
  141. package/dist/core/use-case/use-case.d.ts +77 -2
  142. package/dist/core/use-case/use-case.js +493 -0
  143. package/dist/experimental.d.ts +5 -0
  144. package/dist/experimental.js +18 -161
  145. package/dist/index.d.ts +7 -5
  146. package/dist/index.js +213 -367
  147. package/dist/std/blueprint-plugin.js +42 -0
  148. package/dist/std/blueprint.d.ts +21 -0
  149. package/dist/std/blueprint.js +3 -0
  150. package/dist/std/command.js +3 -0
  151. package/dist/std/entity.d.ts +20 -1
  152. package/dist/std/entity.js +30 -0
  153. package/dist/std/event-policies.js +3 -0
  154. package/dist/std/properties/contact.d.ts +3 -0
  155. package/dist/std/properties/contact.js +29 -0
  156. package/dist/std/properties/enum.js +26 -0
  157. package/dist/std/properties/exceptions.js +14 -0
  158. package/dist/std/properties/headless.js +45 -0
  159. package/dist/std/properties/identity.js +28 -0
  160. package/dist/std/properties/json.js +161 -0
  161. package/dist/std/properties/password.js +79 -0
  162. package/dist/std/properties/primitives.d.ts +4 -0
  163. package/dist/std/properties/primitives.js +28 -0
  164. package/dist/std/properties/time.js +55 -0
  165. package/dist/std/record-plugins.js +73 -0
  166. package/dist/std/record.d.ts +20 -1
  167. package/dist/std/record.js +3 -0
  168. package/dist/std/resource.js +39 -0
  169. package/dist/std/runtime.js +193 -0
  170. package/dist/std/stages.js +4 -0
  171. package/dist/std/use-cases/auth.js +193 -0
  172. package/dist/std/use-cases/crud.js +261 -0
  173. package/dist/std/use-cases/index.js +12 -0
  174. package/dist/std/use-cases/lifecycle.js +97 -0
  175. package/dist/std/use-cases/shared.js +232 -0
  176. package/dist/std/use-cases/types.d.ts +5 -1
  177. package/dist/std/use-cases/types.js +0 -0
  178. package/dist/testing/http-provider-contract.js +344 -0
  179. package/dist/testing/index.js +3 -1435
  180. package/dist/testing/memory-publisher.js +12 -0
  181. package/dist/testing/provider-contract.d.ts +12 -3
  182. package/dist/testing/provider-contract.js +1071 -0
  183. package/package.json +10 -3
  184. package/dist/index-3aae5jfq.js +0 -2997
  185. package/dist/index-ny3as79r.js +0 -4567
  186. 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
- /** Whether each event runs its own handler first, as `"after-commit"` does. `true` by default. */
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"` (default) writes an error to the
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`. Without `resolve`,
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
- /** Lines below it are dropped. `"info"` by default. */
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
+ });