@toa.io/storages.mongodb 1.0.0-alpha.2 → 1.0.0-alpha.21

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.2",
3
+ "version": "1.0.0-alpha.21",
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.2",
23
- "@toa.io/conveyor": "1.0.0-alpha.2",
24
- "@toa.io/core": "1.0.0-alpha.2",
25
- "@toa.io/generic": "1.0.0-alpha.2",
26
- "@toa.io/pointer": "1.0.0-alpha.2",
22
+ "@toa.io/console": "1.0.0-alpha.21",
23
+ "@toa.io/conveyor": "1.0.0-alpha.21",
24
+ "@toa.io/core": "1.0.0-alpha.21",
25
+ "@toa.io/generic": "1.0.0-alpha.21",
26
+ "@toa.io/pointer": "1.0.0-alpha.21",
27
27
  "mongodb": "6.3.0",
28
28
  "saslprep": "1.0.3"
29
29
  },
30
- "gitHead": "7688e6e980a65c82ac2e459be4e355eebf406cd0"
30
+ "gitHead": "da9f4c278f6ab02a28f65c6e25c713c013cbfce9"
31
31
  }
package/src/client.js ADDED
@@ -0,0 +1,159 @@
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 { Connector } = require('@toa.io/core')
10
+ const { resolve } = require('@toa.io/pointer')
11
+ const { ID } = require('./deployment')
12
+ const { MongoClient } = require('mongodb')
13
+
14
+ /**
15
+ * @type {Record<string, Promise<Instance>>}
16
+ */
17
+ const INSTANCES = {}
18
+
19
+ class Client extends Connector {
20
+ /**
21
+ * @public
22
+ * @type {import('mongodb').Collection}
23
+ */
24
+ collection
25
+
26
+ /**
27
+ * @private
28
+ * @type {Locator}
29
+ */
30
+ locator
31
+
32
+ /**
33
+ * @private
34
+ * @type {Instance}
35
+ */
36
+ instance
37
+
38
+ /**
39
+ * @private
40
+ * @type {string}
41
+ */
42
+ key
43
+
44
+ /**
45
+ * @param {Locator} locator
46
+ */
47
+ constructor (locator) {
48
+ super()
49
+
50
+ this.locator = locator
51
+ }
52
+
53
+ /**
54
+ * @protected
55
+ * @override
56
+ * @return {Promise<void>}
57
+ */
58
+ async open () {
59
+ const urls = await this.resolveURLs()
60
+ const dbname = this.resolveDB()
61
+ const collname = this.locator.lowercase
62
+
63
+ this.key = getKey(dbname, urls)
64
+
65
+ INSTANCES[this.key] ??= this.createInstance(urls)
66
+
67
+ this.instance = await INSTANCES[this.key]
68
+ this.instance.count++
69
+
70
+ const db = this.instance.client.db(dbname)
71
+
72
+ try {
73
+ this.collection = await db.createCollection(collname)
74
+ } catch (e) {
75
+ if (e.code !== ALREADY_EXISTS) {
76
+ throw e
77
+ }
78
+
79
+ this.collection = db.collection(collname)
80
+ }
81
+ }
82
+
83
+ /**
84
+ * @protected
85
+ * @override
86
+ * @return {Promise<void>}
87
+ */
88
+ async close () {
89
+ const instance = await INSTANCES[this.key]
90
+
91
+ instance.count--
92
+
93
+ if (instance.count === 0) {
94
+ await instance.client.close()
95
+ delete INSTANCES[this.key]
96
+ }
97
+ }
98
+
99
+ /**
100
+ * @private
101
+ * @param {string[]} urls
102
+ * @return {Promise<Instance>}
103
+ */
104
+ async createInstance (urls) {
105
+ const client = new MongoClient(urls.join(','), OPTIONS)
106
+ const hosts = urls.map((str) => new URL(str).host)
107
+
108
+ console.info('Connecting to MongoDB:', hosts.join(', '))
109
+
110
+ await client.connect()
111
+
112
+ return {
113
+ count: 0,
114
+ client
115
+ }
116
+ }
117
+
118
+ /**
119
+ * @private
120
+ * @return {Promise<string[]>}
121
+ */
122
+ async resolveURLs () {
123
+ if (process.env.TOA_DEV === '1') {
124
+ return ['mongodb://developer:secret@localhost']
125
+ } else {
126
+ return await resolve(ID, this.locator.id)
127
+ }
128
+ }
129
+
130
+ /**
131
+ * @private
132
+ * @return {string}
133
+ */
134
+ resolveDB () {
135
+ if (process.env.TOA_CONTEXT !== undefined) {
136
+ return process.env.TOA_CONTEXT
137
+ }
138
+
139
+ if (process.env.TOA_DEV === '1') {
140
+ return 'toa-dev'
141
+ }
142
+
143
+ throw new Error('Environment variable TOA_CONTEXT is not defined')
144
+ }
145
+ }
146
+
147
+ function getKey (db, urls) {
148
+ return db + ':' + urls.sort().join(' ')
149
+ }
150
+
151
+ const OPTIONS = {
152
+ ignoreUndefined: true,
153
+ connectTimeoutMS: 0,
154
+ serverSelectionTimeoutMS: 0
155
+ }
156
+
157
+ const ALREADY_EXISTS = 48
158
+
159
+ exports.Client = Client
@@ -0,0 +1,69 @@
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
package/src/factory.js CHANGED
@@ -1,13 +1,15 @@
1
1
  'use strict'
