@toa.io/core 1.0.0-alpha.266 → 1.0.0-alpha.270
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 +18 -0
- package/package.json +3 -3
- package/src/call.js +4 -2
- package/src/component.js +30 -10
- package/src/entities/entity.js +20 -4
- package/src/entities/factory.js +4 -4
- package/src/operation.js +10 -1
- package/src/query.js +29 -1
- package/src/receiver.js +28 -22
- package/src/remote.js +3 -2
- package/src/state.js +8 -5
- package/src/transition.js +3 -0
- package/test/entities/entity.test.js +31 -0
- package/test/entities/factory.test.js +4 -2
- package/test/query.test.js +17 -0
- package/test/state.test.js +16 -1
- package/types/entity.d.ts +2 -2
- package/types/state.d.ts +2 -2
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.270](https://github.com/toa-io/toa/compare/v1.0.0-alpha.269...v1.0.0-alpha.270) (2026-08-31)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **core:** apply the input default on a remote call ([341699b](https://github.com/toa-io/toa/commit/341699b26c9299963a898368671633a9b3fb97ed))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Performance Improvements
|
|
15
|
+
|
|
16
|
+
* **core:** do not snapshot a record an operation cannot commit ([a5e695b](https://github.com/toa-io/toa/commit/a5e695b0b1d4b00a91e40336deecd476c9646c89))
|
|
17
|
+
* **core:** keep the parsed criteria of a query ([7efc694](https://github.com/toa-io/toa/commit/7efc694814c2a071ae7e48dbf1c6014db6025a9f))
|
|
18
|
+
* stop rebuilding per-call values that never change ([4202504](https://github.com/toa-io/toa/commit/4202504c33fb9ef9694f9995f3d0397d3a186438))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
# [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)
|
|
7
25
|
|
|
8
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/core",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.270",
|
|
4
4
|
"description": "Toa Core",
|
|
5
5
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/toa-io/toa#readme",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"@toa.io/generic": "1.0.0-alpha.254",
|
|
25
25
|
"@toa.io/yaml": "1.0.0-alpha.254",
|
|
26
26
|
"error-value": "0.3.0",
|
|
27
|
-
"openspan": "1.0.0-alpha.
|
|
27
|
+
"openspan": "1.0.0-alpha.270",
|
|
28
28
|
"uuid": "11.1.1"
|
|
29
29
|
},
|
|
30
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "94400bdd411b4c25074ffcf58184580ddd060268"
|
|
31
31
|
}
|
package/src/call.js
CHANGED
|
@@ -21,14 +21,16 @@ class Call extends Connector {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
async invoke (request = {}) {
|
|
24
|
-
request.input ??= null
|
|
25
|
-
|
|
26
24
|
// the caller may have attributed the call itself, as the node bridge does
|
|
27
25
|
if (this.#source !== undefined)
|
|
28
26
|
request.source ??= this.#source
|
|
29
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
|
|
30
30
|
this.#contract.fit(request)
|
|
31
31
|
|
|
32
|
+
request.input ??= null
|
|
33
|
+
|
|
32
34
|
// avoid validation on the recipient's side
|
|
33
35
|
request.authentic = true
|
|
34
36
|
|
package/src/component.js
CHANGED
|
@@ -13,6 +13,9 @@ class Component extends Connector {
|
|
|
13
13
|
/** @protected */
|
|
14
14
|
kind = 'server'
|
|
15
15
|
|
|
16
|
+
/** @type {Record<string, object>} span options per endpoint */
|
|
17
|
+
#spans = {}
|
|
18
|
+
|
|
16
19
|
constructor (locator, operations) {
|
|
17
20
|
super()
|
|
18
21
|
|
|
@@ -23,8 +26,9 @@ class Component extends Connector {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
async invoke (endpoint, request) {
|
|
26
|
-
|
|
27
|
-
`
|
|
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}'`)
|
|
28
32
|
|
|
29
33
|
// if the request carries no telemetry, the trace starts here
|
|
30
34
|
const remote = request?.telemetry === undefined ? null : decode(request.telemetry)
|
|
@@ -38,14 +42,7 @@ class Component extends Connector {
|
|
|
38
42
|
|
|
39
43
|
/** @private */
|
|
40
44
|
async process (endpoint, request) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// the server span is emitted by the component itself,
|
|
44
|
-
// while the client span belongs to the calling service and inherits it from the context
|
|
45
|
-
if (this.kind === 'server')
|
|
46
|
-
options.service = this.locator.id
|
|
47
|
-
|
|
48
|
-
return console.span(options, async () => {
|
|
45
|
+
return console.span(this.span(endpoint), async () => {
|
|
49
46
|
const reply = await this.operations[endpoint].invoke(request)
|
|
50
47
|
|
|
51
48
|
if (reply?.exception !== undefined) {
|
|
@@ -60,6 +57,29 @@ class Component extends Connector {
|
|
|
60
57
|
return reply
|
|
61
58
|
})
|
|
62
59
|
}
|
|
60
|
+
|
|
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
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return options
|
|
82
|
+
}
|
|
63
83
|
}
|
|
64
84
|
|
|
65
85
|
exports.Component = Component
|
package/src/entities/entity.js
CHANGED
|
@@ -10,15 +10,28 @@ class Entity {
|
|
|
10
10
|
#guards
|
|
11
11
|
#origin = null
|
|
12
12
|
#state
|
|
13
|
+
#mutable = true
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
/**
|
|
16
|
+
* @param {boolean} [mutable] whether the entity may be modified and committed
|
|
17
|
+
*/
|
|
18
|
+
constructor (schema, argument, guards, mutable = true) {
|
|
15
19
|
this.#schema = schema
|
|
16
20
|
this.#guards = guards
|
|
17
21
|
|
|
18
22
|
if (typeof argument === 'object') {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
/*
|
|
24
|
+
* The origin is the pre-image a commit diffs the new state against. An operation
|
|
25
|
+
* that cannot commit has nothing to diff, so it takes the record as it came from
|
|
26
|
+
* the storage instead of paying for a deep copy of every record it read.
|
|
27
|
+
*/
|
|
28
|
+
this.#mutable = mutable
|
|
29
|
+
|
|
30
|
+
if (mutable) {
|
|
31
|
+
this.#set(structuredClone(argument))
|
|
32
|
+
this.#origin = argument
|
|
33
|
+
} else
|
|
34
|
+
this.#set(argument)
|
|
22
35
|
} else {
|
|
23
36
|
const id = argument === undefined ? newid() : argument
|
|
24
37
|
this.#init(id)
|
|
@@ -30,6 +43,9 @@ class Entity {
|
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
set (value, optional = false) {
|
|
46
|
+
if (!this.#mutable)
|
|
47
|
+
throw new Error('Entity acquired by a read-only operation cannot be modified')
|
|
48
|
+
|
|
33
49
|
if (!optional)
|
|
34
50
|
this.#guard(value)
|
|
35
51
|
|
package/src/entities/factory.js
CHANGED
|
@@ -22,12 +22,12 @@ class Factory {
|
|
|
22
22
|
return new Entity(this.#schema, id, this.#guards)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
object (record) {
|
|
26
|
-
return new Entity(this.#schema, record, this.#guards)
|
|
25
|
+
object (record, mutable = true) {
|
|
26
|
+
return new Entity(this.#schema, record, this.#guards, mutable)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
objects (recordset, init) {
|
|
30
|
-
const set = recordset.map((record) => this.object(record))
|
|
29
|
+
objects (recordset, init, mutable = true) {
|
|
30
|
+
const set = recordset.map((record) => this.object(record, mutable))
|
|
31
31
|
|
|
32
32
|
if (init !== undefined)
|
|
33
33
|
for (const id of init)
|
package/src/operation.js
CHANGED
|
@@ -7,6 +7,15 @@ const { Readable } = require('node:stream')
|
|
|
7
7
|
class Operation extends Connector {
|
|
8
8
|
scope
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Whether what this operation acquires may be modified and committed. Only a
|
|
12
|
+
* transition commits, and only a commit needs the pre-image an entity keeps
|
|
13
|
+
* to diff the new state against.
|
|
14
|
+
*
|
|
15
|
+
* @protected
|
|
16
|
+
*/
|
|
17
|
+
mutable = false
|
|
18
|
+
|
|
10
19
|
#cascade
|
|
11
20
|
#contracts
|
|
12
21
|
#query
|
|
@@ -83,7 +92,7 @@ class Operation extends Connector {
|
|
|
83
92
|
if (query === undefined)
|
|
84
93
|
throw new RequestContractException('Request query is required')
|
|
85
94
|
|
|
86
|
-
return this.scope[this.#scope](query)
|
|
95
|
+
return this.scope[this.#scope](query, this.mutable)
|
|
87
96
|
}
|
|
88
97
|
}
|
|
89
98
|
|
package/src/query.js
CHANGED
|
@@ -7,6 +7,9 @@ class Query {
|
|
|
7
7
|
#properties
|
|
8
8
|
#system
|
|
9
9
|
|
|
10
|
+
/** @type {Map<string, object>} parsed criteria by their expression */
|
|
11
|
+
#asts = new Map()
|
|
12
|
+
|
|
10
13
|
constructor (properties) {
|
|
11
14
|
this.#properties = properties
|
|
12
15
|
this.#system = Object.keys(properties).filter((key) => properties[key].system === true)
|
|
@@ -26,7 +29,7 @@ class Query {
|
|
|
26
29
|
if (id !== undefined) result.id = id
|
|
27
30
|
if (ids !== undefined) result.ids = ids
|
|
28
31
|
if (version !== undefined) result.version = version
|
|
29
|
-
if (criteria !== undefined) result.criteria =
|
|
32
|
+
if (criteria !== undefined) result.criteria = this.#criteria(criteria)
|
|
30
33
|
if (search !== undefined) result.search = search
|
|
31
34
|
if (options !== undefined) result.options = options
|
|
32
35
|
|
|
@@ -38,6 +41,31 @@ class Query {
|
|
|
38
41
|
|
|
39
42
|
return parse.options(options, this.#properties, this.#system)
|
|
40
43
|
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Lexing an RSQL expression is the most expensive thing this class does, and the same
|
|
47
|
+
* expression arrives over and over: a route builds it from its own declaration and the
|
|
48
|
+
* request parameters. The tree is only read downstream — a storage builds a fresh query
|
|
49
|
+
* from it — so it is kept rather than parsed again.
|
|
50
|
+
*
|
|
51
|
+
* Expressions come from the client, hence the bound. An invalid one throws before it
|
|
52
|
+
* reaches the cache.
|
|
53
|
+
*/
|
|
54
|
+
#criteria (criteria) {
|
|
55
|
+
const known = this.#asts.get(criteria)
|
|
56
|
+
|
|
57
|
+
if (known !== undefined) return known
|
|
58
|
+
|
|
59
|
+
const ast = parse.criteria(criteria, this.#properties)
|
|
60
|
+
|
|
61
|
+
if (this.#asts.size >= LIMIT) this.#asts.clear()
|
|
62
|
+
|
|
63
|
+
this.#asts.set(criteria, ast)
|
|
64
|
+
|
|
65
|
+
return ast
|
|
66
|
+
}
|
|
41
67
|
}
|
|
42
68
|
|
|
69
|
+
const LIMIT = 1024
|
|
70
|
+
|
|
43
71
|
exports.Query = Query
|
package/src/receiver.js
CHANGED
|
@@ -33,6 +33,12 @@ class Receiver extends Connector {
|
|
|
33
33
|
|
|
34
34
|
#bridge
|
|
35
35
|
|
|
36
|
+
/** @type {object} */
|
|
37
|
+
#delivery
|
|
38
|
+
|
|
39
|
+
/** @type {object} */
|
|
40
|
+
#processing
|
|
41
|
+
|
|
36
42
|
constructor (definition, local, bridge) {
|
|
37
43
|
super()
|
|
38
44
|
|
|
@@ -51,6 +57,27 @@ class Receiver extends Connector {
|
|
|
51
57
|
|
|
52
58
|
this.depends(local)
|
|
53
59
|
if (bridge !== undefined) this.depends(bridge)
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
* The delivery span is created on behalf of the messaging destination, so that
|
|
63
|
+
* each consumer forms its own complete producer/consumer pair, and service graphs
|
|
64
|
+
* display fan-out correctly: producer -> destination -> each consumer
|
|
65
|
+
* (Tempo pairs spans one-to-one, thus multiple consumers can not pair
|
|
66
|
+
* with a single producer span, see grafana/tempo#5408)
|
|
67
|
+
*/
|
|
68
|
+
this.#delivery = {
|
|
69
|
+
name: `${this.#label} deliver`,
|
|
70
|
+
kind: 'producer',
|
|
71
|
+
service: this.#destination,
|
|
72
|
+
attributes: { 'messaging.destination.name': this.#destination }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.#processing = {
|
|
76
|
+
name: `${this.#label} process`,
|
|
77
|
+
kind: 'consumer',
|
|
78
|
+
service: local.locator.id,
|
|
79
|
+
attributes: { 'messaging.destination.name': this.#destination }
|
|
80
|
+
}
|
|
54
81
|
}
|
|
55
82
|
|
|
56
83
|
/** @hot */
|
|
@@ -77,28 +104,7 @@ class Receiver extends Connector {
|
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
async #process (request) {
|
|
80
|
-
|
|
81
|
-
* The delivery span is created on behalf of the messaging destination, so that
|
|
82
|
-
* each consumer forms its own complete producer/consumer pair, and service graphs
|
|
83
|
-
* display fan-out correctly: producer -> destination -> each consumer
|
|
84
|
-
* (Tempo pairs spans one-to-one, thus multiple consumers can not pair
|
|
85
|
-
* with a single producer span, see grafana/tempo#5408)
|
|
86
|
-
*/
|
|
87
|
-
const delivery = {
|
|
88
|
-
name: `${this.#label} deliver`,
|
|
89
|
-
kind: 'producer',
|
|
90
|
-
service: this.#destination,
|
|
91
|
-
attributes: { 'messaging.destination.name': this.#destination }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const options = {
|
|
95
|
-
name: `${this.#label} process`,
|
|
96
|
-
kind: 'consumer',
|
|
97
|
-
service: this.#local.locator.id,
|
|
98
|
-
attributes: { 'messaging.destination.name': this.#destination }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return console.span(delivery, async () => console.span(options, async () => {
|
|
107
|
+
return console.span(this.#delivery, async () => console.span(this.#processing, async () => {
|
|
102
108
|
try {
|
|
103
109
|
await this.#local.invoke(this.#endpoint, request)
|
|
104
110
|
} catch (error) {
|
package/src/remote.js
CHANGED
|
@@ -7,8 +7,9 @@ class Remote extends Component {
|
|
|
7
7
|
kind = 'client'
|
|
8
8
|
|
|
9
9
|
explain (endpoint) {
|
|
10
|
-
|
|
11
|
-
`
|
|
10
|
+
if (!(endpoint in this.operations))
|
|
11
|
+
// `assert.fail`, not `assert.ok`: the message is built only when it is needed
|
|
12
|
+
assert.fail(`Endpoint '${endpoint}' is not provided by '${this.locator.id}'`)
|
|
12
13
|
|
|
13
14
|
return this.operations[endpoint].explain()
|
|
14
15
|
}
|
package/src/state.js
CHANGED
|
@@ -24,7 +24,7 @@ class State {
|
|
|
24
24
|
return this.#entities.fit(values)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
async object (query) {
|
|
27
|
+
async object (query, mutable = true) {
|
|
28
28
|
const record = await this.storage.get(query)
|
|
29
29
|
|
|
30
30
|
if (record === null) {
|
|
@@ -35,15 +35,15 @@ class State {
|
|
|
35
35
|
|
|
36
36
|
return null
|
|
37
37
|
} else
|
|
38
|
-
return this.#entities.object(record)
|
|
38
|
+
return this.#entities.object(record, mutable)
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
async objects (query) {
|
|
41
|
+
async objects (query, mutable = true) {
|
|
42
42
|
const recordset = await this.storage.find(query)
|
|
43
43
|
const missing = this.#associated && query.ids !== undefined && recordset.length < query.ids.length
|
|
44
44
|
const init = missing ? query.ids.filter((id) => !recordset.some((record) => record.id === id)) : undefined
|
|
45
45
|
|
|
46
|
-
return this.#entities.objects(recordset, init)
|
|
46
|
+
return this.#entities.objects(recordset, init, mutable)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
async stream (query) {
|
|
@@ -69,7 +69,7 @@ class State {
|
|
|
69
69
|
const record = await this.storage.ensure(query, properties, object.get())
|
|
70
70
|
|
|
71
71
|
if (record.id !== blank.id) // exists
|
|
72
|
-
return this.#entities.object(record)
|
|
72
|
+
return this.#entities.object(record, NOT_MUTABLE)
|
|
73
73
|
|
|
74
74
|
const event = object.event(input)
|
|
75
75
|
|
|
@@ -132,4 +132,7 @@ class State {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/** an effect never commits what `ensure` hands it, see `Effect` */
|
|
136
|
+
const NOT_MUTABLE = false
|
|
137
|
+
|
|
135
138
|
exports.State = State
|
package/src/transition.js
CHANGED
|
@@ -5,6 +5,9 @@ const { Operation } = require('./operation')
|
|
|
5
5
|
const { StateConcurrencyException, StateNotFoundException } = require('./exceptions')
|
|
6
6
|
|
|
7
7
|
class Transition extends Operation {
|
|
8
|
+
/** a transition is the only operation that commits */
|
|
9
|
+
mutable = true
|
|
10
|
+
|
|
8
11
|
#concurrency
|
|
9
12
|
|
|
10
13
|
constructor (cascade, scope, contract, query, definition) {
|
|
@@ -14,6 +14,37 @@ describe('argument', () => {
|
|
|
14
14
|
|
|
15
15
|
expect(entity.get()).toEqual(state)
|
|
16
16
|
})
|
|
17
|
+
|
|
18
|
+
it('should snapshot the record it may commit', () => {
|
|
19
|
+
const record = fixtures.state()
|
|
20
|
+
const entity = new Entity(fixtures.schema, record)
|
|
21
|
+
|
|
22
|
+
expect(entity.get()).not.toBe(record)
|
|
23
|
+
expect(entity.event().origin).toBe(record)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('read-only', () => {
|
|
28
|
+
it('should take the record as it came', () => {
|
|
29
|
+
const record = fixtures.state()
|
|
30
|
+
const entity = new Entity(fixtures.schema, record, undefined, false)
|
|
31
|
+
|
|
32
|
+
// no pre-image to diff against, hence no copy of it
|
|
33
|
+
expect(entity.get()).toBe(record)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should still report a tombstone', () => {
|
|
37
|
+
const record = { ...fixtures.state(), _deleted: Date.now() }
|
|
38
|
+
const entity = new Entity(fixtures.schema, record, undefined, false)
|
|
39
|
+
|
|
40
|
+
expect(entity.deleted).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should refuse to be modified', () => {
|
|
44
|
+
const entity = new Entity(fixtures.schema, fixtures.state(), undefined, false)
|
|
45
|
+
|
|
46
|
+
expect(() => entity.set(entity.get())).toThrow('read-only')
|
|
47
|
+
})
|
|
17
48
|
})
|
|
18
49
|
|
|
19
50
|
describe('tombstone', () => {
|
|
@@ -30,7 +30,8 @@ it('should create instance', () => {
|
|
|
30
30
|
const object = factory.object(fixtures.entity)
|
|
31
31
|
|
|
32
32
|
expect(object).toBeInstanceOf(mock.Entity)
|
|
33
|
-
expect(object.constructor)
|
|
33
|
+
expect(object.constructor)
|
|
34
|
+
.toHaveBeenCalledWith(fixtures.schema, fixtures.entity, expect.any(Function), true)
|
|
34
35
|
})
|
|
35
36
|
|
|
36
37
|
it('should create set', () => {
|
|
@@ -39,7 +40,8 @@ it('should create set', () => {
|
|
|
39
40
|
expect(objects).toBeInstanceOf(mock.EntitySet)
|
|
40
41
|
|
|
41
42
|
const instances = fixtures.set.map((entity, index) => {
|
|
42
|
-
expect(mock.Entity)
|
|
43
|
+
expect(mock.Entity)
|
|
44
|
+
.toHaveBeenNthCalledWith(index + 1, fixtures.schema, entity, expect.any(Function), true)
|
|
43
45
|
|
|
44
46
|
return mock.Entity.mock.instances[index]
|
|
45
47
|
})
|
package/test/query.test.js
CHANGED
|
@@ -22,6 +22,23 @@ describe('criteria', () => {
|
|
|
22
22
|
expect(query.criteria).toEqual(fixtures.samples.simple.parsed.criteria)
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
+
it('should keep a parsed criteria', () => {
|
|
26
|
+
const instance = new Query(fixtures.samples.simple.properties)
|
|
27
|
+
|
|
28
|
+
const first = instance.parse(fixtures.samples.simple.query).criteria
|
|
29
|
+
const second = instance.parse(fixtures.samples.simple.query).criteria
|
|
30
|
+
|
|
31
|
+
expect(second).toBe(first)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should not keep an invalid criteria', () => {
|
|
35
|
+
const instance = new Query(fixtures.samples.simple.properties)
|
|
36
|
+
const query = { criteria: 'nonexistent==1' }
|
|
37
|
+
|
|
38
|
+
expect(() => instance.parse(query)).toThrow()
|
|
39
|
+
expect(() => instance.parse(query)).toThrow()
|
|
40
|
+
})
|
|
41
|
+
|
|
25
42
|
it('should parse criteria with type coercion', () => {
|
|
26
43
|
const instance = new Query(fixtures.samples.extended.properties)
|
|
27
44
|
const query = instance.parse(fixtures.samples.extended.query)
|
package/test/state.test.js
CHANGED
|
@@ -16,7 +16,22 @@ it('should provide object', async () => {
|
|
|
16
16
|
|
|
17
17
|
expect(fixtures.storage.get).toHaveBeenCalledWith(fixtures.query)
|
|
18
18
|
expect(entity).toStrictEqual(fixtures.factory.object.mock.results[0].value)
|
|
19
|
-
expect(fixtures.factory.object)
|
|
19
|
+
expect(fixtures.factory.object)
|
|
20
|
+
.toHaveBeenCalledWith(fixtures.storage.get.mock.results[0].value, true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should provide read-only object', async () => {
|
|
24
|
+
await state.object(fixtures.query, false)
|
|
25
|
+
|
|
26
|
+
expect(fixtures.factory.object)
|
|
27
|
+
.toHaveBeenCalledWith(fixtures.storage.get.mock.results[0].value, false)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should provide read-only objects', async () => {
|
|
31
|
+
await state.objects(fixtures.query, false)
|
|
32
|
+
|
|
33
|
+
expect(fixtures.factory.objects)
|
|
34
|
+
.toHaveBeenCalledWith(fixtures.storage.find.mock.results[0].value, undefined, false)
|
|
20
35
|
})
|
|
21
36
|
|
|
22
37
|
it('should store entity', async () => {
|
package/types/entity.d.ts
CHANGED
|
@@ -8,9 +8,9 @@ declare namespace toa.core {
|
|
|
8
8
|
interface Factory {
|
|
9
9
|
init(id: string): Entity
|
|
10
10
|
|
|
11
|
-
object(record: Object): Entity
|
|
11
|
+
object(record: Object, mutable?: boolean): Entity
|
|
12
12
|
|
|
13
|
-
objects(recordset: Object[]): Entity[]
|
|
13
|
+
objects(recordset: Object[], init?: string[], mutable?: boolean): Entity[]
|
|
14
14
|
|
|
15
15
|
changeset(query: _storages.Query): Changeset
|
|
16
16
|
}
|
package/types/state.d.ts
CHANGED
|
@@ -16,9 +16,9 @@ declare namespace toa.core {
|
|
|
16
16
|
interface State {
|
|
17
17
|
init(id: string): _entity.Entity
|
|
18
18
|
|
|
19
|
-
object(query: _storages.Query): Promise<_entity.Entity>
|
|
19
|
+
object(query: _storages.Query, mutable?: boolean): Promise<_entity.Entity>
|
|
20
20
|
|
|
21
|
-
objects(query: _storages.Query): Promise<_entity.Entity[]>
|
|
21
|
+
objects(query: _storages.Query, mutable?: boolean): Promise<_entity.Entity[]>
|
|
22
22
|
|
|
23
23
|
changeset(query: _storages.Query): _entity.Changeset
|
|
24
24
|
|