@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,398 @@
1
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2
+ function sorted(value) {
3
+ if (Array.isArray(value))
4
+ return value.map(sorted);
5
+ if (!isObject(value))
6
+ return value;
7
+ return Object.fromEntries(Object.keys(value).filter((key) => !key.startsWith("~") && value[key] !== undefined).sort().map((key) => [key, sorted(value[key])]));
8
+ }
9
+ function evolutionOf(property, description, found = {}, depth = 0) {
10
+ if (property === undefined || depth > 64)
11
+ return found;
12
+ const resolved = "ref" in property ? description.properties[property.ref] : property;
13
+ const lineage = resolved?.lineage;
14
+ if (lineage === undefined || lineage.kind === "factory")
15
+ return found;
16
+ if (lineage.kind === "refined") {
17
+ const { fill, renamedFrom } = lineage.options;
18
+ if (!("fill" in found) && fill !== undefined)
19
+ found.fill = fill;
20
+ if (found.renamedFrom === undefined && typeof renamedFrom === "string") {
21
+ found.renamedFrom = renamedFrom;
22
+ }
23
+ }
24
+ return evolutionOf(lineage.base, description, found, depth + 1);
25
+ }
26
+ function fieldsOf(fields, description) {
27
+ if (fields === undefined)
28
+ return;
29
+ return Object.fromEntries(Object.keys(fields).sort().map((key) => [
30
+ key,
31
+ lockFieldOf(fields[key], description)
32
+ ]));
33
+ }
34
+ function lockFieldOf(field, description) {
35
+ switch (field.kind) {
36
+ case "property": {
37
+ const property = "ref" in field ? description.properties[field.ref] : field;
38
+ const evolution = evolutionOf("ref" in field ? { ref: field.ref } : field, description);
39
+ return {
40
+ schema: sorted(property?.schema ?? {}),
41
+ optional: field.optional,
42
+ ...property?.unique ? { unique: true } : {},
43
+ ..."fill" in evolution ? { fill: sorted(evolution.fill) } : {},
44
+ ...evolution.renamedFrom === undefined ? {} : { renamedFrom: evolution.renamedFrom }
45
+ };
46
+ }
47
+ case "list": {
48
+ const item = lockFieldOf(field.item, description);
49
+ return {
50
+ schema: sorted({
51
+ type: "array",
52
+ items: item.schema,
53
+ minItems: field.minItems,
54
+ maxItems: field.maxItems
55
+ }),
56
+ optional: field.optional,
57
+ ...item.fields === undefined ? {} : { fields: item.fields }
58
+ };
59
+ }
60
+ case "blueprint": {
61
+ const fields = "ref" in field ? description.blueprints[field.ref]?.fields : field.fields;
62
+ return {
63
+ schema: { type: "object" },
64
+ optional: field.optional,
65
+ fields: fieldsOf(fields, description) ?? {}
66
+ };
67
+ }
68
+ case "record": {
69
+ const fields = description.records[field.ref]?.fields;
70
+ return {
71
+ schema: { type: "object" },
72
+ optional: field.optional,
73
+ fields: fieldsOf(fields, description) ?? {}
74
+ };
75
+ }
76
+ case "variant":
77
+ return {
78
+ schema: { discriminator: field.key, type: "object" },
79
+ optional: field.optional,
80
+ fields: Object.fromEntries(Object.keys(field.variants).sort().map((tag) => [
81
+ tag,
82
+ {
83
+ ...lockFieldOf(field.variants[tag], description),
84
+ optional: true
85
+ }
86
+ ]))
87
+ };
88
+ case "entity":
89
+ return {
90
+ schema: { entity: field.ref, type: "string" },
91
+ optional: field.optional
92
+ };
93
+ }
94
+ }
95
+ function lockEntityOf(entity, description) {
96
+ const declared = fieldsOf(entity.fields, description) ?? {};
97
+ const unique = new Set(entity.options.unique.filter((key) => key !== "id"));
98
+ for (const [key, field] of Object.entries(declared)) {
99
+ if (field.unique)
100
+ unique.add(key);
101
+ }
102
+ const fields = Object.fromEntries(Object.entries(declared).map(([key, field]) => [
103
+ key,
104
+ unique.has(key) ? { ...field, unique: true } : field
105
+ ]));
106
+ return {
107
+ schemaVersion: entity.schemaVersion ?? 1,
108
+ previousNames: [...entity.previousNames ?? []].sort(),
109
+ unique: [...unique].sort(),
110
+ fields,
111
+ ...entity.upgrade === undefined || entity.upgrade.length === 0 ? {} : { upgrade: [...entity.upgrade].sort((a, b) => a - b) }
112
+ };
113
+ }
114
+ export function schemaLockOf(description) {
115
+ return {
116
+ lockVersion: 1,
117
+ entities: sorted(Object.fromEntries(Object.keys(description.entities).map((name) => [
118
+ name,
119
+ lockEntityOf(description.entities[name], description)
120
+ ])))
121
+ };
122
+ }
123
+ const METADATA = new Set([
124
+ "title",
125
+ "description",
126
+ "examples",
127
+ "default",
128
+ "deprecated",
129
+ "readOnly",
130
+ "writeOnly",
131
+ "$comment"
132
+ ]);
133
+ const UPPER = new Set([
134
+ "maximum",
135
+ "exclusiveMaximum",
136
+ "maxLength",
137
+ "maxItems",
138
+ "maxProperties"
139
+ ]);
140
+ const LOWER = new Set([
141
+ "minimum",
142
+ "exclusiveMinimum",
143
+ "minLength",
144
+ "minItems",
145
+ "minProperties"
146
+ ]);
147
+ const NARROWING = new Set([
148
+ "pattern",
149
+ "format",
150
+ "multipleOf",
151
+ "allOf",
152
+ "const"
153
+ ]);
154
+ const same = (a, b) => JSON.stringify(sorted(a)) === JSON.stringify(sorted(b));
155
+ function literalsOf(schema) {
156
+ if (Array.isArray(schema.enum)) {
157
+ return schema.enum.map((value) => JSON.stringify(value));
158
+ }
159
+ if (Array.isArray(schema.anyOf) && schema.anyOf.every((item) => isObject(item) && ("const" in item))) {
160
+ return schema.anyOf.map((item) => JSON.stringify(item.const));
161
+ }
162
+ return;
163
+ }
164
+ function compareSchemas(previous, next) {
165
+ if (same(previous, next))
166
+ return "same";
167
+ const strip = (schema) => Object.fromEntries(Object.entries(schema).filter(([key]) => !METADATA.has(key)));
168
+ const [a, b] = [strip(previous), strip(next)];
169
+ if (same(a, b))
170
+ return "metadata";
171
+ if (!same(a.type, b.type) || !same(a.entity, b.entity))
172
+ return "type-changed";
173
+ const [before, after] = [literalsOf(a), literalsOf(b)];
174
+ let loosened = false;
175
+ let tightened = false;
176
+ if (before !== undefined || after !== undefined) {
177
+ if (before === undefined || after === undefined)
178
+ return "type-changed";
179
+ const kept = before.every((value) => after.includes(value));
180
+ const gained = after.some((value) => !before.includes(value));
181
+ if (!kept)
182
+ tightened = true;
183
+ if (gained)
184
+ loosened = true;
185
+ }
186
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
187
+ for (const key of keys) {
188
+ if (["type", "entity", "enum", "anyOf"].includes(key))
189
+ continue;
190
+ if (same(a[key], b[key]))
191
+ continue;
192
+ const [x, y] = [a[key], b[key]];
193
+ if (key === "items") {
194
+ const items = compareSchemas(isObject(x) ? x : {}, isObject(y) ? y : {});
195
+ if (items === "type-changed")
196
+ return "type-changed";
197
+ if (items === "tightened")
198
+ tightened = true;
199
+ if (items === "loosened")
200
+ loosened = true;
201
+ } else if (UPPER.has(key) || LOWER.has(key)) {
202
+ if (x === undefined)
203
+ tightened = true;
204
+ else if (y === undefined)
205
+ loosened = true;
206
+ else if (typeof x !== "number" || typeof y !== "number")
207
+ return "type-changed";
208
+ else if (UPPER.has(key) ? y > x : y < x)
209
+ loosened = true;
210
+ else
211
+ tightened = true;
212
+ } else if (NARROWING.has(key)) {
213
+ if (y === undefined)
214
+ loosened = true;
215
+ else
216
+ tightened = true;
217
+ } else {
218
+ return "type-changed";
219
+ }
220
+ }
221
+ if (tightened)
222
+ return "tightened";
223
+ return loosened ? "loosened" : "metadata";
224
+ }
225
+ function push(context, change, klass, satisfied, extra = {}) {
226
+ context.changes.push({
227
+ entity: context.entity,
228
+ ...extra.key === undefined ? {} : { key: extra.key },
229
+ change,
230
+ class: klass,
231
+ satisfied,
232
+ destructive: extra.destructive ?? false,
233
+ ...extra.from === undefined ? {} : { from: extra.from },
234
+ ...extra.detail === undefined ? {} : { detail: extra.detail }
235
+ });
236
+ }
237
+ const STEP = "an upgrade step (bump schemaVersion and add upgrade[n])";
238
+ function diffFields(context, previous, next, path) {
239
+ const join = (key) => path ? `${path}.${key}` : key;
240
+ const renamedFrom = new Map;
241
+ for (const [key, field] of Object.entries(next)) {
242
+ const from = field.renamedFrom;
243
+ if (from !== undefined && !(key in previous) && from in previous && !(from in next)) {
244
+ renamedFrom.set(key, from);
245
+ }
246
+ }
247
+ const moved = new Set(renamedFrom.values());
248
+ const keys = [...new Set([...Object.keys(previous), ...Object.keys(next)])].filter((key) => !moved.has(key)).sort();
249
+ for (const key of keys) {
250
+ const was = previous[key];
251
+ const is = next[key];
252
+ if (is === undefined) {
253
+ push(context, "removed", "automatic", true, {
254
+ key: join(key),
255
+ destructive: true,
256
+ detail: "Left out on read; a backfill removes it only with destructive: true. Rename it with renamedFrom to keep the data."
257
+ });
258
+ continue;
259
+ }
260
+ const from = renamedFrom.get(key);
261
+ if (from !== undefined) {
262
+ push(context, "renamed", "declared", true, {
263
+ key: join(key),
264
+ from: join(from)
265
+ });
266
+ diffField(context, previous[from], is, join(key));
267
+ continue;
268
+ }
269
+ if (was === undefined) {
270
+ if (is.optional) {
271
+ push(context, "added-optional", "automatic", true, { key: join(key) });
272
+ } else {
273
+ const satisfied = "fill" in is || context.stepped;
274
+ push(context, "added-required", "declared", satisfied, {
275
+ key: join(key),
276
+ ...satisfied ? {} : {
277
+ detail: `Needs a fill (.with({ fill })) or ${STEP} that sets it.`
278
+ }
279
+ });
280
+ }
281
+ continue;
282
+ }
283
+ diffField(context, was, is, join(key));
284
+ }
285
+ }
286
+ function diffField(context, was, is, key) {
287
+ const compared = compareSchemas(was.schema, is.schema);
288
+ if (compared === "type-changed") {
289
+ push(context, "type-changed", "declared", context.stepped, {
290
+ key,
291
+ ...context.stepped ? {} : { detail: `Needs ${STEP} that converts the stored values.` }
292
+ });
293
+ } else if (compared === "tightened") {
294
+ const satisfied = context.stepped || "fill" in is;
295
+ push(context, "tightened", "declared", satisfied, {
296
+ key,
297
+ detail: satisfied ? "Run app.migrate({ mode: 'check' }) to list the rows that violate it." : `Needs ${STEP} or a fill; app.migrate({ mode: 'check' }) lists the rows that violate it.`
298
+ });
299
+ } else if (compared === "loosened") {
300
+ push(context, "loosened", "automatic", true, { key });
301
+ } else if (compared === "metadata") {
302
+ push(context, "metadata", "automatic", true, { key });
303
+ }
304
+ if (was.optional && !is.optional) {
305
+ const satisfied = context.stepped || "fill" in is;
306
+ push(context, "made-required", "declared", satisfied, {
307
+ key,
308
+ ...satisfied ? {} : {
309
+ detail: `Needs a fill (.with({ fill })) or ${STEP} for the rows without it.`
310
+ }
311
+ });
312
+ } else if (!was.optional && is.optional) {
313
+ push(context, "made-optional", "automatic", true, { key });
314
+ }
315
+ if (!was.unique && is.unique) {
316
+ push(context, "unique-added", "check", true, {
317
+ key,
318
+ detail: "Run app.migrate({ mode: 'check' }) and look for duplicates before it is enforced."
319
+ });
320
+ } else if (was.unique && !is.unique) {
321
+ push(context, "unique-removed", "automatic", true, { key });
322
+ }
323
+ if (was.fields !== undefined || is.fields !== undefined) {
324
+ diffFields(context, was.fields ?? {}, is.fields ?? {}, key);
325
+ }
326
+ }
327
+ function diffEntity(entity, from, to, changes) {
328
+ const stepped = to.schemaVersion > from.schemaVersion && (to.upgrade ?? []).some((version) => version > from.schemaVersion && version <= to.schemaVersion);
329
+ const context = { entity, from, to, stepped, changes };
330
+ diffFields(context, from.fields, to.fields, "");
331
+ }
332
+ export function diffSchemas(lock, current) {
333
+ const changes = [];
334
+ const claimed = new Set;
335
+ for (const name of Object.keys(current.entities).sort()) {
336
+ const to = current.entities[name];
337
+ let from = lock.entities[name];
338
+ if (from === undefined) {
339
+ const previous = to.previousNames.find((old) => lock.entities[old] !== undefined && !current.entities[old]);
340
+ if (previous === undefined) {
341
+ changes.push({
342
+ entity: name,
343
+ change: "entity-added",
344
+ class: "automatic",
345
+ satisfied: true,
346
+ destructive: false
347
+ });
348
+ continue;
349
+ }
350
+ claimed.add(previous);
351
+ from = lock.entities[previous];
352
+ changes.push({
353
+ entity: name,
354
+ change: "entity-renamed",
355
+ class: "declared",
356
+ satisfied: true,
357
+ destructive: true,
358
+ from: previous,
359
+ detail: "app.migrate moves its rows; it removes them from the previous name only with destructive: true."
360
+ });
361
+ }
362
+ diffEntity(name, from, to, changes);
363
+ }
364
+ for (const name of Object.keys(lock.entities).sort()) {
365
+ if (current.entities[name] !== undefined || claimed.has(name))
366
+ continue;
367
+ changes.push({
368
+ entity: name,
369
+ change: "entity-removed",
370
+ class: "declared",
371
+ satisfied: false,
372
+ destructive: true,
373
+ detail: `No entity is named "${name}": list it in the previousNames of the entity that replaces it, or remove it from the lock if it is gone.`
374
+ });
375
+ }
376
+ return changes;
377
+ }
378
+ export function schemaLockProblemsOf(lock, current) {
379
+ const problems = [];
380
+ const renamed = new Map;
381
+ for (const [name, entity] of Object.entries(current.entities)) {
382
+ for (const old of entity.previousNames) {
383
+ if (!renamed.has(old))
384
+ renamed.set(old, name);
385
+ }
386
+ }
387
+ for (const name of Object.keys(lock.entities).sort()) {
388
+ const locked = lock.entities[name];
389
+ const now = current.entities[name] === undefined ? renamed.get(name) : name;
390
+ const entity = now === undefined ? undefined : current.entities[now];
391
+ if (entity === undefined) {
392
+ problems.push(`The lock holds the entity "${name}", which no entity is named or lists in previousNames.`);
393
+ } else if (entity.schemaVersion < locked.schemaVersion) {
394
+ problems.push(`The schemaVersion of "${now}" (${entity.schemaVersion}) is lower than the lock's (${locked.schemaVersion}).`);
395
+ }
396
+ }
397
+ return problems;
398
+ }
@@ -0,0 +1,162 @@
1
+ import { InvalidCommandDeclaration } from "../command/exceptions.js";
2
+ import {
3
+ isEnvironment,
4
+ loadEnvironments
5
+ } from "../environment/environment.js";
6
+ import { Kind } from "../symbols/kind.js";
7
+ import {
8
+ ApplicationNotStarted,
9
+ InvalidApplicationDeclaration
10
+ } from "./exceptions.js";
11
+ export function providerFrom(environmentDependencies, create) {
12
+ if (typeof environmentDependencies === "function" && create === undefined) {
13
+ return Object.freeze({
14
+ [Kind]: "provider-factory",
15
+ environments: Object.freeze([]),
16
+ create: environmentDependencies,
17
+ withContext: true
18
+ });
19
+ }
20
+ const list = isEnvironment(environmentDependencies) ? [environmentDependencies] : Array.isArray(environmentDependencies) ? [...environmentDependencies] : [null];
21
+ if (!list.every(isEnvironment) || new Set(list).size !== list.length || typeof create !== "function") {
22
+ throw new InvalidApplicationDeclaration({
23
+ source: "providerFrom",
24
+ details: {
25
+ reason: "Expected distinct environments made by environmentOf and a create function."
26
+ }
27
+ });
28
+ }
29
+ return Object.freeze({
30
+ [Kind]: "provider-factory",
31
+ environments: Object.freeze(list),
32
+ create,
33
+ withContext: false
34
+ });
35
+ }
36
+ export function logFrom(create) {
37
+ if (typeof create !== "function") {
38
+ throw new InvalidApplicationDeclaration({
39
+ source: "logFrom",
40
+ details: { reason: "Expected logFrom to take a create function." }
41
+ });
42
+ }
43
+ return Object.freeze({ [Kind]: "log-factory", create });
44
+ }
45
+ export function isLogFactory(value) {
46
+ return typeof value === "object" && value !== null && value[Kind] === "log-factory";
47
+ }
48
+ export function isProviderFactory(value) {
49
+ return typeof value === "object" && value !== null && value[Kind] === "provider-factory";
50
+ }
51
+ const deferred = new WeakMap;
52
+ const METHODS = [
53
+ "findOne",
54
+ "findMany",
55
+ "insert",
56
+ "update",
57
+ "remove",
58
+ "count"
59
+ ];
60
+ export function deferredProvider(factory, application) {
61
+ const state = {
62
+ factory,
63
+ application,
64
+ provider: undefined,
65
+ starting: undefined
66
+ };
67
+ const real = (method) => {
68
+ const { provider } = state;
69
+ if (!provider) {
70
+ throw new ApplicationNotStarted({
71
+ source: application,
72
+ details: { application }
73
+ });
74
+ }
75
+ if (typeof provider[method] !== "function") {
76
+ throw new InvalidApplicationDeclaration({
77
+ source: application,
78
+ details: {
79
+ reason: `The provider made on start has no "${method}".`
80
+ }
81
+ });
82
+ }
83
+ return provider;
84
+ };
85
+ const proxy = {};
86
+ for (const method of [...METHODS, "increment"]) {
87
+ proxy[method] = (...args) => real(method)[method]?.(...args);
88
+ }
89
+ proxy.transaction = (run) => real("transaction").transaction(run);
90
+ proxy.migrate = async (...args) => {
91
+ const { provider } = state;
92
+ if (!provider)
93
+ real("migrate");
94
+ return provider?.migrate?.(...args);
95
+ };
96
+ Object.defineProperty(proxy, "capabilities", {
97
+ get: () => state.provider?.capabilities,
98
+ enumerable: true
99
+ });
100
+ Object.freeze(proxy);
101
+ deferred.set(proxy, state);
102
+ return proxy;
103
+ }
104
+ export function isDeferredProvider(provider) {
105
+ return typeof provider === "object" && provider !== null && deferred.has(provider);
106
+ }
107
+ export function deferredEnvironments(provider) {
108
+ return (typeof provider === "object" && provider !== null ? deferred.get(provider)?.factory.environments : undefined) ?? [];
109
+ }
110
+ export function startDeferred(provider, context) {
111
+ const state = deferred.get(provider);
112
+ if (!state)
113
+ return;
114
+ state.starting ??= (async () => {
115
+ const made = await state.factory.create(state.factory.withContext ? context : loadEnvironments(state.factory.environments));
116
+ if (typeof made !== "object" || made === null) {
117
+ state.starting = undefined;
118
+ throw new InvalidApplicationDeclaration({
119
+ source: state.application,
120
+ details: {
121
+ reason: "Expected providerFrom's create to give a repository provider."
122
+ }
123
+ });
124
+ }
125
+ state.provider = made;
126
+ return made;
127
+ })();
128
+ return state.starting;
129
+ }
130
+ export async function stopProvider(provider) {
131
+ const state = deferred.get(provider);
132
+ if (!state) {
133
+ await provider.close?.();
134
+ return;
135
+ }
136
+ const made = state.provider;
137
+ state.provider = undefined;
138
+ state.starting = undefined;
139
+ await made?.close?.();
140
+ }
141
+ export function checkTransactions(source, provider, commands) {
142
+ if (commands.length === 0)
143
+ return;
144
+ if (typeof provider.transaction !== "function" || provider.capabilities?.transactions === false) {
145
+ throw new InvalidCommandDeclaration({
146
+ source,
147
+ details: {
148
+ reason: `The provider made on start does not support transactions, which ${commands.join(", ")} declare.`
149
+ }
150
+ });
151
+ }
152
+ }
153
+ export function checkCapabilities(source, provider, repositories) {
154
+ if (repositories.length === 0 || provider.capabilities?.keyset === true)
155
+ return;
156
+ throw new InvalidApplicationDeclaration({
157
+ source,
158
+ details: {
159
+ reason: `The provider made on start does not read by cursor (capabilities.keyset), which the findCursor of ${repositories.join(", ")} needs.`
160
+ }
161
+ });
162
+ }
@@ -9,8 +9,12 @@ import type { AnyOutput, StageDescription } from "../../output/types/output.inte
9
9
  import type { RecordPluginDescriptor } from "../../record/types/plugin.interface.js";