2
2
 
3
- const { Connection } = require('./connection')
3
+ const { Client } = require('./client')
4
+ const { Collection } = require('./collection')
4
5
  const { Storage } = require('./storage')
5
6
 
6
7
  class Factory {
7
- storage (locator) {
8
- const connection = new Connection(locator)
8
+ storage (locator, entity) {
9
+ const client = new Client(locator)
10
+ const connection = new Collection(client)
9
11
 
10
- return new Storage(connection)
12
+ return new Storage(connection, entity)
11
13
  }
12
14
  }
13
15
 
package/src/record.js CHANGED
@@ -5,9 +5,12 @@
5
5
  * @returns {toa.mongodb.Record}
6
6
  */
7
7
  const to = (entity) => {
8
- const { id, _version, ...rest } = entity
8
+ const {
9
+ id,
10
+ ...rest
11
+ } = entity
9
12
 
10
- return /** @type {toa.mongodb.Record} */ { _id: id, _version: _version + 1, ...rest }
13
+ return /** @type {toa.mongodb.Record} */ { _id: id, ...rest }
11
14
  }
12
15
 
13
16
  /**
@@ -17,7 +20,10 @@ const to = (entity) => {
17
20
  const from = (record) => {
18
21
  if (record === undefined || record === null) return null
19
22
 
20
- const { _id, ...rest } = record
23
+ const {
24
+ _id,
25
+ ...rest
26
+ } = record
21
27
 
22
28
  return { id: _id, ...rest }
23
29
  }
package/src/storage.js CHANGED
@@ -1,30 +1,38 @@
1
1
  'use strict'
2
2
 
3
- const { Connector } = require('@toa.io/core')
3
+ const {
4
+ Connector,
5
+ exceptions
6
+ } = require('@toa.io/core')
4
7
 
5
8
  const { translate } = require('./translate')
6
- const { to, from } = require('./record')
9
+ const {
10
+ to,
11
+ from
12
+ } = require('./record')
7
13
 
8
- /**
9
- * @implements {toa.core.Storage}
10
- */
11
14
  class Storage extends Connector {
12
- /** @type {toa.mongodb.Connection} */
13
15
  #connection
16
+ #entity
14
17
 
15
- /**
16
- * @param {toa.mongodb.Connection} connection
17
- */
18
- constructor (connection) {
18
+ constructor (connection, entity) {
19
19
  super()
20
20
 
21
21
  this.#connection = connection
22
+ this.#entity = entity
22
23
 
23
24
  this.depends(connection)
24
25
  }
25
26
 
27
+ async open () {
28
+ await this.index()
29
+ }
30
+
26
31
  async get (query) {
27
- const { criteria, options } = translate(query)
32
+ const {
33
+ criteria,
34
+ options
35
+ } = translate(query)
28
36
 
29
37
  const record = await this.#connection.get(criteria, options)
30
38
 
@@ -32,7 +40,11 @@ class Storage extends Connector {
32
40
  }
33
41
 
34
42
  async find (query) {
35
- const { criteria, options } = translate(query)
43
+ const {
44
+ criteria,
45
+ options
46
+ } = translate(query)
47
+
36
48
  const recordset = await this.#connection.find(criteria, options)
37
49
 
38
50
  return recordset.map((item) => from(item))
@@ -40,35 +52,60 @@ class Storage extends Connector {
40
52
 
41
53
  async add (entity) {
42
54
  const record = to(entity)
55
+ const result = await this.#connection.add(record)
43
56
 
44
- return await this.#connection.add(record)
57
+ return result.acknowledged
45
58
  }
46
59
 
47
60
  async set (entity) {
48
- const criteria = { _id: entity.id, _version: entity._version }
61
+ const criteria = {
62
+ _id: entity.id,
63
+ _version: entity._version - 1
64
+ }
49
65
  const result = await this.#connection.replace(criteria, to(entity))
50
66
 
51
67
  return result !== null
52
68
  }
53
69
 
54
70
  async store (entity) {
55
- if (entity._version === 0) return this.add(entity)
56
- else return this.set(entity)
71
+ try {
72
+ if (entity._version === 1) {
73
+ return await this.add(entity)
74
+ } else {
75
+ return await this.set(entity)
76
+ }
77
+ } catch (error) {
78
+ if (error.code === ERR_DUPLICATE_KEY) {
79
+
80
+ const id = error.keyPattern === undefined
81
+ ? error.message.includes(' index: _id_ ') // AWS DocumentDB
82
+ : error.keyPattern._id === 1
83
+
84
+ if (id) {
85
+ return false
86
+ } else {
87
+ throw new exceptions.DuplicateException()
88
+ }
89
+ } else {
90
+ throw error
91
+ }
92
+ }
57
93
  }
58
94
 
59
- async upsert (query, changeset, insert) {
60
- const { criteria, options } = translate(query)
61
- const update = { $set: { ...changeset }, $inc: { _version: 1 } }
62
-
63
- if (insert !== undefined) {
64
- delete insert._version
95
+ async upsert (query, changeset) {
96
+ const {
97
+ criteria,
98
+ options
99
+ } = translate(query)
65
100
 
66
- options.upsert = true
67
-
68
- if (criteria._id !== undefined) insert._id = criteria._id
69
- else return null // this shouldn't ever happen
101
+ if (!('_deleted' in changeset) || changeset._deleted === null) {
102
+ delete criteria._deleted
103
+ changeset._deleted = null
104
+ }
70
105
 
71
- if (Object.keys(insert) > 0) update.$setOnInsert = insert
106
+ const update = {
107
+ $set: { ...changeset },
108
+ $inc: { _version: 1 }
72
109
  }
73
110
 
74
111
  options.returnDocument = 'after'
@@ -77,6 +114,97 @@ class Storage extends Connector {
77
114
 
78
115
  return from(result)
79
116
  }
117
+
118
+ async index () {
119
+ const indexes = []
120
+
121
+ if (this.#entity.unique !== undefined) {
122
+ for (const [name, fields] of Object.entries(this.#entity.unique)) {
123
+ const sparse = this.checkFields(fields)
124
+ const unique = await this.uniqueIndex(name, fields, sparse)
125
+
126
+ indexes.push(unique)
127
+ }
128
+ }
129
+
130
+ if (this.#entity.index !== undefined) {
131
+ for (const [suffix, declaration] of Object.entries(this.#entity.index)) {
132
+ const name = 'index_' + suffix
133
+ const fields = Object.fromEntries(Object.entries(declaration)
134
+ .map(([name, type]) => [name, INDEX_TYPES[type]]))
135
+
136
+ const sparse = this.checkFields(Object.keys(fields))
137
+
138
+ await this.#connection.index(fields, {
139
+ name,
140
+ sparse
141
+ })
142
+
143
+ indexes.push(name)
144
+ }
145
+ }
146
+
147
+ await this.removeObsolete(indexes)
148
+ }
149
+
150
+ async uniqueIndex (name, properties, sparse = false) {
151
+ const fields = properties.reduce((acc, property) => {
152
+ acc[property] = 1
153
+ return acc
154
+ }, {})
155
+
156
+ name = 'unique_' + name
157
+
158
+ await this.#connection.index(fields, {
159
+ name,
160
+ unique: true,
161
+ sparse
162
+ })
163
+
164
+ return name
165
+ }
166
+
167
+ async removeObsolete (desired) {
168
+ const current = await this.#connection.indexes()
169
+ const obsolete = current.filter((name) => !desired.includes(name))
170
+
171
+ if (obsolete.length > 0) {
172
+ console.info(`Remove obsolete indexes: [${obsolete.join(', ')}]`)
173
+
174
+ await this.#connection.dropIndexes(obsolete)
175
+ }
176
+ }
177
+
178
+ checkFields (fields) {
179
+ const optional = []
180
+
181
+ for (const field of fields) {
182
+ if (!(field in this.#entity.schema.properties)) {
183
+ throw new Error(`Index field '${field}' is not defined.`)
184
+ }
185
+
186
+ if (!this.#entity.schema.required?.includes(field)) {
187
+ optional.push(field)
188
+ }
189
+ }
190
+
191
+ if (optional.length > 0) {
192
+ console.info(`Index fields [${optional.join(', ')}] are optional, creating sparse index.`)
193
+
194
+ return true
195
+ } else {
196
+ return false
197
+ }
198
+ }
199
+
80
200
  }
81
201
 
202
+ const INDEX_TYPES = {
203
+ 'asc': 1,
204
+ 'desc': -1,
205
+ 'hash': 'hashed'
206
+ }
207
+
208
+ const ERR_DUPLICATE_KEY = 11000
209
+
82
210
  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 = { 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
+
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
  }
@@ -1,12 +1,17 @@
1
1
  'use strict'
2
2
 
3
- const { to, from } = require('../src/record')
4
- const { random } = require('@toa.io/generic')
3
+ const {
4
+ to,
5
+ from
6
+ } = require('../src/record')
5
7
 
6
8
  describe('to', () => {
7
9
  it('should rename id to _id', () => {
8
10
  /** @type {toa.core.storages.Record} */
9
- const entity = { id: '1', _version: 0 }
11
+ const entity = {
12
+ id: '1',
13
+ _version: 0
14
+ }
10
15
  const record = to(entity)
11
16
 
12
17
  expect(record).toMatchObject({ _id: '1' })
@@ -14,37 +19,47 @@ describe('to', () => {
14
19
 
15
20
  it('should not modify argument', () => {
16
21
  /** @type {toa.core.storages.Record} */
17
- const entity = { id: '1', _version: 0 }
22
+ const entity = {
23
+ id: '1',
24
+ _version: 0
25
+ }
18
26
 
19
27
  to(entity)
20
28
 
21
- expect(entity).toStrictEqual({ id: '1', _version: 0 })
22
- })
23
-
24
- it('should increment _version', () => {
25
- /** @type {toa.core.storages.Record} */
26
- const entity = { id: '1', _version: random() }
27
- const record = to(entity)
28
-
29
- expect(record).toMatchObject({ _version: entity._version + 1 })
29
+ expect(entity).toStrictEqual({
30
+ id: '1',
31
+ _version: 0
32
+ })
30
33
  })
31
34
  })
32
35
 
33
36
  describe('from', () => {
34
37
  it('should rename _id to id', () => {
35
38
  /** @type {toa.mongodb.Record} */
36
- const record = { _id: '1', _version: 0 }
39
+ const record = {
40
+ _id: '1',
41
+ _version: 0
42
+ }
37
43
  const entity = from(record)
38
44
 
39
- expect(entity).toStrictEqual({ id: '1', _version: 0 })
45
+ expect(entity).toStrictEqual({
46
+ id: '1',
47
+ _version: 0
48
+ })
40
49
  })
41
50
 
42
51
  it('should not modify argument', () => {
43
52
  /** @type {toa.mongodb.Record} */
44
- const record = { _id: '1', _version: 0 }
53
+ const record = {
54
+ _id: '1',
55
+ _version: 0
56
+ }
45
57
 
46
58
  from(record)
47
59
 
48
- expect(record).toStrictEqual({ _id: '1', _version: 0 })
60
+ expect(record).toStrictEqual({
61
+ _id: '1',
62
+ _version: 0
63
+ })
49
64
  })
50
65
  })
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
@@ -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
- })