@toa.io/norm 1.0.0-alpha.282 → 1.0.0-alpha.284
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.
- package/CHANGELOG.md +19 -0
- package/package.json +5 -5
- package/src/.component/.expand/extensions.js +1 -0
- package/src/.component/schema.yaml +5 -1
- package/src/.context/.dependencies/extensions.js +51 -15
- package/src/.context/.dependencies/resolve.js +1 -1
- package/src/.context/dereference.js +8 -1
- package/src/.context/expand.js +6 -0
- package/src/.context/schema.yaml +11 -0
- package/src/shortcuts.js +1 -0
- package/test/context/dereference.test.js +9 -0
- package/test/context/expand.test.js +25 -0
- package/test/context/validate.test.js +23 -0
- package/types/component.d.ts +1 -0
- package/types/context/declaration.d.ts +3 -1
- package/types/context.d.ts +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [1.0.0-alpha.284](https://github.com/toa-io/toa/compare/v1.0.0-alpha.283...v1.0.0-alpha.284) (2026-09-05)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* an operation states what it is ([b5e2f66](https://github.com/toa-io/toa/commit/b5e2f66c8bf67924e2eaaaa11f283dfb4d810981))
|
|
11
|
+
* **cadence:** calls a component makes to itself, and calls it puts off ([8bbde5f](https://github.com/toa-io/toa/commit/8bbde5fc89e8e5549d5aa2bfcddb1beec0871565))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# [1.0.0-alpha.283](https://github.com/toa-io/toa/compare/v1.0.0-alpha.282...v1.0.0-alpha.283) (2026-09-04)
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* **exposition:** declare the errors the built-in components return ([27200c3](https://github.com/toa-io/toa/commit/27200c348ab09068d58c52c3e83208cb29cd7290))
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* **operations:** run extension services inside a composition ([154aced](https://github.com/toa-io/toa/commit/154aced047228a250ac3bab353e09432fcc5a34d))
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
# [1.0.0-alpha.282](https://github.com/toa-io/toa/compare/v1.0.0-alpha.281...v1.0.0-alpha.282) (2026-09-03)
|
|
7
26
|
|
|
8
27
|
**Note:** Version bump only for package @toa.io/norm
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/norm",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.284",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Toa declarations normalization and validation",
|
|
6
6
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@toa.io/core": "1.0.0-alpha.
|
|
25
|
-
"@toa.io/generic": "1.0.0-alpha.
|
|
26
|
-
"@toa.io/schemas": "1.0.0-alpha.
|
|
24
|
+
"@toa.io/core": "1.0.0-alpha.284",
|
|
25
|
+
"@toa.io/generic": "1.0.0-alpha.283",
|
|
26
|
+
"@toa.io/schemas": "1.0.0-alpha.283",
|
|
27
27
|
"fast-glob": "3.3.3",
|
|
28
28
|
"js-yaml": "5.4.1"
|
|
29
29
|
},
|
|
30
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "006755556879e91e8d68fa86753aa3b475bdf4bf"
|
|
31
31
|
}
|
|
@@ -148,15 +148,19 @@ properties:
|
|
|
148
148
|
type: string
|
|
149
149
|
bindings:
|
|
150
150
|
$ref: "#/properties/bindings"
|
|
151
|
+
description:
|
|
152
|
+
# what the operation is, for whoever reads it: a generated type, an OPTIONS, a model
|
|
153
|
+
type: string
|
|
151
154
|
input:
|
|
152
155
|
$ref: https://json-schema.org/draft/2019-09/schema
|
|
153
156
|
output:
|
|
154
157
|
$ref: https://json-schema.org/draft/2019-09/schema
|
|
155
158
|
default: {}
|
|
156
159
|
errors:
|
|
160
|
+
# what a reply may carry as a code: the error schema allows either
|
|
157
161
|
type: array
|
|
158
162
|
items:
|
|
159
|
-
type: string
|
|
163
|
+
type: [string, integer]
|
|
160
164
|
query:
|
|
161
165
|
type: boolean
|
|
162
166
|
required: [type, scope, bindings]
|
|
@@ -4,7 +4,13 @@ import { load as loadDependency } from './load.js'
|
|
|
4
4
|
export const extensions = async (context) => {
|
|
5
5
|
const extensions = {}
|
|
6
6
|
const components = context.components?.slice() ?? []
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
// an extension a composition runs may be referenced by no component of this context,
|
|
9
|
+
// and its own components must be extracted all the same
|
|
10
|
+
const declared = await extractDeclaredServices(context, extensions)
|
|
11
|
+
|
|
12
|
+
const extracted = declared.concat(
|
|
13
|
+
await extractExtensionComponents(components.concat(declared), extensions, context.annotations))
|
|
8
14
|
|
|
9
15
|
components.push(...extracted)
|
|
10
16
|
|
|
@@ -21,6 +27,24 @@ export const extensions = async (context) => {
|
|
|
21
27
|
return { extensions, components: extracted }
|
|
22
28
|
}
|
|
23
29
|
|
|
30
|
+
async function extractDeclaredServices (context, extensions) {
|
|
31
|
+
const extracted = []
|
|
32
|
+
|
|
33
|
+
for (const composition of context.compositions ?? [])
|
|
34
|
+
for (const reference of composition.services ?? []) {
|
|
35
|
+
if (reference in extensions) continue
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
extracted.push(...await extract(reference, extensions, context.annotations))
|
|
39
|
+
} catch (e) {
|
|
40
|
+
throw new Error(`Composition '${composition.name}' lists '${reference}', ` +
|
|
41
|
+
`which cannot be resolved: ${e.message}`, { cause: e })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return extracted
|
|
46
|
+
}
|
|
47
|
+
|
|
24
48
|
async function extractExtensionComponents (components, extensions, annotations) {
|
|
25
49
|
|
|
26
50
|
const extracted = []
|
|
@@ -31,20 +55,7 @@ async function extractExtensionComponents (components, extensions, annotations)
|
|
|
31
55
|
for (const reference of Object.keys(component.extensions)) {
|
|
32
56
|
if (reference in extensions) continue
|
|
33
57
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const { metadata, module: mod } = await loadDependency(reference)
|
|
37
|
-
|
|
38
|
-
if (mod.components === undefined) continue
|
|
39
|
-
|
|
40
|
-
// the annotation decides whether an extension contributes components at all
|
|
41
|
-
const annotation = annotations?.[metadata?.name ?? reference]
|
|
42
|
-
|
|
43
|
-
for (const path of mod.components(annotation).paths) {
|
|
44
|
-
const component = await load(path)
|
|
45
|
-
|
|
46
|
-
extracted.push(component)
|
|
47
|
-
}
|
|
58
|
+
extracted.push(...await extract(reference, extensions, annotations))
|
|
48
59
|
}
|
|
49
60
|
}
|
|
50
61
|
|
|
@@ -55,3 +66,28 @@ async function extractExtensionComponents (components, extensions, annotations)
|
|
|
55
66
|
|
|
56
67
|
return extracted.concat(deeper)
|
|
57
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The components an extension contributes, if it contributes any.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} reference
|
|
74
|
+
* @param {object} extensions
|
|
75
|
+
* @param {object} [annotations]
|
|
76
|
+
* @returns {Promise<Array<toa.norm.Component>>}
|
|
77
|
+
*/
|
|
78
|
+
async function extract (reference, extensions, annotations) {
|
|
79
|
+
extensions[reference] = []
|
|
80
|
+
|
|
81
|
+
const { metadata, module: mod } = await loadDependency(reference)
|
|
82
|
+
|
|
83
|
+
if (mod.components === undefined) return []
|
|
84
|
+
|
|
85
|
+
// the annotation decides whether an extension contributes components at all
|
|
86
|
+
const annotation = annotations?.[metadata?.name ?? reference]
|
|
87
|
+
const extracted = []
|
|
88
|
+
|
|
89
|
+
for (const path of mod.components(annotation).paths)
|
|
90
|
+
extracted.push(await load(path))
|
|
91
|
+
|
|
92
|
+
return extracted
|
|
93
|
+
}
|
|
@@ -8,7 +8,14 @@ export const dereference = (context) => {
|
|
|
8
8
|
|
|
9
9
|
if (context.compositions !== undefined) {
|
|
10
10
|
for (const composition of context.compositions) {
|
|
11
|
-
composition.components = composition.components.map((id) =>
|
|
11
|
+
composition.components = composition.components.map((id) => {
|
|
12
|
+
const component = components[id]
|
|
13
|
+
|
|
14
|
+
if (component === undefined)
|
|
15
|
+
throw new Error(`Composition '${composition.name}' lists an unknown component '${id}'.`)
|
|
16
|
+
|
|
17
|
+
return component
|
|
18
|
+
})
|
|
12
19
|
}
|
|
13
20
|
}
|
|
14
21
|
}
|
package/src/.context/expand.js
CHANGED
|
@@ -6,4 +6,10 @@ import * as shortcuts from '../shortcuts.js'
|
|
|
6
6
|
export const expand = (context) => {
|
|
7
7
|
shortcuts.recognize(shortcuts.SHORTCUTS, context, 'annotations')
|
|
8
8
|
shortcuts.recognize(shortcuts.SHORTCUTS, context.annotations)
|
|
9
|
+
|
|
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)
|
|
9
15
|
}
|
package/src/.context/schema.yaml
CHANGED
|
@@ -50,6 +50,7 @@ properties:
|
|
|
50
50
|
minItems: 1
|
|
51
51
|
items:
|
|
52
52
|
type: object
|
|
53
|
+
additionalProperties: false
|
|
53
54
|
properties:
|
|
54
55
|
name:
|
|
55
56
|
type: string
|
|
@@ -62,6 +63,16 @@ properties:
|
|
|
62
63
|
items:
|
|
63
64
|
type: string
|
|
64
65
|
pattern: ^([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?)(\.([a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?))$
|
|
66
|
+
services:
|
|
67
|
+
description: >
|
|
68
|
+
Extension services this composition runs in its own pod, named by shortcut or
|
|
69
|
+
package reference, rather than each as its own deployment. A service listed here
|
|
70
|
+
keeps its own Service and Ingress; what changes is the pod they select.
|
|
71
|
+
type: array
|
|
72
|
+
minItems: 1
|
|
73
|
+
items:
|
|
74
|
+
type: string
|
|
75
|
+
minLength: 1
|
|
65
76
|
resources:
|
|
66
77
|
$ref: '#/definitions/resources'
|
|
67
78
|
required: [name, components]
|
package/src/shortcuts.js
CHANGED
|
@@ -23,3 +23,12 @@ it('should not throw on empty compositions', () => {
|
|
|
23
23
|
|
|
24
24
|
assert.doesNotThrow(() => dereference(context))
|
|
25
25
|
})
|
|
26
|
+
|
|
27
|
+
it('should throw on an unknown component', () => {
|
|
28
|
+
const broken = clone(fixtures.context)
|
|
29
|
+
|
|
30
|
+
broken.compositions[1].components.push('nope.here')
|
|
31
|
+
|
|
32
|
+
assert.throws(() => dereference(broken),
|
|
33
|
+
{ message: "Composition 'bar' lists an unknown component 'nope.here'." })
|
|
34
|
+
})
|
|
@@ -40,3 +40,28 @@ describe('annotations', () => {
|
|
|
40
40
|
assert.notStrictEqual(context.annotations['@toa.io/storages.mongodb'], undefined)
|
|
41
41
|
})
|
|
42
42
|
})
|
|
43
|
+
|
|
44
|
+
describe('composition services', () => {
|
|
45
|
+
it('should resolve shortcuts', () => {
|
|
46
|
+
context.compositions[0].services = ['exposition', 'configuration']
|
|
47
|
+
|
|
48
|
+
expand(context)
|
|
49
|
+
|
|
50
|
+
assert.deepStrictEqual(context.compositions[0].services,
|
|
51
|
+
['@toa.io/extensions.exposition', '@toa.io/extensions.configuration'])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should leave a package reference alone', () => {
|
|
55
|
+
context.compositions[0].services = ['@acme/extension']
|
|
56
|
+
|
|
57
|
+
expand(context)
|
|
58
|
+
|
|
59
|
+
assert.deepStrictEqual(context.compositions[0].services, ['@acme/extension'])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should not throw without compositions', () => {
|
|
63
|
+
delete context.compositions
|
|
64
|
+
|
|
65
|
+
assert.doesNotThrow(() => expand(context))
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -83,3 +83,26 @@ it('should allow mono replicas and resources', () => {
|
|
|
83
83
|
|
|
84
84
|
assert.doesNotThrow(() => validate(context))
|
|
85
85
|
})
|
|
86
|
+
|
|
87
|
+
describe('compositions', () => {
|
|
88
|
+
it('should allow services', () => {
|
|
89
|
+
context.compositions[0].services = ['@toa.io/extensions.exposition']
|
|
90
|
+
|
|
91
|
+
assert.doesNotThrow(() => validate(context))
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should require services to be a non-empty array of strings', () => {
|
|
95
|
+
context.compositions[0].services = []
|
|
96
|
+
assert.throws(() => validate(context), (error) => /fewer than 1 items/.test(error.message))
|
|
97
|
+
|
|
98
|
+
context.compositions[0].services = 'exposition'
|
|
99
|
+
assert.throws(() => validate(context), (error) => /must be array/.test(error.message))
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should reject an unknown property', () => {
|
|
103
|
+
context.compositions[0].compoments = ['a.b']
|
|
104
|
+
|
|
105
|
+
assert.throws(() => validate(context),
|
|
106
|
+
(error) => /Property compoments is not expected to be here/.test(error.message))
|
|
107
|
+
})
|
|
108
|
+
})
|
package/types/component.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Registry, Runtime } from '../context.js'
|
|
2
2
|
|
|
3
|
+
/** A composition as declared, before its members are resolved. */
|
|
3
4
|
interface Composition {
|
|
4
5
|
name: string,
|
|
5
6
|
components: string[]
|
|
7
|
+
services?: string[]
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export interface Resources {
|
package/types/context.d.ts
CHANGED
|
@@ -20,7 +20,8 @@ interface Registry{
|
|
|
20
20
|
|
|
21
21
|
interface Composition{
|
|
22
22
|
name: string,
|
|
23
|
-
components:
|
|
23
|
+
components: Manifest[]
|
|
24
|
+
services?: string[]
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export interface Dependency<T = undefined>{
|
|
@@ -34,7 +35,7 @@ interface Context extends Declaration{
|
|
|
34
35
|
environment?: string
|
|
35
36
|
registry?: Registry
|
|
36
37
|
compositions?: Composition[]
|
|
37
|
-
components?:
|
|
38
|
+
components?: Manifest[]
|
|
38
39
|
dependencies?: Record<string, Dependency[]>
|
|
39
40
|
}
|
|
40
41
|
|