10
10
  import type { ProviderDescription, RepositoryIndex, RepositoryScans } from "../../repository/types/query.interface.js";
11
11
  import type { AnyRepository, IRepositoryProvider } from "../../repository/types/repository.interface.js";
12
+ import type { SchemaLock, SchemaMigrationOptions, SchemaMigrationReport } from "../../repository/types/schema.interface.js";
12
13
  import type { Kind } from "../../symbols/kind.js";
14
+ import type { ITracer } from "../../tracing/tracing.js";
13
15
  import type { UseCaseStepDescription } from "../../use-case/steps/types/steps.interface.js";
16
+ import type { AccessDeclaration } from "../../use-case/types/access.interface.js";
17
+ import type { RouteDeclaration } from "../../use-case/types/route.interface.js";
14
18
  import type { AnyExceptionClass, AnyUseCaseClass, UseCaseInput, UseCaseResponse } from "../../use-case/use-case.js";
15
19
  import type { ProviderSlot, ProviderStorage } from "../providers.js";
16
20
  import type { LogFactory, ProviderFactory } from "../start.js";
@@ -98,6 +102,14 @@ export interface ApplicationSettings<Name extends string, Extended extends reado
98
102
  * it, the one of the last extended application that has one; none by default.
99
103
  */
100
104
  readonly log?: LogSettings | LogFactory;
