UNPKG

js-data-cloudmine

Version:
728 lines (664 loc) 22.4 kB
import { utils } from 'js-data' import { Adapter } from '../node_modules/js-data-adapter/src/index' function isValidString (value) { return (value != null && value !== '') } function join (items, separator) { separator || (separator = '') return items.filter(isValidString).join(separator) } function makePath (...args) { // eslint-disable-line no-unused-vars let result = join(args, '/') return result.replace(/([^:\/]|^)\/{2,}/g, '$1/') } let queue = [] let taskInProcess = false function enqueue (task) { queue.push(task) } function dequeue () { if (queue.length && !taskInProcess) { taskInProcess = true queue[ 0 ]() } } function queueTask (task) { if (!queue.length) { enqueue(task) dequeue() } else { enqueue(task) } } function createTask (fn) { return new utils.Promise(fn).then((result) => { taskInProcess = false queue.shift() setTimeout(dequeue, 0) return result }, (err) => { taskInProcess = false queue.shift() setTimeout(dequeue, 0) return utils.reject(err) }) } function APICallToPromise (ac, opts) { return new utils.Promise((resolve, reject) => { ac.on('success', (data, response) => { resolve([ data, response ]) }) .on('error', (data, response) => { // throw new Error(data) reject([ data, response ]) }) // Tack on any extra jQuery like options. if (opts.meta) { ac.on('meta', opts.meta) } if (opts.result) { ac.on('result', opts.result) } if (opts.complete) { ac.on('complete', opts.complete) } }) } const __super__ = Adapter.prototype /** * CloudMineAdapter class. * * @example <caption>Browser</caption> * import {DataStore} from 'js-data' * import cloudmine from 'cloudmine' * import {CloudMineAdapter} from 'js-data-cloudmine' * const store = new DataStore() * cloudmine.initializeApp({ * apiKey: 'your-api-key', * databaseURL: 'your-database-url' * }) * const adapter = new CloudMineAdapter({ db: cloudmine.database() }) * store.registerAdapter('cloudmine', adapter, { 'default': true }) * * @example <caption>Node.js</caption> * import {Container} from 'js-data' * import cloudmine from 'cloudmine' * import {CloudMineAdapter} from 'js-data-cloudmine' * const store = new Container() * cloudmine.initializeApp({ * databaseURL: 'your-database-url', * serviceAccount: 'path/to/keyfile' * }) * const adapter = new CloudMineAdapter({ db: cloudmine.database() }) * store.registerAdapter('cloudmine', adapter, { 'default': true }) * * @class CloudMineAdapter * @param {Object} [opts] Configuration opts. * @param {Object} [opts.ws] See {@link CloudMineAdapter#ws} * @param {boolean} [opts.debug=false] See {@link Adapter#debug}. * @param {boolean} [opts.raw=false] See {@link Adapter#raw}. */ export function CloudMineAdapter (opts) { utils.classCallCheck(this, CloudMineAdapter) opts || (opts = {}) Adapter.call(this, opts) /** * The service instance used by this adapter. * * @name CloudMineAdapter#ws * @type {Object} */ this.ws = opts.ws } // Setup prototype inheritance from Adapter CloudMineAdapter.prototype = Object.create(Adapter.prototype, { constructor: { value: CloudMineAdapter, enumerable: false, writable: true, configurable: true } }) Object.defineProperty(CloudMineAdapter, '__super__', { configurable: true, value: Adapter }) /** * Alternative to ES6 class syntax for extending `CloudMineAdapter`. * * @example <caption>Using the ES2015 class syntax.</caption> * class MyCloudMineAdapter extends CloudMineAdapter {...} * const adapter = new MyCloudMineAdapter() * * @example <caption>Using {@link CloudMineAdapter.extend}.</caption> * var instanceProps = {...} * var classProps = {...} * * var MyCloudMineAdapter = CloudMineAdapter.extend(instanceProps, classProps) * var adapter = new MyCloudMineAdapter() * * @method CloudMineAdapter.extend * @static * @param {Object} [instanceProps] Properties that will be added to the * prototype of the subclass. * @param {Object} [classProps] Properties that will be added as static * properties to the subclass itself. * @return {Constructor} Subclass of `CloudMineAdapter`. */ CloudMineAdapter.extend = utils.extend utils.addHiddenPropsToTarget(CloudMineAdapter.prototype, { /** * Retrieve the number of records that match the selection query. Internal * method used by Adapter#count. * * @name CloudMineAdapter#_count * @method * @private * @param {Object} mapper The mapper. * @param {Object} query Selection query. * @param {Object} [opts] Configuration options. * @return {Promise} */ _count (mapper, query, opts) { query || (query = {}) opts || (opts = {}) query.__mapper__ = mapper.name query.count = true return this._findAll(mapper, query, opts).then((result) => { result[ 0 ] = result[ 1 ].count result[ 1 ].data = result[ 1 ].count return result }) }, /** * Create a new record. Internal method used by Adapter#create. * * @name CloudMineAdapter#_create * @method * @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._upsert(mapper, props, opts) }, _upsert (mapper, props, opts) { const _props = utils.plainCopy(props) opts || (opts = {}) let id = utils.get(_props, mapper.idAttribute) if (!utils.isSorN(id)) { id = null } _props.__mapper__ = mapper.name return APICallToPromise(this.ws.set(id, _props), opts) .then((result) => { let record = result[ 0 ] utils.set(_props, mapper.idAttribute, Object.keys(record)[ 0 ]) if (!record) { throw new Error('Not Found') } // Get rid of this since it is internal. delete _props.__mapper__ return [ _props ] }) }, _upsertBatch (mapper, records, opts) { opts || (opts = {}) const updates = [] // generate path for each records.forEach((record) => { updates.push(this._upsert(mapper, record, opts)) }) return utils.Promise.all(updates) .then((values) => { return values.map(item => item[ 0 ]) }) .then((records) => { return [ records ] }) }, /** * Create multiple records in a single batch. Internal method used by * Adapter#createMany. * * @name CloudMineAdapter#_createMany * @method * @private * @param {Object} mapper The mapper. * @param {Object} records The records to be created. * @param {Object} [opts] Configuration options. * @return {Promise} */ _createMany (mapper, records, opts) { opts || (opts = {}) return this._upsertBatch(mapper, records, opts) }, /** * Destroy the record with the given primary key. Internal method used by * Adapter#destroy. * * @name CloudMineAdapter#_destroy * @method * @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 APICallToPromise(this.ws.destroy(id), opts) .then(() => [ undefined, {} ]) .catch((error) => { console.error(error) return [ undefined, {} ] }) }, /** * Destroy the records that match the selection query. Internal method used by * Adapter#destroyAll. * * @name CloudMineAdapter#_destroyAll * @method * @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 = {}) // We need this to do bulk deletes using CloudMine. query.__mapper__ = mapper.name return this._findAll(mapper, query) .then((results) => { const [records] = results const idAttribute = mapper.idAttribute return utils.Promise.all(records.map((record) => { return this._destroy(mapper, utils.get(record, idAttribute), opts) })) }) .then(() => [ undefined, {} ]) }, /** * Retrieve the record with the given primary key. Internal method used by * Adapter#find. * * @name CloudMineAdapter#_find * @method * @private * @param {Object} mapper The mapper. * @param {(string|number)} id Primary key of the record to retrieve. * @param {Object} [opts] Configuration options. * @return {Promise} */ _find (mapper, id, opts) { opts || (opts = {}) return APICallToPromise(this.ws.get(id), opts) .then((result) => { let record = result[ 0 ][ id ] utils.set(record, mapper.idAttribute, id) delete record.__mapper__ result[ 0 ] = record return [ record, {} ] }) .catch(function (error) { console.error(error.stack) return [ undefined, {} ] }) }, /** * Retrieve the records that match the selection query. Internal method used * by Adapter#findAll. * * @name CloudMineAdapter#_findAll * @method * @private * @param {Object} mapper The mapper. * @param {Object} query Selection query. * @param {Object} [opts] Configuration options. * @return {Promise} */ _findAll (mapper, query, opts) { query || (query = {}) opts || (opts = {}) let queryopts = {} let realquery = [] let findAllPromise if (query.count !== undefined) { queryopts = { count: query.count } delete query.count } // We were passed a query. if (utils.isObject(query.where) && Object.keys(query.where).length === 1) { // The query contains the idAttribute of the mapper as an option. let op = Object.keys(query.where[Object.keys(query.where)[0]])[0] if (query.where.hasOwnProperty(mapper.idAttribute)) { // It's a number. Use .get() to grab it and return it. if (utils.isString(query.where[ mapper.idAttribute ])) { findAllPromise = APICallToPromise(this.ws.get(query.where[ mapper.idAttribute ]), opts) // It's not just a number, it's got an additional object. } else if (utils.isObject(query.where[ mapper.idAttribute ]) && (query.where[ mapper.idAttribute ].hasOwnProperty('in') || query.where[ mapper.idAttribute ].hasOwnProperty('=='))) { // If there is nothing here, don't bother. if (op === 'in' && query.where[ mapper.idAttribute ].length === 0) { return Promise.resolve([ undefined, {} ]) } findAllPromise = APICallToPromise(this.ws.get(query.where[ mapper.idAttribute ][ op ]), opts) } else { // There is a chance that the operation is unsupported because in some cases we end up having relations // with filters and the like, but it is possible that we want to do a query where we only want a record // returned if it has been updated (e.g. [id = "abc...", updated > 1234345]). In which case, we want search // to be used instead of delegating over to the regular operation. // So for now, we'll remove these warnings because they cause that functionality to be broken. // console.error('Unsupported operation.') // throw new Error('Unsupported operation.') } } else { // If we have a relation Field... var searchField = Object.keys(query.where)[0] if (mapper.hasOwnProperty('relationList')) { mapper.relationList.forEach((rel) => { if (rel.foreignKey === searchField && rel.mapper !== mapper) { findAllPromise = APICallToPromise(this.ws.get(query.where[ searchField ][ op ]), opts) } }) } if (mapper.hasOwnProperty('relationFields')) { if (mapper.relationFields.indexOf(Object.keys(query.where)[ 0 ]) !== false) { let fieldName = Object.keys(query.where)[ 0 ] let thisField mapper.relationList.forEach((def) => { if (def.localField === fieldName) { thisField = def } }) if (thisField) { // @todo this will really only work if the remote key is an ID let op = Object.keys(query.where[ fieldName ])[ 0 ] findAllPromise = APICallToPromise(this.ws.get(query.where[ fieldName ][ op ]), opts) } // Otherwise, we'll query this mapper normally. } } } // Note that 'in' or 'contains' with multiple values are not supported right now. // According to the CloudMine docs, a ws.search() request can only contain // ", " (and) or " or " (or) operations in a single request. // @see https://cloudmine.io/docs/#/javascript#searching-for-objects Object.keys(query.where).forEach((key) => { if (utils.isObject(query.where[ key ])) { Object.keys(query.where[ key ]).forEach((op) => { let value let realop = op === '==' ? '=' : op if (utils.isNumber(query.where[ key ][ op ])) { value = query.where[ key ][ op ] } else if (op === 'in' && utils.isArray(query.where[ key ][ op ])) { let inClause = [] query.where[ key ][ op ].forEach((item) => { inClause.push(utils.isNumber(item) ? `${key} = ${item}` : `${key} = "${item}"`) }) realquery.push(inClause.join(' or ')) return } else { value = `"${query.where[ key ][ op ]}"` } realquery.push(`${key} ${realop} ${value}`) }) } else { let value if (utils.isNumber(query.where[ key ])) { value = query.where[ key ] } else { value = `"${query.where[ key ]}"` } realquery.push(`${key} = ${value}`) } }) } // CloudMine has some interesting... quirks. As we noted above, you cannot mix // "and (,)" and "or" operators in the same search query. This is problematic when // loading relationships because we basically say, "give us every object of type X // where value = Y". And since CloudMine uses a field to keep track of object type, // loading multiple object types in the same query won't work because we or the search // and then add in the "and" for the __mapper__. // The workaround. When _activeWith is not undefined, we skip adding the __mapper__ to // the query and then filter the results manually once they get back. This way the query // doesn't get mixed and the results are what are desired. if (opts._activeWith === undefined) { query.__mapper__ = mapper.name } // Attempt to convert the JSData query object to something Cloudmine can understand... Object.keys(query).forEach((key) => { if (key === 'where') return if (utils.isObject(query[ key ])) { Object.keys(query[ key ]).forEach((op) => { let value let realop = op === '==' ? '=' : op if (utils.isNumber(query[ key ][ op ])) { value = query[ key ][ op ] } else { value = `"${query[ key ][ op ]}"` } realquery.push(`${key} ${realop} ${value}`) }) } else { let value if (utils.isNumber(query[ key ])) { value = query[ key ] } else { value = `"${query[ key ]}"` } realquery.push(`${key} = ${value}`) } }) if (!findAllPromise) { findAllPromise = APICallToPromise(this.ws.search('[' + realquery.join(', ') + ']', queryopts), opts) } return findAllPromise.then((result) => { let results = [] let records = result[ 0 ] for (let id in records) { let record = records[ id ] if (opts._activeWith !== undefined && record.__mapper__ !== mapper.name) { continue } utils.set(record, mapper.idAttribute, id) delete record.__mapper__ results.push(record) } result[ 0 ] = results delete result[ 1 ]._events return result }) }, /** * Retrieve the number of records that match the selection query. Internal * method used by Adapter#sum. * * @name CloudMineAdapter#_sum * @method * @private * @param {Object} mapper The mapper. * @param {string} field The field to sum. * @param {Object} query Selection query. * @param {Object} [opts] Configuration options. * @return {Promise} */ _sum (mapper, field, query, opts) { return this._findAll(mapper, query, opts).then((result) => { result[ 0 ] = result[ 0 ].reduce((sum, record) => sum + (utils.get(record, field) || 0), 0) return result }) }, /** * Apply the given update to the record with the specified primary key. * Internal method used by Adapter#update. * * @name CloudMineAdapter#_update * @method * @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) .then((currentVal) => { currentVal = currentVal[ 0 ] if (!currentVal) { throw new Error('Not Found') } utils.deepMixIn(currentVal, props) return this._upsert(mapper, currentVal, opts) }) .then((record) => { if (!record) { throw new Error('Not Found') } return record }) }, /** * Apply the given update to all records that match the selection query. * Internal method used by Adapter#updateAll. * * @name CloudMineAdapter#_updateAll * @method * @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) { opts || (opts = {}) props || (props = {}) query || (query = {}) return this._findAll(mapper, query, opts).then((results) => { const [records] = results records.forEach((record) => utils.deepMixIn(record, props)) return this._upsertBatch(mapper, records, opts) }) }, /** * Update the given records in a single batch. Internal method used by * Adapter#updateMany. * * @name CloudMineAdapter#updateMany * @method * @private * @param {Object} mapper The mapper. * @param {Object[]} records The records to update. * @param {Object} [opts] Configuration options. * @return {Promise} */ _updateMany (mapper, records, opts) { opts || (opts = {}) return this._upsertBatch(mapper, records, opts) }, create (mapper, props, opts) { return createTask((success, failure) => { queueTask(() => { __super__.create.call(this, mapper, props, opts).then(success, failure) }) }) }, createMany (mapper, props, opts) { return createTask((success, failure) => { queueTask(() => { __super__.createMany.call(this, mapper, props, opts).then(success, failure) }) }) }, destroy (mapper, id, opts) { return createTask((success, failure) => { queueTask(() => { __super__.destroy.call(this, mapper, id, opts).then(success, failure) }) }) }, destroyAll (mapper, query, opts) { return createTask((success, failure) => { queueTask(() => { __super__.destroyAll.call(this, mapper, query, opts).then(success, failure) }) }) }, update (mapper, id, props, opts) { return createTask((success, failure) => { queueTask(() => { __super__.update.call(this, mapper, id, props, opts).then(success, failure) }) }) }, updateAll (mapper, props, query, opts) { return createTask((success, failure) => { queueTask(() => { __super__.updateAll.call(this, mapper, props, query, opts).then(success, failure) }) }) }, updateMany (mapper, records, opts) { return createTask((success, failure) => { queueTask(() => { __super__.updateMany.call(this, mapper, records, opts).then(success, failure) }) }) } }) /** * Details of the current version of the `js-data-cloudmine` module. * * @name CloudMineAdapter.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 CloudMineAdapter} class. * * @name module:js-data-cloudmine.CloudMineAdapter * @see CloudMineAdapter */ /** * Registered as `js-data-cloudmine` in NPM and Bower. * * @example <caption>Script tag</caption> * var CloudMineAdapter = window.JSDataCloudMine.CloudMineAdapter * var adapter = new CloudMineAdapter() * * @example <caption>CommonJS</caption> * var CloudMineAdapter = require('js-data-cloudmine').CloudMineAdapter * var adapter = new CloudMineAdapter() * * @example <caption>ES2015 Modules</caption> * import {CloudMineAdapter} from 'js-data-cloudmine' * const adapter = new CloudMineAdapter() * * @example <caption>AMD</caption> * define('myApp', ['js-data-cloudmine'], function (JSDataCloudMine) { * var CloudMineAdapter = JSDataCloudMine.CloudMineAdapter * var adapter = new CloudMineAdapter() * * // ... * }) * * @module js-data-cloudmine */