@toa.io/norm 1.0.0-alpha.30 → 1.0.0-alpha.301
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 +136 -0
- package/package.json +9 -7
- package/src/.component/.expand/bridge.js +2 -6
- package/src/.component/.expand/entity.js +2 -9
- package/src/.component/.expand/events.js +2 -6
- package/src/.component/.expand/extensions.js +5 -8
- package/src/.component/.expand/index.js +7 -19
- package/src/.component/.expand/operations.js +2 -9
- package/src/.component/.expand/receivers.js +4 -7
- package/src/.component/.expand/version.js +38 -28
- package/src/.component/.normalize/entity.js +7 -0
- package/src/.component/.normalize/events.js +16 -4
- package/src/.component/.normalize/index.js +4 -9
- package/src/.component/.normalize/operations.js +13 -6
- package/src/.component/.normalize/receivers.js +4 -5
- package/src/.component/collapse.js +60 -9
- package/src/.component/defaults.js +16 -10
- package/src/.component/dependencies.js +14 -10
- package/src/.component/dereference.js +5 -49
- package/src/.component/expand.js +3 -9
- package/src/.component/extensions.js +9 -10
- package/src/.component/index.js +10 -21
- package/src/.component/merge.js +25 -15
- package/src/.component/migrations.js +57 -0
- package/src/.component/normalize.js +10 -6
- package/src/.component/schema.yaml +137 -73
- package/src/.component/validate.js +37 -17
- package/src/.context/.dependencies/connectors.js +5 -9
- package/src/.context/.dependencies/describe.js +4 -5
- package/src/.context/.dependencies/extensions.js +93 -20
- package/src/.context/.dependencies/index.js +3 -9
- package/src/.context/.dependencies/resolve.js +44 -7
- package/src/.context/complete.js +1 -5
- package/src/.context/converge.js +31 -0
- package/src/.context/dependencies.js +43 -6
- package/src/.context/dereference.js +11 -6
- package/src/.context/evict.js +38 -0
- package/src/.context/expand.js +11 -6
- package/src/.context/index.js +8 -15
- package/src/.context/normalize.js +9 -7
- package/src/.context/schema.yaml +178 -11
- package/src/.context/validate.js +8 -14
- package/src/component.js +36 -17
- package/src/context.js +44 -15
- package/src/definition.js +56 -0
- package/src/entity.js +15 -0
- package/src/index.js +6 -9
- package/src/manifest.js +154 -0
- package/src/shortcuts.js +9 -14
- package/test/component/collapse.fixtures.js +30 -39
- package/test/component/collapse.test.js +90 -10
- package/test/component/dependencies.test.js +16 -14
- package/test/component/dereference.fixtures.js +10 -98
- package/test/component/dereference.test.js +10 -14
- package/test/component/dummies/extension/index.js +1 -5
- 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/expand.fixtures.js +13 -13
- package/test/component/expand.test.js +17 -13
- package/test/component/migrations.test.js +62 -0
- package/test/component/normalize.fixtures.js +3 -7
- package/test/component/normalize.test.js +94 -28
- package/test/component/validate.fixtures.js +15 -13
- package/test/component/validate.test.js +180 -109
- package/test/context/complete.fixtures.js +2 -8
- package/test/context/complete.test.js +14 -8
- package/test/context/converge.test.js +57 -0
- package/test/context/dereference.fixtures.js +3 -9
- package/test/context/dereference.test.js +18 -7
- package/test/context/evict.fixtures.js +39 -0
- package/test/context/evict.test.js +72 -0
- package/test/context/expand.fixtures.js +1 -5
- package/test/context/expand.test.js +36 -8
- package/test/context/normalize.fixtures.js +2 -7
- package/test/context/normalize.test.js +33 -9
- package/test/context/validate.fixtures.js +1 -6
- package/test/context/validate.test.js +143 -35
- package/test/definition.test.js +35 -0
- package/test/dummies/bare/index.js +1 -0
- package/test/dummies/defined/definition.js +1 -0
- package/test/dummies/defined/index.js +1 -0
- package/test/dummies/defined/package.json +1 -0
- package/test/manifest.test.js +126 -0
- package/test/shortcuts.fixtures.js +4 -9
- package/test/shortcuts.test.js +17 -15
- package/types/component.d.ts +23 -4
- package/types/context/declaration.d.ts +33 -3
- package/types/context.d.ts +26 -13
- package/types/entity.d.ts +7 -0
- package/types/index.d.ts +12 -3
- package/src/.component/.expand/properties.js +0 -14
- package/src/.context/.dependencies/load.js +0 -36
- package/test/context/dependencies.load.test.js +0 -24
package/src/context.js
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
|
-
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { convolve, environment as variables } from '@toa.io/generic'
|
|
3
|
+
import glob from 'fast-glob'
|
|
4
|
+
import { readFile } from 'node:fs/promises'
|
|
5
|
+
import { yaml as jsyaml } from '@toa.io/generic'
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
const { convolve } = require('@toa.io/generic')
|
|
5
|
-
const { directory: { glob } } = require('@toa.io/filesystem')
|
|
6
|
-
const { load } = require('@toa.io/yaml')
|
|
7
|
+
import { component } from './component.js'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const {
|
|
9
|
+
import {
|
|
11
10
|
dependencies,
|
|
12
11
|
normalize,
|
|
13
12
|
complete,
|
|
14
13
|
dereference,
|
|
14
|
+
evict,
|
|
15
15
|
expand,
|
|
16
16
|
validate
|
|
17
|
-
}
|
|
17
|
+
} from './.context/index.js'
|
|
18
18
|
|
|
19
|
-
const context = async (
|
|
19
|
+
export const context = async (
|
|
20
|
+
root,
|
|
21
|
+
environment = variables.get('TOA_ENV'),
|
|
22
|
+
options = {}
|
|
23
|
+
) => {
|
|
20
24
|
const path = resolve(root, CONTEXT)
|
|
21
|
-
const context = /** @type {toa.norm.Context} */ await
|
|
22
|
-
|
|
25
|
+
const context = /** @type {toa.norm.Context} */ await read(path)
|
|
26
|
+
|
|
27
|
+
const names = environment?.split(':')
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
if (names !== undefined && names.length > 1 && names.some((name) => name.length === 0))
|
|
30
|
+
throw new Error(`Environment '${environment}' contains an empty name.`)
|
|
31
|
+
|
|
32
|
+
context.environment = names?.[0]
|
|
25
33
|
|
|
26
34
|
convolve(context, environment)
|
|
27
35
|
expand(context)
|
|
@@ -29,9 +37,16 @@ const context = async (root, environment = process.env.TOA_ENV) => {
|
|
|
29
37
|
|
|
30
38
|
validate(context)
|
|
31
39
|
|
|
32
|
-
const paths = await glob(
|
|
40
|
+
const paths = await glob(resolve(root, COMPONENTS), GLOB)
|
|
33
41
|
|
|
34
42
|
context.components = await Promise.all(paths.map(component))
|
|
43
|
+
|
|
44
|
+
// before the dependencies, which mark what only evicted components require.
|
|
45
|
+
// `evicted: false` leaves that off: a local run of a component still needs its variables
|
|
46
|
+
if (options.evicted !== false) evict(context)
|
|
47
|
+
|
|
48
|
+
// what a context declares of every component that stores anything, its own and the ones its
|
|
49
|
+
// extensions bring, is given to them where those are known: inside `dependencies`
|
|
35
50
|
context.dependencies = await dependencies(context)
|
|
36
51
|
|
|
37
52
|
dereference(context)
|
|
@@ -41,5 +56,19 @@ const context = async (root, environment = process.env.TOA_ENV) => {
|
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
const CONTEXT = 'context.toa.yaml'
|
|
59
|
+
const COMPONENTS = 'components/*'
|
|
60
|
+
|
|
61
|
+
const GLOB = { onlyDirectories: true, absolute: true }
|
|
44
62
|
|
|
45
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Reads a YAML file, resolving anchors into distinct objects so that
|
|
65
|
+
* mutating one node cannot reach another.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} path
|
|
68
|
+
* @return {Promise<object>}
|
|
69
|
+
*/
|
|
70
|
+
async function read(path) {
|
|
71
|
+
const object = jsyaml.load(await readFile(path, 'utf8'))
|
|
72
|
+
|
|
73
|
+
return jsyaml.load(jsyaml.dump(object, { noRefs: true, lineWidth: -1 }))
|
|
74
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import * as definitions from '@toa.io/definitions'
|
|
6
|
+
|
|
7
|
+
// import.meta.resolve takes no paths, and a dependency is named rather than
|
|
8
|
+
// written as a path
|
|
9
|
+
const require = createRequire(import.meta.url)
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* What a package declares, read without running it.
|
|
13
|
+
*
|
|
14
|
+
* A first-party package's definition is in `@toa.io/definitions`. Another package's is
|
|
15
|
+
* its `definition.js`, or its entry where it ships none — so an extension that exports
|
|
16
|
+
* its `manifest` and `deployment` beside its `Factory` is read as it always was.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} reference a package name, or a directory
|
|
19
|
+
* @returns {Promise<toa.norm.Definition>}
|
|
20
|
+
*/
|
|
21
|
+
export function definition(reference) {
|
|
22
|
+
cache[reference] ??= load(reference)
|
|
23
|
+
|
|
24
|
+
return cache[reference]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const cache = {}
|
|
28
|
+
|
|
29
|
+
async function load(reference) {
|
|
30
|
+
const known = await definitions.definition(reference)
|
|
31
|
+
|
|
32
|
+
if (known !== undefined) return { name: reference, module: known }
|
|
33
|
+
|
|
34
|
+
const name = metadata(reference)?.name ?? reference
|
|
35
|
+
const module = await import(pathToFileURL(resolve(reference)).href)
|
|
36
|
+
|
|
37
|
+
return { name, module }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolve(reference) {
|
|
41
|
+
try {
|
|
42
|
+
return require.resolve(join(reference, 'definition.js'))
|
|
43
|
+
} catch {
|
|
44
|
+
return require.resolve(reference)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function metadata(reference) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(
|
|
51
|
+
readFileSync(require.resolve(join(reference, 'package.json')), 'utf8')
|
|
52
|
+
)
|
|
53
|
+
} catch {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
}
|
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
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
exports.context = context
|
|
9
|
-
exports.component = component
|
|
1
|
+
export { context } from './context.js'
|
|
2
|
+
export { component } from './component.js'
|
|
3
|
+
export { plain, revive, NORMALIZED } from './manifest.js'
|
|
4
|
+
export { definition } from './definition.js'
|
|
5
|
+
export * as shortcuts from './shortcuts.js'
|
|
6
|
+
export * as entity from './entity.js'
|
package/src/manifest.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, isAbsolute, join, parse, relative, sep } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { Locator } from '@toa.io/core'
|
|
5
|
+
import { find } from '@toa.io/generic'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A normalised manifest, as it is carried in a file beside the component it is of.
|
|
9
|
+
*
|
|
10
|
+
* A build normalises every component to tag its image; the process that runs it normalised them
|
|
11
|
+
* again at start, reading every operation module's source through its bridge to do it. What the
|
|
12
|
+
* build derived is written instead, and the two cannot disagree: the sources it was read from are
|
|
13
|
+
* what the image's tag digests, and the version of Toa that read them is in that tag as well.
|
|
14
|
+
*
|
|
15
|
+
* What cannot be carried is where things are. A workspace and an image put a component in one
|
|
16
|
+
* directory and Toa's own packages in another, so every path is written as what it is of rather
|
|
17
|
+
* than as where it was — the component itself, or the package that holds it — and is resolved
|
|
18
|
+
* again on the other side, the way norm resolved it.
|
|
19
|
+
*
|
|
20
|
+
* A key with no value is not carried: a file has no way to say `undefined`, and nothing reads
|
|
21
|
+
* one apart from a key that is not there.
|
|
22
|
+
*/
|
|
23
|
+
export function plain(manifest) {
|
|
24
|
+
const { locator, packages, path, ...rest } = manifest
|
|
25
|
+
|
|
26
|
+
// what is carried is written, not the manifest the caller goes on using, and it is written
|
|
27
|
+
// as a file holds it: a key with no value does not survive and is not meant to
|
|
28
|
+
const declared = JSON.parse(JSON.stringify(rest))
|
|
29
|
+
|
|
30
|
+
walk(declared, (item) => {
|
|
31
|
+
item.path = carried(item.path, path)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
local(declared, path)
|
|
35
|
+
|
|
36
|
+
return declared
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** What `plain` wrote, read back beside the component it is of. */
|
|
40
|
+
export function revive(declared, path) {
|
|
41
|
+
const manifest = { ...structuredClone(declared), path }
|
|
42
|
+
|
|
43
|
+
walk(manifest, (item) => {
|
|
44
|
+
item.path = resolved(item.path, path)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
manifest.locator = new Locator(manifest.name, manifest.namespace)
|
|
48
|
+
|
|
49
|
+
return manifest
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Everything in a manifest that says where a module of it is: what an inherited one declares is
|
|
54
|
+
* in the prototype it came from, and what the component declares is in the component.
|
|
55
|
+
*/
|
|
56
|
+
function walk(manifest, rewrite) {
|
|
57
|
+
for (const property of ROOTED)
|
|
58
|
+
for (const item of Object.values(manifest[property] ?? {}))
|
|
59
|
+
if (item.path !== undefined) rewrite(item)
|
|
60
|
+
|
|
61
|
+
for (let prototype = manifest.prototype; prototype != null; prototype = prototype.prototype)
|
|
62
|
+
if (prototype.path !== undefined) rewrite(prototype)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Where it is written. A workspace has none: it is a build's output, read by a process. */
|
|
66
|
+
export const NORMALIZED = 'manifest.toa.json'
|
|
67
|
+
|
|
68
|
+
/** Where norm says a module is, beside what it says about it. */
|
|
69
|
+
const ROOTED = ['events', 'receivers', 'guards']
|
|
70
|
+
|
|
71
|
+
/** The component's own directory, which is where it is read back. */
|
|
72
|
+
const SELF = '.'
|
|
73
|
+
|
|
74
|
+
function carried(path, root) {
|
|
75
|
+
if (path === root) return SELF
|
|
76
|
+
if (path.startsWith(root + sep)) return SELF + '/' + relative(root, path).split(sep).join('/')
|
|
77
|
+
|
|
78
|
+
return specifier(path)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolved(path, root) {
|
|
82
|
+
if (path === SELF) return root
|
|
83
|
+
if (path.startsWith(SELF + '/')) return join(root, path.slice(2))
|
|
84
|
+
|
|
85
|
+
return find(path, root, MANIFEST)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A directory in a package, named the way a manifest would name it, so that it is found again
|
|
90
|
+
* wherever the package is installed. A prototype that is a directory of the application's own is
|
|
91
|
+
* refused here rather than in the container: an image carries components and nothing between
|
|
92
|
+
* them, so nothing would be there to find, whether it is normalised here or there.
|
|
93
|
+
*/
|
|
94
|
+
function specifier(path) {
|
|
95
|
+
const root = above(path)
|
|
96
|
+
|
|
97
|
+
if (root !== undefined) {
|
|
98
|
+
const { name, private: hidden } = JSON.parse(
|
|
99
|
+
readFileSync(join(root, 'package.json'), 'utf8')
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if (name !== undefined && hidden !== true)
|
|
103
|
+
return [name, ...relative(root, path).split(sep)].filter(Boolean).join('/')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error(
|
|
107
|
+
`'${path}' is in no package, so an image cannot carry what is in it: ` +
|
|
108
|
+
'what a component inherits is reached by the reference of the package it is in.'
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The directory of the package a path is in. */
|
|
113
|
+
function above(path) {
|
|
114
|
+
const { root } = parse(path)
|
|
115
|
+
|
|
116
|
+
let current = path
|
|
117
|
+
|
|
118
|
+
while (current !== root) {
|
|
119
|
+
if (existsSync(join(current, 'package.json'))) return current
|
|
120
|
+
|
|
121
|
+
current = dirname(current)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return undefined
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Nothing that is carried may say where it was on the machine that read it. What is checked is
|
|
129
|
+
* what can be told apart from a value of the application's own — an HTTP route is `/accounts`
|
|
130
|
+
* and is a path to nobody — so: anything inside the component, and any directory a manifest
|
|
131
|
+
* declares a component or a prototype in. This is what catches a path norm starts writing
|
|
132
|
+
* somewhere this file does not know about, at the build rather than at the start of a container
|
|
133
|
+
* that cannot find it.
|
|
134
|
+
*/
|
|
135
|
+
function local(value, root, at = '') {
|
|
136
|
+
if (typeof value === 'string') {
|
|
137
|
+
if (value === root || value.startsWith(root + sep) || component(value))
|
|
138
|
+
throw new Error(`'${at}' carries a path of the machine that read it: ${value}`)
|
|
139
|
+
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (value === null || typeof value !== 'object') return
|
|
144
|
+
|
|
145
|
+
for (const [key, item] of Object.entries(value))
|
|
146
|
+
local(item, root, at === '' ? key : `${at}.${key}`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Whether a value is a directory a component or a prototype is declared in. */
|
|
150
|
+
function component(value) {
|
|
151
|
+
return isAbsolute(value) && existsSync(join(value, MANIFEST))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const MANIFEST = 'manifest.toa.yaml'
|
package/src/shortcuts.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const { empty, merge } = require('@toa.io/generic')
|
|
1
|
+
import { empty, merge } from '@toa.io/generic'
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* Resolves shortcuts
|
|
@@ -8,7 +6,7 @@ const { empty, merge } = require('@toa.io/generic')
|
|
|
8
6
|
* @param {string} token
|
|
9
7
|
* @returns {string}
|
|
10
8
|
*/
|
|
11
|
-
const resolve = (token) => {
|
|
9
|
+
export const resolve = (token) => {
|
|
12
10
|
if (token in SHORTCUTS) return SHORTCUTS[token]
|
|
13
11
|
else return token
|
|
14
12
|
}
|
|
@@ -20,7 +18,7 @@ const resolve = (token) => {
|
|
|
20
18
|
* @param {Object} object
|
|
21
19
|
* @param {string} [group]
|
|
22
20
|
*/
|
|
23
|
-
const recognize = (shortcuts, object, group) => {
|
|
21
|
+
export const recognize = (shortcuts, object, group) => {
|
|
24
22
|
if (object === undefined) return
|
|
25
23
|
|
|
26
24
|
const target = group === undefined ? object : {}
|
|
@@ -38,21 +36,18 @@ const recognize = (shortcuts, object, group) => {
|
|
|
38
36
|
if (group !== undefined && !empty(target)) object[group] = merge(object[group], target)
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
const SHORTCUTS = {
|
|
39
|
+
export const SHORTCUTS = {
|
|
42
40
|
amqp: '@toa.io/bindings.amqp',
|
|
43
41
|
node: '@toa.io/bridges.node',
|
|
44
42
|
bash: '@toa.io/bridges.bash',
|
|
45
43
|
mongodb: '@toa.io/storages.mongodb',
|
|
46
|
-
sql: '@toa.io/storages.sql',
|
|
47
|
-
queues: '@toa.io/storages.queues',
|
|
48
44
|
exposition: '@toa.io/extensions.exposition',
|
|
49
45
|
realtime: '@toa.io/extensions.realtime',
|
|
50
46
|
configuration: '@toa.io/extensions.configuration',
|
|
51
|
-
origins: '@toa.io/extensions.origins',
|
|
52
47
|
stash: '@toa.io/extensions.stash',
|
|
53
|
-
storages: '@toa.io/extensions.storages'
|
|
48
|
+
storages: '@toa.io/extensions.storages',
|
|
49
|
+
telemetry: '@toa.io/extensions.telemetry',
|
|
50
|
+
cadence: '@toa.io/extensions.cadence',
|
|
51
|
+
convergence: '@toa.io/extensions.convergence',
|
|
52
|
+
introspection: '@toa.io/extensions.introspection'
|
|
54
53
|
}
|
|
55
|
-
|
|
56
|
-
exports.recognize = recognize
|
|
57
|
-
exports.resolve = resolve
|
|
58
|
-
exports.SHORTCUTS = SHORTCUTS
|
|
@@ -1,55 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const { generate } = require('randomstring')
|
|
1
|
+
import { generate } from 'randomstring'
|
|
4
2
|
|
|
5
3
|
const entity = {
|
|
6
4
|
manifest: {
|
|
7
5
|
entity: {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
type: 'string'
|
|
12
|
-
},
|
|
13
|
-
bar: {
|
|
14
|
-
type: 'integer'
|
|
15
|
-
}
|
|
6
|
+
properties: {
|
|
7
|
+
foo: {
|
|
8
|
+
type: 'string'
|
|
16
9
|
},
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
bar: {
|
|
11
|
+
type: 'integer'
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
required: ['foo']
|
|
19
15
|
}
|
|
20
16
|
},
|
|
21
17
|
prototype: {
|
|
22
18
|
entity: {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
default: 'ok'
|
|
27
|
-
},
|
|
28
|
-
baz: {
|
|
29
|
-
type: 'boolean'
|
|
30
|
-
}
|
|
19
|
+
properties: {
|
|
20
|
+
foo: {
|
|
21
|
+
minLength: 1
|
|
31
22
|
},
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
baz: {
|
|
24
|
+
type: 'boolean'
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
required: ['baz']
|
|
34
28
|
}
|
|
35
29
|
},
|
|
36
30
|
result: {
|
|
37
31
|
entity: {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
default: 'ok'
|
|
43
|
-
},
|
|
44
|
-
bar: {
|
|
45
|
-
type: 'integer'
|
|
46
|
-
},
|
|
47
|
-
baz: {
|
|
48
|
-
type: 'boolean'
|
|
49
|
-
}
|
|
32
|
+
properties: {
|
|
33
|
+
foo: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
minLength: 1
|
|
50
36
|
},
|
|
51
|
-
|
|
52
|
-
|
|
37
|
+
bar: {
|
|
38
|
+
type: 'integer'
|
|
39
|
+
},
|
|
40
|
+
baz: {
|
|
41
|
+
type: 'boolean'
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
required: ['foo', 'baz']
|
|
53
45
|
}
|
|
54
46
|
}
|
|
55
47
|
}
|
|
@@ -88,7 +80,6 @@ const operations = {
|
|
|
88
80
|
result: {
|
|
89
81
|
prototype: {
|
|
90
82
|
prototype: null,
|
|
91
|
-
path: expect.any(String),
|
|
92
83
|
operations: {
|
|
93
84
|
add: {
|
|
94
85
|
bridge: 'a'
|
|
@@ -131,4 +122,4 @@ const remotes = {
|
|
|
131
122
|
}
|
|
132
123
|
}
|
|
133
124
|
|
|
134
|
-
|
|
125
|
+
export const samples = { entity, operations, remotes }
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { describe, it, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
import clone from 'clone-deep'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
import * as fixtures from './collapse.fixtures.js'
|
|
7
|
+
import { collapse } from '../../src/.component/index.js'
|
|
7
8
|
|
|
8
9
|
let samples
|
|
9
10
|
|
|
@@ -18,7 +19,7 @@ it('should ignore locator', () => {
|
|
|
18
19
|
|
|
19
20
|
collapse(manifest, prototype)
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
assert.deepStrictEqual(manifest, source)
|
|
22
23
|
})
|
|
23
24
|
|
|
24
25
|
it('should remove prototype property', () => {
|
|
@@ -26,7 +27,7 @@ it('should remove prototype property', () => {
|
|
|
26
27
|
|
|
27
28
|
collapse(manifest, {})
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
assert.deepStrictEqual(manifest, {})
|
|
30
31
|
})
|
|
31
32
|
|
|
32
33
|
describe('entity', () => {
|
|
@@ -34,7 +35,80 @@ describe('entity', () => {
|
|
|
34
35
|
const manifest = clone(samples.entity.manifest)
|
|
35
36
|
|
|
36
37
|
collapse(manifest, samples.entity.prototype)
|
|
37
|
-
|
|
38
|
+
assert.deepStrictEqual(manifest, samples.entity.result)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should let a component state its own id', () => {
|
|
42
|
+
const prototype = { entity: { properties: { id: { type: 'string' }, VERSION: {} } } }
|
|
43
|
+
const manifest = { name: 'pot', entity: { properties: { id: { type: 'number' } } } }
|
|
44
|
+
|
|
45
|
+
collapse(manifest, prototype)
|
|
46
|
+
|
|
47
|
+
assert.strictEqual(manifest.entity.custom, true)
|
|
48
|
+
assert.deepStrictEqual(manifest.entity.properties.id, { type: 'number' })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should refuse a component that states a property the runtime writes', () => {
|
|
52
|
+
const prototype = { entity: { properties: { DELETED: { type: 'integer' } } } }
|
|
53
|
+
const manifest = {
|
|
54
|
+
name: 'pot',
|
|
55
|
+
entity: { properties: { DELETED: { type: 'integer' } } }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
assert.throws(
|
|
59
|
+
() => collapse(manifest, prototype),
|
|
60
|
+
/System property 'DELETED' cannot be overridden/
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should inherit migrations ahead of its own, under the prototype name', () => {
|
|
65
|
+
const manifest = { entity: { migrations: [{ id: '0002', steps: [] }] } }
|
|
66
|
+
const prototype = {
|
|
67
|
+
name: 'base',
|
|
68
|
+
entity: { migrations: [{ id: '0001', steps: [] }] }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
collapse(manifest, prototype)
|
|
72
|
+
|
|
73
|
+
assert.deepStrictEqual(manifest.entity.migrations, [
|
|
74
|
+
{ id: 'base:0001', steps: [], prototype: 'base' },
|
|
75
|
+
{ id: '0002', steps: [] }
|
|
76
|
+
])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should pass on what the prototype inherited as it is', () => {
|
|
80
|
+
const manifest = { entity: { migrations: [{ id: '0002', steps: [] }] } }
|
|
81
|
+
const prototype = {
|
|
82
|
+
name: 'base',
|
|
83
|
+
entity: {
|
|
84
|
+
migrations: [
|
|
85
|
+
{ id: 'system:0001', steps: [], prototype: 'system' },
|
|
86
|
+
{ id: '0001', steps: [] }
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
collapse(manifest, prototype)
|
|
92
|
+
|
|
93
|
+
assert.deepStrictEqual(
|
|
94
|
+
manifest.entity.migrations.map(({ id }) => id),
|
|
95
|
+
['system:0001', 'base:0001', '0002']
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should inherit migrations into a component that declares none', () => {
|
|
100
|
+
const manifest = {}
|
|
101
|
+
const prototype = {
|
|
102
|
+
name: 'base',
|
|
103
|
+
entity: { migrations: [{ id: '0001', steps: [] }] }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
collapse(manifest, prototype)
|
|
107
|
+
|
|
108
|
+
assert.deepStrictEqual(
|
|
109
|
+
manifest.entity.migrations.map(({ id }) => id),
|
|
110
|
+
['base:0001']
|
|
111
|
+
)
|
|
38
112
|
})
|
|
39
113
|
})
|
|
40
114
|
|
|
@@ -44,12 +118,12 @@ it('should ignore bindings', () => {
|
|
|
44
118
|
const manifest = clone(source)
|
|
45
119
|
|
|
46
120
|
collapse(manifest, prototype)
|
|
47
|
-
|
|
121
|
+
assert.deepStrictEqual(manifest, source)
|
|
48
122
|
|
|
49
123
|
delete manifest.bindings
|
|
50
124
|
|
|
51
125
|
collapse(manifest, prototype)
|
|
52
|
-
|
|
126
|
+
assert.deepStrictEqual(manifest, {})
|
|
53
127
|
})
|
|
54
128
|
|
|
55
129
|
it('should merge operations', () => {
|
|
@@ -57,5 +131,11 @@ it('should merge operations', () => {
|
|
|
57
131
|
const prototype = clone(samples.operations.prototype)
|
|
58
132
|
|
|
59
133
|
collapse(manifest, prototype, '/somewhere')
|
|
60
|
-
|
|
134
|
+
|
|
135
|
+
// the prototype's path is generated, so it is checked apart from the shape
|
|
136
|
+
assert.strictEqual(typeof manifest.prototype.path, 'string')
|
|
137
|
+
|
|
138
|
+
const { path, ...rest } = manifest.prototype
|
|
139
|
+
|
|
140
|
+
assert.deepStrictEqual({ ...manifest, prototype: rest }, samples.operations.result)
|
|
61
141
|
})
|
|
@@ -1,38 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
import { describe, it, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
import { resolve } from 'node:path'
|
|
5
|
+
import { dependencies } from '../../src/.component/index.js'
|
|
5
6
|
|
|
6
|
-
const NORM = resolve(
|
|
7
|
+
const NORM = resolve(import.meta.dirname, '../../')
|
|
7
8
|
|
|
8
9
|
it('should be', async () => {
|
|
9
|
-
|
|
10
|
+
assert.ok(dependencies instanceof Function)
|
|
10
11
|
})
|
|
11
12
|
|
|
12
13
|
/** @type {toa.norm.Component} */
|
|
13
14
|
let component
|
|
14
15
|
|
|
15
16
|
beforeEach(() => {
|
|
16
|
-
component = /** @type {toa.norm.Component} */ { path:
|
|
17
|
+
component = /** @type {toa.norm.Component} */ { path: import.meta.dirname }
|
|
17
18
|
})
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
for (const [_, reference] of [
|
|
20
21
|
['package id', '@toa.io/norm'],
|
|
21
22
|
['relative path', '../../']
|
|
22
|
-
])
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
])
|
|
24
|
+
describe(`${_}`, () => {
|
|
25
|
+
it('should resolve storage', async () => {
|
|
26
|
+
component.entity = { storage: reference, schema: {} }
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
dependencies(component)
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
assert.deepStrictEqual(component.entity.storage, NORM)
|
|
31
|
+
})
|
|
29
32
|
})
|
|
30
|
-
})
|
|
31
33
|
|
|
32
34
|
it('should resolve toa packages', async () => {
|
|
33
35
|
component.entity = { storage: '@toa.io/core', schema: {} }
|
|
34
36
|
|
|
35
37
|
dependencies(component)
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
assert.notStrictEqual(component.entity.storage, undefined)
|
|
38
40
|
})
|