@toa.io/norm 1.0.0-alpha.3 → 1.0.0-alpha.300

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 (98) hide show
  1. package/CHANGELOG.md +124 -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 +5 -8
  7. package/src/.component/.expand/index.js +7 -17
  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 +55 -0
  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 +10 -6
  15. package/src/.component/.normalize/receivers.js +4 -5
  16. package/src/.component/collapse.js +60 -9
  17. package/src/.component/defaults.js +16 -25
  18. package/src/.component/dependencies.js +14 -10
  19. package/src/.component/dereference.js +20 -18
  20. package/src/.component/expand.js +6 -9
  21. package/src/.component/extensions.js +9 -10
  22. package/src/.component/index.js +10 -21
  23. package/src/.component/merge.js +25 -13
  24. package/src/.component/migrations.js +57 -0
  25. package/src/.component/normalize.js +10 -6
  26. package/src/.component/schema.yaml +139 -59
  27. package/src/.component/validate.js +37 -17
  28. package/src/.context/.dependencies/connectors.js +11 -10
  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 +179 -11
  42. package/src/.context/validate.js +8 -14
  43. package/src/component.js +37 -18
  44. package/src/context.js +33 -14
  45. package/src/definition.js +56 -0
  46. package/src/entity.js +15 -0
  47. package/src/index.js +6 -9
  48. package/src/manifest.js +154 -0
  49. package/src/shortcuts.js +9 -14
  50. package/test/component/collapse.fixtures.js +30 -39
  51. package/test/component/collapse.test.js +90 -10
  52. package/test/component/dependencies.test.js +16 -14
  53. package/test/component/dereference.fixtures.js +15 -24
  54. package/test/component/dereference.test.js +18 -8
  55. package/test/component/dummies/extension/index.js +1 -5
  56. package/test/component/dummies/migrations/conflicting/migrations/0001-one.yaml +2 -0
  57. package/test/component/dummies/migrations/conflicting/migrations/0001-one.yml +2 -0
  58. package/test/component/dummies/migrations/malformed/migrations/0001-one.yaml +1 -0
  59. package/test/component/dummies/migrations/ordered/migrations/0001-first.yml +4 -0
  60. package/test/component/dummies/migrations/ordered/migrations/0002-second.yaml +4 -0
  61. package/test/component/dummies/migrations/ordered/migrations/0010-third.json +1 -0
  62. package/test/component/dummies/migrations/stateless/migrations/0001-one.yaml +2 -0
  63. package/test/component/expand.fixtures.js +13 -12
  64. package/test/component/expand.test.js +17 -13
  65. package/test/component/migrations.test.js +62 -0
  66. package/test/component/normalize.fixtures.js +3 -7
  67. package/test/component/normalize.test.js +86 -28
  68. package/test/component/validate.fixtures.js +15 -13
  69. package/test/component/validate.test.js +181 -107
  70. package/test/context/complete.fixtures.js +2 -8
  71. package/test/context/complete.test.js +14 -8
  72. package/test/context/converge.test.js +57 -0
  73. package/test/context/dereference.fixtures.js +3 -9
  74. package/test/context/dereference.test.js +18 -7
  75. package/test/context/evict.fixtures.js +39 -0
  76. package/test/context/evict.test.js +72 -0
  77. package/test/context/expand.fixtures.js +1 -5
  78. package/test/context/expand.test.js +36 -8
  79. package/test/context/normalize.fixtures.js +2 -7
  80. package/test/context/normalize.test.js +33 -9
  81. package/test/context/validate.fixtures.js +1 -6
  82. package/test/context/validate.test.js +143 -34
  83. package/test/definition.test.js +35 -0
  84. package/test/dummies/bare/index.js +1 -0
  85. package/test/dummies/defined/definition.js +1 -0
  86. package/test/dummies/defined/index.js +1 -0
  87. package/test/dummies/defined/package.json +1 -0
  88. package/test/manifest.test.js +126 -0
  89. package/test/shortcuts.fixtures.js +4 -9
  90. package/test/shortcuts.test.js +17 -15
  91. package/types/component.d.ts +24 -5
  92. package/types/context/declaration.d.ts +33 -3
  93. package/types/context.d.ts +19 -13
  94. package/types/entity.d.ts +7 -0
  95. package/types/index.d.ts +12 -3
  96. package/src/.component/.expand/properties.js +0 -14
  97. package/src/.context/.dependencies/load.js +0 -36
  98. package/test/context/dependencies.load.test.js +0 -24
