@toa.io/norm 1.0.0-alpha.31 → 1.0.0-alpha.310

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.
Files changed (99) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/package.json +9 -7
  3. package/src/.component/.expand/bridge.js +2 -6
  4. package/src/.component/.expand/entity.js +2 -9
  5. package/src/.component/.expand/events.js +2 -6
  6. package/src/.component/.expand/extensions.js +6 -8
  7. package/src/.component/.expand/index.js +7 -19
  8. package/src/.component/.expand/operations.js +2 -9
  9. package/src/.component/.expand/receivers.js +4 -7
  10. package/src/.component/.expand/version.js +67 -28
  11. package/src/.component/.normalize/entity.js +7 -0
  12. package/src/.component/.normalize/events.js +16 -4
  13. package/src/.component/.normalize/index.js +4 -9
  14. package/src/.component/.normalize/operations.js +13 -6
  15. package/src/.component/.normalize/receivers.js +4 -5
  16. package/src/.component/collapse.js +60 -9
  17. package/src/.component/defaults.js +5 -11
  18. package/src/.component/dependencies.js +14 -10
  19. package/src/.component/dereference.js +5 -49
  20. package/src/.component/expand.js +3 -9
  21. package/src/.component/extensions.js +9 -10
  22. package/src/.component/index.js +10 -21
  23. package/src/.component/merge.js +25 -15
  24. package/src/.component/migrations.js +57 -0
  25. package/src/.component/normalize.js +10 -6
  26. package/src/.component/schema.yaml +147 -73
  27. package/src/.component/validate.js +37 -17
  28. package/src/.context/.dependencies/connectors.js +5 -9
  29. package/src/.context/.dependencies/describe.js +4 -5
  30. package/src/.context/.dependencies/extensions.js +93 -20
  31. package/src/.context/.dependencies/index.js +3 -9
  32. package/src/.context/.dependencies/resolve.js +44 -7
  33. package/src/.context/complete.js +1 -5
  34. package/src/.context/converge.js +31 -0
  35. package/src/.context/dependencies.js +43 -6
  36. package/src/.context/dereference.js +11 -6
  37. package/src/.context/evict.js +38 -0
  38. package/src/.context/expand.js +11 -6
  39. package/src/.context/index.js +8 -15
  40. package/src/.context/normalize.js +9 -7
  41. package/src/.context/schema.yaml +181 -11
  42. package/src/.context/validate.js +8 -14
  43. package/src/component.js +33 -14
  44. package/src/context.js +44 -15
  45. package/src/definition.js +56 -0
  46. package/src/entity.js +15 -0
  47. package/src/index.js +7 -9
  48. package/src/manifest.js +154 -0
  49. package/src/map.js +24 -0
  50. package/src/shortcuts.js +9 -14
  51. package/test/component/collapse.fixtures.js +30 -39
  52. package/test/component/collapse.test.js +90 -10
  53. package/test/component/dependencies.test.js +16 -14
  54. package/test/component/dereference.fixtures.js +10 -98
  55. package/test/component/dereference.test.js +10 -14
  56. package/test/component/dummies/extension/index.js +1 -5
  57. package/test/component/dummies/migrations/conflicting/migrations/0001-one.yaml +2 -0
  58. package/test/component/dummies/migrations/conflicting/migrations/0001-one.yml +2 -0
  59. package/test/component/dummies/migrations/malformed/migrations/0001-one.yaml +1 -0
  60. package/test/component/dummies/migrations/ordered/migrations/0001-first.yml +4 -0
  61. package/test/component/dummies/migrations/ordered/migrations/0002-second.yaml +4 -0
  62. package/test/component/dummies/migrations/ordered/migrations/0010-third.json +1 -0
  63. package/test/component/dummies/migrations/stateless/migrations/0001-one.yaml +2 -0
  64. package/test/component/expand.fixtures.js +13 -13
  65. package/test/component/expand.test.js +17 -13
  66. package/test/component/migrations.test.js +62 -0
  67. package/test/component/normalize.fixtures.js +3 -7
  68. package/test/component/normalize.test.js +94 -28
  69. package/test/component/validate.fixtures.js +15 -13
  70. package/test/component/validate.test.js +180 -109
  71. package/test/context/complete.fixtures.js +2 -8
  72. package/test/context/complete.test.js +14 -8
  73. package/test/context/converge.test.js +57 -0
  74. package/test/context/dereference.fixtures.js +3 -9
  75. package/test/context/dereference.test.js +18 -7
  76. package/test/context/evict.fixtures.js +39 -0
  77. package/test/context/evict.test.js +72 -0
  78. package/test/context/expand.fixtures.js +1 -5
  79. package/test/context/expand.test.js +36 -8
  80. package/test/context/normalize.fixtures.js +2 -7
  81. package/test/context/normalize.test.js +33 -9
  82. package/test/context/validate.fixtures.js +1 -6
  83. package/test/context/validate.test.js +158 -35
  84. package/test/definition.test.js +35 -0
  85. package/test/dummies/bare/index.js +1 -0
  86. package/test/dummies/defined/definition.js +1 -0
  87. package/test/dummies/defined/index.js +1 -0
  88. package/test/dummies/defined/package.json +1 -0
  89. package/test/manifest.test.js +126 -0
  90. package/test/shortcuts.fixtures.js +4 -9
  91. package/test/shortcuts.test.js +17 -15
  92. package/types/component.d.ts +27 -4
  93. package/types/context/declaration.d.ts +34 -3
  94. package/types/context.d.ts +27 -13
  95. package/types/entity.d.ts +7 -0
  96. package/types/index.d.ts +18 -3
  97. package/src/.component/.expand/properties.js +0 -14
  98. package/src/.context/.dependencies/load.js +0 -36
  99. package/test/context/dependencies.load.test.js +0 -24
