import {MongoClient} from 'mongodb'
import {ObjectID} from 'bson'
import {utils} from 'js-data'
import {
Adapter,
reserved
} from 'js-data-adapter'
import snakeCase from 'lodash.snakecase'
const DEFAULTS = {
/**
* Convert ObjectIDs to strings when pulling records out of the database.
*
* @name MongoDBAdapter#translateId
* @type {boolean}
* @default true
*/
translateId: true,
/**
* Convert fields of record from database that are ObjectIDs to strings
*
* @name MongoDBAdapter#translateObjectIDs
* @type {Boolean}
* @default false
*/
translateObjectIDs: false,
/**
* MongoDB URI.
*
* @name MongoDBAdapter#uri
* @type {string}
* @default mongodb://localhost:27017
*/
uri: 'mongodb://localhost:27017',
/**
* MongoDB Driver options
*
* @name MongoDBAdapter#mongoDriverOpts
* @type {object}
* @default { ignoreUndefined: true }
*/
mongoDriverOpts: {
ignoreUndefined: true
}
}
const COUNT_OPTS_DEFAULTS = {}
const FIND_OPTS_DEFAULTS = {}
const FIND_ONE_OPTS_DEFAULTS = {}
const INSERT_OPTS_DEFAULTS = {}
const INSERT_MANY_OPTS_DEFAULTS = {}
const UPDATE_OPTS_DEFAULTS = {}
const REMOVE_OPTS_DEFAULTS = {}
/**
* MongoDBAdapter class.
*
* @example
* // Use Container instead of DataStore on the server
* import { Container } from 'js-data';
* import MongoDBAdapter from 'js-data-mongodb';
*
* // Create a store to hold your Mappers
* const store = new Container({
* mapperDefaults: {
* // MongoDB uses "_id" as the primary key
* idAttribute: '_id'
* }
* });
*
* // Create an instance of MongoDBAdapter with default settings
* const adapter = new MongoDBAdapter();
*
* // Mappers in "store" will use the MongoDB adapter by default
* store.registerAdapter('mongodb', adapter, { default: true });
*
* // Create a Mapper that maps to a "user" collection
* store.defineMapper('user');
*
* @class MongoDBAdapter
* @extends Adapter
* @param {object} [opts] Configuration options.
* @param {boolean} [opts.debug=false] See {@link Adapter#debug}.
* @param {object} [opts.countOpts] See {@link MongoDBAdapter#countOpts}.
* @param {object} [opts.findOpts] See {@link MongoDBAdapter#findOpts}.
* @param {object} [opts.findOneOpts] See {@link MongoDBAdapter#findOneOpts}.
* @param {object} [opts.insertOpts] See {@link MongoDBAdapter#insertOpts}.
* @param {object} [opts.insertManyOpts] See {@link MongoDBAdapter#insertManyOpts}.
* @param {boolean} [opts.raw=false] See {@link Adapter#raw}.
* @param {object} [opts.removeOpts] See {@link MongoDBAdapter#removeOpts}.
* @param {boolean} [opts.translateId=true] See {@link MongoDBAdapter#translateId}.
* @param {boolean} [opts.translateObjectIDs=false] See {@link MongoDBAdapter#translateObjectIDs}.
* @param {object} [opts.updateOpts] See {@link MongoDBAdapter#updateOpts}.
* @param {string} [opts.uri="mongodb://localhost:27017"] See {@link MongoDBAdapter#uri}.
*/
export function MongoDBAdapter (opts) {
utils.classCallCheck(this, MongoDBAdapter)
opts || (opts = {})
if (utils.isString(opts)) {
opts = { uri: opts }
}
utils.fillIn(opts, DEFAULTS)
// Setup non-enumerable properties
Object.defineProperties(this, {
/**
* A Promise that resolves to a reference to the MongoDB client being used by
* this adapter.
*
* @name MongoDBAdapter#client
* @type {Promise}
*/
client: {
writable: true,
value: undefined
},
_db: {
writable: true,
value: undefined
}
})
Adapter.call(this, opts)
/**
* Default options to pass to collection#count.
*
* @name MongoDBAdapter#countOpts
* @type {object}
* @default {}
*/
this.countOpts || (this.countOpts = {})
utils.fillIn(this.countOpts, COUNT_OPTS_DEFAULTS)
/**
* Default options to pass to collection#find.
*
* @name MongoDBAdapter#findOpts
* @type {object}
* @default {}
*/
this.findOpts || (this.findOpts = {})
utils.fillIn(this.findOpts, FIND_OPTS_DEFAULTS)
/**
* Default options to pass to collection#findOne.
*
* @name MongoDBAdapter#findOneOpts
* @type {object}
* @default {}
*/
this.findOneOpts || (this.findOneOpts = {})
utils.fillIn(this.findOneOpts, FIND_ONE_OPTS_DEFAULTS)
/**
* Default options to pass to collection#insert.
*
* @name MongoDBAdapter#insertOpts
* @type {object}
* @default {}
*/
this.insertOpts || (this.insertOpts = {})
utils.fillIn(this.insertOpts, INSERT_OPTS_DEFAULTS)
/**
* Default options to pass to collection#insertMany.
*
* @name MongoDBAdapter#insertManyOpts
* @type {object}
* @default {}
*/
this.insertManyOpts || (this.insertManyOpts = {})
utils.fillIn(this.insertManyOpts, INSERT_MANY_OPTS_DEFAULTS)
/**
* Default options to pass to collection#update.
*
* @name MongoDBAdapter#updateOpts
* @type {object}
* @default {}
*/
this.updateOpts || (this.updateOpts = {})
utils.fillIn(this.updateOpts, UPDATE_OPTS_DEFAULTS)
/**
* Default options to pass to collection#destroy.
*
* @name MongoDBAdapter#removeOpts
* @type {object}
* @default {}
*/
this.removeOpts || (this.removeOpts = {})
utils.fillIn(this.removeOpts, REMOVE_OPTS_DEFAULTS)
this.client = new utils.Promise((resolve, reject) => {
MongoClient.connect(opts.uri, opts.mongoDriverOpts, (err, db) => {
if (err) {
return reject(err)
}
this._db = db
resolve(db)
})
})
}
Adapter.extend({
constructor: MongoDBAdapter,
_translateObjectIDs (r, opts) {
opts || (opts = {})
if (this.getOpt('translateObjectIDs', opts)) {
this._translateFieldObjectIDs(r)
} else if (this.getOpt('translateId', opts)) {
this._translateId(r)
}
return r
},
/**
* Translate ObjectIDs to strings.
*
* @method MongoDBAdapter#_translateId
* @return {*}
*/
_translateId (r) {
if (utils.isArray(r)) {
r.forEach((_r) => {
const __id = _r._id ? _r._id.toString() : _r._id
_r._id = typeof __id === 'string' ? __id : _r._id
})
} else if (utils.isObject(r)) {
const __id = r._id ? r._id.toString() : r._id
r._id = typeof __id === 'string' ? __id : r._id
}
return r
},
_translateFieldObjectIDs (r) {
const _checkFields = (r) => {
for (let field in r) {
if (r[field]._bsontype === 'ObjectID') {
r[field] = typeof r[field].toString() === 'string' ? r[field].toString() : r[field]
}
}
}
if (utils.isArray(r)) {
r.forEach((_r) => {
_checkFields(_r)
})
} else if (utils.isObject(r)) {
_checkFields(r)
}
return r
},
/**
* Retrieve the number of records that match the selection query.
*
* @method MongoDBAdapter#count
* @param {object} mapper The mapper.
* @param {object} query Selection query.
* @param {object} [opts] Configuration options.
* @param {object} [opts.countOpts] Options to pass to collection#count.
* @param {boolean} [opts.raw=false] Whether to return a more detailed
* response object.
* @param {string[]} [opts.with=[]] Relations to eager load.
* @return {Promise}
*/
/**
* Retrieve the records that match the selection query. Internal method used
* by Adapter#count.
*
* @method MongoDBAdapter#_count
* @private
* @param {object} mapper The mapper.
* @param {object} query Selection query.
* @param {object} [opts] Configuration options.
* @return {Promise}
*/
_count (mapper, query, opts) {
opts || (opts = {})
return this._run((client, success, failure) => {
const collectionId = this._getCollectionId(mapper, opts)
const countOpts = this.getOpt('countOpts', opts)
utils.fillIn(countOpts, this.getQueryOptions(mapper, query))
const mongoQuery = this.getQuery(mapper, query)
client
.collection(collectionId)
.count(mongoQuery, countOpts, (err, count) => err ? failure(err) : success([count, {}]))
})
},
/**
* Create a new record.
*
* @method MongoDBAdapter#create
* @param {object} mapper The mapper.
* @param {object} props The record to be created.
* @param {object} [opts] Configuration options.
* @param {object} [opts.insertOpts] Options to pass to collection#insert.
* @param {boolean} [opts.raw=false] Whether to return a more detailed
* @return {Promise}
*/
/**
* Create a new record. Internal method used by Adapter#create.
*
* @method MongoDBAdapter#_create
* @private
* @param {object} mapper The mapper.
* @param {object} props The record to be created.
* @param {object} [opts] Configuration options.
* @return {Promise}
*/
_create (mapper, props, opts) {
props || (props = {})
opts || (opts = {})
return this._run((client, success, failure) => {
const collectionId = this._getCollectionId(mapper, opts)
const insertOpts = this.getOpt('insertOpts', opts)
const collection = client.collection(collectionId)
const handler = (err, cursor) => err ? failure(err) : success(cursor)
props = utils.plainCopy(props)
if (collection.insertOne) {
collection
.insertOne(props, insertOpts, handler)
} else {
collection
.insert(props, insertOpts, handler)
}
}).then((cursor) => {
let record
let r = cursor.ops ? cursor.ops : cursor
this._translateObjectIDs(r, opts)
record = utils.isArray(r) ? r[0] : r
cursor.connection = undefined
return [record, cursor]
})
},
/**
* Create multiple records in a single batch.
*
* @method MongoDBAdapter#createMany
* @param {object} mapper The mapper.
* @param {object} props The records to be created.
* @param {object} [opts] Configuration options.
* @param {object} [opts.insertManyOpts] Options to pass to
* collection#insertMany.
* @param {boolean} [opts.raw=false] Whether to return a more detailed
* response object.
* @return {Promise}
*/
/**
* Create multiple records in a single batch. Internal method used by
* Adapter#createMany.
*
* @method MongoDBAdapter#_createMany
* @private
* @param {object} mapper The mapper.
* @param {object} props The records to be created.
* @param {object} [opts] Configuration options.
* @return {Promise}
*/
_createMany (mapper, props, opts) {
props || (props = {})
opts || (opts = {})
return this._run((client, success, failure) => {
const collectionId = this._getCollectionId(mapper, opts)
const insertManyOpts = this.getOpt('insertManyOpts', opts)
props = utils.plainCopy(props)
client.collection(collectionId)
.insertMany(props, insertManyOpts, (err, cursor) => err ? failure(err) : success(cursor))
}).then((cursor) => {
let records = []
let r = cursor.ops ? cursor.ops : cursor
this._translateObjectIDs(r, opts)
records = r
cursor.connection = undefined
return [records, cursor]
})
},
/**
* Destroy the record with the given primary key.
*
* @method MongoDBAdapter#destroy
* @param {object} mapper The mapper.
* @param {(string|number)} id Primary key of the record to destroy.
* @param {object} [opts] Configuration options.
* @param {boolean} [opts.raw=false] Whether to return a more detailed
* response object.
* @param {object} [opts.removeOpts] Options to pass to collection#remove.
* @return {Promise}
*/
/**
* Destroy the record with the given primary key. Internal method used by
* Adapter#destroy.
*
* @method MongoDBAdapter#_destroy
* @private
* @param {object} mapper The mapper.
* @param {(string|number)} id Primary key of the record to destroy.
* @param {object} [opts] Configuration options.
* @return {Promise}
*/
_destroy (mapper, id, opts) {
opts || (opts = {})
return this._run((client, success, failure) => {
const collectionId = this._getCollectionId(mapper, opts)
const removeOpts = this.getOpt('removeOpts', opts)
const mongoQuery = {
[mapper.idAttribute]: this.toObjectID(mapper, id)
}
const collection = client.collection(collectionId)
const handler = (err, cursor) => err ? failure(err) : success(cursor)
if (collection.deleteOne) {
collection
.deleteOne(mongoQuery, removeOpts, handler)
} else {
collection
.remove(mongoQuery, removeOpts, handler)
}
}).then((cursor) => [undefined, cursor])
},
/**
* Destroy the records that match the selection query.
*
* @method MongoDBAdapter#destroyAll
* @param {object} mapper the mapper.
* @param {object} [query] Selection query.
* @param {object} [query.where] Filtering criteria.
* @param {string|Array} [query.orderBy] Sorting criteria.
* @param {string|Array} [query.sort] Same as `query.sort`.
* @param {number} [query.limit] Limit results.
* @param {number} [query.skip] Offset results.
* @param {number} [query.offset] Same as `query.skip`.
* @param {object} [opts] Configuration options.
* @param {boolean} [opts.raw=false] Whether to return a more detailed
* response object.
* @param {object} [opts.removeOpts] Options to pass to collection#remove.
* @return {Promise}
*/
/**
* Destroy the records that match the selection query. Internal method used by
* Adapter#destroyAll.
*
* @method MongoDBAdapter#_destroyAll
* @private
* @param {object} mapper the mapper.
* @param {object} [query] Selection query.
* @param {object} [opts] Configuration options.
* @return {Promise}
*/
_destroyAll (mapper, query, opts) {
query || (query = {})
opts || (opts = {})
return this._run((client, success, failure) => {
const collectionId = this._getCollectionId(mapper, opts)
const removeOpts = this.getOpt('removeOpts', opts)
utils.fillIn(removeOpts, this.getQueryOptions(mapper, query))
const mongoQuery = this.getQuery(mapper, query)
const collection = client.collection(collectionId)
const handler = (err, cursor) => err ? failure(err) : success(cursor)
if (collection.deleteMany) {
collection
.deleteMany(mongoQuery, removeOpts, handler)
} else {
collection
.remove(mongoQuery, removeOpts, handler)
}
}).then((cursor) => {
cursor.connection = undefined
return [undefined, cursor]
})
},
/**
* Retrieve the record with the given primary key.
*
* @method MongoDBAdapter#find
* @param {object} mapper The mapper.
* @param {(string|number)} id Primary key of the record to retrieve.
* @param {object} [opts] Configuration options.
* @param {string|string[]|object} [opts.fields] Select a subset of fields to be returned.
* @param {object} [opts.findOneOpts] Options to pass to collection#findOne.
* @param {boolean} [opts.raw=false] Whether to return a more detailed
* response object.
* @param {string[]} [opts.with=[]] Relations to eager load.
* @return {Promise}
*/
/**
* Retrieve the record with the given primary key. Internal method used by
* Adapter#find.
*
* @method MongoDBAdapter#_find
* @private
* @param {object} mapper The mapper.
* @param {(string|number)} id Primary key of the record to retrieve.
* @param {object} [opts] Configuration options.
* @param {string|string[]|object} [opts.fields] Select a subset of fields to be returned.
* @return {Promise}
*/
_find (mapper, id, opts) {
opts || (opts = {})
opts.with || (opts.with = [])
return this._run((client, success, failure) => {
const collectionId = this._getCollectionId(mapper, opts)
const findOneOpts = this.getOpt('findOneOpts', opts)
findOneOpts.fields = this._getFields(mapper, opts)
const mongoQuery = {
[mapper.idAttribute]: this.toObjectID(mapper, id)
}
client.collection(collectionId)
.findOne(mongoQuery, findOneOpts, (err, record) => err ? failure(err) : success(record))
}).then((record) => {
if (record) {
this._translateObjectIDs(record, opts)
} else {
record = undefined
}
return [record, {}]
})
},
/**
* Retrieve the records that match the selection query.
*
* @method MongoDBAdapter#findAll
* @param {object} mapper The mapper.
* @param {object} query Selection query.
* @param {object} [opts] Configuration options.
* @param {string|string[]|object} [opts.fields] Select a subset of fields to be returned.
* @param {object} [opts.findOpts] Options to pass to collection#find.
* @param {boolean} [opts.raw=false] Whether to return a more detailed
* response object.
* @param {string[]} [opts.with=[]] Relations to eager load.
* @return {Promise}
*/
/**
* Retrieve the records that match the selection query. Internal method used
* by Adapter#findAll.
*
* @method MongoDBAdapter#_findAll
* @private
* @param {object} mapper The mapper.
* @param {object} query Selection query.
* @param {object} [opts] Configuration options.
* @param {string|string[]|object} [opts.fields] Select a subset of fields to be returned.
* @return {Promise}
*/
_findAll (mapper, query, opts) {
opts || (opts = {})
return this._run((client, success, failure) => {
const collectionId = this._getCollectionId(mapper, opts)
const findOpts = this.getOpt('findOpts', opts)
utils.fillIn(findOpts, this.getQueryOptions(mapper, query))
findOpts.fields = this._getFields(mapper, opts)
const mongoQuery = this.getQuery(mapper, query)
client.collection(collectionId)
.find(mongoQuery, findOpts)
.toArray((err, records) => err ? failure(err) : success(records))
}).then((records) => {
this._translateObjectIDs(records, opts)
return [records, {}]
})
},
_getCollectionId (mapper, opts) {
opts || (opts = {})
return opts.table || opts.collection || mapper.table || mapper.collection || snakeCase(mapper.name)
},
_getFields (mapper, opts) {
opts || (opts = {})
if (utils.isString(opts.fields)) {
opts.fields = { [opts.fields]: 1 }
} else if (utils.isArray(opts.fields)) {
const fields = {}
opts.fields.forEach((field) => {
fields[field] = 1
})
return fields
}
return opts.fields
},
_run (cb) {
if (this._db) {
// Use the cached db object
return new utils.Promise((resolve, reject) => {
cb(this._db, resolve, reject)
})
}
return this.getClient().then((client) => {
return new utils.Promise((resolve, reject) => {
cb(client, resolve, reject)
})
})
},
/**
* Apply the given update to the record with the specified primary key.
*
* @method MongoDBAdapter#update
* @param {object} mapper The mapper.
* @param {(string|number)} id The primary key of the record to be updated.
* @param {object} props The update to apply to the record.
* @param {object} [opts] Configuration options.
* @param {boolean} [opts.raw=false] Whether to return a more detailed
* response object.
* @param {object} [opts.updateOpts] Options to pass to collection#update.
* @return {Promise}
*/
/**
* Apply the given update to the record with the specified primary key.
* Internal method used by Adapter#update.
*
* @method MongoDBAdapter#_update
* @private
* @param {object} mapper The mapper.
* @param {(string|number)} id The primary key of the record to be updated.
* @param {object} props The update to apply to the record.
* @param {object} [opts] Configuration options.
* @return {Promise}
*/
_update (mapper, id, props, opts) {
props || (props = {})
opts || (opts = {})
return this._find(mapper, id, { raw: false })
.then((result) => {
if (!result[0]) {
throw new Error('Not Found')
}
return this._run((client, success, failure) => {
const collectionId = this._getCollectionId(mapper, opts)
const updateOpts = this.getOpt('updateOpts', opts)
const mongoQuery = {
[mapper.idAttribute]: this.toObjectID(mapper, id)
}
const collection = client.collection(collectionId)
const handler = (err, cursor) => err ? failure(err) : success(cursor)
if (collection.updateOne) {
collection
.updateOne(mongoQuery, { $set: props }, updateOpts, handler)
} else {
collection
.update(mongoQuery, { $set: props }, updateOpts, handler)
}
})
})
.then((cursor) => {
return this._find(mapper, id, { raw: false })
.then((result) => {
cursor.connection = undefined
return [result[0], cursor]
})
})
},
/**
* Apply the given update to all records that match the selection query.
*
* @method MongoDBAdapter#updateAll
* @param {object} mapper The mapper.
* @param {object} props The update to apply to the selected records.
* @param {object} [query] Selection query.
* @param {object} [opts] Configuration options.
* @param {boolean} [opts.raw=false] Whether to return a more detailed
* response object.
* @param {object} [opts.updateOpts] Options to pass to collection#update.
* @return {Promise}
*/
/**
* Apply the given update to all records that match the selection query.
* Internal method used by Adapter#updateAll.
*
* @method MongoDBAdapter#_updateAll
* @private
* @param {Object} mapper The mapper.
* @param {Object} props The update to apply to the selected records.
* @param {Object} [query] Selection query.
* @param {Object} [opts] Configuration options.
* @return {Promise}
*/
_updateAll (mapper, props, query, opts) {
props || (props = {})
query || (query = {})
opts || (opts = {})
let ids
return this._run((client, success, failure) => {
return this._findAll(mapper, query, { raw: false }).then((result) => {
const collectionId = this._getCollectionId(mapper, opts)
const updateOpts = this.getOpt('updateOpts', opts)
updateOpts.multi = true
const queryOptions = this.getQueryOptions(mapper, query)
queryOptions.$set = props
ids = result[0].map((record) => this.toObjectID(mapper, record[mapper.idAttribute]))
const mongoQuery = this.getQuery(mapper, query)
const collection = client.collection(collectionId)
const handler = (err, cursor) => err ? failure(err) : success(cursor)
if (collection.updateMany) {
collection
.updateMany(mongoQuery, queryOptions, updateOpts, handler)
} else {
collection
.update(mongoQuery, queryOptions, updateOpts, handler)
}
})
}).then((cursor) => {
const query = {
[mapper.idAttribute]: {
'in': ids
}
}
return this._findAll(mapper, query, { raw: false }).then((result) => {
cursor.connection = undefined
return [result[0], cursor]
})
})
},
/**
* Return a Promise that resolves to a reference to the MongoDB client being
* used by this adapter.
*
* Useful when you need to do anything custom with the MongoDB client library.
*
* @method MongoDBAdapter#getClient
* @return {object} MongoDB client.
*/
getClient () {
return this.client
},
/**
* Map filtering params in a selection query to MongoDB a filtering object.
*
* Handles the following:
*
* - where
* - and bunch of filtering operators
*
* @method MongoDBAdapter#getQuery
* @return {object}
*/
getQuery (mapper, query) {
query = utils.plainCopy(query || {})
query.where || (query.where = {})
utils.forOwn(query, function (config, keyword) {
if (reserved.indexOf(keyword) === -1) {
if (utils.isObject(config)) {
query.where[keyword] = config
} else {
query.where[keyword] = {
'==': config
}
}
delete query[keyword]
}
})
let mongoQuery = {}
if (Object.keys(query.where).length !== 0) {
utils.forOwn(query.where, function (criteria, field) {
if (!utils.isObject(criteria)) {
query.where[field] = {
'==': criteria
}
}
utils.forOwn(criteria, function (v, op) {
if (op === '==' || op === '===' || op === 'contains') {
mongoQuery[field] = v
} else if (op === '!=' || op === '!==' || op === 'notContains') {
mongoQuery[field] = mongoQuery[field] || {}
mongoQuery[field].$ne = v
} else if (op === '>') {
mongoQuery[field] = mongoQuery[field] || {}
mongoQuery[field].$gt = v
} else if (op === '>=') {
mongoQuery[field] = mongoQuery[field] || {}
mongoQuery[field].$gte = v
} else if (op === '<') {
mongoQuery[field] = mongoQuery[field] || {}
mongoQuery[field].$lt = v
} else if (op === '<=') {
mongoQuery[field] = mongoQuery[field] || {}
mongoQuery[field].$lte = v
} else if (op === 'in') {
mongoQuery[field] = mongoQuery[field] || {}
mongoQuery[field].$in = v
} else if (op === 'notIn') {
mongoQuery[field] = mongoQuery[field] || {}
mongoQuery[field].$nin = v
} else if (op === '|==' || op === '|===' || op === '|contains') {
mongoQuery.$or = mongoQuery.$or || []
let orEqQuery = {}
orEqQuery[field] = v
mongoQuery.$or.push(orEqQuery)
} else if (op === '|!=' || op === '|!==' || op === '|notContains') {
mongoQuery.$or = mongoQuery.$or || []
let orNeQuery = {}
orNeQuery[field] = {
'$ne': v
}
mongoQuery.$or.push(orNeQuery)
} else if (op === '|>') {
mongoQuery.$or = mongoQuery.$or || []
let orGtQuery = {}
orGtQuery[field] = {
'$gt': v
}
mongoQuery.$or.push(orGtQuery)
} else if (op === '|>=') {
mongoQuery.$or = mongoQuery.$or || []
let orGteQuery = {}
orGteQuery[field] = {
'$gte': v
}
mongoQuery.$or.push(orGteQuery)
} else if (op === '|<') {
mongoQuery.$or = mongoQuery.$or || []
let orLtQuery = {}
orLtQuery[field] = {
'$lt': v
}
mongoQuery.$or.push(orLtQuery)
} else if (op === '|<=') {
mongoQuery.$or = mongoQuery.$or || []
let orLteQuery = {}
orLteQuery[field] = {
'$lte': v
}
mongoQuery.$or.push(orLteQuery)
} else if (op === '|in') {
mongoQuery.$or = mongoQuery.$or || []
let orInQuery = {}
orInQuery[field] = {
'$in': v
}
mongoQuery.$or.push(orInQuery)
} else if (op === '|notIn') {
mongoQuery.$or = mongoQuery.$or || []
let orNinQuery = {}
orNinQuery[field] = {
'$nin': v
}
mongoQuery.$or.push(orNinQuery)
}
})
})
}
return mongoQuery
},
/**
* Map non-filtering params in a selection query to MongoDB query options.
*
* Handles the following:
*
* - limit
* - skip/offset
* - orderBy/sort
*
* @method MongoDBAdapter#getQueryOptions
* @return {object}
*/
getQueryOptions (mapper, query) {
query = utils.plainCopy(query || {})
query.orderBy = query.orderBy || query.sort
query.skip = query.skip || query.offset
let queryOptions = {}
if (query.orderBy) {
if (utils.isString(query.orderBy)) {
query.orderBy = [
[query.orderBy, 'asc']
]
}
for (var i = 0; i < query.orderBy.length; i++) {
if (utils.isString(query.orderBy[i])) {
query.orderBy[i] = [query.orderBy[i], 'asc']
}
}
queryOptions.sort = query.orderBy
}
if (query.skip) {
queryOptions.skip = +query.skip
}
if (query.limit) {
queryOptions.limit = +query.limit
}
return queryOptions
},
/**
* Turn an _id into an ObjectID if it isn't already an ObjectID.
*
* @method MongoDBAdapter#toObjectID
* @return {*}
*/
toObjectID (mapper, id) {
if (id !== undefined && mapper.idAttribute === '_id' && typeof id === 'string' && ObjectID.isValid(id) && !(id instanceof ObjectID)) {
return new ObjectID(id)
}
return id
},
/**
* Return the foreignKey from the given record for the provided relationship.
*
* @method MongoDBAdapter#makeBelongsToForeignKey
* @return {*}
*/
makeBelongsToForeignKey (mapper, def, record) {
return this.toObjectID(def.getRelation(), Adapter.prototype.makeBelongsToForeignKey.call(this, mapper, def, record))
},
/**
* Return the localKeys from the given record for the provided relationship.
*
* Override with care.
*
* @method MongoDBAdapter#makeHasManyLocalKeys
* @return {*}
*/
makeHasManyLocalKeys (mapper, def, record) {
const relatedMapper = def.getRelation()
const localKeys = Adapter.prototype.makeHasManyLocalKeys.call(this, mapper, def, record)
return localKeys.map((key) => this.toObjectID(relatedMapper, key))
},
/**
* Not supported.
*
* @method MongoDBAdapter#updateMany
*/
updateMany () {
throw new Error('not supported!')
}
})
/**
* Details of the current version of the `js-data-mongodb` module.
*
* @example
* import { version } from 'js-data-mongodb';
* console.log(version.full);
*
* @name module:js-data-mongodb.version
* @type {object}
* @property {string} version.full The full semver value.
* @property {number} version.major The major version number.
* @property {number} version.minor The minor version number.
* @property {number} version.patch The patch version number.
* @property {(string|boolean)} version.alpha The alpha version value,
* otherwise `false` if the current version is not alpha.
* @property {(string|boolean)} version.beta The beta version value,
* otherwise `false` if the current version is not beta.
*/
export const version = '<%= version %>'
/**
* {@link MongoDBAdapter} class.
*
* @example
* import { MongoDBAdapter } from 'js-data-mongodb';
* const adapter = new MongoDBAdapter();
*
* @name module:js-data-mongodb.MongoDBAdapter
* @see MongoDBAdapter
* @type {Constructor}
*/
/**
* Registered as `js-data-mongodb` in NPM.
*
* @example <caption>Install from NPM</caption>
* npm i --save js-data-mongodb js-data mongodb bson
*
* @example <caption>Load via CommonJS</caption>
* const MongoDBAdapter = require('js-data-mongodb').MongoDBAdapter;
* const adapter = new MongoDBAdapter();
*
* @example <caption>Load via ES2015 Modules</caption>
* import { MongoDBAdapter } from 'js-data-mongodb';
* const adapter = new MongoDBAdapter();
*
* @module js-data-mongodb
*/
/**
* Create a subclass of this MongoDBAdapter:
* @example <caption>MongoDBAdapter.extend</caption>
* // Normally you would do: import { MongoDBAdapter } from 'js-data-mongodb';
* const JSDataMongoDB = require('js-data-mongodb');
* const { MongoDBAdapter } = JSDataMongoDB;
* console.log('Using JSDataMongoDB v' + JSDataMongoDB.version.full);
*
* // Extend the class using ES2015 class syntax.
* class CustomMongoDBAdapterClass extends MongoDBAdapter {
* foo () { return 'bar'; }
* static beep () { return 'boop'; }
* }
* const customMongoDBAdapter = new CustomMongoDBAdapterClass();
* console.log(customMongoDBAdapter.foo());
* console.log(CustomMongoDBAdapterClass.beep());
*
* // Extend the class using alternate method.
* const OtherMongoDBAdapterClass = MongoDBAdapter.extend({
* foo () { return 'bar'; }
* }, {
* beep () { return 'boop'; }
* });
* const otherMongoDBAdapter = new OtherMongoDBAdapterClass();
* console.log(otherMongoDBAdapter.foo());
* console.log(OtherMongoDBAdapterClass.beep());
*
* // Extend the class, providing a custom constructor.
* function AnotherMongoDBAdapterClass () {
* MongoDBAdapter.call(this);
* this.created_at = new Date().getTime();
* }
* MongoDBAdapter.extend({
* constructor: AnotherMongoDBAdapterClass,
* foo () { return 'bar'; }
* }, {
* beep () { return 'boop'; }
* });
* const anotherMongoDBAdapter = new AnotherMongoDBAdapterClass();
* console.log(anotherMongoDBAdapter.created_at);
* console.log(anotherMongoDBAdapter.foo());
* console.log(AnotherMongoDBAdapterClass.beep());
*
* @method MongoDBAdapter.extend
* @param {object} [props={}] Properties to add to the prototype of the
* subclass.
* @param {object} [props.constructor] Provide a custom constructor function
* to be used as the subclass itself.
* @param {object} [classProps={}] Static properties to add to the subclass.
* @returns {Constructor} Subclass of this MongoDBAdapter class.
* @since 3.0.0
*/