@afora/core 0.0.9 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +62 -189
  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 +17 -5
  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,192 @@
1
+ import { isCommand } from "../command/command.js";
2
+ import { Kind } from "../symbols/kind.js";
3
+ import { accessGuardOf } from "./access.js";
4
+ import { DuplicateOperation, InvalidOutputStage } from "./exceptions.js";
5
+ import { isStage } from "./stage.js";
6
+ function isApplicationLike(value) {
7
+ return typeof value === "object" && value !== null && value[Kind] === "application";
8
+ }
9
+ function invalid(source, reason) {
10
+ throw new InvalidOutputStage({ source, details: { reason } });
11
+ }
12
+ const PATCHABLE = ["meta", "tags", "description", "deprecated", "invoke"];
13
+ export function operationOf(operation, patch) {
14
+ const source = operation?.id ?? "operationOf";
15
+ if (operation?.kind !== "operation" || operation.version !== 1) {
16
+ invalid(source, "Expected an operation descriptor.");
17
+ }
18
+ if (typeof patch !== "object" || patch === null || Array.isArray(patch)) {
19
+ invalid(source, "Expected the patch to be an object.");
20
+ }
21
+ for (const key of Reflect.ownKeys(patch)) {
22
+ if (typeof key !== "string" || !PATCHABLE.includes(key)) {
23
+ invalid(source, `"${String(key)}" cannot be patched; only ${PATCHABLE.join(", ")} can.`);
24
+ }
25
+ }
26
+ const { meta, tags, description, deprecated, invoke } = patch;
27
+ if ("meta" in patch && (typeof meta !== "object" || meta === null || Array.isArray(meta))) {
28
+ invalid(source, "Expected meta to be an object.");
29
+ }
30
+ if ("tags" in patch && (!Array.isArray(tags) || tags.some((tag) => typeof tag !== "string"))) {
31
+ invalid(source, "Expected tags to be a list of strings.");
32
+ }
33
+ if ("description" in patch && description !== undefined && typeof description !== "string") {
34
+ invalid(source, "Expected description to be a string.");
35
+ }
36
+ if ("deprecated" in patch && typeof deprecated !== "boolean") {
37
+ invalid(source, "Expected deprecated to be a boolean.");
38
+ }
39
+ if ("invoke" in patch && typeof invoke !== "function") {
40
+ invalid(source, "Expected invoke to be a function.");
41
+ }
42
+ return Object.freeze({
43
+ ...operation,
44
+ ..."meta" in patch ? { meta: Object.freeze({ ...meta }) } : {},
45
+ ..."tags" in patch ? { tags: Object.freeze([...tags ?? []]) } : {},
46
+ ..."description" in patch ? { description } : {},
47
+ ..."deprecated" in patch ? { deprecated } : {},
48
+ ..."invoke" in patch ? { invoke } : {}
49
+ });
50
+ }
51
+ function operationsOfCommand(command) {
52
+ return command.operations.map((name) => {
53
+ const UseCase = command.useCases[name];
54
+ const context = command.context ?? UseCase.context;
55
+ const description = UseCase.description ?? command.description;
56
+ const id = `${command.commandName}.${name}`;
57
+ const run = command.run;
58
+ const guard = UseCase.access === undefined ? undefined : accessGuardOf(id, UseCase.access, context);
59
+ return Object.freeze({
60
+ kind: "operation",
61
+ version: 1,
62
+ id,
63
+ command: command.commandName,
64
+ name,
65
+ useCase: UseCase.useCaseName,
66
+ ...description === undefined ? {} : { description },
67
+ tags: Object.freeze([
68
+ ...new Set([...command.tags, ...UseCase.tags])
69
+ ]),
70
+ deprecated: UseCase.deprecated,
71
+ input: UseCase.blueprint,
72
+ response: UseCase.response,
73
+ ...context ? { context } : {},
74
+ errors: UseCase.errors,
75
+ ...UseCase.route === undefined ? {} : { route: UseCase.route },
76
+ ...UseCase.access === undefined ? {} : { access: UseCase.access },
77
+ meta: Object.freeze({}),
78
+ invoke: guard ? async (input, options) => run(name, input, guard(input, options)) : (input, options) => run(name, input, options && "principal" in options ? { context: options.context } : options)
79
+ });
80
+ });
81
+ }
82
+ function statusesOf(name, statuses) {
83
+ if (statuses === undefined)
84
+ return Object.freeze({});
85
+ if (typeof statuses !== "object" || statuses === null || Array.isArray(statuses)) {
86
+ invalid(name, "Expected statuses to be a map of exception names to statuses.");
87
+ }
88
+ for (const [exception, status] of Object.entries(statuses)) {
89
+ if (!exception || typeof status !== "number" || !Number.isInteger(status) || status < 400 || status > 599) {
90
+ invalid(name, `Expected the status of "${exception}" to be an integer between 400 and 599.`);
91
+ }
92
+ }
93
+ return Object.freeze({ ...statuses });
94
+ }
95
+ function checkPipe(name, pipe) {
96
+ if (!Array.isArray(pipe))
97
+ invalid(name, "Expected pipe to be a list of stages.");
98
+ const stages = pipe;
99
+ stages.forEach((stage, index) => {
100
+ if (!isStage(stage)) {
101
+ invalid(name, `Expected pipe[${index}] to be a transform or sink stage.`);
102
+ }
103
+ if (stage.kind === "sink" && index !== stages.length - 1) {
104
+ invalid(name, `The sink at pipe[${index}] must be the last stage.`);
105
+ }
106
+ });
107
+ return Object.freeze([...stages]);
108
+ }
109
+ function transformed(name, operations, transforms, context) {
110
+ let current = operations;
111
+ for (const stage of transforms) {
112
+ if (stage.kind !== "transform")
113
+ continue;
114
+ const next = [];
115
+ for (const operation of current) {
116
+ const result = stage.apply(operation, context);
117
+ if (result === null)
118
+ continue;
119
+ if (typeof result !== "object" || result?.kind !== "operation" || result.id !== operation.id) {
120
+ invalid(name, `A transform must return the operation "${operation.id}" or null.`);
121
+ }
122
+ next.push(result);
123
+ }
124
+ current = next;
125
+ }
126
+ return Object.freeze([...current]);
127
+ }
128
+ function make(name, commands, operations, stages, statuses) {
129
+ const context = Object.freeze({
130
+ output: Object.freeze({ name, statuses })
131
+ });
132
+ let built;
133
+ const output = {
134
+ [Kind]: "output",
135
+ outputName: name,
136
+ commands,
137
+ operations,
138
+ stages,
139
+ statuses,
140
+ build() {
141
+ if (built)
142
+ return built.value;
143
+ const sink = stages.at(-1);
144
+ if (sink?.kind !== "sink") {
145
+ invalid(name || "output", "The pipe has no sink to build.");
146
+ }
147
+ built = { value: sink.build(operations, context) };
148
+ return built.value;
149
+ },
150
+ pipe(...more) {
151
+ const next = checkPipe(name || "output", more);
152
+ const last = next.at(-1);
153
+ if (stages.at(-1)?.kind === "sink") {
154
+ invalid(name || "output", "Cannot add stages after the sink.");
155
+ }
156
+ const ops = transformed(name, operations, next, context);
157
+ if (last?.kind === "sink") {
158
+ return last.build(ops, context);
159
+ }
160
+ return make("", commands, ops, Object.freeze([...stages, ...next]), statuses);
161
+ }
162
+ };
163
+ return Object.freeze(output);
164
+ }
165
+ export function outputOf(settings) {
166
+ const { name, commands } = settings ?? {};
167
+ if (typeof name !== "string" || !name.trim()) {
168
+ invalid("outputOf", "Expected name to be a non-empty string.");
169
+ }
170
+ const sources = Array.isArray(commands) ? commands : [commands];
171
+ if (!Array.isArray(commands) && isCommand(commands) || sources.some((entry) => !isCommand(entry) && !isApplicationLike(entry))) {
172
+ invalid(name, "Expected commands to be a list of commands made by commandOf or applications.");
173
+ }
174
+ const flat = [
175
+ ...new Set(sources.flatMap((entry) => isCommand(entry) ? [entry] : Object.values(entry.commands)))
176
+ ];
177
+ const statuses = statusesOf(name, settings.statuses);
178
+ const stages = checkPipe(name, settings.pipe ?? []);
179
+ const all = flat.flatMap(operationsOfCommand);
180
+ const ids = new Set;
181
+ for (const { id } of all) {
182
+ if (ids.has(id))
183
+ throw new DuplicateOperation({ source: name, details: { id } });
184
+ ids.add(id);
185
+ }
186
+ const context = { output: { name, statuses } };
187
+ const operations = transformed(name, all, stages, context);
188
+ return make(name, Object.freeze(flat), operations, stages, statuses);
189
+ }
190
+ export function isOutput(value) {
191
+ return typeof value === "object" && value !== null && value[Kind] === "output";
192
+ }
@@ -0,0 +1,30 @@
1
+ import { Exception } from "../exception/exception.js";
2
+ import { raisedOf } from "./status.js";
3
+ export const PROBLEM_MEDIA_TYPE = "application/problem+json";
4
+ const INTERNAL = "Internal error.";
5
+ export function problemOf(error, options) {
6
+ const { status, debug = false } = options;
7
+ const exception = error instanceof Exception ? error : undefined;
8
+ const title = exception?.name ?? "UnexpectedError";
9
+ const message = exception?.message ?? (error instanceof Error ? error.message : String(error));
10
+ const raised = exception && raisedOf(exception);
11
+ const rawIssues = raised?.details?.issues;
12
+ const issues = status === 400 && Array.isArray(rawIssues) ? rawIssues.map((issue) => ({
13
+ path: [...issue.path],
14
+ code: issue.code,
15
+ message: issue.message
16
+ })) : undefined;
17
+ return {
18
+ type: `urn:afora:exception:${title}`,
19
+ title,
20
+ status,
21
+ detail: status >= 500 && !debug ? INTERNAL : message,
22
+ ...raised ? { layer: raised.layer } : {},
23
+ ...issues ? { issues } : {},
24
+ ...debug ? { trace: exception ? exception.toString() : String(error) } : {}
25
+ };
26
+ }
27
+ export function outputValueOf(value) {
28
+ const text = JSON.stringify(value);
29
+ return text === undefined ? undefined : JSON.parse(text);
30
+ }
@@ -0,0 +1,28 @@
1
+ import { transformOf } from "./stage.js";
2
+ const LISTS = ["tags", "operations", "exclude"];
3
+ export const select = transformOf({
4
+ name: "select",
5
+ validate: (options) => {
6
+ for (const key of Object.keys(options)) {
7
+ if (!LISTS.includes(key)) {
8
+ return `select has no option "${key}".`;
9
+ }
10
+ }
11
+ for (const key of LISTS) {
12
+ const list = options[key];
13
+ if (list !== undefined && (!Array.isArray(list) || list.some((item) => typeof item !== "string"))) {
14
+ return `Expected select.${key} to be a list of strings.`;
15
+ }
16
+ }
17
+ return;
18
+ },
19
+ apply: (operation, { tags, operations, exclude }) => {
20
+ if (tags && !tags.some((tag) => operation.tags.includes(tag)))
21
+ return null;
22
+ if (operations && !operations.includes(operation.id))
23
+ return null;
24
+ if (exclude?.includes(operation.id))
25
+ return null;
26
+ return operation;
27
+ }
28
+ });
@@ -0,0 +1,111 @@
1
+ import { frozenJson } from "../json.js";
2
+ import { Kind } from "../symbols/kind.js";
3
+ import { InvalidOutputStage } from "./exceptions.js";
4
+ const descriptions = new WeakMap;
5
+ const factories = new WeakMap;
6
+ const opaque = Object.freeze({ kind: "opaque" });
7
+ function invalid(source, reason) {
8
+ throw new InvalidOutputStage({ source, details: { reason } });
9
+ }
10
+ function optionsOf(name, given, validate) {
11
+ const options = frozenJson(given ?? {}, "Stage options", (reason) => invalid(name, reason));
12
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
13
+ invalid(name, "Stage options must be an object.");
14
+ }
15
+ const reason = validate?.(options);
16
+ if (reason !== undefined)
17
+ invalid(name, reason);
18
+ return options;
19
+ }
20
+ function checkName(source, name) {
21
+ if (typeof name !== "string" || !name.trim()) {
22
+ invalid(source, "Expected the stage name to be a non-empty string.");
23
+ }
24
+ }
25
+ export function transformOf(settings) {
26
+ checkName("transformOf", settings?.name);
27
+ if (typeof settings.apply !== "function") {
28
+ invalid(settings.name, "Expected apply to be a function.");
29
+ }
30
+ const { name, apply, validate } = settings;
31
+ const factory = (given) => {
32
+ const options = optionsOf(name, given, validate);
33
+ const stage = Object.freeze({
34
+ kind: "transform",
35
+ name,
36
+ options,
37
+ apply: (operation, context) => apply(operation, options, context)
38
+ });
39
+ descriptions.set(stage, Object.freeze({
40
+ kind: "transform",
41
+ name,
42
+ options
43
+ }));
44
+ return stage;
45
+ };
46
+ factories.set(factory, { kind: "transform", name });
47
+ return factory;
48
+ }
49
+ export function sinkOf(settings) {
50
+ checkName("sinkOf", settings?.name);
51
+ if (typeof settings.build !== "function") {
52
+ invalid(settings.name, "Expected build to be a function.");
53
+ }
54
+ const { name, build, validate } = settings;
55
+ const factory = (given, hooks) => {
56
+ const options = optionsOf(name, given, validate);
57
+ if (hooks !== undefined && (typeof hooks !== "object" || hooks === null || Array.isArray(hooks))) {
58
+ invalid(name, "Stage hooks must be an object.");
59
+ }
60
+ const stage = Object.freeze({
61
+ kind: "sink",
62
+ name,
63
+ options,
64
+ build: (operations, context) => build(operations, options, context, hooks)
65
+ });
66
+ descriptions.set(stage, Object.freeze({
67
+ kind: "sink",
68
+ name,
69
+ options
70
+ }));
71
+ return stage;
72
+ };
73
+ factories.set(factory, { kind: "sink", name });
74
+ return factory;
75
+ }
76
+ export function stageDescriptionOf(stage) {
77
+ return descriptions.get(stage) ?? opaque;
78
+ }
79
+ export function stageKindOf(factory) {
80
+ return factories.get(factory)?.kind;
81
+ }
82
+ export function isStage(value) {
83
+ if (typeof value !== "object" || value === null)
84
+ return false;
85
+ const { kind, apply, build } = value;
86
+ return kind === "transform" && typeof apply === "function" || kind === "sink" && typeof build === "function";
87
+ }
88
+ export function stageCatalogOf(...stages) {
89
+ const byName = {};
90
+ for (const factory of stages) {
91
+ const known = factories.get(factory);
92
+ if (!known) {
93
+ invalid("catalog", "A catalog takes factories made by `transformOf` or `sinkOf`.");
94
+ }
95
+ if (Object.hasOwn(byName, known.name)) {
96
+ invalid(known.name, "The catalog has another stage with its name.");
97
+ }
98
+ byName[known.name] = factory;
99
+ }
100
+ return Object.freeze({
101
+ [Kind]: "stage-catalog",
102
+ stages: Object.freeze(byName),
103
+ list: Object.freeze([...stages]),
104
+ resolve(name, options) {
105
+ const factory = Object.hasOwn(byName, name) ? byName[name] : undefined;
106
+ if (!factory)
107
+ invalid("catalog", `Unknown stage "${name}".`);
108
+ return factory(options);
109
+ }
110
+ });
111
+ }
@@ -0,0 +1,52 @@
1
+ import { Exception } from "../exception/exception.js";
2
+ const INVALID_INPUT = new Set([
3
+ "InvalidUseCaseInput",
4
+ "InvalidUseCaseContext",
5
+ "InvalidCommandContext"
6
+ ]);
7
+ const CONFIGURATION = new Set(["InvalidCommandEnvironment"]);
8
+ const NOT_FOUND = new Set([
9
+ "RepositoryEntityNotFound",
10
+ "UnknownCommandOperation",
11
+ "UseCaseEntityNotFound"
12
+ ]);
13
+ const CONFLICT = new Set([
14
+ "RepositoryUniqueViolation",
15
+ "RepositoryConcurrencyConflict",
16
+ "UseCaseEntityConflict"
17
+ ]);
18
+ export function raisedOf(error) {
19
+ let raised = error;
20
+ for (let current = error.cause;current instanceof Exception; current = current.cause) {
21
+ if (current.name === error.name)
22
+ raised = current;
23
+ }
24
+ return raised;
25
+ }
26
+ export function statusOf(error, operation, statuses) {
27
+ if (!(error instanceof Exception))
28
+ return 500;
29
+ const { name } = error;
30
+ if (statuses && Object.hasOwn(statuses, name))
31
+ return statuses[name];
32
+ if (INVALID_INPUT.has(name))
33
+ return 400;
34
+ if (CONFIGURATION.has(name))
35
+ return 500;
36
+ if (Array.isArray(raisedOf(error).details?.issues)) {
37
+ return 400;
38
+ }
39
+ if (NOT_FOUND.has(name))
40
+ return 404;
41
+ if (CONFLICT.has(name))
42
+ return 409;
43
+ if (name === "UseCaseInvalidCredentials" || name === "Unauthenticated") {
44
+ return 401;
45
+ }
46
+ if (name === "Forbidden")
47
+ return 403;
48
+ if (operation?.errors.some((Declared) => error.is(Declared) || Declared.name === name)) {
49
+ return 422;
50
+ }
51
+ return 500;
52
+ }
@@ -15,7 +15,11 @@ export interface HttpOptions extends RouteOptions {
15
15
  readonly info: OpenAPISinkOptions["info"];
16
16
  readonly servers?: OpenAPISinkOptions["servers"];
17
17
  };