package/src/context.js CHANGED
@@ -1,27 +1,35 @@
1
- 'use strict'
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
- const { resolve } = require('node:path')
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
- const { component } = require('./component')
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
- } = require('./.context')
17
+ } from './.context/index.js'
18
18
 
19
- const context = async (root, environment = process.env.TOA_ENV) => {
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 load(path)
22
- const pattern = resolve(root, context.packages)
25
+ const context = /** @type {toa.norm.Context} */ await read(path)
26
+
27
+ const names = environment?.split(':')
23
28
 
24
- context.environment = environment
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(pattern)
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
- exports.context = context
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,7 @@
1
- 'use strict'
2
-
3
- const { context } = require('./context')
4
- const { component } = require('./component')
5
-
6
- exports.shortcuts = require('./shortcuts')
7
-
8
- exports.context = context
9
- exports.component = component
1
+ export { context } from './context.js'
2
+ export { component } from './component.js'
3
+ export { map } from './map.js'
4
+ export { plain, revive, NORMALIZED } from './manifest.js'
5
+ export { definition } from './definition.js'
6
+ export * as shortcuts from './shortcuts.js'
7
+ export * as entity from './entity.js'
@@ -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/map.js ADDED
@@ -0,0 +1,24 @@
1
+ import { contract } from '@toa.io/core'
2
+
3
+ /**
4
+ * Every component a Context has, with the version it runs and the contract of that version: its
5
+ * own, the ones its extensions bring, and the ones it evicts. A map is not rendered into a
6
+ * workload the way variables are, and an evicted component is called like any other, so it is
7
+ * described like any other.
8
+ *
9
+ * @param {toa.norm.Context} context
10
+ * @returns {Record<string, import('@toa.io/core').Contract>}
11
+ */
12
+ export const map = (context) => {
13
+ const components = new Map()
14
+
15
+ for (const component of context.components ?? [])
16
+ components.set(component.locator.id, contract.component(component))
17
+
18
+ // what an extension brings reaches a context as an instance of what it depends on
19
+ for (const instances of Object.values(context.dependencies ?? {}))
20
+ for (const { component } of instances)
21
+ components.set(component.locator.id, contract.component(component))
22
+
23
+ return Object.fromEntries([...components].sort(([one], [other]) => one.localeCompare(other)))
24
+ }
package/src/shortcuts.js CHANGED
@@ -1,6 +1,4 @@
1
- 'use strict'
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
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
1
+ import { generate } from 'randomstring'
4
2
 
5
3
  const entity = {
6
4
  manifest: {
7
5
  entity: {
8
- schema: {
9
- properties: {
10
- foo: {
11
- type: 'string'
12
- },
13
- bar: {
14
- type: 'integer'
15
- }
6
+ properties: {
7
+ foo: {
8
+ type: 'string'
16
9
  },
17
- required: ['foo']
18
- }
10
+ bar: {
11
+ type: 'integer'
12
+ }
13
+ },
14
+ required: ['foo']
19
15
  }
20
16
  },
21
17
  prototype: {
22
18
  entity: {
23
- schema: {
24
- properties: {
25
- foo: {
26
- default: 'ok'
27
- },
28
- baz: {
29
- type: 'boolean'
30
- }
19
+ properties: {
20
+ foo: {
21
+ minLength: 1
31
22
  },
32
- required: ['baz']
33
- }
23
+ baz: {
24
+ type: 'boolean'
25
+ }
26
+ },
27
+ required: ['baz']
34
28
  }
35
29
  },
36
30
  result: {
37
31
  entity: {
38
- schema: {
39
- properties: {
40
- foo: {
41
- type: 'string',
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
- required: ['foo', 'baz']
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
- exports.samples = { entity, operations, remotes }
125
+ export const samples = { entity, operations, remotes }
@@ -1,9 +1,10 @@
1
- 'use strict'
1
+ import { describe, it, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- const clone = require('clone-deep')
4
+ import clone from 'clone-deep'
4
5
 
5
- const fixtures = require('./collapse.fixtures')
6
- const { collapse } = require('../../src/.component')
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
- expect(manifest).toStrictEqual(source)
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
- expect(manifest).toStrictEqual({})
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
- expect(manifest).toStrictEqual(samples.entity.result)
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
- expect(manifest).toStrictEqual(source)
121
+ assert.deepStrictEqual(manifest, source)
48
122
 
49
123
  delete manifest.bindings
50
124
 
51
125
  collapse(manifest, prototype)
52
- expect(manifest).toStrictEqual({})
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
- expect(manifest).toStrictEqual(samples.operations.result)
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
  })