@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.
Files changed (226) hide show
  1. package/README.md +149 -23
  2. package/dist/adapters.d.ts +25 -0
  3. package/dist/adapters.js +32 -0
  4. package/dist/core/application/application.d.ts +30 -0
  5. package/dist/core/application/application.js +1291 -0
  6. package/dist/core/application/exceptions.d.ts +17 -0
  7. package/dist/core/application/exceptions.js +85 -0
  8. package/dist/core/application/migrate.d.ts +19 -0
  9. package/dist/core/application/migrate.js +67 -0
  10. package/dist/core/application/providers.js +108 -0
  11. package/dist/core/application/registry.js +23 -0
  12. package/dist/core/application/schema.d.ts +30 -0
  13. package/dist/core/application/schema.js +398 -0
  14. package/dist/core/application/start.js +162 -0
  15. package/dist/core/application/types/application.interface.d.ts +60 -3
  16. package/dist/core/auth/jwt.d.ts +30 -0
  17. package/dist/core/auth/jwt.js +200 -0
  18. package/dist/core/auth/types/jwt.interface.d.ts +74 -0
  19. package/dist/core/blueprint/blueprint-plugin.js +7 -0
  20. package/dist/core/blueprint/blueprint.js +146 -0
  21. package/dist/core/blueprint/callable.js +13 -0
  22. package/dist/core/blueprint/exceptions.d.ts +5 -0
  23. package/dist/core/blueprint/exceptions.js +20 -0
  24. package/dist/core/blueprint/is-blueprint.js +4 -0
  25. package/dist/core/blueprint/list.js +19 -0
  26. package/dist/core/blueprint/optional.d.ts +3 -1
  27. package/dist/core/blueprint/optional.js +69 -0
  28. package/dist/core/blueprint/record-field.js +14 -0
  29. package/dist/core/blueprint/slice.js +127 -0
  30. package/dist/core/blueprint/types/blueprint.interface.d.ts +95 -5
  31. package/dist/core/blueprint/variant.d.ts +32 -0
  32. package/dist/core/blueprint/variant.js +72 -0
  33. package/dist/core/command/command.js +428 -0
  34. package/dist/core/command/exceptions.js +30 -0
  35. package/dist/core/command/types/command.interface.d.ts +10 -1
  36. package/dist/core/entity/abstract-entity.d.ts +24 -0
  37. package/dist/core/entity/abstract-entity.js +181 -0
  38. package/dist/core/entity/entity.d.ts +1 -1
  39. package/dist/core/entity/entity.js +209 -0
  40. package/dist/core/entity/event.d.ts +39 -0
  41. package/dist/core/entity/event.js +80 -0
  42. package/dist/core/entity/exceptions.d.ts +9 -0
  43. package/dist/core/entity/exceptions.js +27 -0
  44. package/dist/core/entity/trust.d.ts +24 -0
  45. package/dist/core/entity/trust.js +7 -0
  46. package/dist/core/entity/types/entity.interface.d.ts +30 -0
  47. package/dist/core/entity/version.js +13 -0
  48. package/dist/core/environment/environment.d.ts +26 -3
  49. package/dist/core/environment/environment.js +71 -0
  50. package/dist/core/event-policy/event-policy.js +76 -0
  51. package/dist/core/event-policy/exceptions.js +12 -0
  52. package/dist/core/event-policy/publish.js +98 -0
  53. package/dist/core/event-policy/publishers.js +18 -0
  54. package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
  55. package/dist/core/exception/enums/exception-layers.js +15 -0
  56. package/dist/core/exception/exception.d.ts +15 -0
  57. package/dist/core/exception/exception.js +128 -0
  58. package/dist/core/exception/exported.js +49 -0
  59. package/dist/core/exception/guard.d.ts +20 -1
  60. package/dist/core/exception/guard.js +16 -0
  61. package/dist/core/exception/types/exception.interface.d.ts +13 -0
  62. package/dist/core/json.js +19 -0
  63. package/dist/core/logger/exceptions.js +7 -0
  64. package/dist/core/logger/logger.d.ts +14 -1
  65. package/dist/core/logger/logger.js +178 -0
  66. package/dist/core/openapi/exceptions.js +7 -0
  67. package/dist/core/openapi/openapi.js +132 -0
  68. package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
  69. package/dist/core/output/access.d.ts +16 -0
  70. package/dist/core/output/access.js +66 -0
  71. package/dist/core/output/coerce.js +38 -0
  72. package/dist/core/output/exceptions.d.ts +16 -0
  73. package/dist/core/output/exceptions.js +22 -0
  74. package/dist/core/output/http.d.ts +36 -0
  75. package/dist/core/output/http.js +351 -0
  76. package/dist/core/output/openapi-sink.d.ts +33 -4
  77. package/dist/core/output/openapi-sink.js +571 -0
  78. package/dist/core/output/output.js +192 -0
  79. package/dist/core/output/problem.js +30 -0
  80. package/dist/core/output/select.js +28 -0
  81. package/dist/core/output/stage.d.ts +7 -2
  82. package/dist/core/output/stage.js +111 -0
  83. package/dist/core/output/status.d.ts +1 -1
  84. package/dist/core/output/status.js +52 -0
  85. package/dist/core/output/types/http.interface.d.ts +134 -0
  86. package/dist/core/output/types/output.interface.d.ts +49 -5
  87. package/dist/core/parse/issue.d.ts +28 -3
  88. package/dist/core/parse/issue.js +31 -0
  89. package/dist/core/parse/parse.d.ts +16 -0
  90. package/dist/core/parse/parse.js +323 -0
  91. package/dist/core/parse/strip.js +48 -0
  92. package/dist/core/property/abstract-property.d.ts +9 -0
  93. package/dist/core/property/abstract-property.js +37 -0
  94. package/dist/core/property/define-property.d.ts +34 -6
  95. package/dist/core/property/define-property.js +209 -0
  96. package/dist/core/property/enums/property-states.js +8 -0
  97. package/dist/core/property/exceptions.js +12 -0
  98. package/dist/core/property/input.js +10 -0
  99. package/dist/core/property/is-property.js +3 -0
  100. package/dist/core/property/lineage.js +8 -0
  101. package/dist/core/property/property.d.ts +27 -0
  102. package/dist/core/property/property.js +46 -0
  103. package/dist/core/property/refine.js +103 -0
  104. package/dist/core/property/types/property-options.interface.d.ts +10 -0
  105. package/dist/core/property/types/property-type.interface.d.ts +4 -0
  106. package/dist/core/record/abstract-record.d.ts +8 -0
  107. package/dist/core/record/abstract-record.js +307 -0
  108. package/dist/core/record/exceptions.js +49 -0
  109. package/dist/core/record/marks.js +7 -0
  110. package/dist/core/record/mask.js +60 -0
  111. package/dist/core/record/name.js +23 -0
  112. package/dist/core/record/plugin-catalog.js +115 -0
  113. package/dist/core/record/plugin.js +198 -0
  114. package/dist/core/record/record.js +117 -0
  115. package/dist/core/record/types/record.interface.d.ts +17 -1
  116. package/dist/core/repository/cursor.js +83 -0
  117. package/dist/core/repository/exceptions.d.ts +13 -0
  118. package/dist/core/repository/exceptions.js +72 -0
  119. package/dist/core/repository/in-memory.d.ts +2 -2
  120. package/dist/core/repository/in-memory.js +196 -0
  121. package/dist/core/repository/migrate.d.ts +20 -0
  122. package/dist/core/repository/migrate.js +254 -0
  123. package/dist/core/repository/plan.js +27 -0
  124. package/dist/core/repository/provider.js +29 -0
  125. package/dist/core/repository/query.js +153 -0
  126. package/dist/core/repository/references.js +196 -0
  127. package/dist/core/repository/repository.d.ts +22 -2
  128. package/dist/core/repository/repository.js +720 -0
  129. package/dist/core/repository/state.d.ts +2 -0
  130. package/dist/core/repository/state.js +138 -0
  131. package/dist/core/repository/types/query.interface.d.ts +13 -0
  132. package/dist/core/repository/types/repository.interface.d.ts +50 -5
  133. package/dist/core/repository/types/schema.interface.d.ts +132 -0
  134. package/dist/core/repository/upcast.d.ts +64 -0
  135. package/dist/core/repository/upcast.js +179 -0
  136. package/dist/core/schema/schema.d.ts +37 -0
  137. package/dist/core/schema/schema.js +542 -0
  138. package/dist/core/standard/standard-schema.js +1 -0
  139. package/dist/core/standard/standard.js +23 -0
  140. package/dist/core/symbols/demo.d.ts +4 -0
  141. package/dist/core/symbols/demo.js +1 -0
  142. package/dist/core/symbols/kind.d.ts +1 -1
  143. package/dist/core/symbols/kind.js +1 -0
  144. package/dist/core/symbols/meta.js +1 -0
  145. package/dist/core/symbols/state.d.ts +5 -0
  146. package/dist/core/symbols/state.js +1 -0
  147. package/dist/core/tracing/exceptions.d.ts +4 -0
  148. package/dist/core/tracing/exceptions.js +7 -0
  149. package/dist/core/tracing/tracing.d.ts +72 -0
  150. package/dist/core/tracing/tracing.js +101 -0
  151. package/dist/core/use-case/access.d.ts +12 -0
  152. package/dist/core/use-case/access.js +56 -0
  153. package/dist/core/use-case/decorators.d.ts +40 -0
  154. package/dist/core/use-case/decorators.js +65 -0
  155. package/dist/core/use-case/exceptions.d.ts +5 -1
  156. package/dist/core/use-case/exceptions.js +48 -0
  157. package/dist/core/use-case/plugin-catalog.js +68 -0
  158. package/dist/core/use-case/plugin.js +133 -0
  159. package/dist/core/use-case/route.d.ts +18 -0
  160. package/dist/core/use-case/route.js +200 -0
  161. package/dist/core/use-case/scope.d.ts +5 -0
  162. package/dist/core/use-case/scope.js +4 -0
  163. package/dist/core/use-case/steps/describe.d.ts +5 -1
  164. package/dist/core/use-case/steps/describe.js +24 -0
  165. package/dist/core/use-case/steps/references.js +105 -0
  166. package/dist/core/use-case/steps/run.js +139 -0
  167. package/dist/core/use-case/steps/types/check.interface.d.ts +233 -0
  168. package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
  169. package/dist/core/use-case/steps/validate.js +368 -0
  170. package/dist/core/use-case/target.js +100 -0
  171. package/dist/core/use-case/types/access.interface.d.ts +39 -0
  172. package/dist/core/use-case/types/route.interface.d.ts +86 -0
  173. package/dist/core/use-case/use-case.d.ts +127 -2
  174. package/dist/core/use-case/use-case.js +493 -0
  175. package/dist/experimental.d.ts +18 -1
  176. package/dist/experimental.js +18 -123
  177. package/dist/index.d.ts +49 -51
  178. package/dist/index.js +213 -443
  179. package/dist/std/blueprint-plugin.js +42 -0
  180. package/dist/std/blueprint.d.ts +21 -0
  181. package/dist/std/blueprint.js +3 -0
  182. package/dist/std/command.js +3 -0
  183. package/dist/std/entity.d.ts +20 -1
  184. package/dist/std/entity.js +30 -0
  185. package/dist/std/event-policies.js +3 -0
  186. package/dist/std/properties/contact.d.ts +3 -0
  187. package/dist/std/properties/contact.js +29 -0
  188. package/dist/std/properties/enum.js +26 -0
  189. package/dist/std/properties/exceptions.js +14 -0
  190. package/dist/std/properties/headless.js +45 -0
  191. package/dist/std/properties/identity.js +28 -0
  192. package/dist/std/properties/json.js +161 -0
  193. package/dist/std/properties/password.d.ts +0 -2
  194. package/dist/std/properties/password.js +79 -0
  195. package/dist/std/properties/primitives.d.ts +4 -0
  196. package/dist/std/properties/primitives.js +28 -0
  197. package/dist/std/properties/time.js +55 -0
  198. package/dist/std/record-plugins.d.ts +2 -2
  199. package/dist/std/record-plugins.js +73 -0
  200. package/dist/std/record.d.ts +20 -1
  201. package/dist/std/record.js +3 -0
  202. package/dist/std/resource.d.ts +31 -0
  203. package/dist/std/resource.js +39 -0
  204. package/dist/std/runtime.d.ts +6 -0
  205. package/dist/std/runtime.js +193 -0
  206. package/dist/std/stages.js +4 -0
  207. package/dist/std/use-cases/auth.d.ts +5 -1
  208. package/dist/std/use-cases/auth.js +193 -0
  209. package/dist/std/use-cases/crud.js +261 -0
  210. package/dist/std/use-cases/index.d.ts +1 -1
  211. package/dist/std/use-cases/index.js +12 -0
  212. package/dist/std/use-cases/lifecycle.js +97 -0
  213. package/dist/std/use-cases/shared.d.ts +2 -2
  214. package/dist/std/use-cases/shared.js +232 -0
  215. package/dist/std/use-cases/types.d.ts +102 -2
  216. package/dist/std/use-cases/types.js +0 -0
  217. package/dist/testing/http-provider-contract.d.ts +32 -0
  218. package/dist/testing/http-provider-contract.js +344 -0
  219. package/dist/testing/index.d.ts +2 -0
  220. package/dist/testing/index.js +3 -944
  221. package/dist/testing/memory-publisher.js +12 -0
  222. package/dist/testing/provider-contract.d.ts +26 -1
  223. package/dist/testing/provider-contract.js +1071 -0
  224. package/package.json +23 -3
  225. package/dist/index-qxfkyd6k.js +0 -1721
  226. package/dist/index-t5qwtyj5.js +0 -8194
