Skip to content

Commit 67d4edf

Browse files
authored
feat(lib): implement executeOperationV2
Fixes NODE-1896
1 parent d061d2c commit 67d4edf

File tree

4 files changed

+242
-2
lines changed

4 files changed

+242
-2
lines changed

lib/collection.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ const indexes = require('./operations/collection_ops').indexes;
4646
const indexExists = require('./operations/collection_ops').indexExists;
4747
const indexInformation = require('./operations/collection_ops').indexInformation;
4848
const insertMany = require('./operations/collection_ops').insertMany;
49-
const insertOne = require('./operations/collection_ops').insertOne;
5049
const isCapped = require('./operations/collection_ops').isCapped;
5150
const mapReduce = require('./operations/collection_ops').mapReduce;
5251
const optionsOp = require('./operations/collection_ops').optionsOp;
@@ -61,6 +60,9 @@ const updateDocuments = require('./operations/collection_ops').updateDocuments;
6160
const updateMany = require('./operations/collection_ops').updateMany;
6261
const updateOne = require('./operations/collection_ops').updateOne;
6362

63+
const InsertOneOperation = require('./operations/insert_one');
64+
const executeOperationV2 = require('./operations/execute_operation_v2');
65+
6466
/**
6567
* @fileOverview The **Collection** class is an internal class that embodies a MongoDB collection
6668
* allowing for insert/update/remove/find and other command operation on that MongoDB collection.
@@ -460,7 +462,9 @@ Collection.prototype.insertOne = function(doc, options, callback) {
460462
options.ignoreUndefined = this.s.options.ignoreUndefined;
461463
}
462464

463-
return executeOperation(this.s.topology, insertOne, [this, doc, options, callback]);
465+
const insertOneOperation = new InsertOneOperation(this, doc, options);
466+
467+
return executeOperationV2(this.s.topology, insertOneOperation, callback);
464468
};
465469

466470
/**
+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
'use strict';
2+
3+
const MongoError = require('mongodb-core').MongoError;
4+
const Aspect = require('./operation').Aspect;
5+
const OperationBase = require('./operation').OperationBase;
6+
7+
/**
8+
* Executes the given operation with provided arguments.
9+
*
10+
* This method reduces large amounts of duplication in the entire codebase by providing
11+
* a single point for determining whether callbacks or promises should be used. Additionally
12+
* it allows for a single point of entry to provide features such as implicit sessions, which
13+
* are required by the Driver Sessions specification in the event that a ClientSession is
14+
* not provided
15+
*
16+
* @param {object} topology The topology to execute this operation on
17+
* @param {Operation} operation The operation to execute
18+
* @param {function} callback The command result callback
19+
*/
20+
function executeOperationV2(topology, operation, callback) {
21+
if (topology == null) {
22+
throw new TypeError('This method requires a valid topology instance');
23+
}
24+
25+
if (!(operation instanceof OperationBase)) {
26+
throw new TypeError('This method requires a valid operation instance');
27+
}
28+
29+
const Promise = topology.s.promiseLibrary;
30+
31+
// The driver sessions spec mandates that we implicitly create sessions for operations
32+
// that are not explicitly provided with a session.
33+
let session, owner;
34+
if (!operation.hasAspect(Aspect.SKIP_SESSION) && topology.hasSessionSupport()) {
35+
if (operation.session == null) {
36+
owner = Symbol();
37+
session = topology.startSession({ owner });
38+
operation.session = session;
39+
} else if (operation.session.hasEnded) {
40+
throw new MongoError('Use of expired sessions is not permitted');
41+
}
42+
}
43+
44+
const makeExecuteCallback = (resolve, reject) =>
45+
function executeCallback(err, result) {
46+
if (session && session.owner === owner) {
47+
session.endSession(() => {
48+
if (operation.session === session) {
49+
operation.clearSession();
50+
}
51+
if (err) return reject(err);
52+
resolve(result);
53+
});
54+
} else {
55+
if (err) return reject(err);
56+
resolve(result);
57+
}
58+
};
59+
60+
// Execute using callback
61+
if (typeof callback === 'function') {
62+
const handler = makeExecuteCallback(
63+
result => callback(null, result),
64+
err => callback(err, null)
65+
);
66+
67+
try {
68+
return operation.execute(handler);
69+
} catch (e) {
70+
handler(e);
71+
throw e;
72+
}
73+
}
74+
75+
return new Promise(function(resolve, reject) {
76+
const handler = makeExecuteCallback(resolve, reject);
77+
78+
try {
79+
return operation.execute(handler);
80+
} catch (e) {
81+
handler(e);
82+
}
83+
});
84+
}
85+
86+
module.exports = executeOperationV2;

