@toa.io/core 1.0.0-alpha.28 → 1.0.0-alpha.282
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 +168 -0
- package/package.json +8 -10
- package/src/assignment.js +5 -8
- package/src/call.js +35 -9
- package/src/cascade.js +9 -11
- package/src/component.js +61 -21
- package/src/composition.js +3 -7
- package/src/connector.js +29 -22
- package/src/context.js +8 -6
- package/src/contract/contract.js +18 -0
- package/src/contract/index.js +2 -7
- package/src/contract/reply.js +36 -15
- package/src/contract/request.js +20 -10
- package/src/contract/schemas/index.js +9 -5
- package/src/contract/schemas/query.yaml +14 -1
- package/src/contract/schemas/source.yaml +23 -0
- package/src/discovery.js +15 -13
- package/src/effect.js +15 -0
- package/src/emission.js +2 -6
- package/src/entities/changeset.js +7 -14
- package/src/entities/entity.js +75 -29
- package/src/entities/factory.js +21 -14
- package/src/entities/index.js +1 -5
- package/src/entities/newid.js +9 -0
- package/src/entities/set.js +13 -4
- package/src/event.js +21 -7
- package/src/exceptions.js +53 -39
- package/src/exposition.js +5 -8
- package/src/guard.js +13 -0
- package/src/index.js +28 -47
- package/src/locator.js +2 -6
- package/src/observation.js +3 -15
- package/src/operation.js +40 -14
- package/src/outbox/index.js +1 -0
- package/src/outbox/outbox.js +296 -0
- package/src/query/criteria.js +3 -7
- package/src/query/options.js +5 -8
- package/src/query.js +36 -7
- package/src/receiver.js +76 -18
- package/src/reflection.js +2 -6
- package/src/remote.js +9 -12
- package/src/state.js +89 -50
- package/src/transition.js +13 -25
- package/src/transmission.js +15 -10
- package/src/unmanaged.js +7 -0
- package/test/call.fixtures.js +8 -12
- package/test/call.test.js +21 -9
- package/test/component.fixtures.js +5 -8
- package/test/component.test.js +22 -10
- package/test/connector.fixtures.js +10 -8
- package/test/connector.test.js +59 -38
- package/test/context.fixtures.js +8 -12
- package/test/context.test.js +18 -7
- package/test/contract/conditions.test.js +11 -9
- package/test/contract/contract.fixtures.js +12 -16
- package/test/contract/request.test.js +84 -34
- package/test/discovery.test.js +64 -0
- package/test/emission.fixtures.js +6 -10
- package/test/emission.test.js +29 -12
- package/test/entities/entity.fixtures.js +7 -11
- package/test/entities/entity.test.js +70 -49
- package/test/entities/factory.fixtures.js +22 -10
- package/test/entities/factory.test.js +29 -15
- package/test/entities/set.fixtures.js +6 -8
- package/test/entities/set.test.js +6 -5
- package/test/event.fixtures.js +9 -14
- package/test/event.test.js +57 -31
- package/test/locator.test.js +25 -24
- package/test/outbox.test.js +121 -0
- package/test/query.fixtures.js +1 -5
- package/test/query.test.js +42 -14
- package/test/receiver.fixtures.js +9 -12
- package/test/receiver.test.js +45 -22
- package/test/reflection.test.js +7 -6
- package/test/state.fixtures.js +25 -30
- package/test/state.test.js +72 -19
- package/test/transmission.fixtures.js +4 -6
- package/test/transmission.test.js +33 -15
- package/types/atomicity.d.ts +58 -0
- package/types/bindings.d.ts +8 -6
- package/types/bridges.ts +5 -2
- package/types/component.d.ts +7 -4
- package/types/context.d.ts +6 -3
- package/types/entity.d.ts +4 -4
- package/types/event.d.ts +1 -1
- package/types/extensions.d.ts +35 -10
- package/types/index.ts +16 -13
- package/types/message.d.ts +1 -0
- package/types/operations.d.ts +6 -0
- package/types/outbox.d.ts +53 -0
- package/types/query.d.ts +2 -0
- package/types/receiver.d.ts +2 -2
- package/types/reflection.d.ts +1 -1
- package/types/remote.d.ts +18 -0
- package/types/request.d.ts +16 -1
- package/types/state.d.ts +11 -8
- package/types/storages.d.ts +36 -11
- package/src/contract/conditions.js +0 -21
|
@@ -1,27 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
import * as schemas from '@toa.io/schemas'
|
|
2
|
+
import { describe, it, beforeEach, mock } from 'node:test'
|
|
3
|
+
import assert from 'node:assert/strict'
|
|
4
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
6
|
+
import clone from 'clone-deep'
|
|
7
|
+
import { generate } from 'randomstring'
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
import { Request } from '../../src/contract/request.js'
|
|
10
|
+
import { Contract } from '../../src/contract/contract.js'
|
|
11
|
+
import * as fixtures from './contract.fixtures.js'
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const fixtures = require('./contract.fixtures')
|
|
13
|
+
// the base is real; what it was constructed with and told to fit is observable
|
|
14
|
+
const fit = mock.method(Contract.prototype, 'fit', () => undefined)
|
|
11
15
|
|
|
12
16
|
let contract
|
|
13
17
|
|
|
14
18
|
beforeEach(() => {
|
|
15
|
-
|
|
19
|
+
resetCalls()
|
|
20
|
+
fit.mock.resetCalls()
|
|
16
21
|
|
|
17
|
-
contract = new Request(fixtures.schema)
|
|
22
|
+
contract = new Request(fixtures.schema, {})
|
|
18
23
|
})
|
|
19
24
|
|
|
20
25
|
const dummy = { schema: { properties: {} } }
|
|
21
26
|
|
|
22
27
|
it('should extend Conditions', () => {
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
assert.ok(contract instanceof Contract)
|
|
29
|
+
assert.strictEqual(contract.schema, fixtures.schema)
|
|
25
30
|
})
|
|
26
31
|
|
|
27
32
|
it('should fit request', () => {
|
|
@@ -29,7 +34,8 @@ it('should fit request', () => {
|
|
|
29
34
|
|
|
30
35
|
contract.fit(request)
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
assert.ok(fit.mock.calls.some((call) =>
|
|
38
|
+
call.this === contract && isDeepStrictEqual(call.arguments[0], request)))
|
|
33
39
|
})
|
|
34
40
|
|
|
35
41
|
describe('schema', () => {
|
|
@@ -40,73 +46,117 @@ describe('schema', () => {
|
|
|
40
46
|
})
|
|
41
47
|
|
|
42
48
|
it('should provide schema', () => {
|
|
43
|
-
|
|
49
|
+
assert.notStrictEqual(Request.schema({}, dummy), undefined)
|
|
44
50
|
})
|
|
45
51
|
|
|
46
52
|
it('should add required input if defined', () => {
|
|
47
53
|
const input = { type: 'number' }
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
assert.deepStrictEqual(Request.schema({ input }, dummy).properties.input, input)
|
|
50
56
|
})
|
|
51
57
|
|
|
52
58
|
it('should set input as null if undefined', async () => {
|
|
53
|
-
|
|
59
|
+
assert.deepStrictEqual(Request.schema({}, dummy).properties.input, { type: 'null' })
|
|
54
60
|
})
|
|
55
61
|
|
|
56
62
|
it('should contain query if declaration.query is not defined', () => {
|
|
57
|
-
|
|
63
|
+
assert.notStrictEqual(Request.schema({}, dummy).properties.query, undefined)
|
|
58
64
|
})
|
|
59
65
|
|
|
60
66
|
it('should not contain query if declaration.query is false', () => {
|
|
61
67
|
schema.properties.query = { type: 'null' }
|
|
62
|
-
|
|
68
|
+
assert.partialDeepStrictEqual(Request.schema({ query: false }, dummy), schema)
|
|
63
69
|
})
|
|
64
70
|
|
|
65
71
|
it('should require query if declaration.query is true', () => {
|
|
66
72
|
schema.required = ['query']
|
|
67
|
-
|
|
73
|
+
assert.ok(['query'].every((item) => Request.schema({ query: true }, dummy).required.some((candidate) => isDeepStrictEqual(candidate, item))))
|
|
68
74
|
})
|
|
69
75
|
|
|
70
76
|
it('should forbid projection for non observations', () => {
|
|
71
|
-
|
|
72
|
-
.toBe(undefined)
|
|
77
|
+
assert.strictEqual(Request.schema({ type: 'transition' }, dummy).properties.query.properties.projection, undefined)
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
.toBe(undefined)
|
|
79
|
+
assert.strictEqual(Request.schema({ type: 'assignment' }, dummy).properties.query.properties.projection, undefined)
|
|
76
80
|
|
|
77
|
-
|
|
78
|
-
.toBeDefined()
|
|
81
|
+
assert.notStrictEqual(Request.schema({ type: 'observation' }, dummy).properties.query.properties.projection, undefined)
|
|
79
82
|
})
|
|
80
83
|
|
|
81
84
|
it('should forbid version for observations', () => {
|
|
82
|
-
|
|
83
|
-
.toBeDefined()
|
|
85
|
+
assert.notStrictEqual(Request.schema({ type: 'transition' }, dummy).properties.query.properties.version, undefined)
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
.toBe(undefined)
|
|
87
|
+
assert.strictEqual(Request.schema({ type: 'observation' }, dummy).properties.query.properties.version, undefined)
|
|
87
88
|
})
|
|
88
89
|
|
|
89
90
|
it('should allow omit, limit only for set observations', () => {
|
|
90
91
|
const schema = Request.schema({ type: 'transition' }, dummy)
|
|
91
92
|
const transition = schema.properties.query.properties
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
assert.strictEqual(transition.omit, undefined)
|
|
95
|
+
assert.strictEqual(transition.limit, undefined)
|
|
95
96
|
|
|
96
97
|
const object = Request.schema({
|
|
97
98
|
type: 'observation',
|
|
98
99
|
scope: 'object'
|
|
99
100
|
}, dummy).properties.query.properties
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
assert.strictEqual(object.omit, undefined)
|
|
103
|
+
assert.strictEqual(object.limit, undefined)
|
|
103
104
|
|
|
104
105
|
const objects = Request.schema({
|
|
105
106
|
type: 'observation',
|
|
106
107
|
scope: 'objects'
|
|
107
108
|
}, dummy).properties.query.properties
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
assert.notStrictEqual(objects.omit, undefined)
|
|
111
|
+
assert.notStrictEqual(objects.limit, undefined)
|
|
111
112
|
})
|
|
112
113
|
})
|
|
114
|
+
|
|
115
|
+
describe('source', () => {
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
const compile = (definition, entity) =>
|
|
119
|
+
schemas.schema(Request.schema(definition, entity), { removeAdditional: true })
|
|
120
|
+
|
|
121
|
+
it('should declare source', () => {
|
|
122
|
+
const schema = Request.schema({}, dummy)
|
|
123
|
+
|
|
124
|
+
assert.notStrictEqual(schema.properties.source, undefined)
|
|
125
|
+
assert.deepStrictEqual(schema.properties.source.additionalProperties, false)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should pass known source variants', () => {
|
|
129
|
+
const schema = compile({}, undefined)
|
|
130
|
+
|
|
131
|
+
for (const source of [
|
|
132
|
+
{ namespace: 'a', component: 'b', operation: 'c' },
|
|
133
|
+
{ namespace: 'a', component: 'b', event: 'c' },
|
|
134
|
+
{ service: 'exposition' }
|
|
135
|
+
]) {
|
|
136
|
+
const request = { input: null, query: null, source }
|
|
137
|
+
|
|
138
|
+
assert.deepStrictEqual(schema.fit(request), null)
|
|
139
|
+
assert.deepStrictEqual(request.source, source)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// `source` crosses the wire and keys the introspection map, so whatever
|
|
144
|
+
// a peer adds to it must not survive
|
|
145
|
+
it('should strip unknown source properties', () => {
|
|
146
|
+
const schema = compile({}, undefined)
|
|
147
|
+
const request = { input: null, query: null, source: { service: 'exposition', evil: 'x' } }
|
|
148
|
+
|
|
149
|
+
assert.deepStrictEqual(schema.fit(request), null)
|
|
150
|
+
assert.deepStrictEqual(request.source, { service: 'exposition' })
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
function resetCalls (target = [assert, clone, fixtures, dummy], seen = new Set()) {
|
|
155
|
+
if (target === null || typeof target !== 'object' || seen.has(target)) return
|
|
156
|
+
|
|
157
|
+
seen.add(target)
|
|
158
|
+
|
|
159
|
+
for (const value of Object.values(target))
|
|
160
|
+
if (typeof value === 'function' && value.mock !== undefined) value.mock.resetCalls()
|
|
161
|
+
else resetCalls(value, seen)
|
|
162
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { it, beforeEach, afterEach, mock } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
4
|
+
|
|
5
|
+
import { console } from 'openspan'
|
|
6
|
+
import { Connector } from '../src/connector.js'
|
|
7
|
+
import { Discovery } from '../src/discovery.js'
|
|
8
|
+
|
|
9
|
+
class Lookup extends Connector {
|
|
10
|
+
invoke = mock.fn(async () => ({ operations: {} }))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** @type {Lookup} */
|
|
14
|
+
let lookup
|
|
15
|
+
|
|
16
|
+
/** @type {Discovery} */
|
|
17
|
+
let discovery
|
|
18
|
+
|
|
19
|
+
/** @type {import('node:test').Mock<any>} */
|
|
20
|
+
let warn
|
|
21
|
+
|
|
22
|
+
const locator = { id: 'dummies.one' }
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
mock.timers.enable()
|
|
26
|
+
|
|
27
|
+
warn = mock.method(console, 'warn', () => undefined)
|
|
28
|
+
lookup = new Lookup()
|
|
29
|
+
discovery = new Discovery(async () => lookup)
|
|
30
|
+
|
|
31
|
+
await discovery.connect()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
warn.mock.restore()
|
|
36
|
+
mock.timers.reset()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should not warn if a lookup is answered', async () => {
|
|
40
|
+
await discovery.lookup(locator)
|
|
41
|
+
|
|
42
|
+
mock.timers.tick(60_000)
|
|
43
|
+
|
|
44
|
+
assert.strictEqual(warn.mock.callCount(), 0)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should keep warning while a lookup is unanswered', async () => {
|
|
48
|
+
lookup.invoke.mock.mockImplementation(() => new Promise(() => undefined))
|
|
49
|
+
|
|
50
|
+
void discovery.lookup(locator)
|
|
51
|
+
|
|
52
|
+
// the interval is set after the lookup is resolved, so it has to exist
|
|
53
|
+
// before time is advanced
|
|
54
|
+
await new Promise((resolve) => process.nextTick(resolve))
|
|
55
|
+
|
|
56
|
+
// node:test moves the clock before running the callbacks it releases, so the
|
|
57
|
+
// elapsed time each warning reports is only right when time advances by steps
|
|
58
|
+
mock.timers.tick(5_000)
|
|
59
|
+
mock.timers.tick(5_000)
|
|
60
|
+
|
|
61
|
+
assert.strictEqual(warn.mock.callCount(), 2)
|
|
62
|
+
|
|
63
|
+
assert.ok(((call) => call.arguments.length === 2 && isDeepStrictEqual(call.arguments[0], 'Waiting for lookup response') && isDeepStrictEqual(call.arguments[1], { component: locator.id, waiting: 10 }))(warn.mock.calls.at(-1) ?? { arguments: [] }))
|
|
64
|
+
})
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import { mock } from 'node:test'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { generate } from 'randomstring'
|
|
4
4
|
|
|
5
5
|
// noinspection JSCheckFunctionSignatures
|
|
6
|
-
const events = [0, 1, 2].map((index) => ({
|
|
7
|
-
emit:
|
|
6
|
+
export const events = [0, 1, 2].map((index) => ({
|
|
7
|
+
emit: mock.fn(async (state) => ({ ...state, event: index }))
|
|
8
8
|
}))
|
|
9
9
|
|
|
10
|
-
const event = {
|
|
10
|
+
export const event = {
|
|
11
11
|
origin: { [generate()]: generate() },
|
|
12
|
-
state: { [generate()]: generate() }
|
|
13
|
-
changeset: { [generate()]: generate() }
|
|
12
|
+
state: { [generate()]: generate() }
|
|
14
13
|
}
|
|
15
|
-
|
|
16
|
-
exports.events = events
|
|
17
|
-
exports.event = event
|
package/test/emission.test.js
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
import { it, beforeEach, mock } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
import clone from 'clone-deep'
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
import { Connector } from '../src/connector.js'
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
9
|
+
// the fixtures are not connectors, so a dependency is recorded rather than linked
|
|
10
|
+
const depends = mock.method(Connector.prototype, 'depends', () => undefined)
|
|
11
|
+
|
|
12
|
+
const dependencies = (instance) => depends.mock.calls
|
|
13
|
+
.filter((call) => call.this === instance)
|
|
14
|
+
.map((call) => call.arguments[0])
|
|
15
|
+
import { Emission } from '../src/emission.js'
|
|
16
|
+
import * as fixtures from './emission.fixtures.js'
|
|
10
17
|
|
|
11
18
|
let emission, event
|
|
12
19
|
|
|
13
20
|
beforeEach(async () => {
|
|
14
|
-
|
|
21
|
+
resetCalls()
|
|
22
|
+
depends.mock.resetCalls()
|
|
15
23
|
|
|
16
24
|
emission = new Emission(fixtures.events)
|
|
17
25
|
event = clone(fixtures.event)
|
|
@@ -20,16 +28,25 @@ beforeEach(async () => {
|
|
|
20
28
|
})
|
|
21
29
|
|
|
22
30
|
it('should depend on events', () => {
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
assert.ok(emission instanceof Connector)
|
|
32
|
+
assert.ok(dependencies(emission).some((one) => isDeepStrictEqual(one, fixtures.events)))
|
|
25
33
|
})
|
|
26
34
|
|
|
27
35
|
it('should emit events', async () => {
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
|
|
30
37
|
await emission.emit(event)
|
|
31
38
|
|
|
32
39
|
for (const evt of fixtures.events) {
|
|
33
|
-
|
|
40
|
+
assert.ok(evt.emit.mock.calls.some((call) => call.arguments.length === 1 && isDeepStrictEqual(call.arguments[0], event)))
|
|
34
41
|
}
|
|
35
42
|
})
|
|
43
|
+
|
|
44
|
+
function resetCalls (target = [assert, clone, depends, dependencies, fixtures], seen = new Set()) {
|
|
45
|
+
if (target === null || typeof target !== 'object' || seen.has(target)) return
|
|
46
|
+
|
|
47
|
+
seen.add(target)
|
|
48
|
+
|
|
49
|
+
for (const value of Object.values(target))
|
|
50
|
+
if (typeof value === 'function' && value.mock !== undefined) value.mock.resetCalls()
|
|
51
|
+
else resetCalls(value, seen)
|
|
52
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
import { mock } from 'node:test'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { generate } from 'randomstring'
|
|
4
4
|
|
|
5
5
|
// noinspection JSCheckFunctionSignatures
|
|
6
|
-
const schema = {
|
|
7
|
-
fit:
|
|
6
|
+
export const schema = {
|
|
7
|
+
fit: mock.fn((object) =>
|
|
8
8
|
(object.fail ? { [generate()]: generate() } : null)),
|
|
9
9
|
|
|
10
|
-
defaults:
|
|
10
|
+
defaults: mock.fn(() => ({ [generate()]: generate() }))
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const state = () => ({
|
|
13
|
+
export const state = () => ({
|
|
14
14
|
id: generate(),
|
|
15
15
|
foo: generate(),
|
|
16
16
|
_created: generate(),
|
|
@@ -19,11 +19,7 @@ const state = () => ({
|
|
|
19
19
|
_version: 0
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
-
const failed = () => ({
|
|
22
|
+
export const failed = () => ({
|
|
23
23
|
...state(),
|
|
24
24
|
fail: true
|
|
25
25
|
})
|
|
26
|
-
|
|
27
|
-
exports.schema = schema
|
|
28
|
-
exports.state = state
|
|
29
|
-
exports.failed = failed
|
|
@@ -1,46 +1,78 @@
|
|
|
1
|
-
|
|
1
|
+
import { describe, it, beforeEach, mock } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
import { Entity } from '../../src/entities/entity.js'
|
|
5
|
+
import * as fixtures from './entity.fixtures.js'
|
|
5
6
|
|
|
6
7
|
beforeEach(() => {
|
|
7
|
-
|
|
8
|
+
resetCalls()
|
|
8
9
|
})
|
|
9
10
|
|
|
10
|
-
describe('
|
|
11
|
-
it('should
|
|
12
|
-
const
|
|
11
|
+
describe('argument', () => {
|
|
12
|
+
it('should set state', () => {
|
|
13
|
+
const state = fixtures.state()
|
|
14
|
+
const entity = new Entity(fixtures.schema, state)
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
assert.deepStrictEqual(entity.get(), state)
|
|
15
17
|
})
|
|
16
18
|
|
|
17
|
-
it('should
|
|
18
|
-
const
|
|
19
|
-
const
|
|
19
|
+
it('should snapshot the record it may commit', () => {
|
|
20
|
+
const record = fixtures.state()
|
|
21
|
+
const entity = new Entity(fixtures.schema, record)
|
|
20
22
|
|
|
21
|
-
entity.
|
|
23
|
+
assert.notStrictEqual(entity.get(), record)
|
|
24
|
+
assert.strictEqual(entity.event().origin, record)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('read-only', () => {
|
|
29
|
+
it('should take the record as it came', () => {
|
|
30
|
+
const record = fixtures.state()
|
|
31
|
+
const entity = new Entity(fixtures.schema, record, undefined, false)
|
|
22
32
|
|
|
23
|
-
|
|
33
|
+
// no pre-image to diff against, hence no copy of it
|
|
34
|
+
assert.strictEqual(entity.get(), record)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should still report a tombstone', () => {
|
|
38
|
+
const record = { ...fixtures.state(), _deleted: Date.now() }
|
|
39
|
+
const entity = new Entity(fixtures.schema, record, undefined, false)
|
|
40
|
+
|
|
41
|
+
assert.strictEqual(entity.deleted, true)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should refuse to be modified', () => {
|
|
45
|
+
const entity = new Entity(fixtures.schema, fixtures.state(), undefined, false)
|
|
46
|
+
|
|
47
|
+
assert.throws(() => entity.set(entity.get()), (error) => /read-only/.test(error.message))
|
|
24
48
|
})
|
|
25
49
|
})
|
|
26
50
|
|
|
27
|
-
describe('
|
|
28
|
-
it('should
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
describe('tombstone', () => {
|
|
52
|
+
it('should lift tombstone when transition leaves _deleted untouched', () => {
|
|
53
|
+
const origin = fixtures.state()
|
|
54
|
+
const entity = new Entity(fixtures.schema, origin)
|
|
55
|
+
const state = entity.get()
|
|
56
|
+
|
|
57
|
+
state.foo = 'revived'
|
|
58
|
+
entity.set(state)
|
|
59
|
+
|
|
60
|
+
assert.strictEqual(entity.get()._deleted, null)
|
|
61
|
+
assert.strictEqual(entity.deleted, false)
|
|
62
|
+
assert.strictEqual(entity.event().state._deleted, null)
|
|
37
63
|
})
|
|
38
64
|
|
|
39
|
-
it('should
|
|
40
|
-
const
|
|
41
|
-
const entity = new Entity(fixtures.schema,
|
|
65
|
+
it('should keep tombstone written by transition', () => {
|
|
66
|
+
const origin = { ...fixtures.state(), _deleted: null }
|
|
67
|
+
const entity = new Entity(fixtures.schema, origin)
|
|
68
|
+
const state = entity.get()
|
|
69
|
+
const timestamp = Date.now()
|
|
70
|
+
|
|
71
|
+
state._deleted = timestamp
|
|
72
|
+
entity.set(state)
|
|
42
73
|
|
|
43
|
-
|
|
74
|
+
assert.strictEqual(entity.get()._deleted, timestamp)
|
|
75
|
+
assert.strictEqual(entity.deleted, true)
|
|
44
76
|
})
|
|
45
77
|
})
|
|
46
78
|
|
|
@@ -54,29 +86,18 @@ it('should provide event', () => {
|
|
|
54
86
|
|
|
55
87
|
const event = entity.event()
|
|
56
88
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
foo: 'new value',
|
|
62
|
-
_version: 1
|
|
63
|
-
})
|
|
64
|
-
}))
|
|
89
|
+
assert.partialDeepStrictEqual(event, { state, origin })
|
|
90
|
+
assert.strictEqual(event.state.foo, 'new value')
|
|
91
|
+
assert.strictEqual(event.state._version, 1)
|
|
92
|
+
assert.notStrictEqual(event.origin.foo, 'new value')
|
|
65
93
|
})
|
|
66
94
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const entity = new Entity(fixtures.schema, origin)
|
|
70
|
-
const state = entity.get()
|
|
71
|
-
|
|
72
|
-
expect(() => (state.id = 1)).toThrow('assign to read only property')
|
|
73
|
-
})
|
|
95
|
+
function resetCalls (target = [assert, fixtures], seen = new Set()) {
|
|
96
|
+
if (target === null || typeof target !== 'object' || seen.has(target)) return
|
|
74
97
|
|
|
75
|
-
|
|
76
|
-
const origin = fixtures.state()
|
|
77
|
-
const entity = new Entity(fixtures.schema, origin)
|
|
78
|
-
const state = entity.get()
|
|
79
|
-
const redefine = () => Object.defineProperty(state, 'id', { writable: true })
|
|
98
|
+
seen.add(target)
|
|
80
99
|
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
for (const value of Object.values(target))
|
|
101
|
+
if (typeof value === 'function' && value.mock !== undefined) value.mock.resetCalls()
|
|
102
|
+
else resetCalls(value, seen)
|
|
103
|
+
}
|
|
@@ -1,18 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
import { mock } from 'node:test'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import randomstring from 'randomstring'
|
|
4
4
|
|
|
5
5
|
const schema = { [randomstring.generate()]: randomstring.generate() }
|
|
6
|
-
const storage = { id:
|
|
6
|
+
const storage = { id: mock.fn(() => randomstring.generate()) }
|
|
7
7
|
const entity = { [randomstring.generate()]: randomstring.generate() }
|
|
8
8
|
const set = Array.from(Array(5))
|
|
9
9
|
.map((_, index) => ({ id: index, [randomstring.generate()]: randomstring.generate() }))
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
const
|
|
11
|
+
// node:test records a mock's calls but not the instances it constructed
|
|
12
|
+
const entities = []
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const Entity = mock.fn(function () {
|
|
15
|
+
this.id = randomstring.generate()
|
|
16
|
+
entities.push(this)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const EntitySet = mock.fn(function () {})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
// named `mock` for its consumers, which is what node:test calls its own tracker
|
|
28
|
+
const mocks = { Entity, EntitySet }
|
|
29
|
+
|
|
30
|
+
export { schema, storage, entity, set, entities, mocks as mock }
|
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
import { it, beforeEach, mock as mocking } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
import { generate } from 'randomstring'
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
import * as fixtures from './factory.fixtures.js'
|
|
6
8
|
const mock = fixtures.mock
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
mocking.module('../../src/entities/entity', { namedExports: ({ Entity: mock.Entity }) })
|
|
11
|
+
mocking.module('../../src/entities/set', { namedExports: ({ EntitySet: mock.EntitySet }) })
|
|
12
|
+
|
|
13
|
+
const { Factory } = await import('../../src/entities/factory.js')
|
|
10
14
|
|
|
11
|
-
const { Factory } = require('../../src/entities/factory')
|
|
12
15
|
|
|
13
16
|
let factory
|
|
14
17
|
|
|
15
18
|
beforeEach(async () => {
|
|
16
|
-
|
|
19
|
+
resetCalls()
|
|
20
|
+
fixtures.entities.length = 0
|
|
17
21
|
|
|
18
22
|
factory = new Factory(fixtures.schema, () => fixtures.storage.id())
|
|
19
23
|
})
|
|
@@ -22,27 +26,37 @@ it('should create initial', () => {
|
|
|
22
26
|
const id = generate()
|
|
23
27
|
const initial = factory.init(id)
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
assert.ok(initial instanceof mock.Entity)
|
|
30
|
+
assert.ok(mock.Entity.mock.calls.some((call) => call.arguments.length === 3 && isDeepStrictEqual(call.arguments[0], fixtures.schema) && isDeepStrictEqual(call.arguments[1], id) && typeof call.arguments[2] === 'function'))
|
|
27
31
|
})
|
|
28
32
|
|
|
29
33
|
it('should create instance', () => {
|
|
30
34
|
const object = factory.object(fixtures.entity)
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
assert.ok(object instanceof mock.Entity)
|
|
37
|
+
assert.ok(mock.Entity.mock.calls.some((call) => call.arguments.length === 4 && isDeepStrictEqual(call.arguments[0], fixtures.schema) && isDeepStrictEqual(call.arguments[1], fixtures.entity) && typeof call.arguments[2] === 'function' && isDeepStrictEqual(call.arguments[3], true)))
|
|
34
38
|
})
|
|
35
39
|
|
|
36
40
|
it('should create set', () => {
|
|
37
41
|
const objects = factory.objects(fixtures.set)
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
assert.ok(objects instanceof mock.EntitySet)
|
|
40
44
|
|
|
41
45
|
const instances = fixtures.set.map((entity, index) => {
|
|
42
|
-
|
|
46
|
+
assert.ok(((call) => call.arguments.length === 4 && isDeepStrictEqual(call.arguments[0], fixtures.schema) && isDeepStrictEqual(call.arguments[1], entity) && typeof call.arguments[2] === 'function' && isDeepStrictEqual(call.arguments[3], true))(mock.Entity.mock.calls[index + 1 - 1] ?? { arguments: [] }))
|
|
43
47
|
|
|
44
|
-
return
|
|
48
|
+
return fixtures.entities[index]
|
|
45
49
|
})
|
|
46
50
|
|
|
47
|
-
|
|
51
|
+
assert.ok(mock.EntitySet.mock.calls.some((call) => call.arguments.length === 1 && isDeepStrictEqual(call.arguments[0], instances)))
|
|
48
52
|
})
|
|
53
|
+
|
|
54
|
+
function resetCalls (target = [assert, fixtures, mock], seen = new Set()) {
|
|
55
|
+
if (target === null || typeof target !== 'object' || seen.has(target)) return
|
|
56
|
+
|
|
57
|
+
seen.add(target)
|
|
58
|
+
|
|
59
|
+
for (const value of Object.values(target))
|
|
60
|
+
if (typeof value === 'function' && value.mock !== undefined) value.mock.resetCalls()
|
|
61
|
+
else resetCalls(value, seen)
|
|
62
|
+
}
|