You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1007 lines
33 KiB
JavaScript
1007 lines
33 KiB
JavaScript
5 years ago
|
'use strict';
|
||
|
|
||
|
const applyWriteConcern = require('../utils').applyWriteConcern;
|
||
|
const Code = require('mongodb-core').BSON.Code;
|
||
|
const resolveReadPreference = require('../utils').resolveReadPreference;
|
||
|
const crypto = require('crypto');
|
||
|
const debugOptions = require('../utils').debugOptions;
|
||
|
const handleCallback = require('../utils').handleCallback;
|
||
|
const MongoError = require('mongodb-core').MongoError;
|
||
|
const parseIndexOptions = require('../utils').parseIndexOptions;
|
||
|
const ReadPreference = require('mongodb-core').ReadPreference;
|
||
|
const toError = require('../utils').toError;
|
||
|
const CONSTANTS = require('../constants');
|
||
|
|
||
|
const count = require('./collection_ops').count;
|
||
|
const findOne = require('./collection_ops').findOne;
|
||
|
const remove = require('./collection_ops').remove;
|
||
|
const updateOne = require('./collection_ops').updateOne;
|
||
|
|
||
|
let collection;
|
||
|
function loadCollection() {
|
||
|
if (!collection) {
|
||
|
collection = require('../collection');
|
||
|
}
|
||
|
return collection;
|
||
|
}
|
||
|
let db;
|
||
|
function loadDb() {
|
||
|
if (!db) {
|
||
|
db = require('../db');
|
||
|
}
|
||
|
return db;
|
||
|
}
|
||
|
|
||
|
const debugFields = [
|
||
|
'authSource',
|
||
|
'w',
|
||
|
'wtimeout',
|
||
|
'j',
|
||
|
'native_parser',
|
||
|
'forceServerObjectId',
|
||
|
'serializeFunctions',
|
||
|
'raw',
|
||
|
'promoteLongs',
|
||
|
'promoteValues',
|
||
|
'promoteBuffers',
|
||
|
'bufferMaxEntries',
|
||
|
'numberOfRetries',
|
||
|
'retryMiliSeconds',
|
||
|
'readPreference',
|
||
|
'pkFactory',
|
||
|
'parentDb',
|
||
|
'promiseLibrary',
|
||
|
'noListener'
|
||
|
];
|
||
|
|
||
|
// Filter out any write concern options
|
||
|
const illegalCommandFields = [
|
||
|
'w',
|
||
|
'wtimeout',
|
||
|
'j',
|
||
|
'fsync',
|
||
|
'autoIndexId',
|
||
|
'strict',
|
||
|
'serializeFunctions',
|
||
|
'pkFactory',
|
||
|
'raw',
|
||
|
'readPreference',
|
||
|
'session'
|
||
|
];
|
||
|
|
||
|
/**
|
||
|
* Add a user to the database.
|
||
|
* @method
|
||
|
* @param {Db} db The Db instance on which to add a user.
|
||
|
* @param {string} username The username.
|
||
|
* @param {string} password The password.
|
||
|
* @param {object} [options] Optional settings. See Db.prototype.addUser for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The command result callback
|
||
|
*/
|
||
|
function addUser(db, username, password, options, callback) {
|
||
|
let Db = loadDb();
|
||
|
|
||
|
// Did the user destroy the topology
|
||
|
if (db.serverConfig && db.serverConfig.isDestroyed())
|
||
|
return callback(new MongoError('topology was destroyed'));
|
||
|
// Attempt to execute auth command
|
||
|
executeAuthCreateUserCommand(db, username, password, options, (err, r) => {
|
||
|
// We need to perform the backward compatible insert operation
|
||
|
if (err && err.code === -5000) {
|
||
|
const finalOptions = applyWriteConcern(Object.assign({}, options), { db }, options);
|
||
|
|
||
|
// Use node md5 generator
|
||
|
const md5 = crypto.createHash('md5');
|
||
|
// Generate keys used for authentication
|
||
|
md5.update(username + ':mongo:' + password);
|
||
|
const userPassword = md5.digest('hex');
|
||
|
|
||
|
// If we have another db set
|
||
|
const dbToUse = options.dbName ? new Db(options.dbName, db.s.topology, db.s.options) : db;
|
||
|
|
||
|
// Fetch a user collection
|
||
|
const collection = dbToUse.collection(CONSTANTS.SYSTEM_USER_COLLECTION);
|
||
|
|
||
|
// Check if we are inserting the first user
|
||
|
count(collection, {}, finalOptions, (err, count) => {
|
||
|
// We got an error (f.ex not authorized)
|
||
|
if (err != null) return handleCallback(callback, err, null);
|
||
|
// Check if the user exists and update i
|
||
|
const findOptions = Object.assign({ projection: { dbName: 1 } }, finalOptions);
|
||
|
collection.find({ user: username }, findOptions).toArray(err => {
|
||
|
// We got an error (f.ex not authorized)
|
||
|
if (err != null) return handleCallback(callback, err, null);
|
||
|
// Add command keys
|
||
|
finalOptions.upsert = true;
|
||
|
|
||
|
// We have a user, let's update the password or upsert if not
|
||
|
updateOne(
|
||
|
collection,
|
||
|
{ user: username },
|
||
|
{ $set: { user: username, pwd: userPassword } },
|
||
|
finalOptions,
|
||
|
err => {
|
||
|
if (count === 0 && err)
|
||
|
return handleCallback(callback, null, [{ user: username, pwd: userPassword }]);
|
||
|
if (err) return handleCallback(callback, err, null);
|
||
|
handleCallback(callback, null, [{ user: username, pwd: userPassword }]);
|
||
|
}
|
||
|
);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (err) return handleCallback(callback, err);
|
||
|
handleCallback(callback, err, r);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Fetch all collections for the current db.
|
||
|
*
|
||
|
* @method
|
||
|
* @param {Db} db The Db instance on which to fetch collections.
|
||
|
* @param {object} [options] Optional settings. See Db.prototype.collections for a list of options.
|
||
|
* @param {Db~collectionsResultCallback} [callback] The results callback
|
||
|
*/
|
||
|
function collections(db, options, callback) {
|
||
|
let Collection = loadCollection();
|
||
|
|
||
|
options = Object.assign({}, options, { nameOnly: true });
|
||
|
// Let's get the collection names
|
||
|
db.listCollections({}, options).toArray((err, documents) => {
|
||
|
if (err != null) return handleCallback(callback, err, null);
|
||
|
// Filter collections removing any illegal ones
|
||
|
documents = documents.filter(doc => {
|
||
|
return doc.name.indexOf('$') === -1;
|
||
|
});
|
||
|
|
||
|
// Return the collection objects
|
||
|
handleCallback(
|
||
|
callback,
|
||
|
null,
|
||
|
documents.map(d => {
|
||
|
return new Collection(
|
||
|
db,
|
||
|
db.s.topology,
|
||
|
db.s.databaseName,
|
||
|
d.name,
|
||
|
db.s.pkFactory,
|
||
|
db.s.options
|
||
|
);
|
||
|
})
|
||
|
);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a new collection on a server with the specified options. Use this to create capped collections.
|
||
|
* More information about command options available at https://docs.mongodb.com/manual/reference/command/create/
|
||
|
*
|
||
|
* @method
|
||
|
* @param {Db} db The Db instance on which to create the collection.
|
||
|
* @param {string} name The collection name to create.
|
||
|
* @param {object} [options] Optional settings. See Db.prototype.createCollection for a list of options.
|
||
|
* @param {Db~collectionResultCallback} [callback] The results callback
|
||
|
*/
|
||
|
function createCollection(db, name, options, callback) {
|
||
|
let Collection = loadCollection();
|
||
|
|
||
|
// Get the write concern options
|
||
|
const finalOptions = applyWriteConcern(Object.assign({}, options), { db }, options);
|
||
|
|
||
|
// Did the user destroy the topology
|
||
|
if (db.serverConfig && db.serverConfig.isDestroyed()) {
|
||
|
return callback(new MongoError('topology was destroyed'));
|
||
|
}
|
||
|
|
||
|
const listCollectionOptions = Object.assign({}, finalOptions, { nameOnly: true });
|
||
|
|
||
|
// Check if we have the name
|
||
|
db
|
||
|
.listCollections({ name }, listCollectionOptions)
|
||
|
.setReadPreference(ReadPreference.PRIMARY)
|
||
|
.toArray((err, collections) => {
|
||
|
if (err != null) return handleCallback(callback, err, null);
|
||
|
if (collections.length > 0 && finalOptions.strict) {
|
||
|
return handleCallback(
|
||
|
callback,
|
||
|
MongoError.create({
|
||
|
message: `Collection ${name} already exists. Currently in strict mode.`,
|
||
|
driver: true
|
||
|
}),
|
||
|
null
|
||
|
);
|
||
|
} else if (collections.length > 0) {
|
||
|
try {
|
||
|
return handleCallback(
|
||
|
callback,
|
||
|
null,
|
||
|
new Collection(db, db.s.topology, db.s.databaseName, name, db.s.pkFactory, options)
|
||
|
);
|
||
|
} catch (err) {
|
||
|
return handleCallback(callback, err);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Create collection command
|
||
|
const cmd = { create: name };
|
||
|
|
||
|
// Decorate command with writeConcern if supported
|
||
|
applyWriteConcern(cmd, { db }, options);
|
||
|
|
||
|
// Add all optional parameters
|
||
|
for (let n in options) {
|
||
|
if (
|
||
|
options[n] != null &&
|
||
|
typeof options[n] !== 'function' &&
|
||
|
illegalCommandFields.indexOf(n) === -1
|
||
|
) {
|
||
|
cmd[n] = options[n];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Force a primary read Preference
|
||
|
finalOptions.readPreference = ReadPreference.PRIMARY;
|
||
|
// Execute command
|
||
|
executeCommand(db, cmd, finalOptions, err => {
|
||
|
if (err) return handleCallback(callback, err);
|
||
|
|
||
|
try {
|
||
|
return handleCallback(
|
||
|
callback,
|
||
|
null,
|
||
|
new Collection(db, db.s.topology, db.s.databaseName, name, db.s.pkFactory, options)
|
||
|
);
|
||
|
} catch (err) {
|
||
|
return handleCallback(callback, err);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates an index on the db and collection.
|
||
|
* @method
|
||
|
* @param {Db} db The Db instance on which to create an index.
|
||
|
* @param {string} name Name of the collection to create the index on.
|
||
|
* @param {(string|object)} fieldOrSpec Defines the index.
|
||
|
* @param {object} [options] Optional settings. See Db.prototype.createIndex for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The command result callback
|
||
|
*/
|
||
|
function createIndex(db, name, fieldOrSpec, options, callback) {
|
||
|
// Get the write concern options
|
||
|
let finalOptions = Object.assign({}, { readPreference: ReadPreference.PRIMARY }, options);
|
||
|
finalOptions = applyWriteConcern(finalOptions, { db }, options);
|
||
|
|
||
|
// Ensure we have a callback
|
||
|
if (finalOptions.writeConcern && typeof callback !== 'function') {
|
||
|
throw MongoError.create({
|
||
|
message: 'Cannot use a writeConcern without a provided callback',
|
||
|
driver: true
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Did the user destroy the topology
|
||
|
if (db.serverConfig && db.serverConfig.isDestroyed())
|
||
|
return callback(new MongoError('topology was destroyed'));
|
||
|
|
||
|
// Attempt to run using createIndexes command
|
||
|
createIndexUsingCreateIndexes(db, name, fieldOrSpec, finalOptions, (err, result) => {
|
||
|
if (err == null) return handleCallback(callback, err, result);
|
||
|
|
||
|
/**
|
||
|
* The following errors mean that the server recognized `createIndex` as a command so we don't need to fallback to an insert:
|
||
|
* 67 = 'CannotCreateIndex' (malformed index options)
|
||
|
* 85 = 'IndexOptionsConflict' (index already exists with different options)
|
||
|
* 86 = 'IndexKeySpecsConflict' (index already exists with the same name)
|
||
|
* 11000 = 'DuplicateKey' (couldn't build unique index because of dupes)
|
||
|
* 11600 = 'InterruptedAtShutdown' (interrupted at shutdown)
|
||
|
* 197 = 'InvalidIndexSpecificationOption' (`_id` with `background: true`)
|
||
|
*/
|
||
|
if (
|
||
|
err.code === 67 ||
|
||
|
err.code === 11000 ||
|
||
|
err.code === 85 ||
|
||
|
err.code === 86 ||
|
||
|
err.code === 11600 ||
|
||
|
err.code === 197
|
||
|
) {
|
||
|
return handleCallback(callback, err, result);
|
||
|
}
|
||
|
|
||
|
// Create command
|
||
|
const doc = createCreateIndexCommand(db, name, fieldOrSpec, options);
|
||
|
// Set no key checking
|
||
|
finalOptions.checkKeys = false;
|
||
|
// Insert document
|
||
|
db.s.topology.insert(
|
||
|
`${db.s.databaseName}.${CONSTANTS.SYSTEM_INDEX_COLLECTION}`,
|
||
|
doc,
|
||
|
finalOptions,
|
||
|
(err, result) => {
|
||
|
if (callback == null) return;
|
||
|
if (err) return handleCallback(callback, err);
|
||
|
if (result == null) return handleCallback(callback, null, null);
|
||
|
if (result.result.writeErrors)
|
||
|
return handleCallback(callback, MongoError.create(result.result.writeErrors[0]), null);
|
||
|
handleCallback(callback, null, doc.name);
|
||
|
}
|
||
|
);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Add listeners to topology
|
||
|
function createListener(db, e, object) {
|
||
|
function listener(err) {
|
||
|
if (object.listeners(e).length > 0) {
|
||
|
object.emit(e, err, db);
|
||
|
|
||
|
// Emit on all associated db's if available
|
||
|
for (let i = 0; i < db.s.children.length; i++) {
|
||
|
db.s.children[i].emit(e, err, db.s.children[i]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return listener;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Drop a collection from the database, removing it permanently. New accesses will create a new collection.
|
||
|
*
|
||
|
* @method
|
||
|
* @param {Db} db The Db instance on which to drop the collection.
|
||
|
* @param {string} name Name of collection to drop
|
||
|
* @param {Object} [options] Optional settings. See Db.prototype.dropCollection for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The results callback
|
||
|
*/
|
||
|
function dropCollection(db, name, options, callback) {
|
||
|
executeCommand(db, name, options, (err, result) => {
|
||
|
// Did the user destroy the topology
|
||
|
if (db.serverConfig && db.serverConfig.isDestroyed()) {
|
||
|
return callback(new MongoError('topology was destroyed'));
|
||
|
}
|
||
|
|
||
|
if (err) return handleCallback(callback, err);
|
||
|
if (result.ok) return handleCallback(callback, null, true);
|
||
|
handleCallback(callback, null, false);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Drop a database, removing it permanently from the server.
|
||
|
*
|
||
|
* @method
|
||
|
* @param {Db} db The Db instance to drop.
|
||
|
* @param {Object} cmd The command document.
|
||
|
* @param {Object} [options] Optional settings. See Db.prototype.dropDatabase for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The results callback
|
||
|
*/
|
||
|
function dropDatabase(db, cmd, options, callback) {
|
||
|
executeCommand(db, cmd, options, (err, result) => {
|
||
|
// Did the user destroy the topology
|
||
|
if (db.serverConfig && db.serverConfig.isDestroyed()) {
|
||
|
return callback(new MongoError('topology was destroyed'));
|
||
|
}
|
||
|
|
||
|
if (callback == null) return;
|
||
|
if (err) return handleCallback(callback, err, null);
|
||
|
handleCallback(callback, null, result.ok ? true : false);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Ensures that an index exists. If it does not, creates it.
|
||
|
*
|
||
|
* @method
|
||
|
* @param {Db} db The Db instance on which to ensure the index.
|
||
|
* @param {string} name The index name
|
||
|
* @param {(string|object)} fieldOrSpec Defines the index.
|
||
|
* @param {object} [options] Optional settings. See Db.prototype.ensureIndex for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The command result callback
|
||
|
*/
|
||
|
function ensureIndex(db, name, fieldOrSpec, options, callback) {
|
||
|
// Get the write concern options
|
||
|
const finalOptions = applyWriteConcern({}, { db }, options);
|
||
|
// Create command
|
||
|
const selector = createCreateIndexCommand(db, name, fieldOrSpec, options);
|
||
|
const index_name = selector.name;
|
||
|
|
||
|
// Did the user destroy the topology
|
||
|
if (db.serverConfig && db.serverConfig.isDestroyed())
|
||
|
return callback(new MongoError('topology was destroyed'));
|
||
|
|
||
|
// Merge primary readPreference
|
||
|
finalOptions.readPreference = ReadPreference.PRIMARY;
|
||
|
|
||
|
// Check if the index already exists
|
||
|
indexInformation(db, name, finalOptions, (err, indexInformation) => {
|
||
|
if (err != null && err.code !== 26) return handleCallback(callback, err, null);
|
||
|
// If the index does not exist, create it
|
||
|
if (indexInformation == null || !indexInformation[index_name]) {
|
||
|
createIndex(db, name, fieldOrSpec, options, callback);
|
||
|
} else {
|
||
|
if (typeof callback === 'function') return handleCallback(callback, null, index_name);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Evaluate JavaScript on the server
|
||
|
*
|
||
|
* @method
|
||
|
* @param {Db} db The Db instance.
|
||
|
* @param {Code} code JavaScript to execute on server.
|
||
|
* @param {(object|array)} parameters The parameters for the call.
|
||
|
* @param {object} [options] Optional settings. See Db.prototype.eval for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The results callback
|
||
|
* @deprecated Eval is deprecated on MongoDB 3.2 and forward
|
||
|
*/
|
||
|
function evaluate(db, code, parameters, options, callback) {
|
||
|
let finalCode = code;
|
||
|
let finalParameters = [];
|
||
|
|
||
|
// Did the user destroy the topology
|
||
|
if (db.serverConfig && db.serverConfig.isDestroyed())
|
||
|
return callback(new MongoError('topology was destroyed'));
|
||
|
|
||
|
// If not a code object translate to one
|
||
|
if (!(finalCode && finalCode._bsontype === 'Code')) finalCode = new Code(finalCode);
|
||
|
// Ensure the parameters are correct
|
||
|
if (parameters != null && !Array.isArray(parameters) && typeof parameters !== 'function') {
|
||
|
finalParameters = [parameters];
|
||
|
} else if (parameters != null && Array.isArray(parameters) && typeof parameters !== 'function') {
|
||
|
finalParameters = parameters;
|
||
|
}
|
||
|
|
||
|
// Create execution selector
|
||
|
let cmd = { $eval: finalCode, args: finalParameters };
|
||
|
// Check if the nolock parameter is passed in
|
||
|
if (options['nolock']) {
|
||
|
cmd['nolock'] = options['nolock'];
|
||
|
}
|
||
|
|
||
|
// Set primary read preference
|
||
|
options.readPreference = new ReadPreference(ReadPreference.PRIMARY);
|
||
|
|
||
|
// Execute the command
|
||
|
executeCommand(db, cmd, options, (err, result) => {
|
||
|
if (err) return handleCallback(callback, err, null);
|
||
|
if (result && result.ok === 1) return handleCallback(callback, null, result.retval);
|
||
|
if (result)
|
||
|
return handleCallback(
|
||
|
callback,
|
||
|
MongoError.create({ message: `eval failed: ${result.errmsg}`, driver: true }),
|
||
|
null
|
||
|
);
|
||
|
handleCallback(callback, err, result);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Execute a command
|
||
|
*
|
||
|
* @method
|
||
|
* @param {Db} db The Db instance on which to execute the command.
|
||
|
* @param {object} command The command hash
|
||
|
* @param {object} [options] Optional settings. See Db.prototype.command for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The command result callback
|
||
|
*/
|
||
|
function executeCommand(db, command, options, callback) {
|
||
|
// Did the user destroy the topology
|
||
|
if (db.serverConfig && db.serverConfig.isDestroyed())
|
||
|
return callback(new MongoError('topology was destroyed'));
|
||
|
// Get the db name we are executing against
|
||
|
const dbName = options.dbName || options.authdb || db.s.databaseName;
|
||
|
|
||
|
// Convert the readPreference if its not a write
|
||
|
options.readPreference = resolveReadPreference(options, { db, default: ReadPreference.primary });
|
||
|
|
||
|
// Debug information
|
||
|
if (db.s.logger.isDebug())
|
||
|
db.s.logger.debug(
|
||
|
`executing command ${JSON.stringify(
|
||
|
command
|
||
|
)} against ${dbName}.$cmd with options [${JSON.stringify(
|
||
|
debugOptions(debugFields, options)
|
||
|
)}]`
|
||
|
);
|
||
|
|
||
|
// Execute command
|
||
|
db.s.topology.command(`${dbName}.$cmd`, command, options, (err, result) => {
|
||
|
if (err) return handleCallback(callback, err);
|
||
|
if (options.full) return handleCallback(callback, null, result);
|
||
|
handleCallback(callback, null, result.result);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Runs a command on the database as admin.
|
||
|
*
|
||
|
* @method
|
||
|
* @param {Db} db The Db instance on which to execute the command.
|
||
|
* @param {object} command The command hash
|
||
|
* @param {object} [options] Optional settings. See Db.prototype.executeDbAdminCommand for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The command result callback
|
||
|
*/
|
||
|
function executeDbAdminCommand(db, command, options, callback) {
|
||
|
db.s.topology.command('admin.$cmd', command, options, (err, result) => {
|
||
|
// Did the user destroy the topology
|
||
|
if (db.serverConfig && db.serverConfig.isDestroyed()) {
|
||
|
return callback(new MongoError('topology was destroyed'));
|
||
|
}
|
||
|
|
||
|
if (err) return handleCallback(callback, err);
|
||
|
handleCallback(callback, null, result.result);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves this collections index info.
|
||
|
*
|
||
|
* @method
|
||
|
* @param {Db} db The Db instance on which to retrieve the index info.
|
||
|
* @param {string} name The name of the collection.
|
||
|
* @param {object} [options] Optional settings. See Db.prototype.indexInformation for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The command result callback
|
||
|
*/
|
||
|
function indexInformation(db, name, options, callback) {
|
||
|
// If we specified full information
|
||
|
const full = options['full'] == null ? false : options['full'];
|
||
|
|
||
|
// Did the user destroy the topology
|
||
|
if (db.serverConfig && db.serverConfig.isDestroyed())
|
||
|
return callback(new MongoError('topology was destroyed'));
|
||
|
// Process all the results from the index command and collection
|
||
|
function processResults(indexes) {
|
||
|
// Contains all the information
|
||
|
let info = {};
|
||
|
// Process all the indexes
|
||
|
for (let i = 0; i < indexes.length; i++) {
|
||
|
const index = indexes[i];
|
||
|
// Let's unpack the object
|
||
|
info[index.name] = [];
|
||
|
for (let name in index.key) {
|
||
|
info[index.name].push([name, index.key[name]]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return info;
|
||
|
}
|
||
|
|
||
|
// Get the list of indexes of the specified collection
|
||
|
db
|
||
|
.collection(name)
|
||
|
.listIndexes(options)
|
||
|
.toArray((err, indexes) => {
|
||
|
if (err) return callback(toError(err));
|
||
|
if (!Array.isArray(indexes)) return handleCallback(callback, null, []);
|
||
|
if (full) return handleCallback(callback, null, indexes);
|
||
|
handleCallback(callback, null, processResults(indexes));
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Transformation methods for cursor results
|
||
|
function listCollectionsTransforms(databaseName) {
|
||
|
const matching = `${databaseName}.`;
|
||
|
|
||
|
return {
|
||
|
doc: doc => {
|
||
|
const index = doc.name.indexOf(matching);
|
||
|
// Remove database name if available
|
||
|
if (doc.name && index === 0) {
|
||
|
doc.name = doc.name.substr(index + matching.length);
|
||
|
}
|
||
|
|
||
|
return doc;
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieve the current profiling information for MongoDB
|
||
|
*
|
||
|
* @method
|
||
|
* @param {Db} db The Db instance on which to retrieve the profiling info.
|
||
|
* @param {Object} [options] Optional settings. See Db.protoype.profilingInfo for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The command result callback.
|
||
|
* @deprecated Query the system.profile collection directly.
|
||
|
*/
|
||
|
function profilingInfo(db, options, callback) {
|
||
|
try {
|
||
|
db
|
||
|
.collection('system.profile')
|
||
|
.find({}, options)
|
||
|
.toArray(callback);
|
||
|
} catch (err) {
|
||
|
return callback(err, null);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieve the current profiling level for MongoDB
|
||
|
*
|
||
|
* @method
|
||
|
* @param {Db} db The Db instance on which to retrieve the profiling level.
|
||
|
* @param {Object} [options] Optional settings. See Db.prototype.profilingLevel for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The command result callback
|
||
|
*/
|
||
|
function profilingLevel(db, options, callback) {
|
||
|
executeCommand(db, { profile: -1 }, options, (err, doc) => {
|
||
|
if (err == null && doc.ok === 1) {
|
||
|
const was = doc.was;
|
||
|
if (was === 0) return callback(null, 'off');
|
||
|
if (was === 1) return callback(null, 'slow_only');
|
||
|
if (was === 2) return callback(null, 'all');
|
||
|
return callback(new Error('Error: illegal profiling level value ' + was), null);
|
||
|
} else {
|
||
|
err != null ? callback(err, null) : callback(new Error('Error with profile command'), null);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove a user from a database
|
||
|
*
|
||
|
* @method
|
||
|
* @param {Db} db The Db instance on which to remove the user.
|
||
|
* @param {string} username The username.
|
||
|
* @param {object} [options] Optional settings. See Db.prototype.removeUser for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The command result callback
|
||
|
*/
|
||
|
function removeUser(db, username, options, callback) {
|
||
|
let Db = loadDb();
|
||
|
|
||
|
// Attempt to execute command
|
||
|
executeAuthRemoveUserCommand(db, username, options, (err, result) => {
|
||
|
if (err && err.code === -5000) {
|
||
|
const finalOptions = applyWriteConcern(Object.assign({}, options), { db }, options);
|
||
|
// If we have another db set
|
||
|
const db = options.dbName ? new Db(options.dbName, db.s.topology, db.s.options) : db;
|
||
|
|
||
|
// Fetch a user collection
|
||
|
const collection = db.collection(CONSTANTS.SYSTEM_USER_COLLECTION);
|
||
|
|
||
|
// Locate the user
|
||
|
findOne(collection, { user: username }, finalOptions, (err, user) => {
|
||
|
if (user == null) return handleCallback(callback, err, false);
|
||
|
remove(collection, { user: username }, finalOptions, err => {
|
||
|
handleCallback(callback, err, true);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (err) return handleCallback(callback, err);
|
||
|
handleCallback(callback, err, result);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the current profiling level of MongoDB
|
||
|
*
|
||
|
* @method
|
||
|
* @param {Db} db The Db instance on which to execute the command.
|
||
|
* @param {string} level The new profiling level (off, slow_only, all).
|
||
|
* @param {Object} [options] Optional settings. See Db.prototype.setProfilingLevel for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The command result callback.
|
||
|
*/
|
||
|
function setProfilingLevel(db, level, options, callback) {
|
||
|
const command = {};
|
||
|
let profile = 0;
|
||
|
|
||
|
if (level === 'off') {
|
||
|
profile = 0;
|
||
|
} else if (level === 'slow_only') {
|
||
|
profile = 1;
|
||
|
} else if (level === 'all') {
|
||
|
profile = 2;
|
||
|
} else {
|
||
|
return callback(new Error('Error: illegal profiling level value ' + level));
|
||
|
}
|
||
|
|
||
|
// Set up the profile number
|
||
|
command['profile'] = profile;
|
||
|
|
||
|
executeCommand(db, command, options, (err, doc) => {
|
||
|
if (err == null && doc.ok === 1) return callback(null, level);
|
||
|
return err != null
|
||
|
? callback(err, null)
|
||
|
: callback(new Error('Error with profile command'), null);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Validate the database name
|
||
|
function validateDatabaseName(databaseName) {
|
||
|
if (typeof databaseName !== 'string')
|
||
|
throw MongoError.create({ message: 'database name must be a string', driver: true });
|
||
|
if (databaseName.length === 0)
|
||
|
throw MongoError.create({ message: 'database name cannot be the empty string', driver: true });
|
||
|
if (databaseName === '$external') return;
|
||
|
|
||
|
const invalidChars = [' ', '.', '$', '/', '\\'];
|
||
|
for (let i = 0; i < invalidChars.length; i++) {
|
||
|
if (databaseName.indexOf(invalidChars[i]) !== -1)
|
||
|
throw MongoError.create({
|
||
|
message: "database names cannot contain the character '" + invalidChars[i] + "'",
|
||
|
driver: true
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create the command object for Db.prototype.createIndex.
|
||
|
*
|
||
|
* @param {Db} db The Db instance on which to create the command.
|
||
|
* @param {string} name Name of the collection to create the index on.
|
||
|
* @param {(string|object)} fieldOrSpec Defines the index.
|
||
|
* @param {Object} [options] Optional settings. See Db.prototype.createIndex for a list of options.
|
||
|
* @return {Object} The insert command object.
|
||
|
*/
|
||
|
function createCreateIndexCommand(db, name, fieldOrSpec, options) {
|
||
|
const indexParameters = parseIndexOptions(fieldOrSpec);
|
||
|
const fieldHash = indexParameters.fieldHash;
|
||
|
|
||
|
// Generate the index name
|
||
|
const indexName = typeof options.name === 'string' ? options.name : indexParameters.name;
|
||
|
const selector = {
|
||
|
ns: db.databaseName + '.' + name,
|
||
|
key: fieldHash,
|
||
|
name: indexName
|
||
|
};
|
||
|
|
||
|
// Ensure we have a correct finalUnique
|
||
|
const finalUnique = options == null || 'object' === typeof options ? false : options;
|
||
|
// Set up options
|
||
|
options = options == null || typeof options === 'boolean' ? {} : options;
|
||
|
|
||
|
// Add all the options
|
||
|
const keysToOmit = Object.keys(selector);
|
||
|
for (let optionName in options) {
|
||
|
if (keysToOmit.indexOf(optionName) === -1) {
|
||
|
selector[optionName] = options[optionName];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (selector['unique'] == null) selector['unique'] = finalUnique;
|
||
|
|
||
|
// Remove any write concern operations
|
||
|
const removeKeys = ['w', 'wtimeout', 'j', 'fsync', 'readPreference', 'session'];
|
||
|
for (let i = 0; i < removeKeys.length; i++) {
|
||
|
delete selector[removeKeys[i]];
|
||
|
}
|
||
|
|
||
|
// Return the command creation selector
|
||
|
return selector;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create index using the createIndexes command.
|
||
|
*
|
||
|
* @param {Db} db The Db instance on which to execute the command.
|
||
|
* @param {string} name Name of the collection to create the index on.
|
||
|
* @param {(string|object)} fieldOrSpec Defines the index.
|
||
|
* @param {Object} [options] Optional settings. See Db.prototype.createIndex for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The command result callback.
|
||
|
*/
|
||
|
function createIndexUsingCreateIndexes(db, name, fieldOrSpec, options, callback) {
|
||
|
// Build the index
|
||
|
const indexParameters = parseIndexOptions(fieldOrSpec);
|
||
|
// Generate the index name
|
||
|
const indexName = typeof options.name === 'string' ? options.name : indexParameters.name;
|
||
|
// Set up the index
|
||
|
const indexes = [{ name: indexName, key: indexParameters.fieldHash }];
|
||
|
// merge all the options
|
||
|
const keysToOmit = Object.keys(indexes[0]).concat([
|
||
|
'writeConcern',
|
||
|
'w',
|
||
|
'wtimeout',
|
||
|
'j',
|
||
|
'fsync',
|
||
|
'readPreference',
|
||
|
'session'
|
||
|
]);
|
||
|
|
||
|
for (let optionName in options) {
|
||
|
if (keysToOmit.indexOf(optionName) === -1) {
|
||
|
indexes[0][optionName] = options[optionName];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Get capabilities
|
||
|
const capabilities = db.s.topology.capabilities();
|
||
|
|
||
|
// Did the user pass in a collation, check if our write server supports it
|
||
|
if (indexes[0].collation && capabilities && !capabilities.commandsTakeCollation) {
|
||
|
// Create a new error
|
||
|
const error = new MongoError('server/primary/mongos does not support collation');
|
||
|
error.code = 67;
|
||
|
// Return the error
|
||
|
return callback(error);
|
||
|
}
|
||
|
|
||
|
// Create command, apply write concern to command
|
||
|
const cmd = applyWriteConcern({ createIndexes: name, indexes }, { db }, options);
|
||
|
|
||
|
// ReadPreference primary
|
||
|
options.readPreference = ReadPreference.PRIMARY;
|
||
|
|
||
|
// Build the command
|
||
|
executeCommand(db, cmd, options, (err, result) => {
|
||
|
if (err) return handleCallback(callback, err, null);
|
||
|
if (result.ok === 0) return handleCallback(callback, toError(result), null);
|
||
|
// Return the indexName for backward compatibility
|
||
|
handleCallback(callback, null, indexName);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Run the createUser command.
|
||
|
*
|
||
|
* @param {Db} db The Db instance on which to execute the command.
|
||
|
* @param {string} username The username of the user to add.
|
||
|
* @param {string} password The password of the user to add.
|
||
|
* @param {object} [options] Optional settings. See Db.prototype.addUser for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The command result callback
|
||
|
*/
|
||
|
function executeAuthCreateUserCommand(db, username, password, options, callback) {
|
||
|
// Special case where there is no password ($external users)
|
||
|
if (typeof username === 'string' && password != null && typeof password === 'object') {
|
||
|
options = password;
|
||
|
password = null;
|
||
|
}
|
||
|
|
||
|
// Unpack all options
|
||
|
if (typeof options === 'function') {
|
||
|
callback = options;
|
||
|
options = {};
|
||
|
}
|
||
|
|
||
|
// Error out if we digestPassword set
|
||
|
if (options.digestPassword != null) {
|
||
|
return callback(
|
||
|
toError(
|
||
|
"The digestPassword option is not supported via add_user. Please use db.command('createUser', ...) instead for this option."
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Get additional values
|
||
|
const customData = options.customData != null ? options.customData : {};
|
||
|
let roles = Array.isArray(options.roles) ? options.roles : [];
|
||
|
const maxTimeMS = typeof options.maxTimeMS === 'number' ? options.maxTimeMS : null;
|
||
|
|
||
|
// If not roles defined print deprecated message
|
||
|
if (roles.length === 0) {
|
||
|
console.log('Creating a user without roles is deprecated in MongoDB >= 2.6');
|
||
|
}
|
||
|
|
||
|
// Get the error options
|
||
|
const commandOptions = { writeCommand: true };
|
||
|
if (options['dbName']) commandOptions.dbName = options['dbName'];
|
||
|
|
||
|
// Add maxTimeMS to options if set
|
||
|
if (maxTimeMS != null) commandOptions.maxTimeMS = maxTimeMS;
|
||
|
|
||
|
// Check the db name and add roles if needed
|
||
|
if (
|
||
|
(db.databaseName.toLowerCase() === 'admin' || options.dbName === 'admin') &&
|
||
|
!Array.isArray(options.roles)
|
||
|
) {
|
||
|
roles = ['root'];
|
||
|
} else if (!Array.isArray(options.roles)) {
|
||
|
roles = ['dbOwner'];
|
||
|
}
|
||
|
|
||
|
const digestPassword = db.s.topology.lastIsMaster().maxWireVersion >= 7;
|
||
|
|
||
|
// Build the command to execute
|
||
|
let command = {
|
||
|
createUser: username,
|
||
|
customData: customData,
|
||
|
roles: roles,
|
||
|
digestPassword
|
||
|
};
|
||
|
|
||
|
// Apply write concern to command
|
||
|
command = applyWriteConcern(command, { db }, options);
|
||
|
|
||
|
let userPassword = password;
|
||
|
|
||
|
if (!digestPassword) {
|
||
|
// Use node md5 generator
|
||
|
const md5 = crypto.createHash('md5');
|
||
|
// Generate keys used for authentication
|
||
|
md5.update(username + ':mongo:' + password);
|
||
|
userPassword = md5.digest('hex');
|
||
|
}
|
||
|
|
||
|
// No password
|
||
|
if (typeof password === 'string') {
|
||
|
command.pwd = userPassword;
|
||
|
}
|
||
|
|
||
|
// Force write using primary
|
||
|
commandOptions.readPreference = ReadPreference.primary;
|
||
|
|
||
|
// Execute the command
|
||
|
executeCommand(db, command, commandOptions, (err, result) => {
|
||
|
if (err && err.ok === 0 && err.code === undefined)
|
||
|
return handleCallback(callback, { code: -5000 }, null);
|
||
|
if (err) return handleCallback(callback, err, null);
|
||
|
handleCallback(
|
||
|
callback,
|
||
|
!result.ok ? toError(result) : null,
|
||
|
result.ok ? [{ user: username, pwd: '' }] : null
|
||
|
);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Run the dropUser command.
|
||
|
*
|
||
|
* @param {Db} db The Db instance on which to execute the command.
|
||
|
* @param {string} username The username of the user to remove.
|
||
|
* @param {object} [options] Optional settings. See Db.prototype.removeUser for a list of options.
|
||
|
* @param {Db~resultCallback} [callback] The command result callback
|
||
|
*/
|
||
|
function executeAuthRemoveUserCommand(db, username, options, callback) {
|
||
|
if (typeof options === 'function') (callback = options), (options = {});
|
||
|
options = options || {};
|
||
|
|
||
|
// Did the user destroy the topology
|
||
|
if (db.serverConfig && db.serverConfig.isDestroyed())
|
||
|
return callback(new MongoError('topology was destroyed'));
|
||
|
// Get the error options
|
||
|
const commandOptions = { writeCommand: true };
|
||
|
if (options['dbName']) commandOptions.dbName = options['dbName'];
|
||
|
|
||
|
// Get additional values
|
||
|
const maxTimeMS = typeof options.maxTimeMS === 'number' ? options.maxTimeMS : null;
|
||
|
|
||
|
// Add maxTimeMS to options if set
|
||
|
if (maxTimeMS != null) commandOptions.maxTimeMS = maxTimeMS;
|
||
|
|
||
|
// Build the command to execute
|
||
|
let command = {
|
||
|
dropUser: username
|
||
|
};
|
||
|
|
||
|
// Apply write concern to command
|
||
|
command = applyWriteConcern(command, { db }, options);
|
||
|
|
||
|
// Force write using primary
|
||
|
commandOptions.readPreference = ReadPreference.primary;
|
||
|
|
||
|
// Execute the command
|
||
|
executeCommand(db, command, commandOptions, (err, result) => {
|
||
|
if (err && !err.ok && err.code === undefined) return handleCallback(callback, { code: -5000 });
|
||
|
if (err) return handleCallback(callback, err, null);
|
||
|
handleCallback(callback, null, result.ok ? true : false);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
module.exports = {
|
||
|
addUser,
|
||
|
collections,
|
||
|
createCollection,
|
||
|
createListener,
|
||
|
createIndex,
|
||
|
dropCollection,
|
||
|
dropDatabase,
|
||
|
ensureIndex,
|
||
|
evaluate,
|
||
|
executeCommand,
|
||
|
executeDbAdminCommand,
|
||
|
listCollectionsTransforms,
|
||
|
indexInformation,
|
||
|
profilingInfo,
|
||
|
profilingLevel,
|
||
|
removeUser,
|
||
|
setProfilingLevel,
|
||
|
validateDatabaseName
|
||
|
};
|