18
- /** Adds the `trace` to problems and the real `detail` to 5xx ones. Off by default. */
18
+ /**
19
+ * Adds the `trace` to problems and the real `detail` to 5xx ones.
20
+ *
21
+ * @default false
22
+ */
19
23
  readonly debug?: boolean;
20
24
  /**
21
25
  * The context of an operation, by context key: from request headers
@@ -100,6 +100,11 @@ export type StageDescription = {
100
100
  } | {
101
101
  readonly kind: "opaque";
102
102
  };
103
+ /**
104
+ * What `transformOf` takes: the `name` of the stage, the `apply` function that
105
+ * changes each operation (or removes it) and an optional `validate` of its
106
+ * options.
107
+ */
103
108
  export interface TransformSettings<Options extends object> {
104
109
  readonly name: string;
105
110
  /** Returns the operation, a changed copy, or `null` to remove it. */
@@ -107,6 +112,11 @@ export interface TransformSettings<Options extends object> {
107
112
  /** Why `options` cannot be used, or `undefined`. Checked when the stage is made. */
108
113
  readonly validate?: (options: Options) => string | undefined;
109
114
  }
115
+ /**
116
+ * What `sinkOf` takes: the `name` of the stage, the `build` function that turns
117
+ * the operations into the result of the output (a server, a document...) and an
118
+ * optional `validate` of its options.
119
+ */
110
120
  export interface SinkSettings<Result, Options extends object, Hooks = never> {
111
121
  readonly name: string;
112
122
  /**
@@ -120,6 +130,11 @@ export interface SinkSettings<Result, Options extends object, Hooks = never> {
120
130
  }
121
131
  /** Makes a stage from JSON options. */
122
132
  export type TransformFactory<Options extends object = StageOptions> = (options?: Options) => TransformStage<Options>;
133
+ /**
134
+ * Makes a sink stage from JSON `options` and, second, `hooks`: what cannot be
135
+ * JSON (functions, an instance of a server), which the description of the stage
136
+ * leaves out. What `sinkOf` returns.
137
+ */
123
138
  export type SinkFactory<Result = unknown, Options extends object = StageOptions, Hooks = never> = (options?: Options, hooks?: Hooks) => SinkStage<Result, Options>;
124
139
  /** Any stage factory. */
125
140
  export type StageFactory = TransformFactory<any> | SinkFactory<any, any>;
@@ -143,6 +158,11 @@ export interface OutputApplication {
143
158
  }
144
159
  /** What an output takes its commands from. */
145
160
  export type OutputSource = AnyCommand | OutputApplication;
161
+ /**
162
+ * What `outputOf` takes: the `name` of the output, the `commands` it exposes
163
+ * (commands, applications or a mix), the `statuses` by exception name and the
164
+ * `pipe` of stages that transform its operations and end in a sink.
165
+ */
146
166
  export interface OutputSettings<Pipe extends readonly Stage[]> {
147
167
  readonly name: string;
148
168
  /** The commands the output exposes: commands, applications (all their commands) or a mix. */
@@ -184,6 +204,10 @@ export interface Problem {
184
204
  /** Only with `debug`. */
185
205
  readonly trace?: string;
186
206
  }
207
+ /**
208
+ * What `problemOf` takes: the HTTP `status` of the problem and `debug`, which
209
+ * adds the `trace` and the real `detail` of a 5xx.
210
+ */
187
211
  export interface ProblemOptions {
188
212
  readonly status: number;
189
213
  readonly debug?: boolean;
@@ -0,0 +1,31 @@
1
+ export const REJECTED = Symbol("rejected");
2
+
3
+ export class LazyIssue {
4
+ message;
5
+ path;
6
+ code;
7
+ source;
8
+ #origin;
9
+ constructor(message, path, code, source, origin) {
10
+ this.message = message;
11
+ this.path = path;
12
+ this.code = code;
13
+ this.source = source;
14
+ this.#origin = origin;
15
+ }
16
+ get cause() {
17
+ return this.#origin.exception();
18
+ }
19
+ at(path) {
20
+ return new LazyIssue(this.message, path, this.code, this.source, this.#origin);
21
+ }
22
+ }
23
+ export function movedIssue(issue, path) {
24
+ if (issue instanceof LazyIssue)
25
+ return issue.at(path);
26
+ const moved = { ...issue, path };
27
+ const cause = Object.getOwnPropertyDescriptor(issue, "cause");
28
+ if (cause && !cause.enumerable)
29
+ Object.defineProperty(moved, "cause", cause);
30
+ return moved;
31
+ }
@@ -18,6 +18,13 @@ export declare function isPlainObject(value: unknown): value is Record<string, u
18
18
  * is parsing it, e.g. a record name.
19
19
  */
20
20
  export declare function parseCollection(properties: IBlueprintCollection, value: unknown, path: Path, source: string): ParseResult<Readonly<Record<string, unknown>>>;
21
+ /**
22
+ * Makes, once the rest of a collection is known to be valid, the values the
23
+ * caller left out for it (`undefined` in `handled`); it returns the keys it
24
+ * made that still have to be checked.
25
+ * @internal
26
+ */
27
+ export type Complete = (handled: Record<string, unknown>) => readonly string[] | undefined;
21
28
  /**
22
29
  * `parseCollection` at the root, as a function of the value alone: what it
23
30
  * needs of `properties` is looked up on the first value and kept, instead of
@@ -26,7 +33,7 @@ export declare function parseCollection(properties: IBlueprintCollection, value:
26
33
  * unchecked: for what the caller made itself and knows to be valid.
27
34
  * @internal
28
35
  */
29
- export declare function collectionParserOf(properties: IBlueprintCollection, source: string): (value: unknown, vouched?: readonly string[]) => ParseResult<Readonly<Record<string, unknown>>>;
36
+ export declare function collectionParserOf(properties: IBlueprintCollection, source: string): (value: unknown, vouched?: readonly string[], complete?: Complete) => ParseResult<Readonly<Record<string, unknown>>>;
30
37
  /**
31
38
  * `value` handled by `field`, or every issue found: a property runs its
32
39
  * handlers, a blueprint each of its fields, a list each of its items, and a