105
+ /**
106
+ * What traces it: a span per command operation, use case and repository
107
+ * call made through a use case, nested as the calls are, with names and
108
+ * outcomes and never a value (see `docs/tracing.md`). `otelTracer()` of
109
+ * `@afora/otel` gives one for OpenTelemetry. Without it, the one of the
110
+ * last extended application that has one; none by default.
111
+ */
112
+ readonly trace?: ITracer;
101
113
  /**
102
114
  * Where event policies such as `publishEvents` publish, by key: a
103
115
  * WebSocket hub, a queue, a broker. Commands of the application find them
@@ -206,6 +218,17 @@ export interface IApplication<Name extends string, Collections extends Applicati
206
218
  * started application of the process.
207
219
  */
208
220
  stop(): Promise<void>;
221
+ /**
222
+ * Brings the stored rows of its entities up to their current shape,
223
+ * walking each provider directly: in `check` it only reports, in `apply`
224
+ * it gives each provider the plan of its entity (`provider.migrate`, the
225
+ * changes from `lock`), then rewrites the rows below the current
226
+ * `schemaVersion` or with a `fill` or `renamedFrom` pending, and moves the
227
+ * rows stored under `previousNames`. Keys no longer declared, and the
228
+ * rows of the previous names, are only removed with `destructive: true`.
229
+ * Idempotent and resumable.
230
+ */
231
+ migrate(options: SchemaMigrationOptions): Promise<SchemaMigrationReport>;
209
232
  /** The variables of an environment registered in the application. */