@@ -4,7 +4,11 @@ import { Kind } from "../symbols/kind.js";
4
4
  export type EnvironmentSource = () => Readonly<Record<string, string | undefined>>;
5
5
  /** What `environmentOf` takes, after the blueprint. */
6
6
  export interface EnvironmentOptions {
7
- /** The variables to read, e.g. `() => Bun.env`; `process.env` by default. */
7
+ /**
8
+ * The variables to read, e.g. `() => Bun.env`.
9
+ *
10
+ * @default `() => process.env`
11
+ */
8
12
  readonly source?: EnvironmentSource;
9
13
  }
10
14
  /**
@@ -15,7 +19,11 @@ export interface EnvironmentOptions {
15
19
  export interface IEnvironment<Properties extends IBlueprintCollection> {
16
20
  readonly [Kind]: "environment";
17
21
  readonly blueprint: IBlueprint<Properties>;
18
- /** The validated variables; throws `InvalidEnvironment` when any is rejected. */
22
+ /**
23
+ * The validated variables, read on the first call and the same after.
24
+ *
25
+ * @throws {@link InvalidEnvironment} when a variable is missing or rejected.
26
+ */
19
27
  load(): Readonly<BlueprintValue<Properties>>;
20
28
  }
21
29
  /** Any environment, whatever its blueprint. */
