@toa.io/core 1.0.0-alpha.27 → 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 +81 -0
- package/package.json +7 -9
- package/src/call.js +21 -1
- package/src/cascade.js +7 -5
- package/src/component.js +59 -15
- package/src/composition.js +1 -1
- package/src/connector.js +28 -17
- package/src/context.js +6 -0
- package/src/contract/contract.js +22 -0
- package/src/contract/reply.js +26 -9
- package/src/contract/request.js +19 -5
- package/src/contract/schemas/index.js +1 -0
- package/src/contract/schemas/query.yaml +14 -1
- package/src/contract/schemas/source.yaml +23 -0
- package/src/discovery.js +13 -7
- package/src/effect.js +19 -0
- package/src/entities/changeset.js +5 -8
- package/src/entities/entity.js +76 -24
- package/src/entities/factory.js +17 -6
- package/src/entities/newid.js +11 -0
- package/src/entities/set.js +13 -0
- package/src/event.js +19 -1
- package/src/exceptions.js +26 -19
- package/src/exposition.js +3 -2
- package/src/guard.js +17 -0
- package/src/index.js +6 -0
- package/src/observation.js +1 -9
- package/src/operation.js +38 -8
- package/src/query/options.js +3 -2
- package/src/query.js +32 -2
- package/src/receiver.js +73 -11
- package/src/remote.js +8 -7
- package/src/state.js +74 -35
- package/src/transition.js +11 -19
- package/src/transmission.js +12 -3
- package/src/unmanaged.js +11 -0
- package/test/component.test.js +4 -3
- package/test/connector.fixtures.js +8 -0
- package/test/connector.test.js +20 -0
- package/test/contract/conditions.test.js +5 -5
- package/test/contract/request.test.js +46 -7
- package/test/discovery.test.js +56 -0
- package/test/entities/entity.test.js +54 -40
- package/test/entities/factory.test.js +5 -3
- package/test/event.test.js +4 -4
- package/test/query.test.js +17 -0
- package/test/receiver.fixtures.js +1 -0
- package/test/state.test.js +13 -12
- package/types/bindings.d.ts +7 -5
- package/types/bridges.ts +3 -0
- package/types/component.d.ts +4 -1
- package/types/context.d.ts +3 -0
- package/types/entity.d.ts +2 -2
- package/types/extensions.d.ts +7 -4
- package/types/index.ts +2 -1
- package/types/message.d.ts +1 -0
- package/types/operations.d.ts +6 -0
- package/types/query.d.ts +2 -0
- package/types/remote.d.ts +18 -0
- package/types/request.d.ts +15 -0
- package/types/state.d.ts +2 -2
- package/types/storages.d.ts +11 -9
- package/src/contract/conditions.js +0 -21
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
merge,
|
|
5
|
-
overwrite,
|
|
6
|
-
newid
|
|
7
|
-
} = require('@toa.io/generic')
|
|
8
3
|
const { EntityContractException } = require('../exceptions')
|
|
9
4
|
|
|
10
5
|
class Changeset {
|
|
@@ -18,7 +13,7 @@ class Changeset {
|
|
|
18
13
|
this.query = query
|
|
19
14
|
|
|
20
15
|
this.#schema = schema
|
|
21
|
-
this.#state =
|
|
16
|
+
this.#state = {}
|
|
22
17
|
}
|
|
23
18
|
|
|
24
19
|
get () {
|
|
@@ -26,10 +21,12 @@ class Changeset {
|
|
|
26
21
|
}
|
|
27
22
|
|
|
28
23
|
set (value) {
|
|
29
|
-
const error = this.#schema.
|
|
24
|
+
const error = this.#schema.match(value)
|
|
30
25
|
|
|
31
|
-
if (error !== null)
|
|
26
|
+
if (error !== null)
|
|
27
|
+
throw new EntityContractException(error, value)
|
|
32
28
|
|
|
29
|
+
delete value._version
|
|
33
30
|
value._updated = Date.now()
|
|
34
31
|
|
|
35
32
|
this.#state = value
|
package/src/entities/entity.js
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} = require('@toa.io/generic')
|
|
7
|
-
|
|
8
|
-
const { EntityContractException } = require('../exceptions')
|
|
3
|
+
const { difference } = require('@toa.io/generic')
|
|
4
|
+
const { EntityContractException, EntityGuardException } = require('../exceptions')
|
|
5
|
+
const { newid } = require('./newid')
|
|
9
6
|
|
|
10
7
|
class Entity {
|
|
8
|
+
deleted = false
|
|
11
9
|
#schema
|
|
10
|
+
#guards
|
|
12
11
|
#origin = null
|
|
13
12
|
#state
|
|
13
|
+
#mutable = true
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* @param {boolean} [mutable] whether the entity may be modified and committed
|
|
17
|
+
*/
|
|
18
|
+
constructor (schema, argument, guards, mutable = true) {
|
|
16
19
|
this.#schema = schema
|
|
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)
|
|
@@ -29,37 +42,76 @@ class Entity {
|
|
|
29
42
|
return this.#state
|
|
30
43
|
}
|
|
31
44
|
|
|
32
|
-
set (value) {
|
|
33
|
-
|
|
45
|
+
set (value, optional = false) {
|
|
46
|
+
if (!this.#mutable)
|
|
47
|
+
throw new Error('Entity acquired by a read-only operation cannot be modified')
|
|
48
|
+
|
|
49
|
+
if (!optional)
|
|
50
|
+
this.#guard(value)
|
|
34
51
|
|
|
35
|
-
|
|
52
|
+
const error = optional ? this.#schema.fitOptional(value) : this.#schema.fit(value)
|
|
36
53
|
|
|
54
|
+
if (error !== null)
|
|
55
|
+
throw new EntityContractException(error, value)
|
|
56
|
+
|
|
57
|
+
this.#revive(value)
|
|
37
58
|
this.#set(value)
|
|
38
59
|
}
|
|
39
60
|
|
|
40
|
-
event () {
|
|
61
|
+
event (input = undefined) {
|
|
41
62
|
return {
|
|
42
63
|
origin: this.#origin,
|
|
43
64
|
state: this.#state,
|
|
44
|
-
changeset: this.#origin === null ? this.#state : difference(this.#origin, this.#state)
|
|
65
|
+
changeset: this.#origin === null ? this.#state : difference(this.#origin, this.#state),
|
|
66
|
+
trailers: this.#state._trailers,
|
|
67
|
+
input
|
|
45
68
|
}
|
|
46
69
|
}
|
|
47
70
|
|
|
48
71
|
#init (id) {
|
|
49
|
-
const value = {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
72
|
+
const value = { id, _version: 0 }
|
|
73
|
+
|
|
74
|
+
this.set(value, true)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#guard (value) {
|
|
78
|
+
if (this.#guards === undefined)
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
for (const guard of this.#guards) {
|
|
82
|
+
const ok = guard.fit(value, this.#origin)
|
|
83
|
+
|
|
84
|
+
if (ok === false)
|
|
85
|
+
throw new EntityGuardException(guard.name, value)
|
|
53
86
|
}
|
|
87
|
+
}
|
|
54
88
|
|
|
55
|
-
|
|
89
|
+
// deletion is only expressed as a new _deleted timestamp,
|
|
90
|
+
// so committing over a tombstone without touching it means revival
|
|
91
|
+
#revive (value) {
|
|
92
|
+
if (this.#origin?._deleted == null || value._deleted !== this.#origin._deleted)
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
value._deleted = null
|
|
96
|
+
this.deleted = false
|
|
56
97
|
}
|
|
57
98
|
|
|
58
99
|
#set (value) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
100
|
+
if (!('_trailers' in value))
|
|
101
|
+
Object.defineProperty(value, '_trailers', {
|
|
102
|
+
writable: false,
|
|
103
|
+
configurable: false,
|
|
104
|
+
enumerable: false,
|
|
105
|
+
value: {}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
if (!('_created' in value)) {
|
|
109
|
+
value._created = Date.now()
|
|
110
|
+
value._updated ??= value._created
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if ('_deleted' in value && value._deleted !== null)
|
|
114
|
+
this.deleted = true
|
|
63
115
|
|
|
64
116
|
if (this.#state !== undefined) {
|
|
65
117
|
value._updated = Date.now()
|
package/src/entities/factory.js
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { newid } = require('./newid')
|
|
3
4
|
const { Entity } = require('./entity')
|
|
4
5
|
const { EntitySet } = require('./set')
|
|
5
6
|
const { Changeset } = require('./changeset')
|
|
6
7
|
|
|
7
8
|
class Factory {
|
|
8
9
|
#schema
|
|
10
|
+
#guards
|
|
9
11
|
|
|
10
|
-
constructor (schema) {
|
|
12
|
+
constructor (schema, guards) {
|
|
11
13
|
this.#schema = schema
|
|
14
|
+
this.#guards = guards
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fit (values) {
|
|
18
|
+
this.#schema.validate({ id: newid(), ...values }, 'Entity')
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
init (id) {
|
|
15
|
-
return new Entity(this.#schema, id)
|
|
22
|
+
return new Entity(this.#schema, id, this.#guards)
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
object (record) {
|
|
19
|
-
return new Entity(this.#schema, record)
|
|
25
|
+
object (record, mutable = true) {
|
|
26
|
+
return new Entity(this.#schema, record, this.#guards, mutable)
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
objects (recordset) {
|
|
23
|
-
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
|
+
|
|
32
|
+
if (init !== undefined)
|
|
33
|
+
for (const id of init)
|
|
34
|
+
set.unshift(this.init(id))
|
|
24
35
|
|
|
25
36
|
return new EntitySet(set)
|
|
26
37
|
}
|
package/src/entities/set.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { SystemException } = require("../exceptions")
|
|
4
|
+
|
|
3
5
|
class EntitySet {
|
|
4
6
|
#set
|
|
5
7
|
|
|
@@ -10,6 +12,17 @@ class EntitySet {
|
|
|
10
12
|
get () {
|
|
11
13
|
return this.#set.map((entity) => entity.get())
|
|
12
14
|
}
|
|
15
|
+
|
|
16
|
+
set (values) {
|
|
17
|
+
if (values.length !== this.#set.length)
|
|
18
|
+
throw new SystemException('Objects array must not be modified')
|
|
19
|
+
|
|
20
|
+
values.forEach((value, index) => this.#set[index].set(value))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
events (input = undefined) {
|
|
24
|
+
return this.#set.map((entity) => entity.event(input))
|
|
25
|
+
}
|
|
13
26
|
}
|
|
14
27
|
|
|
15
28
|
exports.EntitySet = EntitySet
|
package/src/event.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { console, current, encode } = require('openspan')
|
|
3
4
|
const { Connector } = require('./connector')
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -12,11 +13,15 @@ class Event extends Connector {
|
|
|
12
13
|
#conditioned
|
|
13
14
|
#subjective
|
|
14
15
|
|
|
16
|
+
/** @type {string} */
|
|
17
|
+
#label
|
|
18
|
+
|
|
15
19
|
constructor (definition, emitter, bridge = undefined) {
|
|
16
20
|
super()
|
|
17
21
|
|
|
18
22
|
this.#conditioned = definition.conditioned
|
|
19
23
|
this.#subjective = definition.subjective
|
|
24
|
+
this.#label = definition.label ?? 'event'
|
|
20
25
|
this.#emitter = emitter
|
|
21
26
|
this.#bridge = bridge
|
|
22
27
|
|
|
@@ -32,7 +37,20 @@ class Event extends Connector {
|
|
|
32
37
|
/** @type {toa.core.Message} */
|
|
33
38
|
const message = { payload }
|
|
34
39
|
|
|
35
|
-
|
|
40
|
+
const options = {
|
|
41
|
+
name: `${this.#label} publish`,
|
|
42
|
+
kind: 'producer',
|
|
43
|
+
attributes: { 'messaging.destination.name': this.#label }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await console.span(options, async () => {
|
|
47
|
+
const context = current()
|
|
48
|
+
|
|
49
|
+
if (context !== undefined)
|
|
50
|
+
message.telemetry = encode(context)
|
|
51
|
+
|
|
52
|
+
await this.#emitter.emit(message)
|
|
53
|
+
})
|
|
36
54
|
}
|
|
37
55
|
}
|
|
38
56
|
}
|
package/src/exceptions.js
CHANGED
|
@@ -4,7 +4,6 @@ const { swap } = require('@toa.io/generic')
|
|
|
4
4
|
|
|
5
5
|
const codes = {
|
|
6
6
|
System: 0,
|
|
7
|
-
NotImplemented: 10,
|
|
8
7
|
|
|
9
8
|
Contract: 200,
|
|
10
9
|
RequestSyntax: 201,
|
|
@@ -12,6 +11,7 @@ const codes = {
|
|
|
12
11
|
RequestConflict: 203,
|
|
13
12
|
ResponseContract: 211,
|
|
14
13
|
EntityContract: 212,
|
|
14
|
+
EntityGuard: 213,
|
|
15
15
|
QuerySyntax: 221,
|
|
16
16
|
|
|
17
17
|
State: 300,
|
|
@@ -32,9 +32,12 @@ class Exception {
|
|
|
32
32
|
code
|
|
33
33
|
message
|
|
34
34
|
|
|
35
|
-
constructor (code, message) {
|
|
35
|
+
constructor (code, message, cause) {
|
|
36
36
|
this.code = code
|
|
37
37
|
this.message = message
|
|
38
|
+
|
|
39
|
+
if (cause !== undefined)
|
|
40
|
+
this.cause = cause
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -49,31 +52,30 @@ class SystemException extends Exception {
|
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
class ContractException extends Exception {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this.keyword = error.keyword
|
|
61
|
-
this.property = error.property
|
|
62
|
-
this.schema = error.schema
|
|
63
|
-
this.path = error.path
|
|
55
|
+
constructor (code, error, cause) {
|
|
56
|
+
super(code || codes.Contract, typeof error === 'string' ? error : error?.message, cause)
|
|
57
|
+
|
|
58
|
+
if (typeof error === 'object' && error !== null)
|
|
59
|
+
for (const k of ['keyword', 'property', 'schema', 'path', 'params'])
|
|
60
|
+
if (k in error)
|
|
61
|
+
this[k] = error[k]
|
|
64
62
|
}
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
class RequestContractException extends ContractException {
|
|
68
|
-
constructor (error) { super(codes.RequestContract, error) }
|
|
66
|
+
constructor (error, cause) { super(codes.RequestContract, error, cause) }
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
class ResponseContractException extends ContractException {
|
|
72
|
-
constructor (error) { super(codes.ResponseContract, error) }
|
|
70
|
+
constructor (error, cause) { super(codes.ResponseContract, error, cause) }
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
class EntityContractException extends ContractException {
|
|
76
|
-
constructor (error) { super(codes.EntityContract, error) }
|
|
74
|
+
constructor (error, cause) { super(codes.EntityContract, error, cause) }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class EntityGuardException extends ContractException {
|
|
78
|
+
constructor (name, cause) { super(codes.EntityGuard, name, cause) }
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
// #region exports
|
|
@@ -82,14 +84,19 @@ exports.SystemException = SystemException
|
|
|
82
84
|
exports.RequestContractException = RequestContractException
|
|
83
85
|
exports.ResponseContractException = ResponseContractException
|
|
84
86
|
exports.EntityContractException = EntityContractException
|
|
87
|
+
exports.EntityGuardException = EntityGuardException
|
|
85
88
|
|
|
86
89
|
for (const [name, code] of Object.entries(codes)) {
|
|
87
90
|
const classname = name + 'Exception'
|
|
88
91
|
|
|
89
92
|
if (exports[classname] === undefined) {
|
|
90
93
|
exports[classname] = class extends Exception {
|
|
91
|
-
constructor (message) {
|
|
92
|
-
|
|
94
|
+
constructor (message, cause) {
|
|
95
|
+
message = message
|
|
96
|
+
? `${classname}: ${message}`
|
|
97
|
+
: classname
|
|
98
|
+
|
|
99
|
+
super(code, message ?? classname, cause)
|
|
93
100
|
}
|
|
94
101
|
}
|
|
95
102
|
}
|
package/src/exposition.js
CHANGED
|
@@ -20,8 +20,9 @@ class Exposition extends Connector {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const expose = (manifest) => {
|
|
23
|
-
const { namespace, name, operations, events
|
|
24
|
-
|
|
23
|
+
const { namespace, name, entity, operations, events } = manifest
|
|
24
|
+
|
|
25
|
+
return { namespace, name, entity, operations, events }
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
exports.Exposition = Exposition
|
package/src/guard.js
ADDED
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const { Composition } = require('./composition')
|
|
|
5
5
|
const { Connector } = require('./connector')
|
|
6
6
|
const { Context } = require('./context')
|
|
7
7
|
const { Discovery } = require('./discovery')
|
|
8
|
+
const { Effect } = require('./effect')
|
|
8
9
|
const { Emission } = require('./emission')
|
|
9
10
|
const { Event } = require('./event')
|
|
10
11
|
const { Exposition } = require('./exposition')
|
|
@@ -19,6 +20,8 @@ const { Component } = require('./component')
|
|
|
19
20
|
const { State } = require('./state')
|
|
20
21
|
const { Transition } = require('./transition')
|
|
21
22
|
const { Transmission } = require('./transmission')
|
|
23
|
+
const { Unmanaged } = require('./unmanaged')
|
|
24
|
+
const { Guard } = require('./guard')
|
|
22
25
|
|
|
23
26
|
exports.entities = require('./entities')
|
|
24
27
|
exports.exceptions = require('./exceptions')
|
|
@@ -32,6 +35,7 @@ exports.Composition = Composition
|
|
|
32
35
|
exports.Connector = Connector
|
|
33
36
|
exports.Context = Context
|
|
34
37
|
exports.Discovery = Discovery
|
|
38
|
+
exports.Effect = Effect
|
|
35
39
|
exports.Emission = Emission
|
|
36
40
|
exports.Event = Event
|
|
37
41
|
exports.Exposition = Exposition
|
|
@@ -45,3 +49,5 @@ exports.Remote = Remote
|
|
|
45
49
|
exports.State = State
|
|
46
50
|
exports.Transition = Transition
|
|
47
51
|
exports.Transmission = Transmission
|
|
52
|
+
exports.Unmanaged = Unmanaged
|
|
53
|
+
exports.Guard = Guard
|
package/src/observation.js
CHANGED
|
@@ -3,16 +3,8 @@
|
|
|
3
3
|
const { Operation } = require('./operation')
|
|
4
4
|
|
|
5
5
|
class Observation extends Operation {
|
|
6
|
-
async acquire (store) {
|
|
7
|
-
const scope = await this.query(store.request.query)
|
|
8
|
-
const state = scope === null ? null : scope.get()
|
|
9
|
-
|
|
10
|
-
store.scope = scope
|
|
11
|
-
store.state = state
|
|
12
|
-
}
|
|
13
|
-
|
|
14
6
|
async run (store) {
|
|
15
|
-
if (store.scope === null) store.reply = null
|
|
7
|
+
if (store.scope === null || (store.scope?.deleted === true && store.request.query?.options?.deleted !== true)) store.reply = null
|
|
16
8
|
else await super.run(store)
|
|
17
9
|
}
|
|
18
10
|
}
|
package/src/operation.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { Connector } = require('./connector')
|
|
4
|
-
const { SystemException } = require('./exceptions')
|
|
4
|
+
const { SystemException, RequestContractException } = require('./exceptions')
|
|
5
|
+
const { Readable } = require('node:stream')
|
|
5
6
|
|
|
6
7
|
class Operation extends Connector {
|
|
7
8
|
scope
|
|
8
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
|
+
|
|
9
19
|
#cascade
|
|
10
20
|
#contracts
|
|
11
21
|
#query
|
|
@@ -26,8 +36,15 @@ class Operation extends Connector {
|
|
|
26
36
|
|
|
27
37
|
async invoke (request) {
|
|
28
38
|
try {
|
|
29
|
-
if (request.authentic !== true)
|
|
30
|
-
|
|
39
|
+
if (request.authentic !== true)
|
|
40
|
+
this.#contracts.request.fit(request)
|
|
41
|
+
|
|
42
|
+
if ('query' in request)
|
|
43
|
+
request.query = this.#query.parse(request.query)
|
|
44
|
+
|
|
45
|
+
// validate entity
|
|
46
|
+
if ('entity' in request)
|
|
47
|
+
this.scope.fit(request.entity)
|
|
31
48
|
|
|
32
49
|
const store = { request }
|
|
33
50
|
|
|
@@ -47,14 +64,24 @@ class Operation extends Connector {
|
|
|
47
64
|
return store.reply
|
|
48
65
|
}
|
|
49
66
|
|
|
50
|
-
async acquire () {
|
|
67
|
+
async acquire (store) {
|
|
68
|
+
if (this.#scope === 'none')
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
const scope = await this.query(store.request.query)
|
|
72
|
+
const raw = scope === null || scope instanceof Readable
|
|
73
|
+
|
|
74
|
+
store.scope = scope
|
|
75
|
+
store.state = raw ? scope : scope.get()
|
|
76
|
+
}
|
|
51
77
|
|
|
52
78
|
async run (store) {
|
|
53
79
|
const { request, state } = store
|
|
54
|
-
|
|
55
|
-
const reply = await this.#cascade.run(request.input, state) || {}
|
|
80
|
+
const reply = await this.#cascade.run(request.input, state)
|
|
56
81
|
|
|
57
|
-
//
|
|
82
|
+
// validate reply only on local environments
|
|
83
|
+
if (process.env.TOA_ENV === 'local' && !(reply instanceof Readable))
|
|
84
|
+
this.#contracts.reply.fit(reply)
|
|
58
85
|
|
|
59
86
|
store.reply = reply
|
|
60
87
|
}
|
|
@@ -62,7 +89,10 @@ class Operation extends Connector {
|
|
|
62
89
|
async commit () {}
|
|
63
90
|
|
|
64
91
|
async query (query) {
|
|
65
|
-
|
|
92
|
+
if (query === undefined)
|
|
93
|
+
throw new RequestContractException('Request query is required')
|
|
94
|
+
|
|
95
|
+
return this.scope[this.#scope](query, this.mutable)
|
|
66
96
|
}
|
|
67
97
|
}
|
|
68
98
|
|
package/src/query/options.js
CHANGED
|
@@ -33,8 +33,9 @@ const projection = (projection, properties) => {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
projection.
|
|
36
|
+
for (const property of ['_version', '_created', '_updated', '_deleted'])
|
|
37
|
+
if (!projection.includes(property))
|
|
38
|
+
projection.push(property)
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
exports.options = options
|
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)
|
|
@@ -19,13 +22,15 @@ class Query {
|
|
|
19
22
|
parse (query) {
|
|
20
23
|
/** @type {toa.core.storages.Query} */
|
|
21
24
|
const result = {}
|
|
22
|
-
const { id, version, criteria, ...rest } = query
|
|
25
|
+
const { id, ids, version, criteria, search, ...rest } = query
|
|
23
26
|
|
|
24
27
|
const options = this.#options(rest)
|
|
25
28
|
|
|
26
29
|
if (id !== undefined) result.id = id
|
|
30
|
+
if (ids !== undefined) result.ids = ids
|
|
27
31
|
if (version !== undefined) result.version = version
|
|
28
|
-
if (criteria !== undefined) result.criteria =
|
|
32
|
+
if (criteria !== undefined) result.criteria = this.#criteria(criteria)
|
|
33
|
+
if (search !== undefined) result.search = search
|
|
29
34
|
if (options !== undefined) result.options = options
|
|
30
35
|
|
|
31
36
|
return result
|
|
@@ -36,6 +41,31 @@ class Query {
|
|
|
36
41
|
|
|
37
42
|
return parse.options(options, this.#properties, this.#system)
|
|
38
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
|
+
}
|
|
39
67
|
}
|
|
40
68
|
|
|
69
|
+
const LIMIT = 1024
|
|
70
|
+
|
|
41
71
|
exports.Query = Query
|