@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
@@ -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
+ });
@@ -1,8 +1,13 @@
1
1
  import type { IStageCatalog, SinkFactory, SinkSettings, Stage, StageDescription, StageFactory, StageOptions, TransformFactory, TransformSettings } from "./types/output.interface.js";
2
2
  /** A factory of transform stages: `select({ tags: ["public"] })`. */
3
3
  export declare function transformOf<Options extends object = StageOptions>(settings: TransformSettings<Options>): TransformFactory<Options>;
4
- /** A factory of sink stages: `openapi({ info })`. */
5
- export declare function sinkOf<Result, Options extends object = StageOptions>(settings: SinkSettings<Result, Options>): SinkFactory<Result, Options>;
4
+ /**
5
+ * A factory of sink stages: `openapi({ info })`. A sink that needs what JSON
6
+ * cannot hold (middlewares, an authenticator) takes it as a second argument,
7
+ * its `hooks`, which `build` receives and the description of the stage
8
+ * leaves out.
9
+ */
10
+ export declare function sinkOf<Result, Options extends object = StageOptions, Hooks = never>(settings: SinkSettings<Result, Options, Hooks>): SinkFactory<Result, Options, Hooks>;
6
11
  /**
7
12
  * What a stage is: its kind, name and JSON options when it came from a
8
13
  * factory, `{ kind: "opaque" }` otherwise (a hand-written stage needs an
@@ -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
+ }
@@ -11,7 +11,7 @@ export declare function raisedOf(error: Exception): Exception;
11
11
  * name; 400 for invalid input or context (and any exception whose
12
12
  * `details.issues` is a list, except `InvalidCommandEnvironment`, a 500); 404 for a missing entity or operation; 409 for
13
13
  * a unique violation, a concurrency conflict or an entity that already exists; 401 for
14
- * `UseCaseInvalidCredentials`; 422 for a class in `operation.errors`; 500 for the
14
+ * `UseCaseInvalidCredentials` and `Unauthenticated`; 403 for `Forbidden`; 422 for a class in `operation.errors`; 500 for the
15
15
  * rest, errors that are not exceptions included. Exceptions match by name,
16
16
  * or by class for `operation.errors`.
17
17
  */
