UNPKG

vuexfire

Version:

Firestore binding for Vuex

665 lines (649 loc) 25.5 kB
/*! * vuexfire v3.2.5 * (c) 2020 Eduardo San Martin Morote * @license MIT */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var VUEXFIRE_SET_VALUE = 'vuexfire/SET_VALUE'; var VUEXFIRE_ARRAY_ADD = 'vuexfire/ARRAY_ADD'; var VUEXFIRE_ARRAY_REMOVE = 'vuexfire/ARRAY_REMOVE'; /** * Walks a path inside an object * walkGet({ a: { b: true }}), 'a.b') -> true * @param obj * @param path */ function walkGet(obj, path) { // TODO: development warning when target[key] does not exist return path.split('.').reduce(function (target, key) { return target[key]; }, obj); } /** * Deeply set a property in an object with a string path * walkSet({ a: { b: true }}, 'a.b', false) * @param obj * @param path * @param value * @returns an array with the element that was replaced or the value that was set */ function walkSet(obj, path, value) { // path can be a number var keys = ('' + path).split('.'); var key = keys.pop(); // split will produce at least one element array var target = keys.reduce(function (target, key) { // TODO: dev errors return target[key]; }, obj); return Array.isArray(target) ? target.splice(Number(key), 1, value) : (target[key] = value); } /** * Checks if a variable is an object * @param o */ function isObject(o) { return o && typeof o === 'object'; } /** * Checks if a variable is a Date * @param o */ function isTimestamp(o) { return o.toDate; } /** * Checks if a variable is a Firestore Document Reference * @param o */ function isDocumentRef(o) { return o && o.onSnapshot; } /** * Wraps a function so it gets called only once * @param fn Function to be called once * @param argFn Function to compute the argument passed to fn */ function callOnceWithArg(fn, argFn) { /** @type {boolean | undefined} */ var called = false; return function () { if (!called) { called = true; return fn(argFn()); } }; } /** * Convert firebase RTDB snapshot into a bindable data record. * * @param snapshot * @return */ function createRecordFromRTDBSnapshot(snapshot) { var value = snapshot.val(); var res = isObject(value) ? value : Object.defineProperty({}, '.value', { value: value }); // if (isObject(value)) { // res = value // } else { // res = {} // Object.defineProperty(res, '.value', { value }) // } Object.defineProperty(res, '.key', { value: snapshot.key }); return res; } /** * Find the index for an object with given key. * * @param array * @param key * @return the index where the key was found */ function indexForKey(array, key) { for (var i = 0; i < array.length; i++) { if (array[i]['.key'] === key) return i; } return -1; } var DEFAULT_OPTIONS = { reset: true, serialize: createRecordFromRTDBSnapshot, wait: false, }; /** * Binds a RTDB reference as an object * @param param0 * @param options * @returns a function to be called to stop listeninng for changes */ function rtdbBindAsObject(_a, extraOptions) { var vm = _a.vm, key = _a.key, document = _a.document, resolve = _a.resolve, reject = _a.reject, ops = _a.ops; if (extraOptions === void 0) { extraOptions = DEFAULT_OPTIONS; } var options = Object.assign({}, DEFAULT_OPTIONS, extraOptions); var listener = document.on('value', function (snapshot) { ops.set(vm, key, options.serialize(snapshot)); }, reject); document.once('value', resolve); return function (reset) { document.off('value', listener); if (reset !== false) { var value = typeof reset === 'function' ? reset() : null; ops.set(vm, key, value); } }; } /** * Binds a RTDB reference or query as an array * @param param0 * @param options * @returns a function to be called to stop listeninng for changes */ function rtdbBindAsArray(_a, extraOptions) { var vm = _a.vm, key = _a.key, collection = _a.collection, resolve = _a.resolve, reject = _a.reject, ops = _a.ops; if (extraOptions === void 0) { extraOptions = DEFAULT_OPTIONS; } var options = Object.assign({}, DEFAULT_OPTIONS, extraOptions); var array = options.wait ? [] : ops.set(vm, key, []); var childAdded = collection.on('child_added', function (snapshot, prevKey) { var index = prevKey ? indexForKey(array, prevKey) + 1 : 0; ops.add(array, index, options.serialize(snapshot)); }, reject); var childRemoved = collection.on('child_removed', function (snapshot) { ops.remove(array, indexForKey(array, snapshot.key)); }, reject); var childChanged = collection.on('child_changed', function (snapshot) { ops.set(array, indexForKey(array, snapshot.key), options.serialize(snapshot)); }, reject); var childMoved = collection.on('child_moved', function (snapshot, prevKey) { var index = indexForKey(array, snapshot.key); var oldRecord = ops.remove(array, index)[0]; var newIndex = prevKey ? indexForKey(array, prevKey) + 1 : 0; ops.add(array, newIndex, oldRecord); }, reject); collection.once('value', function (data) { if (options.wait) ops.set(vm, key, array); resolve(data); }); return function (reset) { collection.off('child_added', childAdded); collection.off('child_changed', childChanged); collection.off('child_removed', childRemoved); collection.off('child_moved', childMoved); if (reset !== false) { var value = typeof reset === 'function' ? reset() : []; ops.set(vm, key, value); } }; } // TODO: fix type not to be any function createSnapshot(doc) { // TODO: it should create a deep copy instead because otherwise we will modify internal data // defaults everything to false, so no need to set return Object.defineProperty(doc.data() || {}, 'id', { value: doc.id }); } function extractRefs(doc, oldDoc, subs) { var dataAndRefs = [{}, {}]; var subsByPath = Object.keys(subs).reduce(function (resultSubs, subKey) { var sub = subs[subKey]; resultSubs[sub.path] = sub.data(); return resultSubs; }, {}); function recursiveExtract(doc, oldDoc, path, result) { // make it easier to later on access the value oldDoc = oldDoc || {}; var data = result[0], refs = result[1]; // Add all properties that are not enumerable (not visible in the for loop) // getOwnPropertyDescriptors does not exist on IE Object.getOwnPropertyNames(doc).forEach(function (propertyName) { var descriptor = Object.getOwnPropertyDescriptor(doc, propertyName); if (descriptor && !descriptor.enumerable) { Object.defineProperty(data, propertyName, descriptor); } }); // recursively traverse doc to copy values and extract references for (var key in doc) { var ref = doc[key]; if ( // primitives ref == null || // Firestore < 4.13 ref instanceof Date || isTimestamp(ref) || (ref.longitude && ref.latitude) // GeoPoint ) { data[key] = ref; } else if (isDocumentRef(ref)) { // allow values to be null (like non-existant refs) // TODO: better typing since this isObject shouldn't be necessary but it doesn't work data[key] = typeof oldDoc === 'object' && key in oldDoc && // only copy refs if they were refs before // https://github.com/vuejs/vuefire/issues/831 typeof oldDoc[key] != 'string' ? oldDoc[key] : ref.path; // TODO: handle subpathes? refs[path + key] = ref; } else if (Array.isArray(ref)) { data[key] = Array(ref.length); // fill existing refs into data but leave the rest empty for (var i = 0; i < ref.length; i++) { var newRef = ref[i]; // TODO: this only works with array of primitives but not with nested properties like objects with References if (newRef && newRef.path in subsByPath) data[key][i] = subsByPath[newRef.path]; } // the oldArray is in this case the same array with holes unless the array already existed recursiveExtract(ref, oldDoc[key] || data[key], path + key + '.', [data[key], refs]); } else if (isObject(ref)) { data[key] = {}; recursiveExtract(ref, oldDoc[key], path + key + '.', [data[key], refs]); } else { data[key] = ref; } } } recursiveExtract(doc, oldDoc, '', dataAndRefs); return dataAndRefs; } var DEFAULT_OPTIONS$1 = { maxRefDepth: 2, reset: true, serialize: createSnapshot, wait: false, }; function unsubscribeAll(subs) { for (var sub in subs) { subs[sub].unsub(); } } function updateDataFromDocumentSnapshot(options, target, path, snapshot, subs, ops, depth, resolve) { var _a = extractRefs(options.serialize(snapshot), walkGet(target, path), subs), data = _a[0], refs = _a[1]; ops.set(target, path, data); subscribeToRefs(options, target, path, subs, refs, ops, depth, resolve); } function subscribeToDocument(_a, options) { var ref = _a.ref, target = _a.target, path = _a.path, depth = _a.depth, resolve = _a.resolve, ops = _a.ops; var subs = Object.create(null); var unbind = ref.onSnapshot(function (snapshot) { if (snapshot.exists) { updateDataFromDocumentSnapshot(options, target, path, snapshot, subs, ops, depth, resolve); } else { ops.set(target, path, null); resolve(); } }); return function () { unbind(); unsubscribeAll(subs); }; } // NOTE: not convinced by the naming of subscribeToRefs and subscribeToDocument // first one is calling the other on every ref and subscribeToDocument may call // updateDataFromDocumentSnapshot which may call subscribeToRefs as well function subscribeToRefs(options, target, path, subs, refs, ops, depth, resolve) { var refKeys = Object.keys(refs); var missingKeys = Object.keys(subs).filter(function (refKey) { return refKeys.indexOf(refKey) < 0; }); // unbind keys that are no longer there missingKeys.forEach(function (refKey) { subs[refKey].unsub(); delete subs[refKey]; }); if (!refKeys.length || ++depth > options.maxRefDepth) return resolve(path); var resolvedCount = 0; var totalToResolve = refKeys.length; var validResolves = Object.create(null); function deepResolve(key) { if (key in validResolves) { if (++resolvedCount >= totalToResolve) resolve(path); } } refKeys.forEach(function (refKey) { var sub = subs[refKey]; var ref = refs[refKey]; var docPath = path + "." + refKey; validResolves[docPath] = true; // unsubscribe if bound to a different ref if (sub) { if (sub.path !== ref.path) sub.unsub(); // if has already be bound and as we always walk the objects, it will work else return; } subs[refKey] = { data: function () { return walkGet(target, docPath); }, unsub: subscribeToDocument({ ref: ref, target: target, path: docPath, depth: depth, ops: ops, resolve: deepResolve.bind(null, docPath), }, options), path: ref.path, }; }); } // TODO: refactor without using an object to improve size like the other functions function bindCollection(_a, extraOptions) { var vm = _a.vm, key = _a.key, collection = _a.collection, ops = _a.ops, resolve = _a.resolve, reject = _a.reject; if (extraOptions === void 0) { extraOptions = DEFAULT_OPTIONS$1; } var options = Object.assign({}, DEFAULT_OPTIONS$1, extraOptions); // fill default values // TODO support pathes? nested.obj.list (walkSet) var array = options.wait ? [] : ops.set(vm, key, []); var originalResolve = resolve; var isResolved; // contain ref subscriptions of objects // arraySubs is a mirror of array var arraySubs = []; var change = { added: function (_a) { var newIndex = _a.newIndex, doc = _a.doc; arraySubs.splice(newIndex, 0, Object.create(null)); var subs = arraySubs[newIndex]; var _b = extractRefs(options.serialize(doc), undefined, subs), data = _b[0], refs = _b[1]; ops.add(array, newIndex, data); subscribeToRefs(options, array, newIndex, subs, refs, ops, 0, resolve.bind(null, doc)); }, modified: function (_a) { var oldIndex = _a.oldIndex, newIndex = _a.newIndex, doc = _a.doc; var subs = arraySubs[oldIndex]; var oldData = array[oldIndex]; var _b = extractRefs(options.serialize(doc), oldData, subs), data = _b[0], refs = _b[1]; // only move things around after extracting refs // only move things around after extracting refs arraySubs.splice(newIndex, 0, subs); ops.remove(array, oldIndex); ops.add(array, newIndex, data); subscribeToRefs(options, array, newIndex, subs, refs, ops, 0, resolve); }, removed: function (_a) { var oldIndex = _a.oldIndex; ops.remove(array, oldIndex); unsubscribeAll(arraySubs.splice(oldIndex, 1)[0]); }, }; var unbind = collection.onSnapshot(function (snapshot) { // console.log('pending', metadata.hasPendingWrites) // docs.forEach(d => console.log('doc', d, '\n', 'data', d.data())) // NOTE: this will only be triggered once and it will be with all the documents // from the query appearing as added // (https://firebase.google.com/docs/firestore/query-data/listen#view_changes_between_snapshots) var docChanges = /* istanbul ignore next */ typeof snapshot.docChanges === 'function' ? snapshot.docChanges() : /* istanbul ignore next to support firebase < 5*/ snapshot.docChanges; if (!isResolved && docChanges.length) { // isResolved is only meant to make sure we do the check only once isResolved = true; var count_1 = 0; var expectedItems_1 = docChanges.length; var validDocs_1 = Object.create(null); for (var i = 0; i < expectedItems_1; i++) { validDocs_1[docChanges[i].doc.id] = true; } resolve = function (_a) { var id = _a.id; if (id in validDocs_1) { if (++count_1 >= expectedItems_1) { // if wait is true, finally set the array if (options.wait) ops.set(vm, key, array); originalResolve(vm[key]); // reset resolve to noop resolve = function () { }; } } }; } docChanges.forEach(function (c) { change[c.type](c); }); // resolves when array is empty // since this can only happen once, there is no need to guard against it // being called multiple times if (!docChanges.length) { if (options.wait) ops.set(vm, key, array); resolve(array); } }, reject); return function (reset) { unbind(); if (reset !== false) { var value = typeof reset === 'function' ? reset() : []; ops.set(vm, key, value); } arraySubs.forEach(unsubscribeAll); }; } /** * Binds a Document to a property of vm * @param param0 * @param extraOptions */ function bindDocument(_a, extraOptions) { var vm = _a.vm, key = _a.key, document = _a.document, resolve = _a.resolve, reject = _a.reject, ops = _a.ops; if (extraOptions === void 0) { extraOptions = DEFAULT_OPTIONS$1; } var options = Object.assign({}, DEFAULT_OPTIONS$1, extraOptions); // fill default values // TODO: warning check if key exists? // const boundRefs = Object.create(null) var subs = Object.create(null); // bind here the function so it can be resolved anywhere // this is specially useful for refs resolve = callOnceWithArg(resolve, function () { return walkGet(vm, key); }); var unbind = document.onSnapshot(function (snapshot) { if (snapshot.exists) { updateDataFromDocumentSnapshot(options, vm, key, snapshot, subs, ops, 0, resolve); } else { ops.set(vm, key, null); resolve(null); } }, reject); return function (reset) { unbind(); if (reset !== false) { var value = typeof reset === 'function' ? reset() : null; ops.set(vm, key, value); } unsubscribeAll(subs); }; } var _a; var vuexfireMutations = (_a = {}, _a[VUEXFIRE_SET_VALUE] = function (state, _a) { var path = _a.path, target = _a.target, data = _a.data; walkSet(target, path, data); }, _a[VUEXFIRE_ARRAY_ADD] = function (state, _a) { var newIndex = _a.newIndex, data = _a.data, target = _a.target; target.splice(newIndex, 0, data); }, _a[VUEXFIRE_ARRAY_REMOVE] = function (state, _a) { var oldIndex = _a.oldIndex, target = _a.target; return target.splice(oldIndex, 1)[0]; }, _a); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var commitOptions = { root: true }; // Firebase binding var subscriptions = new WeakMap(); function bind(state, commit, key, ref, ops, options) { // TODO check ref is valid // TODO check defined in state var sub = subscriptions.get(commit); if (!sub) { sub = Object.create(null); subscriptions.set(commit, sub); } // unbind if ref is already bound if (key in sub) { unbind(commit, key, options && options.reset); } return new Promise(function (resolve, reject) { sub[key] = Array.isArray(state[key]) ? rtdbBindAsArray({ vm: state, key: key, collection: ref, ops: ops, resolve: resolve, reject: reject, }, options) : rtdbBindAsObject({ vm: state, key: key, document: ref, ops: ops, resolve: resolve, reject: reject, }, options); }); } function unbind(commit, key, reset) { var sub = subscriptions.get(commit); if (!sub || !sub[key]) return; // TODO dev check before sub[key](reset); delete sub[key]; } function firebaseAction(action) { return function firebaseEnhancedActionFn(context, payload) { // get the local state and commit. These may be bound to a module var state = context.state, commit = context.commit; var ops = { set: function (target, path, data) { commit(VUEXFIRE_SET_VALUE, { path: path, target: target, data: data, }, commitOptions); return data; }, add: function (target, newIndex, data) { return commit(VUEXFIRE_ARRAY_ADD, { target: target, newIndex: newIndex, data: data }, commitOptions); }, remove: function (target, oldIndex) { var data = target[oldIndex]; commit(VUEXFIRE_ARRAY_REMOVE, { target: target, oldIndex: oldIndex }, commitOptions); return [data]; }, }; return action.call(this, __assign(__assign({}, context), { bindFirebaseRef: function (key, ref, options) { return bind(state, commit, key, ref, ops, Object.assign({}, DEFAULT_OPTIONS, options)); }, unbindFirebaseRef: function (key, reset) { return unbind(commit, key, reset); } }), payload); }; } var commitOptions$1 = { root: true }; // Firebase binding var subscriptions$1 = new WeakMap(); function bind$1(state, commit, key, ref, ops, options) { // TODO: check ref is valid warning // TODO: check defined in state warning var sub = subscriptions$1.get(commit); if (!sub) { sub = Object.create(null); subscriptions$1.set(commit, sub); } // unbind if ref is already bound if (key in sub) { unbind$1(commit, key, options.wait ? (typeof options.reset === 'function' ? options.reset : false) : options.reset); } return new Promise(function (resolve, reject) { sub[key] = 'where' in ref ? bindCollection({ vm: state, key: key, collection: ref, ops: ops, resolve: resolve, reject: reject, }, options) : bindDocument({ vm: state, key: key, document: ref, ops: ops, resolve: resolve, reject: reject, }, options); }); } function unbind$1(commit, key, reset) { var sub = subscriptions$1.get(commit); if (!sub || !sub[key]) return; // TODO dev check before sub[key](reset); delete sub[key]; } function firestoreAction(action) { return function firestoreEnhancedActionFn(context, payload) { // get the local state and commit. These may be bound to a module var state = context.state, commit = context.commit; var ops = { set: function (target, path, data) { commit(VUEXFIRE_SET_VALUE, { path: path, target: target, data: data, }, commitOptions$1); return data; }, add: function (target, newIndex, data) { return commit(VUEXFIRE_ARRAY_ADD, { target: target, newIndex: newIndex, data: data }, commitOptions$1); }, remove: function (target, oldIndex) { var data = target[oldIndex]; commit(VUEXFIRE_ARRAY_REMOVE, { target: target, oldIndex: oldIndex }, commitOptions$1); return [data]; }, }; return action.call(this, __assign(__assign({}, context), { bindFirestoreRef: function (key, ref, options) { return bind$1(state, commit, key, // @ts-ignore ref, ops, Object.assign({}, DEFAULT_OPTIONS$1, options)); }, unbindFirestoreRef: function (key, reset) { return unbind$1(commit, key, reset); } }), payload); }; } exports.firebaseAction = firebaseAction; exports.firestoreAction = firestoreAction; exports.firestoreOptions = DEFAULT_OPTIONS$1; exports.rtdbOptions = DEFAULT_OPTIONS; exports.vuexfireMutations = vuexfireMutations;