@toa.io/storages.mongodb 1.0.0-alpha.14 → 1.0.0-alpha.143

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/storages.mongodb",
3
- "version": "1.0.0-alpha.14",
3
+ "version": "1.0.0-alpha.143",
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/console": "1.0.0-alpha.14",
23
- "@toa.io/conveyor": "1.0.0-alpha.14",
24
- "@toa.io/core": "1.0.0-alpha.14",
25
- "@toa.io/generic": "1.0.0-alpha.14",
26
- "@toa.io/pointer": "1.0.0-alpha.14",
27
- "mongodb": "6.3.0",
22
+ "@toa.io/conveyor": "1.0.0-alpha.93",
23
+ "@toa.io/core": "1.0.0-alpha.136",
24
+ "@toa.io/generic": "1.0.0-alpha.93",
25
+ "@toa.io/pointer": "1.0.0-alpha.143",
26
+ "mongodb": "6.7.0",
27
+ "openspan": "1.0.0-alpha.93",
28
28
  "saslprep": "1.0.3"
29
29
  },
30
- "gitHead": "8aa52cb97021695885c8dbe64beca26c9665fc8f"
30
+ "gitHead": "a98d593cac7077f15ef3d178c35dbb9f5742af47"
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 db = this.resolveDB()
61
- const collection = this.locator.lowercase
64
+ const dbname = this.resolveDB()
62
65
 
63
- this.key = getKey(db, urls)
66
+ this.key = getKey(dbname, urls)
64
67
 
65
- INSTANCES[this.key] ??= this.createInstance(urls)
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
- this.collection = this.instance.client.db(db).collection(collection)
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:', hosts.join(', '))
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(connection, entity)
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
- * @param {toa.core.storages.Record} entity
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
- * @param {toa.mongodb.Record} record
18
- * @returns {toa.core.storages.Record}
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,77 @@
1
1
  'use strict'
2
2
 
