@toa.io/norm 1.0.0-alpha.32 → 1.0.0-alpha.321

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 (100) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/package.json +9 -7
  3. package/src/.component/.expand/bridge.js +2 -6
  4. package/src/.component/.expand/entity.js +2 -9
  5. package/src/.component/.expand/events.js +2 -6
  6. package/src/.component/.expand/extensions.js +7 -9
  7. package/src/.component/.expand/index.js +7 -19
  8. package/src/.component/.expand/operations.js +2 -9
  9. package/src/.component/.expand/receivers.js +4 -7
  10. package/src/.component/.expand/version.js +69 -28
  11. package/src/.component/.normalize/entity.js +7 -0
  12. package/src/.component/.normalize/events.js +16 -4
  13. package/src/.component/.normalize/index.js +4 -9
  14. package/src/.component/.normalize/operations.js +13 -6
  15. package/src/.component/.normalize/receivers.js +4 -5
  16. package/src/.component/collapse.js +60 -9
  17. package/src/.component/defaults.js +5 -11
  18. package/src/.component/dependencies.js +14 -10
  19. package/src/.component/dereference.js +5 -49
  20. package/src/.component/expand.js +3 -9
  21. package/src/.component/extensions.js +9 -10
  22. package/src/.component/index.js +10 -21
  23. package/src/.component/merge.js +25 -15
  24. package/src/.component/migrations.js +57 -0
  25. package/src/.component/normalize.js +10 -6
  26. package/src/.component/schema.yaml +165 -73
  27. package/src/.component/validate.js +63 -17
  28. package/src/.context/.dependencies/connectors.js +5 -9
  29. package/src/.context/.dependencies/describe.js +4 -5
  30. package/src/.context/.dependencies/extensions.js +93 -20
  31. package/src/.context/.dependencies/index.js +3 -9
  32. package/src/.context/.dependencies/resolve.js +44 -7
  33. package/src/.context/complete.js +1 -5
  34. package/src/.context/converge.js +31 -0
  35. package/src/.context/dependencies.js +43 -6
  36. package/src/.context/dereference.js +11 -6
  37. package/src/.context/evict.js +38 -0
  38. package/src/.context/expand.js +11 -6
  39. package/src/.context/index.js +8 -15
  40. package/src/.context/normalize.js +9 -7
  41. package/src/.context/schema.yaml +183 -11
  42. package/src/.context/validate.js +11 -14
  43. package/src/component.js +33 -14
  44. package/src/context.js +44 -15
  45. package/src/definition.js +63 -0
  46. package/src/entity.js +15 -0
  47. package/src/index.js +7 -9
  48. package/src/manifest.js +154 -0
  49. package/src/map.js +24 -0
  50. package/src/shortcuts.js +11 -15
  51. package/test/component/collapse.fixtures.js +30 -39
  52. package/test/component/collapse.test.js +90 -10
  53. package/test/component/dependencies.test.js +16 -14
  54. package/test/component/dereference.fixtures.js +10 -98
  55. package/test/component/dereference.test.js +10 -14
  56. package/test/component/dummies/extension/index.js +1 -5
  57. package/test/component/dummies/migrations/conflicting/migrations/0001-one.yaml +2 -0
  58. package/test/component/dummies/migrations/conflicting/migrations/0001-one.yml +2 -0
  59. package/test/component/dummies/migrations/malformed/migrations/0001-one.yaml +1 -0
  60. package/test/component/dummies/migrations/ordered/migrations/0001-first.yml +4 -0
  61. package/test/component/dummies/migrations/ordered/migrations/0002-second.yaml +4 -0
  62. package/test/component/dummies/migrations/ordered/migrations/0010-third.json +1 -0
  63. package/test/component/dummies/migrations/stateless/migrations/0001-one.yaml +2 -0
  64. package/test/component/expand.fixtures.js +13 -13
  65. package/test/component/expand.test.js +17 -13
  66. package/test/component/migrations.test.js +62 -0
  67. package/test/component/normalize.fixtures.js +3 -7
  68. package/test/component/normalize.test.js +94 -28
  69. package/test/component/validate.fixtures.js +17 -15
  70. package/test/component/validate.test.js +289 -109
  71. package/test/context/complete.fixtures.js +2 -8
  72. package/test/context/complete.test.js +14 -8
  73. package/test/context/converge.test.js +57 -0
  74. package/test/context/dereference.fixtures.js +3 -9
  75. package/test/context/dereference.test.js +18 -7
  76. package/test/context/evict.fixtures.js +39 -0
  77. package/test/context/evict.test.js +73 -0
  78. package/test/context/expand.fixtures.js +1 -5
  79. package/test/context/expand.test.js +36 -8
  80. package/test/context/normalize.fixtures.js +2 -7
  81. package/test/context/normalize.test.js +33 -9
  82. package/test/context/validate.fixtures.js +1 -6
  83. package/test/context/validate.test.js +173 -35
  84. package/test/definition.test.js +35 -0
  85. package/test/dummies/bare/index.js +1 -0
  86. package/test/dummies/defined/definition.js +1 -0
  87. package/test/dummies/defined/index.js +1 -0
  88. package/test/dummies/defined/package.json +1 -0
  89. package/test/manifest.test.js +126 -0
  90. package/test/shortcuts.fixtures.js +4 -9
  91. package/test/shortcuts.test.js +17 -15
  92. package/test/system.test.js +31 -0
  93. package/types/component.d.ts +31 -4
  94. package/types/context/declaration.d.ts +34 -3
  95. package/types/context.d.ts +27 -13
  96. package/types/entity.d.ts +7 -0
  97. package/types/index.d.ts +18 -3
  98. package/src/.component/.expand/properties.js +0 -14
  99. package/src/.context/.dependencies/load.js +0 -36
  100. package/test/context/dependencies.load.test.js +0 -24