@@ -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']
@@ -6,10 +6,9 @@ properties:
6
6
  version:
7
7
  type: string
8
8
  name:
9
- $ref: 'definitions#/definitions/label'
10
- description:
11
9
  type: string
12
- packages:
10
+ pattern: ^([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?)(-([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?))*$
11
+ description:
13
12
  type: string
14
13
  build:
15
14
  type: object
@@ -41,28 +40,197 @@ properties:
41
40
  minItems: 1
42
41
  items:
43
42
  type: string
44
- default:
45
- - linux/amd64
46
- - linux/arm/v7
47
- - linux/arm64
48
- required: [base, platforms]
43
+ services:
44
+ description: >
45
+ Where an extension service's image comes from. `build`, which absence means,
46
+ builds it into `registry.base` the way a composition is built. `published` uses
47
+ the image the extension ships, which is neither built nor pushed.
48
+ enum: [build, published]
49
+ # required: [base]
49
50
  compositions:
50
51
  type: array
51
52
  minItems: 1
52
53
  items:
53
54
  type: object
55
+ additionalProperties: false
54
56
  properties:
55
57
  name:
56
- $ref: 'definitions#/definitions/token'
58
+ type: string
59
+ pattern: ^[a-zA-Z]([a-zA-Z0-9]{1,31})?$
57
60
  image:
58
61
  type: string
59
62
  components:
60
63
  type: array
61
64
  minItems: 1
62
65
  items:
63
- $ref: 'definitions#/definitions/locator'
66
+ type: string
67
+ pattern: ^([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?)(\.([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?))$
68
+ services:
69
+ description: >
70
+ Extension services this composition runs in its own pod, named by shortcut or
71
+ package reference, rather than each as its own deployment. A service listed here
72
+ keeps its own Service and Ingress; what changes is the pod they select.
73
+ type: array
74
+ minItems: 1
75
+ items:
76
+ type: string
77
+ minLength: 1
78
+ resources:
79
+ $ref: '#/definitions/resources'
64
80
  required: [name, components]
81
+ evicted:
82
+ description: >
83
+ What Toa does not deploy for this context, even where a composition lists it: it stays
84
+ part of the context, and is deployed by other means. A component named
85
+ here is in no pod, and what only it declared — an extension, a storage, a binding — is
86
+ not deployed either. A service named here gets no Deployment, Service or Ingress, and
87
+ no composition runs it.
88
+ type: object
89
+ additionalProperties: false
90
+ minProperties: 1
91
+ properties:
92
+ components:
93
+ type: array
94
+ minItems: 1
95
+ items:
96
+ type: string
97
+ pattern: ^([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?)(\.([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?))$
98
+ services:
99
+ type: array
100
+ minItems: 1
101
+ items:
102
+ type: string
103
+ minLength: 1
104
+ atomicity:
105
+ description: >
106
+ Where the replicas of a component decide together: who owns what, what they have spent
107
+ between them, and which of them holds a name while it works. Absent or unreachable,
108
+ nothing is owned and whoever asked stands down; metering and locking are refused.
109
+ type: object
110
+ additionalProperties: false
111
+ properties:
112
+ redis:
113
+ description: >
114
+ One address, or an odd number of them — independent servers, not the nodes of a
115
+ cluster. A lock is taken on a quorum of them; the registry and the meter use the
116
+ first, because each of them counts on a single key.
117
+ oneOf:
118
+ - type: string
119
+ - type: array
120
+ minItems: 1
121
+ items:
122
+ type: string
123
+ interval:
124
+ description: >
125
+ How often a replica registers, in milliseconds. It has to comfortably exceed a round
126
+ trip to Redis and the jitter around it, and nothing is owned until two consecutive
127
+ intervals have agreed — so this is also how long a replica owns nothing after it
128
+ joins, restarts or stalls.
129
+ type: integer
130
+ minimum: 100
131
+ required: [redis]
132
+ outbox:
133
+ type: object
134
+ additionalProperties: false
135
+ properties:
136
+ interval:
137
+ description: >
138
+ The pump's cycle, in milliseconds. One cycle reads what is unpublished, publishes it
139
+ and marks it. In steady state it reads nothing, so this is budgeted for how fast a rare
140
+ failure heals rather than for how fast events flow.
141
+ type: integer
142
+ minimum: 100
143
+ batch:
144
+ description: >
145
+ How many rows one read brings back. A page that comes back full is read on from,
146
+ so this bounds a read rather than a cycle.
147
+ type: integer
148
+ minimum: 1
149
+ retention:
150
+ description: >
151
+ Seconds a published row is kept as a change log before it expires. A row that never
152
+ made it out has no expiry and is never reaped.
153
+ type: integer
154
+ minimum: 0
155
+ inbox:
156
+ type: object
157
+ additionalProperties: false
158
+ properties:
159
+ retention:
160
+ description: >
161
+ Seconds a call is remembered, which is the window a duplicate of it is caught in. Budget
162
+ it for how late a duplicate can arrive — a redelivery, the attempts the broker library
163
+ makes of a message, whatever a client retries on — rather than for how long the change
164
+ matters. Every call of an operation that declares `once` is a record for this long.
165
+ type: integer
166
+ minimum: 1
167
+ addressed:
168
+ type: object
169
+ additionalProperties: false
170
+ properties:
171
+ timeout:
172
+ description: >
173
+ Milliseconds an addressed call waits for its reply where it names no timeout of its own.
174
+ Nothing else ends a call to a process that has gone, so every addressed call waits for a
175
+ set time.
176
+ type: integer
177
+ minimum: 1
178
+ events:
179
+ description: >
180
+ Events of this context that are consumed outside it. An event is otherwise published only
181
+ where something in the context receives it.
182
+ type: array
183
+ items:
184
+ type: string
185
+ pattern: ^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*$
186
+ mono:
187
+ type: object
188
+ additionalProperties: false
189
+ properties:
190
+ replicas:
191
+ type: integer
192
+ minimum: 1
193
+ resources:
194
+ $ref: '#/definitions/resources'
195
+ ingress:
196
+ type: object
197
+ additionalProperties: false
198
+ properties:
199
+ hosts:
200
+ type: array
201
+ minItems: 1
202
+ items:
203
+ type: string
204
+ class:
205
+ type: string
206
+ annotations:
207
+ type: object
208
+ default:
209
+ type: boolean
65
210
  annotations:
66
211
  type: object
67
- required: [runtime, name, packages, registry]
212
+ resources:
213
+ $ref: '#/definitions/resources'
214
+ required: [runtime, name, registry]
68
215
  additionalProperties: false
216
+
217
+ definitions:
218
+ # What a deployment may take. `null` deploys it without any, which is a decision
219
+ # rather than an omission — every deployment has to make one.
220
+ resources:
221
+ type: object
222
+ nullable: true
223
+ additionalProperties: false
224
+ properties:
225
+ cpu:
226
+ type: array
227
+ items:
228
+ type: string
229
+ minItems: 2
230
+ maxItems: 2
231
+ memory:
232
+ type: array
233
+ items:
234
+ type: string
235
+ minItems: 2
236
+ maxItems: 2
@@ -1,19 +1,13 @@
1
- 'use strict'
1
+ import { resolve } from 'node:path'
2
2
 
3
- const { resolve } = require('node:path')
3
+ import { readFileSync } from 'node:fs'
4
+ import { yaml } from '@toa.io/generic'
5
+ import * as schemas from '@toa.io/schemas'
4
6
 
5
- const { load } = require('@toa.io/yaml')
6
- const { Schema } = require('@toa.io/schema')
7
+ const path = resolve(import.meta.dirname, 'schema.yaml')
8
+ const object = yaml.load(readFileSync(path, 'utf8'))
9
+ const schema = schemas.schema(object)
7
10
 
8
- const path = resolve(__dirname, 'schema.yaml')
9
- const object = load.sync(path)
10
- const schema = new Schema(object)
11
-
12
- /**
13
- * @param {toa.norm.context.Declaration} context
14
- */
15
- const validate = (context) => {
11
+ export const validate = (context) => {
16
12
  schema.validate(context)
17
13
  }
18
-
19
- exports.validate = validate
package/src/component.js CHANGED
@@ -1,49 +1,57 @@
1
- 'use strict'
1
+ import { join } from 'node:path'
2
2
 
3
- const { join } = require('node:path')
3
+ import { readFile } from 'node:fs/promises'
4
+ import { yaml as jsyaml } from '@toa.io/generic'
5
+ import { find } from '@toa.io/generic'
6
+ import { Locator } from '@toa.io/core'
4
7
 
5
- const { load: yaml } = require('@toa.io/yaml')
6
- const { directory: { find } } = require('@toa.io/filesystem')
7
- const { Locator } = require('@toa.io/core')
8
-
9
- const {
8
+ import {
10
9
  expand,
11
10
  merge,
11
+ migrations,
12
12
  validate,
13
13
  collapse,
14
14
  dereference,
15
15
  defaults,
16
16
  normalize,
17
17
  extensions
18
- } = require('./.component')
18
+ } from './.component/index.js'
19
19
 
20
- const component = async (path) => {
20
+ export const component = async (path) => {
21
21
  const manifest = await load(path)
22
22
 
23
- normalize(manifest)
24
- validate(manifest)
25
- extensions(manifest)
23
+ await normalize(manifest, path)
24
+ await validate(manifest)
25
+ await extensions(manifest)
26
26
 
27
27
  manifest.locator = new Locator(manifest.name, manifest.namespace)
28
28
 
29
29
  return manifest
30
30
  }
31
31
 
32
- const load = async (path, base) => {
32
+ const load = async (path, base, proto = false) => {
33
33
  if (base !== undefined) path = find(path, base, MANIFEST)
34
34
 
35
35
  const file = join(path, MANIFEST)
36
- const manifest = /** @type {toa.norm.Component} */ await yaml(file) ?? {}
36
+ const manifest = (await read(file)) ?? {}
37
37
 
38
38
  manifest.path = path
39
39
 
40
- defaults(manifest)
41
- expand(manifest)
40
+ const anonymous = manifest.name === undefined
41
+
42
+ defaults(manifest, proto)
43
+ await expand(manifest)
42
44
 
43
45
  await merge(path, manifest)
46
+ await migrations(path, manifest)
47
+
48
+ // an inherited migration is recorded under the prototype's name, and a generated one is not
49
+ // the same on the next machine
50
+ if (proto && anonymous && manifest.entity?.migrations !== undefined)
51
+ throw new Error(`Prototype at '${path}' declares migrations, so it has to be named`)
44
52
 
45
53
  if (manifest.prototype !== null) {
46
- const prototype = await load(manifest.prototype, path)
54
+ const prototype = await load(manifest.prototype, path, true)
47
55
 
48
56
  collapse(manifest, prototype)
49
57
  }
@@ -56,4 +64,15 @@ const load = async (path, base) => {
56
64
 
57
65
  const MANIFEST = 'manifest.toa.yaml'
58
66
 
59
- exports.component = component
67
+ /**
68
+ * Reads a YAML file, resolving anchors into distinct objects so that
69
+ * mutating one node cannot reach another.
70
+ *
71
+ * @param {string} path
72
+ * @return {Promise<object>}
73
+ */
74
+ async function read(path) {
75
+ const object = jsyaml.load(await readFile(path, 'utf8'))
76
+
77
+ return jsyaml.load(jsyaml.dump(object, { noRefs: true, lineWidth: -1 }))
78
+ }