@@ -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
+ }
@@ -0,0 +1,134 @@
1
+ import type { DescribedJwtOptions } from "../../auth/types/jwt.interface.js";
2
+ import type { Principal } from "../../use-case/types/access.interface.js";
3
+ import type { OpenAPIMethod, OpenAPISinkOptions, RouteOptions } from "../openapi-sink.js";
4
+ import type { OperationDescriptor } from "./output.interface.js";
5
+ /**
6
+ * The options every sink that serves HTTP takes. JSON, so the stage can live
7
+ * in an `afora.json`. The routing options (`basePath`, `route`, `responses`,
8
+ * `requestId`, `security`) are those of the `openapi` sink, so the served
9
+ * routes and the document agree.
10
+ */
11
+ export interface HttpOptions extends RouteOptions {
12
+ /** Serves the OpenAPI document of the same operations at `path`. */
13
+ readonly docs?: {
14
+ readonly path: string;
15
+ readonly info: OpenAPISinkOptions["info"];
16
+ readonly servers?: OpenAPISinkOptions["servers"];
17
+ };
18
+ /**
19
+ * Adds the `trace` to problems and the real `detail` to 5xx ones.
20
+ *
21
+ * @default false
22
+ */
23
+ readonly debug?: boolean;
24
+ /**
25
+ * The context of an operation, by context key: from request headers
26
+ * (`{ headers: { locale: "accept-language" } }`) and from values a
27
+ * middleware of the server set for the request
28
+ * (`{ variables: { caller: "caller" } }`).
29
+ */
30
+ readonly context?: {
31
+ readonly headers?: {
32
+ readonly [contextKey: string]: string;
33
+ };
34
+ readonly variables?: {
35
+ readonly [contextKey: string]: string;
36
+ };
37
+ };
38
+ /**
39
+ * Who calls, from a JSON Web Token (see `jwtOf`; the secret is named by
40
+ * its environment variable, `{ env: "JWT_SECRET" }`): read from the
41
+ * `authorization` header (`Bearer <token>`; another header with `header`)
42
+ * and, with `cookie`, from that cookie when the header is absent. The
43
+ * document gets a matching security scheme unless `security` declares
44
+ * its own. An `authenticator` given in the hooks of the sink replaces it.
45
+ */
46
+ readonly auth?: {
47
+ readonly jwt: DescribedJwtOptions & {
48
+ readonly header?: string;
49
+ readonly cookie?: string;
50
+ };
51
+ };
52
+ }
53
+ /** A request, as the server of a sink gives it to the route table. */
54
+ export interface HttpRequest {
55
+ /** The value of the `{name}` segment of the path, decoded. */
56
+ param(name: string): string | undefined;
57
+ /** The query string, the first value of each name. */
58
+ query(): Readonly<Record<string, string | undefined>>;
59
+ /** The value of a header, by its name in lower case. */
60
+ header(name: string): string | undefined;
61
+ /** The body, as text. Called at most once, and only by routes with a body. */
62
+ text(): string | Promise<string>;
63
+ /** A value a middleware of the server set for this request, by name. */
64
+ variable?(name: string): unknown;
65
+ /** The request of the server itself, for an authenticator that needs it. */
66
+ readonly native?: unknown;
67
+ }
68
+ /** The answer to a request: what the server of a sink has to send, as is. */
69
+ export interface HttpResult {
70
+ readonly status: number;
71
+ /** `content-type` included, when there is a body. */
72
+ readonly headers: Readonly<Record<string, string>>;
73
+ /** One `set-cookie` header each. */
74
+ readonly cookies: readonly string[];
75
+ /** The body, already serialized (sensitive values masked), or `null` for none. */
76
+ readonly body: string | null;
77
+ }
78
+ /** What an authenticator reads of a request. */
79
+ export interface AuthRequest {
80
+ /** The value of a header, by its name in lower case. */
81
+ header(name: string): string | undefined;
82
+ /** The value of a cookie of the request. */
83
+ cookie(name: string): string | undefined;
84
+ /** The operation being called. */
85
+ readonly operation: OperationDescriptor;
86
+ /** The request of the server itself. */
87
+ readonly native?: unknown;
88
+ }
89
+ /**
90
+ * Who is calling, from the credentials of a request: a `Principal`, or
91
+ * `null` for an anonymous caller. Called for each request to an operation
92
+ * with an `access` other than `public`. It may throw `Unauthenticated` to
93
+ * refuse credentials it does not accept.
94
+ */
95
+ export type Authenticator = (request: AuthRequest) => Principal | null | Promise<Principal | null>;
96
+ /** One operation, ready to serve: the server routes `method` and `path` to `handle`. */
97
+ export interface CompiledRoute {
98
+ readonly operation: OperationDescriptor;
99
+ readonly method: OpenAPIMethod;
100
+ /** `basePath` included, parameters as `{name}`. */
101
+ readonly path: string;
102
+ /** The names of the `{name}` segments of `path`, in order. */
103
+ readonly parameters: readonly string[];
104
+ /** Never rejects: an error is a problem result. */
105
+ handle(request: HttpRequest): Promise<HttpResult>;
106
+ }
107
+ /** The routes of an output, compiled once (see `compileRoutes`). */
108
+ export interface RouteTable {
109
+ readonly routes: readonly CompiledRoute[];
110
+ /** The route that serves the OpenAPI document, with `docs`. */
111
+ readonly docs?: {
112
+ readonly path: string;
113
+ handle(request?: Pick<HttpRequest, "header">): HttpResult;
114
+ };
115
+ /** The 404 problem for a request no route serves. */
116
+ notFound(request?: Pick<HttpRequest, "header">): HttpResult;
117
+ /**
118
+ * The problem for an error raised outside `handle`: its status is the one
119
+ * the output `statuses` give its name, else `status`, else `statusOf`.
120
+ */
121
+ failure(error: unknown, options?: {
122
+ readonly operation?: OperationDescriptor;
123
+ readonly status?: number;
124
+ readonly request?: Pick<HttpRequest, "header">;
125
+ }): HttpResult;
126
+ }
127
+ /** What `compileRoutes` takes besides the operations and the options. */
128
+ export interface CompileRoutesSettings {
129
+ /** The name of the sink, for the messages of `InvalidOutputStage`. */
130
+ readonly source: string;
131
+ readonly authenticator?: Authenticator;
132
+ /** Changes the OpenAPI document before it is served (a sink with routes of its own kind). */
133
+ readonly document?: (document: unknown) => unknown;
134
+ }