@toa.io/storages.mongodb 1.0.0-alpha.8 → 1.0.0-alpha.81

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