@toa.io/norm 1.0.0-alpha.325 → 1.0.0-alpha.327

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,27 @@
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.327](https://github.com/toa-io/toa/compare/v1.0.0-alpha.326...v1.0.0-alpha.327) (2026-10-10)
7
+
8
+ ### Features
9
+
10
+ * name a component without a namespace by its name ([abcdd9c](https://github.com/toa-io/toa/commit/abcdd9c9a3d3c5c1c68b74544791848e59a7e158))
11
+
12
+
13
+ # [1.0.0-alpha.326](https://github.com/toa-io/toa/compare/v1.0.0-alpha.325...v1.0.0-alpha.326) (2026-10-06)
14
+
15
+ * feat(core)!: deliver an operation's error as its code and its cause ([18d303b](https://github.com/toa-io/toa/commit/18d303b9143930735e4b093345db1dbf37f90052))
16
+
17
+ ### Bug Fixes
18
+
19
+ * **deps:** find files with tinyglobby, which carries no braces ([ef951af](https://github.com/toa-io/toa/commit/ef951af26b5f93fec567033520c62ca01f397355))
20
+
21
+ ### BREAKING CHANGES
22
+
23
+ * an error returned with a `code` field and no message is an exception,
24
+ and a property of an error other than `cause` no longer reaches the caller.
25
+
26
+
6
27
  # [1.0.0-alpha.325](https://github.com/toa-io/toa/compare/v1.0.0-alpha.324...v1.0.0-alpha.325) (2026-10-02)
7
28
 
8
29
  **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.325",
3
+ "version": "1.0.0-alpha.327",
4
4
  "type": "module",
5
5
  "description": "Toa declarations normalization and validation",
6
6
  "author": "temich <tema.gurtovoy@gmail.com>",
@@ -21,12 +21,12 @@
21
21
  "test": "echo \"Error: run tests from root\" && exit 1"
22
22
  },
23
23
  "dependencies": {
24
- "@toa.io/core": "1.0.0-alpha.323",
25
- "@toa.io/definitions": "1.0.0-alpha.325",
24
+ "@toa.io/core": "1.0.0-alpha.327",
25
+ "@toa.io/definitions": "1.0.0-alpha.327",
26
26
  "@toa.io/generic": "1.0.0-alpha.311",
27
- "@toa.io/prototype": "1.0.0-alpha.323",
28
- "@toa.io/schemas": "1.0.0-alpha.311",
29
- "fast-glob": "3.3.3"
27
+ "@toa.io/prototype": "1.0.0-alpha.327",
28
+ "@toa.io/schemas": "1.0.0-alpha.326",
29
+ "tinyglobby": "0.2.17"
30
30
  },
31
- "gitHead": "95bc03e65c65f02d536da6ed027cf4f78f5cec74"
31
+ "gitHead": "cd6d6d77f19fbae5d7ff85d18005ca8deb9c77f3"
32
32
  }
@@ -1,7 +1,7 @@
1
1
  import { join } from 'node:path'
2
2
  import { createHash } from 'node:crypto'
3
3
  import fs from 'node:fs/promises'
4
- import glob from 'fast-glob'
4
+ import { glob } from 'tinyglobby'
5
5
 
6
6
  import { definition } from '../../definition.js'
7
7
 
@@ -21,7 +21,9 @@ export async function version(manifest) {
21
21
  */
22
22
  async function hash(manifest) {
23
23
  const files = (await list(manifest)).sort()
24
- const digests = await Promise.all(files.map((file) => digest(join(manifest.path, file))))
24
+ const digests = await Promise.all(
25
+ files.map((file) => digest(join(manifest.path, file)))
26
+ )
25
27
  const total = createHash('sha256')
26
28
 
27
29
  // the path is part of it: moving a file changes the build even if no content did
@@ -39,6 +41,7 @@ async function list(manifest) {
39
41
  cwd: manifest.path,
40
42
  dot: true,
41
43
  onlyFiles: true,
44
+ expandDirectories: false,
42
45
  // sources a component keeps outside its own directory are its own: what a link points to is
43
46
  // hashed as the file it stands for, so changing it changes the version
44
47
  followSymbolicLinks: true,
@@ -1,7 +1,7 @@
1
1
  import { basename, extname, resolve } from 'node:path'
2
2
  import { readFile } from 'node:fs/promises'
3
3
 
4
- import glob from 'fast-glob'
4
+ import { glob } from 'tinyglobby'
5
5
  import { yaml } from '@toa.io/generic'
6
6
 
7
7
  /**
@@ -54,4 +54,4 @@ async function read(path) {
54
54
 
55
55
  const DIRECTORY = 'migrations'
56
56
  const EXTENSIONS = '.{yaml,yml,json}'
57
- const GLOB = { onlyFiles: true, absolute: true }
57
+ const GLOB = { onlyFiles: true, absolute: true, expandDirectories: false }
@@ -186,10 +186,10 @@ properties:
186
186
  output:
187
187
  $ref: https://json-schema.org/draft/2019-09/schema
188
188
  errors:
189
- # what a reply may carry as a code: the error schema allows either
189
+ # what a reply may carry as a code: the message of the error returned
190
190
  type: array
191
191
  items:
192
- type: [string, integer]
192
+ type: string
193
193
  query:
194
194
  type: boolean
195
195
  required: [type, scope, bindings]
@@ -49,7 +49,9 @@ const streams = async (manifest) => {
49
49
  if (operation.stream === undefined || operation.bindings.length === 0) continue
50
50
  if (await carried(operation.bindings)) continue
51
51
 
52
- throw new Error(`Operation '${endpoint}' takes a stream, which none of its bindings carries`)
52
+ throw new Error(
53
+ `Operation '${endpoint}' takes a stream, which none of its bindings carries`
54
+ )
53
55
  }
54
56
  }
55
57
 
@@ -65,7 +65,12 @@ async function extractDeclaredServices(context, extensions, contributed) {
65
65
  return extracted
66
66
  }
67
67
 
68
- async function extractExtensionComponents(components, extensions, contributed, annotations) {
68
+ async function extractExtensionComponents(
69
+ components,
70
+ extensions,
71
+ contributed,
72
+ annotations
73
+ ) {
69
74
  const extracted = []
70
75
 
71
76
  for (const component of components) {
@@ -46,5 +46,6 @@ function unmanaged(context, contributed) {
46
46
  }
47
47
 
48
48
  for (const brought of contributed.values())
49
- for (const component of brought) if (!required.has(component)) component.evicted = true
49
+ for (const component of brought)
50
+ if (!required.has(component)) component.evicted = true
50
51
  }
@@ -31,7 +31,9 @@ export const evict = (context) => {
31
31
  for (const composition of context.compositions) {
32
32
  if (composition.services === undefined) continue
33
33
 
34
- composition.services = composition.services.filter((reference) => !services.has(reference))
34
+ composition.services = composition.services.filter(
35
+ (reference) => !services.has(reference)
36
+ )
35
37
 
36
38
  if (composition.services.length === 0) delete composition.services
37
39
  }
@@ -15,4 +15,31 @@ export const expand = (context) => {
15
15
 
16
16
  if (context.evicted?.services !== undefined)
17
17
  context.evicted.services = context.evicted.services.map(shortcuts.resolve)
18
+
19
+ // a component that declares no namespace is written by its name, and is known by its id
20
+ for (const composition of context.compositions ?? [])
21
+ if (Array.isArray(composition.components))
22
+ composition.components = composition.components.map((id) => complete(id, 2))
23
+
24
+ if (Array.isArray(context.evicted?.components))
25
+ context.evicted.components = context.evicted.components.map((id) => complete(id, 2))
26
+
27
+ if (Array.isArray(context.events))
28
+ context.events = context.events.map((event) => complete(event, 3))
29
+ }
30
+
31
+ /**
32
+ * `name` is `default.name` where an id has two segments, `name.event` is `default.name.event`
33
+ * where it has three. Anything else is left for the schema to refuse as it is written.
34
+ *
35
+ * @param {unknown} id
36
+ * @param {number} segments how many a complete one has
37
+ */
38
+ function complete(id, segments) {
39
+ if (typeof id !== 'string' || id.split('.').length !== segments - 1) return id
40
+
41
+ return `${NAMESPACE}.${id}`
18
42
  }
43
+
44
+ /** Where a component that declares no namespace is. */
45
+ const NAMESPACE = 'default'
package/src/context.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { resolve } from 'node:path'
2
2
  import { convolve, environment as variables } from '@toa.io/generic'
3
- import glob from 'fast-glob'
3
+ import { glob } from 'tinyglobby'
4
4
  import { readFile } from 'node:fs/promises'
5
5
  import { yaml as jsyaml } from '@toa.io/generic'
6
6
 
@@ -37,7 +37,10 @@ export const context = async (
37
37
 
38
38
  validate(context)
39
39
 
40
- const paths = await glob(resolve(root, COMPONENTS), GLOB)
40
+ // a directory is answered with the separator that ends it, and a component's path has none
41
+ const paths = (await glob(resolve(root, COMPONENTS), GLOB)).map((path) =>
42
+ path.slice(0, -1)
43
+ )
41
44
 
42
45
  context.components = await Promise.all(paths.map(component))
43
46
 
@@ -58,7 +61,7 @@ export const context = async (
58
61
  const CONTEXT = 'context.toa.yaml'
59
62
  const COMPONENTS = 'components/*'
60
63
 
61
- const GLOB = { onlyDirectories: true, absolute: true }
64
+ const GLOB = { onlyDirectories: true, absolute: true, expandDirectories: false }
62
65
 
63
66
  /**
64
67
  * Reads a YAML file, resolving anchors into distinct objects so that
package/src/manifest.js CHANGED
@@ -58,7 +58,11 @@ function walk(manifest, rewrite) {
58
58
  for (const item of Object.values(manifest[property] ?? {}))
59
59
  if (item.path !== undefined) rewrite(item)
60
60
 
61
- for (let prototype = manifest.prototype; prototype != null; prototype = prototype.prototype)
61
+ for (
62
+ let prototype = manifest.prototype;
63
+ prototype != null;
64
+ prototype = prototype.prototype
65
+ )
62
66
  if (prototype.path !== undefined) rewrite(prototype)
63
67
  }
64
68
 
@@ -73,7 +77,8 @@ const SELF = '.'
73
77
 
74
78
  function carried(path, root) {
75
79
  if (path === root) return SELF
76
- if (path.startsWith(root + sep)) return SELF + '/' + relative(root, path).split(sep).join('/')
80
+ if (path.startsWith(root + sep))
81
+ return SELF + '/' + relative(root, path).split(sep).join('/')
77
82
 
78
83
  return specifier(path)
79
84
  }
package/src/map.js CHANGED
@@ -20,5 +20,7 @@ export const map = (context) => {
20
20
  for (const { component } of instances)
21
21
  components.set(component.locator.id, contract.component(component))
22
22
 
23
- return Object.fromEntries([...components].sort(([one], [other]) => one.localeCompare(other)))
23
+ return Object.fromEntries(
24
+ [...components].sort(([one], [other]) => one.localeCompare(other))
25
+ )
24
26
  }
@@ -277,7 +277,9 @@ describe('operations', () => {
277
277
  it('should refuse the names the scopes had before `entry` and `entries`', async () => {
278
278
  for (const scope of ['object', 'objects']) {
279
279
  manifest.operations.get.scope = scope
280
- await assert.rejects(validate(manifest), (error) => /allowed values/.test(error.message))
280
+ await assert.rejects(validate(manifest), (error) =>
281
+ /allowed values/.test(error.message)
282
+ )
281
283
  }
282
284
  })
283
285
 
@@ -309,7 +311,9 @@ describe('operations', () => {
309
311
 
310
312
  it('should not be a window shorter than ten minutes', async () => {
311
313
  manifest.operations.add.once = 599
312
- await assert.rejects(validate(manifest), (error) => /must be >= 600/.test(error.message))
314
+ await assert.rejects(validate(manifest), (error) =>
315
+ /must be >= 600/.test(error.message)
316
+ )
313
317
  })
314
318
 
315
319
  it('should not be a fraction of a second', async () => {
@@ -393,7 +397,9 @@ describe('stateful', () => {
393
397
 
394
398
  operation.stateful = 'yes'
395
399
 
396
- await assert.rejects(validate(manifest), (error) => /must be boolean/.test(error.message))
400
+ await assert.rejects(validate(manifest), (error) =>
401
+ /must be boolean/.test(error.message)
402
+ )
397
403
  })
398
404
  })
399
405
 
@@ -411,7 +417,9 @@ describe('unchained', () => {
411
417
 
412
418
  operation.unchained = 'yes'
413
419
 
414
- await assert.rejects(validate(manifest), (error) => /must be boolean/.test(error.message))
420
+ await assert.rejects(validate(manifest), (error) =>
421
+ /must be boolean/.test(error.message)
422
+ )
415
423
  })
416
424
  })
417
425
 
@@ -35,7 +35,12 @@ describe('a manifest carried through a file', () => {
35
35
  })
36
36
 
37
37
  it('does not carry a key with no value', () => {
38
- const carried = plain({ name: 'a', namespace: 'b', path: '/x', operations: { echo: { input: undefined } } })
38
+ const carried = plain({
39
+ name: 'a',
40
+ namespace: 'b',
41
+ path: '/x',
42
+ operations: { echo: { input: undefined } }
43
+ })
39
44
 
40
45
  assert.deepStrictEqual(carried.operations.echo, {})
41
46
  })
@@ -64,7 +69,10 @@ describe('a manifest carried through a file', () => {
64
69
 
65
70
  it('reads what is inherited where the prototype is', async () => {
66
71
  const prototype = (await component(COMPONENT)).prototype.path
67
- const revived = revive(carry(await component(COMPONENT)), '/composition/external-consumer')
72
+ const revived = revive(
73
+ carry(await component(COMPONENT)),
74
+ '/composition/external-consumer'
75
+ )
68
76
 
69
77
  // an event of the prototype's is in the prototype, wherever the component is
70
78
  assert.strictEqual(revived.events.created.path, prototype)
@@ -95,7 +103,10 @@ describe('what cannot be carried', () => {
95
103
  it('refuses a prototype an image would not have', async () => {
96
104
  const manifest = await component(EXTENDED)
97
105
 
98
- assert.throws(() => plain(manifest), /is in no package, so an image cannot carry what is in it/)
106
+ assert.throws(
107
+ () => plain(manifest),
108
+ /is in no package, so an image cannot carry what is in it/
109
+ )
99
110
  })
100
111
 
101
112
  it('refuses a path inside the component it does not know about', () => {
@@ -113,7 +124,7 @@ describe('what cannot be carried', () => {
113
124
  assert.throws(() => plain(manifest), /carries a path of the machine that read it/)
114
125
  })
115
126
 
116
- it('leaves a value of the application\'s own alone', () => {
127
+ it("leaves a value of the application's own alone", () => {
117
128
  const carried = plain({
118
129
  name: 'a',
119
130
  namespace: 'b',
@@ -12,7 +12,7 @@ export type Operation = {
12
12
  description?: string
13
13
  input?: any
14
14
  output?: any
15
- error?: any
15
+ errors?: string[]
16
16
  query?: boolean
17
17
  /** served by every process under an address of its own, which a call to it names */
18
18
  stateful?: boolean