UNPKG

js-data-firebase-admin

Version:
594 lines (540 loc) 17 kB
import {Query, utils} from 'js-data' import {Adapter} from '../node_modules/js-data-adapter/src/index' import firebase from 'firebase-admin' 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) }) } const __super__ = Adapter.prototype /** * FirebaseAdminAdapter class. * * @example <caption>Browser</caption> * import {DataStore} from 'js-data' * import firebase from 'firebase' * import {FirebaseAdminAdapter} from 'js-data-firebase' * const store = new DataStore() * firebase.initializeApp({ * apiKey: 'your-api-key', * databaseURL: 'your-database-url' * }) * const adapter = new FirebaseAdminAdapter({ db: firebase.database() }) * store.registerAdapter('firebase', adapter, { 'default': true }) * * @example <caption>Node.js</caption> * import {Container} from 'js-data' * import firebase from 'firebase' * import {FirebaseAdminAdapter} from 'js-data-firebase' * const store = new Container() * firebase.initializeApp({ * databaseURL: 'your-database-url', * serviceAccount: 'path/to/keyfile' * }) * const adapter = new FirebaseAdminAdapter({ db: firebase.database() }) * store.registerAdapter('firebase', adapter, { 'default': true }) * * @class FirebaseAdminAdapter * @param {Object} [opts] Configuration opts. * @param {Object} [opts.db] See {@link FirebaseAdminAdapter#db} * @param {boolean} [opts.debug=false] See {@link Adapter#debug}. * @param {boolean} [opts.raw=false] See {@link Adapter#raw}. */ export function FirebaseAdminAdapter (opts) { utils.classCallCheck(this, FirebaseAdminAdapter) opts || (opts = {}) Adapter.call(this, opts) /** * The database instance used by this adapter. * * @name FirebaseAdminAdapter#db * @type {Object} * @default firebase.database() */ if (opts.db) { this.db = opts.db || firebase.database() } } // Setup prototype inheritance from Adapter FirebaseAdminAdapter.prototype = Object.create(Adapter.prototype, { constructor: { value: FirebaseAdminAdapter, enumerable: false, writable: true, configurable: true } }) Object.defineProperty(FirebaseAdminAdapter, '__super__', { configurable: true, value: Adapter }) /** * Alternative to ES6 class syntax for extending `FirebaseAdminAdapter`. * * @example <caption>Using the ES2015 class syntax.</caption> * class MyFirebaseAdminAdapter extends FirebaseAdminAdapter {...} * const adapter = new MyFirebaseAdminAdapter() * * @example <caption>Using {@link FirebaseAdminAdapter.extend}.</caption> * var instanceProps = {...} * var classProps = {...} * * var MyFirebaseAdminAdapter = FirebaseAdminAdapter.extend(instanceProps, classProps) * var adapter = new MyFirebaseAdminAdapter() * * @method FirebaseAdminAdapter.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 `FirebaseAdminAdapter`. */ FirebaseAdminAdapter.extend = utils.extend utils.addHiddenPropsToTarget(FirebaseAdminAdapter.prototype, { /** * Retrieve the number of records that match the selection query. Internal * method used by Adapter#count. * * @name FirebaseAdminAdapter#_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 = {}) return this._findAll(mapper, query, opts).then((result) => { result[0] = result[0].length return result }) }, /** * Create a new record. Internal method used by Adapter#create. * * @name FirebaseAdminAdapter#_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 = {}) const id = utils.get(_props, mapper.idAttribute) const collectionRef = this.getRef(mapper, opts) let itemRef if (utils.isSorN(id)) { itemRef = collectionRef.child(id) } else { itemRef = collectionRef.push() utils.set(_props, mapper.idAttribute, itemRef.key) } return itemRef.set(_props) .then(() => this._once(itemRef)) .then((record) => { if (!record) { throw new Error('Not Found') } return [record, { ref: itemRef }] }) }, _upsertBatch (mapper, records, opts) { opts || (opts = {}) const idAttribute = mapper.idAttribute const refValueCollection = [] const collectionRef = this.getRef(mapper, opts) // generate path for each records.forEach((record) => { const id = utils.get(record, idAttribute) let _props = utils.plainCopy(record) let itemRef if (utils.isSorN(id)) { itemRef = collectionRef.child(id) } else { itemRef = collectionRef.push() utils.set(_props, idAttribute, itemRef.key) } refValueCollection.push({ ref: itemRef, props: _props }) }) return this._atomicUpdate(mapper, refValueCollection, opts) .then(() => { // since UDFs and timestamps can alter values on write, let's get the latest values return utils.Promise.all(refValueCollection.map((item) => this._once(item.ref))) }) .then((records) => { // just return the updated records and not the refs? return [records, { ref: refValueCollection.map((item) => item.ref) }] }) }, _once (ref) { return ref.once('value').then((dataSnapshot) => { if (!dataSnapshot.exists()) { return null } return dataSnapshot.val() }) }, _atomicUpdate (mapper, refValueCollection, opts) { // collection of refs and the new value to set at that ref // do a deep-path update off the database // see https://www.firebase.com/blog/2015-09-24-atomic-writes-and-more.html let atomicUpdate = {} refValueCollection.forEach((item) => { atomicUpdate[item.ref.toString().replace(this.getRef(mapper, opts).toString(), '')] = item.props }) return this.getRef(mapper, opts).update(atomicUpdate) }, /** * Create multiple records in a single batch. Internal method used by * Adapter#createMany. * * @name FirebaseAdminAdapter#_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 FirebaseAdminAdapter#_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 = {}) const ref = this.getRef(mapper, opts).child(id) return ref.remove().then(() => [undefined, { ref }]) }, /** * Destroy the records that match the selection query. Internal method used by * Adapter#destroyAll. * * @name FirebaseAdminAdapter#_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 = {}) 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 FirebaseAdminAdapter#_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 = {}) const itemRef = this.getRef(mapper, opts).child(id) return this._once(itemRef).then((record) => { if (!record) { record = undefined; } return [record, { ref: itemRef }] }) }, /** * Retrieve the records that match the selection query. Internal method used * by Adapter#findAll. * * @name FirebaseAdminAdapter#_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 = {}) const collectionRef = this.getRef(mapper, opts) return collectionRef.once('value').then((dataSnapshot) => { const data = dataSnapshot.val() if (!data) { return [[], { ref: collectionRef }] } const records = [] utils.forOwn(data, (value, key) => { records.push(value) }) const _query = new Query({ index: { getAll () { return records } } }) return [_query.filter(query).run(), { ref: collectionRef }] }) }, /** * Retrieve the number of records that match the selection query. Internal * method used by Adapter#sum. * * @name FirebaseAdminAdapter#_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 FirebaseAdminAdapter#_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 = {}) const itemRef = this.getRef(mapper, opts).child(id) return this._once(itemRef) .then((currentVal) => { if (!currentVal) { throw new Error('Not Found') } utils.deepMixIn(currentVal, props) return itemRef.set(currentVal) }) .then(() => this._once(itemRef)) .then((record) => { if (!record) { throw new Error('Not Found') } return [record, { ref: itemRef }] }) }, /** * Apply the given update to all records that match the selection query. * Internal method used by Adapter#updateAll. * * @name FirebaseAdminAdapter#_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 FirebaseAdminAdapter#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) }, getRef (mapper, opts) { opts = opts || {} return this.db.ref().child(opts.endpoint || mapper.endpoint || mapper.name) }, 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-firebase` module. * * @name FirebaseAdminAdapter.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 FirebaseAdminAdapter} class. * * @name module:js-data-firebase.FirebaseAdminAdapter * @see FirebaseAdminAdapter */ /** * Registered as `js-data-firebase` in NPM and Bower. * * @example <caption>Script tag</caption> * var FirebaseAdminAdapter = window.JSDataFirebase.FirebaseAdminAdapter * var adapter = new FirebaseAdminAdapter() * * @example <caption>CommonJS</caption> * var FirebaseAdminAdapter = require('js-data-firebase').FirebaseAdminAdapter * var adapter = new FirebaseAdminAdapter() * * @example <caption>ES2015 Modules</caption> * import {FirebaseAdminAdapter} from 'js-data-firebase' * const adapter = new FirebaseAdminAdapter() * * @example <caption>AMD</caption> * define('myApp', ['js-data-firebase'], function (JSDataFirebase) { * var FirebaseAdminAdapter = JSDataFirebase.FirebaseAdminAdapter * var adapter = new FirebaseAdminAdapter() * * // ... * }) * * @module js-data-firebase */