@toa.io/storages.mongodb 1.0.0-alpha.11 → 1.0.0-alpha.116

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