lib/operations/insert_one.js

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use strict';
2+
3+
const applyRetryableWrites = require('../utils').applyRetryableWrites;
4+
const applyWriteConcern = require('../utils').applyWriteConcern;
5+
const handleCallback = require('../utils').handleCallback;
6+
const MongoError = require('mongodb-core').MongoError;
7+
const OperationBase = require('./operation').OperationBase;
8+
const toError = require('../utils').toError;
9+
10+
class InsertOneOperation extends OperationBase {
11+
constructor(collection, doc, options) {
12+
super(options);
13+
14+
this.collection = collection;
15+
this.doc = doc;
16+
}
17+
18+
execute(callback) {
19+
const coll = this.collection;
20+
const doc = this.doc;
21+
const options = this.options;
22+
23+
if (Array.isArray(doc)) {
24+
return callback(
25+
MongoError.create({ message: 'doc parameter must be an object', driver: true })
26+
);
27+
}
28+
29+
insertDocuments(coll, [doc], options, (err, r) => {
30+
if (callback == null) return;
31+
if (err && callback) return callback(err);
32+
// Workaround for pre 2.6 servers
33+
if (r == null) return callback(null, { result: { ok: 1 } });
34+
// Add values to top level to ensure crud spec compatibility
35+
r.insertedCount = r.result.n;
36+
r.insertedId = doc._id;
37+
if (callback) callback(null, r);
38+
});
39+
}
40+
}
41+
42+
function insertDocuments(coll, docs, options, callback) {
43+
if (typeof options === 'function') (callback = options), (options = {});
44+
options = options || {};
45+
// Ensure we are operating on an array op docs
46+
docs = Array.isArray(docs) ? docs : [docs];
47+
48+
// Final options for retryable writes and write concern
49+
let finalOptions = Object.assign({}, options);
50+
finalOptions = applyRetryableWrites(finalOptions, coll.s.db);
51+
finalOptions = applyWriteConcern(finalOptions, { db: coll.s.db, collection: coll }, options);
52+
53+
// If keep going set unordered
54+
if (finalOptions.keepGoing === true) finalOptions.ordered = false;
55+
finalOptions.serializeFunctions = options.serializeFunctions || coll.s.serializeFunctions;
56+
57+
docs = prepareDocs(coll, docs, options);
58+
59+
// File inserts
60+
coll.s.topology.insert(coll.s.namespace, docs, finalOptions, (err, result) => {
61+
if (callback == null) return;
62+
if (err) return handleCallback(callback, err);
63+
if (result == null) return handleCallback(callback, null, null);
64+
if (result.result.code) return handleCallback(callback, toError(result.result));
65+
if (result.result.writeErrors)
66+
return handleCallback(callback, toError(result.result.writeErrors[0]));
67+
// Add docs to the list
68+
result.ops = docs;
69+
// Return the results
70+
handleCallback(callback, null, result);
71+
});
72+
}
73+
74+
function prepareDocs(coll, docs, options) {
75+
const forceServerObjectId =
76+
typeof options.forceServerObjectId === 'boolean'
77+
? options.forceServerObjectId
78+
: coll.s.db.options.forceServerObjectId;
79+
80+
// no need to modify the docs if server sets the ObjectId
81+
if (forceServerObjectId === true) {
82+
return docs;
83+
}
84+
85+
return docs.map(doc => {
86+
if (forceServerObjectId !== true && doc._id == null) {
87+
doc._id = coll.s.pkFactory.createPk();
88+
}
89+
90+
return doc;
91+
});
92+
}
93+
94+
module.exports = InsertOneOperation;

lib/operations/operation.js

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
3+
const Aspect = {
4+
SKIP_SESSION: Symbol('SKIP_SESSION')
5+
};
6+
7+
/**
8+
* This class acts as a parent class for any operation and is responsible for setting this.options,
9+
* as well as setting and getting a session.
10+
* Additionally, this class implements `hasAspect`, which determines whether an operation has
11+
* a specific aspect, including `SKIP_SESSION` and other aspects to encode retryability
12+
* and other functionality.
13+
*/
14+
class OperationBase {
15+
constructor(options) {
16+
this.options = options || {};
17+
}
18+
19+
hasAspect(aspect) {
20+
if (this.constructor.aspects == null) {
21+
return false;
22+
}
23+
return this.constructor.aspects.has(aspect);
24+
}
25+
26+
set session(session) {
27+
Object.assign(this.options, { session });
28+
}
29+
30+
get session() {
31+
return this.options.session;
32+
}
33+
34+
clearSession() {
35+
delete this.options.session;
36+
}
37+
38+
execute() {
39+
throw new TypeError('`execute` must be implemented for OperationBase subclasses');
40+
}
41+
}
42+
43+
function defineAspects(operation, aspects) {
44+
aspects = new Set(aspects);
45+
Object.defineProperty(operation, 'aspects', {
46+
value: aspects,
47+
writable: false
48+
});
49+
return aspects;
50+
}
51+
52+
module.exports = {
53+
Aspect,
54+
defineAspects,
55+
OperationBase
56+
};

0 commit comments

Comments
 (0)