210
233
  get<Environment extends AnyEnvironment>(environment: Environment): EnvironmentValue<Environment>;
211
234
  /** The bound version of a command registered in the application. */
@@ -225,12 +248,25 @@ export interface IApplication<Name extends string, Collections extends Applicati
225
248
  export interface ApplicationStartOptions {
226
249
  /**
227
250
  * Become the started application of the process (what `useApplication()`
228
- * gives). Default `true`; another one already started throws
229
- * `ApplicationAlreadyStarted`.
251
+ * gives). Another one already started throws `ApplicationAlreadyStarted`.
252
+ *
253
+ * @default true
230
254
  */
231
255
  readonly register?: boolean;
232
- /** Stop the started application of the process and take its place, e.g. on hot reload. Default `false`. */
256
+ /**
257
+ * Stop the started application of the process and take its place, e.g. on hot
258
+ * reload.
259
+ *
260
+ * @default false
261
+ */
233
262
  readonly replace?: boolean;
263
+ /**
264
+ * The committed schema lock (`afora.lock.json`): start throws
265
+ * `SchemaLockMismatch` when an entity of the lock is no longer named, and
266
+ * no entity lists it in `previousNames`, or when the `schemaVersion` of
267
+ * an entity is lower than the lock's.
268
+ */
269
+ readonly lock?: SchemaLock;
234
270
  }
