@toa.io/norm 1.0.0-alpha.27 → 1.0.0-alpha.272

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.
@@ -8,7 +8,7 @@ definitions:
8
8
  binding:
9
9
  type: string
10
10
  not:
11
- const: '@toa.io/bindings.loop' # loop is for system use only
11
+ const: "@toa.io/bindings.loop" # loop is for system use only
12
12
 
13
13
  type: object
14
14
  properties:
@@ -17,19 +17,19 @@ properties:
17
17
  nullable: true
18
18
  properties:
19
19
  prototype:
20
- $ref: '#/properties/prototype'
20
+ $ref: "#/properties/prototype"
21
21
  path:
22
22
  type: string
23
23
  operations:
24
24
  type: object
25
25
  propertyNames:
26
- $ref: '#/definitions/name'
26
+ $ref: "#/definitions/name"
27
27
  patternProperties:
28
- '.*':
28
+ ".*":
29
29
  type: object
30
30
  properties:
31
31
  bridge:
32
- $ref: '#/properties/bridge'
32
+ $ref: "#/properties/bridge"
33
33
 
34
34
  path:
35
35
  type: string
@@ -38,19 +38,23 @@ properties:
38
38
  type: object
39
39
  properties:
40
40
  id:
41
- $ref: 'definitions#/definitions/locator'
41
+ type: string
42
+ pattern: ^([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?)(\.([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?))$
42
43
  label:
43
- $ref: 'definitions#/definitions/label'
44
+ type: string
45
+ pattern: ^([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?)(-([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?))*$
44
46
 
45
47
  name:
46
- $ref: 'definitions#/definitions/token'
48
+ type: string
49
+ pattern: ^[a-zA-Z]([a-zA-Z0-9]{1,31})?$
47
50
 
48
51
  namespace:
49
- $ref: 'definitions#/definitions/token'
50
- default: 'default'
52
+ type: string
53
+ pattern: ^[a-zA-Z]([a-zA-Z0-9]{1,31})?$
54
+ default: "default"
51
55
  not:
52
56
  oneOf:
53
- - const: 'system'
57
+ - const: "system"
54
58
 
55
59
  version:
56
60
  type: string
@@ -60,15 +64,17 @@ properties:
60
64
  properties:
61
65
  image:
62
66
  type: string
67
+ run:
68
+ type: string
63
69
 
64
70
  entity:
65
71
  type: object
66
72
  properties:
67
73
  storage:
68
74
  type: string
69
- default: '@toa.io/storages.mongodb'
75
+ default: "@toa.io/storages.mongodb"
70
76
  schema:
71
- $ref: 'definitions#/definitions/schema'
77
+ $ref: https://json-schema.org/draft/2019-09/schema
72
78
  type: object
73
79
  properties:
74
80
  type:
@@ -78,24 +84,24 @@ properties:
78
84
  type: object
79
85
  propertyNames:
80
86
  oneOf:
81
- - $ref: '#/definitions/name'
87
+ - $ref: "#/definitions/name"
82
88
  - enum: [_version, _created, _updated, _deleted]
83
89
  unique:
84
90
  type: object
85
91
  patternProperties:
86
- '.*':
92
+ ".*":
87
93
  type: array
88
94
  items:
89
95
  type: string
90
96
  index:
91
97
  type: object
92
98
  patternProperties:
93
- '.*':
99
+ ".*":
94
100
  type: object
95
101
  patternProperties:
96
- '.*':
102
+ ".*":
97
103
  type: string
98
- enum: [asc, desc, hash]
104
+ enum: [asc, desc, hash, text]
99
105
  associated:
100
106
  type: boolean
101
107
  default: false
@@ -109,7 +115,7 @@ properties:
109
115
  type: array
110
116
  uniqueItems: true
111
117
  items:
112
- $ref: '#/definitions/binding'
118
+ $ref: "#/definitions/binding"
113
119
 
114
120
  bridge:
115
121
  type: string
@@ -117,27 +123,40 @@ properties:
117
123
  operations:
118
124
  type: object
119
125
  propertyNames:
120
- $ref: '#/definitions/name'
126
+ $ref: "#/definitions/name"
121
127
  patternProperties:
122
- '.*':
128
+ ".*":
123
129
  type: object
124
130
  properties:
125
131
  type:
126
- enum: [transition, observation, assignment, computation, effect]
132
+ enum:
133
+ [
134
+ transition,
135
+ observation,
136
+ assignment,
137
+ computation,
138
+ effect,
139
+ unmanaged,
140
+ ]
127
141
  scope:
128
- enum: [object, objects, changeset, none]
142
+ enum: [object, objects, changeset, stream, none]
129
143
  concurrency:
130
144
  enum: [none, retry]
131
145
  forward:
132
- $ref: '#/definitions/name'
146
+ $ref: "#/definitions/name"
133
147
  bridge:
134
148
  type: string
135
149
  bindings:
136
- $ref: '#/properties/bindings'
150
+ $ref: "#/properties/bindings"
137
151
  input:
138
- $ref: 'definitions#/definitions/schema'
152
+ $ref: https://json-schema.org/draft/2019-09/schema
139
153
  output:
140
- $ref: 'definitions#/definitions/schema'
154
+ $ref: https://json-schema.org/draft/2019-09/schema
155
+ default: {}
156
+ errors:
157
+ type: array
158
+ items:
159
+ type: string
141
160
  query:
142
161
  type: boolean
143
162
  required: [type, scope, bindings]
@@ -149,8 +168,8 @@ properties:
149
168
  then:
150
169
  properties:
151
170
  scope:
152
- enum: [object]
153
- if: # transition query: false
171
+ enum: [object, objects]
172
+ if: # not query: false
154
173
  not:
155
174
  properties:
156
175
  query:
@@ -158,6 +177,17 @@ properties:
158
177
  required: [query]
159
178
  then:
160
179
  required: [concurrency]
180
+ - if: # transition and scope=objects
181
+ properties:
182
+ type:
183
+ const: transition
184
+ scope:
185
+ const: objects
186
+ then:
187
+ properties:
188
+ query:
189
+ not:
190
+ const: false
161
191
  - if: # not transition
162
192
  not:
163
193
  properties:
@@ -174,7 +204,7 @@ properties:
174
204
  then:
175
205
  properties:
176
206
  scope:
177
- enum: [object, objects, none]
207
+ enum: [object, objects, stream, none]
178
208
  query:
179
209
  not:
180
210
  const: false
@@ -189,7 +219,7 @@ properties:
189
219
  - if: # computation
190
220
  properties:
191
221
  type:
192
- const: computation
222
+ enum: [computation, unmanaged]
193
223
  then:
194
224
  properties:
195
225
  scope:
@@ -205,20 +235,17 @@ properties:
205
235
  then:
206
236
  properties:
207
237
  scope:
208
- const: none
209
238
  default: none
210
- query:
211
- const: false
212
- default: false
239
+ enum: [object, objects, stream, none]
213
240
  additionalProperties: false
214
241
  additionalProperties: false
215
242
 
216
243
  events:
217
244
  type: object
218
245
  propertyNames:
219
- $ref: 'definitions#/definitions/name'
246
+ $ref: "#/definitions/name"
220
247
  patternProperties:
221
- '.*':
248
+ ".*":
222
249
  type: object
223
250
  properties:
224
251
  bridge:
@@ -226,7 +253,7 @@ properties:
226
253
  path:
227
254
  type: string
228
255
  binding:
229
- $ref: '#/definitions/binding'
256
+ $ref: "#/definitions/binding"
230
257
  conditioned:
231
258
  type: boolean
232
259
  default: false
@@ -239,17 +266,17 @@ properties:
239
266
  receivers:
240
267
  type: object
241
268
  patternProperties:
242
- '.*':
269
+ ".*":
243
270
  type: object
244
271
  properties:
245
272
  operation:
246
- $ref: 'definitions#/definitions/name'
273
+ $ref: "#/definitions/name"
247
274
  bridge:
248
275
  type: string
249
276
  binding:
250
277
  type: string
251
278
  source:
252
- $ref: 'definitions#/definitions/name'
279
+ $ref: "#/definitions/name"
253
280
  not:
254
281
  const: context
255
282
  path:
@@ -260,9 +287,22 @@ properties:
260
287
  adaptive:
261
288
  type: boolean
262
289
  default: false
290
+ arguments:
291
+ type: array
263
292
  required: [operation]
264
293
  additionalProperties: false
265
294
 
295
+ guards:
296
+ type: object
297
+ patternProperties:
298
+ ".*":
299
+ type: object
300
+ properties:
301
+ bridge: { type: string }
302
+ path: { type: string }
303
+ required: [bridge, path]
304
+ additionalProperties: false
305
+
266
306
  extensions:
267
307
  type: object
268
308
 
@@ -3,15 +3,15 @@
3
3
  const path = require('node:path')
4
4
 
5
5
  const { load } = require('@toa.io/yaml')
6
- const { Schema } = require('@toa.io/schema')
6
+ const schemas = require('@toa.io/schemas')
7
7
 
8
8
  const object = load.sync(path.resolve(__dirname, 'schema.yaml'))
9
- const schema = new Schema(object)
9
+ const schema = schemas.schema(object)
10
10
 
11
11
  const validate = (manifest) => {
12
12
  const error = schema.fit(manifest)
13
13
 
14
- if (error) throw new Error(error.message)
14
+ if (error) throw error
15
15
 
16
16
  if (manifest.events !== undefined) events(manifest)
17
17
  if (manifest.receivers !== undefined) receivers(manifest)
@@ -32,11 +32,11 @@ const receivers = (manifest) => {
32
32
  }
33
33
 
34
34
  if (!TYPES.has(manifest.operations[receiver.operation].type)) {
35
- throw new Error(`Receiver '${locator}' must refer to an operation of one of the allowed types: ${Array.from(TYPES).join(', ')}`)
35
+ throw new Error(`Receiver '${locator}' must refer to an operation of the allowed types: ${Array.from(TYPES).join(', ')}`)
36
36
  }
37
37
  }
38
38
  }
39
39
 
40
- const TYPES = new Set(['transition', 'effect'])
40
+ const TYPES = new Set(['transition', 'assignment', 'effect', 'unmanaged'])
41
41
 
42
42
  exports.validate = validate
@@ -3,7 +3,7 @@
3
3
  const extensions = async (context) => {
4
4
  const extensions = {}
5
5
  const components = context.components?.slice() ?? []
6
- const extracted = await extractExtensionComponents(components, extensions)
6
+ const extracted = await extractExtensionComponents(components, extensions, context.annotations)
7
7
 
8
8
  components.push(...extracted)
9
9
 
@@ -20,8 +20,9 @@ const extensions = async (context) => {
20
20
  return { extensions, components: extracted }
21
21
  }
22
22
 
23
- async function extractExtensionComponents (components, extensions) {
23
+ async function extractExtensionComponents (components, extensions, annotations) {
24
24
  const { component: load } = require('../../component')
25
+ const { load: loadDependency } = require('./load')
25
26
 
26
27
  const extracted = []
27
28
 
@@ -33,11 +34,14 @@ async function extractExtensionComponents (components, extensions) {
33
34
 
34
35
  extensions[reference] = []
35
36
 
36
- const mod = require(reference)
37
+ const { metadata, module: mod } = loadDependency(reference)
37
38
 
38
39
  if (mod.components === undefined) continue
39
40
 
40
- for (const path of mod.components().paths) {
41
+ // the annotation decides whether an extension contributes components at all
42
+ const annotation = annotations?.[metadata?.name ?? reference]
43
+
44
+ for (const path of mod.components(annotation).paths) {
41
45
  const component = await load(path)
42
46
 
43
47
  extracted.push(component)
@@ -48,7 +52,7 @@ async function extractExtensionComponents (components, extensions) {
48
52
  if (extracted.length === 0)
49
53
  return extracted
50
54
 
51
- const deeper = await extractExtensionComponents(extracted, extensions)
55
+ const deeper = await extractExtensionComponents(extracted, extensions, annotations)
52
56
 
53
57
  return extracted.concat(deeper)
54
58
  }
@@ -24,7 +24,28 @@ const resolve = (references, annotations) => {
24
24
  }
25
25
  }
26
26
 
27
+ for (const dependency of Object.keys(annotations)) {
28
+ if (dependency in dependencies) continue
29
+
30
+ // an annotation may be keyed by a dependency id rather than by a module reference
31
+ const module = optional(dependency)
32
+
33
+ if (module?.standalone === true) dependencies[dependency] = []
34
+ }
35
+
27
36
  return dependencies
28
37
  }
29
38
 
39
+ /**
40
+ * @param {string} reference
41
+ * @returns {object | null}
42
+ */
43
+ function optional (reference) {
44
+ try {
45
+ return load(reference).module
46
+ } catch {
47
+ return null
48
+ }
49
+ }
50
+
30
51
  exports.resolve = resolve
@@ -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:
@@ -46,7 +44,7 @@ properties:
46
44
  - linux/amd64
47
45
  - linux/arm/v7
48
46
  - linux/arm64
49
- required: [base, platforms]
47
+ # required: [base]
50
48
  compositions:
51
49
  type: array
52
50
  minItems: 1
@@ -54,16 +52,126 @@ properties:
54
52
  type: object
55
53
  properties:
56
54
  name:
57
- $ref: 'definitions#/definitions/token'
55
+ type: string
56
+ pattern: ^[a-zA-Z]([a-zA-Z0-9]{1,31})?$
58
57
  image:
59
58
  type: string
60
59
  components:
61
60
  type: array
62
61
  minItems: 1
63
62
  items:
64
- $ref: 'definitions#/definitions/locator'
63
+ type: string
64
+ pattern: ^([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?)(\.([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?))$
65
+ resources:
66
+ $ref: '#/definitions/resources'
65
67
  required: [name, components]
68
+ atomicity:
69
+ description: >
70
+ Where the replicas of a component decide together: who owns what, what they have spent
71
+ between them, and which of them holds a name while it works. Absent or unreachable,
72
+ nothing is owned and whoever asked stands down; metering and locking are refused.
73
+ type: object
74
+ additionalProperties: false
75
+ properties:
76
+ redis:
77
+ description: >
78
+ One address, or an odd number of them — independent servers, not the nodes of a
79
+ cluster. A lock is taken on a quorum of them; the registry and the meter use the
80
+ first, because each of them counts on a single key.
81
+ oneOf:
82
+ - type: string
83
+ - type: array
84
+ minItems: 1
85
+ items:
86
+ type: string
87
+ interval:
88
+ description: >
89
+ How often a replica registers, in milliseconds. It has to comfortably exceed a round
90
+ trip to Redis and the jitter around it, and nothing is owned until two consecutive
91
+ intervals have agreed — so this is also how long a replica owns nothing after it
92
+ joins, restarts or stalls.
93
+ type: integer
94
+ minimum: 100
95
+ required: [redis]
96
+ outbox:
97
+ type: object
98
+ additionalProperties: false
99
+ properties:
100
+ interval:
101
+ description: >
102
+ The pump's cycle, in milliseconds. One cycle reads what is unpublished, publishes it
103
+ and marks it. In steady state it reads nothing, so this is budgeted for how fast a rare
104
+ failure heals rather than for how fast events flow.
105
+ type: integer
106
+ minimum: 100
107
+ batch:
108
+ description: >
109
+ How many rows one read brings back. A page that comes back full is read on from,
110
+ so this bounds a read rather than a cycle.
111
+ type: integer
112
+ minimum: 1
113
+ retention:
114
+ description: >
115
+ Seconds a published row is kept as a change log before it expires. A row that never
116
+ made it out has no expiry and is never reaped.
117
+ type: integer
118
+ minimum: 0
119
+ events:
120
+ description: >
121
+ Events of this context that are consumed outside it. An event is otherwise published only
122
+ where something in the context receives it.
123
+ type: array
124
+ items:
125
+ type: string
126
+ pattern: ^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*$
127
+ mono:
128
+ type: object
129
+ additionalProperties: false
130
+ properties:
131
+ replicas:
132
+ type: integer
133
+ minimum: 1
134
+ resources:
135
+ $ref: '#/definitions/resources'
136
+ ingress:
137
+ type: object
138
+ additionalProperties: false
139
+ properties:
140
+ hosts:
141
+ type: array
142
+ minItems: 1
143
+ items:
144
+ type: string
145
+ class:
146
+ type: string
147
+ annotations:
148
+ type: object
149
+ default:
150
+ type: boolean
66
151
  annotations:
67
152
  type: object
153
+ resources:
154
+ $ref: '#/definitions/resources'
68
155
  required: [runtime, name, registry]
69
156
  additionalProperties: false
157
+
158
+ definitions:
159
+ # What a deployment may take. `null` deploys it without any, which is a decision
160
+ # rather than an omission — every deployment has to make one.
161
+ resources:
162
+ type: object
163
+ nullable: true
164
+ additionalProperties: false
165
+ properties:
166
+ cpu:
167
+ type: array
168
+ items:
169
+ type: string
170
+ minItems: 2
171
+ maxItems: 2
172
+ memory:
173
+ type: array
174
+ items:
175
+ type: string
176
+ minItems: 2
177
+ maxItems: 2
@@ -3,15 +3,12 @@
3
3
  const { resolve } = require('node:path')
4
4
 
5
5
  const { load } = require('@toa.io/yaml')
6
- const { Schema } = require('@toa.io/schema')
6
+ const schemas = require('@toa.io/schemas')
7
7
 
8
8
  const path = resolve(__dirname, 'schema.yaml')
9
9
  const object = load.sync(path)
10
- const schema = new Schema(object)
10
+ const schema = schemas.schema(object)
11
11
 
12
- /**
13
- * @param {toa.norm.context.Declaration} context
14
- */
15
12
  const validate = (context) => {
16
13
  schema.validate(context)
17
14
  }
package/src/component.js CHANGED
@@ -20,7 +20,7 @@ const {
20
20
  const component = async (path) => {
21
21
  const manifest = await load(path)
22
22
 
23
- normalize(manifest)
23
+ normalize(manifest, path)
24
24
  validate(manifest)
25
25
  extensions(manifest)
26
26
 
@@ -29,21 +29,21 @@ const component = async (path) => {
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 yaml(file) ?? {}
37
37
 
38
38
  manifest.path = path
39
39
 
40
- defaults(manifest)
40
+ defaults(manifest, proto)
41
41
  await expand(manifest)
42
42
 
43
43
  await merge(path, manifest)
44
44
 
45
45
  if (manifest.prototype !== null) {
46
- const prototype = await load(manifest.prototype, path)
46
+ const prototype = await load(manifest.prototype, path, true)
47
47
 
48
48
  collapse(manifest, prototype)
49
49
  }
package/src/context.js CHANGED
@@ -19,7 +19,6 @@ const {
19
19
  const context = async (root, environment = process.env.TOA_ENV) => {
20
20
  const path = resolve(root, CONTEXT)
21
21
  const context = /** @type {toa.norm.Context} */ await load(path)
22
- const pattern = resolve(root, context.packages)
23
22
 
24
23
  context.environment = environment
25
24
 
@@ -29,7 +28,7 @@ const context = async (root, environment = process.env.TOA_ENV) => {
29
28
 
30
29
  validate(context)
31
30
 
32
- const paths = await glob(pattern)
31
+ const paths = await glob(resolve(root, COMPONENTS))
33
32
 
34
33
  context.components = await Promise.all(paths.map(component))
35
34
  context.dependencies = await dependencies(context)
@@ -41,5 +40,6 @@ const context = async (root, environment = process.env.TOA_ENV) => {
41
40
  }
42
41
 
43
42
  const CONTEXT = 'context.toa.yaml'
43
+ const COMPONENTS = 'components/*'
44
44
 
45
45
  exports.context = context
package/src/shortcuts.js CHANGED
@@ -48,9 +48,10 @@ const SHORTCUTS = {
48
48
  exposition: '@toa.io/extensions.exposition',
49
49
  realtime: '@toa.io/extensions.realtime',
50
50
  configuration: '@toa.io/extensions.configuration',
51
- origins: '@toa.io/extensions.origins',
52
51
  stash: '@toa.io/extensions.stash',
53
- storages: '@toa.io/extensions.storages'
52
+ storages: '@toa.io/extensions.storages',
53
+ telemetry: '@toa.io/extensions.telemetry',
54
+ introspection: '@toa.io/extensions.introspection'
54
55
  }
55
56
 
56
57
  exports.recognize = recognize
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const source = {
4
- path: '',
4
+ path: __dirname, // `version` hashes the component directory
5
5
  entity: {
6
6
  schema: {
7
7
  properties: {
@@ -11,17 +11,31 @@ beforeEach(() => {
11
11
  source = clone(fixtures.source)
12
12
  })
13
13
 
14
- it('should expand', () => {
15
- expand(source)
14
+ it('should expand', async () => {
15
+ await expand(source)
16
16
  expect(source).toMatchObject(fixtures.target)
17
17
  })
18
18
 
19
+ it('should derive version from the component contents', async () => {
20
+ await expand(source)
21
+
22
+ expect(source.version).toMatch(/^[0-9a-f]{8}$/)
23
+ })
24
+
25
+ it('should keep declared version', async () => {
26
+ source.version = '1.0.0'
27
+
28
+ await expand(source)
29
+
30
+ expect(source.version).toStrictEqual('1.0.0')
31
+ })
32
+
19
33
  it('should recognize storages.queues', async () => {
20
34
  const queues = { foo: 'bar' }
21
35
 
22
36
  source.queues = clone(queues)
23
37
 
24
- expand(source)
38
+ await expand(source)
25
39
 
26
40
  expect(source.queues).toBeUndefined()
27
41
  expect(source.properties['@toa.io/storages.queues']).toMatchObject(queues)