@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,200 @@
1
+ import { toOpenAPI } from "../openapi/openapi.js";
2
+ import { isProperty } from "../property/is-property.js";
3
+ const ROUTE_METHODS = [
4
+ "get",
5
+ "post",
6
+ "put",
7
+ "patch",
8
+ "delete"
9
+ ];
10
+ const ROUTE_SUCCESS_STATUSES = [
11
+ 200,
12
+ 201,
13
+ 202,
14
+ 204
15
+ ];
16
+ const PARAMETER = /^\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
17
+ export const HEADER_NAME = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
18
+ export function isRoutePath(value, parameters = true) {
19
+ if (typeof value !== "string" || !value.startsWith("/"))
20
+ return false;
21
+ return value.split("/").every((segment) => parameters && PARAMETER.test(segment) || !/[{}?#\s:*]/.test(segment));
22
+ }
23
+ export function parametersOfPath(path) {
24
+ const names = [];
25
+ for (const segment of path.split("/")) {
26
+ const name = PARAMETER.exec(segment)?.[1];
27
+ if (name)
28
+ names.push(name);
29
+ }
30
+ return names;
31
+ }
32
+ export function isScalarSchema(schema) {
33
+ const types = [schema.type ?? []].flat();
34
+ return !types.some((type) => type === "object" || type === "array");
35
+ }
36
+ export function isScalarKey(blueprint, name) {
37
+ if (!Object.hasOwn(blueprint.properties, name))
38
+ return false;
39
+ const field = blueprint.properties[name];
40
+ return isProperty(field) && isScalarSchema(toOpenAPI(field));
41
+ }
42
+ function isObject(value) {
43
+ return typeof value === "object" && value !== null && !Array.isArray(value);
44
+ }
45
+ const ROUTE_KEYS = [
46
+ "method",
47
+ "path",
48
+ "status",
49
+ "query",
50
+ "headers",
51
+ "cookie",
52
+ "setCookie"
53
+ ];
54
+ const COOKIE_KEYS = [
55
+ "name",
56
+ "httpOnly",
57
+ "secure",
58
+ "sameSite",
59
+ "path",
60
+ "domain",
61
+ "maxAge"
62
+ ];
63
+ function cookieIssue(cookie, where) {
64
+ if (!isObject(cookie) || typeof cookie.name !== "string") {
65
+ return `Expected ${where} to be { name, httpOnly?, secure?, sameSite?, path?, domain?, maxAge? }.`;
66
+ }
67
+ for (const key of Object.keys(cookie)) {
68
+ if (!COOKIE_KEYS.includes(key))
69
+ return `${where} has no option "${key}".`;
70
+ }
71
+ if (!HEADER_NAME.test(cookie.name)) {
72
+ return `Expected ${where}.name to be the name of a cookie.`;
73
+ }
74
+ for (const key of ["httpOnly", "secure"]) {
75
+ if (cookie[key] !== undefined && typeof cookie[key] !== "boolean") {
76
+ return `Expected ${where}.${key} to be a boolean.`;
77
+ }
78
+ }
79
+ if (cookie.sameSite !== undefined && !["strict", "lax", "none"].includes(cookie.sameSite)) {
80
+ return `Expected ${where}.sameSite to be "strict", "lax" or "none".`;
81
+ }
82
+ if (cookie.sameSite === "none" && cookie.secure === false) {
83
+ return `${where}.sameSite "none" needs a secure cookie.`;
84
+ }
85
+ for (const key of ["path", "domain"]) {
86
+ const value = cookie[key];
87
+ if (value !== undefined && (typeof value !== "string" || value === "" || /[;\s]/.test(value))) {
88
+ return `Expected ${where}.${key} to be text without ";" or spaces.`;
89
+ }
90
+ }
91
+ if (cookie.maxAge !== undefined && (!Number.isInteger(cookie.maxAge) || cookie.maxAge < 0)) {
92
+ return `Expected ${where}.maxAge to be a whole number of seconds, 0 or more.`;
93
+ }
94
+ return;
95
+ }
96
+ export function routeDeclarationOf(route, blueprint, response) {
97
+ if (!isObject(route)) {
98
+ return "Expected route to be { method, path?, status?, query?, headers?, cookie?, setCookie? }.";
99
+ }
100
+ for (const key of Object.keys(route)) {
101
+ if (!ROUTE_KEYS.includes(key))
102
+ return `route has no option "${key}".`;
103
+ }
104
+ const method = typeof route.method === "string" ? route.method.toLowerCase() : undefined;
105
+ if (!ROUTE_METHODS.includes(method)) {
106
+ return `Expected route.method to be one of ${ROUTE_METHODS.join(", ")}.`;
107
+ }
108
+ const { path, status, query, headers, cookie, setCookie } = route;
109
+ const places = new Map;
110
+ const place = (key, where) => {
111
+ const taken = places.get(key);
112
+ if (taken !== undefined) {
113
+ return `route reads the input key "${key}" from two places: ${taken} and ${where}.`;
114
+ }
115
+ if (!isScalarKey(blueprint, key)) {
116
+ return `route reads "${key}" from ${where}, but it is not a scalar key of the input.`;
117
+ }
118
+ places.set(key, where);
119
+ return;
120
+ };
121
+ if (path !== undefined) {
122
+ if (!isRoutePath(path)) {
123
+ return 'Expected route.path to start with "/", with parameters only as whole "{name}" segments and no query.';
124
+ }
125
+ for (const name of parametersOfPath(path)) {
126
+ const bad = place(name, "the path");
127
+ if (bad)
128
+ return bad;
129
+ }
130
+ }
131
+ if (status !== undefined && !ROUTE_SUCCESS_STATUSES.includes(status)) {
132
+ return `Expected route.status to be one of ${ROUTE_SUCCESS_STATUSES.join(", ")}.`;
133
+ }
134
+ if (query !== undefined) {
135
+ if (!Array.isArray(query) || query.some((key) => typeof key !== "string")) {
136
+ return "Expected route.query to be a list of input keys.";
137
+ }
138
+ for (const key of query) {
139
+ const bad = place(key, "the query");
140
+ if (bad)
141
+ return bad;
142
+ }
143
+ }
144
+ for (const [option, value, where] of [
145
+ ["headers", headers, "a header"],
146
+ ["cookie", cookie, "a cookie"]
147
+ ]) {
148
+ if (value === undefined)
149
+ continue;
150
+ if (!isObject(value)) {
151
+ return `Expected route.${option} to map input keys to names.`;
152
+ }
153
+ const names = new Set;
154
+ for (const [key, name] of Object.entries(value)) {
155
+ if (typeof name !== "string" || !HEADER_NAME.test(name)) {
156
+ return `Expected route.${option}["${key}"] to be the name of ${where}.`;
157
+ }
158
+ const bad = place(key, where);
159
+ if (bad)
160
+ return bad;
161
+ const folded = option === "headers" ? name.toLowerCase() : name;
162
+ if (names.has(folded)) {
163
+ return `route.${option} reads "${name}" into two input keys.`;
164
+ }
165
+ names.add(folded);
166
+ }
167
+ }
168
+ if (setCookie !== undefined) {
169
+ if (!isObject(setCookie)) {
170
+ return "Expected route.setCookie to map response keys to cookies.";
171
+ }
172
+ const names = new Set;
173
+ for (const [key, settings] of Object.entries(setCookie)) {
174
+ if (!isScalarKey(response, key)) {
175
+ return `route.setCookie sends "${key}", which is not a scalar key of the response.`;
176
+ }
177
+ const bad = cookieIssue(settings, `route.setCookie["${key}"]`);
178
+ if (bad)
179
+ return bad;
180
+ const { name } = settings;
181
+ if (names.has(name)) {
182
+ return `route.setCookie sets the cookie "${name}" from two response keys.`;
183
+ }
184
+ names.add(name);
185
+ }
186
+ }
187
+ return Object.freeze({
188
+ method,
189
+ ...path === undefined ? {} : { path },
190
+ ...status === undefined ? {} : { status },
191
+ ...query === undefined ? {} : { query: Object.freeze([...query]) },
192
+ ...headers === undefined ? {} : {
193
+ headers: Object.freeze(Object.fromEntries(Object.entries(headers).map(([key, name]) => [key, name.toLowerCase()])))
194
+ },
195
+ ...cookie === undefined ? {} : { cookie: Object.freeze({ ...cookie }) },
196
+ ...setCookie === undefined ? {} : {
197
+ setCookie: Object.freeze(Object.fromEntries(Object.entries(setCookie).map(([key, settings]) => [key, Object.freeze({ ...settings })])))
198
+ }
199
+ });
200
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ILogger } from "../logger/logger.js";
2
2
  import type { IRepositoryProvider } from "../repository/types/repository.interface.js";
3
+ import type { ITracer, ITraceSpan } from "../tracing/tracing.js";
3
4
  /**
4
5
  * What a call carries to its repositories, internal: the providers that
5
6
  * replace the ones the repositories declared (e.g. one inside a transaction).
@@ -14,6 +15,10 @@ export interface ExecutionScope {
14
15
  readonly environments?: ReadonlyMap<object, Readonly<Record<string, unknown>>>;
15
16
  /** The logger of the application running the call, given by its command. */
16
17
  readonly logger?: ILogger;
18
+ /** The tracer of the application running the call, given by its command. */
19
+ readonly tracer?: ITracer;
20
+ /** The span of the caller (a command, a use case): the parent of the next. */
21
+ readonly span?: ITraceSpan;
17
22
  }
18
23
  /**
19
24
  * Key of the constructor options (and of the repository view) that carries
@@ -0,0 +1,4 @@
1
+ export const ScopeKey = Symbol("afora.use-case.scope");
2
+ export function scopedProvider(scope, provider) {
3
+ return scope?.providers.get(provider) ?? provider;
4
+ }
@@ -1,3 +1,4 @@
1
+ import type { FieldsDescription } from "../../application/types/application.interface.js";
1
2
  import type { StepErrorDescription, UseCaseStepDescription } from "./types/steps.interface.js";
2
3
  /** How a class of a step is named in the descriptor. */
3
4
  export interface StepNames {
@@ -5,9 +6,12 @@ export interface StepNames {
5
6
  readonly record: (Class: unknown) => string;
6
7
  /** The registered name of an exception, or `{ core }` for one the package exports. */
7
8
  readonly error: (Class: unknown) => StepErrorDescription;
9
+ /** The fields of the `params` blueprint of a `run`. */
10
+ readonly params: (blueprint: unknown) => FieldsDescription;
8
11
  }
9
12
  /**
10
13
  * The steps as `describe()` gives them: the same data, with the classes of
11
- * `build` and `ensure.error` by name and the function of `run` as `opaque`.
14
+ * `build` and `ensure.error` by name, the function of `run` as `opaque` and
15
+ * its `params` by their fields.
12
16
  */
13
17
  export declare function describeSteps(steps: readonly Readonly<Record<string, unknown>>[], names: StepNames): UseCaseStepDescription[];
@@ -0,0 +1,24 @@
1
+ import { isPlain } from "./references.js";
2
+ function cloneData(value) {
3
+ if (Array.isArray(value))
4
+ return value.map(cloneData);
5
+ if (isPlain(value)) {
6
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneData(item)]));
7
+ }
8
+ return value;
9
+ }
10
+ export function describeSteps(steps, names) {
11
+ return steps.map((step) => {
12
+ const copy = cloneData({ ...step });
13
+ if (Object.hasOwn(step, "build"))
14
+ copy.build = names.record(step.build);
15
+ if (Object.hasOwn(step, "ensure"))
16
+ copy.error = names.error(step.error);
17
+ if (Object.hasOwn(step, "run")) {
18
+ copy.fn = { kind: "opaque" };
19
+ if (step.params !== undefined)
20
+ copy.params = names.params(step.params);
21
+ }
22
+ return copy;
23
+ });
24
+ }
@@ -0,0 +1,105 @@
1
+ export const RESERVED_NAMES = [
2
+ "input",
3
+ "context",
4
+ "env",
5
+ "literal"
6
+ ];
7
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
8
+ const SEGMENT = /^(?:[A-Za-z_][A-Za-z0-9_]*|0|[1-9][0-9]*)$/;
9
+ const BLOCKED = new Set(["__proto__", "constructor", "prototype"]);
10
+ export function isStepName(value) {
11
+ return typeof value === "string" && IDENTIFIER.test(value);
12
+ }
13
+ const parsed = new Map;
14
+ export function parseReference(text) {
15
+ const cached = parsed.get(text);
16
+ if (cached !== undefined)
17
+ return cached ?? undefined;
18
+ let result = null;
19
+ if (text.startsWith("$")) {
20
+ const [root, ...path] = text.slice(1).split(".");
21
+ if (root !== undefined && IDENTIFIER.test(root) && path.every((segment) => SEGMENT.test(segment))) {
22
+ result = Object.freeze({ root, path: Object.freeze(path) });
23
+ }
24
+ }
25
+ if (parsed.size < 4096)
26
+ parsed.set(text, result);
27
+ return result ?? undefined;
28
+ }
29
+ export function isPlain(value) {
30
+ if (typeof value !== "object" || value === null)
31
+ return false;
32
+ const prototype = Object.getPrototypeOf(value);
33
+ return prototype === Object.prototype || prototype === null;
34
+ }
35
+ function isLiteralEscape(value) {
36
+ return isPlain(value) && Object.keys(value).length === 1 && Object.hasOwn(value, "literal");
37
+ }
38
+ export function walkValue(value, onReference, onIssue) {
39
+ if (typeof value === "string") {
40
+ if (!value.startsWith("$"))
41
+ return;
42
+ const reference = parseReference(value);
43
+ if (!reference)
44
+ onIssue("invalid_reference", value);
45
+ else
46
+ onReference(reference, value);
47
+ return;
48
+ }
49
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
50
+ return;
51
+ }
52
+ if (isLiteralEscape(value)) {
53
+ return;
54
+ }
55
+ if (Array.isArray(value)) {
56
+ for (const item of value)
57
+ walkValue(item, onReference, onIssue);
58
+ return;
59
+ }
60
+ if (isPlain(value)) {
61
+ for (const item of Object.values(value)) {
62
+ walkValue(item, onReference, onIssue);
63
+ }
64
+ return;
65
+ }
66
+ onIssue("invalid_value", String(value));
67
+ }
68
+ export function followPath(from, path) {
69
+ let current = from;
70
+ for (const segment of path) {
71
+ if (typeof current !== "object" && typeof current !== "function" || current === null || BLOCKED.has(segment)) {
72
+ return;
73
+ }
74
+ current = current[segment];
75
+ }
76
+ return current;
77
+ }
78
+ export function resolveValue(value, lookup) {
79
+ if (typeof value === "string") {
80
+ if (!value.startsWith("$"))
81
+ return value;
82
+ const reference = parseReference(value);
83
+ return reference ? lookup(reference) : undefined;
84
+ }
85
+ if (isLiteralEscape(value))
86
+ return value.literal;
87
+ if (Array.isArray(value))
88
+ return value.map((item) => resolveValue(item, lookup));
89
+ if (isPlain(value)) {
90
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
91
+ key,
92
+ resolveValue(item, lookup)
93
+ ]));
94
+ }
95
+ return value;
96
+ }
97
+ export function freezeData(value) {
98
+ if (Array.isArray(value)) {
99
+ return Object.freeze(value.map(freezeData));
100
+ }
101
+ if (isPlain(value)) {
102
+ return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, freezeData(item)])));
103
+ }
104
+ return value;
105
+ }
@@ -0,0 +1,139 @@
1
+ import { isRecordClass } from "../../blueprint/record-field.js";
2
+ import { sameValue } from "../../repository/query.js";
3
+ import { InvalidUseCaseStep } from "../exceptions.js";
4
+ import { followPath, resolveValue } from "./references.js";
5
+ const FORBIDDEN_METHODS = new Set([
6
+ "set",
7
+ "emit",
8
+ "constructor",
9
+ "__proto__",
10
+ "hasOwnProperty",
11
+ "isPrototypeOf",
12
+ "propertyIsEnumerable",
13
+ "toLocaleString",
14
+ "valueOf",
15
+ "__defineGetter__",
16
+ "__defineSetter__",
17
+ "__lookupGetter__",
18
+ "__lookupSetter__"
19
+ ]);
20
+ function isRecordInstance(value) {
21
+ return typeof value === "object" && value !== null && isRecordClass(value.constructor);
22
+ }
23
+ function parsedArgs(params, args, invalid) {
24
+ const keys = Object.keys(params.properties);
25
+ if (args.length !== keys.length) {
26
+ invalid(`run.params declares ${keys.length} parameter(s), args gave ${args.length}.`, "invalid_args");
27
+ }
28
+ const result = params.safeParse(Object.fromEntries(keys.map((key, at) => [key, args[at]])));
29
+ if (result.issues) {
30
+ const [first] = result.issues;
31
+ const more = result.issues.length > 1 ? ` (and ${result.issues.length - 1} more)` : "";
32
+ return invalid(`the argument "${first?.path.join(".")}" ${first?.message}${more}.`, "invalid_args", result.issues);
33
+ }
34
+ const value = result.value;
35
+ return keys.map((key) => value[key]);
36
+ }
37
+ export async function executeSteps(dependencies = {}) {
38
+ const { useCaseName, steps = [] } = this.constructor;
39
+ const named = new Map;
40
+ const lookup = ({ root, path }) => {
41
+ if (root === "input")
42
+ return followPath(this.input, path);
43
+ if (root === "context")
44
+ return followPath(this.context, path);
45
+ if (root === "env")
46
+ return followPath(dependencies[path[0]], path.slice(1));
47
+ return followPath(named.get(root), path);
48
+ };
49
+ const value = (raw) => resolveValue(raw, lookup);
50
+ for (const [index, step] of steps.entries()) {
51
+ const invalid = (reason, code = "invalid_target", issues) => {
52
+ throw new InvalidUseCaseStep({
53
+ source: useCaseName,
54
+ details: { index, reason, code, ...issues ? { issues } : {} }
55
+ });
56
+ };
57
+ const list = (raw) => {
58
+ if (raw === undefined)
59
+ return [];
60
+ const resolved = value(raw);
61
+ if (!Array.isArray(resolved))
62
+ invalid("args must resolve to a list.");
63
+ return resolved;
64
+ };
65
+ const bind = (result) => {
66
+ if (typeof step.as === "string")
67
+ named.set(step.as, result);
68
+ };
69
+ const target = (field) => {
70
+ const resolved = value(step[field]);
71
+ if (!isRecordInstance(resolved)) {
72
+ invalid(`${field} must reference a record or entity.`);
73
+ }
74
+ return resolved;
75
+ };
76
+ if (Object.hasOwn(step, "return"))
77
+ return value(step.return);
78
+ if (Object.hasOwn(step, "find")) {
79
+ const [repository, action] = step.find.split(".");
80
+ const methods = dependencies[repository];
81
+ bind(await methods[action].call(methods, ...list(step.args)));
82
+ } else if (Object.hasOwn(step, "ensure")) {
83
+ const subject = value(step.value);
84
+ const passed = {
85
+ exists: subject !== undefined && subject !== null,
86
+ absent: subject === undefined || subject === null,
87
+ equals: sameValue(subject, value(step.other)),
88
+ notEquals: !sameValue(subject, value(step.other)),
89
+ true: subject === true,
90
+ false: subject === false
91
+ }[step.ensure];
92
+ if (!passed) {
93
+ const Thrown = step.error;
94
+ throw new Thrown({
95
+ source: useCaseName,
96
+ ...step.details === undefined ? {} : { details: value(step.details) }
97
+ });
98
+ }
99
+ } else if (Object.hasOwn(step, "build")) {
100
+ const Built = step.build;
101
+ bind(new Built(value(step.values)));
102
+ } else if (Object.hasOwn(step, "change")) {
103
+ const record = target("change");
104
+ const namespace = record.change;
105
+ const key = step.key;
106
+ if (typeof namespace !== "object" || namespace === null || typeof namespace[key] !== "function") {
107
+ invalid(`"${key}" is not a change key of the record.`);
108
+ }
109
+ const changed = value(step.value);
110
+ if (step.ifPresent !== true || changed !== undefined) {
111
+ await namespace[key]?.(changed);
112
+ }
113
+ bind(record);
114
+ } else if (Object.hasOwn(step, "invoke")) {
115
+ const record = target("invoke");
116
+ const method = step.method;
117
+ if (FORBIDDEN_METHODS.has(method) || typeof record[method] !== "function") {
118
+ invalid(`"${method}" is not a public method of the record.`);
119
+ }
120
+ bind(await record[method].apply(record, list(step.args)));
121
+ } else if (Object.hasOwn(step, "call")) {
122
+ const child = dependencies[step.call];
123
+ bind(await child(value(step.input)));
124
+ } else if (Object.hasOwn(step, "run")) {
125
+ const fn = step.fn;
126
+ const args = list(step.args);
127
+ bind(await fn(...step.params === undefined ? args : parsedArgs(step.params, args, invalid), { context: this.context, dependencies }));
128
+ } else {
129
+ const action = ["create", "update", "delete", "restore"].find((kind) => Object.hasOwn(step, kind));
130
+ const methods = dependencies[step[action]];
131
+ bind(await methods[action].call(methods, value(step.value)));
132
+ }
133
+ }
134
+ return;
135
+ }
136
+ export function interpretedStepsOf(UseCase) {
137
+ const Declared = UseCase;
138
+ return Declared.prototype.execute === executeSteps ? Declared.steps : undefined;
139
+ }