@toa.io/storages.mongodb 1.0.0-alpha.7 → 1.0.0-alpha.72
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 +160 -0
- package/src/factory.js +4 -4
- package/src/record.js +6 -19
- package/src/storage.js +200 -52
- package/src/translate.js +13 -5
- package/test/record.test.js +0 -15
- package/src/connection.js +0 -103
- package/test/connection.test.js +0 -58
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.72",
|
|
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.63",
|
|
23
|
+
"@toa.io/core": "1.0.0-alpha.67",
|
|
24
|
+
"@toa.io/generic": "1.0.0-alpha.63",
|
|
25
|
+
"@toa.io/pointer": "1.0.0-alpha.63",
|
|
26
|
+
"mongodb": "6.7.0",
|
|
27
|
+
"openspan": "1.0.0-alpha.67",
|
|
28
28
|
"saslprep": "1.0.3"
|
|
29
29
|
},
|
|
30
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "919b587dfcdfab40d18f0f8bd24b34d8107f811a"
|
|
31
31
|
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('mongodb').MongoClient} MongoClient
|
|
5
|
+
* @typedef {{ count: number, client: MongoClient }} Instance
|
|
6
|
+
* @typedef {import('@toa.io/core').Locator} Locator
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { console } = require('openspan')
|
|
10
|
+
const { Connector } = require('@toa.io/core')
|
|
11
|
+
const { resolve } = require('@toa.io/pointer')
|
|
12
|
+
const { ID } = require('./deployment')
|
|
13
|
+
const { MongoClient } = require('mongodb')
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @type {Record<string, Promise<Instance>>}
|
|
17
|
+
*/
|
|
18
|
+
const INSTANCES = {}
|
|
19
|
+
|
|
20
|
+
class Client extends Connector {
|
|
21
|
+
name
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @public
|
|
25
|
+
* @type {import('mongodb').Collection}
|
|
26
|
+
*/
|
|
27
|
+
collection
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @private
|
|
31
|
+
* @type {Locator}
|
|
32
|
+
*/
|
|
33
|
+
locator
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @private
|
|
37
|
+
* @type {Instance}
|
|
38
|
+
*/
|
|
39
|
+
instance
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @private
|
|
43
|
+
* @type {string}
|
|
44
|
+
*/
|
|
45
|
+
key
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {Locator} locator
|
|
49
|
+
*/
|
|
50
|
+
constructor (locator) {
|
|
51
|
+
super()
|
|
52
|
+
|
|
53
|
+
this.locator = locator
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @protected
|
|
58
|
+
* @override
|
|
59
|
+
* @return {Promise<void>}
|
|
60
|
+
*/
|
|
61
|
+
async open () {
|
|
62
|
+
const urls = await this.resolveURLs()
|
|
63
|
+
const dbname = this.resolveDB()
|
|
64
|
+
|
|
65
|
+
this.name = this.locator.lowercase
|
|
66
|
+
this.key = getKey(dbname, urls)
|
|
67
|
+
|
|
68
|
+
INSTANCES[this.key] ??= this.createInstance(urls)
|
|
69
|
+
|
|
70
|
+
this.instance = await INSTANCES[this.key]
|
|
71
|
+
this.instance.count++
|
|
72
|
+
|
|
73
|
+
const db = this.instance.client.db(dbname)
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
this.collection = await db.createCollection(this.name)
|
|
77
|
+
} catch (e) {
|
|
78
|
+
if (e.code !== ALREADY_EXISTS) {
|
|
79
|
+
throw e
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.collection = db.collection(this.name)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @protected
|
|
88
|
+
* @override
|
|
89
|
+
* @return {Promise<void>}
|
|
90
|
+
*/
|
|
91
|
+
async close () {
|
|
92
|
+
const instance = await INSTANCES[this.key]
|
|
93
|
+
|
|
94
|
+
instance.count--
|
|
95
|
+
|
|
96
|
+
if (instance.count === 0) {
|
|
97
|
+
await instance.client.close()
|
|
98
|
+
delete INSTANCES[this.key]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @private
|
|
104
|
+
* @param {string[]} urls
|
|
105
|
+
* @return {Promise<Instance>}
|
|
106
|
+
*/
|
|
107
|
+
async createInstance (urls) {
|
|
108
|
+
const client = new MongoClient(urls.join(','), OPTIONS)
|
|
109
|
+
const hosts = urls.map((str) => new URL(str).host)
|
|
110
|
+
|
|
111
|
+
console.info('Connecting to MongoDB', { address: hosts.join(', ') })
|
|
112
|
+
|
|
113
|
+
await client.connect()
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
count: 0,
|
|
117
|
+
client
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @private
|
|
123
|
+
* @return {Promise<string[]>}
|
|
124
|
+
*/
|
|
125
|
+
async resolveURLs () {
|
|
126
|
+
if (process.env.TOA_DEV === '1') {
|
|
127
|
+
return ['mongodb://developer:secret@localhost']
|
|
128
|
+
} else {
|
|
129
|
+
return await resolve(ID, this.locator.id)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @private
|
|
135
|
+
* @return {string}
|
|
136
|
+
*/
|
|
137
|
+
resolveDB () {
|
|
138
|
+
if (process.env.TOA_CONTEXT !== undefined) {
|
|
139
|
+
return process.env.TOA_CONTEXT
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (process.env.TOA_DEV === '1') {
|
|
143
|
+
return 'toa-dev'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
throw new Error('Environment variable TOA_CONTEXT is not defined')
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getKey (db, urls) {
|
|
151
|
+
return db + ':' + urls.sort().join(' ')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const OPTIONS = {
|
|
155
|
+
ignoreUndefined: true
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const ALREADY_EXISTS = 48
|
|
159
|
+
|
|
160
|
+
exports.Client = Client
|
package/src/factory.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { Client } = require('./client')
|
|
4
4
|
const { Storage } = require('./storage')
|
|
5
5
|
|
|
6
6
|
class Factory {
|
|
7
|
-
storage (locator) {
|
|
8
|
-
const
|
|
7
|
+
storage (locator, entity) {
|
|
8
|
+
const client = new Client(locator)
|
|
9
9
|
|
|
10
|
-
return new Storage(
|
|
10
|
+
return new Storage(client, entity)
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
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,56 +1,69 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { Connector } = require('@toa.io/core')
|
|
4
|
-
|
|
3
|
+
const { Connector, exceptions } = require('@toa.io/core')
|
|
4
|
+
const { console } = require('openspan')
|
|
5
5
|
const { translate } = require('./translate')
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} = require('./record')
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @implements {toa.core.Storage}
|
|
13
|
-
*/
|
|
6
|
+
const { to, from } = require('./record')
|
|
7
|
+
const { ReturnDocument } = require('mongodb')
|
|
8
|
+
|
|
14
9
|
class Storage extends Connector {
|
|
15
|
-
|
|
16
|
-
#connection
|
|
10
|
+
#client
|
|
17
11
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
/** @type {import('mongodb').Collection} */
|
|
13
|
+
#collection
|
|
14
|
+
#entity
|
|
15
|
+
|
|
16
|
+
constructor (client, entity) {
|
|
22
17
|
super()
|
|
23
18
|
|
|
24
|
-
this.#
|
|
19
|
+
this.#client = client
|
|
20
|
+
this.#entity = entity
|
|
21
|
+
|
|
22
|
+
this.depends(client)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async open () {
|
|
26
|
+
this.#collection = this.#client.collection
|
|
25
27
|
|
|
26
|
-
this.
|
|
28
|
+
await this.index()
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
async get (query) {
|
|
30
|
-
const {
|
|
31
|
-
criteria,
|
|
32
|
-
options
|
|
33
|
-
} = translate(query)
|
|
32
|
+
const { criteria, options } = translate(query)
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
this.debug('findOne', { criteria, options })
|
|
35
|
+
|
|
36
|
+
const record = await this.#collection.findOne(criteria, options)
|
|
36
37
|
|
|
37
38
|
return from(record)
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
async find (query) {
|
|
41
|
-
const {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const recordset = await this.#
|
|
42
|
+
const { criteria, options } = translate(query)
|
|
43
|
+
|
|
44
|
+
this.debug('find', { criteria, options })
|
|
45
|
+
|
|
46
|
+
const recordset = await this.#collection.find(criteria, options).toArray()
|
|
46
47
|
|
|
47
48
|
return recordset.map((item) => from(item))
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
async stream (query = undefined) {
|
|
52
|
+
const { criteria, options } = translate(query)
|
|
53
|
+
|
|
54
|
+
this.debug('find (stream)', { criteria, options })
|
|
55
|
+
|
|
56
|
+
return this.#collection.find(criteria, options).stream({ transform: from })
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
async add (entity) {
|
|
51
60
|
const record = to(entity)
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+
this.debug('insertOne', { record })
|
|
63
|
+
|
|
64
|
+
const result = await this.#collection.insertOne(record)
|
|
65
|
+
|
|
66
|
+
return result.acknowledged
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
async set (entity) {
|
|
@@ -58,50 +71,185 @@ class Storage extends Connector {
|
|
|
58
71
|
_id: entity.id,
|
|
59
72
|
_version: entity._version - 1
|
|
60
73
|
}
|
|
61
|
-
|
|
74
|
+
|
|
75
|
+
const record = to(entity)
|
|
76
|
+
|
|
77
|
+
this.debug('findOneAndReplace', { criteria, record })
|
|
78
|
+
|
|
79
|
+
const result = await this.#collection.findOneAndReplace(criteria, record)
|
|
62
80
|
|
|
63
81
|
return result !== null
|
|
64
82
|
}
|
|
65
83
|
|
|
66
|
-
async store (entity) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
async store (entity, attempt = 0) {
|
|
85
|
+
try {
|
|
86
|
+
if (entity._version === 1)
|
|
87
|
+
return await this.add(entity)
|
|
88
|
+
else
|
|
89
|
+
return await this.set(entity)
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error.code === ERR_DUPLICATE_KEY) {
|
|
92
|
+
const id = error.keyPattern === undefined
|
|
93
|
+
? error.message.includes(' index: _id_ ') // AWS DocumentDB
|
|
94
|
+
: error.keyPattern._id === 1
|
|
95
|
+
|
|
96
|
+
if (id)
|
|
97
|
+
return false
|
|
98
|
+
else
|
|
99
|
+
throw new exceptions.DuplicateException(this.#client.name, entity)
|
|
100
|
+
} else if (error.cause?.code === 'ECONNREFUSED') {
|
|
101
|
+
// This is temporary and should be replaced with a class decorator.
|
|
102
|
+
if (attempt > 10)
|
|
103
|
+
throw error
|
|
104
|
+
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
106
|
+
|
|
107
|
+
return this.store(entity)
|
|
108
|
+
} else
|
|
109
|
+
throw error
|
|
71
110
|
}
|
|
72
111
|
}
|
|
73
112
|
|
|
74
|
-
async upsert (query, changeset
|
|
75
|
-
const {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
113
|
+
async upsert (query, changeset) {
|
|
114
|
+
const { criteria, options } = translate(query)
|
|
115
|
+
|
|
116
|
+
if (!('_deleted' in changeset) || changeset._deleted === null) {
|
|
117
|
+
delete criteria._deleted
|
|
118
|
+
changeset._deleted = null
|
|
119
|
+
}
|
|
79
120
|
|
|
80
121
|
const update = {
|
|
81
122
|
$set: { ...changeset },
|
|
82
123
|
$inc: { _version: 1 }
|
|
83
124
|
}
|
|
84
125
|
|
|
85
|
-
|
|
86
|
-
|
|
126
|
+
options.returnDocument = ReturnDocument.AFTER
|
|
127
|
+
|
|
128
|
+
this.debug('findOneAndUpdate', { criteria, update, options })
|
|
129
|
+
|
|
130
|
+
const result = await this.#collection.findOneAndUpdate(criteria, update, options)
|
|
131
|
+
|
|
132
|
+
return from(result)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async ensure (query, properties, state) {
|
|
136
|
+
let { criteria, options } = translate(query)
|
|
137
|
+
|
|
138
|
+
if (query === undefined)
|
|
139
|
+
criteria = properties
|
|
140
|
+
|
|
141
|
+
const update = { $setOnInsert: to(state) }
|
|
142
|
+
|
|
143
|
+
options.upsert = true
|
|
144
|
+
options.returnDocument = ReturnDocument.AFTER
|
|
145
|
+
|
|
146
|
+
console.debug('Database query', { method: 'findOneAndUpdate', criteria, update, options })
|
|
147
|
+
|
|
148
|
+
const result = await this.#collection.findOneAndUpdate(criteria, update, options)
|
|
149
|
+
|
|
150
|
+
if (result._deleted !== undefined && result._deleted !== null)
|
|
151
|
+
return null
|
|
152
|
+
else
|
|
153
|
+
return from(result)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async index () {
|
|
157
|
+
const indexes = []
|
|
158
|
+
|
|
159
|
+
if (this.#entity.unique !== undefined) {
|
|
160
|
+
for (const [name, fields] of Object.entries(this.#entity.unique)) {
|
|
161
|
+
const sparse = this.checkFields(fields)
|
|
162
|
+
const unique = await this.uniqueIndex(name, fields, sparse)
|
|
163
|
+
|
|
164
|
+
indexes.push(unique)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (this.#entity.index !== undefined) {
|
|
169
|
+
for (const [suffix, declaration] of Object.entries(this.#entity.index)) {
|
|
170
|
+
const name = 'index_' + suffix
|
|
171
|
+
const fields = Object.fromEntries(Object.entries(declaration)
|
|
172
|
+
.map(([name, type]) => [name, INDEX_TYPES[type]]))
|
|
87
173
|
|
|
88
|
-
|
|
174
|
+
const sparse = this.checkFields(Object.keys(fields))
|
|
89
175
|
|
|
90
|
-
|
|
91
|
-
insert._id = criteria._id
|
|
92
|
-
} else {
|
|
93
|
-
return null
|
|
94
|
-
} // this shouldn't ever happen
|
|
176
|
+
await this.#collection.createIndex(fields, { name, sparse })
|
|
95
177
|
|
|
96
|
-
|
|
178
|
+
indexes.push(name)
|
|
179
|
+
}
|
|
97
180
|
}
|
|
98
181
|
|
|
99
|
-
|
|
182
|
+
await this.removeObsoleteIndexes(indexes)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async uniqueIndex (name, properties, sparse = false) {
|
|
186
|
+
const fields = properties.reduce((acc, property) => {
|
|
187
|
+
acc[property] = 1
|
|
188
|
+
return acc
|
|
189
|
+
}, {})
|
|
100
190
|
|
|
101
|
-
|
|
191
|
+
name = 'unique_' + name
|
|
102
192
|
|
|
103
|
-
|
|
193
|
+
await this.#collection.createIndex(fields, { name, unique: true, sparse })
|
|
194
|
+
|
|
195
|
+
return name
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async removeObsoleteIndexes (desired) {
|
|
199
|
+
const current = await this.getCurrentIndexes()
|
|
200
|
+
const obsolete = current.filter((name) => !desired.includes(name))
|
|
201
|
+
|
|
202
|
+
if (obsolete.length > 0) {
|
|
203
|
+
console.info('Removing obsolete indexes', { indexes: obsolete.join(', ') })
|
|
204
|
+
|
|
205
|
+
await Promise.all(obsolete.map((name) => this.#collection.dropIndex(name)))
|
|
206
|
+
}
|
|
104
207
|
}
|
|
208
|
+
|
|
209
|
+
async getCurrentIndexes () {
|
|
210
|
+
try {
|
|
211
|
+
const array = await this.#collection.listIndexes().toArray()
|
|
212
|
+
|
|
213
|
+
return array.map(({ name }) => name).filter((name) => name !== '_id_')
|
|
214
|
+
} catch {
|
|
215
|
+
return []
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
checkFields (fields) {
|
|
220
|
+
const optional = []
|
|
221
|
+
|
|
222
|
+
for (const field of fields) {
|
|
223
|
+
if (!(field in this.#entity.schema.properties))
|
|
224
|
+
throw new Error(`Index field '${field}' is not defined.`)
|
|
225
|
+
|
|
226
|
+
if (!this.#entity.schema.required?.includes(field))
|
|
227
|
+
optional.push(field)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (optional.length > 0) {
|
|
231
|
+
console.info('Index fields are optional, creating sparse index', { fields: optional.join(', ') })
|
|
232
|
+
|
|
233
|
+
return true
|
|
234
|
+
} else
|
|
235
|
+
return false
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
debug (method, attributes) {
|
|
239
|
+
console.debug('Database query', {
|
|
240
|
+
collection: this.#client.name,
|
|
241
|
+
method,
|
|
242
|
+
...attributes
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const INDEX_TYPES = {
|
|
248
|
+
'asc': 1,
|
|
249
|
+
'desc': -1,
|
|
250
|
+
'hash': 'hashed'
|
|
105
251
|
}
|
|
106
252
|
|
|
253
|
+
const ERR_DUPLICATE_KEY = 11000
|
|
254
|
+
|
|
107
255
|
exports.Storage = Storage
|
package/src/translate.js
CHANGED
|
@@ -7,12 +7,20 @@ const parse = { ...require('./translate/criteria'), ...require('./translate/opti
|
|
|
7
7
|
* @returns {{criteria: Object, options: Object}}
|
|
8
8
|
*/
|
|
9
9
|
const translate = (query) => {
|
|
10
|
-
const result = {
|
|
10
|
+
const result = {
|
|
11
|
+
criteria: query?.criteria === undefined ? {} : parse.criteria(query.criteria),
|
|
12
|
+
options: query?.options === undefined ? {} : parse.options(query.options)
|
|
13
|
+
}
|
|
11
14
|
|
|
12
|
-
if (query
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
if (query?.id !== undefined) {
|
|
16
|
+
result.criteria._id = query.id
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (query?.version !== undefined) {
|
|
20
|
+
result.criteria._version = query.version
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
result.criteria._deleted = null
|
|
16
24
|
|
|
17
25
|
return result
|
|
18
26
|
}
|
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/connection.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
// noinspection JSCheckFunctionSignatures
|
|
2
|
-
|
|
3
|
-
'use strict'
|
|
4
|
-
|
|
5
|
-
const { MongoClient } = require('mongodb')
|
|
6
|
-
const { Connector } = require('@toa.io/core')
|
|
7
|
-
const { resolve } = require('@toa.io/pointer')
|
|
8
|
-
const { Conveyor } = require('@toa.io/conveyor')
|
|
9
|
-
const { ID } = require('./deployment')
|
|
10
|
-
|
|
11
|
-
class Connection extends Connector {
|
|
12
|
-
#locator
|
|
13
|
-
/** @type {import('mongodb').MongoClient} */
|
|
14
|
-
#client
|
|
15
|
-
/** @type {import('mongodb').Collection<toa.mongodb.Record>} */
|
|
16
|
-
#collection
|
|
17
|
-
/** @type {toa.conveyor.Conveyor<toa.core.storages.Record, boolean>} */
|
|
18
|
-
#conveyor
|
|
19
|
-
|
|
20
|
-
constructor (locator) {
|
|
21
|
-
super()
|
|
22
|
-
|
|
23
|
-
this.#locator = locator
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async open () {
|
|
27
|
-
const urls = await this.#resolveURLs()
|
|
28
|
-
const db = this.#locator.namespace
|
|
29
|
-
const collection = this.#locator.name
|
|
30
|
-
|
|
31
|
-
this.#client = new MongoClient(urls[0], OPTIONS)
|
|
32
|
-
|
|
33
|
-
await this.#client.connect()
|
|
34
|
-
|
|
35
|
-
this.#collection = this.#client.db(db).collection(collection)
|
|
36
|
-
this.#conveyor = new Conveyor((objects) => this.addMany(objects))
|
|
37
|
-
|
|
38
|
-
console.info(`Storage Mongo '${this.#locator.id}' connected`)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async close () {
|
|
42
|
-
await this.#client?.close()
|
|
43
|
-
|
|
44
|
-
console.info(`Storage Mongo '${this.#locator.id}' disconnected`)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** @hot */
|
|
48
|
-
async get (query, options) {
|
|
49
|
-
return /** @type {toa.mongodb.Record} */ this.#collection.findOne(query, options)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** @hot */
|
|
53
|
-
async find (query, options) {
|
|
54
|
-
const cursor = this.#collection.find(query, options)
|
|
55
|
-
|
|
56
|
-
return cursor.toArray()
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** @hot */
|
|
60
|
-
async add (record) {
|
|
61
|
-
return this.#conveyor.process(record)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async addMany (records) {
|
|
65
|
-
let result
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
const response = await this.#collection.insertMany(records, { ordered: false })
|
|
69
|
-
|
|
70
|
-
result = response.acknowledged
|
|
71
|
-
} catch (e) {
|
|
72
|
-
if (e.code === ERR_DUPLICATE_KEY) result = false
|
|
73
|
-
else throw e
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return result
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** @hot */
|
|
80
|
-
async replace (query, record, options) {
|
|
81
|
-
return await this.#collection.findOneAndReplace(query, record, options)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** @hot */
|
|
85
|
-
async update (query, update, options) {
|
|
86
|
-
return this.#collection.findOneAndUpdate(query, update, options)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async #resolveURLs () {
|
|
90
|
-
if (process.env.TOA_DEV === '1') return ['mongodb://developer:secret@localhost']
|
|
91
|
-
else return await resolve(ID, this.#locator.id)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const OPTIONS = {
|
|
96
|
-
ignoreUndefined: true,
|
|
97
|
-
connectTimeoutMS: 0,
|
|
98
|
-
serverSelectionTimeoutMS: 0
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const ERR_DUPLICATE_KEY = 11000
|
|
102
|
-
|
|
103
|
-
exports.Connection = Connection
|
package/test/connection.test.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const insertManyMock = jest.fn(() => ({ acknowledged: true }))
|
|
4
|
-
jest.mock('mongodb', () => ({
|
|
5
|
-
__esModule: true,
|
|
6
|
-
MongoClient: function () {
|
|
7
|
-
this.connect = () => {},
|
|
8
|
-
this.db = () => ({
|
|
9
|
-
collection: () => ({
|
|
10
|
-
insertMany: insertManyMock
|
|
11
|
-
})
|
|
12
|
-
})
|
|
13
|
-
return this
|
|
14
|
-
},
|
|
15
|
-
}))
|
|
16
|
-
jest.mock('@toa.io/pointer', () => ({
|
|
17
|
-
__esModule: true,
|
|
18
|
-
resolve: () => ['url'],
|
|
19
|
-
}))
|
|
20
|
-
const { generate } = require('randomstring')
|
|
21
|
-
const { Connection } = require('../src/connection')
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
let connection
|
|
25
|
-
|
|
26
|
-
beforeEach(async () => {
|
|
27
|
-
jest.clearAllMocks()
|
|
28
|
-
connection = new Connection({ id: 1 })
|
|
29
|
-
await connection.open()
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('should be', () => {
|
|
33
|
-
expect(Connection).toBeDefined()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('should insert', async () => {
|
|
37
|
-
const object = generate()
|
|
38
|
-
|
|
39
|
-
await connection.add(object)
|
|
40
|
-
|
|
41
|
-
expect(insertManyMock).toHaveBeenCalledWith([object], { ordered: false })
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('should batch insert', async () => {
|
|
45
|
-
const a = generate()
|
|
46
|
-
const b = generate()
|
|
47
|
-
const c = generate()
|
|
48
|
-
|
|
49
|
-
await Promise.all([
|
|
50
|
-
connection.add(a),
|
|
51
|
-
connection.add(b),
|
|
52
|
-
connection.add(c)
|
|
53
|
-
])
|
|
54
|
-
|
|
55
|
-
expect(insertManyMock).toHaveBeenCalledTimes(2)
|
|
56
|
-
expect(insertManyMock).toHaveBeenNthCalledWith(1, [a], { ordered: false })
|
|
57
|
-
expect(insertManyMock).toHaveBeenNthCalledWith(2, [b, c], { ordered: false })
|
|
58
|
-
})
|