235
271
  /** Any application. */
236
272
  export type AnyApplication = IApplication<string, any>;
@@ -290,7 +326,14 @@ export type FieldDescription = {
290
326
  readonly kind: "record" | "entity";
291
327
  readonly ref: string;
292
328
  readonly optional: boolean;
329
+ } | {
330
+ /** One of several blueprints, told apart by the value of `key`, the name of the variant. */
331
+ readonly kind: "variant";
332
+ readonly key: string;
333
+ readonly optional: boolean;
334
+ readonly variants: Readonly<Record<string, FieldDescription>>;
293
335
  };
336
+ /** The fields of a blueprint, record or entity in a description, by name. */
294
337
  export type FieldsDescription = Readonly<Record<string, FieldDescription>>;
295
338
  /** A record or an entity: its fields (an entity's identity and its plugins' keys left out), options and plugins. */
296
339
  export interface RecordDescription {
@@ -318,6 +361,12 @@ export interface RecordDescription {
318
361
  * `fields`) or `{ at: key }`. Present only when it has one.
319
362
  */
320
363
  readonly expires?: EntityExpires;
364
+ /** The `schemaVersion` of an entity; present only when declared. */
365
+ readonly schemaVersion?: number;
366
+ /** The versions the `upgrade` of an entity has a step to, ascending; present only when it has some. */
367
+ readonly upgrade?: readonly number[];
368
+ /** The `previousNames` of an entity; present only when it has some. */
369
+ readonly previousNames?: readonly string[];
321
370
  }
322
371
  /** What a use case depends on, by the name it has in the application. */
323
372
  export interface UseCaseDependencyDescription {
@@ -333,6 +382,10 @@ export interface UseCaseDescription {
333
382
  readonly description?: string;
334
383
  readonly tags: readonly string[];
335
384
  readonly deprecated: boolean;
385
+ /** Where it is served over HTTP, from the `route` setting or `@Route`. */
386
+ readonly route?: RouteDeclaration;
387
+ /** Who may call it, from the `access` setting or `@Access`. */
388
+ readonly access?: AccessDeclaration;
336
389
  readonly input: FieldsDescription;
337
390
  readonly response: FieldsDescription;
338
391
  readonly context?: FieldsDescription;
@@ -448,6 +501,10 @@ export interface ApplicationDescription {
448
501
  readonly log?: LogDescription | {
449
502
  readonly kind: "opaque";
450
503
  };
504
+ /** Present, and opaque, only when it has a `trace` (its own or inherited). */
505
+ readonly trace?: {
506
+ readonly kind: "opaque";
507
+ };
451
508
  /** Its publishers (inherited ones included), by key, with the application that declared each; present only when it has some. */
452
509
  readonly publishers?: Readonly<Record<string, {
453
510
  readonly from: string;