@toa.io/storages.mongodb 1.0.0-alpha.20 → 1.0.0-alpha.201
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/package.json +8 -8
- package/src/client.js +26 -9
- package/src/factory.js +1 -3
- package/src/record.js +6 -19
- package/src/storage.js +224 -80
- package/src/translate.js +9 -6
- package/test/record.test.js +0 -15
- package/src/collection.js +0 -69
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/storages.mongodb",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.201",
|
|
4
4
|
"description": "Toa MongoDB Storage Connector",
|
|
5
5
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/toa-io/toa#readme",
|
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@toa.io/
|
|
23
|
-
"@toa.io/
|
|
24
|
-
"@toa.io/
|
|
25
|
-
"@toa.io/
|
|
26
|
-
"
|
|
27
|
-
"
|
|
22
|
+
"@toa.io/conveyor": "1.0.0-alpha.173",
|
|
23
|
+
"@toa.io/core": "1.0.0-alpha.201",
|
|
24
|
+
"@toa.io/generic": "1.0.0-alpha.173",
|
|
25
|
+
"@toa.io/pointer": "1.0.0-alpha.200",
|
|
26
|
+
"mongodb": "7.1.0",
|
|
27
|
+
"openspan": "1.0.0-alpha.173",
|
|
28
28
|
"saslprep": "1.0.3"
|
|
29
29
|
},
|
|
30
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "e4955f36b0f2519516bc4be7c71e55a0e7cfdd44"
|
|
31
31
|
}
|
package/src/client.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @typedef {import('@toa.io/core').Locator} Locator
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
const { console } = require('openspan')
|
|
9
10
|
const { Connector } = require('@toa.io/core')
|
|
10
11
|
const { resolve } = require('@toa.io/pointer')
|
|
11
12
|
const { ID } = require('./deployment')
|
|
@@ -17,6 +18,8 @@ const { MongoClient } = require('mongodb')
|
|
|
17
18
|
const INSTANCES = {}
|
|
18
19
|
|
|
19
20
|
class Client extends Connector {
|
|
21
|
+
name
|
|
22
|
+
|
|
20
23
|
/**
|
|
21
24
|
* @public
|
|
22
25
|
* @type {import('mongodb').Collection}
|
|
@@ -48,6 +51,7 @@ class Client extends Connector {
|
|
|
48
51
|
super()
|
|
49
52
|
|
|
50
53
|
this.locator = locator
|
|
54
|
+
this.name = locator.lowercase
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
/**
|
|
@@ -57,17 +61,30 @@ class Client extends Connector {
|
|
|
57
61
|
*/
|
|
58
62
|
async open () {
|
|
59
63
|
const urls = await this.resolveURLs()
|
|
60
|
-
const
|
|
61
|
-
const collection = this.locator.lowercase
|
|
64
|
+
const dbname = this.resolveDB()
|
|
62
65
|
|
|
63
|
-
this.key = getKey(
|
|
66
|
+
this.key = getKey(dbname, urls)
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
try {
|
|
69
|
+
INSTANCES[this.key] ??= this.createInstance(urls)
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Failed to connect to MongoDB', { urls, error })
|
|
72
|
+
}
|
|
66
73
|
|
|
67
74
|
this.instance = await INSTANCES[this.key]
|
|
68
75
|
this.instance.count++
|
|
69
76
|
|
|
70
|
-
|
|
77
|
+
const db = this.instance.client.db(dbname)
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
this.collection = await db.createCollection(this.name)
|
|
81
|
+
} catch (e) {
|
|
82
|
+
if (e.code !== ALREADY_EXISTS) {
|
|
83
|
+
throw e
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.collection = db.collection(this.name)
|
|
87
|
+
}
|
|
71
88
|
}
|
|
72
89
|
|
|
73
90
|
/**
|
|
@@ -95,7 +112,7 @@ class Client extends Connector {
|
|
|
95
112
|
const client = new MongoClient(urls.join(','), OPTIONS)
|
|
96
113
|
const hosts = urls.map((str) => new URL(str).host)
|
|
97
114
|
|
|
98
|
-
console.info('Connecting to MongoDB
|
|
115
|
+
console.info('Connecting to MongoDB', { address: hosts.join(', ') })
|
|
99
116
|
|
|
100
117
|
await client.connect()
|
|
101
118
|
|
|
@@ -139,9 +156,9 @@ function getKey (db, urls) {
|
|
|
139
156
|
}
|
|
140
157
|
|
|
141
158
|
const OPTIONS = {
|
|
142
|
-
ignoreUndefined: true
|
|
143
|
-
connectTimeoutMS: 0,
|
|
144
|
-
serverSelectionTimeoutMS: 0
|
|
159
|
+
ignoreUndefined: true
|
|
145
160
|
}
|
|
146
161
|
|
|
162
|
+
const ALREADY_EXISTS = 48
|
|
163
|
+
|
|
147
164
|
exports.Client = Client
|
package/src/factory.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { Client } = require('./client')
|
|
4
|
-
const { Collection } = require('./collection')
|
|
5
4
|
const { Storage } = require('./storage')
|
|
6
5
|
|
|
7
6
|
class Factory {
|
|
8
7
|
storage (locator, entity) {
|
|
9
8
|
const client = new Client(locator)
|
|
10
|
-
const connection = new Collection(client)
|
|
11
9
|
|
|
12
|
-
return new Storage(
|
|
10
|
+
return new Storage(client, entity)
|
|
13
11
|
}
|
|
14
12
|
}
|
|
15
13
|
|
package/src/record.js
CHANGED
|
@@ -1,29 +1,16 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* @returns {toa.mongodb.Record}
|
|
6
|
-
*/
|
|
7
|
-
const to = (entity) => {
|
|
8
|
-
const {
|
|
9
|
-
id,
|
|
10
|
-
...rest
|
|
11
|
-
} = entity
|
|
3
|
+
function to (entity) {
|
|
4
|
+
const { id, ...rest } = entity
|
|
12
5
|
|
|
13
6
|
return /** @type {toa.mongodb.Record} */ { _id: id, ...rest }
|
|
14
7
|
}
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*/
|
|
20
|
-
const from = (record) => {
|
|
21
|
-
if (record === undefined || record === null) return null
|
|
9
|
+
function from (record) {
|
|
10
|
+
if (record === undefined || record === null)
|
|
11
|
+
return null
|
|
22
12
|
|
|
23
|
-
const {
|
|
24
|
-
_id,
|
|
25
|
-
...rest
|
|
26
|
-
} = record
|
|
13
|
+
const { _id, ...rest } = record
|
|
27
14
|
|
|
28
15
|
return { id: _id, ...rest }
|
|
29
16
|
}
|
package/src/storage.js
CHANGED
|
@@ -1,58 +1,86 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
|
|
5
|
-
exceptions
|
|
6
|
-
} = require('@toa.io/core')
|
|
7
|
-
|
|
3
|
+
const { Connector, exceptions } = require('@toa.io/core')
|
|
4
|
+
const { console } = require('openspan')
|
|
8
5
|
const { translate } = require('./translate')
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
from
|
|
12
|
-
} = require('./record')
|
|
6
|
+
const { to, from } = require('./record')
|
|
7
|
+
const { ReturnDocument } = require('mongodb')
|
|
13
8
|
|
|
14
9
|
class Storage extends Connector {
|
|
15
|
-
#
|
|
10
|
+
#client
|
|
11
|
+
|
|
12
|
+
/** @type {import('mongodb').Collection} */
|
|
13
|
+
#collection
|
|
16
14
|
#entity
|
|
17
15
|
|
|
18
|
-
constructor (
|
|
16
|
+
constructor (client, entity) {
|
|
19
17
|
super()
|
|
20
18
|
|
|
21
|
-
this.#
|
|
19
|
+
this.#client = client
|
|
22
20
|
this.#entity = entity
|
|
23
21
|
|
|
24
|
-
this.depends(
|
|
22
|
+
this.depends(client)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get raw () {
|
|
26
|
+
return this.#collection
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
async open () {
|
|
30
|
+
this.#collection = this.#client.collection
|
|
31
|
+
|
|
28
32
|
await this.index()
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
async get (query) {
|
|
32
|
-
const {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} = translate(query)
|
|
36
|
+
const { criteria, options } = translate(query)
|
|
37
|
+
|
|
38
|
+
this.debug('findOne', { criteria, options })
|
|
36
39
|
|
|
37
|
-
const record = await this.#
|
|
40
|
+
const record = await this.#collection.findOne(criteria, options)
|
|
38
41
|
|
|
39
42
|
return from(record)
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
async find (query) {
|
|
43
|
-
const {
|
|
44
|
-
criteria,
|
|
45
|
-
options
|
|
46
|
-
} = translate(query)
|
|
46
|
+
const { criteria, options, sample } = translate(query)
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
if (query?.options?.deleted !== true)
|
|
49
|
+
criteria._deleted = null
|
|
50
|
+
|
|
51
|
+
let cursor
|
|
52
|
+
|
|
53
|
+
if (sample === undefined) {
|
|
54
|
+
this.debug('find', { criteria, options })
|
|
55
|
+
|
|
56
|
+
cursor = this.#collection.find(criteria, options)
|
|
57
|
+
} else {
|
|
58
|
+
const pipeline = toPipeline(criteria, options, sample)
|
|
59
|
+
|
|
60
|
+
this.debug('aggregate', { pipeline })
|
|
61
|
+
|
|
62
|
+
cursor = this.#collection.aggregate(pipeline)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const recordset = await cursor.toArray()
|
|
49
66
|
|
|
50
67
|
return recordset.map((item) => from(item))
|
|
51
68
|
}
|
|
52
69
|
|
|
70
|
+
async stream (query = undefined) {
|
|
71
|
+
const { criteria, options } = translate(query)
|
|
72
|
+
|
|
73
|
+
this.debug('find (stream)', { criteria, options })
|
|
74
|
+
|
|
75
|
+
return this.#collection.find(criteria, options).stream({ transform: from })
|
|
76
|
+
}
|
|
77
|
+
|
|
53
78
|
async add (entity) {
|
|
54
79
|
const record = to(entity)
|
|
55
|
-
|
|
80
|
+
|
|
81
|
+
this.debug('insertOne', { record })
|
|
82
|
+
|
|
83
|
+
const result = await this.#collection.insertOne(record)
|
|
56
84
|
|
|
57
85
|
return result.acknowledged
|
|
58
86
|
}
|
|
@@ -62,41 +90,69 @@ class Storage extends Connector {
|
|
|
62
90
|
_id: entity.id,
|
|
63
91
|
_version: entity._version - 1
|
|
64
92
|
}
|
|
65
|
-
|
|
93
|
+
|
|
94
|
+
const record = to(entity)
|
|
95
|
+
|
|
96
|
+
this.debug('findOneAndReplace', { criteria, record })
|
|
97
|
+
|
|
98
|
+
const result = await this.#collection.findOneAndReplace(criteria, record)
|
|
66
99
|
|
|
67
100
|
return result !== null
|
|
68
101
|
}
|
|
69
102
|
|
|
70
|
-
async store (entity) {
|
|
103
|
+
async store (entity, attempt = 0) {
|
|
71
104
|
try {
|
|
72
|
-
if (entity._version === 1)
|
|
105
|
+
if (entity._version === 1)
|
|
73
106
|
return await this.add(entity)
|
|
74
|
-
|
|
107
|
+
else
|
|
75
108
|
return await this.set(entity)
|
|
76
|
-
}
|
|
77
109
|
} catch (error) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
110
|
+
const retry = await retriable(error, attempt)
|
|
111
|
+
|
|
112
|
+
if (retry)
|
|
113
|
+
return await this.store(entity, attempt + 1)
|
|
114
|
+
else
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async massStore (entities, attempt = 0) {
|
|
120
|
+
if (entities.length === 0)
|
|
121
|
+
return true
|
|
122
|
+
|
|
123
|
+
const operations = entities.map((entity) => {
|
|
124
|
+
const record = to(entity)
|
|
125
|
+
|
|
126
|
+
if (entity._version === 1)
|
|
127
|
+
return { insertOne: { document: record } }
|
|
128
|
+
else
|
|
129
|
+
return { replaceOne: { filter: { _id: entity.id, _version: entity._version - 1 }, replacement: record } }
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const client = this.#client.instance.client
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
await client.withSession(async (session) => {
|
|
136
|
+
await session.withTransaction(async () => {
|
|
137
|
+
this.debug('bulkWrite', { operations: operations.length })
|
|
138
|
+
|
|
139
|
+
await this.#collection.bulkWrite(operations, { session })
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
return true
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const retry = await retriable(error, attempt)
|
|
146
|
+
|
|
147
|
+
if (retry)
|
|
148
|
+
return await this.massStore(entities, attempt + 1)
|
|
149
|
+
else
|
|
150
|
+
return false
|
|
92
151
|
}
|
|
93
152
|
}
|
|
94
153
|
|
|
95
154
|
async upsert (query, changeset) {
|
|
96
|
-
const {
|
|
97
|
-
criteria,
|
|
98
|
-
options
|
|
99
|
-
} = translate(query)
|
|
155
|
+
const { criteria, options } = translate(query)
|
|
100
156
|
|
|
101
157
|
if (!('_deleted' in changeset) || changeset._deleted === null) {
|
|
102
158
|
delete criteria._deleted
|
|
@@ -108,20 +164,50 @@ class Storage extends Connector {
|
|
|
108
164
|
$inc: { _version: 1 }
|
|
109
165
|
}
|
|
110
166
|
|
|
111
|
-
options.returnDocument =
|
|
167
|
+
options.returnDocument = ReturnDocument.AFTER
|
|
168
|
+
|
|
169
|
+
this.debug('findOneAndUpdate', { criteria, update, options })
|
|
112
170
|
|
|
113
|
-
const result = await this.#
|
|
171
|
+
const result = await this.#collection.findOneAndUpdate(criteria, update, options)
|
|
114
172
|
|
|
115
173
|
return from(result)
|
|
116
174
|
}
|
|
117
175
|
|
|
176
|
+
async ensure (query, properties, state) {
|
|
177
|
+
let { criteria, options } = translate(query)
|
|
178
|
+
|
|
179
|
+
if (query === undefined)
|
|
180
|
+
criteria = properties
|
|
181
|
+
|
|
182
|
+
const update = { $setOnInsert: to(state) }
|
|
183
|
+
|
|
184
|
+
options.upsert = true
|
|
185
|
+
options.returnDocument = ReturnDocument.AFTER
|
|
186
|
+
|
|
187
|
+
console.debug('Database query', { collection: this.#collection.collectionName, method: 'findOneAndUpdate', criteria, update, options })
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const result = await this.#collection.findOneAndUpdate(criteria, update, options)
|
|
191
|
+
|
|
192
|
+
if (result._deleted !== undefined && result._deleted !== null)
|
|
193
|
+
return null
|
|
194
|
+
else
|
|
195
|
+
return from(result)
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (error.code === ERR_DUPLICATE_KEY)
|
|
198
|
+
throw new exceptions.DuplicateException(this.#client.name)
|
|
199
|
+
else
|
|
200
|
+
throw error
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
118
204
|
async index () {
|
|
119
205
|
const indexes = []
|
|
120
206
|
|
|
121
207
|
if (this.#entity.unique !== undefined) {
|
|
122
208
|
for (const [name, fields] of Object.entries(this.#entity.unique)) {
|
|
123
|
-
const
|
|
124
|
-
const unique = await this.uniqueIndex(name, fields,
|
|
209
|
+
const optional = this.getOptional(fields)
|
|
210
|
+
const unique = await this.uniqueIndex(name, fields, optional)
|
|
125
211
|
|
|
126
212
|
indexes.push(unique)
|
|
127
213
|
}
|
|
@@ -131,23 +217,24 @@ class Storage extends Connector {
|
|
|
131
217
|
for (const [suffix, declaration] of Object.entries(this.#entity.index)) {
|
|
132
218
|
const name = 'index_' + suffix
|
|
133
219
|
const fields = Object.fromEntries(Object.entries(declaration)
|
|
134
|
-
.map(([name, type]) => [name, INDEX_TYPES[type]]))
|
|
220
|
+
.map(([name, type]) => [name, INDEX_TYPES[type] ?? type]))
|
|
135
221
|
|
|
136
|
-
const
|
|
222
|
+
const optional = this.getOptional(Object.keys(fields))
|
|
223
|
+
const options = { name, sparse: optional.length > 0 }
|
|
137
224
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
225
|
+
console.info('Creating index', { fields, options })
|
|
226
|
+
|
|
227
|
+
await this.#collection.createIndex(fields, options)
|
|
228
|
+
.catch((e) => console.warn('MongoDB index creation failed', { collection: this.#collection.collectionName, name, fields, error: e }))
|
|
142
229
|
|
|
143
230
|
indexes.push(name)
|
|
144
231
|
}
|
|
145
232
|
}
|
|
146
233
|
|
|
147
|
-
await this.
|
|
234
|
+
await this.removeObsoleteIndexes(indexes)
|
|
148
235
|
}
|
|
149
236
|
|
|
150
|
-
async uniqueIndex (name, properties,
|
|
237
|
+
async uniqueIndex (name, properties, optional) {
|
|
151
238
|
const fields = properties.reduce((acc, property) => {
|
|
152
239
|
acc[property] = 1
|
|
153
240
|
return acc
|
|
@@ -155,48 +242,80 @@ class Storage extends Connector {
|
|
|
155
242
|
|
|
156
243
|
name = 'unique_' + name
|
|
157
244
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
245
|
+
const options = { name, unique: true }
|
|
246
|
+
|
|
247
|
+
if (optional.length > 0)
|
|
248
|
+
options.partialFilterExpression = Object.fromEntries(optional.map((field) => [field, { $exists: true }]))
|
|
249
|
+
|
|
250
|
+
console.info('Creating unique index', { name, fields, options })
|
|
251
|
+
|
|
252
|
+
await this.#collection.createIndex(fields, options)
|
|
253
|
+
.catch((e) => console.warn('MongoDB unique index creation failed',
|
|
254
|
+
{ collection: this.#collection.collectionName, name, fields, error: e }))
|
|
163
255
|
|
|
164
256
|
return name
|
|
165
257
|
}
|
|
166
258
|
|
|
167
|
-
async
|
|
168
|
-
const current = await this
|
|
259
|
+
async removeObsoleteIndexes (desired) {
|
|
260
|
+
const current = await this.getCurrentIndexes()
|
|
169
261
|
const obsolete = current.filter((name) => !desired.includes(name))
|
|
170
262
|
|
|
171
263
|
if (obsolete.length > 0) {
|
|
172
|
-
console.info(
|
|
264
|
+
console.info('Removing obsolete indexes', { collection: this.#collection.collectionName, indexes: obsolete.join(', ') })
|
|
265
|
+
|
|
266
|
+
await Promise.all(obsolete.map((name) => this.#collection.dropIndex(name)))
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async getCurrentIndexes () {
|
|
271
|
+
try {
|
|
272
|
+
const array = await this.#collection.listIndexes().toArray()
|
|
173
273
|
|
|
174
|
-
|
|
274
|
+
return array.map(({ name }) => name).filter((name) => name !== '_id_')
|
|
275
|
+
} catch {
|
|
276
|
+
return []
|
|
175
277
|
}
|
|
176
278
|
}
|
|
177
279
|
|
|
178
|
-
|
|
280
|
+
getOptional (fields) {
|
|
179
281
|
const optional = []
|
|
180
282
|
|
|
181
|
-
for (const field of fields) {
|
|
182
|
-
if (!(field in this.#entity.schema.properties))
|
|
283
|
+
for (const field of fields) {
|
|
284
|
+
if (!field.includes('.') && !(field in this.#entity.schema.properties))
|
|
183
285
|
throw new Error(`Index field '${field}' is not defined.`)
|
|
184
|
-
}
|
|
185
286
|
|
|
186
|
-
if (!this.#entity.schema.required?.includes(field))
|
|
287
|
+
if (!this.#entity.schema.required?.includes(field))
|
|
187
288
|
optional.push(field)
|
|
188
|
-
}
|
|
189
289
|
}
|
|
190
290
|
|
|
191
|
-
|
|
192
|
-
|
|
291
|
+
return optional
|
|
292
|
+
}
|
|
193
293
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
294
|
+
debug (method, attributes) {
|
|
295
|
+
console.debug('MongoDB query', {
|
|
296
|
+
collection: this.#collection.collectionName,
|
|
297
|
+
method,
|
|
298
|
+
...attributes
|
|
299
|
+
})
|
|
198
300
|
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function toPipeline (criteria, options, sample) {
|
|
304
|
+
const pipeline = []
|
|
305
|
+
|
|
306
|
+
if (criteria !== undefined)
|
|
307
|
+
pipeline.push({ $match: criteria })
|
|
199
308
|
|
|
309
|
+
if (sample !== undefined)
|
|
310
|
+
pipeline.push({ $sample: { size: sample } })
|
|
311
|
+
|
|
312
|
+
if (options?.sort !== undefined)
|
|
313
|
+
pipeline.push({ $sort: options.sort })
|
|
314
|
+
|
|
315
|
+
if (options?.projection !== undefined)
|
|
316
|
+
pipeline.push({ $project: options.projection })
|
|
317
|
+
|
|
318
|
+
return pipeline
|
|
200
319
|
}
|
|
201
320
|
|
|
202
321
|
const INDEX_TYPES = {
|
|
@@ -207,4 +326,29 @@ const INDEX_TYPES = {
|
|
|
207
326
|
|
|
208
327
|
const ERR_DUPLICATE_KEY = 11000
|
|
209
328
|
|
|
329
|
+
async function retriable (error, attempt) {
|
|
330
|
+
if (error.code === ERR_DUPLICATE_KEY) {
|
|
331
|
+
const id = error.keyPattern === undefined
|
|
332
|
+
? error.message.includes(' index: _id_ ') // AWS DocumentDB
|
|
333
|
+
: error.keyPattern._id === 1
|
|
334
|
+
|
|
335
|
+
if (id)
|
|
336
|
+
return false
|
|
337
|
+
else
|
|
338
|
+
throw new exceptions.DuplicateException()
|
|
339
|
+
} else if (error.cause?.code === 'ECONNREFUSED') {
|
|
340
|
+
if (attempt === LAST_ATTEMPT)
|
|
341
|
+
throw error
|
|
342
|
+
|
|
343
|
+
const timeout = 1000 + 500 * attempt
|
|
344
|
+
|
|
345
|
+
await new Promise((resolve) => setTimeout(resolve, timeout))
|
|
346
|
+
|
|
347
|
+
return true
|
|
348
|
+
} else
|
|
349
|
+
throw error
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const LAST_ATTEMPT = 9
|
|
353
|
+
|
|
210
354
|
exports.Storage = Storage
|
package/src/translate.js
CHANGED
|
@@ -9,18 +9,21 @@ const parse = { ...require('./translate/criteria'), ...require('./translate/opti
|
|
|
9
9
|
const translate = (query) => {
|
|
10
10
|
const result = {
|
|
11
11
|
criteria: query?.criteria === undefined ? {} : parse.criteria(query.criteria),
|
|
12
|
-
options: query?.options === undefined ? {} : parse.options(query.options)
|
|
12
|
+
options: query?.options === undefined ? {} : parse.options(query.options),
|
|
13
|
+
sample: query?.options?.sample
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
if (query?.id !== undefined)
|
|
16
|
+
if (query?.id !== undefined)
|
|
16
17
|
result.criteria._id = query.id
|
|
17
|
-
}
|
|
18
18
|
|
|
19
|
-
if (query?.
|
|
19
|
+
if (query?.ids !== undefined)
|
|
20
|
+
result.criteria._id = { $in: query.ids }
|
|
21
|
+
|
|
22
|
+
if (query?.version !== undefined)
|
|
20
23
|
result.criteria._version = query.version
|
|
21
|
-
}
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
if (query?.search !== undefined)
|
|
26
|
+
result.criteria.$text = { $search: query.search }
|
|
24
27
|
|
|
25
28
|
return result
|
|
26
29
|
}
|
package/test/record.test.js
CHANGED
|
@@ -47,19 +47,4 @@ describe('from', () => {
|
|
|
47
47
|
_version: 0
|
|
48
48
|
})
|
|
49
49
|
})
|
|
50
|
-
|
|
51
|
-
it('should not modify argument', () => {
|
|
52
|
-
/** @type {toa.mongodb.Record} */
|
|
53
|
-
const record = {
|
|
54
|
-
_id: '1',
|
|
55
|
-
_version: 0
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
from(record)
|
|
59
|
-
|
|
60
|
-
expect(record).toStrictEqual({
|
|
61
|
-
_id: '1',
|
|
62
|
-
_version: 0
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
50
|
})
|
package/src/collection.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { Connector } = require('@toa.io/core')
|
|
4
|
-
|
|
5
|
-
class Collection extends Connector {
|
|
6
|
-
#client
|
|
7
|
-
#collection
|
|
8
|
-
|
|
9
|
-
constructor (client) {
|
|
10
|
-
super()
|
|
11
|
-
|
|
12
|
-
this.#client = client
|
|
13
|
-
|
|
14
|
-
this.depends(client)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async open () {
|
|
18
|
-
this.#collection = this.#client.collection
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** @hot */
|
|
22
|
-
async get (query, options) {
|
|
23
|
-
return /** @type {toa.mongodb.Record} */ this.#collection.findOne(query, options)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** @hot */
|
|
27
|
-
async find (query, options) {
|
|
28
|
-
const cursor = this.#collection.find(query, options)
|
|
29
|
-
|
|
30
|
-
return cursor.toArray()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** @hot */
|
|
34
|
-
async add (record) {
|
|
35
|
-
return await this.#collection.insertOne(record)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** @hot */
|
|
39
|
-
async replace (query, record, options) {
|
|
40
|
-
return await this.#collection.findOneAndReplace(query, record, options)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** @hot */
|
|
44
|
-
async update (query, update, options) {
|
|
45
|
-
return this.#collection.findOneAndUpdate(query, update, options)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async index (keys, options) {
|
|
49
|
-
return this.#collection.createIndex(keys, options)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async indexes () {
|
|
53
|
-
try {
|
|
54
|
-
const array = await this.#collection.listIndexes().toArray()
|
|
55
|
-
|
|
56
|
-
return array.map(({ name }) => name).filter((name) => name !== '_id_')
|
|
57
|
-
} catch {
|
|
58
|
-
return []
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async dropIndexes (names) {
|
|
63
|
-
const all = names.map((name) => this.#collection.dropIndex(name))
|
|
64
|
-
|
|
65
|
-
return Promise.all(all)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
exports.Collection = Collection
|