@toa.io/core 1.0.0-alpha.27 → 1.0.0-alpha.272

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 (71) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/package.json +7 -9
  3. package/src/assignment.js +3 -2
  4. package/src/call.js +21 -1
  5. package/src/cascade.js +7 -5
  6. package/src/component.js +59 -15
  7. package/src/composition.js +1 -1
  8. package/src/connector.js +28 -17
  9. package/src/context.js +6 -0
  10. package/src/contract/contract.js +22 -0
  11. package/src/contract/reply.js +34 -9
  12. package/src/contract/request.js +19 -5
  13. package/src/contract/schemas/index.js +1 -0
  14. package/src/contract/schemas/query.yaml +14 -1
  15. package/src/contract/schemas/source.yaml +23 -0
  16. package/src/discovery.js +13 -7
  17. package/src/effect.js +19 -0
  18. package/src/entities/changeset.js +5 -8
  19. package/src/entities/entity.js +74 -24
  20. package/src/entities/factory.js +17 -6
  21. package/src/entities/newid.js +11 -0
  22. package/src/entities/set.js +13 -0
  23. package/src/event.js +19 -1
  24. package/src/exceptions.js +26 -19
  25. package/src/exposition.js +3 -2
  26. package/src/guard.js +17 -0
  27. package/src/index.js +8 -0
  28. package/src/observation.js +1 -9
  29. package/src/operation.js +38 -8
  30. package/src/outbox/index.js +6 -0
  31. package/src/outbox/outbox.js +301 -0
  32. package/src/query/options.js +3 -2
  33. package/src/query.js +32 -2
  34. package/src/receiver.js +73 -11
  35. package/src/remote.js +8 -7
  36. package/src/state.js +90 -47
  37. package/src/transition.js +11 -19
  38. package/src/transmission.js +12 -3
  39. package/src/unmanaged.js +11 -0
  40. package/test/component.test.js +4 -3
  41. package/test/connector.fixtures.js +8 -0
  42. package/test/connector.test.js +20 -0
  43. package/test/contract/conditions.test.js +5 -5
  44. package/test/contract/request.test.js +46 -7
  45. package/test/discovery.test.js +56 -0
  46. package/test/emission.fixtures.js +1 -2
  47. package/test/entities/entity.test.js +58 -48
  48. package/test/entities/factory.test.js +5 -3
  49. package/test/event.test.js +4 -4
  50. package/test/outbox.test.js +114 -0
  51. package/test/query.test.js +17 -0
  52. package/test/receiver.fixtures.js +1 -0
  53. package/test/state.fixtures.js +11 -8
  54. package/test/state.test.js +61 -13
  55. package/types/atomicity.d.ts +58 -0
  56. package/types/bindings.d.ts +7 -5
  57. package/types/bridges.ts +3 -0
  58. package/types/component.d.ts +4 -1
  59. package/types/context.d.ts +3 -0
  60. package/types/entity.d.ts +2 -2
  61. package/types/extensions.d.ts +7 -4
  62. package/types/index.ts +4 -1
  63. package/types/message.d.ts +1 -0
  64. package/types/operations.d.ts +6 -0
  65. package/types/outbox.d.ts +53 -0
  66. package/types/query.d.ts +2 -0
  67. package/types/remote.d.ts +18 -0
  68. package/types/request.d.ts +15 -0
  69. package/types/state.d.ts +9 -6
  70. package/types/storages.d.ts +34 -9
  71. package/src/contract/conditions.js +0 -21
