@toa.io/storages.mongodb 1.0.0-alpha.21 → 1.0.0-alpha.212
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 +13 -8
- package/src/factory.js +1 -3
- package/src/record.js +6 -19
- package/src/storage.js +244 -77
- 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.212",
|
|
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.208",
|
|
23
|
+
"@toa.io/core": "1.0.0-alpha.212",
|
|
24
|
+
"@toa.io/generic": "1.0.0-alpha.208",
|
|
25
|
+
"@toa.io/pointer": "1.0.0-alpha.208",
|
|
26
|
+
"mongodb": "7.2.0",
|
|
27
|
+
"openspan": "1.0.0-alpha.173",
|
|
28
28
|
"saslprep": "1.0.3"
|
|
29
29
|
},
|
|
30
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "d8aefb3b37df15be74bb63e1447f76c27e88d9be"
|
|
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
|
/**
|
|
@@ -58,11 +62,14 @@ class Client extends Connector {
|
|
|
58
62
|
async open () {
|
|
59
63
|
const urls = await this.resolveURLs()
|
|
60
64
|
const dbname = this.resolveDB()
|
|
61
|
-
const collname = this.locator.lowercase
|
|
62
65
|
|
|
63
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++
|
|
@@ -70,13 +77,13 @@ class Client extends Connector {
|
|
|
70
77
|
const db = this.instance.client.db(dbname)
|
|
71
78
|
|
|
72
79
|
try {
|
|
73
|
-
this.collection = await db.createCollection(
|
|
80
|
+
this.collection = await db.createCollection(this.name)
|
|
74
81
|
} catch (e) {
|
|
75
82
|
if (e.code !== ALREADY_EXISTS) {
|
|
76
83
|
throw e
|
|
77
84
|
}
|
|
78
85
|
|
|
79
|
-
this.collection = db.collection(
|
|
86
|
+
this.collection = db.collection(this.name)
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
|
|
@@ -105,7 +112,7 @@ class Client extends Connector {
|
|
|
105
112
|
const client = new MongoClient(urls.join(','), OPTIONS)
|
|
106
113
|
const hosts = urls.map((str) => new URL(str).host)
|
|
107
114
|
|
|
108
|
-
console.info('Connecting to MongoDB
|
|
115
|
+
console.info('Connecting to MongoDB', { address: hosts.join(', ') })
|
|
109
116
|
|
|
110
117
|
await client.connect()
|
|
111
118
|
|
|
@@ -149,9 +156,7 @@ function getKey (db, urls) {
|
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
const OPTIONS = {
|
|
152
|
-
ignoreUndefined: true
|
|
153
|
-
connectTimeoutMS: 0,
|
|
154
|
-
serverSelectionTimeoutMS: 0
|
|
159
|
+
ignoreUndefined: true
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
const ALREADY_EXISTS = 48
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
const { criteria, options, sample } = translate(query)
|
|
47
|
+
|
|
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 })
|
|
47
61
|
|
|
48
|
-
|
|
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,92 @@ 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
|
-
|
|
110
|
+
console.error('MongoDB error', error)
|
|
111
|
+
|
|
112
|
+
const retry = await retriable(error, attempt)
|
|
113
|
+
|
|
114
|
+
if (retry)
|
|
115
|
+
return await this.store(entity, attempt + 1)
|
|
116
|
+
else
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
79
120
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
121
|
+
async massStore (entities, attempt = 0) {
|
|
122
|
+
if (entities.length === 0)
|
|
123
|
+
return true
|
|
124
|
+
|
|
125
|
+
const operations = entities.map((entity) => {
|
|
126
|
+
const record = to(entity)
|
|
83
127
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
128
|
+
if (entity._version === 1) {
|
|
129
|
+
const { _version, ...rest } = record
|
|
130
|
+
|
|
131
|
+
return { // upsert in required when document is deleted
|
|
132
|
+
updateOne: {
|
|
133
|
+
filter: { _id: entity.id },
|
|
134
|
+
update: {
|
|
135
|
+
$set: {
|
|
136
|
+
...rest,
|
|
137
|
+
_deleted: null
|
|
138
|
+
},
|
|
139
|
+
$inc: { _version: 1 },
|
|
140
|
+
},
|
|
141
|
+
upsert: true
|
|
142
|
+
}
|
|
88
143
|
}
|
|
89
|
-
} else
|
|
90
|
-
|
|
91
|
-
|
|
144
|
+
} else
|
|
145
|
+
return {
|
|
146
|
+
replaceOne: {
|
|
147
|
+
filter: { _id: entity.id, _version: entity._version - 1 },
|
|
148
|
+
replacement: record
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const client = this.#client.instance.client
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await client.withSession(async (session) => {
|
|
157
|
+
await session.withTransaction(async () => {
|
|
158
|
+
this.debug('bulkWrite', { operations: operations.length })
|
|
159
|
+
|
|
160
|
+
await this.#collection.bulkWrite(operations, { session })
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
return true
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('MongoDB error', error)
|
|
167
|
+
|
|
168
|
+
const retry = await retriable(error, attempt)
|
|
169
|
+
|
|
170
|
+
if (retry)
|
|
171
|
+
return await this.massStore(entities, attempt + 1)
|
|
172
|
+
else
|
|
173
|
+
return false
|
|
92
174
|
}
|
|
93
175
|
}
|
|
94
176
|
|
|
95
177
|
async upsert (query, changeset) {
|
|
96
|
-
const {
|
|
97
|
-
criteria,
|
|
98
|
-
options
|
|
99
|
-
} = translate(query)
|
|
178
|
+
const { criteria, options } = translate(query)
|
|
100
179
|
|
|
101
180
|
if (!('_deleted' in changeset) || changeset._deleted === null) {
|
|
102
181
|
delete criteria._deleted
|
|
@@ -108,20 +187,50 @@ class Storage extends Connector {
|
|
|
108
187
|
$inc: { _version: 1 }
|
|
109
188
|
}
|
|
110
189
|
|
|
111
|
-
options.returnDocument =
|
|
190
|
+
options.returnDocument = ReturnDocument.AFTER
|
|
112
191
|
|
|
113
|
-
|
|
192
|
+
this.debug('findOneAndUpdate', { criteria, update, options })
|
|
193
|
+
|
|
194
|
+
const result = await this.#collection.findOneAndUpdate(criteria, update, options)
|
|
114
195
|
|
|
115
196
|
return from(result)
|
|
116
197
|
}
|
|
117
198
|
|
|
199
|
+
async ensure (query, properties, state) {
|
|
200
|
+
let { criteria, options } = translate(query)
|
|
201
|
+
|
|
202
|
+
if (query === undefined)
|
|
203
|
+
criteria = properties
|
|
204
|
+
|
|
205
|
+
const update = { $setOnInsert: to(state) }
|
|
206
|
+
|
|
207
|
+
options.upsert = true
|
|
208
|
+
options.returnDocument = ReturnDocument.AFTER
|
|
209
|
+
|
|
210
|
+
console.debug('Database query', { collection: this.#collection.collectionName, method: 'findOneAndUpdate', criteria, update, options })
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const result = await this.#collection.findOneAndUpdate(criteria, update, options)
|
|
214
|
+
|
|
215
|
+
if (result._deleted !== undefined && result._deleted !== null)
|
|
216
|
+
return null
|
|
217
|
+
else
|
|
218
|
+
return from(result)
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (error.code === ERR_DUPLICATE_KEY)
|
|
221
|
+
throw new exceptions.DuplicateException(this.#client.name)
|
|
222
|
+
else
|
|
223
|
+
throw error
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
118
227
|
async index () {
|
|
119
228
|
const indexes = []
|
|
120
229
|
|
|
121
230
|
if (this.#entity.unique !== undefined) {
|
|
122
231
|
for (const [name, fields] of Object.entries(this.#entity.unique)) {
|
|
123
|
-
const
|
|
124
|
-
const unique = await this.uniqueIndex(name, fields,
|
|
232
|
+
const optional = this.getOptional(fields)
|
|
233
|
+
const unique = await this.uniqueIndex(name, fields, optional)
|
|
125
234
|
|
|
126
235
|
indexes.push(unique)
|
|
127
236
|
}
|
|
@@ -131,23 +240,24 @@ class Storage extends Connector {
|
|
|
131
240
|
for (const [suffix, declaration] of Object.entries(this.#entity.index)) {
|
|
132
241
|
const name = 'index_' + suffix
|
|
133
242
|
const fields = Object.fromEntries(Object.entries(declaration)
|
|
134
|
-
.map(([name, type]) => [name, INDEX_TYPES[type]]))
|
|
243
|
+
.map(([name, type]) => [name, INDEX_TYPES[type] ?? type]))
|
|
135
244
|
|
|
136
|
-
const
|
|
245
|
+
const optional = this.getOptional(Object.keys(fields))
|
|
246
|
+
const options = { name, sparse: optional.length > 0 }
|
|
137
247
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
248
|
+
console.info('Creating index', { fields, options })
|
|
249
|
+
|
|
250
|
+
await this.#collection.createIndex(fields, options)
|
|
251
|
+
.catch((e) => console.warn('MongoDB index creation failed', { collection: this.#collection.collectionName, name, fields, error: e }))
|
|
142
252
|
|
|
143
253
|
indexes.push(name)
|
|
144
254
|
}
|
|
145
255
|
}
|
|
146
256
|
|
|
147
|
-
await this.
|
|
257
|
+
await this.removeObsoleteIndexes(indexes)
|
|
148
258
|
}
|
|
149
259
|
|
|
150
|
-
async uniqueIndex (name, properties,
|
|
260
|
+
async uniqueIndex (name, properties, optional) {
|
|
151
261
|
const fields = properties.reduce((acc, property) => {
|
|
152
262
|
acc[property] = 1
|
|
153
263
|
return acc
|
|
@@ -155,48 +265,80 @@ class Storage extends Connector {
|
|
|
155
265
|
|
|
156
266
|
name = 'unique_' + name
|
|
157
267
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
268
|
+
const options = { name, unique: true }
|
|
269
|
+
|
|
270
|
+
if (optional.length > 0)
|
|
271
|
+
options.partialFilterExpression = Object.fromEntries(optional.map((field) => [field, { $exists: true }]))
|
|
272
|
+
|
|
273
|
+
console.info('Creating unique index', { name, fields, options })
|
|
274
|
+
|
|
275
|
+
await this.#collection.createIndex(fields, options)
|
|
276
|
+
.catch((e) => console.warn('MongoDB unique index creation failed',
|
|
277
|
+
{ collection: this.#collection.collectionName, name, fields, error: e }))
|
|
163
278
|
|
|
164
279
|
return name
|
|
165
280
|
}
|
|
166
281
|
|
|
167
|
-
async
|
|
168
|
-
const current = await this
|
|
282
|
+
async removeObsoleteIndexes (desired) {
|
|
283
|
+
const current = await this.getCurrentIndexes()
|
|
169
284
|
const obsolete = current.filter((name) => !desired.includes(name))
|
|
170
285
|
|
|
171
286
|
if (obsolete.length > 0) {
|
|
172
|
-
console.info(
|
|
287
|
+
console.info('Removing obsolete indexes', { collection: this.#collection.collectionName, indexes: obsolete.join(', ') })
|
|
288
|
+
|
|
289
|
+
await Promise.all(obsolete.map((name) => this.#collection.dropIndex(name)))
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async getCurrentIndexes () {
|
|
294
|
+
try {
|
|
295
|
+
const array = await this.#collection.listIndexes().toArray()
|
|
173
296
|
|
|
174
|
-
|
|
297
|
+
return array.map(({ name }) => name).filter((name) => name !== '_id_')
|
|
298
|
+
} catch {
|
|
299
|
+
return []
|
|
175
300
|
}
|
|
176
301
|
}
|
|
177
302
|
|
|
178
|
-
|
|
303
|
+
getOptional (fields) {
|
|
179
304
|
const optional = []
|
|
180
305
|
|
|
181
|
-
for (const field of fields) {
|
|
182
|
-
if (!(field in this.#entity.schema.properties))
|
|
306
|
+
for (const field of fields) {
|
|
307
|
+
if (!field.includes('.') && !(field in this.#entity.schema.properties))
|
|
183
308
|
throw new Error(`Index field '${field}' is not defined.`)
|
|
184
|
-
}
|
|
185
309
|
|
|
186
|
-
if (!this.#entity.schema.required?.includes(field))
|
|
310
|
+
if (!this.#entity.schema.required?.includes(field))
|
|
187
311
|
optional.push(field)
|
|
188
|
-
}
|
|
189
312
|
}
|
|
190
313
|
|
|
191
|
-
|
|
192
|
-
|
|
314
|
+
return optional
|
|
315
|
+
}
|
|
193
316
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
317
|
+
debug (method, attributes) {
|
|
318
|
+
console.debug('MongoDB query', {
|
|
319
|
+
collection: this.#collection.collectionName,
|
|
320
|
+
method,
|
|
321
|
+
...attributes
|
|
322
|
+
})
|
|
198
323
|
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function toPipeline (criteria, options, sample) {
|
|
327
|
+
const pipeline = []
|
|
328
|
+
|
|
329
|
+
if (criteria !== undefined)
|
|
330
|
+
pipeline.push({ $match: criteria })
|
|
199
331
|
|
|
332
|
+
if (sample !== undefined)
|
|
333
|
+
pipeline.push({ $sample: { size: sample } })
|
|
334
|
+
|
|
335
|
+
if (options?.sort !== undefined)
|
|
336
|
+
pipeline.push({ $sort: options.sort })
|
|
337
|
+
|
|
338
|
+
if (options?.projection !== undefined)
|
|
339
|
+
pipeline.push({ $project: options.projection })
|
|
340
|
+
|
|
341
|
+
return pipeline
|
|
200
342
|
}
|
|
201
343
|
|
|
202
344
|
const INDEX_TYPES = {
|
|
@@ -207,4 +349,29 @@ const INDEX_TYPES = {
|
|
|
207
349
|
|
|
208
350
|
const ERR_DUPLICATE_KEY = 11000
|
|
209
351
|
|
|
352
|
+
async function retriable (error, attempt) {
|
|
353
|
+
if (error.code === ERR_DUPLICATE_KEY) {
|
|
354
|
+
const id = error.keyPattern === undefined
|
|
355
|
+
? error.message.includes(' index: _id_ ') // AWS DocumentDB
|
|
356
|
+
: error.keyPattern._id === 1
|
|
357
|
+
|
|
358
|
+
if (id)
|
|
359
|
+
return false
|
|
360
|
+
else
|
|
361
|
+
throw new exceptions.DuplicateException()
|
|
362
|
+
} else if (error.cause?.code === 'ECONNREFUSED') {
|
|
363
|
+
if (attempt === LAST_ATTEMPT)
|
|
364
|
+
throw error
|
|
365
|
+
|
|
366
|
+
const timeout = 1000 + 500 * attempt
|
|
367
|
+
|
|
368
|
+
await new Promise((resolve) => setTimeout(resolve, timeout))
|
|
369
|
+
|
|
370
|
+
return true
|
|
371
|
+
} else
|
|
372
|
+
throw error
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const LAST_ATTEMPT = 9
|
|
376
|
+
|
|
210
377
|
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
|