vuexfire
Version:
Firestore binding for Vuex
665 lines (649 loc) • 25.5 kB
JavaScript
/*!
* vuexfire v3.2.5
* (c) 2020 Eduardo San Martin Morote
* @license MIT
*/
;
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;