diff --git a/dist/index.cjs.js b/dist/index.cjs.js index c5697767..9ccac511 100644 --- a/dist/index.cjs.js +++ b/dist/index.cjs.js @@ -9,6 +9,7 @@ var Firebase = require('firebase/app'); var vuexEasyAccess = require('vuex-easy-access'); var merge = _interopDefault(require('merge-anything')); var findAndReplaceAnything = require('find-and-replace-anything'); +var filter = _interopDefault(require('filter-anything')); require('@firebase/firestore'); @@ -62,6 +63,7 @@ var defaultConfig = { orderBy: [], fillables: [], guard: [], + defaultValues: {}, // HOOKS for local changes: insertHook: function (updateStore, doc, store) { return updateStore(doc); }, patchHook: function (updateStore, doc, store) { return updateStore(doc); }, @@ -74,10 +76,11 @@ var defaultConfig = { // When items on the server side are changed: serverChange: { defaultValues: {}, + convertTimestamps: {}, // HOOKS for changes on SERVER: - addedHook: function (updateStore, doc, id, store, source, change) { return updateStore(doc); }, - modifiedHook: function (updateStore, doc, id, store, source, change) { return updateStore(doc); }, - removedHook: function (updateStore, doc, id, store, source, change) { return updateStore(doc); }, + addedHook: function (updateStore, doc, id, store) { return updateStore(doc); }, + modifiedHook: function (updateStore, doc, id, store) { return updateStore(doc); }, + removedHook: function (updateStore, doc, id, store) { return updateStore(doc); }, }, // When items are fetched through `dispatch('module/fetch', filters)`. fetch: { @@ -588,17 +591,17 @@ function stringifyParams(params) { }).join(); } /** - * Gets an object with {whereFilters, orderBy} filters and returns a unique identifier for that + * Gets an object with {where, orderBy} filters and returns a unique identifier for that * * @export - * @param {AnyObject} [whereOrderBy={}] whereOrderBy {whereFilters, orderBy} + * @param {AnyObject} [whereOrderBy={}] whereOrderBy {where, orderBy} * @returns {string} */ function createFetchIdentifier(whereOrderBy) { if (whereOrderBy === void 0) { whereOrderBy = {}; } var identifier = ''; - if ('whereFilters' in whereOrderBy) { - identifier += '[where]' + whereOrderBy.whereFilters.map(function (where) { return stringifyParams(where); }).join(); + if ('where' in whereOrderBy) { + identifier += '[where]' + whereOrderBy.where.map(function (where) { return stringifyParams(where); }).join(); } if ('orderBy' in whereOrderBy) { identifier += '[orderBy]' + stringifyParams(whereOrderBy.orderBy); @@ -707,6 +710,20 @@ function pluginActions (Firebase$$1) { return console.error('[vuex-easy-firestore] ids needs to be an array'); if (id) ids.push(id); + // EXTRA: check if doc is being inserted if so + state._sync.syncStack.inserts.forEach(function (newDoc, newDocIndex) { + // get the index of the id that is also in the insert stack + var indexIdInInsert = ids.indexOf(newDoc.id); + if (indexIdInInsert === -1) + return; + // the doc trying to be synced is also in insert + // prepare the doc as new doc: + var patchDoc = getters.prepareForInsert([doc])[0]; + // replace insert sync stack with merged item: + state._sync.syncStack.inserts[newDocIndex] = merge(newDoc, patchDoc); + // empty out the id that was to be patched: + ids.splice(indexIdInInsert, 1); + }); // 1. Prepare for patching var syncStackItems = getters.prepareForPatch(ids, doc); // 2. Push to syncStack @@ -810,26 +827,28 @@ function pluginActions (Firebase$$1) { }); }, fetch: function (_a, _b - // whereFilters: [['archived', '==', true]] + // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] ) { var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; - var _c = _b === void 0 ? { whereFilters: [], orderBy: [] } : _b - // whereFilters: [['archived', '==', true]] + var _c = _b === void 0 ? { where: [], whereFilters: [], orderBy: [] } : _b + // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] - , _d = _c.whereFilters, whereFilters = _d === void 0 ? [] : _d, _e = _c.orderBy, orderBy = _e === void 0 ? [] : _e; + , _d = _c.where, where = _d === void 0 ? [] : _d, _e = _c.whereFilters, whereFilters = _e === void 0 ? [] : _e, _f = _c.orderBy, orderBy = _f === void 0 ? [] : _f; + if (whereFilters.length) + where = whereFilters; return new Promise(function (resolve, reject) { if (state._conf.logging) console.log('[vuex-easy-firestore] Fetch starting'); if (!getters.signedIn) return resolve(); - var identifier = createFetchIdentifier({ whereFilters: whereFilters, orderBy: orderBy }); + var identifier = createFetchIdentifier({ where: where, orderBy: orderBy }); var fetched = state._sync.fetched[identifier]; // We've never fetched this before: if (!fetched) { var ref_1 = getters.dbRef; // apply where filters and orderBy - whereFilters.forEach(function (paramsArr) { + getters.getWhereArrays(where).forEach(function (paramsArr) { ref_1 = ref_1.where.apply(ref_1, paramsArr); }); if (orderBy.length) { @@ -886,22 +905,26 @@ function pluginActions (Firebase$$1) { }); }, fetchAndAdd: function (_a, _b - // whereFilters: [['archived', '==', true]] + // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] ) { var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; - var _c = _b === void 0 ? { whereFilters: [], orderBy: [] } : _b - // whereFilters: [['archived', '==', true]] + var _c = _b === void 0 ? { where: [], whereFilters: [], orderBy: [] } : _b + // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] - , _d = _c.whereFilters, whereFilters = _d === void 0 ? [] : _d, _e = _c.orderBy, orderBy = _e === void 0 ? [] : _e; - return dispatch('fetch', { whereFilters: whereFilters, orderBy: orderBy }) + , _d = _c.where, where = _d === void 0 ? [] : _d, _e = _c.whereFilters, whereFilters = _e === void 0 ? [] : _e, _f = _c.orderBy, orderBy = _f === void 0 ? [] : _f; + if (whereFilters.length) + where = whereFilters; + return dispatch('fetch', { where: where, orderBy: orderBy }) .then(function (querySnapshot) { if (querySnapshot.done === true) return querySnapshot; if (isWhat.isFunction(querySnapshot.forEach)) { querySnapshot.forEach(function (_doc) { var id = _doc.id; - var doc = setDefaultValues(_doc.data(), state._conf.serverChange.defaultValues); + var defaultValues = merge(state._conf.sync.defaultValues, state._conf.serverChange.defaultValues, // depreciated + state._conf.serverChange.convertTimestamps); + var doc = setDefaultValues(_doc.data(), defaultValues); doc.id = id; commit('INSERT_DOC', doc); }); @@ -935,18 +958,11 @@ function pluginActions (Firebase$$1) { delete pathVariables.orderBy; commit('SET_PATHVARS', pathVariables); } - // get userId - var userId = null; - if (Firebase$$1.auth().currentUser) { - state._sync.signedIn = true; - userId = Firebase$$1.auth().currentUser.uid; - state._sync.userId = userId; - } // getters.dbRef should already have pathVariables swapped out var dbRef = getters.dbRef; // apply where filters and orderBy if (getters.collectionMode) { - getters.whereFilters.forEach(function (whereParams) { + getters.getWhereArrays().forEach(function (whereParams) { dbRef = dbRef.where.apply(dbRef, whereParams); }); if (state._conf.sync.orderBy.length) { @@ -986,8 +1002,10 @@ function pluginActions (Firebase$$1) { } if (source === 'local') return resolve(); - var doc = setDefaultValues(querySnapshot.data(), state._conf.serverChange.defaultValues); - var id = getters.firestorePathComplete.split('/').pop(); + var defaultValues = merge(state._conf.sync.defaultValues, state._conf.serverChange.defaultValues, // depreciated + state._conf.serverChange.convertTimestamps); + var doc = setDefaultValues(querySnapshot.data(), defaultValues); + var id = getters.docModeId; doc.id = id; handleDoc('modified', id, doc); return resolve(); @@ -998,8 +1016,10 @@ function pluginActions (Firebase$$1) { if (source === 'local') return resolve(); var id = change.doc.id; + var defaultValues = merge(state._conf.sync.defaultValues, state._conf.serverChange.defaultValues, // depreciated + state._conf.serverChange.convertTimestamps); var doc = (changeType === 'added') - ? setDefaultValues(change.doc.data(), state._conf.serverChange.defaultValues) + ? setDefaultValues(change.doc.data(), defaultValues) : change.doc.data(); handleDoc(changeType, id, doc); }); @@ -1045,6 +1065,8 @@ function pluginActions (Firebase$$1) { var newDoc = doc; if (!newDoc.id) newDoc.id = getters.dbRef.doc().id; + // apply default values + var newDocWithDefaults = setDefaultValues(newDoc, state._conf.sync.defaultValues); // define the store update function storeUpdateFn(_doc) { commit('INSERT_DOC', _doc); @@ -1052,11 +1074,11 @@ function pluginActions (Firebase$$1) { } // check for hooks if (state._conf.sync.insertHook) { - state._conf.sync.insertHook(storeUpdateFn, newDoc, store); - return newDoc.id; + state._conf.sync.insertHook(storeUpdateFn, newDocWithDefaults, store); + return newDocWithDefaults.id; } - storeUpdateFn(newDoc); - return newDoc.id; + storeUpdateFn(newDocWithDefaults); + return newDocWithDefaults.id; }, insertBatch: function (_a, docs) { var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; @@ -1241,61 +1263,6 @@ function flattenToPaths (object) { return retrievePaths(object, null, result); } -function recursiveCheck(obj, fillables, guard, pathUntilNow) { - if (pathUntilNow === void 0) { pathUntilNow = ''; } - if (!isWhat.isPlainObject(obj)) { - console.log('obj → ', obj); - return obj; - } - return Object.keys(obj).reduce(function (carry, key) { - var path = pathUntilNow; - if (path) - path += '.'; - path += key; - // check guard regardless - if (guard.includes(path)) { - return carry; - } - var value = obj[key]; - // check fillables up to this point - if (fillables.length) { - var passed_1 = false; - fillables.forEach(function (fillable) { - var pathDepth = path.split('.').length; - var fillableDepth = fillable.split('.').length; - var fillableUpToNow = fillable.split('.').slice(0, pathDepth).join('.'); - var pathUpToFillableDepth = path.split('.').slice(0, fillableDepth).join('.'); - if (fillableUpToNow === pathUpToFillableDepth) - passed_1 = true; - }); - // there's not one fillable that allows up to now - if (!passed_1) - return carry; - } - // no fillables or fillables up to now allow it - if (!isWhat.isPlainObject(value)) { - carry[key] = value; - return carry; - } - carry[key] = recursiveCheck(obj[key], fillables, guard, path); - return carry; - }, {}); -} -/** - * Checks all props of an object and deletes guarded and non-fillables. - * - * @export - * @param {object} obj the target object to check - * @param {string[]} [fillables=[]] an array of strings, with the props which should be allowed on returned object - * @param {string[]} [guard=[]] an array of strings, with the props which should NOT be allowed on returned object - * @returns {AnyObject} the cleaned object after deleting guard and non-fillables - */ -function checkFillables (obj, fillables, guard) { - if (fillables === void 0) { fillables = []; } - if (guard === void 0) { guard = []; } - return recursiveCheck(obj, fillables, guard); -} - /** * A function returning the getters object * @@ -1343,6 +1310,9 @@ function pluginGetters (Firebase$$1) { collectionMode: function (state, getters, rootState) { return (state._conf.firestoreRefType.toLowerCase() === 'collection'); }, + docModeId: function (state, getters) { + return getters.firestorePathComplete.split('/').pop(); + }, prepareForPatch: function (state, getters, rootState, rootGetters) { return function (ids, doc) { if (ids === void 0) { ids = []; } @@ -1350,7 +1320,7 @@ function pluginGetters (Firebase$$1) { // get relevant data from the storeRef var collectionMode = getters.collectionMode; if (!collectionMode) - ids.push('singleDoc'); + ids.push(getters.docModeId); // returns {object} -> {id: data} return ids.reduce(function (carry, id) { var patchData = {}; @@ -1380,7 +1350,7 @@ function pluginGetters (Firebase$$1) { fillables = fillables.concat(['updated_at', 'updated_by']); var guard = state._conf.sync.guard.concat(['_conf', '_sync']); // clean up item - var cleanedPatchData = checkFillables(patchData, fillables, guard); + var cleanedPatchData = filter(patchData, fillables, guard); var itemToUpdate = flattenToPaths(cleanedPatchData); // add id (required to get ref later at apiHelpers.ts) itemToUpdate.id = id; @@ -1404,7 +1374,7 @@ function pluginGetters (Firebase$$1) { fillables = fillables.concat(['updated_at', 'updated_by']); var guard = state._conf.sync.guard.concat(['_conf', '_sync']); // clean up item - var cleanedPatchData = checkFillables(patchData, fillables, guard); + var cleanedPatchData = filter(patchData, fillables, guard); // add id (required to get ref later at apiHelpers.ts) var id, cleanedPath; if (collectionMode) { @@ -1412,7 +1382,7 @@ function pluginGetters (Firebase$$1) { cleanedPath = path.substring(path.indexOf('.') + 1); } else { - id = 'singleDoc'; + id = getters.docModeId; cleanedPath = path; } cleanedPatchData[cleanedPath] = Firebase$$1.firestore.FieldValue.delete(); @@ -1433,7 +1403,7 @@ function pluginGetters (Firebase$$1) { item.created_at = Firebase$$1.firestore.FieldValue.serverTimestamp(); item.created_by = state._sync.userId; // clean up item - item = checkFillables(item, fillables, guard); + item = filter(item, fillables, guard); carry.push(item); return carry; }, []); @@ -1450,12 +1420,17 @@ function pluginGetters (Firebase$$1) { doc.created_at = Firebase$$1.firestore.FieldValue.serverTimestamp(); doc.created_by = state._sync.userId; // clean up item - doc = checkFillables(doc, fillables, guard); + doc = filter(doc, fillables, guard); return doc; }; }, - whereFilters: function (state, getters) { - var whereArrays = state._conf.sync.where; + getWhereArrays: function (state, getters) { return function (whereArrays) { + if (!isWhat.isArray(whereArrays)) + whereArrays = state._conf.sync.where; + if (Firebase$$1.auth().currentUser) { + state._sync.signedIn = true; + state._sync.userId = Firebase$$1.auth().currentUser.uid; + } return whereArrays.map(function (whereClause) { return whereClause.map(function (param) { if (!isWhat.isString(param)) @@ -1481,7 +1456,7 @@ function pluginGetters (Firebase$$1) { return cleanedParam; }); }); - }, + }; }, }; } @@ -1506,7 +1481,7 @@ function errorCheck (config) { if (/\./.test(config.moduleName)) { errors.push("moduleName must only include letters from [a-z] and forward slashes '/'"); } - var syncProps = ['where', 'orderBy', 'fillables', 'guard', 'insertHook', 'patchHook', 'deleteHook', 'insertBatchHook', 'patchBatchHook', 'deleteBatchHook']; + var syncProps = ['where', 'orderBy', 'fillables', 'guard', 'defaultValues', 'insertHook', 'patchHook', 'deleteHook', 'insertBatchHook', 'patchBatchHook', 'deleteBatchHook']; syncProps.forEach(function (prop) { if (config[prop]) { errors.push("We found `" + prop + "` on your module, are you sure this shouldn't be inside a prop called `sync`?"); @@ -1541,7 +1516,7 @@ function errorCheck (config) { var objectProps = ['sync', 'serverChange', 'defaultValues', 'fetch']; objectProps.forEach(function (prop) { var _prop = (prop === 'defaultValues') - ? config.serverChange[prop] + ? config.sync[prop] : config[prop]; if (!isWhat.isPlainObject(_prop)) errors.push("`" + prop + "` should be an Object, but is not."); diff --git a/dist/index.esm.js b/dist/index.esm.js index 4567b5fa..318fc156 100644 --- a/dist/index.esm.js +++ b/dist/index.esm.js @@ -4,6 +4,7 @@ import { firestore } from 'firebase/app'; import { getDeepRef, getKeysFromPath } from 'vuex-easy-access'; import merge from 'merge-anything'; import { findAndReplace, findAndReplaceIf } from 'find-and-replace-anything'; +import filter from 'filter-anything'; require('@firebase/firestore'); @@ -57,6 +58,7 @@ var defaultConfig = { orderBy: [], fillables: [], guard: [], + defaultValues: {}, // HOOKS for local changes: insertHook: function (updateStore, doc, store) { return updateStore(doc); }, patchHook: function (updateStore, doc, store) { return updateStore(doc); }, @@ -69,10 +71,11 @@ var defaultConfig = { // When items on the server side are changed: serverChange: { defaultValues: {}, + convertTimestamps: {}, // HOOKS for changes on SERVER: - addedHook: function (updateStore, doc, id, store, source, change) { return updateStore(doc); }, - modifiedHook: function (updateStore, doc, id, store, source, change) { return updateStore(doc); }, - removedHook: function (updateStore, doc, id, store, source, change) { return updateStore(doc); }, + addedHook: function (updateStore, doc, id, store) { return updateStore(doc); }, + modifiedHook: function (updateStore, doc, id, store) { return updateStore(doc); }, + removedHook: function (updateStore, doc, id, store) { return updateStore(doc); }, }, // When items are fetched through `dispatch('module/fetch', filters)`. fetch: { @@ -583,17 +586,17 @@ function stringifyParams(params) { }).join(); } /** - * Gets an object with {whereFilters, orderBy} filters and returns a unique identifier for that + * Gets an object with {where, orderBy} filters and returns a unique identifier for that * * @export - * @param {AnyObject} [whereOrderBy={}] whereOrderBy {whereFilters, orderBy} + * @param {AnyObject} [whereOrderBy={}] whereOrderBy {where, orderBy} * @returns {string} */ function createFetchIdentifier(whereOrderBy) { if (whereOrderBy === void 0) { whereOrderBy = {}; } var identifier = ''; - if ('whereFilters' in whereOrderBy) { - identifier += '[where]' + whereOrderBy.whereFilters.map(function (where) { return stringifyParams(where); }).join(); + if ('where' in whereOrderBy) { + identifier += '[where]' + whereOrderBy.where.map(function (where) { return stringifyParams(where); }).join(); } if ('orderBy' in whereOrderBy) { identifier += '[orderBy]' + stringifyParams(whereOrderBy.orderBy); @@ -702,6 +705,20 @@ function pluginActions (Firebase$$1) { return console.error('[vuex-easy-firestore] ids needs to be an array'); if (id) ids.push(id); + // EXTRA: check if doc is being inserted if so + state._sync.syncStack.inserts.forEach(function (newDoc, newDocIndex) { + // get the index of the id that is also in the insert stack + var indexIdInInsert = ids.indexOf(newDoc.id); + if (indexIdInInsert === -1) + return; + // the doc trying to be synced is also in insert + // prepare the doc as new doc: + var patchDoc = getters.prepareForInsert([doc])[0]; + // replace insert sync stack with merged item: + state._sync.syncStack.inserts[newDocIndex] = merge(newDoc, patchDoc); + // empty out the id that was to be patched: + ids.splice(indexIdInInsert, 1); + }); // 1. Prepare for patching var syncStackItems = getters.prepareForPatch(ids, doc); // 2. Push to syncStack @@ -805,26 +822,28 @@ function pluginActions (Firebase$$1) { }); }, fetch: function (_a, _b - // whereFilters: [['archived', '==', true]] + // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] ) { var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; - var _c = _b === void 0 ? { whereFilters: [], orderBy: [] } : _b - // whereFilters: [['archived', '==', true]] + var _c = _b === void 0 ? { where: [], whereFilters: [], orderBy: [] } : _b + // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] - , _d = _c.whereFilters, whereFilters = _d === void 0 ? [] : _d, _e = _c.orderBy, orderBy = _e === void 0 ? [] : _e; + , _d = _c.where, where = _d === void 0 ? [] : _d, _e = _c.whereFilters, whereFilters = _e === void 0 ? [] : _e, _f = _c.orderBy, orderBy = _f === void 0 ? [] : _f; + if (whereFilters.length) + where = whereFilters; return new Promise(function (resolve, reject) { if (state._conf.logging) console.log('[vuex-easy-firestore] Fetch starting'); if (!getters.signedIn) return resolve(); - var identifier = createFetchIdentifier({ whereFilters: whereFilters, orderBy: orderBy }); + var identifier = createFetchIdentifier({ where: where, orderBy: orderBy }); var fetched = state._sync.fetched[identifier]; // We've never fetched this before: if (!fetched) { var ref_1 = getters.dbRef; // apply where filters and orderBy - whereFilters.forEach(function (paramsArr) { + getters.getWhereArrays(where).forEach(function (paramsArr) { ref_1 = ref_1.where.apply(ref_1, paramsArr); }); if (orderBy.length) { @@ -881,22 +900,26 @@ function pluginActions (Firebase$$1) { }); }, fetchAndAdd: function (_a, _b - // whereFilters: [['archived', '==', true]] + // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] ) { var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; - var _c = _b === void 0 ? { whereFilters: [], orderBy: [] } : _b - // whereFilters: [['archived', '==', true]] + var _c = _b === void 0 ? { where: [], whereFilters: [], orderBy: [] } : _b + // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] - , _d = _c.whereFilters, whereFilters = _d === void 0 ? [] : _d, _e = _c.orderBy, orderBy = _e === void 0 ? [] : _e; - return dispatch('fetch', { whereFilters: whereFilters, orderBy: orderBy }) + , _d = _c.where, where = _d === void 0 ? [] : _d, _e = _c.whereFilters, whereFilters = _e === void 0 ? [] : _e, _f = _c.orderBy, orderBy = _f === void 0 ? [] : _f; + if (whereFilters.length) + where = whereFilters; + return dispatch('fetch', { where: where, orderBy: orderBy }) .then(function (querySnapshot) { if (querySnapshot.done === true) return querySnapshot; if (isFunction(querySnapshot.forEach)) { querySnapshot.forEach(function (_doc) { var id = _doc.id; - var doc = setDefaultValues(_doc.data(), state._conf.serverChange.defaultValues); + var defaultValues = merge(state._conf.sync.defaultValues, state._conf.serverChange.defaultValues, // depreciated + state._conf.serverChange.convertTimestamps); + var doc = setDefaultValues(_doc.data(), defaultValues); doc.id = id; commit('INSERT_DOC', doc); }); @@ -930,18 +953,11 @@ function pluginActions (Firebase$$1) { delete pathVariables.orderBy; commit('SET_PATHVARS', pathVariables); } - // get userId - var userId = null; - if (Firebase$$1.auth().currentUser) { - state._sync.signedIn = true; - userId = Firebase$$1.auth().currentUser.uid; - state._sync.userId = userId; - } // getters.dbRef should already have pathVariables swapped out var dbRef = getters.dbRef; // apply where filters and orderBy if (getters.collectionMode) { - getters.whereFilters.forEach(function (whereParams) { + getters.getWhereArrays().forEach(function (whereParams) { dbRef = dbRef.where.apply(dbRef, whereParams); }); if (state._conf.sync.orderBy.length) { @@ -981,8 +997,10 @@ function pluginActions (Firebase$$1) { } if (source === 'local') return resolve(); - var doc = setDefaultValues(querySnapshot.data(), state._conf.serverChange.defaultValues); - var id = getters.firestorePathComplete.split('/').pop(); + var defaultValues = merge(state._conf.sync.defaultValues, state._conf.serverChange.defaultValues, // depreciated + state._conf.serverChange.convertTimestamps); + var doc = setDefaultValues(querySnapshot.data(), defaultValues); + var id = getters.docModeId; doc.id = id; handleDoc('modified', id, doc); return resolve(); @@ -993,8 +1011,10 @@ function pluginActions (Firebase$$1) { if (source === 'local') return resolve(); var id = change.doc.id; + var defaultValues = merge(state._conf.sync.defaultValues, state._conf.serverChange.defaultValues, // depreciated + state._conf.serverChange.convertTimestamps); var doc = (changeType === 'added') - ? setDefaultValues(change.doc.data(), state._conf.serverChange.defaultValues) + ? setDefaultValues(change.doc.data(), defaultValues) : change.doc.data(); handleDoc(changeType, id, doc); }); @@ -1040,6 +1060,8 @@ function pluginActions (Firebase$$1) { var newDoc = doc; if (!newDoc.id) newDoc.id = getters.dbRef.doc().id; + // apply default values + var newDocWithDefaults = setDefaultValues(newDoc, state._conf.sync.defaultValues); // define the store update function storeUpdateFn(_doc) { commit('INSERT_DOC', _doc); @@ -1047,11 +1069,11 @@ function pluginActions (Firebase$$1) { } // check for hooks if (state._conf.sync.insertHook) { - state._conf.sync.insertHook(storeUpdateFn, newDoc, store); - return newDoc.id; + state._conf.sync.insertHook(storeUpdateFn, newDocWithDefaults, store); + return newDocWithDefaults.id; } - storeUpdateFn(newDoc); - return newDoc.id; + storeUpdateFn(newDocWithDefaults); + return newDocWithDefaults.id; }, insertBatch: function (_a, docs) { var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; @@ -1236,61 +1258,6 @@ function flattenToPaths (object) { return retrievePaths(object, null, result); } -function recursiveCheck(obj, fillables, guard, pathUntilNow) { - if (pathUntilNow === void 0) { pathUntilNow = ''; } - if (!isPlainObject(obj)) { - console.log('obj → ', obj); - return obj; - } - return Object.keys(obj).reduce(function (carry, key) { - var path = pathUntilNow; - if (path) - path += '.'; - path += key; - // check guard regardless - if (guard.includes(path)) { - return carry; - } - var value = obj[key]; - // check fillables up to this point - if (fillables.length) { - var passed_1 = false; - fillables.forEach(function (fillable) { - var pathDepth = path.split('.').length; - var fillableDepth = fillable.split('.').length; - var fillableUpToNow = fillable.split('.').slice(0, pathDepth).join('.'); - var pathUpToFillableDepth = path.split('.').slice(0, fillableDepth).join('.'); - if (fillableUpToNow === pathUpToFillableDepth) - passed_1 = true; - }); - // there's not one fillable that allows up to now - if (!passed_1) - return carry; - } - // no fillables or fillables up to now allow it - if (!isPlainObject(value)) { - carry[key] = value; - return carry; - } - carry[key] = recursiveCheck(obj[key], fillables, guard, path); - return carry; - }, {}); -} -/** - * Checks all props of an object and deletes guarded and non-fillables. - * - * @export - * @param {object} obj the target object to check - * @param {string[]} [fillables=[]] an array of strings, with the props which should be allowed on returned object - * @param {string[]} [guard=[]] an array of strings, with the props which should NOT be allowed on returned object - * @returns {AnyObject} the cleaned object after deleting guard and non-fillables - */ -function checkFillables (obj, fillables, guard) { - if (fillables === void 0) { fillables = []; } - if (guard === void 0) { guard = []; } - return recursiveCheck(obj, fillables, guard); -} - /** * A function returning the getters object * @@ -1338,6 +1305,9 @@ function pluginGetters (Firebase$$1) { collectionMode: function (state, getters, rootState) { return (state._conf.firestoreRefType.toLowerCase() === 'collection'); }, + docModeId: function (state, getters) { + return getters.firestorePathComplete.split('/').pop(); + }, prepareForPatch: function (state, getters, rootState, rootGetters) { return function (ids, doc) { if (ids === void 0) { ids = []; } @@ -1345,7 +1315,7 @@ function pluginGetters (Firebase$$1) { // get relevant data from the storeRef var collectionMode = getters.collectionMode; if (!collectionMode) - ids.push('singleDoc'); + ids.push(getters.docModeId); // returns {object} -> {id: data} return ids.reduce(function (carry, id) { var patchData = {}; @@ -1375,7 +1345,7 @@ function pluginGetters (Firebase$$1) { fillables = fillables.concat(['updated_at', 'updated_by']); var guard = state._conf.sync.guard.concat(['_conf', '_sync']); // clean up item - var cleanedPatchData = checkFillables(patchData, fillables, guard); + var cleanedPatchData = filter(patchData, fillables, guard); var itemToUpdate = flattenToPaths(cleanedPatchData); // add id (required to get ref later at apiHelpers.ts) itemToUpdate.id = id; @@ -1399,7 +1369,7 @@ function pluginGetters (Firebase$$1) { fillables = fillables.concat(['updated_at', 'updated_by']); var guard = state._conf.sync.guard.concat(['_conf', '_sync']); // clean up item - var cleanedPatchData = checkFillables(patchData, fillables, guard); + var cleanedPatchData = filter(patchData, fillables, guard); // add id (required to get ref later at apiHelpers.ts) var id, cleanedPath; if (collectionMode) { @@ -1407,7 +1377,7 @@ function pluginGetters (Firebase$$1) { cleanedPath = path.substring(path.indexOf('.') + 1); } else { - id = 'singleDoc'; + id = getters.docModeId; cleanedPath = path; } cleanedPatchData[cleanedPath] = Firebase$$1.firestore.FieldValue.delete(); @@ -1428,7 +1398,7 @@ function pluginGetters (Firebase$$1) { item.created_at = Firebase$$1.firestore.FieldValue.serverTimestamp(); item.created_by = state._sync.userId; // clean up item - item = checkFillables(item, fillables, guard); + item = filter(item, fillables, guard); carry.push(item); return carry; }, []); @@ -1445,12 +1415,17 @@ function pluginGetters (Firebase$$1) { doc.created_at = Firebase$$1.firestore.FieldValue.serverTimestamp(); doc.created_by = state._sync.userId; // clean up item - doc = checkFillables(doc, fillables, guard); + doc = filter(doc, fillables, guard); return doc; }; }, - whereFilters: function (state, getters) { - var whereArrays = state._conf.sync.where; + getWhereArrays: function (state, getters) { return function (whereArrays) { + if (!isArray(whereArrays)) + whereArrays = state._conf.sync.where; + if (Firebase$$1.auth().currentUser) { + state._sync.signedIn = true; + state._sync.userId = Firebase$$1.auth().currentUser.uid; + } return whereArrays.map(function (whereClause) { return whereClause.map(function (param) { if (!isString(param)) @@ -1476,7 +1451,7 @@ function pluginGetters (Firebase$$1) { return cleanedParam; }); }); - }, + }; }, }; } @@ -1501,7 +1476,7 @@ function errorCheck (config) { if (/\./.test(config.moduleName)) { errors.push("moduleName must only include letters from [a-z] and forward slashes '/'"); } - var syncProps = ['where', 'orderBy', 'fillables', 'guard', 'insertHook', 'patchHook', 'deleteHook', 'insertBatchHook', 'patchBatchHook', 'deleteBatchHook']; + var syncProps = ['where', 'orderBy', 'fillables', 'guard', 'defaultValues', 'insertHook', 'patchHook', 'deleteHook', 'insertBatchHook', 'patchBatchHook', 'deleteBatchHook']; syncProps.forEach(function (prop) { if (config[prop]) { errors.push("We found `" + prop + "` on your module, are you sure this shouldn't be inside a prop called `sync`?"); @@ -1536,7 +1511,7 @@ function errorCheck (config) { var objectProps = ['sync', 'serverChange', 'defaultValues', 'fetch']; objectProps.forEach(function (prop) { var _prop = (prop === 'defaultValues') - ? config.serverChange[prop] + ? config.sync[prop] : config[prop]; if (!isPlainObject(_prop)) errors.push("`" + prop + "` should be an Object, but is not."); diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index d1519845..16783b7e 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -10,8 +10,9 @@ module.exports = { sidebar: [ ['/setup', 'Installation & setup'], '/guide', - '/extra-features', '/firestore-fields-and-functions', + '/hooks', + '/extra-features', '/config-example', '/feedback', ], diff --git a/docs/README.md b/docs/README.md index fc877517..8b9e0071 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,7 +7,7 @@ features: - title: Simplicity First details: Minimal setup to get a vuex-module synced with Firestore automatically. - title: Powerful - details: Easy to use features include filtering, hooks, automatic Firestore Timestamp conversion & much more. + details: Easy to use features include filtering, hooks, default values, automatic Firestore Timestamp conversion & much more. - title: Performant details: Automatic 2-way sync is fully optimised through api call batches. footer: MIT Licensed | Copyright © 2018-present Luca Ban - Mesqueeb @@ -36,10 +36,11 @@ Now you just update and add docs with `dispatch('userData/set', newItem)` and fo # Features - Automatic 2-way sync between your Vuex module & Firestore -- [Timestamp conversion to Date()](extra-features.html#defaultvalues-set-after-server-retrieval) +- [Default values](extra-features.html#default-values) +- [Hooks](hooks.html#hooks) (before / after sync) - [Fillables / guard](extra-features.html#fillables-and-guard) (limit fields which will sync) -- [Hooks](extra-features.html#hooks-before-insert-patch-delete) (before / after sync) -- [Where / orderBy filters](extra-features.html#filters) +- [Timestamp conversion to Date()](extra-features.html#firestore-timestamp-conversion) +- [Where / orderBy filters](guide.html#query-data-filters) # Motivation diff --git a/docs/config-example.md b/docs/config-example.md index 389c5c65..65571580 100644 --- a/docs/config-example.md +++ b/docs/config-example.md @@ -19,6 +19,7 @@ const firestoreModule = { orderBy: [], fillables: [], guard: [], + defaultValues: {}, // HOOKS for local changes: insertHook: function (updateStore, doc, store) { return updateStore(doc) }, patchHook: function (updateStore, doc, store) { return updateStore(doc) }, @@ -31,7 +32,7 @@ const firestoreModule = { // When docs on the server side are changed: serverChange: { - defaultValues: {}, + convertTimestamps: {}, // HOOKS for changes on SERVER: addedHook: function (updateStore, doc, id, store) { return updateStore(doc) }, modifiedHook: function (updateStore, doc, id, store) { return updateStore(doc) }, diff --git a/docs/extra-features.md b/docs/extra-features.md index 64e9b655..ded7d143 100644 --- a/docs/extra-features.md +++ b/docs/extra-features.md @@ -1,55 +1,5 @@ # Extra features -## Filters - -> Only for 'collection' mode. - -Just as in Firestore's [onSnapshot](https://firebase.google.com/docs/firestore/query-data/listen) listener, you can set `where` and `orderBy` filters to filter which docs are retrieved and synced. - -- *where:* arrays of `parameters` that get passed on to firestore's `.where(...parameters)` -- *orderBy:* a single `array` that gets passed on to firestore's `.orderBy(...array)` - -### Usage: - -```js -const where = [ // an array of arrays - ['certain_field', '==', false], - ['another_field', '==', true], -] -const orderBy = ['created_date'] // or more params - -// Add to openDBChannel: -store.dispatch('myModule/openDBChannel', {where, orderBy}) // like this - -// OR -// Add to your vuex-easy-fire module in the config -myModule = { - // your other vuex-easy-fire config... - sync: { - where, - orderBy - } -} -``` - -You can also use all kind of variables like `'{userId}'` like so: - -```js -store.dispatch('myModule/openDBChannel', { - where: [ - ['created_by', '==', '{userId}'], - ['some field', '==', '{pathVar}'] - ], - pathVar: 'value' -}) -``` - -What happens here above is: -- `{userId}` will be automatically replaced with the authenticated user -- `{pathVar}` is a custom variable that will be replaced with `'value'` - -For more information on custom variables, see the next chapter: - ## Variables for firestorePath or filters Besides `'{userId}'` in your `firestorePath` in the config or in where filters, you can also use **any variable** in the `firestorePath` or the `where` filter. @@ -81,7 +31,7 @@ store.dispatch('userData/openDBChannel') ## Fillables and guard -You can prevent props on your docs in 'collection' mode (or on your single doc in 'doc' mode) to be synced to the firestore server. For this you should use either `fillables` **or** `guard`: +You can prevent props on your docs to be synced to the firestore server. For this you should use either `fillables` **or** `guard`: - *Fillables:* Array of keys - the props which **may be synced** to the server. - 0 fillables = all props are synced @@ -99,7 +49,7 @@ You can prevent props on your docs in 'collection' mode (or on your single doc i } ``` -**Example fillables:** +### Example fillables ```js // settings: @@ -116,7 +66,7 @@ dispatch('user/set', newUser) {name: 'Ash', age: 10} ``` -**Example guard:** +### Example guard If you have only one prop you do not want to sync to firestore you can set `guard` instead of `fillables`. @@ -125,198 +75,151 @@ If you have only one prop you do not want to sync to firestore you can set `guar guard: ['email'] ``` -## Hooks before insert/patch/delete +### Nested fillables/guard -A function where you can check something or even change the doc (the doc object) before the store mutation occurs. The `doc` passed in these hooks will also have an `id` field which is the id with which it will be added to the store and to Firestore. +In the example below we will prevent the nested field called `notAllowed` from being synced: -Please make sure to check the overview of [execution timings of hooks](#execution-timings-of-hooks). +```js +const docToPatch = {nested: {allowed: true, notAllowed: true}} + +// in your module config, either set the fillables like so: +fillables: ['nested.allowed'] + +// OR set your guard like so: +guard: ['nested.notAllowed'] +``` + +### Wildcard fillables/guard + +You can also use wildcards! + +In this example you have a document with an object called `lists`. The lists each have an id as the key, and a nested property you want to prevent from being synced: ```js -{ - // your other vuex-easy-fire config... - sync: { - insertHook: function (updateStore, doc, store) { updateStore(doc) }, - patchHook: function (updateStore, doc, store) { updateStore(doc) }, - deleteHook: function (updateStore, id, store) { updateStore(id) }, - // Batches have separate hooks! - insertBatchHook: function (updateStore, doc, store) { updateStore(doc) }, - patchBatchHook: function (updateStore, doc, ids, store) { updateStore(doc, ids) }, - deleteBatchHook: function (updateStore, ids, store) { updateStore(ids) }, +const docToPatch = { + lists: { + 'list-id1': {allowed: true, notAllowed: true}, + 'list-id2': {allowed: true, notAllowed: true} } } + +// in your module config, either set the fillables like so: +fillables: ['lists.*.allowed'] + +// OR set your guard like so: +guard: ['lists.*.notAllowed'] +``` + +## Duplicating docs + +> Only for 'collection' mode. + +You can duplicate a document really simply by dispatching 'duplicate' and passing the id of the target document. + +```js +// let's duplicate Bulbasaur who has the id '001' +dispatch('pokemonBox/duplicate', '001') ``` -::: warning You must call `updateStore(doc)` to make the store mutation. -But you may choose not to call this to abort the mutation. If you do not call `updateStore(doc)` nothing will happen. -::: +This will create a copy of Bulbasaur (and all its props) with a random new ID. The duplicated doc will automatically be added to your vuex module and synced as well. + +If you need to know which new ID was generated for the duplicate, you can retrieve it from the action: + +```js +const idMap = await dispatch('pokemonBox/duplicate', '001') +// mind the await! +// idMap will look like this: +{'001': dupeId} +// dupeId will be a string with the ID of the duplicate! +``` -## Hooks after changes on the server +In the example above, if Bulbasaur ('001') was duplicated and the new document has random ID `'123abc'` the `idMap` will be `{'001': '123abc'}`. -Exactly the same as above, but for changes that have occured on the server. You also have some extra parameters to work with: +### Duplicate batch -- *id:* the doc id returned in `change.doc.id` (see firestore documentation for more info) -- *doc:* the doc returned in `change.doc.data()` (see firestore documentation for more info) +This is how you duplicate a batch of documents: ```js +const idMap = await dispatch('pokemonBox/duplicateBatch', ['001', '004', '007']) +// idMap will look like this: { - // your other vuex-easy-fire config... - serverChange: { - addedHook: function (updateStore, doc, id, store) { updateStore(doc) }, - modifiedHook: function (updateStore, doc, id, store) { updateStore(doc) }, - removedHook: function (updateStore, doc, id, store) { updateStore(doc) }, + '001': 'some-random-new-ID-1', + '004': 'some-random-new-ID-2', + '007': 'some-random-new-ID-3', +} +``` + +This way you can use the result if you need to do extra things to your duplicated docs and you will know for each ID which new ID was used to make a duplication. + +## Default values + +You can set up default values for your docs that will be added to the object on each insert. + +**In 'doc' mode** this can just be done by adding those values to your module state. This is how's it's done regularly with Vuex. + +**In 'collection' mode** the library takes care of applying these default values to each doc that's inserted. Default values should be set in your modules `sync` config: + +```js +const pokemonBoxModule = { + // your other vuex-easy-firestore config... + sync: { + defaultValues: { + freed: false, + }, } } + +// Now, when you add a new pokemon, it will automatically have `freed` +dispatch('pokemonBox/insert', {name: 'Poliwag'}) +// This will appear in your module like so: +// {name: 'Poliwag', freed: false} ``` -Please make sure to check the overview of execution timings of hooks, in the next chapter: - -## Execution timings of hooks - -Notice that the `created_at` and `updated_at` fields mentioned below is used by default, but can be disabled. To disable just add them to your [guard config](#fillables-and-guard). - -**Collection mode hooks** - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
change typelocalserver
insertion - without created_at -
    -
  1. sync.insertHook
  2. -
-
- with created_at -
    -
  1. sync.insertHook
  2. -
  3. serverChange.modifiedHook
  4. -
-
serverChange.addedHook
modification - without updated_at -
    -
  1. sync.patchHook
  2. -
-
- with updated_at -
    -
  1. sync.patchHook
  2. -
  3. serverChange.modifiedHook
  4. -
-
serverChange.modifiedHook
deletion -
    -
  1. sync.deleteHook
  2. -
  3. serverChange.removedHook
  4. -
-
serverChange.removedHook
after openDBChannelserverChange.addedHook is executed once for each doc
- -**Doc mode hooks** - - - - - - - - - - - - - - - - - -
change typelocalserver
modification - without updated_at -
    -
  1. sync.patchHook
  2. -
-
- with updated_at -
    -
  1. sync.patchHook
  2. -
  3. serverChange.modifiedHook
  4. -
-
serverChange.modifiedHook
after openDBChannelserverChange.modifiedHook is executed once
- -### Note about the serverChange hooks executing on local changes - -I have done my best to limit the hooks to only be executed on the proper events. The server hooks are executed during Firestore's [onSnapshot events](https://firebase.google.com/docs/firestore/query-data/listen). - -The reason for `updated_at` to trigger `serverChange.modifiedHook` even on just a local change, is because `updated_at` uses Firestore's `firebase.firestore.FieldValue.serverTimestamp()` which is replaced by a timestamp on the server and therefor there is a "server change". - -## defaultValues set after server retrieval - -If you create a `defaultValues` object, then each document from the server will receive those default values! - -**Use case 1: Firestore Timestamp conversion**
-Automatically convert Firestore Timestamps into `new Date()` objects! Do this by setting `'%convertTimestamp%'` as the value of a `defaultValues` prop. (see example below). - -**Use case 2: Reactivity**
-With VueJS, if you need a prop on an object to be fully reactive with your vue templates, it needs to exist from the start. If some docs in your user's firestore doesn't have all props (because you added new functionality to your app at later dates), the *retrieved docs will have reactivity problems!* - -However, if you add these props to `defaultValues` with some value (or just `'null'`), vuex-easy-firestore will automatically add those props to the doc *before* inserting it into vuex! - -**Example:** +Also, to make sure there are no vue reactivity issues, these default values are also applied to any doc that doesn't have them that's retrieved from the server. + +## Firestore Timestamp conversion + +Firestore works with special "[timestamp](https://firebase.google.com/docs/reference/js/firebase.firestore.Timestamp)" fields rather than with `new Date()`. Vuex-easy-firestore also uses the timestamp fields for the auto-generated fields `created_at` and `updated_at` which are added to your docs. + +In your app, if you want to use these timestamps as a `new Date()` object you have to call `timestamp.toDate()` on each of these fields. **Luckily this can do this for you!** + +You just have to specify the fields in a `convertTimestamps` object in your module config like so: + ```js const vuexModule = { // your other vuex-easy-firestore config... serverChange: { - defaultValues: { - defaultInt: 1, - propAddedLater: null, - date: '%convertTimestamp%', + convertTimestamps: { + updated_at: '%convertTimestamp%' }, } } +``` -// Now an example of what happens to the docs which are retrieved from the server: -const retrievedDoc = { - defaultInt: 2, - date: Timestamp // firestore Timestamp object -} +Now the Timestamp on `updated_at` will automatically trigger `Timestamp.toDate()` before being added to your vuex store! -// This doc will be inserted into vuex like so: -const docToBeInserted = { - defaultInt: 2, // stays 2 - propAddedLater: null, // receives propAddedLater prop with default val - date: Timestamp.toDate() // will execute firestore's Timestamp.toDate() -} +## Shortcut: set(path, doc) + +Inside Vue component templates there is a shortcut for `dispatch('module/set', newVal)`. If you enable support for my other library called 'vuex-easy-access' you will be able to just use `set('module', newVal)` instead! -// '%convertTimestamp%' works also with date strings: -const retrievedDoc = {date: '1990-06-22 17:35:00'} // date string -const docToBeInserted = {date: new Date('1990-06-22 17:35:00')} // converted to new Date +For this shortcut usage, import the npm module 'vuex-easy-access' when you initialise your store and add it as plugin. Pass the 'vuex-easy-firestore' plugin first and **the 'vuex-easy-access' plugin second** for it to work properly. -// in case the retrieved val is not present `null` will be added -const retrievedDoc = {} -const docToBeInserted = {date: null} +Also add `{vuexEasyFirestore: true}` in the options when you initialise 'vuex-easy-access' like so: + +```js +import vuexEasyAccess from 'vuex-easy-access' +const easyAccess = vuexEasyAccess({vuexEasyFirestore: true}) + +const store = { + plugins: [easyFirestoreModules, easyAccess] +} ``` -To learn more about Firestore's Timestamp format see [here](https://firebase.google.com/docs/reference/js/firebase.firestore.Timestamp). +Please check the relevant documentation [on the vuex-easy-access repository](https://mesqueeb.github.io/vuex-easy-access/advanced.html#firestore-integration-for-google-firebase)! + +### About 'vuex-easy-access' + +Vuex easy access has a lot of different features to make working with your store extremely easy. The main purpose of that library is to be able to do any kind of mutation to your store directly from the templates without having to set up actions yourself. It is especially usefull when working with wildcards. Please see the [introduction on Vuex Easy Access here](https://mesqueeb.github.io/vuex-easy-access/). ## Pass Firebase dependency diff --git a/docs/guide.md b/docs/guide.md index cf58cc8f..91b00c1c 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -106,7 +106,7 @@ dispatch('moduleName/insert', newDoc) As you can see in the example above, each vuex-easy-firestore module has a getter called `dbRef` with the reference of your `firestorePath`. So when you add `.doc().id` to that reference you will "create" a new ID, just how Firestore would do it. This way you can do whatever you want with the ID before / after the insert. -Please note you can also access the ID (even if you don't manually pass it) in the [hooks](extra-features.html#hooks-before-insert-patch-delete). +Please note you can also access the ID (even if you don't manually pass it) in the [hooks](hooks.html#hooks). ## 'doc' mode @@ -144,14 +144,6 @@ When working with a single doc, your document updates will automatically receive - `updated_at` uses: `Firebase.firestore.FieldValue.serverTimestamp()` - `updated_by` will automatically fill in the userId -## Shortcut: set(path, doc) - -Inside Vue component templates you can also access the `set` action through a shortcut: `$store.set(path, doc)`. Or with our path: `$store.set('userData', doc)`. - -For this shortcut usage, import the npm module 'vuex-easy-access' and just add `{vuexEasyFirestore: true}` in its options. Please also check the relevant documentation [on the vuex-easy-access repository](https://mesqueeb.github.io/vuex-easy-access/advanced.html#firestore-integration-for-google-firebase)! - -Please note that **it is important to pass the 'vuex-easy-firestore' plugin first**, and the 'vuex-easy-access' plugin second for it to work properly. - ## Multiple modules with 2-way sync Of course you can have multiple vuex modules, all in sync with different firestore paths. @@ -231,7 +223,7 @@ const settingsModule = { > Only for 'collection' mode. -In cases you don't want to loop through items you can also use the special batch actions below. The main difference is you will have separate hooks (see [hooks](extra-features.html#hooks-before-insert-patch-delete)), and batches are optimised to update the vuex store first for all changes and the syncs to firestore last. +In cases you don't want to loop through items you can also use the special batch actions below. The main difference is you will have separate hooks (see [hooks](hooks.html#hooks)), and batches are optimised to update the vuex store first for all changes and the syncs to firestore last. ```js dispatch('moduleName/insertBatch', docs) // an array of docs @@ -241,24 +233,74 @@ dispatch('moduleName/deleteBatch', ids) // an array of ids > All batch actions will return a promise resolving to an array of the edited / added ids. +## Query data (filters) + +> Only for 'collection' mode. + +Just as in Firestore's [onSnapshot](https://firebase.google.com/docs/firestore/query-data/listen) listener, you can set `where` and `orderBy` filters to filter which docs are retrieved and synced. + +- *where:* arrays of `parameters` that get passed on to firestore's `.where(...parameters)` +- *orderBy:* a single `array` that gets passed on to firestore's `.orderBy(...array)` + +### Usage: + +```js +const where = [ // an array of arrays + ['certain_field', '==', false], + ['another_field', '==', true], +] +const orderBy = ['created_date'] // or more params + +// Add to openDBChannel: +store.dispatch('myModule/openDBChannel', {where, orderBy}) // like this + +// OR +// Add to your vuex-easy-fire module in the config +myModule = { + // your other vuex-easy-fire config... + sync: { + where, + orderBy + } +} +``` + +You can also use all kind of variables like `'{userId}'` like so: + +```js +store.dispatch('myModule/openDBChannel', { + where: [ + ['created_by', '==', '{userId}'], + ['some field', '==', '{pathVar}'] + ], + pathVar: 'value' // the actual value to replace {pathVar} with above +}) +``` + +What happens here above is: +- `{userId}` will be automatically replaced with the authenticated user +- `{pathVar}` is a custom variable that will be replaced with `'value'` + +For more information on custom variables, see the chapter on [variables for firestorePath or filters](extra-features.html#variables-for-firestorepath-or-filters). + ## Fetching docs (with different filters) > Only for 'collection' mode. -Say that you have a default filter set on the documents you are syncing when you `openDBChannel` (see [Filters](extra-features.html#filters)). And you want to fetch extra documents with other filters. (eg. archived posts) In this case you can use the fetch actions to retrieve documents from the same firestore path your module is synced to: +Say that you have a default filter set on the documents you are syncing when you `openDBChannel` (see [Query data](#query-data-filters) above). And you want to fetch extra documents with other filters. (eg. archived posts) In this case you can use the fetch actions to retrieve documents from the same firestore path your module is synced to: - `dispatch('fetch')` for manual handling the fetched docs - `dispatch('fetchAndAdd')` for fetching and automatically adding the docs to the module like your other docs You can have two extra parameters: -- *whereFilters:* The same as firestore's `.where()`. An array of arrays with the filters you want. eg. `[['field', '==', false], ...]` +- *where:* The same as firestore's `.where()`. An array of arrays with the filters you want. eg. `[['field', '==', false], ...]` - *orderBy:* The same as firestore's `.orderBy()`. eg. `['created_date']` ### Usage example `fetchAndAdd`: ```js -dispatch('pokemonBox/fetchAndAdd', {whereFilters: [['freed', '==', true]], orderBy: ['freedDate']}) +dispatch('pokemonBox/fetchAndAdd', {where: [['freed', '==', true]], orderBy: ['freedDate']}) .then(querySnapshot => { if (querySnapshot.done === true) { // `{done: true}` is returned when everything is already fetched and there are 0 docs: @@ -275,7 +317,7 @@ Using the `fetchAndAdd` method means your documents will be added to your vuex-m ### Usage example `fetch`: ```js -dispatch('pokemonBox/fetch', {whereFilters: [['freed', '==', true]], orderBy: ['freedDate']}) +dispatch('pokemonBox/fetch', {where: [['freed', '==', true]], orderBy: ['freedDate']}) .then(querySnapshot => { if (querySnapshot.done === true) { // `{done: true}` is returned when everything is already fetched and there are 0 docs: @@ -301,7 +343,7 @@ The fetch limit defaults to 50 docs. If you watch to fetch *the next 50 docs* yo ```js function fetchFreedPokemon () { - dispatch('pokemonBox/fetchAndAdd', {whereFilters: [['freed', '==', true]], orderBy: ['freedDate']}) + dispatch('pokemonBox/fetchAndAdd', {where: [['freed', '==', true]], orderBy: ['freedDate']}) } // call once to fetch the first 50: fetchFreedPokemon() @@ -321,44 +363,3 @@ You can change the default fetch limit like so: }, } ``` - -## Duplicating docs - -> Only for 'collection' mode. - -You can duplicate a document really simply by dispatching 'duplicate' and passing the id of the target document. - -```js -// let's duplicate Bulbasaur who has the id '001' -dispatch('pokemonBox/duplicate', '001') -``` - -This will create a copy of Bulbasaur (and all its props) with a random new ID. The duplicated doc will automatically be added to your vuex module and synced as well. - -If you need to know which new ID was generated for the duplicate, you can retrieve it from the action: - -```js -const idMap = await dispatch('pokemonBox/duplicate', '001') -// mind the await! -// idMap will look like this: -{'001': dupeId} -// dupeId will be a string with the ID of the duplicate! -``` - -In the example above, if Bulbasaur ('001') was duplicated and the new document has random ID `'123abc'` the `idMap` will be `{'001': '123abc'}`. - -### Duplicate batch - -This is how you duplicate a batch of documents: - -```js -const idMap = await dispatch('pokemonBox/duplicateBatch', ['001', '004', '007']) -// idMap will look like this: -{ - '001': 'some-random-new-ID-1', - '004': 'some-random-new-ID-2', - '007': 'some-random-new-ID-3', -} -``` - -This way you can use the result if you need to do extra things to your duplicated docs and you will know for each ID which new ID was used to make a duplication. diff --git a/docs/hooks.md b/docs/hooks.md new file mode 100644 index 00000000..f671e615 --- /dev/null +++ b/docs/hooks.md @@ -0,0 +1,147 @@ +# Hooks + +A hook is a function do anything the doc (the doc object) before the store mutation occurs. You can even modify the docs, or add fields based on conditional checks etc. + +## Hooks on local changes + +Hooks must be defined inside your vuex module under `sync`. Below are the examples of all possible hooks that will trigger on 'local' changes. Please also check the overview of [execution timings of hooks](#execution-timings-of-hooks) to understand the difference between 'local' and 'server' changes. + +```js +{ + // your other vuex-easy-fire config... + sync: { + insertHook: function (updateStore, doc, store) { updateStore(doc) }, + patchHook: function (updateStore, doc, store) { updateStore(doc) }, + deleteHook: function (updateStore, id, store) { updateStore(id) }, + // Batches have separate hooks! + insertBatchHook: function (updateStore, docs, store) { updateStore(doc) }, + patchBatchHook: function (updateStore, doc, ids, store) { updateStore(doc, ids) }, + deleteBatchHook: function (updateStore, ids, store) { updateStore(ids) }, + } +} +``` + +The `doc` passed in the `insert` and `patch` hooks will also have an `id` field which is either the new id or the id of the doc to be patched. + +::: warning You must call `updateStore(doc)` to make the store mutation. +But you may choose not to call this to abort the mutation. If you do not call `updateStore(doc)` nothing will happen. +::: + +## Hooks after server changes + +Exactly the same as above, but for changes that have occured on the server. You also have some extra parameters to work with: + +- *id:* the doc id returned in `change.doc.id` (see firestore documentation for more info) +- *doc:* the doc returned in `change.doc.data()` (see firestore documentation for more info) + +```js +{ + // your other vuex-easy-fire config... + serverChange: { + addedHook: function (updateStore, doc, id, store) { updateStore(doc) }, + modifiedHook: function (updateStore, doc, id, store) { updateStore(doc) }, + removedHook: function (updateStore, doc, id, store) { updateStore(doc) }, + } +} +``` + +Please make sure to check the overview of execution timings of hooks, in the next chapter: + +## Execution timings of hooks + +Notice that the `created_at` and `updated_at` fields mentioned below is used by default, but can be disabled. To disable just add them to your [guard config](extra-features.html#fillables-and-guard). + +**Hooks on 'collection' mode** + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
change typelocalserver
insertion + without created_at +
    +
  1. sync.insertHook
  2. +
+
+ with created_at +
    +
  1. sync.insertHook
  2. +
  3. serverChange.modifiedHook
  4. +
+
serverChange.addedHook
modification + without updated_at +
    +
  1. sync.patchHook
  2. +
+
+ with updated_at +
    +
  1. sync.patchHook
  2. +
  3. serverChange.modifiedHook
  4. +
+
serverChange.modifiedHook
deletion +
    +
  1. sync.deleteHook
  2. +
  3. serverChange.removedHook
  4. +
+
serverChange.removedHook
after openDBChannelserverChange.addedHook is executed once for each doc
+ +**Hooks on 'doc' mode** + + + + + + + + + + + + + + + + + +
change typelocalserver
modification + without updated_at +
    +
  1. sync.patchHook
  2. +
+
+ with updated_at +
    +
  1. sync.patchHook
  2. +
  3. serverChange.modifiedHook
  4. +
+
serverChange.modifiedHook
after openDBChannelserverChange.modifiedHook is executed once
+ +### Note about the serverChange hooks executing on local changes + +I have done my best to limit the hooks to only be executed on the proper events. The server hooks are executed during Firestore's [onSnapshot events](https://firebase.google.com/docs/firestore/query-data/listen). + +The reason for `updated_at` to trigger `serverChange.modifiedHook` even on just a local change, is because `updated_at` uses Firestore's `firebase.firestore.FieldValue.serverTimestamp()` which is replaced by a timestamp on the server and therefor there is a "server change". diff --git a/docs/ja/README.md b/docs/ja/README.md deleted file mode 100644 index 0e4965fd..00000000 --- a/docs/ja/README.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -home: true -# heroImage: /hero.png -actionText: 始めよう → -actionLink: /setup -features: -- title: 何よりシンプル - details: Minimal setup to get a vuex-module synced with Firestore automatically. -- title: パワフル - details: Easy to use features include filtering, hooks, automatic Firestore Timestamp conversion & much more. -- title: パフォーマンスが第一 - details: Automatic 2-way sync is fully optimised through api call batches. -footer: MIT Licensed | Copyright © 2018-present Luca Ban - Mesqueeb ---- - -# Overview - -たった4つの行のコードを追加するだけで、VuexモジュールがFirestoreと自動的に同期される状態にできる。 - -```js -const userModule = { - firestorePath: 'users/{userId}/data', - firestoreRefType: 'collection', // or 'doc' - moduleName: 'userData', - statePropName: 'docs', - // モジュールのその他 state, actions など -} -// vuex-easy-firestoreでこのuserModuleをvuex pluginとしてstoreに入れるだけ -``` - -and Alakazam! Now you have a vuex module called `userData` with `state: {docs: {}}`. -All firestore documents in your collection will be added with the doc's id as key inside `docs` in your state. - -Now you just update and add docs with `dispatch('userData/set', newItem)` and forget about the rest! - -# Features - -- Complete 2-way sync between your Vuex module & Firestore -- [Automatic Firestore Timestamp conversion](extra-features.html#defaultvalues-set-after-server-retrieval) -- [Fillables](extra-features.html#fillables-and-guard) (limit props able to sync) -- [Hooks](extra-features.html#hooks-before-insert-patch-delete) (before / after sync) -- [Where / orderBy filters](extra-features.html#filters) - -# Motivation - -I didn't like writing an entire an API wrapper from scratch for firestore every single project. If only a vuex module could be in perfect sync with firestore without having to code all the boilerplate yourself... - -And that's how Vuex Easy Firestore was born. - -
Installation and setup →
- -# Support - -If you like what I built, you can say thanks by buying me a coffee! :) - -Buy me a coffeeBuy me a coffee - -Thank you so much!! Every little bit helps. diff --git a/docs/setup.md b/docs/setup.md index c804212d..599911de 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -89,7 +89,7 @@ Firebase.auth().onAuthStateChanged(user => { Vuex-easy-firestore uses Firestore's [onSnapshot](https://firebase.google.com/docs/firestore/query-data/listen) to retrieve the doc(s) from the server and has added logic to save those doc(s) in a vuex module. If you do not want to open an `onSnapshot` listener you can also use [fetch](guide.html#fetching-docs-with-different-filters) instead. -Also note that just like Firestore you can use `where` and `orderBy` filters (see [Filters](extra-features.html#filters)). +Also note that just like Firestore you can use `where` and `orderBy` filters (see [Filters](guide.html#query-data-filters)). ### Close DB channel diff --git a/package.json b/package.json index 388ab0a7..96447542 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vuex-easy-firestore", - "version": "1.21.0", + "version": "1.22.0", "description": "Easy coupling of firestore and a vuex module. 2-way sync with 0 boilerplate!", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", @@ -35,6 +35,7 @@ }, "homepage": "https://github.com/mesqueeb/vuex-easy-firestore#readme", "dependencies": { + "filter-anything": "^1.1.0", "find-and-replace-anything": "^2.0.0", "is-what": "^3.1.1", "merge-anything": "^2.2.0", diff --git a/src/module/actions.ts b/src/module/actions.ts index dfd9b6c6..b71b297f 100644 --- a/src/module/actions.ts +++ b/src/module/actions.ts @@ -1,4 +1,4 @@ -import { isArray, isPlainObject, isAnyObject, isFunction } from 'is-what' +import { isArray, isPlainObject, isFunction } from 'is-what' import merge from 'merge-anything' import { AnyObject, IPluginState } from '../declarations' import setDefaultValues from '../utils/setDefaultValues' @@ -6,6 +6,7 @@ import startDebounce from '../utils/debounceHelper' import { makeBatchFromSyncstack, createFetchIdentifier } from '../utils/apiHelpers' import { getId, getValueFromPayloadPiece } from '../utils/payloadHelpers' import error from './errors' +import { createDiffieHellman } from 'crypto'; /** * A function returning the actions object @@ -42,6 +43,20 @@ export default function (Firebase: any): AnyObject { if (!isArray(ids)) return console.error('[vuex-easy-firestore] ids needs to be an array') if (id) ids.push(id) + // EXTRA: check if doc is being inserted if so + state._sync.syncStack.inserts.forEach((newDoc, newDocIndex) => { + // get the index of the id that is also in the insert stack + const indexIdInInsert = ids.indexOf(newDoc.id) + if (indexIdInInsert === -1) return + // the doc trying to be synced is also in insert + // prepare the doc as new doc: + const patchDoc = getters.prepareForInsert([doc])[0] + // replace insert sync stack with merged item: + state._sync.syncStack.inserts[newDocIndex] = merge(newDoc, patchDoc) + // empty out the id that was to be patched: + ids.splice(indexIdInInsert, 1) + }) + // 1. Prepare for patching const syncStackItems = getters.prepareForPatch(ids, doc) @@ -152,20 +167,21 @@ export default function (Firebase: any): AnyObject { }, fetch ( {state, getters, commit, dispatch}, - {whereFilters = [], orderBy = []} = {whereFilters: [], orderBy: []} - // whereFilters: [['archived', '==', true]] + {where = [], whereFilters = [], orderBy = []} = {where: [], whereFilters: [], orderBy: []} + // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] ) { + if (whereFilters.length) where = whereFilters return new Promise((resolve, reject) => { if (state._conf.logging) console.log('[vuex-easy-firestore] Fetch starting') if (!getters.signedIn) return resolve() - const identifier = createFetchIdentifier({whereFilters, orderBy}) + const identifier = createFetchIdentifier({where, orderBy}) const fetched = state._sync.fetched[identifier] // We've never fetched this before: if (!fetched) { let ref = getters.dbRef // apply where filters and orderBy - whereFilters.forEach(paramsArr => { + getters.getWhereArrays(where).forEach(paramsArr => { ref = ref.where(...paramsArr) }) if (orderBy.length) { @@ -222,17 +238,23 @@ export default function (Firebase: any): AnyObject { }, fetchAndAdd ( {state, getters, commit, dispatch}, - {whereFilters = [], orderBy = []} = {whereFilters: [], orderBy: []} - // whereFilters: [['archived', '==', true]] + {where = [], whereFilters = [], orderBy = []} = {where: [], whereFilters: [], orderBy: []} + // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] ) { - return dispatch('fetch', {whereFilters, orderBy}) + if (whereFilters.length) where = whereFilters + return dispatch('fetch', {where, orderBy}) .then(querySnapshot => { if (querySnapshot.done === true) return querySnapshot if (isFunction(querySnapshot.forEach)) { querySnapshot.forEach(_doc => { const id = _doc.id - const doc = setDefaultValues(_doc.data(), state._conf.serverChange.defaultValues) + const defaultValues = merge( + state._conf.sync.defaultValues, + state._conf.serverChange.defaultValues, // depreciated + state._conf.serverChange.convertTimestamps, + ) + const doc = setDefaultValues(_doc.data(), defaultValues) doc.id = id commit('INSERT_DOC', doc) }) @@ -266,18 +288,11 @@ export default function (Firebase: any): AnyObject { delete pathVariables.orderBy commit('SET_PATHVARS', pathVariables) } - // get userId - let userId = null - if (Firebase.auth().currentUser) { - state._sync.signedIn = true - userId = Firebase.auth().currentUser.uid - state._sync.userId = userId - } // getters.dbRef should already have pathVariables swapped out let dbRef = getters.dbRef // apply where filters and orderBy if (getters.collectionMode) { - getters.whereFilters.forEach(whereParams => { + getters.getWhereArrays().forEach(whereParams => { dbRef = dbRef.where(...whereParams) }) if (state._conf.sync.orderBy.length) { @@ -314,8 +329,13 @@ export default function (Firebase: any): AnyObject { return resolve() } if (source === 'local') return resolve() - const doc = setDefaultValues(querySnapshot.data(), state._conf.serverChange.defaultValues) - const id = getters.firestorePathComplete.split('/').pop() + const defaultValues = merge( + state._conf.sync.defaultValues, + state._conf.serverChange.defaultValues, // depreciated + state._conf.serverChange.convertTimestamps, + ) + const doc = setDefaultValues(querySnapshot.data(), defaultValues) + const id = getters.docModeId doc.id = id handleDoc('modified', id, doc) return resolve() @@ -325,8 +345,13 @@ export default function (Firebase: any): AnyObject { // Don't do anything for local modifications & removals if (source === 'local') return resolve() const id = change.doc.id + const defaultValues = merge( + state._conf.sync.defaultValues, + state._conf.serverChange.defaultValues, // depreciated + state._conf.serverChange.convertTimestamps, + ) const doc = (changeType === 'added') - ? setDefaultValues(change.doc.data(), state._conf.serverChange.defaultValues) + ? setDefaultValues(change.doc.data(), defaultValues) : change.doc.data() handleDoc(changeType, id, doc) }) @@ -365,6 +390,8 @@ export default function (Firebase: any): AnyObject { if (!doc) return const newDoc = doc if (!newDoc.id) newDoc.id = getters.dbRef.doc().id + // apply default values + const newDocWithDefaults = setDefaultValues(newDoc, state._conf.sync.defaultValues) // define the store update function storeUpdateFn (_doc) { commit('INSERT_DOC', _doc) @@ -372,11 +399,11 @@ export default function (Firebase: any): AnyObject { } // check for hooks if (state._conf.sync.insertHook) { - state._conf.sync.insertHook(storeUpdateFn, newDoc, store) - return newDoc.id + state._conf.sync.insertHook(storeUpdateFn, newDocWithDefaults, store) + return newDocWithDefaults.id } - storeUpdateFn(newDoc) - return newDoc.id + storeUpdateFn(newDocWithDefaults) + return newDocWithDefaults.id }, insertBatch ({state, getters, commit, dispatch}, docs) { const store = this diff --git a/src/module/defaultConfig.ts b/src/module/defaultConfig.ts index 0230a08d..352424f1 100644 --- a/src/module/defaultConfig.ts +++ b/src/module/defaultConfig.ts @@ -11,7 +11,7 @@ export type SyncHookId = (updateStore: HandleId, id: string, store) => (void | H export type InsertBatchHook = (updateStore: HandleDocs, docs: any[], store) => (void | HandleDocs) export type PatchBatchHook = (updateStore: HandleDocIds, doc: any, ids: string[], store) => (void | HandleDocIds) export type DeleteBatchHook = (updateStore: HandleIds, ids: string[], store) => (void | HandleIds) -export type ServerChangeHook = (updateStore: HandleDoc, doc: any, id, store, source, change) => (void | HandleDoc) +export type ServerChangeHook = (updateStore: HandleDoc, doc: any, id, store) => (void | HandleDoc) export type IConfig = { firestorePath: string @@ -24,6 +24,7 @@ export type IConfig = { orderBy?: string[] fillables?: string[] guard?: string[] + defaultValues?: AnyObject insertHook?: SyncHookDoc patchHook?: SyncHookDoc deleteHook?: SyncHookId @@ -33,6 +34,7 @@ export type IConfig = { } serverChange?: { defaultValues?: AnyObject + convertTimestamps?: AnyObject addedHook?: ServerChangeHook modifiedHook?: ServerChangeHook removedHook?: ServerChangeHook @@ -62,6 +64,7 @@ export default { orderBy: [], fillables: [], guard: [], + defaultValues: {}, // HOOKS for local changes: insertHook: function (updateStore, doc, store) { return updateStore(doc) }, patchHook: function (updateStore, doc, store) { return updateStore(doc) }, @@ -74,11 +77,12 @@ export default { // When items on the server side are changed: serverChange: { - defaultValues: {}, + defaultValues: {}, // depreciated + convertTimestamps: {}, // HOOKS for changes on SERVER: - addedHook: function (updateStore, doc, id, store, source, change) { return updateStore(doc) }, - modifiedHook: function (updateStore, doc, id, store, source, change) { return updateStore(doc) }, - removedHook: function (updateStore, doc, id, store, source, change) { return updateStore(doc) }, + addedHook: function (updateStore, doc, id, store) { return updateStore(doc) }, + modifiedHook: function (updateStore, doc, id, store) { return updateStore(doc) }, + removedHook: function (updateStore, doc, id, store) { return updateStore(doc) }, }, // When items are fetched through `dispatch('module/fetch', filters)`. diff --git a/src/module/errorCheckConfig.ts b/src/module/errorCheckConfig.ts index 06021fa8..61f10e85 100644 --- a/src/module/errorCheckConfig.ts +++ b/src/module/errorCheckConfig.ts @@ -22,7 +22,7 @@ export default function (config: IEasyFirestoreModule): boolean { if (/\./.test(config.moduleName)) { errors.push(`moduleName must only include letters from [a-z] and forward slashes '/'`) } - const syncProps = ['where', 'orderBy', 'fillables', 'guard', 'insertHook', 'patchHook', 'deleteHook', 'insertBatchHook', 'patchBatchHook', 'deleteBatchHook'] + const syncProps = ['where', 'orderBy', 'fillables', 'guard', 'defaultValues', 'insertHook', 'patchHook', 'deleteHook', 'insertBatchHook', 'patchBatchHook', 'deleteBatchHook'] syncProps.forEach(prop => { if (config[prop]) { errors.push(`We found \`${prop}\` on your module, are you sure this shouldn't be inside a prop called \`sync\`?`) @@ -55,7 +55,7 @@ export default function (config: IEasyFirestoreModule): boolean { const objectProps = ['sync', 'serverChange', 'defaultValues', 'fetch'] objectProps.forEach(prop => { const _prop = (prop === 'defaultValues') - ? config.serverChange[prop] + ? config.sync[prop] : config[prop] if (!isPlainObject(_prop)) errors.push(`\`${prop}\` should be an Object, but is not.`) }) diff --git a/src/module/getters.ts b/src/module/getters.ts index f9e2c0c1..febcb27f 100644 --- a/src/module/getters.ts +++ b/src/module/getters.ts @@ -1,8 +1,8 @@ -import { isString, isPlainObject, isAnyObject } from 'is-what' +import { isString, isArray } from 'is-what' import { getDeepRef } from 'vuex-easy-access' import { findAndReplaceIf } from 'find-and-replace-anything' +import filter from 'filter-anything' import flattenToPaths from '../utils/objectFlattenToPaths' -import checkFillables from '../utils/checkFillables' import { getPathVarMatches } from '../utils/apiHelpers' import { isArrayHelper } from '../utils/arrayHelpers' import { AnyObject } from '../declarations' @@ -63,11 +63,14 @@ export default function (Firebase: any): AnyObject { collectionMode: (state, getters, rootState) => { return (state._conf.firestoreRefType.toLowerCase() === 'collection') }, + docModeId: (state, getters) => { + return getters.firestorePathComplete.split('/').pop() + }, prepareForPatch: (state, getters, rootState, rootGetters) => (ids = [], doc = {}) => { // get relevant data from the storeRef const collectionMode = getters.collectionMode - if (!collectionMode) ids.push('singleDoc') + if (!collectionMode) ids.push(getters.docModeId) // returns {object} -> {id: data} return ids.reduce((carry, id) => { let patchData: AnyObject = {} @@ -95,7 +98,7 @@ export default function (Firebase: any): AnyObject { if (fillables.length) fillables = fillables.concat(['updated_at', 'updated_by']) const guard = state._conf.sync.guard.concat(['_conf', '_sync']) // clean up item - const cleanedPatchData = checkFillables(patchData, fillables, guard) + const cleanedPatchData = filter(patchData, fillables, guard) const itemToUpdate = flattenToPaths(cleanedPatchData) // add id (required to get ref later at apiHelpers.ts) itemToUpdate.id = id @@ -115,14 +118,14 @@ export default function (Firebase: any): AnyObject { if (fillables.length) fillables = fillables.concat(['updated_at', 'updated_by']) const guard = state._conf.sync.guard.concat(['_conf', '_sync']) // clean up item - const cleanedPatchData = checkFillables(patchData, fillables, guard) + const cleanedPatchData = filter(patchData, fillables, guard) // add id (required to get ref later at apiHelpers.ts) let id, cleanedPath if (collectionMode) { id = path.substring(0, path.indexOf('.')) cleanedPath = path.substring(path.indexOf('.') + 1) } else { - id = 'singleDoc' + id = getters.docModeId cleanedPath = path } cleanedPatchData[cleanedPath] = Firebase.firestore.FieldValue.delete() @@ -140,7 +143,7 @@ export default function (Firebase: any): AnyObject { item.created_at = Firebase.firestore.FieldValue.serverTimestamp() item.created_by = state._sync.userId // clean up item - item = checkFillables(item, fillables, guard) + item = filter(item, fillables, guard) carry.push(item) return carry }, []) @@ -155,11 +158,15 @@ export default function (Firebase: any): AnyObject { doc.created_at = Firebase.firestore.FieldValue.serverTimestamp() doc.created_by = state._sync.userId // clean up item - doc = checkFillables(doc, fillables, guard) + doc = filter(doc, fillables, guard) return doc }, - whereFilters: (state, getters) => { - const whereArrays = state._conf.sync.where + getWhereArrays: (state, getters) => (whereArrays) => { + if (!isArray(whereArrays)) whereArrays = state._conf.sync.where + if (Firebase.auth().currentUser) { + state._sync.signedIn = true + state._sync.userId = Firebase.auth().currentUser.uid + } return whereArrays.map(whereClause => { return whereClause.map(param => { if (!isString(param)) return param diff --git a/src/module/mutations.ts b/src/module/mutations.ts index 8debf6c0..67bbe2fb 100644 --- a/src/module/mutations.ts +++ b/src/module/mutations.ts @@ -1,9 +1,9 @@ -import { isPlainObject, isAnyObject, isArray } from 'is-what' +import { isPlainObject, isArray } from 'is-what' import { getDeepRef } from 'vuex-easy-access' import error from './errors' import merge from 'merge-anything' import { AnyObject } from '../declarations' -import { isArrayHelper, ArrayUnion } from '../utils/arrayHelpers' +import { isArrayHelper } from '../utils/arrayHelpers' /** * a function returning the mutations object diff --git a/src/utils/apiHelpers.ts b/src/utils/apiHelpers.ts index 051ca6f8..525b70e2 100644 --- a/src/utils/apiHelpers.ts +++ b/src/utils/apiHelpers.ts @@ -161,16 +161,16 @@ function stringifyParams (params: any[]): string { } /** - * Gets an object with {whereFilters, orderBy} filters and returns a unique identifier for that + * Gets an object with {where, orderBy} filters and returns a unique identifier for that * * @export - * @param {AnyObject} [whereOrderBy={}] whereOrderBy {whereFilters, orderBy} + * @param {AnyObject} [whereOrderBy={}] whereOrderBy {where, orderBy} * @returns {string} */ export function createFetchIdentifier (whereOrderBy: AnyObject = {}): string { let identifier = '' - if ('whereFilters' in whereOrderBy) { - identifier += '[where]' + whereOrderBy.whereFilters.map(where => stringifyParams(where)).join() + if ('where' in whereOrderBy) { + identifier += '[where]' + whereOrderBy.where.map(where => stringifyParams(where)).join() } if ('orderBy' in whereOrderBy) { identifier += '[orderBy]' + stringifyParams(whereOrderBy.orderBy) diff --git a/src/utils/checkFillables.ts b/src/utils/checkFillables.ts deleted file mode 100644 index fe212513..00000000 --- a/src/utils/checkFillables.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { isPlainObject } from 'is-what' -import { AnyObject } from '../declarations' - -function recursiveCheck ( - obj: object, - fillables: string[], - guard: string[], - pathUntilNow: string = '' -): AnyObject { - if (!isPlainObject(obj)) { - console.log('obj → ', obj) - return obj - } - return Object.keys(obj).reduce((carry, key) => { - let path = pathUntilNow - if (path) path += '.' - path += key - // check guard regardless - if (guard.includes(path)) { - return carry - } - const value = obj[key] - // check fillables up to this point - if (fillables.length) { - let passed = false - fillables.forEach(fillable => { - const pathDepth = path.split('.').length - const fillableDepth = fillable.split('.').length - const fillableUpToNow = fillable.split('.').slice(0, pathDepth).join('.') - const pathUpToFillableDepth = path.split('.').slice(0, fillableDepth).join('.') - if (fillableUpToNow === pathUpToFillableDepth) passed = true - }) - // there's not one fillable that allows up to now - if (!passed) return carry - } - // no fillables or fillables up to now allow it - if (!isPlainObject(value)) { - carry[key] = value - return carry - } - carry[key] = recursiveCheck(obj[key], fillables, guard, path) - return carry - }, {}) -} - -/** - * Checks all props of an object and deletes guarded and non-fillables. - * - * @export - * @param {object} obj the target object to check - * @param {string[]} [fillables=[]] an array of strings, with the props which should be allowed on returned object - * @param {string[]} [guard=[]] an array of strings, with the props which should NOT be allowed on returned object - * @returns {AnyObject} the cleaned object after deleting guard and non-fillables - */ -export default function ( - obj: object, - fillables: string[] = [], - guard: string[] = [] -): AnyObject { - return recursiveCheck(obj, fillables, guard) -} diff --git a/test/DBChannel.js b/test/DBChannel.js index 4b6709b1..60dd12c1 100644 --- a/test/DBChannel.js +++ b/test/DBChannel.js @@ -35,21 +35,21 @@ test('[openDBChannel] check where filter after openDBChannel', async t => { char._conf.sync.where = [['hi.{userId}.docs.{name}', '==', '{big}']] char._sync.userId = 'charlie' // 0. initial path - res = store.getters['mainCharacter/whereFilters'] + res = store.getters['mainCharacter/getWhereArrays']() t.deepEqual(char._sync.pathVariables, {}) t.deepEqual(char._conf.sync.where, [['hi.{userId}.docs.{name}', '==', '{big}']]) t.deepEqual(res, [['hi.charlie.docs.{name}', '==', '{big}']]) // 1. open once store.dispatch('mainCharacter/openDBChannel', {name: 'Luca'}).catch(console.error) await wait(2) - res = store.getters['mainCharacter/whereFilters'] + res = store.getters['mainCharacter/getWhereArrays']() t.deepEqual(char._sync.pathVariables, {name: 'Luca'}) t.deepEqual(char._conf.sync.where, [['hi.{userId}.docs.{name}', '==', '{big}']]) t.deepEqual(res, [['hi.charlie.docs.Luca', '==', '{big}']]) // 2. open again store.dispatch('mainCharacter/openDBChannel', {name: 'Mesqueeb'}).catch(console.error) await wait(2) - res = store.getters['mainCharacter/whereFilters'] + res = store.getters['mainCharacter/getWhereArrays']() t.deepEqual(char._sync.pathVariables, {name: 'Mesqueeb'}) t.deepEqual(res, [['hi.charlie.docs.Mesqueeb', '==', '{big}']]) }) diff --git a/test/actions.js b/test/actions.js index c2626e5d..5d8ec891 100644 --- a/test/actions.js +++ b/test/actions.js @@ -23,7 +23,6 @@ test('[COLLECTION] set with no id', async t => { await wait(2) // insert set id = await store.dispatch('pokemonBox/insert', {name: 'Unown'}) - console.log('id set with no id → ', id) t.truthy(box.pokemon[id]) t.is(box.pokemon[id].name, 'Unown') await wait(2) @@ -56,6 +55,27 @@ test('[COLLECTION] set with no id', async t => { t.deepEqual(doc.name, {is: 'nested1'}) }) +test('[COLLECTION] insert and patch right after each other', async t => { + await wait(2) + // insert + const id = boxRef.doc().id + console.log('id → ', id) + store.dispatch('pokemonBox/insert', {id, name: 'Mew', type: {normal: true}}) + t.truthy(box.pokemon[id]) + t.is(box.pokemon[id].name, 'Mew') + t.deepEqual(box.pokemon[id].type, {normal: true}) + // patch + store.dispatch('pokemonBox/patch', {id, type: {psychic: true}}) + t.is(box.pokemon[id].name, 'Mew') + t.deepEqual(box.pokemon[id].type, {normal: true, psychic: true}) + // await server + await wait(2) + const docR = await boxRef.doc(id).get() + const doc = docR.data() + t.is(doc.name, 'Mew') + t.deepEqual(doc.type, {normal: true, psychic: true}) +}) + test('[COLLECTION] set & delete: top lvl', async t => { const id = boxRef.doc().id const id2 = boxRef.doc().id diff --git a/test/getters.js b/test/getters.js index a71b9ade..b39c36f6 100644 --- a/test/getters.js +++ b/test/getters.js @@ -52,34 +52,35 @@ test('[prepareForPatch] doc', async t => { char._conf.sync.fillables = ['body', 'del', 'pathdel'] char._conf.sync.guard = [] // prepareForPatch + const docModeId = store.getters['mainCharacter/docModeId'] res = store.getters['mainCharacter/prepareForPatch']([], {body: 'new', del: Firebase.firestore.FieldValue.delete()}) - t.deepEqual(Object.keys(res), ['singleDoc']) - t.is(res['singleDoc'].body, 'new') - t.is(res['singleDoc'].del._methodName, 'FieldValue.delete') - t.is(res['singleDoc'].id, 'singleDoc') - t.is(res['singleDoc'].updated_by, 'charlie') - t.is(res['singleDoc'].updated_at._methodName, 'FieldValue.serverTimestamp') + t.deepEqual(Object.keys(res), [docModeId]) + t.is(res[docModeId].body, 'new') + t.is(res[docModeId].del._methodName, 'FieldValue.delete') + t.is(res[docModeId].id, docModeId) + t.is(res[docModeId].updated_by, 'charlie') + t.is(res[docModeId].updated_at._methodName, 'FieldValue.serverTimestamp') // prepareForPropDeletion res = store.getters['mainCharacter/prepareForPropDeletion']('1.pathdel.a') - t.is(res['singleDoc'].id, 'singleDoc') - t.is(res['singleDoc']['1.pathdel.a']._methodName, 'FieldValue.delete') - t.is(res['singleDoc'].updated_by, 'charlie') - t.is(res['singleDoc'].updated_at._methodName, 'FieldValue.serverTimestamp') + t.is(res[docModeId].id, docModeId) + t.is(res[docModeId]['1.pathdel.a']._methodName, 'FieldValue.delete') + t.is(res[docModeId].updated_by, 'charlie') + t.is(res[docModeId].updated_at._methodName, 'FieldValue.serverTimestamp') // different fillables & guard char._conf.sync.guard = ['updated_at', 'updated_by', 'id'] res = store.getters['mainCharacter/prepareForPropDeletion']('1.pathdel.a') - t.is(res['singleDoc'].id, 'singleDoc') // id stays even if it's added to guard - t.is(res['singleDoc']['1.pathdel.a']._methodName, 'FieldValue.delete') - t.is(res['singleDoc'].updated_by, undefined) - t.is(res['singleDoc'].updated_at, undefined) + t.is(res[docModeId].id, docModeId) // id stays even if it's added to guard + t.is(res[docModeId]['1.pathdel.a']._methodName, 'FieldValue.delete') + t.is(res[docModeId].updated_by, undefined) + t.is(res[docModeId].updated_at, undefined) }) -test('[whereFilters]', async t => { +test('[where]', async t => { let res char._conf.sync.where = [['hi.{userId}.docs.{nr}', '==', '{big}'], ['{userId}', '==', '{userId}']] char._sync.userId = 'charlie' char._sync.pathVariables = {nr: '1', big: 'shot'} - res = store.getters['mainCharacter/whereFilters'] + res = store.getters['mainCharacter/getWhereArrays']() t.deepEqual(res, [['hi.charlie.docs.1', '==', 'shot'], ['charlie', '==', 'charlie']]) t.deepEqual(char._conf.sync.where, [['hi.{userId}.docs.{nr}', '==', '{big}'], ['{userId}', '==', '{userId}']]) // accept other values than strings @@ -87,12 +88,12 @@ test('[whereFilters]', async t => { char._sync.userId = '' const date = new Date() char._sync.pathVariables = {date, nulll: null, undef: undefined} - res = store.getters['mainCharacter/whereFilters'] + res = store.getters['mainCharacter/getWhereArrays']() t.deepEqual(res, [[1, '==', true], ['', '==', date, null, undefined]]) t.deepEqual(char._conf.sync.where, [[1, '==', true], ['{userId}', '==', '{date}', '{nulll}', '{undef}']]) char._conf.sync.where = [[1, true, undefined, '{a}', NaN]] char._sync.pathVariables = {a: {}} - res = store.getters['mainCharacter/whereFilters'] + res = store.getters['mainCharacter/getWhereArrays']() t.deepEqual(res, [[1, true, undefined, {}, NaN]]) t.deepEqual(char._conf.sync.where, [[1, true, undefined, '{a}', NaN]]) }) diff --git a/test/helpers/index.cjs.js b/test/helpers/index.cjs.js index 7f005e10..56f0aed6 100644 --- a/test/helpers/index.cjs.js +++ b/test/helpers/index.cjs.js @@ -8,6 +8,7 @@ var isWhat = require('is-what'); var Firebase = require('firebase/app'); var merge = _interopDefault(require('merge-anything')); var findAndReplaceAnything = require('find-and-replace-anything'); +var filter = _interopDefault(require('filter-anything')); var Vue = _interopDefault(require('vue')); var Vuex = _interopDefault(require('vuex')); @@ -31,8 +32,24 @@ var pokemonBox = { sync: { where: [['id', '==', '{pokeId}']], orderBy: [], - fillables: ['fillable', 'name', 'id', 'type', 'freed', 'nested', 'addedBeforeInsert', 'addedBeforePatch', 'arr1', 'arr2', 'guarded'], + fillables: [ + 'fillable', + 'name', + 'id', + 'type', + 'freed', + 'nested', + 'addedBeforeInsert', + 'addedBeforePatch', + 'arr1', + 'arr2', + 'guarded', + 'defaultVal' + ], guard: ['guarded'], + defaultValues: { + defaultVal: true + }, // HOOKS for local changes: insertHook: function (updateStore, doc, store) { doc.addedBeforeInsert = true; @@ -236,6 +253,7 @@ var defaultConfig = { orderBy: [], fillables: [], guard: [], + defaultValues: {}, // HOOKS for local changes: insertHook: function (updateStore, doc, store) { return updateStore(doc); }, patchHook: function (updateStore, doc, store) { return updateStore(doc); }, @@ -248,10 +266,11 @@ var defaultConfig = { // When items on the server side are changed: serverChange: { defaultValues: {}, + convertTimestamps: {}, // HOOKS for changes on SERVER: - addedHook: function (updateStore, doc, id, store, source, change) { return updateStore(doc); }, - modifiedHook: function (updateStore, doc, id, store, source, change) { return updateStore(doc); }, - removedHook: function (updateStore, doc, id, store, source, change) { return updateStore(doc); }, + addedHook: function (updateStore, doc, id, store) { return updateStore(doc); }, + modifiedHook: function (updateStore, doc, id, store) { return updateStore(doc); }, + removedHook: function (updateStore, doc, id, store) { return updateStore(doc); }, }, // When items are fetched through `dispatch('module/fetch', filters)`. fetch: { @@ -723,17 +742,17 @@ function stringifyParams(params) { }).join(); } /** - * Gets an object with {whereFilters, orderBy} filters and returns a unique identifier for that + * Gets an object with {where, orderBy} filters and returns a unique identifier for that * * @export - * @param {AnyObject} [whereOrderBy={}] whereOrderBy {whereFilters, orderBy} + * @param {AnyObject} [whereOrderBy={}] whereOrderBy {where, orderBy} * @returns {string} */ function createFetchIdentifier(whereOrderBy) { if (whereOrderBy === void 0) { whereOrderBy = {}; } var identifier = ''; - if ('whereFilters' in whereOrderBy) { - identifier += '[where]' + whereOrderBy.whereFilters.map(function (where) { return stringifyParams(where); }).join(); + if ('where' in whereOrderBy) { + identifier += '[where]' + whereOrderBy.where.map(function (where) { return stringifyParams(where); }).join(); } if ('orderBy' in whereOrderBy) { identifier += '[orderBy]' + stringifyParams(whereOrderBy.orderBy); @@ -842,6 +861,20 @@ function pluginActions (Firebase$$1) { return console.error('[vuex-easy-firestore] ids needs to be an array'); if (id) ids.push(id); + // EXTRA: check if doc is being inserted if so + state._sync.syncStack.inserts.forEach(function (newDoc, newDocIndex) { + // get the index of the id that is also in the insert stack + var indexIdInInsert = ids.indexOf(newDoc.id); + if (indexIdInInsert === -1) + return; + // the doc trying to be synced is also in insert + // prepare the doc as new doc: + var patchDoc = getters.prepareForInsert([doc])[0]; + // replace insert sync stack with merged item: + state._sync.syncStack.inserts[newDocIndex] = merge(newDoc, patchDoc); + // empty out the id that was to be patched: + ids.splice(indexIdInInsert, 1); + }); // 1. Prepare for patching var syncStackItems = getters.prepareForPatch(ids, doc); // 2. Push to syncStack @@ -945,26 +978,28 @@ function pluginActions (Firebase$$1) { }); }, fetch: function (_a, _b - // whereFilters: [['archived', '==', true]] + // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] ) { var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; - var _c = _b === void 0 ? { whereFilters: [], orderBy: [] } : _b - // whereFilters: [['archived', '==', true]] + var _c = _b === void 0 ? { where: [], whereFilters: [], orderBy: [] } : _b + // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] - , _d = _c.whereFilters, whereFilters = _d === void 0 ? [] : _d, _e = _c.orderBy, orderBy = _e === void 0 ? [] : _e; + , _d = _c.where, where = _d === void 0 ? [] : _d, _e = _c.whereFilters, whereFilters = _e === void 0 ? [] : _e, _f = _c.orderBy, orderBy = _f === void 0 ? [] : _f; + if (whereFilters.length) + where = whereFilters; return new Promise(function (resolve, reject) { if (state._conf.logging) console.log('[vuex-easy-firestore] Fetch starting'); if (!getters.signedIn) return resolve(); - var identifier = createFetchIdentifier({ whereFilters: whereFilters, orderBy: orderBy }); + var identifier = createFetchIdentifier({ where: where, orderBy: orderBy }); var fetched = state._sync.fetched[identifier]; // We've never fetched this before: if (!fetched) { var ref_1 = getters.dbRef; // apply where filters and orderBy - whereFilters.forEach(function (paramsArr) { + getters.getWhereArrays(where).forEach(function (paramsArr) { ref_1 = ref_1.where.apply(ref_1, paramsArr); }); if (orderBy.length) { @@ -1021,22 +1056,26 @@ function pluginActions (Firebase$$1) { }); }, fetchAndAdd: function (_a, _b - // whereFilters: [['archived', '==', true]] + // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] ) { var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; - var _c = _b === void 0 ? { whereFilters: [], orderBy: [] } : _b - // whereFilters: [['archived', '==', true]] + var _c = _b === void 0 ? { where: [], whereFilters: [], orderBy: [] } : _b + // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] - , _d = _c.whereFilters, whereFilters = _d === void 0 ? [] : _d, _e = _c.orderBy, orderBy = _e === void 0 ? [] : _e; - return dispatch('fetch', { whereFilters: whereFilters, orderBy: orderBy }) + , _d = _c.where, where = _d === void 0 ? [] : _d, _e = _c.whereFilters, whereFilters = _e === void 0 ? [] : _e, _f = _c.orderBy, orderBy = _f === void 0 ? [] : _f; + if (whereFilters.length) + where = whereFilters; + return dispatch('fetch', { where: where, orderBy: orderBy }) .then(function (querySnapshot) { if (querySnapshot.done === true) return querySnapshot; if (isWhat.isFunction(querySnapshot.forEach)) { querySnapshot.forEach(function (_doc) { var id = _doc.id; - var doc = setDefaultValues(_doc.data(), state._conf.serverChange.defaultValues); + var defaultValues = merge(state._conf.sync.defaultValues, state._conf.serverChange.defaultValues, // depreciated + state._conf.serverChange.convertTimestamps); + var doc = setDefaultValues(_doc.data(), defaultValues); doc.id = id; commit('INSERT_DOC', doc); }); @@ -1070,18 +1109,11 @@ function pluginActions (Firebase$$1) { delete pathVariables.orderBy; commit('SET_PATHVARS', pathVariables); } - // get userId - var userId = null; - if (Firebase$$1.auth().currentUser) { - state._sync.signedIn = true; - userId = Firebase$$1.auth().currentUser.uid; - state._sync.userId = userId; - } // getters.dbRef should already have pathVariables swapped out var dbRef = getters.dbRef; // apply where filters and orderBy if (getters.collectionMode) { - getters.whereFilters.forEach(function (whereParams) { + getters.getWhereArrays().forEach(function (whereParams) { dbRef = dbRef.where.apply(dbRef, whereParams); }); if (state._conf.sync.orderBy.length) { @@ -1121,8 +1153,10 @@ function pluginActions (Firebase$$1) { } if (source === 'local') return resolve(); - var doc = setDefaultValues(querySnapshot.data(), state._conf.serverChange.defaultValues); - var id = getters.firestorePathComplete.split('/').pop(); + var defaultValues = merge(state._conf.sync.defaultValues, state._conf.serverChange.defaultValues, // depreciated + state._conf.serverChange.convertTimestamps); + var doc = setDefaultValues(querySnapshot.data(), defaultValues); + var id = getters.docModeId; doc.id = id; handleDoc('modified', id, doc); return resolve(); @@ -1133,8 +1167,10 @@ function pluginActions (Firebase$$1) { if (source === 'local') return resolve(); var id = change.doc.id; + var defaultValues = merge(state._conf.sync.defaultValues, state._conf.serverChange.defaultValues, // depreciated + state._conf.serverChange.convertTimestamps); var doc = (changeType === 'added') - ? setDefaultValues(change.doc.data(), state._conf.serverChange.defaultValues) + ? setDefaultValues(change.doc.data(), defaultValues) : change.doc.data(); handleDoc(changeType, id, doc); }); @@ -1180,6 +1216,8 @@ function pluginActions (Firebase$$1) { var newDoc = doc; if (!newDoc.id) newDoc.id = getters.dbRef.doc().id; + // apply default values + var newDocWithDefaults = setDefaultValues(newDoc, state._conf.sync.defaultValues); // define the store update function storeUpdateFn(_doc) { commit('INSERT_DOC', _doc); @@ -1187,11 +1225,11 @@ function pluginActions (Firebase$$1) { } // check for hooks if (state._conf.sync.insertHook) { - state._conf.sync.insertHook(storeUpdateFn, newDoc, store); - return newDoc.id; + state._conf.sync.insertHook(storeUpdateFn, newDocWithDefaults, store); + return newDocWithDefaults.id; } - storeUpdateFn(newDoc); - return newDoc.id; + storeUpdateFn(newDocWithDefaults); + return newDocWithDefaults.id; }, insertBatch: function (_a, docs) { var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; @@ -1376,61 +1414,6 @@ function flattenToPaths (object) { return retrievePaths(object, null, result); } -function recursiveCheck(obj, fillables, guard, pathUntilNow) { - if (pathUntilNow === void 0) { pathUntilNow = ''; } - if (!isWhat.isPlainObject(obj)) { - console.log('obj → ', obj); - return obj; - } - return Object.keys(obj).reduce(function (carry, key) { - var path = pathUntilNow; - if (path) - path += '.'; - path += key; - // check guard regardless - if (guard.includes(path)) { - return carry; - } - var value = obj[key]; - // check fillables up to this point - if (fillables.length) { - var passed_1 = false; - fillables.forEach(function (fillable) { - var pathDepth = path.split('.').length; - var fillableDepth = fillable.split('.').length; - var fillableUpToNow = fillable.split('.').slice(0, pathDepth).join('.'); - var pathUpToFillableDepth = path.split('.').slice(0, fillableDepth).join('.'); - if (fillableUpToNow === pathUpToFillableDepth) - passed_1 = true; - }); - // there's not one fillable that allows up to now - if (!passed_1) - return carry; - } - // no fillables or fillables up to now allow it - if (!isWhat.isPlainObject(value)) { - carry[key] = value; - return carry; - } - carry[key] = recursiveCheck(obj[key], fillables, guard, path); - return carry; - }, {}); -} -/** - * Checks all props of an object and deletes guarded and non-fillables. - * - * @export - * @param {object} obj the target object to check - * @param {string[]} [fillables=[]] an array of strings, with the props which should be allowed on returned object - * @param {string[]} [guard=[]] an array of strings, with the props which should NOT be allowed on returned object - * @returns {AnyObject} the cleaned object after deleting guard and non-fillables - */ -function checkFillables (obj, fillables, guard) { - if (fillables === void 0) { fillables = []; } - if (guard === void 0) { guard = []; } - return recursiveCheck(obj, fillables, guard); -} - /** * A function returning the getters object * @@ -1478,6 +1461,9 @@ function pluginGetters (Firebase$$1) { collectionMode: function (state, getters, rootState) { return (state._conf.firestoreRefType.toLowerCase() === 'collection'); }, + docModeId: function (state, getters) { + return getters.firestorePathComplete.split('/').pop(); + }, prepareForPatch: function (state, getters, rootState, rootGetters) { return function (ids, doc) { if (ids === void 0) { ids = []; } @@ -1485,7 +1471,7 @@ function pluginGetters (Firebase$$1) { // get relevant data from the storeRef var collectionMode = getters.collectionMode; if (!collectionMode) - ids.push('singleDoc'); + ids.push(getters.docModeId); // returns {object} -> {id: data} return ids.reduce(function (carry, id) { var patchData = {}; @@ -1515,7 +1501,7 @@ function pluginGetters (Firebase$$1) { fillables = fillables.concat(['updated_at', 'updated_by']); var guard = state._conf.sync.guard.concat(['_conf', '_sync']); // clean up item - var cleanedPatchData = checkFillables(patchData, fillables, guard); + var cleanedPatchData = filter(patchData, fillables, guard); var itemToUpdate = flattenToPaths(cleanedPatchData); // add id (required to get ref later at apiHelpers.ts) itemToUpdate.id = id; @@ -1539,7 +1525,7 @@ function pluginGetters (Firebase$$1) { fillables = fillables.concat(['updated_at', 'updated_by']); var guard = state._conf.sync.guard.concat(['_conf', '_sync']); // clean up item - var cleanedPatchData = checkFillables(patchData, fillables, guard); + var cleanedPatchData = filter(patchData, fillables, guard); // add id (required to get ref later at apiHelpers.ts) var id, cleanedPath; if (collectionMode) { @@ -1547,7 +1533,7 @@ function pluginGetters (Firebase$$1) { cleanedPath = path.substring(path.indexOf('.') + 1); } else { - id = 'singleDoc'; + id = getters.docModeId; cleanedPath = path; } cleanedPatchData[cleanedPath] = Firebase$$1.firestore.FieldValue.delete(); @@ -1568,7 +1554,7 @@ function pluginGetters (Firebase$$1) { item.created_at = Firebase$$1.firestore.FieldValue.serverTimestamp(); item.created_by = state._sync.userId; // clean up item - item = checkFillables(item, fillables, guard); + item = filter(item, fillables, guard); carry.push(item); return carry; }, []); @@ -1585,12 +1571,17 @@ function pluginGetters (Firebase$$1) { doc.created_at = Firebase$$1.firestore.FieldValue.serverTimestamp(); doc.created_by = state._sync.userId; // clean up item - doc = checkFillables(doc, fillables, guard); + doc = filter(doc, fillables, guard); return doc; }; }, - whereFilters: function (state, getters) { - var whereArrays = state._conf.sync.where; + getWhereArrays: function (state, getters) { return function (whereArrays) { + if (!isWhat.isArray(whereArrays)) + whereArrays = state._conf.sync.where; + if (Firebase$$1.auth().currentUser) { + state._sync.signedIn = true; + state._sync.userId = Firebase$$1.auth().currentUser.uid; + } return whereArrays.map(function (whereClause) { return whereClause.map(function (param) { if (!isWhat.isString(param)) @@ -1616,7 +1607,7 @@ function pluginGetters (Firebase$$1) { return cleanedParam; }); }); - }, + }; }, }; } @@ -1641,7 +1632,7 @@ function errorCheck (config) { if (/\./.test(config.moduleName)) { errors.push("moduleName must only include letters from [a-z] and forward slashes '/'"); } - var syncProps = ['where', 'orderBy', 'fillables', 'guard', 'insertHook', 'patchHook', 'deleteHook', 'insertBatchHook', 'patchBatchHook', 'deleteBatchHook']; + var syncProps = ['where', 'orderBy', 'fillables', 'guard', 'defaultValues', 'insertHook', 'patchHook', 'deleteHook', 'insertBatchHook', 'patchBatchHook', 'deleteBatchHook']; syncProps.forEach(function (prop) { if (config[prop]) { errors.push("We found `" + prop + "` on your module, are you sure this shouldn't be inside a prop called `sync`?"); @@ -1676,7 +1667,7 @@ function errorCheck (config) { var objectProps = ['sync', 'serverChange', 'defaultValues', 'fetch']; objectProps.forEach(function (prop) { var _prop = (prop === 'defaultValues') - ? config.serverChange[prop] + ? config.sync[prop] : config[prop]; if (!isWhat.isPlainObject(_prop)) errors.push("`" + prop + "` should be an Object, but is not."); diff --git a/test/helpers/store/initialDoc.ts b/test/helpers/store/initialDoc.ts new file mode 100644 index 00000000..cfc4728c --- /dev/null +++ b/test/helpers/store/initialDoc.ts @@ -0,0 +1,22 @@ +import { defaultMutations } from 'vuex-easy-access' + +function initialState () { + return { + name: 'Satoshi', + pokemonBelt: [], + items: [] + } +} + +export default { + // easy firestore config + firestorePath: 'docs/{randomId}', // this should be randomized each test + firestoreRefType: 'doc', + moduleName: 'initialDoc', + statePropName: '', + // module + state: initialState(), + mutations: defaultMutations(initialState()), + actions: {}, + getters: {}, +} diff --git a/test/helpers/store/pokemonBox.ts b/test/helpers/store/pokemonBox.ts index 18703b3f..cdb8cc28 100644 --- a/test/helpers/store/pokemonBox.ts +++ b/test/helpers/store/pokemonBox.ts @@ -21,8 +21,24 @@ export default { sync: { where: [['id', '==', '{pokeId}']], orderBy: [], - fillables: ['fillable', 'name', 'id', 'type', 'freed', 'nested', 'addedBeforeInsert', 'addedBeforePatch', 'arr1', 'arr2', 'guarded'], + fillables: [ + 'fillable', + 'name', + 'id', + 'type', + 'freed', + 'nested', + 'addedBeforeInsert', + 'addedBeforePatch', + 'arr1', + 'arr2', + 'guarded', + 'defaultVal' + ], guard: ['guarded'], + defaultValues: { + defaultVal: true + }, // HOOKS for local changes: insertHook: function (updateStore, doc, store) { doc.addedBeforeInsert = true diff --git a/test/mutations.js b/test/mutations.js index 702cc964..65b698a4 100644 --- a/test/mutations.js +++ b/test/mutations.js @@ -121,13 +121,13 @@ test('SET_PATHVARS & where getter', async t => { box._conf.sync.where = [['hi.{userId}.docs.{name}', '==', '{big}']] box._sync.userId = 'charlie' // pokemonBox._sync.userId = 'charlie' - res = store.getters['pokemonBox/whereFilters'] + res = store.getters['pokemonBox/getWhereArrays']() t.deepEqual(box._sync.pathVariables, {}) t.deepEqual(box._conf.sync.where, [['hi.{userId}.docs.{name}', '==', '{big}']]) t.deepEqual(res, [['hi.charlie.docs.{name}', '==', '{big}']]) store.commit('pokemonBox/SET_PATHVARS', {name: 'Satoshi'}) - res = store.getters['pokemonBox/whereFilters'] + res = store.getters['pokemonBox/getWhereArrays']() t.deepEqual(box._sync.pathVariables, {name: 'Satoshi'}) t.deepEqual(box._conf.sync.where, [['hi.{userId}.docs.{name}', '==', '{big}']]) t.deepEqual(res, [['hi.charlie.docs.Satoshi', '==', '{big}']]) diff --git a/test/syncDefaultValues.js b/test/syncDefaultValues.js new file mode 100644 index 00000000..605d6784 --- /dev/null +++ b/test/syncDefaultValues.js @@ -0,0 +1,24 @@ +import test from 'ava' +import wait from './helpers/wait' +import {storeSyncConfig as store} from './helpers/index.cjs.js' + +const box = store.state.pokemonBox +const boxRef = store.getters['pokemonBox/dbRef'] +// const char = store.state.mainCharacter +// const charRef = store.getters['mainCharacter/dbRef'] + +test('[COLLECTION] sync: defaultValues', async t => { + const id = boxRef.doc().id + store.dispatch('pokemonBox/insert', {id, name: 'Squirtle'}) + .catch(console.error) + t.truthy(box.pokemon[id]) + t.is(box.pokemon[id].name, 'Squirtle') + t.is(box.pokemon[id].defaultVal, true) + // fetch from server to check + await wait(2) + const docR = await boxRef.doc(id).get() + const doc = docR.data() + t.truthy(doc) + t.is(doc.name, 'Squirtle') + t.is(doc.defaultVal, true) +}) diff --git a/test/utils/apiHelpers.js b/test/utils/apiHelpers.js index aa86f302..df67dae4 100644 --- a/test/utils/apiHelpers.js +++ b/test/utils/apiHelpers.js @@ -69,12 +69,12 @@ test('getPathVarMatches', t => { test('createFetchIdentifier', t => { let res res = createFetchIdentifier({ - whereFilters: [['hi.{userId}.docs.{nr}', '==', '{big}'], ['{userId}', '==', '{userId}']], + where: [['hi.{userId}.docs.{nr}', '==', '{big}'], ['{userId}', '==', '{userId}']], orderBy: ['date'] }) t.is(res, '[where]hi.{userId}.docs.{nr},==,{big},{userId},==,{userId}[orderBy]date') res = createFetchIdentifier({ - whereFilters: [['thatRef', '==', store.getters['mainCharacter/dbRef']]] + where: [['thatRef', '==', store.getters['mainCharacter/dbRef']]] }) t.is(res, `[where]thatRef,==,DocumentReferenceSatoshi`) }) diff --git a/test/utils/checkFillables.js b/test/utils/checkFillables.js deleted file mode 100644 index d8b2c188..00000000 --- a/test/utils/checkFillables.js +++ /dev/null @@ -1,76 +0,0 @@ -import checkFillables from '../../src/utils/checkFillables' -import test from 'ava' - -test('check fillables FLAT', t => { - let res, doc, fillables, guard - doc = {name: 'n1', id: '1', filled: true, notfilled: false} - fillables = ['name', 'filled', 'id'] - res = checkFillables(doc, fillables, guard) - t.deepEqual(res, {name: 'n1', id: '1', filled: true}) -}) - -test('check guard FLAT', t => { - let res, doc, fillables, guard - doc = {name: 'n1', id: '1', filled: true, guarded: true} - fillables = [] - guard = ['guarded'] - res = checkFillables(doc, fillables, guard) - t.deepEqual(res, {name: 'n1', id: '1', filled: true}) -}) - -test('check fillables NESTED - single fillable', t => { - let res, doc, fillables, guard - doc = {nested: {fillables: {yes: 0, no: 0}}, secondProp: true} - fillables = ['nested.fillables.yes'] - res = checkFillables(doc, fillables, guard) - t.deepEqual(res, {nested: {fillables: {yes: 0}}}) - fillables = ['nested.fillables'] - res = checkFillables(doc, fillables, guard) - t.deepEqual(res, {nested: {fillables: {yes: 0, no: 0}}}) - fillables = ['nested'] - res = checkFillables(doc, fillables, guard) - t.deepEqual(res, {nested: {fillables: {yes: 0, no: 0}}}) -}) - -test('check fillables NESTED - multiple fillable', t => { - let res, doc, fillables, guard - doc = {nested: {fillables: {yes: 0, no: 0}}, secondProp: {yes: true, no: false}} - fillables = ['nested.fillables.yes', 'secondProp.yes'] - res = checkFillables(doc, fillables, guard) - t.deepEqual(res, {nested: {fillables: {yes: 0}}, secondProp: {yes: true}}) - fillables = ['nested.fillables', 'secondProp.yes'] - res = checkFillables(doc, fillables, guard) - t.deepEqual(res, {nested: {fillables: {yes: 0, no: 0}}, secondProp: {yes: true}}) - fillables = ['nested', 'secondProp.yes'] - res = checkFillables(doc, fillables, guard) - t.deepEqual(res, {nested: {fillables: {yes: 0, no: 0}}, secondProp: {yes: true}}) -}) - -test('check guard NESTED', t => { - let res, doc, fillables, guard - doc = {nested: {guard: {yes: 0, no: 0}}, secondProp: true} - guard = ['nested.guard.yes'] - res = checkFillables(doc, fillables, guard) - t.deepEqual(res, {nested: {guard: {no: 0}}, secondProp: true}) - guard = ['nested.guard'] - res = checkFillables(doc, fillables, guard) - t.deepEqual(res, {nested: {}, secondProp: true}) - guard = ['nested'] - res = checkFillables(doc, fillables, guard) - t.deepEqual(res, {secondProp: true}) -}) - -test('check fillables NESTED - multiple fillable & guard', t => { - let res, doc, fillables, guard - doc = {nested: {fillables: {yes: 0, no: 0}}, secondProp: {yes: true, no: false}, guardedTop: true, guardedDeep: {yes: true, no: true}} - fillables = ['nested.fillables.yes', 'secondProp.yes', 'guardedTop', 'guardedDeep'] - guard = ['guardedTop', 'guardedDeep.yes'] - res = checkFillables(doc, fillables, guard) - t.deepEqual(res, {nested: {fillables: {yes: 0}}, secondProp: {yes: true}, guardedDeep: {no: true}}) - fillables = ['nested.fillables', 'secondProp.yes', 'guardedTop', 'guardedDeep'] - res = checkFillables(doc, fillables, guard) - t.deepEqual(res, {nested: {fillables: {yes: 0, no: 0}}, secondProp: {yes: true}, guardedDeep: {no: true}}) - fillables = ['nested', 'secondProp.yes', 'guardedTop', 'guardedDeep'] - res = checkFillables(doc, fillables, guard) - t.deepEqual(res, {nested: {fillables: {yes: 0, no: 0}}, secondProp: {yes: true}, guardedDeep: {no: true}}) -}) diff --git a/types/module/defaultConfig.d.ts b/types/module/defaultConfig.d.ts index ca14d26e..bd42cff7 100644 --- a/types/module/defaultConfig.d.ts +++ b/types/module/defaultConfig.d.ts @@ -9,7 +9,7 @@ export declare type SyncHookId = (updateStore: HandleId, id: string, store: any) export declare type InsertBatchHook = (updateStore: HandleDocs, docs: any[], store: any) => (void | HandleDocs); export declare type PatchBatchHook = (updateStore: HandleDocIds, doc: any, ids: string[], store: any) => (void | HandleDocIds); export declare type DeleteBatchHook = (updateStore: HandleIds, ids: string[], store: any) => (void | HandleIds); -export declare type ServerChangeHook = (updateStore: HandleDoc, doc: any, id: any, store: any, source: any, change: any) => (void | HandleDoc); +export declare type ServerChangeHook = (updateStore: HandleDoc, doc: any, id: any, store: any) => (void | HandleDoc); export declare type IConfig = { firestorePath: string; firestoreRefType: string; @@ -21,6 +21,7 @@ export declare type IConfig = { orderBy?: string[]; fillables?: string[]; guard?: string[]; + defaultValues?: AnyObject; insertHook?: SyncHookDoc; patchHook?: SyncHookDoc; deleteHook?: SyncHookId; @@ -30,6 +31,7 @@ export declare type IConfig = { }; serverChange?: { defaultValues?: AnyObject; + convertTimestamps?: AnyObject; addedHook?: ServerChangeHook; modifiedHook?: ServerChangeHook; removedHook?: ServerChangeHook; @@ -53,6 +55,7 @@ declare const _default: { orderBy: any[]; fillables: any[]; guard: any[]; + defaultValues: {}; insertHook: (updateStore: any, doc: any, store: any) => any; patchHook: (updateStore: any, doc: any, store: any) => any; deleteHook: (updateStore: any, id: any, store: any) => any; @@ -62,9 +65,10 @@ declare const _default: { }; serverChange: { defaultValues: {}; - addedHook: (updateStore: any, doc: any, id: any, store: any, source: any, change: any) => any; - modifiedHook: (updateStore: any, doc: any, id: any, store: any, source: any, change: any) => any; - removedHook: (updateStore: any, doc: any, id: any, store: any, source: any, change: any) => any; + convertTimestamps: {}; + addedHook: (updateStore: any, doc: any, id: any, store: any) => any; + modifiedHook: (updateStore: any, doc: any, id: any, store: any) => any; + removedHook: (updateStore: any, doc: any, id: any, store: any) => any; }; fetch: { docLimit: number; diff --git a/types/src/module/defaultConfig.d.ts b/types/src/module/defaultConfig.d.ts index ca14d26e..bd42cff7 100644 --- a/types/src/module/defaultConfig.d.ts +++ b/types/src/module/defaultConfig.d.ts @@ -9,7 +9,7 @@ export declare type SyncHookId = (updateStore: HandleId, id: string, store: any) export declare type InsertBatchHook = (updateStore: HandleDocs, docs: any[], store: any) => (void | HandleDocs); export declare type PatchBatchHook = (updateStore: HandleDocIds, doc: any, ids: string[], store: any) => (void | HandleDocIds); export declare type DeleteBatchHook = (updateStore: HandleIds, ids: string[], store: any) => (void | HandleIds); -export declare type ServerChangeHook = (updateStore: HandleDoc, doc: any, id: any, store: any, source: any, change: any) => (void | HandleDoc); +export declare type ServerChangeHook = (updateStore: HandleDoc, doc: any, id: any, store: any) => (void | HandleDoc); export declare type IConfig = { firestorePath: string; firestoreRefType: string; @@ -21,6 +21,7 @@ export declare type IConfig = { orderBy?: string[]; fillables?: string[]; guard?: string[]; + defaultValues?: AnyObject; insertHook?: SyncHookDoc; patchHook?: SyncHookDoc; deleteHook?: SyncHookId; @@ -30,6 +31,7 @@ export declare type IConfig = { }; serverChange?: { defaultValues?: AnyObject; + convertTimestamps?: AnyObject; addedHook?: ServerChangeHook; modifiedHook?: ServerChangeHook; removedHook?: ServerChangeHook; @@ -53,6 +55,7 @@ declare const _default: { orderBy: any[]; fillables: any[]; guard: any[]; + defaultValues: {}; insertHook: (updateStore: any, doc: any, store: any) => any; patchHook: (updateStore: any, doc: any, store: any) => any; deleteHook: (updateStore: any, id: any, store: any) => any; @@ -62,9 +65,10 @@ declare const _default: { }; serverChange: { defaultValues: {}; - addedHook: (updateStore: any, doc: any, id: any, store: any, source: any, change: any) => any; - modifiedHook: (updateStore: any, doc: any, id: any, store: any, source: any, change: any) => any; - removedHook: (updateStore: any, doc: any, id: any, store: any, source: any, change: any) => any; + convertTimestamps: {}; + addedHook: (updateStore: any, doc: any, id: any, store: any) => any; + modifiedHook: (updateStore: any, doc: any, id: any, store: any) => any; + removedHook: (updateStore: any, doc: any, id: any, store: any) => any; }; fetch: { docLimit: number; diff --git a/types/src/utils/apiHelpers.d.ts b/types/src/utils/apiHelpers.d.ts index 88c4e00f..0e19ba93 100644 --- a/types/src/utils/apiHelpers.d.ts +++ b/types/src/utils/apiHelpers.d.ts @@ -37,10 +37,10 @@ export declare function getPathVarMatches(pathPiece: string): string[]; */ export declare function trimAccolades(pathPiece: string): string; /** - * Gets an object with {whereFilters, orderBy} filters and returns a unique identifier for that + * Gets an object with {where, orderBy} filters and returns a unique identifier for that * * @export - * @param {AnyObject} [whereOrderBy={}] whereOrderBy {whereFilters, orderBy} + * @param {AnyObject} [whereOrderBy={}] whereOrderBy {where, orderBy} * @returns {string} */ export declare function createFetchIdentifier(whereOrderBy?: AnyObject): string; diff --git a/types/test/helpers/store/pokemonBox.d.ts b/types/test/helpers/store/pokemonBox.d.ts index 73b5b1ed..8ab09bb8 100644 --- a/types/test/helpers/store/pokemonBox.d.ts +++ b/types/test/helpers/store/pokemonBox.d.ts @@ -8,6 +8,9 @@ declare const _default: { orderBy: any[]; fillables: string[]; guard: string[]; + defaultValues: { + defaultVal: boolean; + }; insertHook: (updateStore: any, doc: any, store: any) => any; patchHook: (updateStore: any, doc: any, store: any) => any; deleteHook: (updateStore: any, id: any, store: any) => any; diff --git a/types/utils/apiHelpers.d.ts b/types/utils/apiHelpers.d.ts index 88c4e00f..0e19ba93 100644 --- a/types/utils/apiHelpers.d.ts +++ b/types/utils/apiHelpers.d.ts @@ -37,10 +37,10 @@ export declare function getPathVarMatches(pathPiece: string): string[]; */ export declare function trimAccolades(pathPiece: string): string; /** - * Gets an object with {whereFilters, orderBy} filters and returns a unique identifier for that + * Gets an object with {where, orderBy} filters and returns a unique identifier for that * * @export - * @param {AnyObject} [whereOrderBy={}] whereOrderBy {whereFilters, orderBy} + * @param {AnyObject} [whereOrderBy={}] whereOrderBy {where, orderBy} * @returns {string} */ export declare function createFetchIdentifier(whereOrderBy?: AnyObject): string; diff --git a/yarn.lock b/yarn.lock index f8d11694..cba48156 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4073,6 +4073,13 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" +filter-anything@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/filter-anything/-/filter-anything-1.1.0.tgz#9821649f17be540297f13ec4d5bdf340a72a7e01" + integrity sha512-c5+DdGvv1qJLXctPCPEfers1iz9TzjmYo4w9JQeh4sy8/au+lJH70mpJ4QEKcCHN8vy4QdlvhMSzYS1BGtBGfw== + dependencies: + is-what "^3.1.1" + find-and-replace-anything@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/find-and-replace-anything/-/find-and-replace-anything-1.2.0.tgz#d2f2b69718da4e7aee363441d42485274cf79a2d"