@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,368 @@
1
+ import { isBlueprint } from "../../blueprint/is-blueprint.js";
2
+ import { isBlueprintList } from "../../blueprint/list.js";
3
+ import { isRecordClass, recordFieldOf } from "../../blueprint/record-field.js";
4
+ import {
5
+ blueprintOfVariant,
6
+ isBlueprintVariant
7
+ } from "../../blueprint/variant.js";
8
+ import { isEnvironment } from "../../environment/environment.js";
9
+ import { isDeclaredException } from "../../exception/exception.js";
10
+ import { isRepository } from "../../repository/repository.js";
11
+ import { Kind } from "../../symbols/kind.js";
12
+ import { InvalidUseCaseStep } from "../exceptions.js";
13
+ import {
14
+ isPlain,
15
+ isStepName,
16
+ parseReference,
17
+ RESERVED_NAMES,
18
+ walkValue
19
+ } from "./references.js";
20
+ const SHAPES = {
21
+ find: { required: ["find"], optional: ["args", "as"] },
22
+ ensure: {
23
+ required: ["ensure", "value", "error"],
24
+ optional: ["other", "details"]
25
+ },
26
+ build: { required: ["build", "values", "as"], optional: [] },
27
+ change: {
28
+ required: ["change", "key", "value"],
29
+ optional: ["ifPresent", "as"]
30
+ },
31
+ invoke: { required: ["invoke", "method"], optional: ["args", "as"] },
32
+ create: { required: ["create", "value"], optional: ["as"] },
33
+ update: { required: ["update", "value"], optional: ["as"] },
34
+ delete: { required: ["delete", "value"], optional: ["as"] },
35
+ restore: { required: ["restore", "value"], optional: ["as"] },
36
+ call: { required: ["call", "input"], optional: ["as"] },
37
+ run: { required: ["run", "fn"], optional: ["args", "params", "as"] },
38
+ return: { required: ["return"], optional: [] }
39
+ };
40
+ const KINDS = Object.keys(SHAPES);
41
+ const ENSURE_KINDS = new Set([
42
+ "exists",
43
+ "absent",
44
+ "equals",
45
+ "notEquals",
46
+ "true",
47
+ "false"
48
+ ]);
49
+ const READ_ACTION = /^(?:findBy.+|findManyBy.+|findMany|exists|findPage|findCursor|count)$/;
50
+ const WRITE_ACTIONS = new Set([
51
+ "create",
52
+ "update",
53
+ "delete",
54
+ "restore",
55
+ "increment"
56
+ ]);
57
+ const INDEX = /^(?:0|[1-9][0-9]*)$/;
58
+ const LENGTH = Symbol("length");
59
+ const PRIMITIVE_SCHEMA_TYPES = new Set([
60
+ "string",
61
+ "number",
62
+ "integer",
63
+ "boolean",
64
+ "null"
65
+ ]);
66
+ function isPrimitiveProperty(field) {
67
+ const schema = field?.type?.schema;
68
+ if (typeof field !== "function" || schema === undefined)
69
+ return false;
70
+ try {
71
+ const type = JSON.parse(schema.toString()).type;
72
+ return typeof type === "string" && PRIMITIVE_SCHEMA_TYPES.has(type);
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+ function missingSegment(field, path) {
78
+ let current = field;
79
+ for (const segment of path) {
80
+ if (current === LENGTH)
81
+ return segment;
82
+ if (isBlueprint(current)) {
83
+ if (!Object.hasOwn(current.properties, segment))
84
+ return segment;
85
+ current = current.properties[segment];
86
+ continue;
87
+ }
88
+ if (isBlueprintList(current)) {
89
+ if (segment === "length") {
90
+ current = LENGTH;
91
+ continue;
92
+ }
93
+ if (!INDEX.test(segment))
94
+ return segment;
95
+ current = current.item;
96
+ continue;
97
+ }
98
+ if (isBlueprintVariant(current)) {
99
+ if (segment === current.key)
100
+ return;
101
+ const variants = current.variants;
102
+ const holder = Object.values(variants).map(blueprintOfVariant).find((variant) => Object.hasOwn(variant.properties, segment));
103
+ if (!holder)
104
+ return segment;
105
+ current = holder.properties[segment];
106
+ continue;
107
+ }
108
+ const record = recordFieldOf(current)?.record;
109
+ if (record) {
110
+ const { properties } = record.blueprint;
111
+ if (Object.hasOwn(properties, segment)) {
112
+ current = properties[segment];
113
+ continue;
114
+ }
115
+ return segment in record.prototype ? undefined : segment;
116
+ }
117
+ return isPrimitiveProperty(current) ? segment : undefined;
118
+ }
119
+ return;
120
+ }
121
+ export function validateSteps(declaration, steps) {
122
+ const { name } = declaration;
123
+ const fail = (index, code, reason) => {
124
+ throw new InvalidUseCaseStep({
125
+ source: name,
126
+ details: { index, reason, code }
127
+ });
128
+ };
129
+ if (!Array.isArray(steps) || steps.length === 0) {
130
+ fail(0, "missing_return", "Expected steps to be a list ending in return.");
131
+ }
132
+ const list = steps;
133
+ const bound = new Map;
134
+ const runs = new Set;
135
+ let returned = false;
136
+ const useCaseDependency = (key) => typeof key === "string" && Object.hasOwn(declaration.dependencies, key) ? declaration.dependencies[key] : undefined;
137
+ const checkReference = (index, text) => {
138
+ const reference = parseReference(text);
139
+ if (!reference) {
140
+ fail(index, "invalid_reference", `"${text}" is not a valid reference.`);
141
+ return;
142
+ }
143
+ const { root, path } = reference;
144
+ const firstKey = path[0];
145
+ if (root === "input" || root === "context") {
146
+ const blueprint = root === "input" ? declaration.blueprint : declaration.context;
147
+ if (blueprint === undefined) {
148
+ fail(index, "invalid_reference", `"${text}" uses $context, but the use case declares no context.`);
149
+ return;
150
+ }
151
+ const missing = missingSegment(blueprint, path);
152
+ if (missing !== undefined) {
153
+ fail(index, "unknown_key", `"${text}" names "${missing}", which the ${root} does not declare.`);
154
+ }
155
+ return;
156
+ }
157
+ if (root === "env") {
158
+ if (firstKey === undefined || !isEnvironment(useCaseDependency(firstKey))) {
159
+ fail(index, "unknown_dependency", `"${text}" must name an environment dependency.`);
160
+ }
161
+ return;
162
+ }
163
+ if (RESERVED_NAMES.includes(root)) {
164
+ fail(index, "invalid_reference", `"${root}" is reserved.`);
165
+ return;
166
+ }
167
+ if (!bound.has(root)) {
168
+ fail(index, "unbound_reference", `"${text}" uses "${root}", which no earlier step binds.`);
169
+ }
170
+ };
171
+ const checkValue = (index, value, field) => {
172
+ walkValue(value, (_reference, text) => checkReference(index, text), (issue, text) => fail(index, issue, issue === "invalid_reference" ? `"${text}" is not a valid reference.` : `${field} must hold only JSON values and references.`));
173
+ };
174
+ const checkArgs = (index, args) => {
175
+ if (args === undefined)
176
+ return;
177
+ if (typeof args === "string" && args.startsWith("$")) {
178
+ checkReference(index, args);
179
+ return;
180
+ }
181
+ if (!Array.isArray(args)) {
182
+ fail(index, "invalid_step", "args must be a list of values or a reference to one.");
183
+ }
184
+ checkValue(index, args, "args");
185
+ };
186
+ const checkTarget = (index, target, kind) => {
187
+ if (typeof target !== "string" || !target.startsWith("$")) {
188
+ fail(index, "invalid_reference", `${kind} must be a reference.`);
189
+ return;
190
+ }
191
+ checkReference(index, target);
192
+ };
193
+ const repositoryOf = (index, key, action) => {
194
+ const dependency = useCaseDependency(key);
195
+ if (!isRepository(dependency)) {
196
+ return fail(index, "unknown_dependency", `"${String(key)}" is not a repository dependency.`);
197
+ }
198
+ const actions = dependency.actions;
199
+ if (!actions.includes(action)) {
200
+ fail(index, "unknown_action", `The repository "${String(key)}" has no "${action}" action.`);
201
+ }
202
+ return { actions };
203
+ };
204
+ for (const [index, step] of list.entries()) {
205
+ if (!isPlain(step) || Array.isArray(step)) {
206
+ fail(index, "unknown_step", "A step must be an object.");
207
+ }
208
+ const record = step;
209
+ const kinds = KINDS.filter((kind) => Object.hasOwn(record, kind));
210
+ if (kinds.length !== 1) {
211
+ fail(index, "unknown_step", kinds.length === 0 ? `Unknown step; expected one of ${KINDS.join(", ")}.` : `A step has one kind, found ${kinds.join(" and ")}.`);
212
+ }
213
+ const kind = kinds[0];
214
+ const shape = SHAPES[kind];
215
+ for (const key of shape.required) {
216
+ if (!Object.hasOwn(record, key) || record[key] === undefined) {
217
+ fail(index, "invalid_step", `A ${kind} step needs "${key}".`);
218
+ }
219
+ }
220
+ for (const key of Object.keys(record)) {
221
+ if (!shape.required.includes(key) && !shape.optional.includes(key)) {
222
+ fail(index, "invalid_step", `A ${kind} step has no "${key}".`);
223
+ }
224
+ }
225
+ const as = record.as;
226
+ if (as !== undefined) {
227
+ if (!isStepName(as) || RESERVED_NAMES.includes(as) || bound.has(as)) {
228
+ fail(index, "invalid_step_name", `"${String(as)}" must be an identifier, not reserved, bound once.`);
229
+ }
230
+ }
231
+ let knownRecord;
232
+ switch (kind) {
233
+ case "find": {
234
+ const target = record.find;
235
+ const parts = typeof target === "string" ? target.split(".") : [];
236
+ const [repository, action] = parts;
237
+ if (parts.length !== 2 || !repository || !action) {
238
+ fail(index, "invalid_step", 'find must be "<repository>.<action>".');
239
+ }
240
+ if (WRITE_ACTIONS.has(action)) {
241
+ fail(index, "write_in_find", `find cannot call "${action}".`);
242
+ }
243
+ if (action === "forEachPage") {
244
+ fail(index, "unknown_action", '"forEachPage" runs a function on each page and cannot be a step; call it from a use case written in TypeScript.');
245
+ }
246
+ if (!READ_ACTION.test(action)) {
247
+ fail(index, "unknown_action", `"${action}" is not a read action.`);
248
+ }
249
+ repositoryOf(index, repository, action);
250
+ checkArgs(index, record.args);
251
+ break;
252
+ }
253
+ case "ensure": {
254
+ if (!ENSURE_KINDS.has(record.ensure)) {
255
+ fail(index, "invalid_step", `ensure must be one of ${[...ENSURE_KINDS].join(", ")}.`);
256
+ }
257
+ const compares = record.ensure === "equals" || record.ensure === "notEquals";
258
+ if (compares && record.other === undefined) {
259
+ fail(index, "invalid_step", `ensure "${record.ensure}" needs other.`);
260
+ }
261
+ if (!compares && record.other !== undefined) {
262
+ fail(index, "invalid_step", `ensure "${String(record.ensure)}" has no other.`);
263
+ }
264
+ if (!isDeclaredException(record.error) || !declaration.errors.includes(record.error)) {
265
+ fail(index, "undeclared_error", "ensure.error must be a class made by exceptionOf, listed in errors.");
266
+ }
267
+ checkValue(index, record.value, "value");
268
+ if (record.other !== undefined)
269
+ checkValue(index, record.other, "other");
270
+ if (record.details !== undefined) {
271
+ checkValue(index, record.details, "details");
272
+ }
273
+ break;
274
+ }
275
+ case "build": {
276
+ if (!isRecordClass(record.build)) {
277
+ fail(index, "invalid_class", "build must be a record or entity class.");
278
+ }
279
+ const values = record.values;
280
+ if (!(typeof values === "string" && values.startsWith("$")) && !isPlain(values)) {
281
+ fail(index, "invalid_step", "build.values must be a mapping or a reference.");
282
+ }
283
+ checkValue(index, values, "values");
284
+ knownRecord = record.build;
285
+ break;
286
+ }
287
+ case "change": {
288
+ checkTarget(index, record.change, "change");
289
+ if (typeof record.key !== "string" || record.key === "") {
290
+ fail(index, "invalid_step", "change.key must be a string.");
291
+ }
292
+ if (record.ifPresent !== undefined && typeof record.ifPresent !== "boolean") {
293
+ fail(index, "invalid_step", "change.ifPresent must be a boolean.");
294
+ }
295
+ const reference = parseReference(record.change);
296
+ const known = reference && reference.path.length === 0 ? bound.get(reference.root)?.record : undefined;
297
+ if (known) {
298
+ const keys = known.options?.change ?? [];
299
+ if (!keys.includes(record.key)) {
300
+ fail(index, "invalid_change_key", `"${String(record.key)}" is not a change key of ${known.name}.`);
301
+ }
302
+ }
303
+ checkValue(index, record.value, "value");
304
+ knownRecord = known;
305
+ break;
306
+ }
307
+ case "invoke": {
308
+ checkTarget(index, record.invoke, "invoke");
309
+ if (typeof record.method !== "string" || record.method === "") {
310
+ fail(index, "invalid_step", "invoke.method must be a string.");
311
+ }
312
+ checkArgs(index, record.args);
313
+ break;
314
+ }
315
+ case "create":
316
+ case "update":
317
+ case "delete":
318
+ case "restore": {
319
+ repositoryOf(index, record[kind], kind);
320
+ checkValue(index, record.value, "value");
321
+ break;
322
+ }
323
+ case "call": {
324
+ const dependency = useCaseDependency(record.call);
325
+ if (typeof dependency !== "function" || dependency[Kind] !== "use-case") {
326
+ fail(index, "unknown_dependency", `"${String(record.call)}" is not a use case dependency.`);
327
+ }
328
+ checkValue(index, record.input, "input");
329
+ break;
330
+ }
331
+ case "run": {
332
+ const runName = record.run;
333
+ if (!isStepName(runName) || runs.has(runName)) {
334
+ fail(index, "invalid_step_name", `run "${String(runName)}" must be an identifier, unique in the use case.`);
335
+ }
336
+ if (typeof record.fn !== "function") {
337
+ fail(index, "invalid_step", "run.fn must be a function.");
338
+ }
339
+ runs.add(runName);
340
+ checkArgs(index, record.args);
341
+ if (record.params !== undefined) {
342
+ if (!isBlueprint(record.params)) {
343
+ fail(index, "invalid_step", "run.params must be a blueprint.");
344
+ }
345
+ const declared = Object.keys(record.params.properties).length;
346
+ const given = record.args === undefined ? 0 : Array.isArray(record.args) ? record.args.length : undefined;
347
+ if (given !== undefined && given !== declared) {
348
+ fail(index, "invalid_args", `run.params declares ${declared} parameter(s), args gives ${given}.`);
349
+ }
350
+ }
351
+ break;
352
+ }
353
+ case "return": {
354
+ checkValue(index, record.return, "return");
355
+ returned = true;
356
+ if (index !== list.length - 1) {
357
+ fail(index, "misplaced_return", "return must be the last step, exactly once.");
358
+ }
359
+ break;
360
+ }
361
+ }
362
+ if (typeof as === "string")
363
+ bound.set(as, { record: knownRecord });
364
+ }
365
+ if (!returned) {
366
+ fail(list.length, "missing_return", "The steps must end with return.");
367
+ }
368
+ }
@@ -0,0 +1,100 @@
1
+ import { fieldOf } from "../blueprint/callable.js";
2
+ import { isBlueprint } from "../blueprint/is-blueprint.js";
3
+ import { isBlueprintList } from "../blueprint/list.js";
4
+ import { optionalOf } from "../blueprint/optional.js";
5
+ import { recordFieldOf } from "../blueprint/record-field.js";
6
+ import { isBlueprintVariant } from "../blueprint/variant.js";
7
+ import { identityKeysOf } from "../entity/abstract-entity.js";
8
+ import { isEntityClass } from "../entity/entity.js";
9
+ import { propertyInputOf } from "../property/input.js";
10
+ import { isProperty } from "../property/is-property.js";
11
+ import { recordNameOf } from "../record/name.js";
12
+ import { pluginsOf } from "../record/plugin.js";
13
+ import { findState, relatedRepositoryOf } from "../repository/state.js";
14
+ import { UseCaseUnavailable } from "./exceptions.js";
15
+ function isOptionalField(field) {
16
+ if (isProperty(field))
17
+ return field.type.optional === true;
18
+ const record = recordFieldOf(field);
19
+ if (record)
20
+ return record.optional;
21
+ return field.optional === true;
22
+ }
23
+ function planOf(entity, key, owners) {
24
+ const field = fieldOf(entity.blueprint.properties[key]);
25
+ const optional = isOptionalField(field);
26
+ const record = recordFieldOf(field);
27
+ const item = isBlueprintList(field) ? recordFieldOf(field.item) : undefined;
28
+ const related = [record?.record, item?.record].find(isEntityClass);
29
+ const kind = record ? related ? "entity" : "record" : isBlueprintList(field) ? related ? "entity-list" : "list" : isBlueprint(field) || isBlueprintVariant(field) ? "blueprint" : "property";
30
+ let input = { key, field };
31
+ if (kind === "entity" && related) {
32
+ const id = related.blueprint.properties.id;
33
+ input = { key: `${key}Id`, field: optional ? optionalOf(id) : id };
34
+ }
35
+ const declared = kind === "property" ? propertyInputOf(field) : undefined;
36
+ if (declared) {
37
+ input = {
38
+ key,
39
+ field: optional ? optionalOf(declared.accepts) : declared.accepts
40
+ };
41
+ }
42
+ return Object.freeze({
43
+ key,
44
+ field,
45
+ identity: identityKeysOf(entity).includes(key),
46
+ plugin: owners.get(key),
47
+ sensitive: entity.options.sensitive.includes(key),
48
+ unique: entity.options.unique.includes(key),
49
+ optional,
50
+ change: entity.options.change.includes(key),
51
+ kind,
52
+ related,
53
+ input: Object.freeze(input),
54
+ convert: declared ? (value) => value === undefined ? undefined : declared.convert(value) : undefined
55
+ });
56
+ }
57
+ export function useCaseTargetOf(repository, source = "useCaseTargetOf") {
58
+ const state = findState(repository);
59
+ if (!state)
60
+ throw new TypeError("Expected a repository made by repositoryOf");
61
+ const { entity } = state;
62
+ const unavailable = (reason) => {
63
+ throw new UseCaseUnavailable({
64
+ source,
65
+ details: { repository: state.name, reason }
66
+ });
67
+ };
68
+ const owners = new Map;
69
+ for (const plugin of pluginsOf(entity)) {
70
+ for (const key of plugin.keys)
71
+ owners.set(key, plugin.name);
72
+ }
73
+ const fields = Object.freeze(Object.keys(entity.blueprint.properties).map((key) => planOf(entity, key, owners)));
74
+ const field = (key) => fields.find((plan) => plan.key === key) ?? unavailable(`the entity has no key "${key}"`);
75
+ return Object.freeze({
76
+ repository,
77
+ repositoryName: state.name,
78
+ entity,
79
+ entityName: recordNameOf(entity),
80
+ actions: state.actions,
81
+ deletion: state.deletion,
82
+ fields,
83
+ changeKeys: entity.options.change,
84
+ uniqueKeys: entity.options.unique,
85
+ field,
86
+ relation(key, actions = []) {
87
+ const { related } = field(key);
88
+ const found = related ? relatedRepositoryOf(repository, related) : undefined;
89
+ if (!found) {
90
+ return unavailable(`the repository has no relation for the entity of "${key}"`);
91
+ }
92
+ const missing = actions.filter((action) => !found.actions.includes(action));
93
+ if (missing.length > 0) {
94
+ unavailable(`the repository of "${key}" lacks the action ${missing.map((action) => `"${action}"`).join(", ")}`);
95
+ }
96
+ return found;
97
+ },
98
+ unavailable
99
+ });
100
+ }
@@ -6,13 +6,29 @@ export type RouteSuccessStatus = 200 | 201 | 202 | 204;
6
6
  export interface RouteCookie {
7
7
  /** The name of the cookie. */
8
8
  readonly name: string;
9
- /** Not readable by scripts of the page. `true` by default. */
9
+ /**
10
+ * Not readable by scripts of the page.
11
+ *
12
+ * @default true
13
+ */
10
14
  readonly httpOnly?: boolean;
11
- /** Sent over HTTPS only. `true` by default. */
15
+ /**
16
+ * Sent over HTTPS only.
17
+ *
18
+ * @default true
19
+ */
12
20
  readonly secure?: boolean;
13
- /** `"lax"` by default. */
21
+ /**
22
+ * The `SameSite` attribute of the cookie.
23
+ *
24
+ * @default "lax"
25
+ */
14
26
  readonly sameSite?: "strict" | "lax" | "none";
15
- /** `"/"` by default. */
27
+ /**
28
+ * The `Path` attribute of the cookie.
29
+ *
30
+ * @default "/"
31
+ */
16
32
  readonly path?: string;
17
33
  readonly domain?: string;
18
34
  /** The seconds the cookie lives; without it, until the browser closes. */
@@ -32,7 +48,11 @@ export interface RouteDeclaration {
32
48
  * is the default of the sink: `/{kebab(command)}/{kebab(operation)}`.
33
49
  */
34
50
  readonly path?: string;
35
- /** `200` by default. */
51
+ /**
52
+ * The status of a successful response.
53
+ *
54
+ * @default 200
55
+ */
36
56
  readonly status?: RouteSuccessStatus;
37
57
  /**
38
58
  * Scalar input keys read from the query string. A `get` or `delete` route
@@ -37,14 +37,31 @@ export type AnyExceptionClass = DeclaredExceptionClass<any>;
37
37
  * `repositoryOf`, an environment made by `environmentOf`, or `Logger`.
38
38
  */
39
39
  export type UseCaseDependency = AnyUseCaseClass | AnyRepository | AnyEnvironment | LoggerDeclaration;
40
+ /**
41
+ * The `dependencies` of a use case, by name: use case classes, repositories,
42
+ * environments or the {@link Logger}.
43
+ */
40
44
  export type UseCaseCollection = Readonly<Record<string, UseCaseDependency>>;
41
45
  type Declaration = {
42
46
  readonly blueprint: AnyBlueprint;
43
47
  readonly response: AnyBlueprint;
44
48
  readonly dependencies: UseCaseCollection;
45
49
  };
50
+ /**
51
+ * The input of a use case class, as its constructor takes it: the input of its
52
+ * `blueprint`.
53
+ */
46
54
  export type UseCaseInput<Class extends Declaration> = BlueprintInput<Class["blueprint"]["properties"]>;
55
+ /**
56
+ * The value `execute` of a use case class resolves with: the value of its
57
+ * `response` blueprint.
58
+ */
47
59
  export type UseCaseResponse<Class extends Declaration> = BlueprintValue<Class["response"]["properties"]>;
60
+ /**
61
+ * The dependencies of a use case class as `this.dependencies` gives them: a
62
+ * function for a use case, the repository itself, the variables of an
63
+ * environment or an {@link ILogger} (see {@link DependencyOf}).
64
+ */
48
65
  export type UseCaseDependencies<Class extends Declaration> = DependencyFunctions<Class["dependencies"]>;
49
66
  /** The context of a use case class, as `this.context` gives it; `undefined` without one. */
50
67
  export type UseCaseContext<Class extends {
@@ -66,6 +83,10 @@ type OptionsArgs<Context extends IBlueprintCollection | undefined> = Context ext
66
83
  * repository itself, the variables of an environment, or an `ILogger`.
67
84
  */
68
85
  export type DependencyOf<Dependency> = Dependency extends AnyUseCaseClass ? (input: UseCaseInput<Dependency>) => Promise<UseCaseResponse<Dependency>> : Dependency extends AnyEnvironment ? EnvironmentValue<Dependency> : Dependency extends LoggerDeclaration ? ILogger : Dependency;
86
+ /**
87
+ * A collection of dependencies as a use case receives them: each one as {@link
88
+ * DependencyOf} gives it.
89
+ */
69
90
  export type DependencyFunctions<Dependencies extends UseCaseCollection> = {
70
91
  readonly [Key in keyof Dependencies]: DependencyOf<Dependencies[Key]>;
71
92
  };
@@ -76,6 +97,12 @@ export type DependencyFunctions<Dependencies extends UseCaseCollection> = {
76
97
  export type DependencyOverrides<Dependencies extends UseCaseCollection> = {
77
98
  readonly [Key in keyof Dependencies]?: Dependencies[Key] extends AnyRepository ? Partial<Dependencies[Key]> : DependencyOf<Dependencies[Key]>;
78
99
  };
100
+ /**
101
+ * What {@link useCaseOf} takes: the `name`, the `blueprint` of the input, the
102
+ * `response` blueprint, the `dependencies` and the `context`, with the
103
+ * documentation of the operation (`description`, `tags`, `deprecated`,
104
+ * `errors`) and where it is served and who may call it (`route`, `access`).
105
+ */
79
106
  export interface UseCaseSettings<Input extends IBlueprintCollection, Output extends IBlueprintCollection, Dependencies extends UseCaseCollection, Context extends IBlueprintCollection | undefined = undefined> {
80
107
  readonly name: string;
81
108
  readonly blueprint: IBlueprint<Input>;
@@ -125,9 +152,10 @@ export interface UseCaseDeclaration<Input extends IBlueprintCollection, Output e
125
152
  * class concrete: validated here, interpreted by its `execute`, and
126
153
  * described by `describe()`. TypeScript checks the kinds of the steps and
127
154
  * the names of dependencies and actions; with `strictSteps`, also their
128
- * references and values (see `CheckedSteps`).
155
+ * references and values, and the `return` against the response (see
156
+ * `CheckedSteps`).
129
157
  */
130
- readonly steps?: [Strict] extends [true] ? CheckedSteps<Steps, NoInfer<Input>, NoInfer<Context>, NoInfer<Dependencies>> : Steps;
158
+ readonly steps?: [Strict] extends [true] ? CheckedSteps<Steps, NoInfer<Input>, NoInfer<Context>, NoInfer<Dependencies>, NoInfer<Output>> : Steps;
131
159
  /**
132
160
  * `true` checks the steps, written inline, at compile time: each reference
133
161
  * must name a value in scope (the input, the context, an environment or the
@@ -137,10 +165,20 @@ export interface UseCaseDeclaration<Input extends IBlueprintCollection, Output e
137
165
  */
138
166
  readonly strictSteps?: Strict;
139
167
  }
168
+ /**
169
+ * A use case, or a decorator of one, is declared with what it cannot use;
170
+ * `reason` says what.
171
+ */
140
172
  export declare const InvalidUseCaseDeclaration: DeclaredExceptionClass<{
141
173
  reason: string;
142
174
  }>;
175
+ /** `execute` was called twice on one instance of a use case, which runs only once. */
143
176
  export declare const UseCaseAlreadyExecuted: DeclaredExceptionClass<undefined>;
177
+ /**
178
+ * The overrides given to `execute` name a dependency the use case does not
179
+ * declare, or give the wrong kind of value for one; or a dependency was called
180
+ * after `execute` ended.
181
+ */
144
182
  export declare const InvalidUseCaseDependencies: DeclaredExceptionClass<undefined>;
145
183
  /** An operation with validated input, dependency functions and a drainable event queue. */
146
184
  export declare abstract class AbstractUseCase<Input extends IBlueprintCollection, Output extends IBlueprintCollection, Dependencies extends UseCaseCollection> {
@@ -148,6 +186,14 @@ export declare abstract class AbstractUseCase<Input extends IBlueprintCollection
148
186
  constructor(input: BlueprintInput<Input>, ...args: unknown[]);
149
187
  /** Default parameter for implementations: `execute(deps = this.dependencies)`. */
150
188
  get dependencies(): DependencyFunctions<Dependencies>;
189
+ /**
190
+ * Runs the operation and gives its response. `dependencies` replace the
191
+ * declared ones for this call: a function for a use case, some methods of a
192
+ * repository, the variables of an environment.
193
+ *
194
+ * @throws {@link UseCaseAlreadyExecuted} when this instance already executed.
195
+ * @throws {@link InvalidUseCaseDependencies} when an override names an undeclared dependency or is of the wrong kind.
196
+ */
151
197
  abstract execute(dependencies?: DependencyOverrides<Dependencies>): Promise<BlueprintValue<Output>>;
152
198
  /** Immediately drains collected events; never executes their handlers. */
153
199
  pullEvents(): EntityEvent[];
@@ -176,6 +222,11 @@ type UseCaseInstanceContext<Context extends IBlueprintCollection | undefined> =
176
222
  /** What the caller supplied with the call, parsed and frozen; `undefined` without a declared context. */
177
223
  readonly context: Context extends IBlueprintCollection ? Readonly<BlueprintValue<Context>> : undefined;
178
224
  };
225
+ /**
226
+ * What {@link useCaseOf} gives without `steps`: an abstract class to extend,
227
+ * implementing an async `execute`. Its constructor takes the input and, when
228
+ * the use case declares a context, `{ context }`.
229
+ */
179
230
  export type UseCaseConstructor<Input extends IBlueprintCollection, Output extends IBlueprintCollection, Dependencies extends UseCaseCollection, Context extends IBlueprintCollection | undefined = undefined> = (abstract new (input: BlueprintInput<Input>, ...options: OptionsArgs<Context>) => AbstractUseCase<Input, Output, Dependencies> & UseCaseInstanceContext<Context>) & UseCaseStatics<Input, Output, Dependencies, Context>;
180
231
  /**
181
232
  * What `useCaseOf` gives with `steps`: concrete, its `execute` interprets the
@@ -188,8 +239,32 @@ export type StepsUseCaseConstructor<Input extends IBlueprintCollection, Output e
188
239
  * Declares an operation. With `steps` the class is concrete: its `execute`
189
240
  * interprets them. Without, it is abstract: extend it and implement an async
190
241
  * execute method.
242
+ *
243
+ * @throws {@link InvalidUseCaseDeclaration} when the name, the blueprints or the dependencies are missing or of the wrong kind, or a setting is not valid.
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * import { blueprintOf, useCaseOf } from "@afora/core";
248
+ *
249
+ * class Greet extends useCaseOf({
250
+ * name: "Greet",
251
+ * blueprint: blueprintOf((ctx) => ({ name: ctx.text })),
252
+ * response: blueprintOf((ctx) => ({ message: ctx.string })),
253
+ * }) {
254
+ * override async execute() {
255
+ * return { message: `Hello, ${this.input.name}!` };
256
+ * }
257
+ * }
258
+ *
259
+ * const { message } = await new Greet({ name: "Ana" }).execute();
260
+ * ```
191
261
  */
192
262
  export declare function useCaseOf<Input extends IBlueprintCollection, Output extends IBlueprintCollection, const Dependencies extends UseCaseCollection = Record<never, never>, Context extends IBlueprintCollection | undefined = undefined, const Steps extends readonly UseCaseStep<NoInfer<Dependencies>>[] | undefined = undefined, Strict extends boolean = false>(settings: UseCaseDeclaration<Input, Output, Dependencies, Context, Steps, Strict>): [Steps] extends [undefined] ? UseCaseConstructor<Input, Output, Dependencies, Context> : StepsUseCaseConstructor<Input, Output, Dependencies, Context>;
263
+ /**
264
+ * Whether `value` is a use case class made by {@link useCaseOf}, abstract or
265
+ * concrete, or a subclass of one.
266
+ */
193
267
  export declare function isUseCaseClass(value: unknown): value is AnyUseCaseClass;
268
+ /** Whether `value` is an instance of a use case class. */
194
269
  export declare function isUseCase(value: unknown): value is AbstractUseCase<IBlueprintCollection, IBlueprintCollection, UseCaseCollection>;
195
270
  export {};