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

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,24 @@
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.312](https://github.com/toa-io/toa/compare/v1.0.0-alpha.311...v1.0.0-alpha.312) (2026-09-18)
7
+
8
+ ### Features
9
+
10
+ * **bindings.http:** carry a call that holds a stream ([c571793](https://github.com/toa-io/toa/commit/c571793b617e3d5ae1dce5b98b64d67d7d8224f6))
11
+ * **cli:** write the property that carries a stream into a component's types ([8eae8ce](https://github.com/toa-io/toa/commit/8eae8cea011c0be78b27ed1380a1a6f17c9479f8))
12
+ * **norm:** let an operation declare the input property that carries a stream ([85a8f1f](https://github.com/toa-io/toa/commit/85a8f1ff5b62f658e0527dcce7230d8388d28fe0))
13
+ * **operations:** refuse to render a chart for a context with no version ([d6cd083](https://github.com/toa-io/toa/commit/d6cd083bfe8d7da9f3d451c2c25d821eb41ce958))
14
+
15
+
16
+ # [1.0.0-alpha.311](https://github.com/toa-io/toa/compare/v1.0.0-alpha.310...v1.0.0-alpha.311) (2026-09-17)
17
+
18
+ **Note:** Version bump only for package @toa.io/norm
19
+
20
+
21
+
22
+
23
+
6
24
  # [1.0.0-alpha.310](https://github.com/toa-io/toa/compare/v1.0.0-alpha.309...v1.0.0-alpha.310) (2026-09-16)
7
25
 
8
26
  **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.310",
3
+ "version": "1.0.0-alpha.312",
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.310",
25
- "@toa.io/definitions": "1.0.0-alpha.310",
26
- "@toa.io/generic": "1.0.0-alpha.310",
24
+ "@toa.io/core": "1.0.0-alpha.312",
25
+ "@toa.io/definitions": "1.0.0-alpha.312",
26
+ "@toa.io/generic": "1.0.0-alpha.311",
27
27
  "@toa.io/prototype": "1.0.0-alpha.301",
28
- "@toa.io/schemas": "1.0.0-alpha.310",
28
+ "@toa.io/schemas": "1.0.0-alpha.311",
29
29
  "fast-glob": "3.3.3"
30
30
  },
31
- "gitHead": "d952c57f77279729e26d52d50c6df47138ad0a4f"
31
+ "gitHead": "3cdff9b9779eeb6ae83913fefc0b502a10a0236a"
32
32
  }
@@ -161,6 +161,9 @@ properties:
161
161
  stateful:
162
162
  # served by every process under an address of its own, which a call names; see documentation/stateful.md
163
163
  type: boolean
164
+ stream:
165
+ # the input property that carries a stream, read as its caller writes it; see documentation/streams.md
166
+ $ref: "#/definitions/name"
164
167
  forward:
165
168
  $ref: "#/definitions/name"
166
169
  bridge:
@@ -280,6 +283,13 @@ properties:
280
283
  properties:
281
284
  scope:
282
285
  enum: [object, objects, stream, none]
286
+ - if: # takes a stream
287
+ required: [stream]
288
+ then:
289
+ properties:
290
+ type:
291
+ # a transition may run its algorithm twice over one call, and a stream is read once
292
+ enum: [effect, computation, unmanaged]
283
293
  additionalProperties: false
284
294
  additionalProperties: false
285
295
 
@@ -16,6 +16,7 @@ export const validate = async (manifest) => {
16
16
  if (error) throw error
17
17
 
18
18
  if (manifest.entity !== undefined) entity(manifest)
19
+ if (manifest.operations !== undefined) await streams(manifest)
19
20
  if (manifest.events !== undefined) await events(manifest)
20
21
  if (manifest.receivers !== undefined) receivers(manifest)
21
22
  }
@@ -33,6 +34,27 @@ const entity = (manifest) => {
33
34
  throw new Error(`Entity blank names property '${name}', which is not defined`)
34
35
  }
35
36
 
37
+ /**
38
+ * An operation that takes a stream is reachable over a binding that carries one. A binding that
39
+ * declares none is served in this process alone, which is a component saying so.
40
+ */
41
+ const streams = async (manifest) => {
42
+ for (const [endpoint, operation] of Object.entries(manifest.operations)) {
43
+ if (operation.stream === undefined || operation.bindings.length === 0) continue
44
+ if (await carried(operation.bindings)) continue
45
+
46
+ throw new Error(`Operation '${endpoint}' takes a stream, which none of its bindings carries`)
47
+ }
48
+ }
49
+
50
+ /** Whether any of these bindings carries a call whose input holds a stream. */
51
+ const carried = async (bindings) => {
52
+ for (const binding of bindings)
53
+ if ((await definition(binding)).module.properties.streams === true) return true
54
+
55
+ return false
56
+ }
57
+
36
58
  const events = async (manifest) => {
37
59
  for (const [label, event] of Object.entries(manifest.events)) {
38
60
  const { properties } = (await definition(event.binding)).module
@@ -9,5 +9,8 @@ const object = yaml.load(readFileSync(path, 'utf8'))
9
9
  const schema = schemas.schema(object)
10
10
 
11
11
  export const validate = (context) => {
12
- schema.validate(context)
12
+ schema.validate(context, CONTEXT)
13
13
  }
14
+
15
+ /** The file a reader has to edit, since nothing else in the message names it. */
16
+ const CONTEXT = 'context.toa.yaml'
package/src/shortcuts.js CHANGED
@@ -38,6 +38,7 @@ export const recognize = (shortcuts, object, group) => {
38
38
 
39
39
  export const SHORTCUTS = {
40
40
  amqp: '@toa.io/bindings.amqp',
41
+ http: '@toa.io/bindings.http',
41
42
  node: '@toa.io/bridges.node',
42
43
  bash: '@toa.io/bridges.bash',
43
44
  mongodb: '@toa.io/storages.mongodb',
@@ -330,3 +330,42 @@ describe('stateful', () => {
330
330
  await assert.rejects(validate(manifest), (error) => /must be boolean/.test(error.message))
331
331
  })
332
332
  })
333
+
334
+ describe('stream', () => {
335
+ beforeEach(() => {
336
+ manifest.operations.read = {
337
+ type: 'computation',
338
+ scope: 'none',
339
+ query: false,
340
+ bridge: 'whatever',
341
+ bindings: ['@toa.io/bindings.amqp'],
342
+ stream: 'content'
343
+ }
344
+ })
345
+
346
+ it('should reject an operation whose bindings carry no stream', async () => {
347
+ await assert.rejects(validate(manifest), (error) =>
348
+ /takes a stream, which none of its bindings carries/.test(error.message)
349
+ )
350
+ })
351
+
352
+ it('should accept an operation served where it is composed', async () => {
353
+ manifest.operations.read.bindings = []
354
+
355
+ await assert.doesNotReject(validate(manifest))
356
+ })
357
+
358
+ it('should accept an operation bound to a binding that carries a stream', async () => {
359
+ manifest.operations.read.bindings = ['@toa.io/bindings.http']
360
+
361
+ await assert.doesNotReject(validate(manifest))
362
+ })
363
+
364
+ it('should reject a transition that takes a stream', async () => {
365
+ manifest.operations.add.stream = 'content'
366
+
367
+ await assert.rejects(validate(manifest), (error) =>
368
+ /must be equal to one of the allowed values/.test(error.message)
369
+ )
370
+ })
371
+ })
@@ -16,6 +16,8 @@ export type Operation = {
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
19
+ /** the input property that carries a stream, read as its caller writes it */
20
+ stream?: string
19
21
  }
20
22
 
21
23
  export type Operations = Record<string, Operation>