@@ -40,8 +48,23 @@ export declare const InvalidEnvironment: import("../../index.js").DeclaredExcept
40
48
  * boolean (`"true"`, `"false"`) or a number when its property takes one
41
49
  * instead of a string, validates them once and
42
50
  * gives the same value after. As a dependency of a repository it reaches the
43
- * provider as `context.env`; of a use case, the use case gets its value, e.g.
51
+ * provider as `context.env`; of a use case ({@link useCaseOf}), the use case gets its value, e.g.
44
52
  * `environmentOf(blueprintOf((ctx) => ({ DATABASE_URL: ctx.url })))`.
53
+ *
54
+ * @throws {TypeError} when `blueprint` is not a blueprint, or is an optional one.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { blueprintOf, environmentOf } from "@afora/core";
59
+ *
60
+ * const Env = environmentOf(
61
+ * blueprintOf((ctx) => ({ DATABASE_URL: ctx.url, PORT: ctx.integer })),
62
+ * { source: () => ({ DATABASE_URL: "https://db.example.com", PORT: "3000" }) },
63
+ * );
64
+ *
65
+ * Env.load().PORT; // 3000, converted from the string
66
+ * // Env.load() throws InvalidEnvironment when a variable is missing or invalid
67
+ * ```
45
68
  */
46
69
  export declare function environmentOf<Properties extends IBlueprintCollection>(blueprint: IBlueprint<Properties, boolean>, { source }?: EnvironmentOptions): IEnvironment<Properties>;
47
70
  /** The variables of every environment of `dependencies`, merged in order. */
@@ -0,0 +1,71 @@
1
+ import { isBlueprint } from "../blueprint/is-blueprint.js";
2
+ import { ExceptionLayers } from "../exception/enums/exception-layers.js";
3
+ import { exceptionOf } from "../exception/exception.js";
4
+ import { isProperty } from "../property/is-property.js";
5
+ import { Kind } from "../symbols/kind.js";
6
+ export const InvalidEnvironment = exceptionOf({
7
+ layer: ExceptionLayers.Environment,
8
+ name: "InvalidEnvironment",
9
+ message: ({ keys }) => `Invalid environment variables: ${keys.join(", ")}.`
10
+ });
11
+ const processEnv = () => globalThis.process?.env ?? {};
12
+ export function environmentOf(blueprint, { source = processEnv } = {}) {
13
+ if (!isBlueprint(blueprint) || blueprint.optional) {
14
+ throw new TypeError("An environment requires a blueprint");
15
+ }
16
+ let value;
17
+ return Object.freeze({
18
+ [Kind]: "environment",
19
+ blueprint,
20
+ load() {
21
+ if (value !== undefined)
22
+ return value;
23
+ value = parseEnvironment(blueprint, source());
24
+ return value;
25
+ }
26
+ });
27
+ }
28
+ function parseEnvironment(blueprint, variables) {
29
+ const declared = {};
30
+ for (const [key, field] of Object.entries(blueprint.properties)) {
31
+ const variable = variables[key];
32
+ if (variable !== undefined)
33
+ declared[key] = convert(field, variable);
34
+ }
35
+ const parsed = blueprint.safeParse(declared);
36
+ if (!parsed.issues)
37
+ return Object.freeze(parsed.value);
38
+ const issues = parsed.issues;
39
+ throw new InvalidEnvironment({
40
+ source: "environment",
41
+ details: {
42
+ keys: [...new Set(issues.map((issue) => String(issue.path[0] ?? "")))]
43
+ },
44
+ cause: issues.find((issue) => issue.cause)?.cause
45
+ });
46
+ }
47
+ function convert(field, variable) {
48
+ if (!isProperty(field))
49
+ return variable;
50
+ const { schema } = field.type;
51
+ if (schema.matchShape(variable))
52
+ return variable;
53
+ const text = variable.trim();
54
+ if (text === "true" || text === "false") {
55
+ if (schema.matchShape(text === "true"))
56
+ return text === "true";
57
+ }
58
+ const number = text === "" ? Number.NaN : Number(text);
59
+ if (Number.isFinite(number) && schema.matchShape(number))
60
+ return number;
61
+ return variable;
62
+ }
63
+ export function loadEnvironments(dependencies) {
64
+ if (dependencies === undefined)
65
+ return Object.freeze({});
66
+ const list = isEnvironment(dependencies) ? [dependencies] : dependencies;
67
+ return Object.freeze(Object.assign({}, ...list.map((environment) => environment.load())));
68
+ }
69
+ export function isEnvironment(value) {
70
+ return typeof value === "object" && value !== null && value[Kind] === "environment";
71
+ }
@@ -0,0 +1,76 @@
1
+ import { frozenJson } from "../json.js";
2
+ import { Kind } from "../symbols/kind.js";
3
+ import { InvalidEventPolicy } from "./exceptions.js";
4
+ const described = new WeakMap;
5
+ const factories = new WeakMap;
6
+ function invalid(source, reason) {
7
+ throw new InvalidEventPolicy({ source, details: { reason } });
8
+ }
9
+ function optionsOf(name, given, validate) {
10
+ const options = frozenJson(given ?? {}, "Event policy options", (reason) => invalid(name, reason));
11
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
12
+ invalid(name, "Event policy options must be an object.");
13
+ }
14
+ const reason = validate?.(options);
15
+ if (reason !== undefined)
16
+ invalid(name, reason);
17
+ return options;
18
+ }
19
+ export function eventPolicyOf(settings) {
20
+ const name = settings?.name;
21
+ if (typeof name !== "string" || !/^[A-Za-z_$][\w$]*$/.test(name)) {
22
+ invalid("eventPolicyOf", "Expected the policy name to be an identifier.");
23
+ }
24
+ if (typeof settings.handle !== "function") {
25
+ invalid(name, "Expected handle to be a function.");
26
+ }
27
+ if (settings.publishers !== undefined && typeof settings.publishers !== "function") {
28
+ invalid(name, "Expected publishers to be a function.");
29
+ }
30
+ const { handle, validate, publishers } = settings;
31
+ const factory = (given) => {
32
+ const options = optionsOf(name, given, validate);
33
+ const handler = (events, info) => handle(events, info, options);
34
+ Object.defineProperty(handler, "name", { value: name });
35
+ described.set(handler, {
36
+ description: Object.freeze({
37
+ kind: "policy",
38
+ name,
39
+ options
40
+ }),
41
+ publishers: Object.freeze([...publishers?.(options) ?? []])
42
+ });
43
+ return handler;
44
+ };
45
+ factories.set(factory, name);
46
+ return factory;
47
+ }
48
+ export function eventPolicyDescriptionOf(handler) {
49
+ return typeof handler === "function" ? described.get(handler)?.description : undefined;
50
+ }
51
+ export function eventPolicyPublishersOf(handler) {
52
+ return typeof handler === "function" ? described.get(handler)?.publishers ?? [] : [];
53
+ }
54
+ export function eventPolicyCatalogOf(...policies) {
55
+ const byName = {};
56
+ for (const factory of policies) {
57
+ const name = factories.get(factory);
58
+ if (name === undefined) {
59
+ invalid("catalog", "A catalog takes factories made by `eventPolicyOf`.");
60
+ }
61
+ if (Object.hasOwn(byName, name)) {
62
+ invalid(name, "The catalog has another event policy with its name.");
63
+ }
64
+ byName[name] = factory;
65
+ }
66
+ return Object.freeze({
67
+ [Kind]: "event-policy-catalog",
68
+ policies: Object.freeze(byName),
69
+ resolve(name, options) {
70
+ const factory = Object.hasOwn(byName, name) ? byName[name] : undefined;
71
+ if (!factory)
72
+ invalid("catalog", `Unknown event policy "${name}".`);
73
+ return factory(options);
74
+ }
75
+ });
76
+ }
@@ -0,0 +1,12 @@
1
+ import { ExceptionLayers } from "../exception/enums/exception-layers.js";
2
+ import { exceptionOf } from "../exception/exception.js";
3
+ export const InvalidEventPolicy = exceptionOf({
4
+ layer: ExceptionLayers.Event,
5
+ name: "InvalidEventPolicy",
6
+ message: ({ reason }) => reason
7
+ });
8
+ export const PublisherNotBound = exceptionOf({
9
+ layer: ExceptionLayers.Application,
10
+ name: "PublisherNotBound",
11
+ message: ({ publisher }) => `No publisher "${publisher}" is declared: add it to applicationOf({ publishers }).`
12
+ });
@@ -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
+ });