package/CHANGELOG.md ADDED
@@ -0,0 +1,137 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ # [1.0.0-alpha.272](https://github.com/toa-io/toa/compare/v1.0.0-alpha.271...v1.0.0-alpha.272) (2026-09-01)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **core:** stop the reply contract from relaxing the declaration ([6222893](https://github.com/toa-io/toa/commit/622289326630b94deff93499d6a851bfaa57c69a))
12
+
13
+
14
+ * feat(atomicity)!: take a quorum of independent servers ([862562f](https://github.com/toa-io/toa/commit/862562f24d77ec00127dbbd88d43802b208c643b))
15
+ * refactor(atomicity)!: take the lock manager from the stash ([4fd91eb](https://github.com/toa-io/toa/commit/4fd91eb2fdb68f7b87aaf16d182794bbc567ba57))
16
+ * refactor(core)!: pump the outbox in one cycle ([bf590fe](https://github.com/toa-io/toa/commit/bf590fe8ed59c701b5fd1a91ba4874685b79242f))
17
+ * refactor(atomicity)!: make the connector a family, not a partitioner ([06fd63a](https://github.com/toa-io/toa/commit/06fd63ad6296b724ef83afccf4e4e25d0bcaf080))
18
+ * refactor(atomicity)!: rename the connector and free it of the outbox ([21f1a41](https://github.com/toa-io/toa/commit/21f1a41aceafcfd35c7620014062fe46b54afa95))
19
+ * feat(core)!: commit events with the state that produced them ([1eb68cc](https://github.com/toa-io/toa/commit/1eb68cc435dbfa03faa16009fceb866693d22e1a)), closes [#20](https://github.com/toa-io/toa/issues/20)
20
+
21
+
22
+ ### Features
23
+
24
+ * **atomicity:** meter what the group has spent ([bb1118a](https://github.com/toa-io/toa/commit/bb1118aa60bfeb43d3212f2495b7797445f003c3))
25
+ * **core:** give every component an atom aspect ([b501b9c](https://github.com/toa-io/toa/commit/b501b9cd3d5f5408d9faa71213e953fe2792cf9f))
26
+
27
+
28
+ ### BREAKING CHANGES
29
+
30
+ * `atomicity.redis` accepts a list again, of independent servers rather
31
+ than cluster nodes, and refuses an even number of them.
32
+
33
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
34
+ * `context.stash.lock` is gone; lock through `context.atom` instead.
35
+ A stash pointer resolving to several addresses now uses the first.
36
+
37
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
38
+ * `Storage.outbox.pending` takes a fourth argument, the id to
39
+ continue from.
40
+
41
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
42
+ * `Factory.partition(group)` is `Factory.atom(group)`, and the
43
+ `Partition` it returned is an `Atom`. `slots(total)` is unchanged.
44
+
45
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
46
+ * `@toa.io/partitions.redis` is now `@toa.io/atomicity`, and its
47
+ `lanes(total)` is `slots(total)`. Redis is declared as `atomicity` in the context
48
+ rather than `outbox.redis`, and read from `TOA_ATOMICITY_REDIS`.
49
+ `TOA_OUTBOX_PARTITION_INTERVAL` is `TOA_ATOMICITY_INTERVAL`.
50
+
51
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
52
+ * `event.changeset` is removed; use `origin` and `state`. `State`
53
+ takes an `Outbox` in place of an `Emission`. `difference` is dropped from
54
+ `@toa.io/generic` along with its last caller.
55
+
56
+ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
57
+
58
+
59
+
60
+
61
+
62
+ # [1.0.0-alpha.270](https://github.com/toa-io/toa/compare/v1.0.0-alpha.269...v1.0.0-alpha.270) (2026-08-31)
63
+
64
+
65
+ ### Bug Fixes
66
+
67
+ * **core:** apply the input default on a remote call ([341699b](https://github.com/toa-io/toa/commit/341699b26c9299963a898368671633a9b3fb97ed))
68
+
69
+
70
+ ### Performance Improvements
71
+
72
+ * **core:** do not snapshot a record an operation cannot commit ([a5e695b](https://github.com/toa-io/toa/commit/a5e695b0b1d4b00a91e40336deecd476c9646c89))
73
+ * **core:** keep the parsed criteria of a query ([7efc694](https://github.com/toa-io/toa/commit/7efc694814c2a071ae7e48dbf1c6014db6025a9f))
74
+ * stop rebuilding per-call values that never change ([4202504](https://github.com/toa-io/toa/commit/4202504c33fb9ef9694f9995f3d0397d3a186438))
75
+
76
+
77
+
78
+
79
+
80
+ # [1.0.0-alpha.266](https://github.com/toa-io/toa/compare/v1.0.0-alpha.265...v1.0.0-alpha.266) (2026-08-29)
81
+
82
+
83
+ ### Bug Fixes
84
+
85
+ * **core:** say a connector is disconnected once it has closed, not before ([d67b48e](https://github.com/toa-io/toa/commit/d67b48eaafaacd5c635d8b066413f84b8e6a3771))
86
+
87
+
88
+
89
+
90
+
91
+ # [1.0.0-alpha.265](https://github.com/toa-io/toa/compare/v1.0.0-alpha.264...v1.0.0-alpha.265) (2026-08-29)
92
+
93
+
94
+ ### Features
95
+
96
+ * **bridges.node:** add the dispose run command phase ([00df715](https://github.com/toa-io/toa/commit/00df7158f638b976ab09d4007102a4ead3c696c3))
97
+
98
+
99
+
100
+
101
+
102
+ # [1.0.0-alpha.264](https://github.com/toa-io/toa/compare/v1.0.0-alpha.263...v1.0.0-alpha.264) (2026-08-29)
103
+
104
+ **Note:** Version bump only for package @toa.io/core
105
+
106
+
107
+
108
+
109
+
110
+ # [1.0.0-alpha.262](https://github.com/toa-io/toa/compare/v1.0.0-alpha.261...v1.0.0-alpha.262) (2026-08-28)
111
+
112
+
113
+ ### Features
114
+
115
+ * **core:** identify the origin of a call with `request.source` ([9da6b66](https://github.com/toa-io/toa/commit/9da6b66df852690351a1e82c7b8cc176fd89774a))
116
+
117
+
118
+
119
+
120
+
121
+ # [1.0.0-alpha.259](https://github.com/toa-io/toa/compare/v1.0.0-alpha.258...v1.0.0-alpha.259) (2026-08-24)
122
+
123
+
124
+ ### Features
125
+
126
+ * **runtime:** split component RC into preflight and settle ([0d648b3](https://github.com/toa-io/toa/commit/0d648b314e4aac226c254bf01a86affa04b59b34))
127
+
128
+
129
+
130
+
131
+
132
+ # [1.0.0-alpha.257](https://github.com/toa-io/toa/compare/v1.0.0-alpha.256...v1.0.0-alpha.257) (2026-08-24)
133
+
134
+
135
+ ### Bug Fixes
136
+
137
+ * **core:** lift tombstone on commit when transition leaves it untouched ([38c0d15](https://github.com/toa-io/toa/commit/38c0d15e946c3e2d559d4b18aecc33e0fda2471a))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/core",
3
- "version": "1.0.0-alpha.27",
3
+ "version": "1.0.0-alpha.272",
4
4
  "description": "Toa Core",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -21,13 +21,11 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@rsql/parser": "1.2.4",
24
- "@toa.io/console": "1.0.0-alpha.27",
25
- "@toa.io/generic": "1.0.0-alpha.27",
26
- "@toa.io/yaml": "1.0.0-alpha.27",
27
- "error-value": "0.3.0"
24
+ "@toa.io/generic": "1.0.0-alpha.272",
25
+ "@toa.io/yaml": "1.0.0-alpha.272",
26
+ "error-value": "0.3.0",
27
+ "openspan": "1.0.0-alpha.272",
28
+ "uuid": "11.1.1"
28
29
  },
29
- "devDependencies": {
30
- "clone-deep": "4.0.1"
31
- },
32
- "gitHead": "9bc4bdb919688c5272791020746f70d51a520750"
30
+ "gitHead": "1a451755445935c04cf5b373bcc73942cac0bb10"
33
31
  }
package/src/assignment.js CHANGED
@@ -12,14 +12,15 @@ class Assignment extends Operation {
12
12
  const {
13
13
  scope,
14
14
  state,
15
- reply
15
+ reply,
16
+ request
16
17
  } = store
17
18
 
18
19
  if (reply.error !== undefined) return
19
20
 
20
21
  scope.set(state)
21
22
 
22
- const output = await this.scope.apply(scope)
23
+ const output = await this.scope.apply(scope, request.input)
23
24
 
24
25
  // assignment returns new state by default
25
26
  if (store.reply.output === undefined) {
package/src/call.js CHANGED
@@ -1,28 +1,44 @@
1
1
  'use strict'
2
2
 
3
3
  const { Readable } = require('node:stream')
4
+ const { current, encode } = require('openspan')
4
5
  const { Connector } = require('./connector')
5
6
  const { Err } = require('error-value')
6
7
 
7
8
  class Call extends Connector {
8
9
  #transmitter
9
10
  #contract
11
+ #source
10
12
 
11
- constructor (transmitter, contract) {
13
+ constructor (transmitter, contract, source) {
12
14
  super()
13
15
 
14
16
  this.#transmitter = transmitter
15
17
  this.#contract = contract
18
+ this.#source = source
16
19
 
17
20
  this.depends(transmitter)
18
21
  }
19
22
 
20
23
  async invoke (request = {}) {
24
+ // the caller may have attributed the call itself, as the node bridge does
25
+ if (this.#source !== undefined)
26
+ request.source ??= this.#source
27
+
28
+ // fitting first lets the input schema supply its default;
29
+ // an operation that takes no input still has to send an explicit null
21
30
  this.#contract.fit(request)
22
31
 
32
+ request.input ??= null
33
+
23
34
  // avoid validation on the recipient's side
24
35
  request.authentic = true
25
36
 
37
+ const context = current()
38
+
39
+ if (context !== undefined)
40
+ request.telemetry = encode(context)
41
+
26
42
  const reply = await this.#transmitter.request(request)
27
43
 
28
44
  if (reply === null) return null
@@ -37,6 +53,10 @@ class Call extends Connector {
37
53
  return reply.output
38
54
  }
39
55
  }
56
+
57
+ explain () {
58
+ return this.#contract.discovery
59
+ }
40
60
  }
41
61
 
42
62
  exports.Call = Call
package/src/cascade.js CHANGED
@@ -1,19 +1,21 @@
1
1
  'use strict'
2
2
 
3
- const { merge } = require('@toa.io/generic')
4
3
  const { Connector } = require('./connector')
5
4
 
6
5
  class Cascade extends Connector {
7
- #bridges
6
+ // #bridges
8
7
  #last
9
8
 
10
- constructor (bridges) {
9
+ constructor (bridges, preflight) {
11
10
  super()
12
11
 
13
- this.#bridges = bridges
12
+ // this.#bridges = bridges
14
13
  this.#last = bridges[bridges.length - 1]
15
14
 
16
- this.depends(bridges)
15
+ if (preflight === undefined)
16
+ this.depends(bridges)
17
+ else
18
+ this.depends(bridges).depends(preflight)
17
19
  }
18
20
 
19
21
  async run (...args) {
package/src/component.js CHANGED
@@ -1,40 +1,84 @@
1
1
  'use strict'
2
2
 
3
- const { console } = require('@toa.io/console')
3
+ const assert = require('node:assert')
4
+ const { console, current, decode, run } = require('openspan')
4
5
  const { Connector } = require('./connector')
5
- const { NotImplementedException } = require('./exceptions')
6
6
 
7
- /**
8
- * @implements {toa.core.Component}
9
- */
10
7
  class Component extends Connector {
11
8
  locator
12
9
 
13
- #operations
10
+ /** @protected */
11
+ operations
12
+
13
+ /** @protected */
14
+ kind = 'server'
15
+
16
+ /** @type {Record<string, object>} span options per endpoint */
17
+ #spans = {}
14
18
 
15
19
  constructor (locator, operations) {
16
20
  super()
17
21
 
18
22
  this.locator = locator
19
- this.#operations = operations
23
+ this.operations = operations
20
24
 
21
25
  Object.values(operations).forEach((operation) => this.depends(operation))
22
26
  }
23
27
 
24
- async open () {
25
- console.info(`Runtime '${this.locator.id}' connected`)
28
+ async invoke (endpoint, request) {
29
+ if (!(endpoint in this.operations))
30
+ // `assert.fail`, not `assert.ok`: the message is built only when it is needed
31
+ assert.fail(`Endpoint '${endpoint}' is not provided by '${this.locator.id}'`)
32
+
33
+ // if the request carries no telemetry, the trace starts here
34
+ const remote = request?.telemetry === undefined ? null : decode(request.telemetry)
35
+ const task = () => this.process(endpoint, request)
36
+
37
+ if (remote === null)
38
+ return task()
39
+ else
40
+ return run(remote, task)
26
41
  }
27
42
 
28
- async dispose () {
29
- console.info(`Runtime '${this.locator.id}' disconnected`)
43
+ /** @private */
44
+ async process (endpoint, request) {
45
+ return console.span(this.span(endpoint), async () => {
46
+ const reply = await this.operations[endpoint].invoke(request)
47
+
48
+ if (reply?.exception !== undefined) {
49
+ current().status = 'error'
50
+
51
+ console.error('Failed to execute operation', {
52
+ endpoint: `${this.locator.id}.${endpoint}`,
53
+ exception: reply.exception
54
+ })
55
+ }
56
+
57
+ return reply
58
+ })
30
59
  }
31
60
 
32
- async invoke (endpoint, request) {
33
- if (!(endpoint in this.#operations)) {
34
- throw new NotImplementedException(`Endpoint '${endpoint}' not found in '${this.locator.id}'`)
61
+ /**
62
+ * The span of an endpoint never changes, so it is built once. Not in the constructor:
63
+ * `kind` is a field of the subclass, and those are assigned after this one is built.
64
+ *
65
+ * @private
66
+ */
67
+ span (endpoint) {
68
+ let options = this.#spans[endpoint]
69
+
70
+ if (options === undefined) {
71
+ options = { name: `${this.locator.id}.${endpoint}`, kind: this.kind }
72
+
73
+ // the server span is emitted by the component itself, while the client span
74
+ // belongs to the calling service and inherits it from the context
75
+ if (this.kind === 'server')
76
+ options.service = this.locator.id
77
+
78
+ this.#spans[endpoint] = options
35
79
  }
36
80
 
37
- return this.#operations[endpoint].invoke(request)
81
+ return options
38
82
  }
39
83
  }
40
84
 
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { console } = require('@toa.io/console')
3
+ const { console } = require('openspan')
4
4
  const { Connector } = require('./connector')
5
5
 
6
6
  class Composition extends Connector {
package/src/connector.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { console } = require('@toa.io/console')
4
- const { newid } = require('@toa.io/generic')
3
+ const { console } = require('openspan')
5
4
 
6
5
  /**
7
6
  * Abstract connections hierarchy
@@ -26,7 +25,7 @@ class Connector {
26
25
  connected = false
27
26
 
28
27
  constructor () {
29
- this.id = this.constructor.name + '#' + newid().substring(0, 8)
28
+ this.id = this.constructor.name + '#' + Math.random().toString(36).substring(2, 8)
30
29
  }
31
30
 
32
31
  /**
@@ -86,12 +85,18 @@ class Connector {
86
85
 
87
86
  this.#disconnecting = undefined
88
87
 
89
- console.debug(`Connecting '${this.id}' with ${this.#dependencies.length} dependencies...`)
90
-
91
- this.#connecting = (async () => {
88
+ const work = async () => {
92
89
  await Promise.all(this.#dependencies.map((connector) => connector.connect()))
93
90
  await this.open()
94
- })()
91
+ }
92
+
93
+ // anonymous grouping nodes are not worth a span
94
+ this.#connecting = TRACE_BOOT && this.constructor.name !== 'Connector'
95
+ ? console.span({
96
+ name: `connect ${this.constructor.name}`,
97
+ attributes: { id: this.locator?.id ?? this.id }
98
+ }, work)
99
+ : work()
95
100
 
96
101
  try {
97
102
  await this.#connecting
@@ -100,8 +105,6 @@ class Connector {
100
105
  await this.disconnect(true)
101
106
  throw e
102
107
  }
103
-
104
- console.debug(`Connector '${this.id}' connected`)
105
108
  }
106
109
 
107
110
  /**
@@ -115,7 +118,11 @@ class Connector {
115
118
  * @returns {Promise<void>}
116
119
  */
117
120
  async disconnect (interrupt) {
118
- if (interrupt !== true) await this.#connecting
121
+ // a connector that has not finished connecting has nothing to close, and
122
+ // awaiting a connection that may never settle outlives any grace period
123
+ const pending = interrupt === true || this.connected === false
124
+
125
+ if (!pending) await this.#connecting
119
126
 
120
127
  if (this.#disconnecting) return this.#disconnecting
121
128
 
@@ -123,19 +130,24 @@ class Connector {
123
130
 
124
131
  if (linked && interrupt !== true) return
125
132
 
126
- this.connected = false
127
- this.#connecting = undefined
128
-
129
133
  this.#disconnecting = (async () => {
130
134
  const start = +new Date()
131
135
 
132
136
  const interval = setInterval(() => {
133
137
  const delay = +new Date() - start
134
138
 
135
- if (delay > DELAY) console.warn(`Connector ${this.id} still disconnecting (${delay})`)
139
+ if (delay > DELAY)
140
+ console.warn(`Connector ${this.id} still disconnecting (${delay})`)
136
141
  }, DELAY)
137
142
 
138
- if (interrupt !== true) await this.close()
143
+ if (!pending) await this.close()
144
+
145
+ // said once the closing is done rather than when it starts: a dependant that is
146
+ // still closing is still using what it depends on, and the check above reads this
147
+ // of every dependant. Saying it early lets the first to start tear down a shared
148
+ // dependency under the others.
149
+ this.connected = false
150
+ this.#connecting = undefined
139
151
 
140
152
  clearInterval(interval)
141
153
 
@@ -145,8 +157,6 @@ class Connector {
145
157
  })()
146
158
 
147
159
  await this.#disconnecting
148
-
149
- console.debug(`Connector '${this.id}' disconnected`)
150
160
  }
151
161
 
152
162
  async reconnect () {
@@ -179,5 +189,6 @@ class Connector {
179
189
  }
180
190
 
181
191
  const DELAY = 5000
192
+ const TRACE_BOOT = process.env.TOA_BOOT_TRACE === '1'
182
193
 
183
194
  exports.Connector = Connector
package/src/context.js CHANGED
@@ -6,7 +6,10 @@ const { Connector } = require('./connector')
6
6
  * @implements {toa.core.Context}
7
7
  */
8
8
  class Context extends Connector {
9
+ env
10
+ name
9
11
  aspects
12
+ locator
10
13
 
11
14
  #local
12
15
  #discover
@@ -15,7 +18,10 @@ class Context extends Connector {
15
18
  constructor (local, discover, aspects = []) {
16
19
  super()
17
20
 
21
+ this.env = process.env.TOA_ENV
22
+ this.name = process.env.TOA_CONTEXT
18
23
  this.aspects = aspects
24
+ this.locator = local?.locator
19
25
 
20
26
  this.#local = local
21
27
  this.#discover = discover
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ const { SystemException } = require('../exceptions')
4
+
5
+ class Contract {
6
+ schema
7
+
8
+ constructor (schema) {
9
+ this.schema = schema
10
+ }
11
+
12
+ fit (value) {
13
+ const error = this.schema.fit(value)
14
+
15
+ if (error !== null)
16
+ throw new this.constructor.Exception(error, value)
17
+ }
18
+
19
+ static Exception = SystemException
20
+ }
21
+
22
+ exports.Contract = Contract
@@ -1,22 +1,47 @@
1
1
  'use strict'
2
2
 
3
3
  const schemas = require('./schemas')
4
- const { Conditions } = require('./conditions')
4
+ const { Contract } = require('./contract')
5
5
  const { ResponseContractException } = require('../exceptions')
6
6
 
7
- class Reply extends Conditions {
7
+ class Reply extends Contract {
8
8
  static Exception = ResponseContractException
9
9
 
10
- /**
11
- * @returns {toa.schema.JSON}
12
- */
13
- static schema (output, error) {
10
+ static schema (output, errors) {
14
11
  const schema = { type: 'object', properties: {}, additionalProperties: false }
15
12
 
16
- if (output !== undefined) schema.properties.output = output
13
+ if (output !== undefined) {
14
+ /*
15
+ * A reply carries more than the operation declares — `_version` and the rest of the
16
+ * record's own fields — so what it is validated against is the declaration relaxed.
17
+ * On a copy: the declaration itself is what a component publishes when asked to
18
+ * explain, and relaxing that would publish a contract nobody wrote.
19
+ */
20
+ output = structuredClone(output)
17
21
 
18
- if (error !== undefined) schema.properties.error = error
19
- else schema.properties.error = schemas.error
22
+ if (output.type === 'object')
23
+ output.additionalProperties = true
24
+ else if (output.type === 'array' && output.items?.type === 'object')
25
+ output.items.additionalProperties = true
26
+
27
+ schema.properties.output = output
28
+ }
29
+
30
+ if (errors !== undefined)
31
+ schema.properties.error = {
32
+ type: 'object',
33
+ properties: {
34
+ code: {
35
+ enum: errors
36
+ },
37
+ message: {
38
+ type: 'string'
39
+ }
40
+ },
41
+ required: ['code']
42
+ }
43
+ else
44
+ schema.properties.error = schemas.error
20
45
 
21
46
  return schema
22
47
  }
@@ -2,9 +2,20 @@
2
2
 
3
3
  const schemas = require('./schemas')
4
4
  const { RequestContractException } = require('../exceptions')
5
- const { Conditions } = require('./conditions')
5
+ const { Contract } = require('./contract')
6
+
7
+ class Request extends Contract {
8
+ /** @readonly */
9
+ discovery = {}
10
+
11
+ constructor (schema, definition) {
12
+ super(schema)
13
+
14
+ for (const key of ['input', 'output', 'errors'])
15
+ if (definition[key] !== undefined)
16
+ this.discovery[key] = definition[key]
17
+ }
6
18
 
7
- class Request extends Conditions {
8
19
  static Exception = RequestContractException
9
20
 
10
21
  /**
@@ -13,7 +24,11 @@ class Request extends Conditions {
13
24
  static schema (definition, entity) {
14
25
  const schema = {
15
26
  type: 'object',
16
- properties: { authentic: { type: 'boolean' } },
27
+ properties: {
28
+ authentic: { type: 'boolean' },
29
+ task: { type: 'boolean' },
30
+ source: structuredClone(schemas.source)
31
+ },
17
32
  additionalProperties: true
18
33
  }
19
34
 
@@ -22,9 +37,8 @@ class Request extends Conditions {
22
37
  if (definition.input !== undefined) {
23
38
  schema.properties.input = definition.input
24
39
  required.push('input')
25
- } else {
40
+ } else
26
41
  schema.properties.input = { type: 'null' }
27
- }
28
42
 
29
43
  if (entity === undefined)
30
44
  definition.query = false
@@ -5,3 +5,4 @@ const { load } = require('@toa.io/yaml')
5
5
 
6
6
  exports.query = load.sync(resolve(__dirname, './query.yaml'))
7
7
  exports.error = load.sync(resolve(__dirname, './error.yaml'))
8
+ exports.source = load.sync(resolve(__dirname, './source.yaml'))
@@ -1,11 +1,22 @@
1
1
  type: object
2
2
  properties:
3
- id: { }
3
+ id:
4
+ type: string
5
+ ids:
6
+ type: array
7
+ uniqueItems: true
8
+ minItems: 1
9
+ items:
10
+ type: string
4
11
  version:
5
12
  type: integer
6
13
  minimum: 0
7
14
  criteria:
8
15
  type: string
16
+ search:
17
+ type: string
18
+ sample:
19
+ type: number
9
20
  omit:
10
21
  type: integer
11
22
  minimum: 0
@@ -27,4 +38,6 @@ properties:
27
38
  type: string
28
39
  not:
29
40
  const: id
41
+ deleted:
42
+ type: boolean
30
43
  additionalProperties: false