3
- const {
4
- Connector,
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
- to,
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
- #connection
10
+ #client
11
+
12
+ /** @type {import('mongodb').Collection} */
13
+ #collection
16
14
  #entity
17
15
 
18
- constructor (connection, entity) {
16
+ #logs
17
+
18
+ constructor (client, entity) {
19
19
  super()
20
20
 
21
- this.#connection = connection
21
+ this.#client = client
22
22
  this.#entity = entity
23
23
 
24
- this.depends(connection)
24
+ this.depends(client)
25
+
26
+ this.#logs = console.fork({ collection: client.name })
27
+ }
28
+
29
+ get raw () {
30
+ return this.#collection
25
31
  }
26
32
 
27
33
  async open () {
34
+ this.#collection = this.#client.collection
35
+
28
36
  await this.index()
29
37
  }
30
38
 
31
39
  async get (query) {
32
- const {
33
- criteria,
34
- options
35
- } = translate(query)
40
+ const { criteria, options } = translate(query)
41
+
42
+ this.debug('findOne', { criteria, options })
36
43
 
37
- const record = await this.#connection.get(criteria, options)
44
+ const record = await this.#collection.findOne(criteria, options)
38
45
 
39
46
  return from(record)
40
47
  }
41
48
 
42
49
  async find (query) {
43
- const {
44
- criteria,
45
- options
46
- } = translate(query)
50
+ const { criteria, options } = translate(query)
51
+
52
+ criteria._deleted = null
47
53
 
48
- const recordset = await this.#connection.find(criteria, options)
54
+ this.debug('find', { criteria, options })
55
+
56
+ const recordset = await this.#collection.find(criteria, options).toArray()
49
57
 
50
58
  return recordset.map((item) => from(item))
51
59
  }
52
60
 
61
+ async stream (query = undefined) {
62
+ const { criteria, options } = translate(query)
63
+
64
+ this.debug('find (stream)', { criteria, options })
65
+
66
+ return this.#collection.find(criteria, options).stream({ transform: from })
67
+ }
68
+
53
69
  async add (entity) {
54
70
  const record = to(entity)
55
- const result = await this.#connection.add(record)
71
+
72
+ this.debug('insertOne', { record })
73
+
74
+ const result = await this.#collection.insertOne(record)
56
75
 
57
76
  return result.acknowledged
58
77
  }
@@ -62,41 +81,47 @@ class Storage extends Connector {
62
81
  _id: entity.id,
63
82
  _version: entity._version - 1
64
83
  }
65
- const result = await this.#connection.replace(criteria, to(entity))
84
+
85
+ const record = to(entity)
86
+
87
+ this.debug('findOneAndReplace', { criteria, record })
88
+
89
+ const result = await this.#collection.findOneAndReplace(criteria, record)
66
90
 
67
91
  return result !== null
68
92
  }
69
93
 
70
- async store (entity) {
94
+ async store (entity, attempt = 0) {
71
95
  try {
72
- if (entity._version === 1) {
96
+ if (entity._version === 1)
73
97
  return await this.add(entity)
74
- } else {
98
+ else
75
99
  return await this.set(entity)
76
- }
77
100
  } catch (error) {
78
101
  if (error.code === ERR_DUPLICATE_KEY) {
79
-
80
102
  const id = error.keyPattern === undefined
81
103
  ? error.message.includes(' index: _id_ ') // AWS DocumentDB
82
104
  : error.keyPattern._id === 1
83
105
 
84
- if (id) {
106
+ if (id)
85
107
  return false
86
- } else {
87
- throw new exceptions.DuplicateException()
88
- }
89
- } else {
108
+ else
109
+ throw new exceptions.DuplicateException(this.#client.name, entity)
110
+ } else if (error.cause?.code === 'ECONNREFUSED') {
111
+ // This is temporary and should be replaced with a class decorator.
112
+ if (attempt > 10)
113
+ throw error
114
+
115
+ await new Promise((resolve) => setTimeout(resolve, 1000))
116
+
117
+ return this.store(entity)
118
+ } else
90
119
  throw error
91
- }
92
120
  }
93
121
  }
94
122
 
95
123
  async upsert (query, changeset) {
96
- const {
97
- criteria,
98
- options
99
- } = translate(query)
124
+ const { criteria, options } = translate(query)
100
125
 
101
126
  if (!('_deleted' in changeset) || changeset._deleted === null) {
102
127
  delete criteria._deleted
@@ -108,13 +133,36 @@ class Storage extends Connector {
108
133
  $inc: { _version: 1 }
109
134
  }
110
135
 
111
- options.returnDocument = 'after'
136
+ options.returnDocument = ReturnDocument.AFTER
137
+
138
+ this.debug('findOneAndUpdate', { criteria, update, options })
112
139
 
113
- const result = await this.#connection.update(criteria, update, options)
140
+ const result = await this.#collection.findOneAndUpdate(criteria, update, options)
114
141
 
115
142
  return from(result)
116
143
  }
117
144
 
145
+ async ensure (query, properties, state) {
146
+ let { criteria, options } = translate(query)
147
+
148
+ if (query === undefined)
149
+ criteria = properties
150
+
151
+ const update = { $setOnInsert: to(state) }
152
+
153
+ options.upsert = true
154
+ options.returnDocument = ReturnDocument.AFTER
155
+
156
+ this.#logs.debug('Database query', { method: 'findOneAndUpdate', criteria, update, options })
157
+
158
+ const result = await this.#collection.findOneAndUpdate(criteria, update, options)
159
+
160
+ if (result._deleted !== undefined && result._deleted !== null)
161
+ return null
162
+ else
163
+ return from(result)
164
+ }
165
+
118
166
  async index () {
119
167
  const indexes = []
120
168
 
@@ -135,16 +183,14 @@ class Storage extends Connector {
135
183
 
136
184
  const sparse = this.checkFields(Object.keys(fields))
137
185
 
138
- await this.#connection.index(fields, {
139
- name,
140
- sparse
141
- })
186
+ await this.#collection.createIndex(fields, { name, sparse })
187
+ .catch((e) => this.#logs.warn('Index creation failed', { name, fields, error: e }))
142
188
 
143
189
  indexes.push(name)
144
190
  }
145
191
  }
146
192
 
147
- await this.removeObsolete(indexes)
193
+ await this.removeObsoleteIndexes(indexes)
148
194
  }
149
195
 
150
196
  async uniqueIndex (name, properties, sparse = false) {
@@ -155,23 +201,30 @@ class Storage extends Connector {
155
201
 
156
202
  name = 'unique_' + name
157
203
 
158
- await this.#connection.index(fields, {
159
- name,
160
- unique: true,
161
- sparse
162
- })
204
+ await this.#collection.createIndex(fields, { name, unique: true, sparse })
205
+ .catch((e) => this.#logs.warn('Unique index creation failed', { name, fields, error: e }))
163
206
 
164
207
  return name
165
208
  }
166
209
 
167
- async removeObsolete (desired) {
168
- const current = await this.#connection.indexes()
210
+ async removeObsoleteIndexes (desired) {
211
+ const current = await this.getCurrentIndexes()
169
212
  const obsolete = current.filter((name) => !desired.includes(name))
170
213
 
171
214
  if (obsolete.length > 0) {
172
- console.info(`Remove obsolete indexes: [${obsolete.join(', ')}]`)
215
+ this.#logs.info('Removing obsolete indexes', { indexes: obsolete.join(', ') })
216
+
217
+ await Promise.all(obsolete.map((name) => this.#collection.dropIndex(name)))
218
+ }
219
+ }
220
+
221
+ async getCurrentIndexes () {
222
+ try {
223
+ const array = await this.#collection.listIndexes().toArray()
173
224
 
174
- await this.#connection.dropIndexes(obsolete)
225
+ return array.map(({ name }) => name).filter((name) => name !== '_id_')
226
+ } catch {
227
+ return []
175
228
  }
176
229
  }
177
230
 
@@ -179,24 +232,27 @@ class Storage extends Connector {
179
232
  const optional = []
180
233
 
181
234
  for (const field of fields) {
182
- if (!(field in this.#entity.schema.properties)) {
235
+ if (!(field in this.#entity.schema.properties))
183
236
  throw new Error(`Index field '${field}' is not defined.`)
184
- }
185
237
 
186
- if (!this.#entity.schema.required?.includes(field)) {
238
+ if (!this.#entity.schema.required?.includes(field))
187
239
  optional.push(field)
188
- }
189
240
  }
190
241
 
191
242
  if (optional.length > 0) {
192
- console.info(`Index fields [${optional.join(', ')}] are optional, creating sparse index.`)
243
+ this.#logs.info('Index fields are optional, creating sparse index', { fields: optional })
193
244
 
194
245
  return true
195
- } else {
246
+ } else
196
247
  return false
197
- }
198
248
  }
199
249
 
250
+ debug (method, attributes) {
251
+ this.#logs.debug('Database query', {
252
+ method,
253
+ ...attributes
254
+ })
255
+ }
200
256
  }
201
257
 
202
258
  const INDEX_TYPES = {
package/src/translate.js CHANGED
@@ -12,15 +12,11 @@ const translate = (query) => {
12
12
  options: query?.options === undefined ? {} : parse.options(query.options)
13
13
  }
14
14
 
15
- if (query?.id !== undefined) {
15
+ if (query?.id !== undefined)
16
16
  result.criteria._id = query.id
17
- }
18
17
 
19
- if (query?.version !== undefined) {
18
+ if (query?.version !== undefined)
20
19
  result.criteria._version = query.version
21
- }
22
-
23
- result.criteria._deleted = null
24
20
 
25
21
  return result
26
22
  }
@@ -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