@toa.io/storages.mongodb 1.0.0-alpha.13 → 1.0.0-alpha.136

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.13",
3
+ "version": "1.0.0-alpha.136",
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.13",
23
- "@toa.io/conveyor": "1.0.0-alpha.13",
24
- "@toa.io/core": "1.0.0-alpha.13",
25
- "@toa.io/generic": "1.0.0-alpha.13",
26
- "@toa.io/pointer": "1.0.0-alpha.13",
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.125",
26
+ "mongodb": "6.7.0",
27
+ "openspan": "1.0.0-alpha.93",
28
28
  "saslprep": "1.0.3"
29
29
  },
30
- "gitHead": "f779cc4a98abe96e4e928b918f9521826403536b"
30
+ "gitHead": "73722f06d137283915ca8710ae09e5e53307e1f9"
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,26 @@ class Client extends Connector {
57
61
  */
58
62
  async open () {
59
63
  const urls = await this.resolveURLs()
60
- const db = process.env.CONTEXT ?? 'toa-dev'
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
68
  INSTANCES[this.key] ??= this.createInstance(urls)
66
69
 
67
70
  this.instance = await INSTANCES[this.key]
68
71
  this.instance.count++
69
72
 
70
- this.collection = this.instance.client.db(db).collection(collection)
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
+ }
71
84
  }
72
85
 
