UNPKG

firebase-mock

Version:

Firebase mock library for writing unit tests

316 lines (284 loc) 9.4 kB
'use strict'; var _ = require('./lodash'); var assert = require('assert'); var Promise = require('rsvp').Promise; var autoId = require('firebase-auto-ids'); var DocumentSnapshot = require('./firestore-document-snapshot'); var Queue = require('./queue').Queue; var utils = require('./utils'); var validate = require('./validators'); function MockFirestoreDocument(path, data, parent, name, CollectionReference) { this.ref = this; this.path = path || 'Mock://'; this.CollectionReference = CollectionReference; this.errs = {}; this.id = parent ? name : extractName(path); this.flushDelay = parent ? parent.flushDelay : false; this.queue = parent ? parent.queue : new Queue(); this.parent = parent || null; this.firestore = parent ? parent.firestore : null; this.children = {}; if (parent) parent.children[this.id] = this; this.data = null; this._dataChanged(_.cloneDeep(data) || null); } MockFirestoreDocument.prototype.flush = function (delay) { this.queue.flush(delay); return this; }; MockFirestoreDocument.prototype.autoFlush = function (delay) { if (_.isUndefined(delay)) { delay = true; } if (this.flushDelay !== delay) { this.flushDelay = delay; _.forEach(this.children, function (child) { child.autoFlush(delay); }); if (this.parent) { this.parent.autoFlush(delay); } } return this; }; MockFirestoreDocument.prototype.getFlushQueue = function () { return this.queue.getEvents(); }; MockFirestoreDocument.prototype._getData = function () { return _.cloneDeep(this.data); }; MockFirestoreDocument.prototype.toString = function () { return this.path; }; MockFirestoreDocument.prototype.collection = function (path) { assert(path, 'A child path is required'); var parts = _.compact(path.split('/')); var childKey = parts.shift(); var child = this.children[childKey]; if (!child) { child = new this.CollectionReference(utils.mergePaths(this.path, childKey), this._childData(childKey), this, childKey, MockFirestoreDocument); this.children[child.id] = child; } if (parts.length > 0) { child = child.doc(parts.join('/')); } return child; }; MockFirestoreDocument.prototype.get = function () { var err = this._nextErr('get'); var self = this; return new Promise(function (resolve, reject) { self._defer('get', _.toArray(arguments), function () { if (err === null) { resolve(new DocumentSnapshot(self.id, self.ref, self._getData())); } else { reject(err); } }); }); }; MockFirestoreDocument.prototype._validateDoesNotExist = function (data) { var ALREADY_EXISTS_CODE = 6; if (data !== null) { var err = new Error('Cannot create a document which already exists'); err.code = ALREADY_EXISTS_CODE; return err; } return null; }; MockFirestoreDocument.prototype.create = function (data, callback) { var err = this._nextErr('create'); data = _.cloneDeep(data); var self = this; return new Promise(function (resolve, reject) { self._defer('create', _.toArray(arguments), function () { var base = self._getData(); err = err || self._validateDoesNotExist(base); if (err === null) { self._dataChanged(data); resolve(); } else { if (callback) { callback(err); } reject(err); } }); }); }; MockFirestoreDocument.prototype.set = function (data, opts, callback) { var _opts = _.assign({}, { merge: false }, opts); if (_opts.merge) { return this._update(data, { setMerge: true }, callback); } var err = this._nextErr('set'); data = _.cloneDeep(data); var self = this; return new Promise(function (resolve, reject) { self._defer('set', _.toArray(arguments), function () { if (err === null) { self._dataChanged(data); resolve(); } else { if (callback) { callback(err); } reject(err); } }); }); }; MockFirestoreDocument.prototype._update = function (changes, opts, callback) { assert.equal(typeof changes, 'object', 'First argument must be an object when calling "update"'); var _opts = _.assign({}, { setMerge: false }, opts); var err = this._nextErr('update'); var self = this; return new Promise(function (resolve, reject) { self._defer('update', _.toArray(arguments), function () { if (!err) { var base = self._getData(); var original = _.cloneDeep(base); var data; if (_opts.setMerge) { data = _.merge(_.isObject(base) ? base : {}, changes); } else { // check if changes contain no nested objects if (_.every(Object.keys(changes), function(key) { return !_.isObject(changes[key]); })) { // allow data to be merged, which allows merging of nested data data = _.merge(_.isObject(base) ? base : {}, utils.updateToFirestoreObject(changes)); } else { // don't allow data to be merged, which overwrite nested data data = _.assign(_.isObject(base) ? base : {}, utils.updateToFirestoreObject(changes)); } } data = utils.removeEmptyFirestoreProperties(data, original); self._dataChanged(data); resolve(data); } else { if (callback) { callback(err); } reject(err); } }); }); }; MockFirestoreDocument.prototype.update = function (changes, callback) { return this._update(changes, { setMerge: false }, callback); }; MockFirestoreDocument.prototype.delete = function (callback) { var err = this._nextErr('delete'); var self = this; return new Promise(function (resolve, reject) { self._defer('delete', _.toArray(arguments), function () { if (callback) callback(err); if (err === null) { self._dataChanged(null); resolve(null); } else { reject(err); } }); }); }; MockFirestoreDocument.prototype.onSnapshot = function (optionsOrObserverOrOnNext, observerOrOnNextOrOnError, onErrorArg) { var err = this._nextErr('onSnapshot'); var self = this; var onNext = optionsOrObserverOrOnNext; var onError = observerOrOnNextOrOnError; var includeMetadataChanges = optionsOrObserverOrOnNext.includeMetadataChanges; if (includeMetadataChanges) { // Note this doesn't truly mimic the firestore metadata changes behavior, however // since everything is syncronous, there isn't any difference in behavior. onNext = observerOrOnNextOrOnError; onError = onErrorArg; } var context = { data: self._getData(), }; var onSnapshot = function (forceTrigger) { // compare the current state to the one from when this function was created // and send the data to the callback if different. if (err === null) { if (!_.isEqual(self.data, context.data) || includeMetadataChanges || forceTrigger) { onNext(new DocumentSnapshot(self.id, self.ref, self._getData())); context.data = self._getData(); } } else { onError(err); } }; // onSnapshot should always return when initially called, then // every time data changes. onSnapshot(true); var unsubscribe = this.queue.onPostFlush(onSnapshot); // return the unsubscribe function return unsubscribe; }; /** * Fetches the subcollections that are direct children of the document. * @see https://cloud.google.com/nodejs/docs/reference/firestore/0.15.x/DocumentReference#getCollections */ MockFirestoreDocument.prototype.getCollections = function () { var err = this._nextErr('getCollections'); var self = this; return new Promise(function (resolve, reject) { self._defer('getCollections', _.toArray(arguments), function () { if (err === null) { var collections = _.toArray(this.children); // Filter out empty collections collections = _.filter(collections, function (collection) { return !_.isEmpty(collection.data); }); resolve(collections); } else { reject(err); } }); }); }; MockFirestoreDocument.prototype._hasChild = function (key) { return _.isObject(this.data) && _.has(this.data, key); }; MockFirestoreDocument.prototype._childData = function (key) { return this._hasChild(key) ? this.data[key] : null; }; MockFirestoreDocument.prototype._dataChanged = function (unparsedData) { this.data = utils.cleanFirestoreData(unparsedData); if (this.parent) { if (this.data) { this.parent.data = this.parent.data || {}; this.parent.data[this.id] = this.data; } else { if (this.parent.data) { delete this.parent.data[this.id]; } if (utils.cleanFirestoreData(this.parent.data) === null) { this.parent.data = null; } } } }; MockFirestoreDocument.prototype._defer = function (sourceMethod, sourceArgs, callback) { this.queue.push({ fn: callback, context: this, sourceData: { ref: this, method: sourceMethod, args: sourceArgs } }); if (this.flushDelay !== false) { this.flush(this.flushDelay); } }; MockFirestoreDocument.prototype._nextErr = function (type) { var err = this.errs[type]; delete this.errs[type]; return err || null; }; function extractName(path) { return ((path || '').match(/\/([^.$\[\]#\/]+)$/) || [null, null])[1]; } module.exports = MockFirestoreDocument;