@@ -1,12 +1,34 @@
1
- 'use strict'
1
+ import { Locator } from '@toa.io/core'
2
+ import { converge } from '../converge.js'
3
+ import { component as load } from '../../component.js'
4
+ import { definition } from '../../definition.js'
2
5
 
3
- const extensions = async (context) => {
6
+ export const extensions = async (context) => {
4
7
  const extensions = {}
5
8
  const components = context.components?.slice() ?? []
6
- const extracted = await extractExtensionComponents(components, extensions)
9
+
10
+ /** the components each extension brings, by reference
11
+ * @type {Map<string, toa.norm.Component[]>} */
12
+ const contributed = new Map()
13
+
14
+ // an extension a composition runs may be referenced by no component of this context,
15
+ // and its own components must be extracted all the same
16
+ const declared = await extractDeclaredServices(context, extensions, contributed)
17
+
18
+ const extracted = declared.concat(
19
+ await extractExtensionComponents(
20
+ components.concat(declared),
21
+ extensions,
22
+ contributed,
23
+ context.annotations
24
+ )
25
+ )
7
26
 
8
27
  components.push(...extracted)
9
28
 
29
+ // after the extraction, because what an extension ships converges like anything else
30
+ converge(context, components)
31
+
10
32
  for (const component of components) {
11
33
  if (component.extensions === undefined) continue
12
34
 
@@ -17,12 +39,33 @@ const extensions = async (context) => {
17
39
  }
18
40
  }
19
41
 
20
- return { extensions, components: extracted }
42
+ return { extensions, components: extracted, contributed }
21
43
  }
22
44
 
23
- async function extractExtensionComponents (components, extensions) {
24
- const { component: load } = require('../../component')
45
+ async function extractDeclaredServices(context, extensions, contributed) {
46
+ const extracted = []
47
+
48
+ for (const composition of context.compositions ?? [])
49
+ for (const reference of composition.services ?? []) {
50
+ if (reference in extensions) continue
25
51
 
52
+ try {
53
+ extracted.push(
54
+ ...(await extract(reference, extensions, contributed, context.annotations))
55
+ )
56
+ } catch (e) {
57
+ throw new Error(
58
+ `Composition '${composition.name}' lists '${reference}', ` +
59
+ `which cannot be resolved: ${e.message}`,
60
+ { cause: e }
61
+ )
62
+ }
63
+ }
64
+
65
+ return extracted
66
+ }
67
+
68
+ async function extractExtensionComponents(components, extensions, contributed, annotations) {
26
69
  const extracted = []
27
70
 
28
71
  for (const component of components) {
@@ -31,26 +74,56 @@ async function extractExtensionComponents (components, extensions) {
31
74
  for (const reference of Object.keys(component.extensions)) {
32
75
  if (reference in extensions) continue
33
76
 
34
- extensions[reference] = []
77
+ extracted.push(...(await extract(reference, extensions, contributed, annotations)))
78
+ }
79
+ }
80
+
81
+ if (extracted.length === 0) return extracted
35
82
 
36
- const mod = require(reference)
83
+ const deeper = await extractExtensionComponents(
84
+ extracted,
85
+ extensions,
86
+ contributed,
87
+ annotations
88
+ )
37
89
 
38
- if (mod.components === undefined) continue
90
+ return extracted.concat(deeper)
91
+ }
39
92
 
40
- for (const path of mod.components().paths) {
41
- const component = await load(path)
93
+ /**
94
+ * The components an extension contributes, if it contributes any.
95
+ *
96
+ * @param {string} reference
97
+ * @param {object} extensions
98
+ * @param {Map<string, toa.norm.Component[]>} contributed
99
+ * @param {object} [annotations]
100
+ * @returns {Promise<Array<toa.norm.Component>>}
101
+ */
102
+ async function extract(reference, extensions, contributed, annotations) {
103
+ extensions[reference] = []
42
104
 
43
- extracted.push(component)
44
- }
45
- }
46
- }
105
+ const { name, module: mod } = await definition(reference)
47
106
 
48
- if (extracted.length === 0)
49
- return extracted
107
+ if (mod.components === undefined) return []
50
108
 
51
- const deeper = await extractExtensionComponents(extracted, extensions)
109
+ // the annotation decides whether an extension contributes components at all
110
+ const annotation = annotations?.[name]
111
+ const { manifests, paths } = mod.components(annotation)
52
112
 
53
- return extracted.concat(deeper)
113
+ // a definition carries the manifests, read where the extension is not installed;
114
+ // an entry names the directories, where it is
115
+ const extracted = manifests === undefined ? [] : manifests.map(revive)
116
+
117
+ if (manifests === undefined) for (const path of paths) extracted.push(await load(path))
118
+
119
+ contributed.set(reference, extracted)
120
+
121
+ return extracted
54
122
  }
55
123
 
56
- exports.extensions = extensions
124
+ /** A manifest as a digest carries it, which is one norm read, without the locator. */
125
+ function revive(manifest) {
126
+ manifest.locator = new Locator(manifest.name, manifest.namespace)
127
+
128
+ return manifest
129
+ }
@@ -1,9 +1,3 @@
1
- 'use strict'
2
-
3
- const { connectors } = require('./connectors')
4
- const { extensions } = require('./extensions')
5
- const { resolve } = require('./resolve')
6
-
7
- exports.connectors = connectors
8
- exports.extensions = extensions
9
- exports.resolve = resolve
1
+ export { connectors } from './connectors.js'
2
+ export { extensions } from './extensions.js'
3
+ export { resolve } from './resolve.js'
@@ -1,13 +1,10 @@
1
- 'use strict'
1
+ import { definition } from '../../definition.js'
2
2
 
3
- const { load } = require('./load')
4
-
5
- const resolve = (references, annotations) => {
3
+ export const resolve = async (references, annotations = {}, context = {}) => {
6
4
  const dependencies = {}
7
5
 
8
6
  for (const [dependency, components] of Object.entries(references)) {
9
- const { metadata, module } = load(dependency)
10
- const id = metadata.name
7
+ const { name: id, module } = await definition(dependency)
11
8
 
12
9
  const instances = components.map((component) => ({
13
10
  locator: component.locator,
@@ -22,9 +19,49 @@ const resolve = (references, annotations) => {
22
19
  if (annotation !== undefined && module.annotation !== undefined) {
23
20
  annotations[id] = module.annotation(annotation, instances)
24
21
  }
22
+
23
+ // what a declaration costs to install, read from the definition rather than from the
24
+ // package: a deploy installs it, and nothing that declares nothing carries it. A component
25
+ // is asked for its own instance; a composition that runs the package's service is asked
26
+ // with none, because what a service brings is not a component's to declare
27
+ if (module.installs !== undefined) {
28
+ for (const instance of instances)
29
+ Object.assign(
30
+ (instance.component.packages ??= {}),
31
+ module.installs(instance, annotations[id])
32
+ )
33
+
34
+ const service = module.installs(undefined, annotations[id])
35
+
36
+ // mono runs every service in one process, so it takes every service's packages
37
+ Object.assign((context.packages ??= {}), service)
38
+
39
+ for (const composition of context.compositions ?? [])
40
+ if (composition.services?.includes(dependency) === true)
41
+ Object.assign((composition.packages ??= {}), service)
42
+ }
43
+ }
44
+
45
+ for (const dependency of Object.keys(annotations)) {
46
+ if (dependency in dependencies) continue
47
+
48
+ // an annotation may be keyed by a dependency id rather than by a module reference
49
+ const module = await optional(dependency)
50
+
51
+ if (module?.standalone === true) dependencies[dependency] = []
25
52
  }
26
53
 
27
54
  return dependencies
28
55
  }
29
56
 
30
- exports.resolve = resolve
57
+ /**
58
+ * @param {string} reference
59
+ * @returns {object | null}
60
+ */
61
+ async function optional(reference) {
62
+ try {
63
+ return (await definition(reference)).module
64
+ } catch {
65
+ return null
66
+ }
67
+ }
@@ -1,11 +1,9 @@
1
- 'use strict'
2
-
3
1
  /**
4
2
  * Completes missing compositions with unused components
5
3
  * @param {toa.norm.Context} context
6
4
  * @returns {void}
7
5
  */
8
- const complete = (context) => {
6
+ export const complete = (context) => {
9
7
  /** @type {Set<string>} */
10
8
  const composed = new Set()
11
9
 
@@ -29,5 +27,3 @@ const complete = (context) => {
29
27
  context.compositions.push({ name, components: [component] })
30
28
  }
31
29
  }
32
-
33
- exports.complete = complete
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A context that declares convergence converges every component that stores anything, and no
3
+ * manifest says so. An extension is loaded, deployed and hooked because a component's
4
+ * `extensions` names it, so this is where they come to name it.
5
+ *
6
+ * **Including the components an extension ships**, which is why this runs where they are
7
+ * known rather than over what the context declares: an application deployed as two regions
8
+ * whose identity does not converge is one where a user registered in the first does not
9
+ * exist in the second.
10
+ *
11
+ * There is nothing per-component to normalize and nothing to opt out of: a region is
12
+ * infrastructure, and what a component stores is not its business. What is genuinely owned by
13
+ * one region — which delayed calls this one makes — is said where that is decided, in
14
+ * `cadence.regions`, and not by holding the records back.
15
+ *
16
+ * @param {toa.norm.Context} context
17
+ * @param {toa.norm.Component[]} components every component the context deploys
18
+ * @returns {void}
19
+ */
20
+ export const converge = (context, components) => {
21
+ if (context.annotations?.[REFERENCE] === undefined) return
22
+
23
+ for (const component of components) {
24
+ if (component.entity === undefined) continue
25
+
26
+ component.extensions ??= {}
27
+ component.extensions[REFERENCE] ??= null
28
+ }
29
+ }
30
+
31
+ const REFERENCE = '@toa.io/extensions.convergence'
@@ -1,13 +1,50 @@
1
- 'use strict'
1
+ import { connectors, extensions, resolve } from './.dependencies/index.js'
2
2
 
3
- const { connectors, extensions, resolve } = require('./.dependencies')
3
+ export const dependencies = async (context) => {
4
+ const { extensions: e, components, contributed } = await extensions(context)
5
+
6
+ unmanaged(context, contributed)
4
7
 
5
- const dependencies = async (context) => {
6
- const { extensions: e, components } = await extensions(context)
7
8
  const c = connectors(context, components)
8
9
  const references = { ...c, ...e }
9
10
 
10
- return resolve(references, context.annotations)
11
+ return resolve(references, context.annotations, context)
11
12
  }
12
13
 
13
- exports.dependencies = dependencies
14
+ /**
15
+ * Marks as evicted what only evicted components require: the components an extension brings,
16
+ * where nothing a deployment carries references the extension and no composition runs it. A
17
+ * storage or a binding needs no mark of its own — its instances are components, and a
18
+ * deployment reads theirs.
19
+ *
20
+ * @param {toa.norm.Context} context
21
+ * @param {Map<string, toa.norm.Component[]>} contributed
22
+ * @returns {void}
23
+ */
24
+ function unmanaged(context, contributed) {
25
+ const components = context.components ?? []
26
+
27
+ if (!components.some((component) => component.evicted === true)) return
28
+
29
+ const required = new Set()
30
+ const pending = components.filter((component) => component.evicted !== true)
31
+
32
+ // what a composition runs is deployed for being listed, whoever references it
33
+ for (const composition of context.compositions ?? [])
34
+ for (const reference of composition.services ?? [])
35
+ pending.push(...(contributed.get(reference) ?? []))
36
+
37
+ while (pending.length > 0) {
38
+ const component = pending.pop()
39
+
40
+ if (required.has(component)) continue
41
+
42
+ required.add(component)
43
+
44
+ for (const reference of Object.keys(component.extensions ?? {}))
45
+ pending.push(...(contributed.get(reference) ?? []))
46
+ }
47
+
48
+ for (const brought of contributed.values())
49
+ for (const component of brought) if (!required.has(component)) component.evicted = true
50
+ }
@@ -1,16 +1,23 @@
1
- 'use strict'
2
-
3
1
  /**
4
2
  * Resolves component IDs within compositions with Component objects
5
3
  * @param {toa.norm.Context} context
6
4
  * @returns {void}
7
5
  */
8
- const dereference = (context) => {
6
+ export const dereference = (context) => {
9
7
  const components = map(context.components)
10
8
 
11
9
  if (context.compositions !== undefined) {
12
10
  for (const composition of context.compositions) {
13
- composition.components = composition.components.map((id) => components[id])
11
+ composition.components = composition.components.map((id) => {
12
+ const component = components[id]
13
+
14
+ if (component === undefined)
15
+ throw new Error(
16
+ `Composition '${composition.name}' lists an unknown component '${id}'.`
17
+ )
18
+
19
+ return component
20
+ })
14
21
  }
15
22
  }
16
23
  }
@@ -27,5 +34,3 @@ const map = (components) => {
27
34
 
28
35
  return map
29
36
  }
30
-
31
- exports.dereference = dereference
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Marks what the context evicts. An evicted component stays in the context like any other — it
3
+ * is called, its types are written, what it receives is published — and only a deployment
4
+ * leaves it out, because something else deploys it. What only evicted components require is
5
+ * marked once the extensions are extracted, by `dependencies`.
6
+ *
7
+ * An evicted service is taken off the compositions that list it, so that none of them runs it.
8
+ *
9
+ * @param {toa.norm.Context} context
10
+ * @returns {void}
11
+ */
12
+ export const evict = (context) => {
13
+ if (context.evicted === undefined) return
14
+
15
+ const components = new Set(context.evicted.components ?? [])
16
+ const services = new Set(context.evicted.services ?? [])
17
+
18
+ if (components.size > 0) {
19
+ const present = new Set(context.components.map((component) => component.locator.id))
20
+
21
+ for (const id of components)
22
+ if (!present.has(id))
23
+ throw new Error(`'evicted' names an unknown component '${id}'.`)
24
+
25
+ for (const component of context.components)
26
+ if (components.has(component.locator.id)) component.evicted = true
27
+ }
28
+
29
+ if (context.compositions === undefined) return
30
+
31
+ for (const composition of context.compositions) {
32
+ if (composition.services === undefined) continue
33
+
34
+ composition.services = composition.services.filter((reference) => !services.has(reference))
35
+
36
+ if (composition.services.length === 0) delete composition.services
37
+ }
38
+ }
@@ -1,13 +1,18 @@
1
- 'use strict'
2
-
3
- const shortcuts = require('../shortcuts')
1
+ import * as shortcuts from '../shortcuts.js'
4
2
 
5
3
  /**
6
4
  * @param {toa.norm.context.Declaration | object} context
7
5
  */
8
- const expand = (context) => {
6
+ export const expand = (context) => {
9
7
  shortcuts.recognize(shortcuts.SHORTCUTS, context, 'annotations')
10
8
  shortcuts.recognize(shortcuts.SHORTCUTS, context.annotations)
11
- }
12
9
 
13
- exports.expand = expand
10
+ // a composition names a service the way a manifest names an extension, and everything
11
+ // downstream matches it against a dependency, which is keyed by package reference
12
+ for (const composition of context.compositions ?? [])
13
+ if (composition.services !== undefined)
14
+ composition.services = composition.services.map(shortcuts.resolve)
15
+
16
+ if (context.evicted?.services !== undefined)
17
+ context.evicted.services = context.evicted.services.map(shortcuts.resolve)
18
+ }
@@ -1,15 +1,8 @@
1
- 'use strict'
2
-
3
- const { complete } = require('./complete')
4
- const { dependencies } = require('./dependencies')
5
- const { dereference } = require('./dereference')
6
- const { expand } = require('./expand')
7
- const { normalize } = require('./normalize')
8
- const { validate } = require('./validate')
9
-
10
- exports.complete = complete
11
- exports.dependencies = dependencies
12
- exports.dereference = dereference
13
- exports.expand = expand
14
- exports.normalize = normalize
15
- exports.validate = validate
1
+ export { complete } from './complete.js'
2
+ export { converge } from './converge.js'
3
+ export { dependencies } from './dependencies.js'
4
+ export { dereference } from './dereference.js'
5
+ export { evict } from './evict.js'
6
+ export { expand } from './expand.js'
7
+ export { normalize } from './normalize.js'
8
+ export { validate } from './validate.js'
@@ -1,19 +1,21 @@
1
- 'use strict'
1
+ import * as definitions from '@toa.io/definitions'
2
2
 
3
3
  /**
4
4
  * @param {toa.norm.context.Declaration | Object} context
5
5
  */
6
- const normalize = (context) => {
7
- const runtime = require('@toa.io/runtime')
8
-
9
- if (context.runtime === undefined) context.runtime = { version: runtime.version }
6
+ export const normalize = (context) => {
7
+ if (context.runtime === undefined) context.runtime = { version: definitions.version }
10
8
  if (typeof context.runtime === 'string') context.runtime = { version: context.runtime }
11
9
 
12
10
  if (context.runtime.version === undefined || context.runtime.version === '.') {
13
- context.runtime.version = runtime.version
11
+ context.runtime.version = definitions.version
14
12
  }
15
13
 
16
14
  if (typeof context.registry === 'string') context.registry = { base: context.registry }
15
+
16
+ // what an image is built for, where the Context does not say; `~` says the runner's own
17
+ if (context.registry !== undefined && context.registry.platforms === undefined)
18
+ context.registry.platforms = PLATFORMS
17
19
  }
18
20
 
19
- exports.normalize = normalize
21
+ const PLATFORMS = ['linux/amd64', 'linux/arm/v7', 'linux/arm64']