@toa.io/norm 1.0.0-alpha.284 → 1.0.0-alpha.287
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 +1 -196
- package/package.json +5 -5
- package/src/.component/.expand/bridge.js +1 -1
- package/src/.component/.expand/entity.js +1 -1
- package/src/.component/.expand/events.js +1 -1
- package/src/.component/.expand/extensions.js +1 -1
- package/src/.component/.expand/operations.js +1 -1
- package/src/.component/.expand/properties.js +1 -1
- package/src/.component/.expand/receivers.js +3 -2
- package/src/.component/.expand/version.js +11 -13
- package/src/.component/.normalize/entity.js +7 -0
- package/src/.component/.normalize/events.js +4 -1
- package/src/.component/.normalize/index.js +1 -0
- package/src/.component/.normalize/operations.js +9 -1
- package/src/.component/.normalize/receivers.js +4 -1
- package/src/.component/collapse.js +30 -6
- package/src/.component/defaults.js +3 -4
- package/src/.component/dependencies.js +7 -4
- package/src/.component/dereference.js +14 -13
- package/src/.component/expand.js +11 -2
- package/src/.component/extensions.js +2 -1
- package/src/.component/index.js +1 -0
- package/src/.component/merge.js +3 -5
- package/src/.component/migrations.js +57 -0
- package/src/.component/normalize.js +9 -1
- package/src/.component/schema.yaml +29 -39
- package/src/.component/validate.js +23 -3
- package/src/.context/.dependencies/connectors.js +3 -3
- package/src/.context/.dependencies/describe.js +4 -1
- package/src/.context/.dependencies/extensions.js +18 -13
- package/src/.context/.dependencies/load.js +6 -4
- package/src/.context/.dependencies/resolve.js +1 -1
- package/src/.context/dereference.js +3 -1
- package/src/.context/normalize.js +5 -0
- package/src/.context/schema.yaml +6 -4
- package/src/component.js +14 -5
- package/src/context.js +9 -5
- package/src/entity.js +15 -0
- package/src/index.js +1 -0
- package/src/shortcuts.js +0 -1
- package/test/component/collapse.fixtures.js +28 -34
- package/test/component/collapse.test.js +41 -0
- package/test/component/dependencies.test.js +6 -6
- package/test/component/dereference.fixtures.js +13 -17
- package/test/component/dereference.test.js +12 -3
- package/test/component/dummies/migrations/conflicting/migrations/0001-one.yaml +2 -0
- package/test/component/dummies/migrations/conflicting/migrations/0001-one.yml +2 -0
- package/test/component/dummies/migrations/malformed/migrations/0001-one.yaml +1 -0
- package/test/component/dummies/migrations/ordered/migrations/0001-first.yml +4 -0
- package/test/component/dummies/migrations/ordered/migrations/0002-second.yaml +4 -0
- package/test/component/dummies/migrations/ordered/migrations/0010-third.json +1 -0
- package/test/component/dummies/migrations/stateless/migrations/0001-one.yaml +2 -0
- package/test/component/migrations.test.js +62 -0
- package/test/component/normalize.test.js +61 -13
- package/test/component/validate.fixtures.js +14 -8
- package/test/component/validate.test.js +113 -59
- package/test/context/complete.test.js +5 -1
- package/test/context/dereference.test.js +3 -2
- package/test/context/expand.test.js +4 -2
- package/test/context/normalize.test.js +14 -1
- package/test/context/validate.test.js +40 -20
- package/test/shortcuts.test.js +2 -1
- package/types/component.d.ts +14 -3
- package/types/context/declaration.d.ts +1 -1
- package/types/context.d.ts +10 -9
- package/types/entity.d.ts +7 -0
- package/types/index.d.ts +1 -0
package/src/.component/merge.js
CHANGED
|
@@ -28,8 +28,7 @@ const define = async (root, manifest, property) => {
|
|
|
28
28
|
// default bridge
|
|
29
29
|
const definition = await scan(manifest.bridge, root, property)
|
|
30
30
|
|
|
31
|
-
if (definition === undefined)
|
|
32
|
-
return
|
|
31
|
+
if (definition === undefined) return
|
|
33
32
|
|
|
34
33
|
const items = Object.entries(definition)
|
|
35
34
|
|
|
@@ -57,7 +56,7 @@ const define = async (root, manifest, property) => {
|
|
|
57
56
|
const cache = {}
|
|
58
57
|
|
|
59
58
|
// the promise is what is remembered, so a bridge is loaded once
|
|
60
|
-
function req
|
|
59
|
+
function req(mod) {
|
|
61
60
|
cache[mod] ??= import(mod)
|
|
62
61
|
|
|
63
62
|
return cache[mod]
|
|
@@ -66,8 +65,7 @@ function req (mod) {
|
|
|
66
65
|
const scan = async (bridge, root, property) => {
|
|
67
66
|
const { define } = await req(bridge)
|
|
68
67
|
|
|
69
|
-
if (property in define)
|
|
70
|
-
return define[property](root)
|
|
68
|
+
if (property in define) return define[property](root)
|
|
71
69
|
else return undefined
|
|
72
70
|
}
|
|
73
71
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { basename, extname, resolve } from 'node:path'
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
|
|
4
|
+
import glob from 'fast-glob'
|
|
5
|
+
import { yaml } from '@toa.io/generic'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The migrations a component declares, in the order they are applied. The file name without its
|
|
9
|
+
* extension is the migration's id, and sorting those ids is what orders them — so a name is
|
|
10
|
+
* written to sort: `0001-indexes`, then `0002-default-role`.
|
|
11
|
+
*
|
|
12
|
+
* What a file contains is the storage's business; this reads it and hands it over.
|
|
13
|
+
*/
|
|
14
|
+
export const migrations = async (root, manifest) => {
|
|
15
|
+
const paths = await glob(resolve(root, DIRECTORY, '*' + EXTENSIONS), GLOB)
|
|
16
|
+
|
|
17
|
+
if (paths.length === 0) return
|
|
18
|
+
|
|
19
|
+
if (manifest.entity === undefined)
|
|
20
|
+
throw new Error(`Component at '${root}' declares migrations but stores nothing`)
|
|
21
|
+
|
|
22
|
+
const ids = new Map()
|
|
23
|
+
|
|
24
|
+
// sorted, so the two files a conflict names are the same two on every machine
|
|
25
|
+
for (const path of paths.sort()) {
|
|
26
|
+
const id = basename(path, extname(path))
|
|
27
|
+
const found = ids.get(id)
|
|
28
|
+
|
|
29
|
+
if (found !== undefined)
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Component at '${root}' has more than one ${DIRECTORY}/${id}: ` +
|
|
32
|
+
`${basename(found)} and ${basename(path)}`
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
ids.set(id, path)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
manifest.entity.migrations = await Promise.all(
|
|
39
|
+
Array.from(ids, async ([id, path]) => ({ id, steps: await read(path) }))
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A migration is a list of steps. A file that is not one is refused here rather than at the
|
|
45
|
+
* storage, where the component it belongs to is no longer known.
|
|
46
|
+
*/
|
|
47
|
+
async function read(path) {
|
|
48
|
+
const steps = yaml.load(await readFile(path, 'utf8'))
|
|
49
|
+
|
|
50
|
+
if (!Array.isArray(steps)) throw new Error(`Migration '${path}' is not a list of steps`)
|
|
51
|
+
|
|
52
|
+
return steps
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DIRECTORY = 'migrations'
|
|
56
|
+
const EXTENSIONS = '.{yaml,yml,json}'
|
|
57
|
+
const GLOB = { onlyFiles: true, absolute: true }
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { events, operations, receivers } from './.normalize/index.js'
|
|
1
|
+
import { entity, events, operations, receivers } from './.normalize/index.js'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* What a manifest does not say but the runtime needs said. The schema states what a value may
|
|
5
|
+
* be; what it is where nothing was written is decided here, so that validating a manifest is
|
|
6
|
+
* reading it rather than completing it.
|
|
7
|
+
*/
|
|
3
8
|
export const normalize = async (component) => {
|
|
9
|
+
component.namespace ??= 'default'
|
|
10
|
+
|
|
11
|
+
entity(component)
|
|
4
12
|
operations(component)
|
|
5
13
|
await events(component)
|
|
6
14
|
receivers(component)
|
|
@@ -51,7 +51,6 @@ properties:
|
|
|
51
51
|
namespace:
|
|
52
52
|
type: string
|
|
53
53
|
pattern: ^[a-zA-Z]([a-zA-Z0-9]{1,31})?$
|
|
54
|
-
default: "default"
|
|
55
54
|
not:
|
|
56
55
|
oneOf:
|
|
57
56
|
- const: "system"
|
|
@@ -72,43 +71,42 @@ properties:
|
|
|
72
71
|
properties:
|
|
73
72
|
storage:
|
|
74
73
|
type: string
|
|
75
|
-
|
|
76
|
-
schema:
|
|
77
|
-
$ref: https://json-schema.org/draft/2019-09/schema
|
|
74
|
+
properties:
|
|
78
75
|
type: object
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
76
|
+
# a system property is named like any other, so nothing is enumerated beside them
|
|
77
|
+
propertyNames:
|
|
78
|
+
$ref: "#/definitions/name"
|
|
79
|
+
additionalProperties:
|
|
80
|
+
$ref: https://json-schema.org/draft/2019-09/schema
|
|
81
|
+
required:
|
|
82
|
+
type: array
|
|
83
|
+
uniqueItems: true
|
|
84
|
+
items:
|
|
85
|
+
$ref: "#/definitions/name"
|
|
86
|
+
blank:
|
|
87
|
+
# what a record holds before anything is written to it; a system property is the
|
|
88
|
+
# runtime's to write, so none may be named here
|
|
90
89
|
type: object
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
propertyNames:
|
|
91
|
+
not:
|
|
92
|
+
enum: [id, VERSION, CREATED, UPDATED, DELETED]
|
|
93
|
+
migrations:
|
|
94
|
+
type: array
|
|
95
|
+
items:
|
|
96
|
+
type: object
|
|
97
|
+
properties:
|
|
98
|
+
id:
|
|
95
99
|
type: string
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
patternProperties:
|
|
102
|
-
".*":
|
|
103
|
-
type: string
|
|
104
|
-
enum: [asc, desc, hash, text]
|
|
100
|
+
# what a step means belongs to the storage, which is the only thing that reads it
|
|
101
|
+
steps:
|
|
102
|
+
type: array
|
|
103
|
+
required: [id, steps]
|
|
104
|
+
additionalProperties: false
|
|
105
105
|
associated:
|
|
106
106
|
type: boolean
|
|
107
|
-
default: false
|
|
108
107
|
custom:
|
|
109
108
|
type: boolean
|
|
110
|
-
|
|
111
|
-
required: [schema]
|
|
109
|
+
required: [properties]
|
|
112
110
|
additionalProperties: false
|
|
113
111
|
|
|
114
112
|
bindings:
|
|
@@ -155,7 +153,6 @@ properties:
|
|
|
155
153
|
$ref: https://json-schema.org/draft/2019-09/schema
|
|
156
154
|
output:
|
|
157
155
|
$ref: https://json-schema.org/draft/2019-09/schema
|
|
158
|
-
default: {}
|
|
159
156
|
errors:
|
|
160
157
|
# what a reply may carry as a code: the error schema allows either
|
|
161
158
|
type: array
|
|
@@ -228,10 +225,8 @@ properties:
|
|
|
228
225
|
properties:
|
|
229
226
|
scope:
|
|
230
227
|
const: none
|
|
231
|
-
default: none
|
|
232
228
|
query:
|
|
233
229
|
const: false
|
|
234
|
-
default: false
|
|
235
230
|
- if: # effect
|
|
236
231
|
properties:
|
|
237
232
|
type:
|
|
@@ -239,7 +234,6 @@ properties:
|
|
|
239
234
|
then:
|
|
240
235
|
properties:
|
|
241
236
|
scope:
|
|
242
|
-
default: none
|
|
243
237
|
enum: [object, objects, stream, none]
|
|
244
238
|
additionalProperties: false
|
|
245
239
|
additionalProperties: false
|
|
@@ -260,10 +254,8 @@ properties:
|
|
|
260
254
|
$ref: "#/definitions/binding"
|
|
261
255
|
conditioned:
|
|
262
256
|
type: boolean
|
|
263
|
-
default: false
|
|
264
257
|
subjective:
|
|
265
258
|
type: boolean
|
|
266
|
-
default: false
|
|
267
259
|
required: [bridge, path]
|
|
268
260
|
additionalProperties: false
|
|
269
261
|
|
|
@@ -287,10 +279,8 @@ properties:
|
|
|
287
279
|
type: string
|
|
288
280
|
conditioned:
|
|
289
281
|
type: boolean
|
|
290
|
-
default: false
|
|
291
282
|
adaptive:
|
|
292
283
|
type: boolean
|
|
293
|
-
default: false
|
|
294
284
|
arguments:
|
|
295
285
|
type: array
|
|
296
286
|
required: [operation]
|
|
@@ -4,7 +4,9 @@ import { readFileSync } from 'node:fs'
|
|
|
4
4
|
import { yaml } from '@toa.io/generic'
|
|
5
5
|
import * as schemas from '@toa.io/schemas'
|
|
6
6
|
|
|
7
|
-
const object = yaml.load(
|
|
7
|
+
const object = yaml.load(
|
|
8
|
+
readFileSync(path.resolve(import.meta.dirname, 'schema.yaml'), 'utf8')
|
|
9
|
+
)
|
|
8
10
|
const schema = schemas.schema(object)
|
|
9
11
|
|
|
10
12
|
export const validate = async (manifest) => {
|
|
@@ -12,10 +14,24 @@ export const validate = async (manifest) => {
|
|
|
12
14
|
|
|
13
15
|
if (error) throw error
|
|
14
16
|
|
|
17
|
+
if (manifest.entity !== undefined) entity(manifest)
|
|
15
18
|
if (manifest.events !== undefined) await events(manifest)
|
|
16
19
|
if (manifest.receivers !== undefined) receivers(manifest)
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
/** What only the entity's own declaration can answer: whether a name it uses is one it declares. */
|
|
23
|
+
const entity = (manifest) => {
|
|
24
|
+
const { properties, required, blank } = manifest.entity
|
|
25
|
+
|
|
26
|
+
for (const name of required ?? [])
|
|
27
|
+
if (properties[name] === undefined)
|
|
28
|
+
throw new Error(`Entity requires property '${name}', which is not defined`)
|
|
29
|
+
|
|
30
|
+
for (const name of Object.keys(blank ?? {}))
|
|
31
|
+
if (properties[name] === undefined)
|
|
32
|
+
throw new Error(`Entity blank names property '${name}', which is not defined`)
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
const events = async (manifest) => {
|
|
20
36
|
for (const [label, event] of Object.entries(manifest.events)) {
|
|
21
37
|
const { properties } = await import(event.binding)
|
|
@@ -29,11 +45,15 @@ const events = async (manifest) => {
|
|
|
29
45
|
const receivers = (manifest) => {
|
|
30
46
|
for (const [locator, receiver] of Object.entries(manifest.receivers)) {
|
|
31
47
|
if (manifest.operations?.[receiver.operation] === undefined) {
|
|
32
|
-
throw new Error(
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Receiver '${locator}' refers to undefined operation '${receiver.operation}'`
|
|
50
|
+
)
|
|
33
51
|
}
|
|
34
52
|
|
|
35
53
|
if (!TYPES.has(manifest.operations[receiver.operation].type)) {
|
|
36
|
-
throw new Error(
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Receiver '${locator}' must refer to an operation of the allowed types: ${Array.from(TYPES).join(', ')}`
|
|
56
|
+
)
|
|
37
57
|
}
|
|
38
58
|
}
|
|
39
59
|
}
|
|
@@ -3,10 +3,10 @@ import { resolve } from 'node:path'
|
|
|
3
3
|
export const connectors = (context, extracted) => {
|
|
4
4
|
const connectors = {}
|
|
5
5
|
|
|
6
|
-
const components =
|
|
6
|
+
const components =
|
|
7
|
+
(context.components === undefined
|
|
7
8
|
? extracted
|
|
8
|
-
: context.components.concat(extracted)
|
|
9
|
-
) ?? []
|
|
9
|
+
: context.components.concat(extracted)) ?? []
|
|
10
10
|
|
|
11
11
|
for (const component of components) {
|
|
12
12
|
if (component.entity !== undefined) {
|
|
@@ -3,4 +3,7 @@
|
|
|
3
3
|
* @param {Object} manifest
|
|
4
4
|
* @returns {toa.norm.context.dependencies.Instance}
|
|
5
5
|
*/
|
|
6
|
-
export const describe = (component, manifest = undefined) => ({
|
|
6
|
+
export const describe = (component, manifest = undefined) => ({
|
|
7
|
+
locator: component.locator,
|
|
8
|
+
manifest
|
|
9
|
+
})
|
|
@@ -10,7 +10,12 @@ export const extensions = async (context) => {
|
|
|
10
10
|
const declared = await extractDeclaredServices(context, extensions)
|
|
11
11
|
|
|
12
12
|
const extracted = declared.concat(
|
|
13
|
-
await extractExtensionComponents(
|
|
13
|
+
await extractExtensionComponents(
|
|
14
|
+
components.concat(declared),
|
|
15
|
+
extensions,
|
|
16
|
+
context.annotations
|
|
17
|
+
)
|
|
18
|
+
)
|
|
14
19
|
|
|
15
20
|
components.push(...extracted)
|
|
16
21
|
|
|
@@ -27,7 +32,7 @@ export const extensions = async (context) => {
|
|
|
27
32
|
return { extensions, components: extracted }
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
async function extractDeclaredServices
|
|
35
|
+
async function extractDeclaredServices(context, extensions) {
|
|
31
36
|
const extracted = []
|
|
32
37
|
|
|
33
38
|
for (const composition of context.compositions ?? [])
|
|
@@ -35,18 +40,20 @@ async function extractDeclaredServices (context, extensions) {
|
|
|
35
40
|
if (reference in extensions) continue
|
|
36
41
|
|
|
37
42
|
try {
|
|
38
|
-
extracted.push(...await extract(reference, extensions, context.annotations))
|
|
43
|
+
extracted.push(...(await extract(reference, extensions, context.annotations)))
|
|
39
44
|
} catch (e) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
`
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Composition '${composition.name}' lists '${reference}', ` +
|
|
47
|
+
`which cannot be resolved: ${e.message}`,
|
|
48
|
+
{ cause: e }
|
|
49
|
+
)
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
return extracted
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
async function extractExtensionComponents
|
|
49
|
-
|
|
56
|
+
async function extractExtensionComponents(components, extensions, annotations) {
|
|
50
57
|
const extracted = []
|
|
51
58
|
|
|
52
59
|
for (const component of components) {
|
|
@@ -55,12 +62,11 @@ async function extractExtensionComponents (components, extensions, annotations)
|
|
|
55
62
|
for (const reference of Object.keys(component.extensions)) {
|
|
56
63
|
if (reference in extensions) continue
|
|
57
64
|
|
|
58
|
-
extracted.push(...await extract(reference, extensions, annotations))
|
|
65
|
+
extracted.push(...(await extract(reference, extensions, annotations)))
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
if (extracted.length === 0)
|
|
63
|
-
return extracted
|
|
69
|
+
if (extracted.length === 0) return extracted
|
|
64
70
|
|
|
65
71
|
const deeper = await extractExtensionComponents(extracted, extensions, annotations)
|
|
66
72
|
|
|
@@ -75,7 +81,7 @@ async function extractExtensionComponents (components, extensions, annotations)
|
|
|
75
81
|
* @param {object} [annotations]
|
|
76
82
|
* @returns {Promise<Array<toa.norm.Component>>}
|
|
77
83
|
*/
|
|
78
|
-
async function extract
|
|
84
|
+
async function extract(reference, extensions, annotations) {
|
|
79
85
|
extensions[reference] = []
|
|
80
86
|
|
|
81
87
|
const { metadata, module: mod } = await loadDependency(reference)
|
|
@@ -86,8 +92,7 @@ async function extract (reference, extensions, annotations) {
|
|
|
86
92
|
const annotation = annotations?.[metadata?.name ?? reference]
|
|
87
93
|
const extracted = []
|
|
88
94
|
|
|
89
|
-
for (const path of mod.components(annotation).paths)
|
|
90
|
-
extracted.push(await load(path))
|
|
95
|
+
for (const path of mod.components(annotation).paths) extracted.push(await load(path))
|
|
91
96
|
|
|
92
97
|
return extracted
|
|
93
98
|
}
|
|
@@ -11,16 +11,18 @@ const require = createRequire(import.meta.url)
|
|
|
11
11
|
* @param {string} path
|
|
12
12
|
* @returns {Promise<{ metadata: object, module: object }>}
|
|
13
13
|
*/
|
|
14
|
-
export async function load
|
|
14
|
+
export async function load(path) {
|
|
15
15
|
const metadata = loadMetadata(path)
|
|
16
16
|
const module = await loadModule(path)
|
|
17
17
|
|
|
18
18
|
return { metadata, module }
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function loadMetadata
|
|
21
|
+
function loadMetadata(reference) {
|
|
22
22
|
try {
|
|
23
|
-
return JSON.parse(
|
|
23
|
+
return JSON.parse(
|
|
24
|
+
readFileSync(require.resolve(join(reference, 'package.json')), 'utf8')
|
|
25
|
+
)
|
|
24
26
|
} catch {
|
|
25
27
|
return null
|
|
26
28
|
}
|
|
@@ -30,7 +32,7 @@ function loadMetadata (reference) {
|
|
|
30
32
|
* @param {string} reference
|
|
31
33
|
* @returns {Promise<object>}
|
|
32
34
|
*/
|
|
33
|
-
async function loadModule
|
|
35
|
+
async function loadModule(reference) {
|
|
34
36
|
const path = require.resolve(reference)
|
|
35
37
|
|
|
36
38
|
return await import(pathToFileURL(path).href)
|
|
@@ -38,7 +38,7 @@ export const resolve = async (references, annotations = {}) => {
|
|
|
38
38
|
* @param {string} reference
|
|
39
39
|
* @returns {object | null}
|
|
40
40
|
*/
|
|
41
|
-
async function optional
|
|
41
|
+
async function optional(reference) {
|
|
42
42
|
try {
|
|
43
43
|
return (await load(reference)).module
|
|
44
44
|
} catch {
|
|
@@ -12,7 +12,9 @@ export const dereference = (context) => {
|
|
|
12
12
|
const component = components[id]
|
|
13
13
|
|
|
14
14
|
if (component === undefined)
|
|
15
|
-
throw new Error(
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Composition '${composition.name}' lists an unknown component '${id}'.`
|
|
17
|
+
)
|
|
16
18
|
|
|
17
19
|
return component
|
|
18
20
|
})
|
|
@@ -12,4 +12,9 @@ export const normalize = (context) => {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
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
|
|
17
|
+
if (context.registry !== undefined) context.registry.platforms ??= PLATFORMS
|
|
15
18
|
}
|
|
19
|
+
|
|
20
|
+
const PLATFORMS = ['linux/amd64', 'linux/arm/v7', 'linux/arm64']
|
package/src/.context/schema.yaml
CHANGED
|
@@ -40,10 +40,12 @@ properties:
|
|
|
40
40
|
minItems: 1
|
|
41
41
|
items:
|
|
42
42
|
type: string
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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]
|
|
47
49
|
# required: [base]
|
|
48
50
|
compositions:
|
|
49
51
|
type: array
|
package/src/component.js
CHANGED
|
@@ -5,7 +5,17 @@ import { yaml as jsyaml } from '@toa.io/generic'
|
|
|
5
5
|
import { find } from '@toa.io/generic'
|
|
6
6
|
import { Locator } from '@toa.io/core'
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
expand,
|
|
10
|
+
merge,
|
|
11
|
+
migrations,
|
|
12
|
+
validate,
|
|
13
|
+
collapse,
|
|
14
|
+
dereference,
|
|
15
|
+
defaults,
|
|
16
|
+
normalize,
|
|
17
|
+
extensions
|
|
18
|
+
} from './.component/index.js'
|
|
9
19
|
|
|
10
20
|
export const component = async (path) => {
|
|
11
21
|
const manifest = await load(path)
|
|
@@ -23,7 +33,7 @@ const load = async (path, base, proto = false) => {
|
|
|
23
33
|
if (base !== undefined) path = find(path, base, MANIFEST)
|
|
24
34
|
|
|
25
35
|
const file = join(path, MANIFEST)
|
|
26
|
-
const manifest = await read(file) ?? {}
|
|
36
|
+
const manifest = (await read(file)) ?? {}
|
|
27
37
|
|
|
28
38
|
manifest.path = path
|
|
29
39
|
|
|
@@ -31,6 +41,7 @@ const load = async (path, base, proto = false) => {
|
|
|
31
41
|
await expand(manifest)
|
|
32
42
|
|
|
33
43
|
await merge(path, manifest)
|
|
44
|
+
await migrations(path, manifest)
|
|
34
45
|
|
|
35
46
|
if (manifest.prototype !== null) {
|
|
36
47
|
const prototype = await load(manifest.prototype, path, true)
|
|
@@ -46,8 +57,6 @@ const load = async (path, base, proto = false) => {
|
|
|
46
57
|
|
|
47
58
|
const MANIFEST = 'manifest.toa.yaml'
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
|
|
51
60
|
/**
|
|
52
61
|
* Reads a YAML file, resolving anchors into distinct objects so that
|
|
53
62
|
* mutating one node cannot reach another.
|
|
@@ -55,7 +64,7 @@ const MANIFEST = 'manifest.toa.yaml'
|
|
|
55
64
|
* @param {string} path
|
|
56
65
|
* @return {Promise<object>}
|
|
57
66
|
*/
|
|
58
|
-
async function read
|
|
67
|
+
async function read(path) {
|
|
59
68
|
const object = jsyaml.load(await readFile(path, 'utf8'))
|
|
60
69
|
|
|
61
70
|
return jsyaml.load(jsyaml.dump(object, { noRefs: true, lineWidth: -1 }))
|
package/src/context.js
CHANGED
|
@@ -6,7 +6,14 @@ import { yaml as jsyaml } from '@toa.io/generic'
|
|
|
6
6
|
|
|
7
7
|
import { component } from './component.js'
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
dependencies,
|
|
11
|
+
normalize,
|
|
12
|
+
complete,
|
|
13
|
+
dereference,
|
|
14
|
+
expand,
|
|
15
|
+
validate
|
|
16
|
+
} from './.context/index.js'
|
|
10
17
|
|
|
11
18
|
export const context = async (root, environment = process.env.TOA_ENV) => {
|
|
12
19
|
const path = resolve(root, CONTEXT)
|
|
@@ -34,11 +41,8 @@ export const context = async (root, environment = process.env.TOA_ENV) => {
|
|
|
34
41
|
const CONTEXT = 'context.toa.yaml'
|
|
35
42
|
const COMPONENTS = 'components/*'
|
|
36
43
|
|
|
37
|
-
|
|
38
44
|
const GLOB = { onlyDirectories: true, absolute: true }
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
46
|
/**
|
|
43
47
|
* Reads a YAML file, resolving anchors into distinct objects so that
|
|
44
48
|
* mutating one node cannot reach another.
|
|
@@ -46,7 +50,7 @@ const GLOB = { onlyDirectories: true, absolute: true }
|
|
|
46
50
|
* @param {string} path
|
|
47
51
|
* @return {Promise<object>}
|
|
48
52
|
*/
|
|
49
|
-
async function read
|
|
53
|
+
async function read(path) {
|
|
50
54
|
const object = jsyaml.load(await readFile(path, 'utf8'))
|
|
51
55
|
|
|
52
56
|
return jsyaml.load(jsyaml.dump(object, { noRefs: true, lineWidth: -1 }))
|
package/src/entity.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a component declares about its records is a property map and a list of the properties a
|
|
3
|
+
* whole one has. A validator wants a schema, so it is assembled here rather than declared: one
|
|
4
|
+
* that a stored record must fit, and one for a changeset, which is whatever subset of the same
|
|
5
|
+
* properties an assignment writes.
|
|
6
|
+
*/
|
|
7
|
+
export const schema = (entity) =>
|
|
8
|
+
entity.required === undefined
|
|
9
|
+
? changeset(entity)
|
|
10
|
+
: { type: 'object', properties: entity.properties, required: entity.required }
|
|
11
|
+
|
|
12
|
+
export const changeset = (entity) => ({
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: entity.properties
|
|
15
|
+
})
|
package/src/index.js
CHANGED
package/src/shortcuts.js
CHANGED
|
@@ -41,7 +41,6 @@ export const SHORTCUTS = {
|
|
|
41
41
|
node: '@toa.io/bridges.node',
|
|
42
42
|
bash: '@toa.io/bridges.bash',
|
|
43
43
|
mongodb: '@toa.io/storages.mongodb',
|
|
44
|
-
sql: '@toa.io/storages.sql',
|
|
45
44
|
queues: '@toa.io/storages.queues',
|
|
46
45
|
exposition: '@toa.io/extensions.exposition',
|
|
47
46
|
realtime: '@toa.io/extensions.realtime',
|
|
@@ -3,51 +3,45 @@ import { generate } from 'randomstring'
|
|
|
3
3
|
const entity = {
|
|
4
4
|
manifest: {
|
|
5
5
|
entity: {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type: 'string'
|
|
10
|
-
},
|
|
11
|
-
bar: {
|
|
12
|
-
type: 'integer'
|
|
13
|
-
}
|
|
6
|
+
properties: {
|
|
7
|
+
foo: {
|
|
8
|
+
type: 'string'
|
|
14
9
|
},
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
bar: {
|
|
11
|
+
type: 'integer'
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
required: ['foo']
|
|
17
15
|
}
|
|
18
16
|
},
|
|
19
17
|
prototype: {
|
|
20
18
|
entity: {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
default: 'ok'
|
|
25
|
-
},
|
|
26
|
-
baz: {
|
|
27
|
-
type: 'boolean'
|
|
28
|
-
}
|
|
19
|
+
properties: {
|
|
20
|
+
foo: {
|
|
21
|
+
minLength: 1
|
|
29
22
|
},
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
baz: {
|
|
24
|
+
type: 'boolean'
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
required: ['baz']
|
|
32
28
|
}
|
|
33
29
|
},
|
|
34
30
|
result: {
|
|
35
31
|
entity: {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
default: 'ok'
|
|
41
|
-
},
|
|
42
|
-
bar: {
|
|
43
|
-
type: 'integer'
|
|
44
|
-
},
|
|
45
|
-
baz: {
|
|
46
|
-
type: 'boolean'
|
|
47
|
-
}
|
|
32
|
+
properties: {
|
|
33
|
+
foo: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
minLength: 1
|
|
48
36
|
},
|
|
49
|
-
|
|
50
|
-
|
|
37
|
+
bar: {
|
|
38
|
+
type: 'integer'
|
|
39
|
+
},
|
|
40
|
+
baz: {
|
|
41
|
+
type: 'boolean'
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
required: ['foo', 'baz']
|
|
51
45
|
}
|
|
52
46
|
}
|
|
53
47
|
}
|