@toa.io/norm 1.0.0-alpha.31 → 1.0.0-alpha.310

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 (99) hide show
  1. package/CHANGELOG.md +208 -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 +6 -8
  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 +67 -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 +147 -73
  27. package/src/.component/validate.js +37 -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 +181 -11
  42. package/src/.context/validate.js +8 -14
  43. package/src/component.js +33 -14
  44. package/src/context.js +44 -15
  45. package/src/definition.js +56 -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 +9 -14
  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 +15 -13
  70. package/test/component/validate.test.js +180 -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 +72 -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 +158 -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/types/component.d.ts +27 -4
  93. package/types/context/declaration.d.ts +34 -3
  94. package/types/context.d.ts +27 -13
  95. package/types/entity.d.ts +7 -0
  96. package/types/index.d.ts +18 -3
  97. package/src/.component/.expand/properties.js +0 -14
  98. package/src/.context/.dependencies/load.js +0 -36
  99. 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,12 +6,10 @@ 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
- default: components/*
15
13
  build:
16
14
  type: object
17
15
  properties:
@@ -42,28 +40,200 @@ properties:
42
40
  minItems: 1
43
41
  items:
44
42
  type: string
45
- default:
46
- - linux/amd64
47
- - linux/arm/v7
48
- - linux/arm64
49
- 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]
50
50
  compositions:
51
51
  type: array
52
52
  minItems: 1
53
53
  items:
54
54
  type: object
55
+ additionalProperties: false
55
56
  properties:
56
57
  name:
57
- $ref: 'definitions#/definitions/token'
58
+ type: string
59
+ pattern: ^[a-zA-Z]([a-zA-Z0-9]{1,31})?$
58
60
  image:
59
61
  type: string
60
62
  components:
61
63
  type: array
62
64
  minItems: 1
63
65
  items:
64
- $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'
80
+ replicas:
81
+ type: integer
82
+ minimum: 1
65
83
  required: [name, components]
84
+ evicted:
85
+ description: >
86
+ What Toa does not deploy for this context, even where a composition lists it: it stays
87
+ part of the context, and is deployed by other means. A component named
88
+ here is in no pod, and what only it declared — an extension, a storage, a binding — is
89
+ not deployed either. A service named here gets no Deployment, Service or Ingress, and
90
+ no composition runs it.
91
+ type: object
92
+ additionalProperties: false
93
+ minProperties: 1
94
+ properties:
95
+ components:
96
+ type: array
97
+ minItems: 1
98
+ items:
99
+ type: string
100
+ pattern: ^([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?)(\.([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?))$
101
+ services:
102
+ type: array
103
+ minItems: 1
104
+ items:
105
+ type: string
106
+ minLength: 1
107
+ atomicity:
108
+ description: >
109
+ Where the replicas of a component decide together: who owns what, what they have spent
110
+ between them, and which of them holds a name while it works. Absent or unreachable,
111
+ nothing is owned and whoever asked stands down; metering and locking are refused.
112
+ type: object
113
+ additionalProperties: false
114
+ properties:
115
+ redis:
116
+ description: >
117
+ One address, or an odd number of them — independent servers, not the nodes of a
118
+ cluster. A lock is taken on a quorum of them; the registry and the meter use the
119
+ first, because each of them counts on a single key.
120
+ oneOf:
121
+ - type: string
122
+ - type: array
123
+ minItems: 1
124
+ items:
125
+ type: string
126
+ interval:
127
+ description: >
128
+ How often a replica registers, in milliseconds. It has to comfortably exceed a round
129
+ trip to Redis and the jitter around it, and nothing is owned until two consecutive
130
+ intervals have agreed — so this is also how long a replica owns nothing after it
131
+ joins, restarts or stalls.
132
+ type: integer
133
+ minimum: 100
134
+ required: [redis]
135
+ outbox:
136
+ type: object
137
+ additionalProperties: false
138
+ properties:
139
+ interval:
140
+ description: >
141
+ The pump's cycle, in milliseconds. One cycle reads what is unpublished, publishes it
142
+ and marks it. In steady state it reads nothing, so this is budgeted for how fast a rare
143
+ failure heals rather than for how fast events flow.
144
+ type: integer
145
+ minimum: 100
146
+ batch:
147
+ description: >
148
+ How many rows one read brings back. A page that comes back full is read on from,
149
+ so this bounds a read rather than a cycle.
150
+ type: integer
151
+ minimum: 1
152
+ retention:
153
+ description: >
154
+ Seconds a published row is kept as a change log before it expires. A row that never
155
+ made it out has no expiry and is never reaped.
156
+ type: integer
157
+ minimum: 0
158
+ inbox:
159
+ type: object
160
+ additionalProperties: false
161
+ properties:
162
+ retention:
163
+ description: >
164
+ Seconds a call is remembered, which is the window a duplicate of it is caught in. Budget
165
+ it for how late a duplicate can arrive — a redelivery, the attempts the broker library
166
+ makes of a message, whatever a client retries on — rather than for how long the change
167
+ matters. Every call of an operation that declares `once` is a record for this long.
168
+ type: integer
169
+ minimum: 1
170
+ addressed:
171
+ type: object
172
+ additionalProperties: false
173
+ properties:
174
+ timeout:
175
+ description: >
176
+ Milliseconds an addressed call waits for its reply where it names no timeout of its own.
177
+ Nothing else ends a call to a process that has gone, so every addressed call waits for a
178
+ set time.
179
+ type: integer
180
+ minimum: 1
181
+ events:
182
+ description: >
183
+ Events of this context that are consumed outside it. An event is otherwise published only
184
+ where something in the context receives it.
185
+ type: array
186
+ items:
187
+ type: string
188
+ pattern: ^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*$
189
+ mono:
190
+ type: object
191
+ additionalProperties: false
192
+ properties:
193
+ replicas:
194
+ type: integer
195
+ minimum: 1
196
+ resources:
197
+ $ref: '#/definitions/resources'
198
+ ingress:
199
+ type: object
200
+ additionalProperties: false
201
+ properties:
202
+ hosts:
203
+ type: array
204
+ minItems: 1
205
+ items:
206
+ type: string
207
+ class:
208
+ type: string
209
+ annotations:
210
+ type: object
211
+ default:
212
+ type: boolean
66
213
  annotations:
67
214
  type: object
215
+ resources:
216
+ $ref: '#/definitions/resources'
68
217
  required: [runtime, name, registry]
69
218
  additionalProperties: false
219
+
220
+ definitions:
221
+ # What a deployment may take. `null` deploys it without any, which is a decision
222
+ # rather than an omission — every deployment has to make one.
223
+ resources:
224
+ type: object
225
+ nullable: true
226
+ additionalProperties: false
227
+ properties:
228
+ cpu:
229
+ type: array
230
+ items:
231
+ type: string
232
+ minItems: 2
233
+ maxItems: 2
234
+ memory:
235
+ type: array
236
+ items:
237
+ type: string
238
+ minItems: 2
239
+ 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,28 +1,28 @@
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, path)
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
 
@@ -33,14 +33,22 @@ 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
+ const anonymous = manifest.name === undefined
41
+
40
42
  defaults(manifest, proto)
41
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
54
  const prototype = await load(manifest.prototype, path, true)
@@ -56,4 +64,15 @@ const load = async (path, base, proto = false) => {
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
+ }