@toa.io/norm 1.0.0-alpha.282 → 1.0.0-alpha.283
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 +11 -0
- package/package.json +5 -5
- package/src/.component/schema.yaml +2 -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/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/context/declaration.d.ts +3 -1
- package/types/context.d.ts +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
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.283](https://github.com/toa-io/toa/compare/v1.0.0-alpha.282...v1.0.0-alpha.283) (2026-09-04)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **exposition:** declare the errors the built-in components return ([27200c3](https://github.com/toa-io/toa/commit/27200c348ab09068d58c52c3e83208cb29cd7290))
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* **operations:** run extension services inside a composition ([154aced](https://github.com/toa-io/toa/commit/154aced047228a250ac3bab353e09432fcc5a34d))
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
# [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
18
|
|
|
8
19
|
**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.283",
|
|
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.283",
|
|
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": "2f52b023bed0ccb2ecdb2b82fe59fbe38bf282e1"
|
|
31
31
|
}
|
|
@@ -154,9 +154,10 @@ properties:
|
|
|
154
154
|
$ref: https://json-schema.org/draft/2019-09/schema
|
|
155
155
|
default: {}
|
|
156
156
|
errors:
|
|
157
|
+
# what a reply may carry as a code: the error schema allows either
|
|
157
158
|
type: array
|
|
158
159
|
items:
|
|
159
|
-
type: string
|
|
160
|
+
type: [string, integer]
|
|
160
161
|
query:
|
|
161
162
|
type: boolean
|
|
162
163
|
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]
|
|
@@ -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
|
+
})
|
|
@@ -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
|
|