@afora/core 0.0.9 → 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 (186) hide show
  1. package/README.md +52 -30
  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 +10 -3
  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,85 @@
1
+ import { ExceptionLayers } from "../exception/enums/exception-layers.js";
2
+ import { exceptionOf } from "../exception/exception.js";
3
+ export const InvalidApplicationDeclaration = exceptionOf({
4
+ layer: ExceptionLayers.Application,
5
+ name: "InvalidApplicationDeclaration",
6
+ message: ({ reason }) => reason
7
+ });
8
+ export const DuplicateApplicationDeclaration = exceptionOf({
9
+ layer: ExceptionLayers.Application,
10
+ name: "DuplicateApplicationDeclaration",
11
+ message: ({
12
+ first,
13
+ second
14
+ }) => `The same object is registered as ${first} and as ${second}.`
15
+ });
16
+ export const ApplicationConflict = exceptionOf({
17
+ layer: ExceptionLayers.Application,
18
+ name: "ApplicationConflict",
19
+ message: ({
20
+ reason
21
+ }) => reason
22
+ });
23
+ export const UnregisteredApplicationReference = exceptionOf({
24
+ layer: ExceptionLayers.Application,
25
+ name: "UnregisteredApplicationReference",
26
+ message: ({
27
+ holder,
28
+ reference
29
+ }) => `${holder} holds ${reference}, which is not registered in the application.`
30
+ });
31
+ export const UnregisteredRepositoryDependency = exceptionOf({
32
+ layer: ExceptionLayers.Application,
33
+ name: "UnregisteredRepositoryDependency",
34
+ message: ({
35
+ repository,
36
+ dependency
37
+ }) => `Repository ${repository} depends on ${dependency}, which is not registered in the application.`
38
+ });
39
+ export const UnregisteredUseCaseDependency = exceptionOf({
40
+ layer: ExceptionLayers.Application,
41
+ name: "UnregisteredUseCaseDependency",
42
+ message: ({
43
+ useCase,
44
+ dependency
45
+ }) => `Use case ${useCase} depends on ${dependency}, which is not registered in the application.`
46
+ });
47
+ export const ApplicationNotStarted = exceptionOf({
48
+ layer: ExceptionLayers.Application,
49
+ name: "ApplicationNotStarted",
50
+ message: ({
51
+ application
52
+ }) => application === undefined ? "No application is started: call await app.start() first." : `The application "${application}" is not started: call await app.start() first.`
53
+ });
54
+ export const ProviderNotBound = exceptionOf({
55
+ layer: ExceptionLayers.Application,
56
+ name: "ProviderNotBound",
57
+ message: ({
58
+ provider
59
+ }) => `The provider "${provider}" is made by an application: register its storage in applicationOf({ providers }) and call the repository through the application.`
60
+ });
61
+ export const ApplicationAlreadyStarted = exceptionOf({
62
+ layer: ExceptionLayers.Application,
63
+ name: "ApplicationAlreadyStarted",
64
+ message: ({
65
+ current,
66
+ attempted
67
+ }) => `The application "${current}" is already started, so "${attempted}" cannot be: stop it, or call start({ replace: true }).`
68
+ });
69
+ export const SchemaLockMismatch = exceptionOf({
70
+ layer: ExceptionLayers.Application,
71
+ name: "SchemaLockMismatch",
72
+ message: ({ problems }) => `The application does not match its schema lock: ${problems.join(" ")}`
73
+ });
74
+ export const InvalidSchemaMigration = exceptionOf({
75
+ layer: ExceptionLayers.Application,
76
+ name: "InvalidSchemaMigration",
77
+ message: ({ reason }) => reason
78
+ });
79
+ export const InvalidApplicationEnvironment = exceptionOf({
80
+ layer: ExceptionLayers.Environment,
81
+ name: "InvalidApplicationEnvironment",
82
+ message: ({
83
+ environments
84
+ }) => `Invalid environment variables: ${Object.entries(environments).map(([name, keys]) => `${name} (${keys.join(", ")})`).join("; ")}.`
85
+ });
@@ -0,0 +1,67 @@
1
+ import { migrateRepository } from "../repository/migrate.js";
2
+ import { findState } from "../repository/state.js";
3
+ import { InvalidSchemaMigration } from "./exceptions.js";
4
+ import { diffSchemas, schemaLockOf } from "./schema.js";
5
+ export async function migrateApplication(source, options) {
6
+ const fail = (reason) => {
7
+ throw new InvalidSchemaMigration({
8
+ source: source.name,
9
+ details: { reason }
10
+ });
11
+ };
12
+ if (typeof options !== "object" || options === null) {
13
+ fail('Expected the options of migrate, e.g. { mode: "check" }.');
14
+ }
15
+ const { mode, destructive = false, pageSize = 100, lock } = options;
16
+ if (mode !== "check" && mode !== "apply") {
17
+ fail('Expected mode to be "check" or "apply".');
18
+ }
19
+ if (typeof destructive !== "boolean") {
20
+ fail("Expected destructive to be a boolean.");
21
+ }
22
+ if (!Number.isSafeInteger(pageSize) || pageSize < 1) {
23
+ fail("Expected pageSize to be a positive integer.");
24
+ }
25
+ if (lock !== undefined && !isLock(lock)) {
26
+ fail("Expected lock to be a schema lock ({ lockVersion: 1, entities }).");
27
+ }
28
+ const byEntity = new Map;
29
+ for (const repository of source.repositories) {
30
+ const state = findState(repository);
31
+ if (state && !byEntity.has(state.entity))
32
+ byEntity.set(state.entity, state);
33
+ }
34
+ const names = options.entities ?? [...source.entities].filter(([, entity]) => byEntity.has(entity)).map(([name]) => name);
35
+ if (!Array.isArray(names))
36
+ fail("Expected entities to be a list of names.");
37
+ const targets = [];
38
+ for (const name of names) {
39
+ const entity = source.entities.get(name);
40
+ if (entity === undefined) {
41
+ fail(`"${name}" is not an entity of the application.`);
42
+ }
43
+ const state = byEntity.get(entity);
44
+ if (state === undefined) {
45
+ fail(`The entity "${name}" has no repository to migrate it through.`);
46
+ }
47
+ targets.push([name, state]);
48
+ }
49
+ const changes = lock === undefined ? [] : diffSchemas(lock, schemaLockOf(source.describe()));
50
+ const entities = {};
51
+ for (const [name, state] of targets) {
52
+ entities[name] = await migrateRepository(state, {
53
+ mode,
54
+ destructive,
55
+ pageSize,
56
+ plan: Object.freeze({
57
+ entity: name,
58
+ changes: changes.filter((change) => change.entity === name),
59
+ destructive
60
+ })
61
+ });
62
+ }
63
+ return { mode, destructive, entities };
64
+ }
65
+ export function isLock(value) {
66
+ return typeof value === "object" && value !== null && value.lockVersion === 1 && typeof value.entities === "object" && value.entities !== null;
67
+ }
@@ -0,0 +1,108 @@
1
+ import { Kind } from "../symbols/kind.js";
2
+ import { InvalidApplicationDeclaration, ProviderNotBound } from "./exceptions.js";
3
+ import { isProviderFactory } from "./start.js";
4
+ const slots = new WeakMap;
5
+ const storages = new WeakMap;
6
+ const METHODS = [
7
+ "findOne",
8
+ "findMany",
9
+ "insert",
10
+ "update",
11
+ "remove",
12
+ "count"
13
+ ];
14
+ const KEY = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
15
+ const RESERVED = new Set(["inMemory", "__proto__", "constructor", "prototype"]);
16
+ function invalid(reason) {
17
+ throw new InvalidApplicationDeclaration({
18
+ source: "providersOf",
19
+ details: { reason }
20
+ });
21
+ }
22
+ function isProviderObject(value) {
23
+ return typeof value === "object" && value !== null && !isProviderFactory(value) && METHODS.some((method) => typeof value[method] === "function");
24
+ }
25
+ function slotOf(key, storage, value) {
26
+ const slot = {
27
+ [Kind]: "provider-slot",
28
+ key
29
+ };
30
+ if (isProviderFactory(value)) {
31
+ const unbound = () => {
32
+ throw new ProviderNotBound({ source: key, details: { provider: key } });
33
+ };
34
+ for (const method of METHODS)
35
+ slot[method] = unbound;
36
+ slot.transaction = unbound;
37
+ slot.increment = unbound;
38
+ Object.defineProperty(slot, "capabilities", {
39
+ get: () => {
40
+ return;
41
+ },
42
+ enumerable: true
43
+ });
44
+ } else {
45
+ const target = value;
46
+ for (const method of [
47
+ ...METHODS,
48
+ "increment",
49
+ "transaction",
50
+ "close",
51
+ "migrate"
52
+ ]) {
53
+ if (typeof target[method] === "function") {
54
+ slot[method] = (...args) => target[method]?.(...args);
55
+ }
56
+ }
57
+ Object.defineProperty(slot, "capabilities", {
58
+ get: () => value.capabilities,
59
+ enumerable: true
60
+ });
61
+ }
62
+ Object.freeze(slot);
63
+ slots.set(slot, { key, storage, value });
64
+ return slot;
65
+ }
66
+ export function providersOf(entries) {
67
+ if (typeof entries !== "object" || entries === null || Array.isArray(entries) || Object.keys(entries).length === 0) {
68
+ invalid("Expected an object of providers by key.");
69
+ }
70
+ const storage = Object.create(null);
71
+ storage[Kind] = "provider-storage";
72
+ const seen = new Map;
73
+ const list = [];
74
+ for (const [key, value] of Object.entries(entries)) {
75
+ if (!KEY.test(key) || RESERVED.has(key)) {
76
+ invalid(key === "inMemory" ? `"inMemory" is the built-in provider; choose another key.` : `Expected "${key}" to be an identifier that is not reserved.`);
77
+ }
78
+ if (isProviderSlot(value)) {
79
+ invalid(`"${key}" holds a provider of another storage; a storage holds providers, not slots.`);
80
+ }
81
+ if (!isProviderFactory(value) && !isProviderObject(value)) {
82
+ invalid(`Expected "${key}" to be a repository provider or a factory of providerFrom.`);
83
+ }
84
+ const other = seen.get(value);
85
+ if (other !== undefined) {
86
+ invalid(`"${other}" and "${key}" hold the same provider.`);
87
+ }
88
+ seen.set(value, key);
89
+ const slot = slotOf(key, storage, value);
90
+ storage[key] = slot;
91
+ list.push(slot);
92
+ }
93
+ Object.freeze(storage);
94
+ storages.set(storage, Object.freeze(list));
95
+ return storage;
96
+ }
97
+ export function isProviderSlot(value) {
98
+ return typeof value === "object" && value !== null && slots.has(value);
99
+ }
100
+ export function isProviderStorage(value) {
101
+ return typeof value === "object" && value !== null && storages.has(value);
102
+ }
103
+ export function slotsOf(storage) {
104
+ return storages.get(storage) ?? [];
105
+ }
106
+ export function slotEntry(slot) {
107
+ return slots.get(slot);
108
+ }
@@ -0,0 +1,23 @@
1
+ import { ApplicationNotStarted } from "./exceptions.js";
2
+ const SLOT = Symbol.for("afora.application");
3
+ function slot() {
4
+ const global = globalThis;
5
+ global[SLOT] ??= { current: undefined };
6
+ return global[SLOT];
7
+ }
8
+ export function useApplication() {
9
+ const { current } = slot();
10
+ if (current === undefined) {
11
+ throw new ApplicationNotStarted({ source: "useApplication", details: {} });
12
+ }
13
+ return current;
14
+ }
15
+ export function resetApplication() {
16
+ slot().current = undefined;
17
+ }
18
+ export function startedApplication() {
19
+ return slot().current;
20
+ }
21
+ export function setStartedApplication(application) {
22
+ slot().current = application;
23
+ }
@@ -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
+ }