@toa.io/norm 1.0.0-alpha.287 → 1.0.0-alpha.289

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 CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [1.0.0-alpha.289](https://github.com/toa-io/toa/compare/v1.0.0-alpha.288...v1.0.0-alpha.289) (2026-09-07)
7
+
8
+ * A deploy moves only what changed, and a component sees none of the runtime's environment (#1073) ([f38e3db](https://github.com/toa-io/toa/commit/f38e3db533f24db866c57bfa1ef294eff1eaf499)), closes [#1073](https://github.com/toa-io/toa/issues/1073) [#1064](https://github.com/toa-io/toa/issues/1064) [#1066](https://github.com/toa-io/toa/issues/1066) [#1067](https://github.com/toa-io/toa/issues/1067) [#1068](https://github.com/toa-io/toa/issues/1068) [#1069](https://github.com/toa-io/toa/issues/1069) [#1071](https://github.com/toa-io/toa/issues/1071) [#1070](https://github.com/toa-io/toa/issues/1070) [#1072](https://github.com/toa-io/toa/issues/1072)
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * a component that read `process.env.TOA_*` reads `context` instead;
13
+ `echo(input)` no longer substitutes from the environment; a bash operation sees no
14
+ `TOA_*`; images no longer set `USER node` — see migrations/289.md.
15
+
16
+
17
+ # [1.0.0-alpha.288](https://github.com/toa-io/toa/compare/v1.0.0-alpha.287...v1.0.0-alpha.288) (2026-09-06)
18
+
19
+ **Note:** Version bump only for package @toa.io/norm
20
+
21
+
22
+
23
+
24
+
6
25
  # [1.0.0-alpha.287](https://github.com/toa-io/toa/compare/v1.0.0-alpha.286...v1.0.0-alpha.287) (2026-09-06)
7
26
 
8
27
  **Note:** Version bump only for package @toa.io/norm
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/norm",
3
- "version": "1.0.0-alpha.287",
3
+ "version": "1.0.0-alpha.289",
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.287",
25
- "@toa.io/generic": "1.0.0-alpha.286",
26
- "@toa.io/schemas": "1.0.0-alpha.287",
24
+ "@toa.io/core": "1.0.0-alpha.289",
25
+ "@toa.io/generic": "1.0.0-alpha.289",
26
+ "@toa.io/schemas": "1.0.0-alpha.289",
27
27
  "fast-glob": "3.3.3",
28
28
  "js-yaml": "5.4.1"
29
29
  },
30
- "gitHead": "5c7944a70c16065ab6372072385cc1f02f5db77c"
30
+ "gitHead": "4fcb482beac5896305d8084cdbc1d5fcf372d108"
31
31
  }
@@ -62,12 +62,38 @@ export const collapse = (manifest, prototype) => {
62
62
  for (const event of Object.keys(prototype.events))
63
63
  if (event in manifest.events) delete prototype.events[event]
64
64
 
65
- // a migration is applied to a collection, and a prototype has none: what it declares belongs
66
- // to the component that declared it, so it is not collapsed into this one
67
- if (entity?.migrations !== undefined) delete entity.migrations
65
+ if (entity?.migrations !== undefined) inherit(manifest, prototype)
68
66
 
69
67
  merge(manifest, { entity, events, extensions })
70
68
  }
71
69
 
70
+ /**
71
+ * What a prototype declares is in every record of its descendants, so its migrations are applied
72
+ * to each descendant's collection — ahead of the descendant's own, which are written against
73
+ * what they leave. An inherited one is recorded under the prototype's name, so that it collides
74
+ * with nothing the component names itself; one inherited from further up already carries its
75
+ * name and is passed on as it is.
76
+ */
77
+ function inherit(manifest, prototype) {
78
+ const inherited = prototype.entity.migrations.map((migration) =>
79
+ migration.prototype === undefined
80
+ ? {
81
+ ...migration,
82
+ id: `${prototype.name}:${migration.id}`,
83
+ prototype: prototype.name
84
+ }
85
+ : migration
86
+ )
87
+
88
+ const own = manifest.entity?.migrations ?? []
89
+
90
+ if (manifest.entity === undefined) manifest.entity = {}
91
+
92
+ manifest.entity.migrations = [...inherited, ...own]
93
+
94
+ // merged below by `merge`, which would append them a second time
95
+ delete prototype.entity.migrations
96
+ }
97
+
72
98
  /** What the runtime writes into every record. `id` is not among them: a component may own it. */
73
99
  const SYSTEM = ['VERSION', 'CREATED', 'UPDATED', 'DELETED']
@@ -100,6 +100,9 @@ properties:
100
100
  # what a step means belongs to the storage, which is the only thing that reads it
101
101
  steps:
102
102
  type: array
103
+ # the prototype it is inherited from; absent on the component's own
104
+ prototype:
105
+ type: string
103
106
  required: [id, steps]
104
107
  additionalProperties: false
105
108
  associated:
