@toa.io/storages.mongodb 1.0.0-alpha.31 → 1.0.0-alpha.310
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 +203 -0
- package/package.json +10 -9
- package/readme.md +15 -0
- package/src/client.js +173 -39
- package/src/factory.js +6 -12
- package/src/inbox.js +85 -0
- package/src/index.js +1 -7
- package/src/indexes.js +84 -0
- package/src/measurements.js +39 -0
- package/src/migrations.js +343 -0
- package/src/outbox.js +131 -0
- package/src/record.js +88 -20
- package/src/storage.js +497 -130
- package/src/translate/criteria.js +25 -16
- package/src/translate/options.js +17 -11
- package/src/translate/rename.js +1 -5
- package/src/translate.js +14 -15
- package/test/migrations.test.js +388 -0
- package/test/query/criteria.fixtures.js +2 -7
- package/test/query/criteria.test.js +5 -4
- package/test/record.test.js +98 -25
- package/test/storage.test.js +217 -0
- package/test/translate.test.js +59 -7
- package/types/connection.d.ts +18 -12
- package/types/pointer.d.ts +4 -6
- package/types/record.d.ts +5 -7
- package/src/collection.js +0 -69
- package/src/deployment.js +0 -22
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const { rename } = require('./rename')
|
|
1
|
+
import { rename } from './rename.js'
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
|
-
* @param {toa.core.storages.
|
|
4
|
+
* @param {import('@toa.io/core/types').storages.Node} node
|
|
5
|
+
* @param {string[]} dates properties held as BSON dates
|
|
7
6
|
* @returns {import('mongodb').Filter}
|
|
8
7
|
*/
|
|
9
|
-
const criteria = (node) => {
|
|
10
|
-
if (TYPES[node.type] === undefined)
|
|
8
|
+
export const criteria = (node, dates = NONE) => {
|
|
9
|
+
if (TYPES[node.type] === undefined)
|
|
10
|
+
throw new Error(`AST parse error: unknown node type '${node.type}'`)
|
|
11
11
|
|
|
12
|
-
return TYPES[node.type](node)
|
|
12
|
+
return TYPES[node.type](node, dates)
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const OPERATORS = {
|
|
@@ -33,24 +33,33 @@ const OPERATORS = {
|
|
|
33
33
|
|
|
34
34
|
const TYPES = {}
|
|
35
35
|
|
|
36
|
-
TYPES.LOGIC = (expression) => {
|
|
37
|
-
const left = criteria(expression.left)
|
|
38
|
-
const right = criteria(expression.right)
|
|
36
|
+
TYPES.LOGIC = (expression, dates) => {
|
|
37
|
+
const left = criteria(expression.left, dates)
|
|
38
|
+
const right = criteria(expression.right, dates)
|
|
39
39
|
|
|
40
40
|
return { [OPERATORS.LOGIC[expression.operator]]: [left, right] }
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
TYPES.COMPARISON = (expression) => {
|
|
44
|
-
const left = criteria(expression.left)
|
|
45
|
-
const right = criteria(expression.right)
|
|
43
|
+
TYPES.COMPARISON = (expression, dates) => {
|
|
44
|
+
const left = criteria(expression.left, dates)
|
|
45
|
+
const right = criteria(expression.right, dates)
|
|
46
46
|
const operator = OPERATORS.COMPARISON[expression.operator]
|
|
47
47
|
|
|
48
|
-
if (operator === undefined)
|
|
48
|
+
if (operator === undefined)
|
|
49
|
+
throw new Error(`AST parse error: unknown operator '${expression.operator}'`)
|
|
50
|
+
|
|
51
|
+
// the record holds a date where the criterion carries the string the entity declares, and a
|
|
52
|
+
// string compared against a date is a criterion that quietly matches nothing
|
|
53
|
+
const value = dates.includes(left) ? date(right) : right
|
|
49
54
|
|
|
50
|
-
return { [left]: { [operator]:
|
|
55
|
+
return { [left]: { [operator]: value } }
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
TYPES.SELECTOR = (expression) => rename(expression.selector)
|
|
54
59
|
TYPES.VALUE = (expression) => expression.value
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
/** `=in=` and `=out=` carry a list, and every other operator one value. */
|
|
62
|
+
const date = (value) =>
|
|
63
|
+
Array.isArray(value) ? value.map(date) : value === null ? null : new Date(value)
|
|
64
|
+
|
|
65
|
+
const NONE = []
|
package/src/translate/options.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
import { rename } from './rename.js'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const options = (options) => {
|
|
3
|
+
export const options = (options) => {
|
|
6
4
|
const result = {}
|
|
7
5
|
|
|
8
|
-
if (options.omit) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (options.
|
|
6
|
+
if (options.omit) {
|
|
7
|
+
result.skip = options.omit
|
|
8
|
+
}
|
|
9
|
+
if (options.limit) {
|
|
10
|
+
result.limit = options.limit
|
|
11
|
+
}
|
|
12
|
+
if (options.sort) {
|
|
13
|
+
result.sort = sort(options.sort)
|
|
14
|
+
}
|
|
15
|
+
if (options.projection) {
|
|
16
|
+
result.projection = projection(options.projection)
|
|
17
|
+
}
|
|
12
18
|
|
|
13
19
|
return result
|
|
14
20
|
}
|
|
@@ -20,7 +26,9 @@ const sort = (sort) => {
|
|
|
20
26
|
const projection = (projection) => {
|
|
21
27
|
const result = {}
|
|
22
28
|
|
|
23
|
-
for (const property of projection) {
|
|
29
|
+
for (const property of projection) {
|
|
30
|
+
result[rename(property)] = 1
|
|
31
|
+
}
|
|
24
32
|
|
|
25
33
|
return result
|
|
26
34
|
}
|
|
@@ -29,5 +37,3 @@ const DIRECTIONS = {
|
|
|
29
37
|
asc: 1,
|
|
30
38
|
desc: -1
|
|
31
39
|
}
|
|
32
|
-
|
|
33
|
-
exports.options = options
|
package/src/translate/rename.js
CHANGED
package/src/translate.js
CHANGED
|
@@ -1,28 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
import * as _criteria from './translate/criteria.js'
|
|
2
|
+
import * as _options from './translate/options.js'
|
|
2
3
|
|
|
3
|
-
const parse = { ...
|
|
4
|
+
const parse = { ..._criteria, ..._options }
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* @param {toa.core.storages.Query} query
|
|
7
|
+
* @param {import('@toa.io/core/types').storages.Query} query
|
|
8
|
+
* @param {string[]} [dates] properties held as BSON dates, so a criterion against one is one too
|
|
7
9
|
* @returns {{criteria: Object, options: Object}}
|
|
8
10
|
*/
|
|
9
|
-
const translate = (query) => {
|
|
11
|
+
export const translate = (query, dates) => {
|
|
10
12
|
const result = {
|
|
11
|
-
criteria: query?.criteria === undefined ? {} : parse.criteria(query.criteria),
|
|
12
|
-
options: query?.options === undefined ? {} : parse.options(query.options)
|
|
13
|
+
criteria: query?.criteria === undefined ? {} : parse.criteria(query.criteria, dates),
|
|
14
|
+
options: query?.options === undefined ? {} : parse.options(query.options),
|
|
15
|
+
sample: query?.options?.sample
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
if (query?.id !== undefined)
|
|
16
|
-
result.criteria._id = query.id
|
|
17
|
-
}
|
|
18
|
+
if (query?.id !== undefined) result.criteria._id = query.id
|
|
18
19
|
|
|
19
|
-
if (query?.
|
|
20
|
-
result.criteria._version = query.version
|
|
21
|
-
}
|
|
20
|
+
if (query?.ids !== undefined) result.criteria._id = { $in: query.ids }
|
|
22
21
|
|
|
23
|
-
result.criteria.
|
|
22
|
+
if (query?.version !== undefined) result.criteria.VERSION = query.version
|
|
23
|
+
|
|
24
|
+
if (query?.search !== undefined) result.criteria.$text = { $search: query.search }
|
|
24
25
|
|
|
25
26
|
return result
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
-
exports.translate = translate
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, mock } from 'node:test'
|
|
2
|
+
import { console } from 'openspan'
|
|
3
|
+
import assert from 'node:assert/strict'
|
|
4
|
+
|
|
5
|
+
import { Migrations, STATE } from '../src/migrations.js'
|
|
6
|
+
|
|
7
|
+
let collection
|
|
8
|
+
let state
|
|
9
|
+
let db
|
|
10
|
+
let rows
|
|
11
|
+
|
|
12
|
+
/** whatever `insertOne` has accepted, keyed as MongoDB would key it */
|
|
13
|
+
function stateCollection() {
|
|
14
|
+
return {
|
|
15
|
+
insertOne: mock.fn(async (row) => {
|
|
16
|
+
if (rows.has(row._id)) throw Object.assign(new Error('duplicate'), { code: 11000 })
|
|
17
|
+
|
|
18
|
+
rows.set(row._id, { ...row })
|
|
19
|
+
|
|
20
|
+
return { insertedId: row._id }
|
|
21
|
+
}),
|
|
22
|
+
findOne: mock.fn(async ({ _id }) => rows.get(_id) ?? null),
|
|
23
|
+
updateOne: mock.fn(async (criteria, { $set }) => {
|
|
24
|
+
const row = rows.get(criteria._id)
|
|
25
|
+
|
|
26
|
+
if (row === undefined) return { modifiedCount: 0 }
|
|
27
|
+
|
|
28
|
+
for (const [key, value] of Object.entries(criteria))
|
|
29
|
+
if (key !== '_id' && String(row[key]) !== String(value))
|
|
30
|
+
return { modifiedCount: 0 }
|
|
31
|
+
|
|
32
|
+
Object.assign(row, $set)
|
|
33
|
+
|
|
34
|
+
return { modifiedCount: 1 }
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
rows = new Map()
|
|
41
|
+
|
|
42
|
+
collection = {
|
|
43
|
+
collectionName: 'test_one',
|
|
44
|
+
createIndex: mock.fn(async () => undefined),
|
|
45
|
+
dropIndex: mock.fn(async () => undefined),
|
|
46
|
+
updateMany: mock.fn(async () => ({ modifiedCount: 2 })),
|
|
47
|
+
deleteMany: mock.fn(async () => ({ deletedCount: 1 }))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
state = stateCollection()
|
|
51
|
+
db = { collection: mock.fn((name) => (name === STATE ? state : null)) }
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const run = (list) => new Migrations(db, collection, list).run()
|
|
55
|
+
|
|
56
|
+
describe('steps', () => {
|
|
57
|
+
it('should create an index', async () => {
|
|
58
|
+
await run([
|
|
59
|
+
{
|
|
60
|
+
id: '0001',
|
|
61
|
+
steps: [
|
|
62
|
+
{
|
|
63
|
+
index: {
|
|
64
|
+
name: 'index_a',
|
|
65
|
+
keys: { a: 'asc', b: 'desc', c: 'hash', d: 'text' }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
assert.deepEqual(collection.createIndex.mock.calls[0].arguments, [
|
|
73
|
+
{ a: 1, b: -1, c: 'hashed', d: 'text' },
|
|
74
|
+
{ name: 'index_a' }
|
|
75
|
+
])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should translate index options', async () => {
|
|
79
|
+
await run([
|
|
80
|
+
{
|
|
81
|
+
id: '0001',
|
|
82
|
+
steps: [
|
|
83
|
+
{
|
|
84
|
+
index: {
|
|
85
|
+
name: 'unique_a',
|
|
86
|
+
keys: { a: 'asc' },
|
|
87
|
+
unique: true,
|
|
88
|
+
sparse: true,
|
|
89
|
+
partial: { a: { $exists: true } },
|
|
90
|
+
ttl: 60
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
assert.deepEqual(collection.createIndex.mock.calls[0].arguments[1], {
|
|
98
|
+
name: 'unique_a',
|
|
99
|
+
unique: true,
|
|
100
|
+
sparse: true,
|
|
101
|
+
partialFilterExpression: { a: { $exists: true } },
|
|
102
|
+
expireAfterSeconds: 60
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should refuse an unknown index option', async () => {
|
|
107
|
+
await assert.rejects(
|
|
108
|
+
run([
|
|
109
|
+
{
|
|
110
|
+
id: '0001',
|
|
111
|
+
steps: [{ index: { name: 'a', keys: { a: 'asc' }, backgorund: true } }]
|
|
112
|
+
}
|
|
113
|
+
]),
|
|
114
|
+
/unknown index option 'backgorund'/
|
|
115
|
+
)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should refuse an index without keys', async () => {
|
|
119
|
+
await assert.rejects(
|
|
120
|
+
run([{ id: '0001', steps: [{ index: { name: 'a' } }] }]),
|
|
121
|
+
/without a name or keys/
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should recreate an index whose declaration changed', async () => {
|
|
126
|
+
let conflicted = false
|
|
127
|
+
|
|
128
|
+
collection.createIndex = mock.fn(async () => {
|
|
129
|
+
if (conflicted) return undefined
|
|
130
|
+
|
|
131
|
+
conflicted = true
|
|
132
|
+
|
|
133
|
+
throw Object.assign(new Error('conflict'), { code: 85 })
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
await run([
|
|
137
|
+
{ id: '0001', steps: [{ index: { name: 'index_a', keys: { a: 'asc' } } }] }
|
|
138
|
+
])
|
|
139
|
+
|
|
140
|
+
assert.deepEqual(collection.dropIndex.mock.calls[0].arguments, ['index_a'])
|
|
141
|
+
assert.equal(collection.createIndex.mock.callCount(), 2)
|
|
142
|
+
assert.equal(rows.get('test_one:0001').state, 'done')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should not swallow a creation error that is not a conflict', async () => {
|
|
146
|
+
collection.createIndex = mock.fn(async () => {
|
|
147
|
+
throw Object.assign(new Error('nope'), { code: 13 })
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
await assert.rejects(
|
|
151
|
+
run([{ id: '0001', steps: [{ index: { name: 'a', keys: { a: 'asc' } } }] }]),
|
|
152
|
+
/nope/
|
|
153
|
+
)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('should drop an index', async () => {
|
|
157
|
+
await run([{ id: '0001', steps: [{ dropIndex: { name: 'index_a' } }] }])
|
|
158
|
+
|
|
159
|
+
assert.deepEqual(collection.dropIndex.mock.calls[0].arguments, ['index_a'])
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should ignore dropping an index that is not there', async () => {
|
|
163
|
+
collection.dropIndex = mock.fn(async () => {
|
|
164
|
+
throw Object.assign(new Error('missing'), { code: 27 })
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
await run([{ id: '0001', steps: [{ dropIndex: { name: 'index_a' } }] }])
|
|
168
|
+
|
|
169
|
+
assert.equal(rows.get('test_one:0001').state, 'done')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should update', async () => {
|
|
173
|
+
await run([
|
|
174
|
+
{
|
|
175
|
+
id: '0001',
|
|
176
|
+
steps: [{ update: { filter: { a: 1 }, update: { $set: { b: 2 } } } }]
|
|
177
|
+
}
|
|
178
|
+
])
|
|
179
|
+
|
|
180
|
+
assert.deepEqual(collection.updateMany.mock.calls[0].arguments, [
|
|
181
|
+
{ a: 1 },
|
|
182
|
+
{ $set: { b: 2 } }
|
|
183
|
+
])
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should update every record when no filter is given', async () => {
|
|
187
|
+
await run([{ id: '0001', steps: [{ update: { update: { $set: { b: 2 } } } }] }])
|
|
188
|
+
|
|
189
|
+
assert.deepEqual(collection.updateMany.mock.calls[0].arguments[0], {})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should delete', async () => {
|
|
193
|
+
await run([{ id: '0001', steps: [{ delete: { filter: { a: 1 } } }] }])
|
|
194
|
+
|
|
195
|
+
assert.deepEqual(collection.deleteMany.mock.calls[0].arguments, [{ a: 1 }])
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('should refuse deleting without a filter', async () => {
|
|
199
|
+
await assert.rejects(
|
|
200
|
+
run([{ id: '0001', steps: [{ delete: {} }] }]),
|
|
201
|
+
/deletes without a filter/
|
|
202
|
+
)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('should refuse a step that is not a verb', async () => {
|
|
206
|
+
await assert.rejects(
|
|
207
|
+
run([{ id: '0001', steps: [{ truncate: {} }] }]),
|
|
208
|
+
/has a step that is not one of/
|
|
209
|
+
)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should refuse a step naming two verbs', async () => {
|
|
213
|
+
await assert.rejects(
|
|
214
|
+
run([{ id: '0001', steps: [{ update: {}, delete: {} }] }]),
|
|
215
|
+
/has a step that is not one of/
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should apply steps in order', async () => {
|
|
220
|
+
const order = []
|
|
221
|
+
|
|
222
|
+
collection.createIndex = mock.fn(async () => order.push('index'))
|
|
223
|
+
collection.updateMany = mock.fn(async () => {
|
|
224
|
+
order.push('update')
|
|
225
|
+
return { modifiedCount: 0 }
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
await run([
|
|
229
|
+
{
|
|
230
|
+
id: '0001',
|
|
231
|
+
steps: [
|
|
232
|
+
{ update: { update: {} } },
|
|
233
|
+
{ index: { name: 'a', keys: { a: 'asc' } } },
|
|
234
|
+
{ update: { update: {} } }
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
])
|
|
238
|
+
|
|
239
|
+
assert.deepEqual(order, ['update', 'index', 'update'])
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('state', () => {
|
|
244
|
+
it('should apply migrations in order', async () => {
|
|
245
|
+
const order = []
|
|
246
|
+
|
|
247
|
+
collection.updateMany = mock.fn(async (filter) => {
|
|
248
|
+
order.push(filter.n)
|
|
249
|
+
return { modifiedCount: 0 }
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
await run([
|
|
253
|
+
{ id: '0001', steps: [{ update: { filter: { n: 1 }, update: {} } }] },
|
|
254
|
+
{ id: '0002', steps: [{ update: { filter: { n: 2 }, update: {} } }] }
|
|
255
|
+
])
|
|
256
|
+
|
|
257
|
+
assert.deepEqual(order, [1, 2])
|
|
258
|
+
assert.deepEqual([...rows.keys()], ['test_one:0001', 'test_one:0002'])
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('should name a row after the collection and the migration', async () => {
|
|
262
|
+
await run([{ id: '0001-indexes', steps: [] }])
|
|
263
|
+
|
|
264
|
+
assert.equal(rows.has('test_one:0001-indexes'), true)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should not apply a migration that is done', async () => {
|
|
268
|
+
rows.set('test_one:0001', { _id: 'test_one:0001', state: 'done' })
|
|
269
|
+
|
|
270
|
+
await run([{ id: '0001', steps: [{ update: { update: {} } }] }])
|
|
271
|
+
|
|
272
|
+
assert.equal(collection.updateMany.mock.callCount(), 0)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('should leave the row running where a step throws', async () => {
|
|
276
|
+
collection.updateMany = mock.fn(async () => {
|
|
277
|
+
throw new Error('nope')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
await assert.rejects(
|
|
281
|
+
run([{ id: '0001', steps: [{ update: { update: {} } }] }]),
|
|
282
|
+
/nope/
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
assert.equal(rows.get('test_one:0001').state, 'running')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('should wait for a claim whose owner is alive', async () => {
|
|
289
|
+
rows.set('test_one:0001', {
|
|
290
|
+
_id: 'test_one:0001',
|
|
291
|
+
state: 'running',
|
|
292
|
+
owner: 'other/1',
|
|
293
|
+
heartbeat: new Date()
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
const pending = run([{ id: '0001', steps: [{ update: { update: {} } }] }])
|
|
297
|
+
const settled = await Promise.race([
|
|
298
|
+
pending.then(() => 'done'),
|
|
299
|
+
new Promise((resolve) => setTimeout(() => resolve('waiting'), 1500))
|
|
300
|
+
])
|
|
301
|
+
|
|
302
|
+
assert.equal(settled, 'waiting')
|
|
303
|
+
assert.equal(collection.updateMany.mock.callCount(), 0)
|
|
304
|
+
|
|
305
|
+
// let it finish, so the test does not leave a promise polling
|
|
306
|
+
rows.get('test_one:0001').state = 'done'
|
|
307
|
+
|
|
308
|
+
await pending
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('should take over a claim whose owner stopped', async () => {
|
|
312
|
+
rows.set('test_one:0001', {
|
|
313
|
+
_id: 'test_one:0001',
|
|
314
|
+
state: 'running',
|
|
315
|
+
owner: 'dead/1',
|
|
316
|
+
heartbeat: new Date(Date.now() - 60_000)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
await run([{ id: '0001', steps: [{ update: { update: {} } }] }])
|
|
320
|
+
|
|
321
|
+
assert.equal(collection.updateMany.mock.callCount(), 1)
|
|
322
|
+
assert.equal(rows.get('test_one:0001').state, 'done')
|
|
323
|
+
assert.notEqual(rows.get('test_one:0001').owner, 'dead/1')
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('should apply a migration once across concurrent replicas', async () => {
|
|
327
|
+
const list = [{ id: '0001', steps: [{ update: { update: {} } }] }]
|
|
328
|
+
|
|
329
|
+
await Promise.all(Array.from({ length: 5 }, () => run(list)))
|
|
330
|
+
|
|
331
|
+
assert.equal(collection.updateMany.mock.callCount(), 1)
|
|
332
|
+
assert.equal(rows.size, 1)
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
describe('logging', () => {
|
|
337
|
+
let info
|
|
338
|
+
let debug
|
|
339
|
+
|
|
340
|
+
const messages = (spy) => spy.mock.calls.map((call) => call.arguments[0])
|
|
341
|
+
|
|
342
|
+
beforeEach(() => {
|
|
343
|
+
info = mock.method(console, 'info', () => undefined)
|
|
344
|
+
debug = mock.method(console, 'debug', () => undefined)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
afterEach(() => {
|
|
348
|
+
info.mock.restore()
|
|
349
|
+
debug.mock.restore()
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('should report a migration it applies', async () => {
|
|
353
|
+
await run([{ id: '0001', steps: [{ update: { update: {} } }] }])
|
|
354
|
+
|
|
355
|
+
assert.ok(messages(info).includes('Applying migration'))
|
|
356
|
+
assert.ok(messages(info).includes('Migration applied'))
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('should say at debug that a migration was applied already', async () => {
|
|
360
|
+
const list = [{ id: '0001', steps: [{ update: { update: {} } }] }]
|
|
361
|
+
|
|
362
|
+
await run(list)
|
|
363
|
+
|
|
364
|
+
info.mock.resetCalls()
|
|
365
|
+
debug.mock.resetCalls()
|
|
366
|
+
|
|
367
|
+
await run(list)
|
|
368
|
+
|
|
369
|
+
assert.deepStrictEqual(messages(info), [], 'the ordinary start has nothing to report')
|
|
370
|
+
assert.ok(messages(debug).includes('Migration was applied already'))
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('should say how many of the declared ones it applied', async () => {
|
|
374
|
+
await run([
|
|
375
|
+
{ id: '0001', steps: [{ update: { update: {} } }] },
|
|
376
|
+
{ id: '0002', steps: [{ update: { update: {} } }] }
|
|
377
|
+
])
|
|
378
|
+
|
|
379
|
+
const [message, attributes] = debug.mock.calls.at(-1).arguments
|
|
380
|
+
|
|
381
|
+
assert.strictEqual(message, 'Migrations checked')
|
|
382
|
+
assert.deepStrictEqual(attributes, {
|
|
383
|
+
collection: 'test_one',
|
|
384
|
+
declared: 2,
|
|
385
|
+
applied: 2
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
})
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const ast = {
|
|
1
|
+
export const ast = {
|
|
4
2
|
left: {
|
|
5
3
|
type: 'SELECTOR',
|
|
6
4
|
selector: 'id'
|
|
@@ -13,9 +11,6 @@ const ast = {
|
|
|
13
11
|
}
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
const criteria = {
|
|
14
|
+
export const criteria = {
|
|
17
15
|
_id: { $eq: 100500 }
|
|
18
16
|
}
|
|
19
|
-
|
|
20
|
-
exports.ast = ast
|
|
21
|
-
exports.criteria = criteria
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import { it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
import * as fixtures from './criteria.fixtures.js'
|
|
5
|
+
import { criteria } from '../../src/translate/criteria.js'
|
|
5
6
|
|
|
6
7
|
it('should translate', () => {
|
|
7
8
|
const result = criteria(fixtures.ast)
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
assert.deepStrictEqual(result, fixtures.criteria)
|
|
10
11
|
})
|