73
86
  /**
@@ -95,7 +108,7 @@ class Client extends Connector {
95
108
  const client = new MongoClient(urls.join(','), OPTIONS)
96
109
  const hosts = urls.map((str) => new URL(str).host)
97
110
 
98
- console.info('Connecting to MongoDB:', hosts.join(', '))
111
+ console.info('Connecting to MongoDB', { address: hosts.join(', ') })
99
112
 
100
113
  await client.connect()
101
114
 
@@ -116,6 +129,22 @@ class Client extends Connector {
116
129
  return await resolve(ID, this.locator.id)
117
130
  }
118
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
+ }
119
148
  }
120
149
 
121
150
  function getKey (db, urls) {
@@ -123,9 +152,9 @@ function getKey (db, urls) {
123
152
  }
124
153
 
125
154
  const OPTIONS = {
126
- ignoreUndefined: true,
127
- connectTimeoutMS: 0,
128
- serverSelectionTimeoutMS: 0
155
+ ignoreUndefined: true
129
156
  }
130
157
 
158
+ const ALREADY_EXISTS = 48
159
+
131
160
  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,57 +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)
47
- const recordset = await this.#connection.find(criteria, options)
50
+ const { criteria, options } = translate(query)
51
+
52
+ criteria._deleted = null
53
+
54
+ this.debug('find', { criteria, options })
55
+
56
+ const recordset = await this.#collection.find(criteria, options).toArray()
48
57
 
49
58
  return recordset.map((item) => from(item))
50
59
  }
51
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
+
52
69
  async add (entity) {
53
70
  const record = to(entity)
54
- const result = await this.#connection.add(record)
71
+
72
+ this.debug('insertOne', { record })
73
+
74
+ const result = await this.#collection.insertOne(record)
55
75
 
56
76
  return result.acknowledged
57
77
  }
@@ -61,51 +81,88 @@ class Storage extends Connector {
61
81
  _id: entity.id,
62
82
  _version: entity._version - 1
63
83
  }
64
- 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)
65
90
 
66
91
  return result !== null
67
92
  }
68
93
 
69
- async store (entity) {
94
+ async store (entity, attempt = 0) {
70
95
  try {
71
- if (entity._version === 1) {
96
+ if (entity._version === 1)
72
97
  return await this.add(entity)
73
- } else {
98
+ else
74
99
  return await this.set(entity)
75
- }
76
100
  } catch (error) {
77
101
  if (error.code === ERR_DUPLICATE_KEY) {
78
- const keys = error.keyValue ? Object.keys(error.keyValue) : undefined
79
-
80
- if (keys === undefined) {
81
- console.error(error)
82
- }
83
-
84
- return new exceptions.DuplicateException(keys)
85
- } else {
102
+ const id = error.keyPattern === undefined
103
+ ? error.message.includes(' index: _id_ ') // AWS DocumentDB
104
+ : error.keyPattern._id === 1
105
+
106
+ if (id)
107
+ return false
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
86
119
  throw error
87
- }
88
120
  }
89
121
  }
90
122
 
91
123
  async upsert (query, changeset) {
92
- const {
93
- criteria,
94
- options
95
- } = translate(query)
124
+ const { criteria, options } = translate(query)
125
+
126
+ if (!('_deleted' in changeset) || changeset._deleted === null) {
127
+ delete criteria._deleted
128
+ changeset._deleted = null
129
+ }
96
130
 
97
131
  const update = {
98
132
  $set: { ...changeset },
99
133
  $inc: { _version: 1 }
100
134
  }
101
135
 
102
- options.returnDocument = 'after'
136
+ options.returnDocument = ReturnDocument.AFTER
103
137
 
104
- const result = await this.#connection.update(criteria, update, options)
138
+ this.debug('findOneAndUpdate', { criteria, update, options })
139
+
140
+ const result = await this.#collection.findOneAndUpdate(criteria, update, options)
105
141
 
106
142
  return from(result)
107
143
  }
108
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
+
109
166
  async index () {
110
167
  const indexes = []
111
168
 
@@ -126,16 +183,14 @@ class Storage extends Connector {
126
183
 
127
184
  const sparse = this.checkFields(Object.keys(fields))
128
185
 
129
- await this.#connection.index(fields, {
130
- name,
131
- sparse
132
- })
186
+ await this.#collection.createIndex(fields, { name, sparse })
187
+ .catch((e) => this.#logs.warn('Index creation failed', { name, fields, error: e }))
133
188
 
134
189
  indexes.push(name)
135
190
  }
136
191
  }
137
192
 
138
- await this.removeObsolete(indexes)
193
+ await this.removeObsoleteIndexes(indexes)
139
194
  }
140
195
 
141
196
  async uniqueIndex (name, properties, sparse = false) {
@@ -146,23 +201,30 @@ class Storage extends Connector {
146
201
 
147
202
  name = 'unique_' + name
148
203
 
149
- await this.#connection.index(fields, {
150
- name,
151
- unique: true,
152
- sparse
153
- })
204
+ await this.#collection.createIndex(fields, { name, unique: true, sparse })
205
+ .catch((e) => this.#logs.warn('Unique index creation failed', { name, fields, error: e }))
154
206
 
155
207
  return name
156
208
  }
157
209
 
158
- async removeObsolete (desired) {
159
- const current = await this.#connection.indexes()
210
+ async removeObsoleteIndexes (desired) {
211
+ const current = await this.getCurrentIndexes()
160
212
  const obsolete = current.filter((name) => !desired.includes(name))
161
213
 
162
214
  if (obsolete.length > 0) {
163
- 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()
164
224
 
165
- await this.#connection.dropIndexes(obsolete)
225
+ return array.map(({ name }) => name).filter((name) => name !== '_id_')
226
+ } catch {
227
+ return []
166
228
  }
167
229
  }
168
230
 
@@ -170,24 +232,27 @@ class Storage extends Connector {
170
232
  const optional = []
171
233
 
172
234
  for (const field of fields) {
173
- if (!(field in this.#entity.schema.properties)) {
235
+ if (!(field in this.#entity.schema.properties))
174
236
  throw new Error(`Index field '${field}' is not defined.`)
175
- }
176
237
 
177
- if (!this.#entity.schema.required?.includes(field)) {
238
+ if (!this.#entity.schema.required?.includes(field))
178
239
  optional.push(field)
179
- }
180
240
  }
181
241
 
182
242
  if (optional.length > 0) {
183
- console.info(`Index fields [${optional.join(', ')}] are optional, creating sparse index.`)
243
+ this.#logs.info('Index fields are optional, creating sparse index', { fields: optional })
184
244
 
185
245
  return true
186
- } else {
246
+ } else
187
247
  return false
188
- }
189
248
  }
190
249
 
250
+ debug (method, attributes) {
251
+ this.#logs.debug('Database query', {
252
+ method,
253
+ ...attributes
254
+ })
255
+ }
191
256
  }
192
257
 
193
258
  const INDEX_TYPES = {
package/src/translate.js CHANGED
@@ -7,12 +7,16 @@ 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 = { criteria: {}, options: {} }
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.criteria !== undefined) result.criteria = parse.criteria(query.criteria)
13
- if (query.options !== undefined) result.options = parse.options(query.options)
14
- if (query.id !== undefined) result.criteria._id = query.id
15
- if (query.version !== undefined) result.criteria._version = query.version
15
+ if (query?.id !== undefined)
16
+ result.criteria._id = query.id
17
+
18
+ if (query?.version !== undefined)
19
+ result.criteria._version = query.version
16
20
 
17
21
  return result
18
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