@@ -13,8 +13,9 @@ export const normalize = (context) => {
13
13
 
14
14
  if (typeof context.registry === 'string') context.registry = { base: context.registry }
15
15
 
16
- // what an image is built for, where the Context does not say
17
- if (context.registry !== undefined) context.registry.platforms ??= PLATFORMS
16
+ // what an image is built for, where the Context does not say; `~` says the runner's own
17
+ if (context.registry !== undefined && context.registry.platforms === undefined)
18
+ context.registry.platforms = PLATFORMS
18
19
  }
19
20
 
20
21
  const PLATFORMS = ['linux/amd64', 'linux/arm/v7', 'linux/arm64']
package/src/component.js CHANGED
@@ -37,12 +37,19 @@ const load = async (path, base, proto = false) => {
37
37
 
38
38
  manifest.path = path
39
39
 
40
+ const anonymous = manifest.name === undefined
41
+
40
42
  defaults(manifest, proto)
41
43
  await expand(manifest)
42
44
 
43
45
  await merge(path, manifest)
44
46
  await migrations(path, manifest)
45
47
 
48
+ // an inherited migration is recorded under the prototype's name, and a generated one is not
49
+ // the same on the next machine
50
+ if (proto && anonymous && manifest.entity?.migrations !== undefined)
51
+ throw new Error(`Prototype at '${path}' declares migrations, so it has to be named`)
52
+
46
53
  if (manifest.prototype !== null) {
47
54
  const prototype = await load(manifest.prototype, path, true)
48
55
 
package/src/context.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { resolve } from 'node:path'
2
- import { convolve } from '@toa.io/generic'
2
+ import { convolve, environment as variables } from '@toa.io/generic'
3
3
  import glob from 'fast-glob'
4
4
  import { readFile } from 'node:fs/promises'
5
5
  import { yaml as jsyaml } from '@toa.io/generic'
@@ -15,7 +15,7 @@ import {
15
15
  validate
16
16
  } from './.context/index.js'
17
17
 
18
- export const context = async (root, environment = process.env.TOA_ENV) => {
18
+ export const context = async (root, environment = variables.get('TOA_ENV')) => {
19
19
  const path = resolve(root, CONTEXT)
20
20
  const context = /** @type {toa.norm.Context} */ await read(path)
21
21
 
@@ -50,7 +50,10 @@ describe('entity', () => {
50
50
 
51
51
  it('should refuse a component that states a property the runtime writes', () => {
52
52
  const prototype = { entity: { properties: { DELETED: { type: 'integer' } } } }
53
- const manifest = { name: 'pot', entity: { properties: { DELETED: { type: 'integer' } } } }
53
+ const manifest = {
54
+ name: 'pot',
55
+ entity: { properties: { DELETED: { type: 'integer' } } }
56
+ }
54
57
 
55
58
  assert.throws(
56
59
  () => collapse(manifest, prototype),
@@ -58,25 +61,54 @@ describe('entity', () => {
58
61
  )
59
62
  })
60
63
 
61
- it('should not inherit migrations', () => {
64
+ it('should inherit migrations ahead of its own, under the prototype name', () => {
62
65
  const manifest = { entity: { migrations: [{ id: '0002', steps: [] }] } }
63
- const prototype = { entity: { migrations: [{ id: '0001', 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
+ }
64
90
 
65
91
  collapse(manifest, prototype)
66
92
 
67
93
  assert.deepStrictEqual(
68
94
  manifest.entity.migrations.map(({ id }) => id),
69
- ['0002']
95
+ ['system:0001', 'base:0001', '0002']
70
96
  )
71
97
  })
72
98
 
73
- it('should leave a component with no migrations without any', () => {
99
+ it('should inherit migrations into a component that declares none', () => {
74
100
  const manifest = {}
75
- const prototype = { entity: { migrations: [{ id: '0001', steps: [] }] } }
101
+ const prototype = {
102
+ name: 'base',
103
+ entity: { migrations: [{ id: '0001', steps: [] }] }
104
+ }
76
105
 
77
106
  collapse(manifest, prototype)
78
107
 
79
- assert.strictEqual(manifest.entity?.migrations, undefined)
108
+ assert.deepStrictEqual(
109
+ manifest.entity.migrations.map(({ id }) => id),
110
+ ['base:0001']
111
+ )
80
112
  })
81
113
  })
82
114
 
@@ -47,3 +47,12 @@ it('should set default platforms', () => {
47
47
  'linux/arm64'
48
48
  ])
49
49
  })
50
+
51
+ it('should keep platforms declared as none', () => {
52
+ // `platforms: ~` in the context: the runner's own platform, not the default list
53
+ context.registry = { base: generate(), platforms: null }
54
+
55
+ normalize(context)
56
+
57
+ assert.strictEqual(context.registry.platforms, null)
58
+ })
@@ -39,6 +39,8 @@ type Receiver = {
39
39
  type Migration = {
40
40
  id: string
41
41
  steps: unknown[]
42
+ /** the prototype it is inherited from; absent on the component's own */
43
+ prototype?: string
42
44
  }
43
45
 
44
46
  export type Entity = {