cosa 6.1.0 → 6.2.0

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2021 Losant IoT, Inc.
3
+ Copyright (c) 2022 Losant IoT, Inc.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/lib/db.js CHANGED
@@ -3,7 +3,7 @@ const { ObjectId } = require('bson');
3
3
  const { EventEmitter } = require('events');
4
4
  const { MongoClient } = require('mongodb');
5
5
  const {
6
- curry, split, last, pipe, isEmpty, clamp, isPlainObject
6
+ curry, split, last, pipe, clamp, isPlainObject, isNilOrEmpty
7
7
  } = require('omnibelt');
8
8
  const Immutable = require('./immutable');
9
9
  const RESULT_FIELDS = [ 'insertedId', 'insertedCount', 'insertedIds', 'matchedCount', 'modifiedCount', 'upsertedCount', 'upsertedId', 'deletedCount', 'upsertedIds' ];
@@ -182,6 +182,7 @@ class Database extends EventEmitter {
182
182
  * @param {number} [options.batchSize] - number of items per batch (default in mongo driver is 1000)
183
183
  * @param {boolean} [options.noCursorTimeout] - boolan, if the cursor can time out after being idle, mongo driver default is false
184
184
  * @param {number} [options.maxTimeMS] - maximum amount of time (in ms) this cursor is allowed to live
185
+ * @param {object} [options.session] - Mongo session or Cosa Mongo session wrapper
185
186
  * @returns {Cursor} returns Cursor object
186
187
  * @see {@link https://mongodb.github.io/node-mongodb-native/4.0/classes/findcursor.html}
187
188
  * @see {@link https://mongodb.github.io/node-mongodb-native/4.0/classes/collection.html#find}
@@ -195,8 +196,9 @@ class Database extends EventEmitter {
195
196
  async find(collectionName, query, options = {}) {
196
197
  query = deserialize(query);
197
198
  const collection = await this.collection(collectionName);
199
+ const session = options.session?.mongoSession || options.session;
198
200
  if (options.count) {
199
- if (!query || isEmpty(query)) {
201
+ if (isNilOrEmpty(query) && !session) {
200
202
  const countOptions = {
201
203
  readPreference: options.readPreference,
202
204
  maxTimeMS: options.maxTimeMS
@@ -209,7 +211,8 @@ class Database extends EventEmitter {
209
211
  limit: options.limit,
210
212
  skip: options.skip,
211
213
  readPreference: options.readPreference,
212
- maxTimeMS: options.maxTimeMS
214
+ maxTimeMS: options.maxTimeMS,
215
+ session
213
216
  };
214
217
  debug(`db.${collection.collectionName}.countDocuments`, query, countOptions);
215
218
  return collection.countDocuments(query, countOptions);
@@ -221,10 +224,11 @@ class Database extends EventEmitter {
221
224
  sort: options.sort,
222
225
  readPreference: options.readPreference,
223
226
  noCursorTimeout: options.noCursorTimeout,
224
- maxTimeMS: options.maxTimeMS
227
+ maxTimeMS: options.maxTimeMS,
228
+ session
225
229
  };
226
230
  debug(`db.${collection.collectionName}.findOne`, query, findOptions);
227
- return collection.findOne(query, options);
231
+ return collection.findOne(query, findOptions);
228
232
  } else {
229
233
  const findOptions = {
230
234
  projection: options.projection || options.fields,
@@ -234,10 +238,11 @@ class Database extends EventEmitter {
234
238
  readPreference: options.readPreference,
235
239
  batchSize: options.batchSize,
236
240
  noCursorTimeout: options.noCursorTimeout,
237
- maxTimeMS: options.maxTimeMS
241
+ maxTimeMS: options.maxTimeMS,
242
+ session
238
243
  };
239
244
  debug(`db.${collection.collectionName}.find`, query, findOptions);
240
- return collection.find(query, options);
245
+ return collection.find(query, findOptions);
241
246
  }
242
247
  }
243
248
 
@@ -247,15 +252,19 @@ class Database extends EventEmitter {
247
252
  * @param {(object|Array)} docs - Documents objects to insert.
248
253
  * @param {object} options - options on insert
249
254
  * @param {object} [options.writeConcern] - the write concern
255
+ * @param {object} [options.session] - Mongo session or Cosa Mongo session wrapper
250
256
  * @returns {Promise} resolves with an object with results, and ops as keys
251
257
  * @see {@link https://mongodb.github.io/node-mongodb-native/4.0/classes/collection.html#insertmany}
252
258
  * @see {@link https://mongodb.github.io/node-mongodb-native/4.0/classes/collection.html#insertOne}
253
259
  * @see {@link https://mongodb.github.io/node-mongodb-native/4.0/interfaces/insertoneoptions.html#writeconcern}
254
260
  */
255
- async insert(collectionName, docs, { writeConcern } = {}) {
261
+ async insert(collectionName, docs, { writeConcern, session } = {}) {
256
262
  docs = deserialize(docs);
257
263
  const collection = await this.collection(collectionName);
258
- const results = await insertOneOrMany(collection, Array.isArray(docs), docs, { writeConcern });
264
+ const results = await insertOneOrMany(collection, Array.isArray(docs), docs, {
265
+ writeConcern,
266
+ session: session?.mongoSession || session
267
+ });
259
268
 
260
269
  if (Array.isArray(docs)) {
261
270
  docs.forEach((doc, i) => {
@@ -278,6 +287,7 @@ class Database extends EventEmitter {
278
287
  * @param {boolean} [options.multiple=false] - Should multiple documents be updated.
279
288
  * @param {boolean} [options.upsert=false] - Should documents be inserted if they don't already exist.
280
289
  * @param {object} [options.writeConcern] - the write concern options
290
+ * @param {object} [options.session] - Mongo session or Cosa Mongo session wrapper
281
291
  * @returns {Promise} resolves with an object with results, and ops as keys
282
292
  * @see {@link https://mongodb.github.io/node-mongodb-native/4.0/classes/collection.html#updateMany}
283
293
  * @see {@link https://mongodb.github.io/node-mongodb-native/4.0/classes/collection.html#updateOne}
@@ -289,7 +299,8 @@ class Database extends EventEmitter {
289
299
  const collection = await this.collection(collectionName);
290
300
  return updateOneOrMany(collection, options.multiple, query, update, {
291
301
  upsert: options.upsert,
292
- writeConcern: options.writeConcern
302
+ writeConcern: options.writeConcern,
303
+ session: options.session?.mongoSession || options.session
293
304
  });
294
305
  }
295
306
 
@@ -300,6 +311,7 @@ class Database extends EventEmitter {
300
311
  * @param {object} [options] - Optional settings see mongo documentation
301
312
  * @param {boolean} [options.multiple=false] - Should multiple documents be removed.
302
313
  * @param {object} [options.writeConcern] - the write concern options
314
+ * @param {object} [options.session] - Mongo session or Cosa Mongo session wrapper
303
315
  * @returns {Promise} resolves with an object with results, and ops as keys
304
316
  * @see {@link https://mongodb.github.io/node-mongodb-native/4.0/classes/collection.html#deleteMany}
305
317
  * @see {@link https://mongodb.github.io/node-mongodb-native/4.0/classes/collection.html#deleteOne}
@@ -308,7 +320,10 @@ class Database extends EventEmitter {
308
320
  async remove(collectionName, query, options = {}) {
309
321
  query = deserialize(query);
310
322
  const collection = await this.collection(collectionName);
311
- return deleteOneOrMany(collection, options.multiple, query, { writeConcern: options.writeConcern });
323
+ return deleteOneOrMany(collection, options.multiple, query, {
324
+ writeConcern: options.writeConcern,
325
+ session: options.session?.mongoSession || options.session
326
+ });
312
327
  }
313
328
 
314
329
  /**
@@ -319,6 +334,7 @@ class Database extends EventEmitter {
319
334
  * @param {object} [options.readPreference] - the read preference for the query with one of the read constants
320
335
  * @param {number} [options.batchSize] - number of items per batch (default in mongo driver is 1000)
321
336
  * @param {number} [options.maxTimeMS] - maximum amount of time (in ms) this cursor is allowed to live
337
+ * @param {object} [options.session] - Mongo session or Cosa Mongo session wrapper
322
338
  * @returns {Promise} resolves with the result of the aggregation from mongo
323
339
  * @see {@link https://mongodb.github.io/node-mongodb-native/4.0/classes/collection.html#aggregate}
324
340
  * @see {@link https://mongodb.github.io/node-mongodb-native/4.0/interfaces/aggregateoptions.html#readpreference}
@@ -330,7 +346,8 @@ class Database extends EventEmitter {
330
346
  return collection.aggregate(pipeline, {
331
347
  readPreference: options.readPreference,
332
348
  batchSize: options.batchSize,
333
- maxTimeMS: options.maxTimeMS
349
+ maxTimeMS: options.maxTimeMS,
350
+ session: options.session?.mongoSession || options.session
334
351
  });
335
352
  }
336
353
 
@@ -342,6 +359,7 @@ class Database extends EventEmitter {
342
359
  * @param {object} [options] - Optional settings see mongo documentation
343
360
  * @param {string} [options.readPreference] - the read preference for the query
344
361
  * @param {number} [options.maxTimeMS] - maximum amount of time (in ms) this cursor is allowed to live
362
+ * @param {object} [options.session] - Mongo session or Cosa Mongo session wrapper
345
363
  * @returns {Promise} resolves with the result of the distinct query from mongo
346
364
  * @see {@link https://mongodb.github.io/node-mongodb-native/4.0/classes/collection.html#distinct}
347
365
  * @see {@link https://mongodb.github.io/node-mongodb-native/4.0/interfaces/commandoperationoptions.html#readpreference}
@@ -352,7 +370,8 @@ class Database extends EventEmitter {
352
370
  const collection = await this.collection(collectionName);
353
371
  return collection.distinct(key, query, {
354
372
  readPreference: options.readPreference,
355
- maxTimeMS: options.maxTimeMS
373
+ maxTimeMS: options.maxTimeMS,
374
+ session: options.session?.mongoSession || options.session
356
375
  });
357
376
  }
358
377
 
@@ -363,16 +382,20 @@ class Database extends EventEmitter {
363
382
  * @param {object} replace - doc to save on the collection
364
383
  * @param {object} [options] - Optional settings see mongo documentation
365
384
  * @param {object} [options.writeConcern] - the write concern options
385
+ * @param {object} [options.session] - Mongo session or Cosa Mongo session wrapper
366
386
  * @returns {Promise} resolves with the result of the distinct query from mongo
367
387
  * @see {@link http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#replace}
368
388
  * @see {@link https://mongodb.github.io/node-mongodb-native/4.0/interfaces/replaceoptions.html#writeconcern}
369
389
  */
370
- async replace(collectionName, query, replace, { writeConcern } = {}) {
390
+ async replace(collectionName, query, replace, { writeConcern, session } = {}) {
371
391
  const collection = await this.collection(collectionName);
372
392
  query = deserialize(query);
373
393
  replace = deserialize(replace);
374
394
  debug(`db.${collection.collectionName}.replaceOne`, query, replace);
375
- const r = await collection.replaceOne(query, replace, { writeConcern });
395
+ const r = await collection.replaceOne(query, replace, {
396
+ writeConcern,
397
+ session: session?.mongoSession || session
398
+ });
376
399
  r.ops = replace;
377
400
  return normalizeResult(r);
378
401
  }
package/lib/model.js CHANGED
@@ -116,7 +116,7 @@ const _create = (data, definition) => {
116
116
  await context.beforeSave.apply(obj, [options]);
117
117
  }
118
118
  obj = await context._validate(obj, options);
119
- const original = obj.__original;
119
+ const original = obj.__original || null;
120
120
  obj = removeMeta(obj);
121
121
  const newEtag = etag(JSON.stringify(obj));
122
122
  const collection = definition.collection;
@@ -159,10 +159,27 @@ const _create = (data, definition) => {
159
159
  obj = Array.isArray(result.ops) ? result.ops[0] : result.ops;
160
160
  newOrUpdatedModel = _create(obj, definition);
161
161
  }
162
- if ('function' === typeof newOrUpdatedModel.afterSave) {
163
- // should not be awaited
164
- const afterSave = newOrUpdatedModel.afterSave(original || null, options);
165
- if (options.waitAfterSave) { await afterSave; }
162
+ if (options.session) {
163
+ if ('function' === typeof newOrUpdatedModel.afterSave) {
164
+ await newOrUpdatedModel.afterSave(original, options);
165
+ }
166
+ if ('function' === typeof newOrUpdatedModel.afterSaveCommit) {
167
+ options.session.afterCommits.push(() => newOrUpdatedModel.afterSaveCommit(original, options));
168
+ }
169
+ if ('function' === typeof newOrUpdatedModel.afterSaveAbort) {
170
+ options.session.afterAborts.push(() => newOrUpdatedModel.afterSaveAbort(original, options));
171
+ }
172
+ } else {
173
+ let chain = Promise.resolve();
174
+ if ('function' === typeof newOrUpdatedModel.afterSave) {
175
+ chain = chain.then(() => newOrUpdatedModel.afterSave(original, options));
176
+ }
177
+ if ('function' === typeof newOrUpdatedModel.afterSaveCommit) {
178
+ chain = chain.then(() => newOrUpdatedModel.afterSaveCommit(original, options));
179
+ }
180
+ if (options.waitAfterSave) {
181
+ await chain;
182
+ }
166
183
  }
167
184
  return newOrUpdatedModel;
168
185
  };
@@ -297,10 +314,30 @@ const _create = (data, definition) => {
297
314
  throw errors.Conflict({ message: 'Document remove conflict' });
298
315
  }
299
316
  debug(`remove from ${collection} successful`);
300
- if ('function' === typeof this.afterRemove) {
301
- const afterRemove = this.afterRemove(options);
302
- if (options.waitAfterRemove) { await afterRemove; }
317
+
318
+ if (options.session) {
319
+ if ('function' === typeof this.afterRemove) {
320
+ await this.afterRemove(options);
321
+ }
322
+ if ('function' === typeof this.afterRemoveCommit) {
323
+ options.session.afterCommits.push(() => this.afterRemoveCommit(options));
324
+ }
325
+ if ('function' === typeof this.afterRemoveAbort) {
326
+ options.session.afterAborts.push(() => this.afterRemoveAbort(options));
327
+ }
328
+ } else {
329
+ let chain = Promise.resolve();
330
+ if ('function' === typeof this.afterRemove) {
331
+ chain = chain.then(() => this.afterRemove(options));
332
+ }
333
+ if ('function' === typeof this.afterRemoveCommit) {
334
+ chain = chain.then(() => this.afterRemoveCommit(options));
335
+ }
336
+ if (options.waitAfterRemove) {
337
+ await chain;
338
+ }
303
339
  }
340
+
304
341
  return result;
305
342
  };
306
343
 
package/lib/session.js ADDED
@@ -0,0 +1,58 @@
1
+ const db = require('./db');
2
+ const { forEachSerialP } = require('omnibelt');
3
+
4
+ const createSession = async () => {
5
+ const afterCommits = [];
6
+ const afterAborts = [];
7
+ if (!db._client) {
8
+ await db.init();
9
+ }
10
+ const session = await db._client.startSession();
11
+
12
+ const abortTransaction = async () => {
13
+ let error;
14
+ await session.abortTransaction();
15
+ await session.endSession();
16
+ await forEachSerialP(async (afterAborted) => {
17
+ try {
18
+ await afterAborted(error);
19
+ } catch (err) {
20
+ if (!err) {
21
+ error = err;
22
+ }
23
+ }
24
+ }, afterAborts);
25
+ if (error) { throw error; }
26
+ };
27
+
28
+ return {
29
+ get mongoSession() {
30
+ return session;
31
+ },
32
+ startTransaction: () => {
33
+ return session.startTransaction();
34
+ },
35
+ commitTransaction: async () => {
36
+ let error;
37
+ await session.commitTransaction();
38
+ await session.endSession();
39
+ await forEachSerialP(async (afterCommitFunc) => {
40
+ try {
41
+ await afterCommitFunc();
42
+ } catch (err) {
43
+ if (!error) {
44
+ error = err;
45
+ }
46
+ }
47
+ }, afterCommits);
48
+ if (error) { throw error; }
49
+ },
50
+ abortTransaction,
51
+ afterCommits,
52
+ afterAborts
53
+ };
54
+ };
55
+
56
+ module.exports = {
57
+ createSession
58
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cosa",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "description": "Cosa Models for MongoDB",
5
5
  "main": "lib/index.js",
6
6
  "engines": {
@@ -35,25 +35,25 @@
35
35
  "lib"
36
36
  ],
37
37
  "dependencies": {
38
- "bson": "~4.5.2",
38
+ "bson": "~4.6.4",
39
39
  "clone": "^2.1.2",
40
- "debug": "^4.3.2",
40
+ "debug": "^4.3.4",
41
41
  "error": "^7.0.2",
42
42
  "etag": "^1.8.1",
43
43
  "@hapi/joi": "^17.1.1",
44
- "mongodb": "~4.1.2",
44
+ "mongodb": "~4.7.0",
45
45
  "object-path": "^0.11.8",
46
46
  "omnibelt": "^2.1.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@losant/eslint-config-losant": "^1.4.3",
50
- "husky": "^7.0.2",
51
- "lint-staged": "^11.1.2",
52
- "chai": "^4.3.4",
50
+ "husky": "^8.0.1",
51
+ "lint-staged": "^13.0.2",
52
+ "chai": "^4.3.6",
53
53
  "chai-as-promised": "^7.1.1",
54
54
  "chai-datetime": "^1.8.0",
55
55
  "documentation": "^13.2.5",
56
- "mocha": "^9.1.1",
56
+ "mocha": "^10.0.0",
57
57
  "string-template": "^1.0.0"
58
58
  },
59
59
  "eslintConfig": {