From 13c532a7ed20810adf8515963ac12ab5083c9556 Mon Sep 17 00:00:00 2001 From: Mesqueeb Date: Fri, 6 Dec 2019 07:00:35 +0900 Subject: [PATCH 1/9] =?UTF-8?q?v1.35.1=20=F0=9F=8F=84=F0=9F=8F=BC=E2=80=8D?= =?UTF-8?q?=E2=99=82=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9d480576..3c6b446d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vuex-easy-firestore", - "version": "1.35.0", + "version": "1.35.1", "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", From 35306dfd0e350cebc53e673b4352417a1eab659c Mon Sep 17 00:00:00 2001 From: louisameline Date: Sat, 7 Dec 2019 00:57:59 +0100 Subject: [PATCH 2/9] use metadata in onSnapshot, fixes #172, #240 and #258 --- docs/query-data.md | 28 +++-- src/module/actions.ts | 268 +++++++++++++++++++++++++++++++----------- 2 files changed, 216 insertions(+), 80 deletions(-) diff --git a/docs/query-data.md b/docs/query-data.md index 9a5186c2..c0dce747 100644 --- a/docs/query-data.md +++ b/docs/query-data.md @@ -19,17 +19,17 @@ Both with _realtime updates_ and with _fetching docs_ you can use `where` filter ## Realtime updates: openDBChannel -If you want to use _realtime updates_ the only thing you need to do is to dispatch the `openDBChannel` action. Eg. +If you want to use _realtime updates_, you need to dispatch the `openDBChannel` action. Eg. ```js -dispatch('moduleName/openDBChannel').catch(console.error) +dispatch('moduleName/openDBChannel') ``` `openDBChannel` relies on the Firestore [onSnapshot](https://firebase.google.com/docs/firestore/query-data/listen) function to get notified of remote changes. The doc(s) are automatically added to/updated in your Vuex module under `moduleName/statePropName` Mind that: -- if the channel is opened on a document which does not exist yet at Firebase, the document gets automatically created, unless you set `preventInitialDocInsertion` to `true` -- the action returns a promise that lets you know you when the streaming through the channel has successfully started +- if the channel is opened on a document which does not exist yet at Firebase, the document gets automatically (re)created, unless you set `preventInitialDocInsertion` to `true` +- the action returns a promise which resolves when the state of the Vuex module has been populated with data (served either from cache or server), so you know you can start working on it - when this promise is resolved, it resolves with a function which wraps a promise that lets you know when an error occurs Just like the Firebase SDK, you can also use a [where filter](#where-orderby-filters). @@ -37,22 +37,28 @@ Just like the Firebase SDK, you can also use a [where filter](#where-orderby-fil ```js dispatch('moduleName/openDBChannel') .then(streaming => { - // streaming data through the channel has started (ie. data was received) + // the state has been populated with data, you may start working with it + startDoingThings() - // you must monitor if the channel keeps streaming to catch errors. Mind that - // even after going offline, the channel is still considered as "streaming" - // and will remain so until you close it or an error occurs. + // you must check that the channel keeps streaming and catch errors. Mind that + // even while offline, the channel is considered as "streaming" and will remain + // so until you close it or until an error occurs. streaming() .then(() => { // this gets resolved when you close the channel yourself }) .catch(error => { - // an error occured and the channel has been closed by Firestore, you - // should figure out what happened and open a new channel. + // an error occured and the channel has been closed, you should figure + // out what happened and open a new channel. + + // Perhaps the user lost his `read` rights on the resource, or maybe the + // document got deleted and it wasn't possible to recreate it (possibly + // because `preventInitialDocInsertion` is `false`). Or some other error + // from Firestore. }) }) .catch(error => { - // the document didn't exist and `preventInitialDocInsertion` is `false` + // Same as the other `catch` block above }) ``` diff --git a/src/module/actions.ts b/src/module/actions.ts index 7e61c883..122616f7 100644 --- a/src/module/actions.ts +++ b/src/module/actions.ts @@ -447,17 +447,67 @@ export default function (Firebase: any): AnyObject { } }) }, - openDBChannel ({getters, state, commit, dispatch}, pathVariables) { + openDBChannel ({getters, state, commit, dispatch}, parameters = {filters: {}, pathVariables: {}, includeMetadataChanges: false}) { + /* COMPATIBILITY START + * this ensures backward compatibility for people who passed pathVariables and + * filters directly at the root of the `parameters` object. Can be removed in + * a later version + */ + if (!parameters.filters && !parameters.pathVariables && parameters.includeMetadataChanges === undefined) { + const filters = parameters.filters || parameters + let pathVariables = parameters.pathVariables + if (pathVariables === undefined) { + pathVariables = Object.assign({}, parameters) + // @ts-ignore + delete pathVariables.where + // @ts-ignore + delete pathVariables.orderBy + Object.entries(pathVariables).forEach(entry => { + if (typeof entry[1] === 'object') { + delete pathVariables[entry[0]] + } + }) + } + parameters = Object.assign( + {includeMetadataChanges: parameters.includeMetadataChanges || false}, + {filters, pathVariables} + ) + } + /* COMPATIBILITY END */ + const defaultParameters = { + filters: {}, + pathVariables: {}, + includeMetadataChanges: false + } + parameters = Object.assign(defaultParameters, parameters) dispatch('setUserId') - // `firstCall` makes sure that local changes made during offline are reflected as server changes which the app is refreshed during offline mode - let firstCall = true - // set state for pathVariables - if (pathVariables && isPlainObject(pathVariables)) { - commit('SET_SYNCFILTERS', pathVariables) - delete pathVariables.where - delete pathVariables.orderBy - commit('SET_PATHVARS', pathVariables) + // creates promises that can be resolved from outside their scope and that + // can give their status + const nicePromise = ():any => { + const m = { + resolve: null, + reject: null, + isFulfilled: false, + isRejected: false, + isPending: true + } + const p = new Promise((resolve, reject) => { + m.resolve = resolve + m.reject = reject + }) + Object.assign(p, m) + p + // @ts-ignore + .then(() => p.isFulfilled = true) + // @ts-ignore + .catch(() => p.isRejected = true) + // @ts-ignore + .finally(() => p.isPending = false) + return p } + // set state for filters and pathVariables + commit('SET_SYNCFILTERS', parameters.filters) + commit('SET_PATHVARS', parameters.pathVariables) const identifier = createFetchIdentifier({ where: state._conf.sync.where, orderBy: state._conf.sync.orderBy, @@ -481,72 +531,152 @@ export default function (Firebase: any): AnyObject { dbRef = dbRef.orderBy(...state._conf.sync.orderBy) } } - // make a promise - return new Promise((resolve, reject) => { - // log - if (state._conf.logging) { - console.log(`%c openDBChannel for Firestore PATH: ${getters.firestorePathComplete} [${state._conf.firestorePath}]`, 'color: goldenrod') + // log + if (state._conf.logging) { + console.log(`%c openDBChannel for Firestore PATH: ${getters.firestorePathComplete} [${state._conf.firestorePath}]`, 'color: goldenrod') + } + const initialPromise = nicePromise() + const refreshedPromise = nicePromise() + const streamingPromise = nicePromise() + let gotFirstLocalResponse = false + let gotFirstServerResponse = false + const includeMetadataChanges = parameters.includeMetadataChanges + const streamingStart = () => { + // create a promise for the life of the snapshot that can be resolved from + // outside its scope. This promise will be resolved when the user calls + // closeDBChannel, or rejected if the stream is ended prematurely by the + // error() callback + state._sync.streaming[identifier] = streamingPromise + initialPromise.resolve({ + refreshed: includeMetadataChanges ? refreshedPromise : null, + streaming: streamingPromise, + stop: () => dispatch('closeDBChannel', { _identifier: identifier }) + }) + } + const streamingStop = error => { + // when this function is called by the error callback of onSnapshot, the + // subscription will actually already have been cancelled + unsubscribe() + if (initialPromise.isPending) { + initialPromise.reject(error) } - const okToStream = function () { - // create a promise for the life of the snapshot that can be resolved from outside its scope. - // this promise will be resolved when the user calls closeDBChannel, or rejected if the - // stream is ended prematurely by the error() callback - const promiseMethods = {resolve: null, reject: null} - const streaming = new Promise((_resolve, _reject) => { - promiseMethods.resolve = _resolve - promiseMethods.reject = _reject - }) - Object.assign(streaming, promiseMethods) - state._sync.streaming[identifier] = streaming - // we can't resolve the promise with a promise, it would hang, so we wrap it - resolve(() => streaming) + if (refreshedPromise.isPending) { + refreshedPromise.reject(error) } - const unsubscribe = dbRef.onSnapshot(async querySnapshot => { - const source = querySnapshot.metadata.hasPendingWrites ? 'local' : 'server' - // 'doc' mode: - if (!getters.collectionMode) { - if (!querySnapshot.data()) { - // No initial doc found in docMode - if (state._conf.sync.preventInitialDocInsertion) { - unsubscribe() - reject('preventInitialDocInsertion') - return + streamingPromise.reject(error) + state._sync.patching = 'error' + state._sync.unsubscribe[identifier] = null + state._sync.streaming[identifier] = null + } + const processDocument = data => { + const doc = getters.cleanUpRetrievedDoc(data, getters.docModeId) + dispatch('applyHooksAndUpdateState', {change: 'modified', id: getters.docModeId, doc}) + } + const processCollection = docChanges => { + docChanges.forEach(change => { + const doc = getters.cleanUpRetrievedDoc(change.doc.data(), change.doc.id) + dispatch('applyHooksAndUpdateState', {change: change.type, id: change.doc.id, doc}) + }) + } + const unsubscribe = dbRef.onSnapshot( + { includeMetadataChanges }, + async querySnapshot => { + // if the data comes from cache + if (querySnapshot.metadata.fromCache) { + // if it's the very first call, we are at the initial app load. If so, we'll use + // the data in cache (if available) to populate the state. + // if it's not, this is only the result of a local modification which does not + // require to do anything else. + if (!gotFirstLocalResponse) { + // 'doc' mode: + if (!getters.collectionMode) { + // note: we don't want to insert a document ever when the data comes from cache, + // and we don't want to start the app if the data doesn't exist (no persistence) + if (querySnapshot.data()) { + processDocument(querySnapshot.data()) + streamingStart() + } + } + // 'collection' mode + else { + processCollection(querySnapshot.docChanges()) + streamingStart() } - if (state._conf.logging) console.log('[vuex-easy-firestore] inserting initial doc') - await dispatch('insertInitialDoc') - okToStream() - return + gotFirstLocalResponse = true } - if (source === 'local' && !firstCall) return - const id = getters.docModeId - const doc = getters.cleanUpRetrievedDoc(querySnapshot.data(), id) - dispatch('applyHooksAndUpdateState', {change: 'modified', id, doc}) - firstCall = false - okToStream() - return } - // 'collection' mode: - querySnapshot.docChanges().forEach(change => { - const changeType = change.type - // Don't do anything for local modifications & removals - if (source === 'local' && !firstCall) return - const id = change.doc.id - const doc = getters.cleanUpRetrievedDoc(change.doc.data(), id) - dispatch('applyHooksAndUpdateState', {change: changeType, id, doc}) - }) - firstCall = false - okToStream() - }, error => { - state._sync.patching = 'error' - state._sync.streaming[identifier].reject(error) - state._sync.streaming[identifier] = null - state._sync.unsubscribe[identifier] = null - }) - state._sync.unsubscribe[identifier] = unsubscribe - }) + // if data comes from server + else { + // 'doc' mode: + if (!getters.collectionMode) { + // if the document doesn't exist yet + if (!querySnapshot.data()) { + // if it's ok to insert an initial document + if (!state._conf.sync.preventInitialDocInsertion) { + if (state._conf.logging) { + const message = gotFirstServerResponse + ? '[vuex-easy-firestore] recreating doc after remote deletion' + : '[vuex-easy-firestore] inserting initial doc' + console.log(message) + } + const resp = await dispatch('insertInitialDoc') + // if the initial document was successfully inserted + if (!resp) { + if (initialPromise.isPending) { + streamingStart() + } + if (refreshedPromise.isPending) { + refreshedPromise.resolve() + } + } + else { + // we close the channel ourselves. Firestore does not, as it leaves the + // channel open as long as the user has read rights on the document, even + // if it does not exist. But since the dev enabled `insertInitialDoc`, + // it makes some sense to close as we can assume the user should have had + // write rights + streamingStop('failedRecreatingDoc') + } + } + // we are not allowed to (re)create the doc: close the channel and reject + else { + streamingStop('preventInitialDocInsertion') + } + } + // the remote document exists: apply to the local store + else { + processDocument(querySnapshot.data()) + if (initialPromise.isPending) { + streamingStart() + } + // the promise should still be pending at this point only if there is no persistence, + // as only then the first call to our listener will have `fromCache` === `false` + if (refreshedPromise.isPending) { + refreshedPromise.resolve() + } + } + } + // 'collection' mode: + else { + processCollection(querySnapshot.docChanges()) + if (initialPromise.isPending) { + streamingStart() + } + if (refreshedPromise.isPending) { + refreshedPromise.resolve() + } + } + gotFirstServerResponse = true + } + }, + streamingStop + ) + state._sync.unsubscribe[identifier] = unsubscribe + + return initialPromise }, - closeDBChannel ({getters, state, commit, dispatch}, { clearModule = false } = { clearModule: false }) { - const identifier = createFetchIdentifier({ + closeDBChannel ({getters, state, commit, dispatch}, { clearModule = false, _identifier = null } = { clearModule: false, _identifier: null }) { + const identifier = _identifier || createFetchIdentifier({ where: state._conf.sync.where, orderBy: state._conf.sync.orderBy, pathVariables: state._sync.pathVariables From 6904ad27c2cbd38d8e3800a44f5317ef7bd4f16d Mon Sep 17 00:00:00 2001 From: louisameline Date: Mon, 9 Dec 2019 00:06:00 +0100 Subject: [PATCH 3/9] new syntax for fetch actions + update documentation --- docs/README.md | 2 +- docs/config-example.md | 2 +- docs/extra-features.md | 22 ++++---- docs/query-data.md | 75 +++++++++++++++++------- src/module/actions.ts | 110 ++++++++++++++++++++++-------------- src/module/defaultConfig.ts | 2 +- src/module/mutations.ts | 2 +- src/utils/apiHelpers.ts | 2 +- test/DBChannel.js | 4 +- test/initialDoc.js | 2 +- test/mutations.js | 4 +- 11 files changed, 144 insertions(+), 83 deletions(-) diff --git a/docs/README.md b/docs/README.md index ebc036bc..82274c8a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -40,7 +40,7 @@ Now you just update and add docs with `dispatch('userData/set', newItem)` and fo - [Hooks](hooks.html#hooks) (before / after sync) - [Fillables / guard](extra-features.html#fillables-and-guard) (limit fields which will sync) - [Timestamp conversion to Date()](extra-features.html#firestore-timestamp-conversion) -- [Where / orderBy filters](query-data.html#where-orderby-filters) +- [Where / orderBy clauses](query-data.html#where-orderby-clauses) # Motivation diff --git a/docs/config-example.md b/docs/config-example.md index 392fc3b6..22c94a42 100644 --- a/docs/config-example.md +++ b/docs/config-example.md @@ -48,7 +48,7 @@ const firestoreModule = { removedHook: function (updateStore, doc, id, store) { return updateStore(doc) }, }, - // When docs are fetched through `dispatch('module/fetch', filters)`. + // When docs are fetched through `dispatch('module/fetch', {clauses})`. fetch: { // The max amount of documents to be fetched. Defaults to 50. docLimit: 50, diff --git a/docs/extra-features.md b/docs/extra-features.md index f9661ae3..92c7148e 100644 --- a/docs/extra-features.md +++ b/docs/extra-features.md @@ -1,8 +1,8 @@ # Extra features -## Variables for firestorePath or filters +## Variables for firestorePath or clauses -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. +Besides `'{userId}'` in your `firestorePath` in the config or in `where` clauses, you can also use **any variable** in the `firestorePath` or the `where` filter. ```js // your vuex module @@ -16,10 +16,10 @@ You can replace a path variable with the actual string by: ```js // 1. Passing it as a parameter to openDBChannel -dispatch('groupUserData/openDBChannel', {groupId: 'group-A'}) +dispatch('groupUserData/openDBChannel', {pathVariables: {groupId: 'group-A'}}) // 2. Passing it as a parameter to fetchAndAdd -dispatch('groupUserData/fetchAndAdd', {groupId: 'group-A'}) +dispatch('groupUserData/fetchAndAdd', {pathVariables: {groupId: 'group-A'}}) // 3. Dispatching setPathVars dispatch('groupUserData/setPathVars', {groupId: 'group-A'}) @@ -50,11 +50,11 @@ store.dispatch('userData/openDBChannel') // Then we can get the groupId: const userGroupId = store.state.userData.groupId // Then we can pass it as variable to the next openDBChannel: - store.dispatch('groupUserData/openDBChannel', {groupId: userGroupId}) + store.dispatch('groupUserData/openDBChannel', {pathVariables: {groupId: userGroupId}}) }) ``` -### Use case: Retieve data based on the Vue Router path +### Use case: Retrieve data based on the Vue Router path This is a great use case! But it has a good and a bad implementation. I'll go over both so you can see what I mean: @@ -74,7 +74,7 @@ SpecificGroupUserModule: { export default { created () { const pageId = this.$route.params.id - this.$store.dispatch('page/fetchAndAdd', {pageId}) + this.$store.dispatch('page/fetchAndAdd', {pathVariables: {pageId}}) }, computed: { openDoc () { @@ -84,14 +84,12 @@ export default { } ``` -The above example shows a Vuex module linked to a single doc, but this path is changed every time the user opens a page and then the doc is retrieved. The reasons not to do this are: - -- When opening a new page you will need to release the previous doc from memory every time, so when the user goes back you will be charged with a read every single time! -- Please see [this thread](https://github.com/mesqueeb/vuex-easy-firestore/issues/172) for problems when there's an internet interruption. +The above example shows a Vuex module linked to a single doc, but this path is changed every time the user opens a page and then the doc is retrieved. +When opening a new page you will need to release the previous doc from memory every time, so when the user goes back you will be charged with a read again. #### Good implementation of Vue Router -Instead, use 'collection' mode! This way you can keep the pages that were openend already and opening those pages again is much faster. That implementation would look like this: +Instead, use 'collection' mode! This way you can keep the pages that were opened already and opening those pages again is much faster. That implementation would look like this: ```js // MUCH BETTER: diff --git a/docs/query-data.md b/docs/query-data.md index c0dce747..b4d588e3 100644 --- a/docs/query-data.md +++ b/docs/query-data.md @@ -15,28 +15,28 @@ With Vuex easy firestore using _realtime updates_ will effectively make **a 2-wa Fetching the document(s) once is when you want to retrieve the document(s) once, when your application or a page is opened, but do not require to have the data to be live updated when the server data changes. -Both with _realtime updates_ and with _fetching docs_ you can use `where` filters to specify which docs you want to retrieve (just like Firestore). In some modules you might initially open a channel for _realtime updates_ with a certain `where` filter, and later when the user requests other docs do an additional `fetch` with another `where` filter. +Both with _realtime updates_ and with _fetching docs_ you can use `where` clauses to specify which docs you want to retrieve (just like Firestore). In some modules you might initially open a channel for _realtime updates_ with a certain `where` clause, and later when the user requests other docs do an additional `fetch` with another `where` clause. ## Realtime updates: openDBChannel -If you want to use _realtime updates_, you need to dispatch the `openDBChannel` action. Eg. +If you want to get _realtime updates_, you need to dispatch the `openDBChannel` action. Eg. ```js dispatch('moduleName/openDBChannel') ``` -`openDBChannel` relies on the Firestore [onSnapshot](https://firebase.google.com/docs/firestore/query-data/listen) function to get notified of remote changes. The doc(s) are automatically added to/updated in your Vuex module under `moduleName/statePropName` +`openDBChannel` relies on the Firestore [onSnapshot](https://firebase.google.com/docs/firestore/query-data/listen) function to get notified of remote changes. The doc(s) are automatically added to/updated in your Vuex module under `moduleName/statePropName`. Mind that: - if the channel is opened on a document which does not exist yet at Firebase, the document gets automatically (re)created, unless you set `preventInitialDocInsertion` to `true` -- the action returns a promise which resolves when the state of the Vuex module has been populated with data (served either from cache or server), so you know you can start working on it -- when this promise is resolved, it resolves with a function which wraps a promise that lets you know when an error occurs +- the action returns a promise which resolves when the state of the module has been populated with data (served either from cache or server), so you know you can start working with it +- when this promise is resolved, you get another one which lets you know when an error occurs -Just like the Firebase SDK, you can also use a [where filter](#where-orderby-filters). +Just like the Firebase SDK, you can also use a [`where` clause](#where-orderby-clauses). ```js dispatch('moduleName/openDBChannel') - .then(streaming => { + .then(({streaming}) => { // the state has been populated with data, you may start working with it startDoingThings() @@ -62,6 +62,39 @@ dispatch('moduleName/openDBChannel') }) ``` +Sometimes the promise returned by the action might not be enough for you, because you need to know when the module has been populated with fresh data, not cached data. In that case, you need to pass an option when calling the action, which in turn will provide you with an additional promise. This, however, will also trigger your server hook listeners more frequently. Read [Firestore's documentation](https://firebase.google.com/docs/firestore/query-data/listen#events-metadata-changes) to know more about this. + +```js +dispatch('moduleName/openDBChannel', {includeMetadataChanges: true}) + .then(({refreshed, streaming}) => { + + refreshed + .then(() => { + // the state has been populated with fresh data + }) + .catch(error => {...}) + + streaming() + .then(() => {...}) + .catch(error => {...}) + }) + .catch(error => {...}) +``` + +Finally, if you open multiple channels on a same module with different clauses, you will need to use the `stop` method provided in the promise to handle their closing at your discretion: + +```js +dispatch('moduleName/openDBChannel') + .then(({streaming, stop}) => { + + stop().then(() => { + // the channel has been closed + }) + }) + .catch(error => {...}) +``` + + ### Close DB channel In some cases you need to close the connection to Firestore (unsubscribe from Firestore's `onSnapshot` listener). Eg. when your user signs out. In this case, make sure you call `closeDBChannel` like so: @@ -126,7 +159,7 @@ Or you could retrieve all Pokémon like so: dispatch('pokemon/fetchAndAdd') ``` -Of course, just like the Firebase SDK, you can also use a `where` filter to retrieve eg. all water Pokémon. (Read more on [where filters](#where-orderby-filters) down below) +Of course, just like the Firebase SDK, you can also use a `where` clause to retrieve eg. all water Pokémon. (Read more on [where clauses](#where-orderby-clauses) down below) ```js dispatch('pokemon/fetchAndAdd', {where: [['type', '==', 'water']]}) @@ -148,9 +181,9 @@ You can pass a custom fetch limit or disable the fetch limit by passing 0: ```js // custom fetch limit: -dispatch('myModule/fetchAndAdd', {limit: 1000}) +dispatch('myModule/fetchAndAdd', {clauses: {limit: 1000}}) // no fetch limit: -dispatch('myModule/fetchAndAdd', {limit: 0}) +dispatch('myModule/fetchAndAdd', {clauses: {limit: 0}}) ``` The `fetchAndAdd` action will return a promise resolving in `{done: true}` if there are no more docs to be fetched. You can use this to check when to stop fetching like so: @@ -209,11 +242,11 @@ Firebase.auth().onAuthStateChanged(user => { When required you can also manually pass a user id like so: `dispatch('userData/setUserId', id)` -## where / orderBy filters +## where / orderBy clauses > Only for 'collection' mode. -Just like Firestore, you can use `where` and `orderBy` to filter which docs are retrieved and synced. +Just like Firestore, you can use `where` and `orderBy` to clauses 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)` @@ -221,23 +254,25 @@ Just like Firestore, you can use `where` and `orderBy` to filter which docs are There are three ways to use where and orderBy. As per example we will define `where` and `orderBy` variables first, then show how you can use them: ```js -const where = [ // an array of arrays +// an array of arrays +const where = [ ['some_field', '==', false], ['another_field', '==', true], ] -const orderBy = ['created_at'] // or more params +// can have several parameters +const orderBy = ['created_at'] ``` 1. Pass together with openDBChannel: ```js -dispatch('myModule/openDBChannel', {where, orderBy}) +dispatch('myModule/openDBChannel', {clauses: {where, orderBy}}) ``` 2. Pass together with fetchAndAdd: ```js -dispatch('myModule/fetchAndAdd', {where, orderBy}) +dispatch('myModule/fetchAndAdd', {clauses: {where, orderBy}}) ``` 3. Define inside your vuex module, to set as default when for `openDBChannel`: @@ -276,7 +311,7 @@ getters: { ### userId in where/orderBy -You can also use variables like `userId` (of the authenticated user) inside where filters. Eg. +You can also use variables like `userId` (of the authenticated user) inside where clauses. Eg. ```js store.dispatch('myModule/openDBChannel', { @@ -286,7 +321,7 @@ store.dispatch('myModule/openDBChannel', { `{userId}` will be automatically replaced with the authenticated user id. -Besides `userId` you can also use "custom variables". For more information on this, see the chapter on [variables for firestorePath or filters](extra-features.html#variables-for-firestorepath-or-filters). +Besides `userId` you can also use "custom variables". For more information on this, see the chapter on [variables for firestorePath or clauses](extra-features.html#variables-for-firestorepath-or-clauses). ### Example usage: openDBChannel and fetchAndAdd @@ -453,7 +488,7 @@ Please note, just like [fetchAndAdd](#fetching-docs) explained above, `fetch` al ```js // custom fetch limit: -dispatch('myModule/fetch', {limit: 1000}) +dispatch('myModule/fetch', {clauses: {limit: 1000}}) // no fetch limit: -dispatch('myModule/fetch', {limit: 0}) +dispatch('myModule/fetch', {clauses: {limit: 0}}) ``` diff --git a/src/module/actions.ts b/src/module/actions.ts index 122616f7..dc866a79 100644 --- a/src/module/actions.ts +++ b/src/module/actions.ts @@ -239,19 +239,34 @@ export default function (Firebase: any): AnyObject { }, fetch ( {state, getters, commit, dispatch}, - pathVariables: AnyObject = {where: [], whereFilters: [], orderBy: []} - // where: [['archived', '==', true]] - // orderBy: ['done_date', 'desc'] + parameters:any = {clauses: {}, pathVariables: {}} ) { + /* COMPATIBILITY START + * this ensures backward compatibility for people who passed pathVariables and + * clauses directly at the root of the `parameters` object. Can be removed in + * a later version + */ + if (!parameters.clauses && !parameters.pathVariables) { + const pathVariables = Object.assign({}, parameters) + // @ts-ignore + delete pathVariables.where + // @ts-ignore + delete pathVariables.orderBy + Object.entries(pathVariables).forEach(entry => { + if (typeof entry[1] === 'object') { + delete pathVariables[entry[0]] + } + }) + parameters = Object.assign({}, {clauses: parameters, pathVariables}) + } + /* COMPATIBILITY END */ if (!getters.collectionMode) return logError('only-in-collection-mode') dispatch('setUserId') - let {where, whereFilters, orderBy} = pathVariables + let {where, whereFilters, orderBy} = parameters.clauses if (!isArray(where)) where = [] if (!isArray(orderBy)) orderBy = [] - if (isArray(whereFilters) && whereFilters.length) where = whereFilters // depreciated - if (pathVariables && isPlainObject(pathVariables)) { - commit('SET_PATHVARS', pathVariables) - } + if (isArray(whereFilters) && whereFilters.length) where = whereFilters // deprecated + commit('SET_PATHVARS', parameters.pathVariables) return new Promise((resolve, reject) => { // log if (state._conf.logging) { @@ -267,7 +282,7 @@ export default function (Firebase: any): AnyObject { // We've never fetched this before: if (!fetched) { let ref = getters.dbRef - // apply where filters and orderBy + // apply where clauses and orderBy getters.getWhereArrays(where).forEach(paramsArr => { ref = ref.where(...paramsArr) }) @@ -285,15 +300,15 @@ export default function (Firebase: any): AnyObject { if (state._conf.logging) console.log('[vuex-easy-firestore] done fetching') return resolve({done: true}) } - // attach fetch filters + // attach fetch clauses let fRef = state._sync.fetched[identifier].ref if (fRequest.nextFetchRef) { // get next ref if saved in state fRef = state._sync.fetched[identifier].nextFetchRef } // add doc limit - let limit = (isNumber(pathVariables.limit)) - ? pathVariables.limit + let limit = (isNumber(parameters.clauses.limit)) + ? parameters.clauses.limit : state._conf.fetch.docLimit if (limit > 0) fRef = fRef.limit(limit) // Stop if all records already fetched @@ -324,15 +339,32 @@ export default function (Firebase: any): AnyObject { }) }) }, + // where: [['archived', '==', true]] + // orderBy: ['done_date', 'desc'] fetchAndAdd ( {state, getters, commit, dispatch}, - pathVariables = {where: [], whereFilters: [], orderBy: []} - // where: [['archived', '==', true]] - // orderBy: ['done_date', 'desc'] + parameters = {clauses: {}, pathVariables: {}} ) { - if (pathVariables && isPlainObject(pathVariables)) { - commit('SET_PATHVARS', pathVariables) + /* COMPATIBILITY START + * this ensures backward compatibility for people who passed pathVariables and + * clauses directly at the root of the `parameters` object. Can be removed in + * a later version + */ + if (!parameters.clauses && !parameters.pathVariables) { + const pathVariables = Object.assign({}, parameters) + // @ts-ignore + delete pathVariables.where + // @ts-ignore + delete pathVariables.orderBy + Object.entries(pathVariables).forEach(entry => { + if (typeof entry[1] === 'object') { + delete pathVariables[entry[0]] + } + }) + parameters = Object.assign({}, {clauses: parameters, pathVariables}) } + /* COMPATIBILITY END */ + commit('SET_PATHVARS', parameters.pathVariables) // 'doc' mode: if (!getters.collectionMode) { dispatch('setUserId') @@ -356,7 +388,7 @@ export default function (Firebase: any): AnyObject { }) } // 'collection' mode: - return dispatch('fetch', pathVariables) + return dispatch('fetch', parameters) .then(querySnapshot => { if (querySnapshot.done === true) return querySnapshot if (isFunction(querySnapshot.forEach)) { @@ -447,35 +479,31 @@ export default function (Firebase: any): AnyObject { } }) }, - openDBChannel ({getters, state, commit, dispatch}, parameters = {filters: {}, pathVariables: {}, includeMetadataChanges: false}) { + openDBChannel ({getters, state, commit, dispatch}, parameters = {clauses: {}, pathVariables: {}, includeMetadataChanges: false}) { /* COMPATIBILITY START * this ensures backward compatibility for people who passed pathVariables and - * filters directly at the root of the `parameters` object. Can be removed in + * clauses directly at the root of the `parameters` object. Can be removed in * a later version */ - if (!parameters.filters && !parameters.pathVariables && parameters.includeMetadataChanges === undefined) { - const filters = parameters.filters || parameters - let pathVariables = parameters.pathVariables - if (pathVariables === undefined) { - pathVariables = Object.assign({}, parameters) - // @ts-ignore - delete pathVariables.where - // @ts-ignore - delete pathVariables.orderBy - Object.entries(pathVariables).forEach(entry => { - if (typeof entry[1] === 'object') { - delete pathVariables[entry[0]] - } - }) - } + if (!parameters.clauses && !parameters.pathVariables && parameters.includeMetadataChanges === undefined) { + const pathVariables = Object.assign({}, parameters) + // @ts-ignore + delete pathVariables.where + // @ts-ignore + delete pathVariables.orderBy + Object.entries(pathVariables).forEach(entry => { + if (typeof entry[1] === 'object') { + delete pathVariables[entry[0]] + } + }) parameters = Object.assign( {includeMetadataChanges: parameters.includeMetadataChanges || false}, - {filters, pathVariables} + {clauses: parameters, pathVariables} ) } /* COMPATIBILITY END */ const defaultParameters = { - filters: {}, + clauses: {}, pathVariables: {}, includeMetadataChanges: false } @@ -505,8 +533,8 @@ export default function (Firebase: any): AnyObject { .finally(() => p.isPending = false) return p } - // set state for filters and pathVariables - commit('SET_SYNCFILTERS', parameters.filters) + // set state for clauses and pathVariables + commit('SET_SYNCCLAUSES', parameters.clauses) commit('SET_PATHVARS', parameters.pathVariables) const identifier = createFetchIdentifier({ where: state._conf.sync.where, @@ -514,7 +542,7 @@ export default function (Firebase: any): AnyObject { pathVariables: state._sync.pathVariables }) if (isFunction(state._sync.unsubscribe[identifier])) { - const channelAlreadyOpenError = `openDBChannel was already called for these filters and pathvariables. Identifier: ${identifier}` + const channelAlreadyOpenError = `openDBChannel was already called for these clauses and pathvariables. Identifier: ${identifier}` if (state._conf.logging) { console.log(channelAlreadyOpenError) } @@ -522,7 +550,7 @@ export default function (Firebase: any): AnyObject { } // getters.dbRef should already have pathVariables swapped out let dbRef = getters.dbRef - // apply where filters and orderBy + // apply where and orderBy clauses if (getters.collectionMode) { getters.getWhereArrays().forEach(whereParams => { dbRef = dbRef.where(...whereParams) diff --git a/src/module/defaultConfig.ts b/src/module/defaultConfig.ts index d69e0192..e920cbad 100644 --- a/src/module/defaultConfig.ts +++ b/src/module/defaultConfig.ts @@ -96,7 +96,7 @@ export default { removedHook: function (updateStore, doc, id, store) { return updateStore(doc) }, }, - // When items are fetched through `dispatch('module/fetch', filters)`. + // When items are fetched through `dispatch('module/fetch', {clauses})`. fetch: { // The max amount of documents to be fetched. Defaults to 50. docLimit: 50, diff --git a/src/module/mutations.ts b/src/module/mutations.ts index 757b1b9d..3b8524c1 100644 --- a/src/module/mutations.ts +++ b/src/module/mutations.ts @@ -24,7 +24,7 @@ export default function (userState: object): AnyObject { self._vm.$set(state._sync.pathVariables, key, pathPiece) }) }, - SET_SYNCFILTERS (state, {where, orderBy}) { + SET_SYNCCLAUSES (state, {where, orderBy}) { if (where && isArray(where)) state._conf.sync.where = where if (orderBy && isArray(orderBy)) state._conf.sync.orderBy = orderBy }, diff --git a/src/utils/apiHelpers.ts b/src/utils/apiHelpers.ts index 89a02128..b67ce89e 100644 --- a/src/utils/apiHelpers.ts +++ b/src/utils/apiHelpers.ts @@ -180,7 +180,7 @@ function stringifyParams (params: any[]): string { } /** - * Gets an object with {where, orderBy} filters and returns a unique identifier for that + * Gets an object with {where, orderBy} clauses and returns a unique identifier for that * * @export * @param {AnyObject} [whereOrderBy={}] whereOrderBy {where, orderBy, pathVariables} diff --git a/test/DBChannel.js b/test/DBChannel.js index ff523d92..acb3437a 100644 --- a/test/DBChannel.js +++ b/test/DBChannel.js @@ -97,7 +97,7 @@ test('[openDBChannel] open multiple times', async t => { try { await store.dispatch('multipleOpenDBChannels/openDBChannel') } catch (e) { - t.is(e, `openDBChannel was already called for these filters and pathvariables. Identifier: [where][orderBy][pathVariables]{}`) + t.is(e, `openDBChannel was already called for these clauses and pathvariables. Identifier: [where][orderBy][pathVariables]{}`) } try { await store.dispatch('multipleOpenDBChannels/openDBChannel', { name: 'Lucaz' }) @@ -112,7 +112,7 @@ test('[openDBChannel] open multiple times', async t => { try { await store.dispatch('multipleOpenDBChannels/openDBChannel', { name: 'Lucas' }) } catch (e) { - t.is(e, `openDBChannel was already called for these filters and pathvariables. Identifier: [where][orderBy][pathVariables]{"name":"Lucas"}`) + t.is(e, `openDBChannel was already called for these clauses and pathvariables. Identifier: [where][orderBy][pathVariables]{"name":"Lucas"}`) } }) diff --git a/test/initialDoc.js b/test/initialDoc.js index 1fba1403..85710928 100644 --- a/test/initialDoc.js +++ b/test/initialDoc.js @@ -40,7 +40,7 @@ test('initialDoc through openDBRef & fetchAndAdd', async t => { docR = await Firebase.firestore().doc(path).get() t.is(docR.exists, false) try { - store.dispatch('initialDoc/fetchAndAdd', {randomId: randomId2}) + store.dispatch('initialDoc/fetchAndAdd', {pathVariables: {randomId: randomId2}}) } catch (error) { t.fail() } diff --git a/test/mutations.js b/test/mutations.js index 65ec489a..cc08eb5c 100644 --- a/test/mutations.js +++ b/test/mutations.js @@ -94,11 +94,11 @@ test('RESET_VUEX_EASY_FIRESTORE_STATE', t => { t.falsy(noStatePropModule.rain) }) -test('SET_SYNCFILTERS', t => { +test('SET_SYNCCLAUSES', t => { const sync = char._conf.sync t.deepEqual(sync.where, []) t.deepEqual(sync.orderBy, []) - store.commit('mainCharacter/SET_SYNCFILTERS', { + store.commit('mainCharacter/SET_SYNCCLAUSES', { where: [['hi.{userId}.docs.{nr}', '==', '{big}'], ['{userId}', '==', '{userId}']], orderBy: ['date'] }) From 6fc9132bda9267e50693bb636f3ff9de87bd3cc6 Mon Sep 17 00:00:00 2001 From: louisameline Date: Mon, 9 Dec 2019 00:46:41 +0100 Subject: [PATCH 4/9] build --- dist/index.cjs.js | 358 ++++++++++++++++++++++---------- dist/index.esm.js | 358 ++++++++++++++++++++++---------- test/helpers/index.cjs.js | 358 ++++++++++++++++++++++---------- types/src/utils/apiHelpers.d.ts | 2 +- types/utils/apiHelpers.d.ts | 2 +- 5 files changed, 752 insertions(+), 326 deletions(-) diff --git a/dist/index.cjs.js b/dist/index.cjs.js index 6a49c9c3..4f64a8ca 100644 --- a/dist/index.cjs.js +++ b/dist/index.cjs.js @@ -57,7 +57,7 @@ var defaultConfig = { 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)`. + // When items are fetched through `dispatch('module/fetch', {clauses})`. fetch: { // The max amount of documents to be fetched. Defaults to 50. docLimit: 50, @@ -298,7 +298,7 @@ function pluginMutations (userState) { self._vm.$set(state._sync.pathVariables, key, pathPiece); }); }, - SET_SYNCFILTERS: function (state, _a) { + SET_SYNCCLAUSES: function (state, _a) { var where = _a.where, orderBy = _a.orderBy; if (where && isWhat.isArray(where)) state._conf.sync.where = where; @@ -643,7 +643,7 @@ function stringifyParams(params) { }).join(); } /** - * Gets an object with {where, orderBy} filters and returns a unique identifier for that + * Gets an object with {where, orderBy} clauses and returns a unique identifier for that * * @export * @param {AnyObject} [whereOrderBy={}] whereOrderBy {where, orderBy, pathVariables} @@ -956,25 +956,39 @@ function pluginActions (Firebase) { }); }); }, - fetch: function (_a, pathVariables - // where: [['archived', '==', true]] - // orderBy: ['done_date', 'desc'] - ) { + fetch: function (_a, parameters) { var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; - if (pathVariables === void 0) { pathVariables = { where: [], whereFilters: [], orderBy: [] }; } + if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {} }; } + /* COMPATIBILITY START + * this ensures backward compatibility for people who passed pathVariables and + * clauses directly at the root of the `parameters` object. Can be removed in + * a later version + */ + if (!parameters.clauses && !parameters.pathVariables) { + var pathVariables_1 = Object.assign({}, parameters); + // @ts-ignore + delete pathVariables_1.where; + // @ts-ignore + delete pathVariables_1.orderBy; + Object.entries(pathVariables_1).forEach(function (entry) { + if (typeof entry[1] === 'object') { + delete pathVariables_1[entry[0]]; + } + }); + parameters = Object.assign({}, { clauses: parameters, pathVariables: pathVariables_1 }); + } + /* COMPATIBILITY END */ if (!getters.collectionMode) return error('only-in-collection-mode'); dispatch('setUserId'); - var where = pathVariables.where, whereFilters = pathVariables.whereFilters, orderBy = pathVariables.orderBy; + var _b = parameters.clauses, where = _b.where, whereFilters = _b.whereFilters, orderBy = _b.orderBy; if (!isWhat.isArray(where)) where = []; if (!isWhat.isArray(orderBy)) orderBy = []; if (isWhat.isArray(whereFilters) && whereFilters.length) - where = whereFilters; // depreciated - if (pathVariables && isWhat.isPlainObject(pathVariables)) { - commit('SET_PATHVARS', pathVariables); - } + where = whereFilters; // deprecated + commit('SET_PATHVARS', parameters.pathVariables); return new Promise(function (resolve, reject) { // log if (state._conf.logging) { @@ -991,7 +1005,7 @@ function pluginActions (Firebase) { // We've never fetched this before: if (!fetched) { var ref_1 = getters.dbRef; - // apply where filters and orderBy + // apply where clauses and orderBy getters.getWhereArrays(where).forEach(function (paramsArr) { ref_1 = ref_1.where.apply(ref_1, paramsArr); }); @@ -1011,15 +1025,15 @@ function pluginActions (Firebase) { console.log('[vuex-easy-firestore] done fetching'); return resolve({ done: true }); } - // attach fetch filters + // attach fetch clauses var fRef = state._sync.fetched[identifier].ref; if (fRequest.nextFetchRef) { // get next ref if saved in state fRef = state._sync.fetched[identifier].nextFetchRef; } // add doc limit - var limit = (isWhat.isNumber(pathVariables.limit)) - ? pathVariables.limit + var limit = (isWhat.isNumber(parameters.clauses.limit)) + ? parameters.clauses.limit : state._conf.fetch.docLimit; if (limit > 0) fRef = fRef.limit(limit); @@ -1051,16 +1065,32 @@ function pluginActions (Firebase) { }); }); }, - fetchAndAdd: function (_a, pathVariables // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] - ) { + fetchAndAdd: function (_a, parameters) { var _this = this; var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; - if (pathVariables === void 0) { pathVariables = { where: [], whereFilters: [], orderBy: [] }; } - if (pathVariables && isWhat.isPlainObject(pathVariables)) { - commit('SET_PATHVARS', pathVariables); + if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {} }; } + /* COMPATIBILITY START + * this ensures backward compatibility for people who passed pathVariables and + * clauses directly at the root of the `parameters` object. Can be removed in + * a later version + */ + if (!parameters.clauses && !parameters.pathVariables) { + var pathVariables_2 = Object.assign({}, parameters); + // @ts-ignore + delete pathVariables_2.where; + // @ts-ignore + delete pathVariables_2.orderBy; + Object.entries(pathVariables_2).forEach(function (entry) { + if (typeof entry[1] === 'object') { + delete pathVariables_2[entry[0]]; + } + }); + parameters = Object.assign({}, { clauses: parameters, pathVariables: pathVariables_2 }); } + /* COMPATIBILITY END */ + commit('SET_PATHVARS', parameters.pathVariables); // 'doc' mode: if (!getters.collectionMode) { dispatch('setUserId'); @@ -1094,7 +1124,7 @@ function pluginActions (Firebase) { }); } // 'collection' mode: - return dispatch('fetch', pathVariables) + return dispatch('fetch', parameters) .then(function (querySnapshot) { if (querySnapshot.done === true) return querySnapshot; @@ -1203,26 +1233,70 @@ function pluginActions (Firebase) { } }); }, - openDBChannel: function (_a, pathVariables) { + openDBChannel: function (_a, parameters) { var _this = this; var getters = _a.getters, state = _a.state, commit = _a.commit, dispatch = _a.dispatch; - dispatch('setUserId'); - // `firstCall` makes sure that local changes made during offline are reflected as server changes which the app is refreshed during offline mode - var firstCall = true; - // set state for pathVariables - if (pathVariables && isWhat.isPlainObject(pathVariables)) { - commit('SET_SYNCFILTERS', pathVariables); - delete pathVariables.where; - delete pathVariables.orderBy; - commit('SET_PATHVARS', pathVariables); + if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {}, includeMetadataChanges: false }; } + /* COMPATIBILITY START + * this ensures backward compatibility for people who passed pathVariables and + * clauses directly at the root of the `parameters` object. Can be removed in + * a later version + */ + if (!parameters.clauses && !parameters.pathVariables && parameters.includeMetadataChanges === undefined) { + var pathVariables_3 = Object.assign({}, parameters); + // @ts-ignore + delete pathVariables_3.where; + // @ts-ignore + delete pathVariables_3.orderBy; + Object.entries(pathVariables_3).forEach(function (entry) { + if (typeof entry[1] === 'object') { + delete pathVariables_3[entry[0]]; + } + }); + parameters = Object.assign({ includeMetadataChanges: parameters.includeMetadataChanges || false }, { clauses: parameters, pathVariables: pathVariables_3 }); } + /* COMPATIBILITY END */ + var defaultParameters = { + clauses: {}, + pathVariables: {}, + includeMetadataChanges: false + }; + parameters = Object.assign(defaultParameters, parameters); + dispatch('setUserId'); + // creates promises that can be resolved from outside their scope and that + // can give their status + var nicePromise = function () { + var m = { + resolve: null, + reject: null, + isFulfilled: false, + isRejected: false, + isPending: true + }; + var p = new Promise(function (resolve, reject) { + m.resolve = resolve; + m.reject = reject; + }); + Object.assign(p, m); + p + // @ts-ignore + .then(function () { return p.isFulfilled = true; }) + // @ts-ignore + .catch(function () { return p.isRejected = true; }) + // @ts-ignore + .finally(function () { return p.isPending = false; }); + return p; + }; + // set state for clauses and pathVariables + commit('SET_SYNCCLAUSES', parameters.clauses); + commit('SET_PATHVARS', parameters.pathVariables); var identifier = createFetchIdentifier({ where: state._conf.sync.where, orderBy: state._conf.sync.orderBy, pathVariables: state._sync.pathVariables }); if (isWhat.isFunction(state._sync.unsubscribe[identifier])) { - var channelAlreadyOpenError = "openDBChannel was already called for these filters and pathvariables. Identifier: " + identifier; + var channelAlreadyOpenError = "openDBChannel was already called for these clauses and pathvariables. Identifier: " + identifier; if (state._conf.logging) { console.log(channelAlreadyOpenError); } @@ -1230,7 +1304,7 @@ function pluginActions (Firebase) { } // getters.dbRef should already have pathVariables swapped out var dbRef = getters.dbRef; - // apply where filters and orderBy + // apply where and orderBy clauses if (getters.collectionMode) { getters.getWhereArrays().forEach(function (whereParams) { dbRef = dbRef.where.apply(dbRef, whereParams); @@ -1239,85 +1313,153 @@ function pluginActions (Firebase) { dbRef = dbRef.orderBy.apply(dbRef, state._conf.sync.orderBy); } } - // make a promise - return new Promise(function (resolve, reject) { - // log - if (state._conf.logging) { - console.log("%c openDBChannel for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: goldenrod'); + // log + if (state._conf.logging) { + console.log("%c openDBChannel for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: goldenrod'); + } + var initialPromise = nicePromise(); + var refreshedPromise = nicePromise(); + var streamingPromise = nicePromise(); + var gotFirstLocalResponse = false; + var gotFirstServerResponse = false; + var includeMetadataChanges = parameters.includeMetadataChanges; + var streamingStart = function () { + // create a promise for the life of the snapshot that can be resolved from + // outside its scope. This promise will be resolved when the user calls + // closeDBChannel, or rejected if the stream is ended prematurely by the + // error() callback + state._sync.streaming[identifier] = streamingPromise; + initialPromise.resolve({ + refreshed: includeMetadataChanges ? refreshedPromise : null, + streaming: streamingPromise, + stop: function () { return dispatch('closeDBChannel', { _identifier: identifier }); } + }); + }; + var streamingStop = function (error) { + // when this function is called by the error callback of onSnapshot, the + // subscription will actually already have been cancelled + unsubscribe(); + if (initialPromise.isPending) { + initialPromise.reject(error); } - var okToStream = function () { - // create a promise for the life of the snapshot that can be resolved from outside its scope. - // this promise will be resolved when the user calls closeDBChannel, or rejected if the - // stream is ended prematurely by the error() callback - var promiseMethods = { resolve: null, reject: null }; - var streaming = new Promise(function (_resolve, _reject) { - promiseMethods.resolve = _resolve; - promiseMethods.reject = _reject; - }); - Object.assign(streaming, promiseMethods); - state._sync.streaming[identifier] = streaming; - // we can't resolve the promise with a promise, it would hang, so we wrap it - resolve(function () { return streaming; }); - }; - var unsubscribe = dbRef.onSnapshot(function (querySnapshot) { return __awaiter(_this, void 0, void 0, function () { - var source, id, doc; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - source = querySnapshot.metadata.hasPendingWrites ? 'local' : 'server'; - if (!!getters.collectionMode) return [3 /*break*/, 3]; - if (!!querySnapshot.data()) return [3 /*break*/, 2]; - // No initial doc found in docMode - if (state._conf.sync.preventInitialDocInsertion) { - unsubscribe(); - reject('preventInitialDocInsertion'); - return [2 /*return*/]; + if (refreshedPromise.isPending) { + refreshedPromise.reject(error); + } + streamingPromise.reject(error); + state._sync.patching = 'error'; + state._sync.unsubscribe[identifier] = null; + state._sync.streaming[identifier] = null; + }; + var processDocument = function (data) { + var doc = getters.cleanUpRetrievedDoc(data, getters.docModeId); + dispatch('applyHooksAndUpdateState', { change: 'modified', id: getters.docModeId, doc: doc }); + }; + var processCollection = function (docChanges) { + docChanges.forEach(function (change) { + var doc = getters.cleanUpRetrievedDoc(change.doc.data(), change.doc.id); + dispatch('applyHooksAndUpdateState', { change: change.type, id: change.doc.id, doc: doc }); + }); + }; + var unsubscribe = dbRef.onSnapshot({ includeMetadataChanges: includeMetadataChanges }, function (querySnapshot) { return __awaiter(_this, void 0, void 0, function () { + var message, resp; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!querySnapshot.metadata.fromCache) return [3 /*break*/, 1]; + // if it's the very first call, we are at the initial app load. If so, we'll use + // the data in cache (if available) to populate the state. + // if it's not, this is only the result of a local modification which does not + // require to do anything else. + if (!gotFirstLocalResponse) { + // 'doc' mode: + if (!getters.collectionMode) { + // note: we don't want to insert a document ever when the data comes from cache, + // and we don't want to start the app if the data doesn't exist (no persistence) + if (querySnapshot.data()) { + processDocument(querySnapshot.data()); + streamingStart(); + } } - if (state._conf.logging) - console.log('[vuex-easy-firestore] inserting initial doc'); - return [4 /*yield*/, dispatch('insertInitialDoc')]; - case 1: - _a.sent(); - okToStream(); - return [2 /*return*/]; - case 2: - if (source === 'local' && !firstCall) - return [2 /*return*/]; - id = getters.docModeId; - doc = getters.cleanUpRetrievedDoc(querySnapshot.data(), id); - dispatch('applyHooksAndUpdateState', { change: 'modified', id: id, doc: doc }); - firstCall = false; - okToStream(); - return [2 /*return*/]; - case 3: - // 'collection' mode: - querySnapshot.docChanges().forEach(function (change) { - var changeType = change.type; - // Don't do anything for local modifications & removals - if (source === 'local' && !firstCall) - return; - var id = change.doc.id; - var doc = getters.cleanUpRetrievedDoc(change.doc.data(), id); - dispatch('applyHooksAndUpdateState', { change: changeType, id: id, doc: doc }); - }); - firstCall = false; - okToStream(); - return [2 /*return*/]; - } - }); - }); }, function (error) { - state._sync.patching = 'error'; - state._sync.streaming[identifier].reject(error); - state._sync.streaming[identifier] = null; - state._sync.unsubscribe[identifier] = null; + // 'collection' mode + else { + processCollection(querySnapshot.docChanges()); + streamingStart(); + } + gotFirstLocalResponse = true; + } + return [3 /*break*/, 9]; + case 1: + if (!!getters.collectionMode) return [3 /*break*/, 7]; + if (!!querySnapshot.data()) return [3 /*break*/, 5]; + if (!!state._conf.sync.preventInitialDocInsertion) return [3 /*break*/, 3]; + if (state._conf.logging) { + message = gotFirstServerResponse + ? '[vuex-easy-firestore] recreating doc after remote deletion' + : '[vuex-easy-firestore] inserting initial doc'; + console.log(message); + } + return [4 /*yield*/, dispatch('insertInitialDoc') + // if the initial document was successfully inserted + ]; + case 2: + resp = _a.sent(); + // if the initial document was successfully inserted + if (!resp) { + if (initialPromise.isPending) { + streamingStart(); + } + if (refreshedPromise.isPending) { + refreshedPromise.resolve(); + } + } + else { + // we close the channel ourselves. Firestore does not, as it leaves the + // channel open as long as the user has read rights on the document, even + // if it does not exist. But since the dev enabled `insertInitialDoc`, + // it makes some sense to close as we can assume the user should have had + // write rights + streamingStop('failedRecreatingDoc'); + } + return [3 /*break*/, 4]; + case 3: + streamingStop('preventInitialDocInsertion'); + _a.label = 4; + case 4: return [3 /*break*/, 6]; + case 5: + processDocument(querySnapshot.data()); + if (initialPromise.isPending) { + streamingStart(); + } + // the promise should still be pending at this point only if there is no persistence, + // as only then the first call to our listener will have `fromCache` === `false` + if (refreshedPromise.isPending) { + refreshedPromise.resolve(); + } + _a.label = 6; + case 6: return [3 /*break*/, 8]; + case 7: + processCollection(querySnapshot.docChanges()); + if (initialPromise.isPending) { + streamingStart(); + } + if (refreshedPromise.isPending) { + refreshedPromise.resolve(); + } + _a.label = 8; + case 8: + gotFirstServerResponse = true; + _a.label = 9; + case 9: return [2 /*return*/]; + } }); - state._sync.unsubscribe[identifier] = unsubscribe; - }); + }); }, streamingStop); + state._sync.unsubscribe[identifier] = unsubscribe; + return initialPromise; }, closeDBChannel: function (_a, _b) { var getters = _a.getters, state = _a.state, commit = _a.commit, dispatch = _a.dispatch; - var _c = (_b === void 0 ? { clearModule: false } : _b).clearModule, clearModule = _c === void 0 ? false : _c; - var identifier = createFetchIdentifier({ + var _c = _b === void 0 ? { clearModule: false, _identifier: null } : _b, _d = _c.clearModule, clearModule = _d === void 0 ? false : _d, _e = _c._identifier, _identifier = _e === void 0 ? null : _e; + var identifier = _identifier || createFetchIdentifier({ where: state._conf.sync.where, orderBy: state._conf.sync.orderBy, pathVariables: state._sync.pathVariables diff --git a/dist/index.esm.js b/dist/index.esm.js index 697213fc..dfbd2d77 100644 --- a/dist/index.esm.js +++ b/dist/index.esm.js @@ -51,7 +51,7 @@ var defaultConfig = { 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)`. + // When items are fetched through `dispatch('module/fetch', {clauses})`. fetch: { // The max amount of documents to be fetched. Defaults to 50. docLimit: 50, @@ -292,7 +292,7 @@ function pluginMutations (userState) { self._vm.$set(state._sync.pathVariables, key, pathPiece); }); }, - SET_SYNCFILTERS: function (state, _a) { + SET_SYNCCLAUSES: function (state, _a) { var where = _a.where, orderBy = _a.orderBy; if (where && isArray(where)) state._conf.sync.where = where; @@ -637,7 +637,7 @@ function stringifyParams(params) { }).join(); } /** - * Gets an object with {where, orderBy} filters and returns a unique identifier for that + * Gets an object with {where, orderBy} clauses and returns a unique identifier for that * * @export * @param {AnyObject} [whereOrderBy={}] whereOrderBy {where, orderBy, pathVariables} @@ -950,25 +950,39 @@ function pluginActions (Firebase) { }); }); }, - fetch: function (_a, pathVariables - // where: [['archived', '==', true]] - // orderBy: ['done_date', 'desc'] - ) { + fetch: function (_a, parameters) { var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; - if (pathVariables === void 0) { pathVariables = { where: [], whereFilters: [], orderBy: [] }; } + if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {} }; } + /* COMPATIBILITY START + * this ensures backward compatibility for people who passed pathVariables and + * clauses directly at the root of the `parameters` object. Can be removed in + * a later version + */ + if (!parameters.clauses && !parameters.pathVariables) { + var pathVariables_1 = Object.assign({}, parameters); + // @ts-ignore + delete pathVariables_1.where; + // @ts-ignore + delete pathVariables_1.orderBy; + Object.entries(pathVariables_1).forEach(function (entry) { + if (typeof entry[1] === 'object') { + delete pathVariables_1[entry[0]]; + } + }); + parameters = Object.assign({}, { clauses: parameters, pathVariables: pathVariables_1 }); + } + /* COMPATIBILITY END */ if (!getters.collectionMode) return error('only-in-collection-mode'); dispatch('setUserId'); - var where = pathVariables.where, whereFilters = pathVariables.whereFilters, orderBy = pathVariables.orderBy; + var _b = parameters.clauses, where = _b.where, whereFilters = _b.whereFilters, orderBy = _b.orderBy; if (!isArray(where)) where = []; if (!isArray(orderBy)) orderBy = []; if (isArray(whereFilters) && whereFilters.length) - where = whereFilters; // depreciated - if (pathVariables && isPlainObject(pathVariables)) { - commit('SET_PATHVARS', pathVariables); - } + where = whereFilters; // deprecated + commit('SET_PATHVARS', parameters.pathVariables); return new Promise(function (resolve, reject) { // log if (state._conf.logging) { @@ -985,7 +999,7 @@ function pluginActions (Firebase) { // We've never fetched this before: if (!fetched) { var ref_1 = getters.dbRef; - // apply where filters and orderBy + // apply where clauses and orderBy getters.getWhereArrays(where).forEach(function (paramsArr) { ref_1 = ref_1.where.apply(ref_1, paramsArr); }); @@ -1005,15 +1019,15 @@ function pluginActions (Firebase) { console.log('[vuex-easy-firestore] done fetching'); return resolve({ done: true }); } - // attach fetch filters + // attach fetch clauses var fRef = state._sync.fetched[identifier].ref; if (fRequest.nextFetchRef) { // get next ref if saved in state fRef = state._sync.fetched[identifier].nextFetchRef; } // add doc limit - var limit = (isNumber(pathVariables.limit)) - ? pathVariables.limit + var limit = (isNumber(parameters.clauses.limit)) + ? parameters.clauses.limit : state._conf.fetch.docLimit; if (limit > 0) fRef = fRef.limit(limit); @@ -1045,16 +1059,32 @@ function pluginActions (Firebase) { }); }); }, - fetchAndAdd: function (_a, pathVariables // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] - ) { + fetchAndAdd: function (_a, parameters) { var _this = this; var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; - if (pathVariables === void 0) { pathVariables = { where: [], whereFilters: [], orderBy: [] }; } - if (pathVariables && isPlainObject(pathVariables)) { - commit('SET_PATHVARS', pathVariables); + if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {} }; } + /* COMPATIBILITY START + * this ensures backward compatibility for people who passed pathVariables and + * clauses directly at the root of the `parameters` object. Can be removed in + * a later version + */ + if (!parameters.clauses && !parameters.pathVariables) { + var pathVariables_2 = Object.assign({}, parameters); + // @ts-ignore + delete pathVariables_2.where; + // @ts-ignore + delete pathVariables_2.orderBy; + Object.entries(pathVariables_2).forEach(function (entry) { + if (typeof entry[1] === 'object') { + delete pathVariables_2[entry[0]]; + } + }); + parameters = Object.assign({}, { clauses: parameters, pathVariables: pathVariables_2 }); } + /* COMPATIBILITY END */ + commit('SET_PATHVARS', parameters.pathVariables); // 'doc' mode: if (!getters.collectionMode) { dispatch('setUserId'); @@ -1088,7 +1118,7 @@ function pluginActions (Firebase) { }); } // 'collection' mode: - return dispatch('fetch', pathVariables) + return dispatch('fetch', parameters) .then(function (querySnapshot) { if (querySnapshot.done === true) return querySnapshot; @@ -1197,26 +1227,70 @@ function pluginActions (Firebase) { } }); }, - openDBChannel: function (_a, pathVariables) { + openDBChannel: function (_a, parameters) { var _this = this; var getters = _a.getters, state = _a.state, commit = _a.commit, dispatch = _a.dispatch; - dispatch('setUserId'); - // `firstCall` makes sure that local changes made during offline are reflected as server changes which the app is refreshed during offline mode - var firstCall = true; - // set state for pathVariables - if (pathVariables && isPlainObject(pathVariables)) { - commit('SET_SYNCFILTERS', pathVariables); - delete pathVariables.where; - delete pathVariables.orderBy; - commit('SET_PATHVARS', pathVariables); + if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {}, includeMetadataChanges: false }; } + /* COMPATIBILITY START + * this ensures backward compatibility for people who passed pathVariables and + * clauses directly at the root of the `parameters` object. Can be removed in + * a later version + */ + if (!parameters.clauses && !parameters.pathVariables && parameters.includeMetadataChanges === undefined) { + var pathVariables_3 = Object.assign({}, parameters); + // @ts-ignore + delete pathVariables_3.where; + // @ts-ignore + delete pathVariables_3.orderBy; + Object.entries(pathVariables_3).forEach(function (entry) { + if (typeof entry[1] === 'object') { + delete pathVariables_3[entry[0]]; + } + }); + parameters = Object.assign({ includeMetadataChanges: parameters.includeMetadataChanges || false }, { clauses: parameters, pathVariables: pathVariables_3 }); } + /* COMPATIBILITY END */ + var defaultParameters = { + clauses: {}, + pathVariables: {}, + includeMetadataChanges: false + }; + parameters = Object.assign(defaultParameters, parameters); + dispatch('setUserId'); + // creates promises that can be resolved from outside their scope and that + // can give their status + var nicePromise = function () { + var m = { + resolve: null, + reject: null, + isFulfilled: false, + isRejected: false, + isPending: true + }; + var p = new Promise(function (resolve, reject) { + m.resolve = resolve; + m.reject = reject; + }); + Object.assign(p, m); + p + // @ts-ignore + .then(function () { return p.isFulfilled = true; }) + // @ts-ignore + .catch(function () { return p.isRejected = true; }) + // @ts-ignore + .finally(function () { return p.isPending = false; }); + return p; + }; + // set state for clauses and pathVariables + commit('SET_SYNCCLAUSES', parameters.clauses); + commit('SET_PATHVARS', parameters.pathVariables); var identifier = createFetchIdentifier({ where: state._conf.sync.where, orderBy: state._conf.sync.orderBy, pathVariables: state._sync.pathVariables }); if (isFunction(state._sync.unsubscribe[identifier])) { - var channelAlreadyOpenError = "openDBChannel was already called for these filters and pathvariables. Identifier: " + identifier; + var channelAlreadyOpenError = "openDBChannel was already called for these clauses and pathvariables. Identifier: " + identifier; if (state._conf.logging) { console.log(channelAlreadyOpenError); } @@ -1224,7 +1298,7 @@ function pluginActions (Firebase) { } // getters.dbRef should already have pathVariables swapped out var dbRef = getters.dbRef; - // apply where filters and orderBy + // apply where and orderBy clauses if (getters.collectionMode) { getters.getWhereArrays().forEach(function (whereParams) { dbRef = dbRef.where.apply(dbRef, whereParams); @@ -1233,85 +1307,153 @@ function pluginActions (Firebase) { dbRef = dbRef.orderBy.apply(dbRef, state._conf.sync.orderBy); } } - // make a promise - return new Promise(function (resolve, reject) { - // log - if (state._conf.logging) { - console.log("%c openDBChannel for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: goldenrod'); + // log + if (state._conf.logging) { + console.log("%c openDBChannel for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: goldenrod'); + } + var initialPromise = nicePromise(); + var refreshedPromise = nicePromise(); + var streamingPromise = nicePromise(); + var gotFirstLocalResponse = false; + var gotFirstServerResponse = false; + var includeMetadataChanges = parameters.includeMetadataChanges; + var streamingStart = function () { + // create a promise for the life of the snapshot that can be resolved from + // outside its scope. This promise will be resolved when the user calls + // closeDBChannel, or rejected if the stream is ended prematurely by the + // error() callback + state._sync.streaming[identifier] = streamingPromise; + initialPromise.resolve({ + refreshed: includeMetadataChanges ? refreshedPromise : null, + streaming: streamingPromise, + stop: function () { return dispatch('closeDBChannel', { _identifier: identifier }); } + }); + }; + var streamingStop = function (error) { + // when this function is called by the error callback of onSnapshot, the + // subscription will actually already have been cancelled + unsubscribe(); + if (initialPromise.isPending) { + initialPromise.reject(error); } - var okToStream = function () { - // create a promise for the life of the snapshot that can be resolved from outside its scope. - // this promise will be resolved when the user calls closeDBChannel, or rejected if the - // stream is ended prematurely by the error() callback - var promiseMethods = { resolve: null, reject: null }; - var streaming = new Promise(function (_resolve, _reject) { - promiseMethods.resolve = _resolve; - promiseMethods.reject = _reject; - }); - Object.assign(streaming, promiseMethods); - state._sync.streaming[identifier] = streaming; - // we can't resolve the promise with a promise, it would hang, so we wrap it - resolve(function () { return streaming; }); - }; - var unsubscribe = dbRef.onSnapshot(function (querySnapshot) { return __awaiter(_this, void 0, void 0, function () { - var source, id, doc; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - source = querySnapshot.metadata.hasPendingWrites ? 'local' : 'server'; - if (!!getters.collectionMode) return [3 /*break*/, 3]; - if (!!querySnapshot.data()) return [3 /*break*/, 2]; - // No initial doc found in docMode - if (state._conf.sync.preventInitialDocInsertion) { - unsubscribe(); - reject('preventInitialDocInsertion'); - return [2 /*return*/]; + if (refreshedPromise.isPending) { + refreshedPromise.reject(error); + } + streamingPromise.reject(error); + state._sync.patching = 'error'; + state._sync.unsubscribe[identifier] = null; + state._sync.streaming[identifier] = null; + }; + var processDocument = function (data) { + var doc = getters.cleanUpRetrievedDoc(data, getters.docModeId); + dispatch('applyHooksAndUpdateState', { change: 'modified', id: getters.docModeId, doc: doc }); + }; + var processCollection = function (docChanges) { + docChanges.forEach(function (change) { + var doc = getters.cleanUpRetrievedDoc(change.doc.data(), change.doc.id); + dispatch('applyHooksAndUpdateState', { change: change.type, id: change.doc.id, doc: doc }); + }); + }; + var unsubscribe = dbRef.onSnapshot({ includeMetadataChanges: includeMetadataChanges }, function (querySnapshot) { return __awaiter(_this, void 0, void 0, function () { + var message, resp; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!querySnapshot.metadata.fromCache) return [3 /*break*/, 1]; + // if it's the very first call, we are at the initial app load. If so, we'll use + // the data in cache (if available) to populate the state. + // if it's not, this is only the result of a local modification which does not + // require to do anything else. + if (!gotFirstLocalResponse) { + // 'doc' mode: + if (!getters.collectionMode) { + // note: we don't want to insert a document ever when the data comes from cache, + // and we don't want to start the app if the data doesn't exist (no persistence) + if (querySnapshot.data()) { + processDocument(querySnapshot.data()); + streamingStart(); + } } - if (state._conf.logging) - console.log('[vuex-easy-firestore] inserting initial doc'); - return [4 /*yield*/, dispatch('insertInitialDoc')]; - case 1: - _a.sent(); - okToStream(); - return [2 /*return*/]; - case 2: - if (source === 'local' && !firstCall) - return [2 /*return*/]; - id = getters.docModeId; - doc = getters.cleanUpRetrievedDoc(querySnapshot.data(), id); - dispatch('applyHooksAndUpdateState', { change: 'modified', id: id, doc: doc }); - firstCall = false; - okToStream(); - return [2 /*return*/]; - case 3: - // 'collection' mode: - querySnapshot.docChanges().forEach(function (change) { - var changeType = change.type; - // Don't do anything for local modifications & removals - if (source === 'local' && !firstCall) - return; - var id = change.doc.id; - var doc = getters.cleanUpRetrievedDoc(change.doc.data(), id); - dispatch('applyHooksAndUpdateState', { change: changeType, id: id, doc: doc }); - }); - firstCall = false; - okToStream(); - return [2 /*return*/]; - } - }); - }); }, function (error) { - state._sync.patching = 'error'; - state._sync.streaming[identifier].reject(error); - state._sync.streaming[identifier] = null; - state._sync.unsubscribe[identifier] = null; + // 'collection' mode + else { + processCollection(querySnapshot.docChanges()); + streamingStart(); + } + gotFirstLocalResponse = true; + } + return [3 /*break*/, 9]; + case 1: + if (!!getters.collectionMode) return [3 /*break*/, 7]; + if (!!querySnapshot.data()) return [3 /*break*/, 5]; + if (!!state._conf.sync.preventInitialDocInsertion) return [3 /*break*/, 3]; + if (state._conf.logging) { + message = gotFirstServerResponse + ? '[vuex-easy-firestore] recreating doc after remote deletion' + : '[vuex-easy-firestore] inserting initial doc'; + console.log(message); + } + return [4 /*yield*/, dispatch('insertInitialDoc') + // if the initial document was successfully inserted + ]; + case 2: + resp = _a.sent(); + // if the initial document was successfully inserted + if (!resp) { + if (initialPromise.isPending) { + streamingStart(); + } + if (refreshedPromise.isPending) { + refreshedPromise.resolve(); + } + } + else { + // we close the channel ourselves. Firestore does not, as it leaves the + // channel open as long as the user has read rights on the document, even + // if it does not exist. But since the dev enabled `insertInitialDoc`, + // it makes some sense to close as we can assume the user should have had + // write rights + streamingStop('failedRecreatingDoc'); + } + return [3 /*break*/, 4]; + case 3: + streamingStop('preventInitialDocInsertion'); + _a.label = 4; + case 4: return [3 /*break*/, 6]; + case 5: + processDocument(querySnapshot.data()); + if (initialPromise.isPending) { + streamingStart(); + } + // the promise should still be pending at this point only if there is no persistence, + // as only then the first call to our listener will have `fromCache` === `false` + if (refreshedPromise.isPending) { + refreshedPromise.resolve(); + } + _a.label = 6; + case 6: return [3 /*break*/, 8]; + case 7: + processCollection(querySnapshot.docChanges()); + if (initialPromise.isPending) { + streamingStart(); + } + if (refreshedPromise.isPending) { + refreshedPromise.resolve(); + } + _a.label = 8; + case 8: + gotFirstServerResponse = true; + _a.label = 9; + case 9: return [2 /*return*/]; + } }); - state._sync.unsubscribe[identifier] = unsubscribe; - }); + }); }, streamingStop); + state._sync.unsubscribe[identifier] = unsubscribe; + return initialPromise; }, closeDBChannel: function (_a, _b) { var getters = _a.getters, state = _a.state, commit = _a.commit, dispatch = _a.dispatch; - var _c = (_b === void 0 ? { clearModule: false } : _b).clearModule, clearModule = _c === void 0 ? false : _c; - var identifier = createFetchIdentifier({ + var _c = _b === void 0 ? { clearModule: false, _identifier: null } : _b, _d = _c.clearModule, clearModule = _d === void 0 ? false : _d, _e = _c._identifier, _identifier = _e === void 0 ? null : _e; + var identifier = _identifier || createFetchIdentifier({ where: state._conf.sync.where, orderBy: state._conf.sync.orderBy, pathVariables: state._sync.pathVariables diff --git a/test/helpers/index.cjs.js b/test/helpers/index.cjs.js index 205e9576..ceef2f4c 100644 --- a/test/helpers/index.cjs.js +++ b/test/helpers/index.cjs.js @@ -682,7 +682,7 @@ var defaultConfig = { 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)`. + // When items are fetched through `dispatch('module/fetch', {clauses})`. fetch: { // The max amount of documents to be fetched. Defaults to 50. docLimit: 50, @@ -818,7 +818,7 @@ function pluginMutations (userState) { self._vm.$set(state._sync.pathVariables, key, pathPiece); }); }, - SET_SYNCFILTERS: function (state, _a) { + SET_SYNCCLAUSES: function (state, _a) { var where = _a.where, orderBy = _a.orderBy; if (where && isWhat.isArray(where)) state._conf.sync.where = where; @@ -1163,7 +1163,7 @@ function stringifyParams(params) { }).join(); } /** - * Gets an object with {where, orderBy} filters and returns a unique identifier for that + * Gets an object with {where, orderBy} clauses and returns a unique identifier for that * * @export * @param {AnyObject} [whereOrderBy={}] whereOrderBy {where, orderBy, pathVariables} @@ -1476,25 +1476,39 @@ function pluginActions (Firebase) { }); }); }, - fetch: function (_a, pathVariables - // where: [['archived', '==', true]] - // orderBy: ['done_date', 'desc'] - ) { + fetch: function (_a, parameters) { var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; - if (pathVariables === void 0) { pathVariables = { where: [], whereFilters: [], orderBy: [] }; } + if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {} }; } + /* COMPATIBILITY START + * this ensures backward compatibility for people who passed pathVariables and + * clauses directly at the root of the `parameters` object. Can be removed in + * a later version + */ + if (!parameters.clauses && !parameters.pathVariables) { + var pathVariables_1 = Object.assign({}, parameters); + // @ts-ignore + delete pathVariables_1.where; + // @ts-ignore + delete pathVariables_1.orderBy; + Object.entries(pathVariables_1).forEach(function (entry) { + if (typeof entry[1] === 'object') { + delete pathVariables_1[entry[0]]; + } + }); + parameters = Object.assign({}, { clauses: parameters, pathVariables: pathVariables_1 }); + } + /* COMPATIBILITY END */ if (!getters.collectionMode) return error('only-in-collection-mode'); dispatch('setUserId'); - var where = pathVariables.where, whereFilters = pathVariables.whereFilters, orderBy = pathVariables.orderBy; + var _b = parameters.clauses, where = _b.where, whereFilters = _b.whereFilters, orderBy = _b.orderBy; if (!isWhat.isArray(where)) where = []; if (!isWhat.isArray(orderBy)) orderBy = []; if (isWhat.isArray(whereFilters) && whereFilters.length) - where = whereFilters; // depreciated - if (pathVariables && isWhat.isPlainObject(pathVariables)) { - commit('SET_PATHVARS', pathVariables); - } + where = whereFilters; // deprecated + commit('SET_PATHVARS', parameters.pathVariables); return new Promise(function (resolve, reject) { // log if (state._conf.logging) { @@ -1511,7 +1525,7 @@ function pluginActions (Firebase) { // We've never fetched this before: if (!fetched) { var ref_1 = getters.dbRef; - // apply where filters and orderBy + // apply where clauses and orderBy getters.getWhereArrays(where).forEach(function (paramsArr) { ref_1 = ref_1.where.apply(ref_1, paramsArr); }); @@ -1531,15 +1545,15 @@ function pluginActions (Firebase) { console.log('[vuex-easy-firestore] done fetching'); return resolve({ done: true }); } - // attach fetch filters + // attach fetch clauses var fRef = state._sync.fetched[identifier].ref; if (fRequest.nextFetchRef) { // get next ref if saved in state fRef = state._sync.fetched[identifier].nextFetchRef; } // add doc limit - var limit = (isWhat.isNumber(pathVariables.limit)) - ? pathVariables.limit + var limit = (isWhat.isNumber(parameters.clauses.limit)) + ? parameters.clauses.limit : state._conf.fetch.docLimit; if (limit > 0) fRef = fRef.limit(limit); @@ -1571,16 +1585,32 @@ function pluginActions (Firebase) { }); }); }, - fetchAndAdd: function (_a, pathVariables // where: [['archived', '==', true]] // orderBy: ['done_date', 'desc'] - ) { + fetchAndAdd: function (_a, parameters) { var _this = this; var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; - if (pathVariables === void 0) { pathVariables = { where: [], whereFilters: [], orderBy: [] }; } - if (pathVariables && isWhat.isPlainObject(pathVariables)) { - commit('SET_PATHVARS', pathVariables); + if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {} }; } + /* COMPATIBILITY START + * this ensures backward compatibility for people who passed pathVariables and + * clauses directly at the root of the `parameters` object. Can be removed in + * a later version + */ + if (!parameters.clauses && !parameters.pathVariables) { + var pathVariables_2 = Object.assign({}, parameters); + // @ts-ignore + delete pathVariables_2.where; + // @ts-ignore + delete pathVariables_2.orderBy; + Object.entries(pathVariables_2).forEach(function (entry) { + if (typeof entry[1] === 'object') { + delete pathVariables_2[entry[0]]; + } + }); + parameters = Object.assign({}, { clauses: parameters, pathVariables: pathVariables_2 }); } + /* COMPATIBILITY END */ + commit('SET_PATHVARS', parameters.pathVariables); // 'doc' mode: if (!getters.collectionMode) { dispatch('setUserId'); @@ -1614,7 +1644,7 @@ function pluginActions (Firebase) { }); } // 'collection' mode: - return dispatch('fetch', pathVariables) + return dispatch('fetch', parameters) .then(function (querySnapshot) { if (querySnapshot.done === true) return querySnapshot; @@ -1723,26 +1753,70 @@ function pluginActions (Firebase) { } }); }, - openDBChannel: function (_a, pathVariables) { + openDBChannel: function (_a, parameters) { var _this = this; var getters = _a.getters, state = _a.state, commit = _a.commit, dispatch = _a.dispatch; - dispatch('setUserId'); - // `firstCall` makes sure that local changes made during offline are reflected as server changes which the app is refreshed during offline mode - var firstCall = true; - // set state for pathVariables - if (pathVariables && isWhat.isPlainObject(pathVariables)) { - commit('SET_SYNCFILTERS', pathVariables); - delete pathVariables.where; - delete pathVariables.orderBy; - commit('SET_PATHVARS', pathVariables); + if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {}, includeMetadataChanges: false }; } + /* COMPATIBILITY START + * this ensures backward compatibility for people who passed pathVariables and + * clauses directly at the root of the `parameters` object. Can be removed in + * a later version + */ + if (!parameters.clauses && !parameters.pathVariables && parameters.includeMetadataChanges === undefined) { + var pathVariables_3 = Object.assign({}, parameters); + // @ts-ignore + delete pathVariables_3.where; + // @ts-ignore + delete pathVariables_3.orderBy; + Object.entries(pathVariables_3).forEach(function (entry) { + if (typeof entry[1] === 'object') { + delete pathVariables_3[entry[0]]; + } + }); + parameters = Object.assign({ includeMetadataChanges: parameters.includeMetadataChanges || false }, { clauses: parameters, pathVariables: pathVariables_3 }); } + /* COMPATIBILITY END */ + var defaultParameters = { + clauses: {}, + pathVariables: {}, + includeMetadataChanges: false + }; + parameters = Object.assign(defaultParameters, parameters); + dispatch('setUserId'); + // creates promises that can be resolved from outside their scope and that + // can give their status + var nicePromise = function () { + var m = { + resolve: null, + reject: null, + isFulfilled: false, + isRejected: false, + isPending: true + }; + var p = new Promise(function (resolve, reject) { + m.resolve = resolve; + m.reject = reject; + }); + Object.assign(p, m); + p + // @ts-ignore + .then(function () { return p.isFulfilled = true; }) + // @ts-ignore + .catch(function () { return p.isRejected = true; }) + // @ts-ignore + .finally(function () { return p.isPending = false; }); + return p; + }; + // set state for clauses and pathVariables + commit('SET_SYNCCLAUSES', parameters.clauses); + commit('SET_PATHVARS', parameters.pathVariables); var identifier = createFetchIdentifier({ where: state._conf.sync.where, orderBy: state._conf.sync.orderBy, pathVariables: state._sync.pathVariables }); if (isWhat.isFunction(state._sync.unsubscribe[identifier])) { - var channelAlreadyOpenError = "openDBChannel was already called for these filters and pathvariables. Identifier: " + identifier; + var channelAlreadyOpenError = "openDBChannel was already called for these clauses and pathvariables. Identifier: " + identifier; if (state._conf.logging) { console.log(channelAlreadyOpenError); } @@ -1750,7 +1824,7 @@ function pluginActions (Firebase) { } // getters.dbRef should already have pathVariables swapped out var dbRef = getters.dbRef; - // apply where filters and orderBy + // apply where and orderBy clauses if (getters.collectionMode) { getters.getWhereArrays().forEach(function (whereParams) { dbRef = dbRef.where.apply(dbRef, whereParams); @@ -1759,85 +1833,153 @@ function pluginActions (Firebase) { dbRef = dbRef.orderBy.apply(dbRef, state._conf.sync.orderBy); } } - // make a promise - return new Promise(function (resolve, reject) { - // log - if (state._conf.logging) { - console.log("%c openDBChannel for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: goldenrod'); + // log + if (state._conf.logging) { + console.log("%c openDBChannel for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: goldenrod'); + } + var initialPromise = nicePromise(); + var refreshedPromise = nicePromise(); + var streamingPromise = nicePromise(); + var gotFirstLocalResponse = false; + var gotFirstServerResponse = false; + var includeMetadataChanges = parameters.includeMetadataChanges; + var streamingStart = function () { + // create a promise for the life of the snapshot that can be resolved from + // outside its scope. This promise will be resolved when the user calls + // closeDBChannel, or rejected if the stream is ended prematurely by the + // error() callback + state._sync.streaming[identifier] = streamingPromise; + initialPromise.resolve({ + refreshed: includeMetadataChanges ? refreshedPromise : null, + streaming: streamingPromise, + stop: function () { return dispatch('closeDBChannel', { _identifier: identifier }); } + }); + }; + var streamingStop = function (error) { + // when this function is called by the error callback of onSnapshot, the + // subscription will actually already have been cancelled + unsubscribe(); + if (initialPromise.isPending) { + initialPromise.reject(error); } - var okToStream = function () { - // create a promise for the life of the snapshot that can be resolved from outside its scope. - // this promise will be resolved when the user calls closeDBChannel, or rejected if the - // stream is ended prematurely by the error() callback - var promiseMethods = { resolve: null, reject: null }; - var streaming = new Promise(function (_resolve, _reject) { - promiseMethods.resolve = _resolve; - promiseMethods.reject = _reject; - }); - Object.assign(streaming, promiseMethods); - state._sync.streaming[identifier] = streaming; - // we can't resolve the promise with a promise, it would hang, so we wrap it - resolve(function () { return streaming; }); - }; - var unsubscribe = dbRef.onSnapshot(function (querySnapshot) { return __awaiter(_this, void 0, void 0, function () { - var source, id, doc; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - source = querySnapshot.metadata.hasPendingWrites ? 'local' : 'server'; - if (!!getters.collectionMode) return [3 /*break*/, 3]; - if (!!querySnapshot.data()) return [3 /*break*/, 2]; - // No initial doc found in docMode - if (state._conf.sync.preventInitialDocInsertion) { - unsubscribe(); - reject('preventInitialDocInsertion'); - return [2 /*return*/]; + if (refreshedPromise.isPending) { + refreshedPromise.reject(error); + } + streamingPromise.reject(error); + state._sync.patching = 'error'; + state._sync.unsubscribe[identifier] = null; + state._sync.streaming[identifier] = null; + }; + var processDocument = function (data) { + var doc = getters.cleanUpRetrievedDoc(data, getters.docModeId); + dispatch('applyHooksAndUpdateState', { change: 'modified', id: getters.docModeId, doc: doc }); + }; + var processCollection = function (docChanges) { + docChanges.forEach(function (change) { + var doc = getters.cleanUpRetrievedDoc(change.doc.data(), change.doc.id); + dispatch('applyHooksAndUpdateState', { change: change.type, id: change.doc.id, doc: doc }); + }); + }; + var unsubscribe = dbRef.onSnapshot({ includeMetadataChanges: includeMetadataChanges }, function (querySnapshot) { return __awaiter(_this, void 0, void 0, function () { + var message, resp; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!querySnapshot.metadata.fromCache) return [3 /*break*/, 1]; + // if it's the very first call, we are at the initial app load. If so, we'll use + // the data in cache (if available) to populate the state. + // if it's not, this is only the result of a local modification which does not + // require to do anything else. + if (!gotFirstLocalResponse) { + // 'doc' mode: + if (!getters.collectionMode) { + // note: we don't want to insert a document ever when the data comes from cache, + // and we don't want to start the app if the data doesn't exist (no persistence) + if (querySnapshot.data()) { + processDocument(querySnapshot.data()); + streamingStart(); + } } - if (state._conf.logging) - console.log('[vuex-easy-firestore] inserting initial doc'); - return [4 /*yield*/, dispatch('insertInitialDoc')]; - case 1: - _a.sent(); - okToStream(); - return [2 /*return*/]; - case 2: - if (source === 'local' && !firstCall) - return [2 /*return*/]; - id = getters.docModeId; - doc = getters.cleanUpRetrievedDoc(querySnapshot.data(), id); - dispatch('applyHooksAndUpdateState', { change: 'modified', id: id, doc: doc }); - firstCall = false; - okToStream(); - return [2 /*return*/]; - case 3: - // 'collection' mode: - querySnapshot.docChanges().forEach(function (change) { - var changeType = change.type; - // Don't do anything for local modifications & removals - if (source === 'local' && !firstCall) - return; - var id = change.doc.id; - var doc = getters.cleanUpRetrievedDoc(change.doc.data(), id); - dispatch('applyHooksAndUpdateState', { change: changeType, id: id, doc: doc }); - }); - firstCall = false; - okToStream(); - return [2 /*return*/]; - } - }); - }); }, function (error) { - state._sync.patching = 'error'; - state._sync.streaming[identifier].reject(error); - state._sync.streaming[identifier] = null; - state._sync.unsubscribe[identifier] = null; + // 'collection' mode + else { + processCollection(querySnapshot.docChanges()); + streamingStart(); + } + gotFirstLocalResponse = true; + } + return [3 /*break*/, 9]; + case 1: + if (!!getters.collectionMode) return [3 /*break*/, 7]; + if (!!querySnapshot.data()) return [3 /*break*/, 5]; + if (!!state._conf.sync.preventInitialDocInsertion) return [3 /*break*/, 3]; + if (state._conf.logging) { + message = gotFirstServerResponse + ? '[vuex-easy-firestore] recreating doc after remote deletion' + : '[vuex-easy-firestore] inserting initial doc'; + console.log(message); + } + return [4 /*yield*/, dispatch('insertInitialDoc') + // if the initial document was successfully inserted + ]; + case 2: + resp = _a.sent(); + // if the initial document was successfully inserted + if (!resp) { + if (initialPromise.isPending) { + streamingStart(); + } + if (refreshedPromise.isPending) { + refreshedPromise.resolve(); + } + } + else { + // we close the channel ourselves. Firestore does not, as it leaves the + // channel open as long as the user has read rights on the document, even + // if it does not exist. But since the dev enabled `insertInitialDoc`, + // it makes some sense to close as we can assume the user should have had + // write rights + streamingStop('failedRecreatingDoc'); + } + return [3 /*break*/, 4]; + case 3: + streamingStop('preventInitialDocInsertion'); + _a.label = 4; + case 4: return [3 /*break*/, 6]; + case 5: + processDocument(querySnapshot.data()); + if (initialPromise.isPending) { + streamingStart(); + } + // the promise should still be pending at this point only if there is no persistence, + // as only then the first call to our listener will have `fromCache` === `false` + if (refreshedPromise.isPending) { + refreshedPromise.resolve(); + } + _a.label = 6; + case 6: return [3 /*break*/, 8]; + case 7: + processCollection(querySnapshot.docChanges()); + if (initialPromise.isPending) { + streamingStart(); + } + if (refreshedPromise.isPending) { + refreshedPromise.resolve(); + } + _a.label = 8; + case 8: + gotFirstServerResponse = true; + _a.label = 9; + case 9: return [2 /*return*/]; + } }); - state._sync.unsubscribe[identifier] = unsubscribe; - }); + }); }, streamingStop); + state._sync.unsubscribe[identifier] = unsubscribe; + return initialPromise; }, closeDBChannel: function (_a, _b) { var getters = _a.getters, state = _a.state, commit = _a.commit, dispatch = _a.dispatch; - var _c = (_b === void 0 ? { clearModule: false } : _b).clearModule, clearModule = _c === void 0 ? false : _c; - var identifier = createFetchIdentifier({ + var _c = _b === void 0 ? { clearModule: false, _identifier: null } : _b, _d = _c.clearModule, clearModule = _d === void 0 ? false : _d, _e = _c._identifier, _identifier = _e === void 0 ? null : _e; + var identifier = _identifier || createFetchIdentifier({ where: state._conf.sync.where, orderBy: state._conf.sync.orderBy, pathVariables: state._sync.pathVariables diff --git a/types/src/utils/apiHelpers.d.ts b/types/src/utils/apiHelpers.d.ts index 0ff2e1e9..b603a195 100644 --- a/types/src/utils/apiHelpers.d.ts +++ b/types/src/utils/apiHelpers.d.ts @@ -37,7 +37,7 @@ export declare function getPathVarMatches(pathPiece: string): string[]; */ export declare function trimAccolades(pathPiece: string): string; /** - * Gets an object with {where, orderBy} filters and returns a unique identifier for that + * Gets an object with {where, orderBy} clauses and returns a unique identifier for that * * @export * @param {AnyObject} [whereOrderBy={}] whereOrderBy {where, orderBy, pathVariables} diff --git a/types/utils/apiHelpers.d.ts b/types/utils/apiHelpers.d.ts index 0ff2e1e9..b603a195 100644 --- a/types/utils/apiHelpers.d.ts +++ b/types/utils/apiHelpers.d.ts @@ -37,7 +37,7 @@ export declare function getPathVarMatches(pathPiece: string): string[]; */ export declare function trimAccolades(pathPiece: string): string; /** - * Gets an object with {where, orderBy} filters and returns a unique identifier for that + * Gets an object with {where, orderBy} clauses and returns a unique identifier for that * * @export * @param {AnyObject} [whereOrderBy={}] whereOrderBy {where, orderBy, pathVariables} From b03e2008581b7ee717c33729fa6e0d3a9fa2a751 Mon Sep 17 00:00:00 2001 From: Mesqueeb Date: Tue, 10 Dec 2019 17:02:38 +0900 Subject: [PATCH 5/9] =?UTF-8?q?allow=20passing=20`null`=20to=20fetch=20and?= =?UTF-8?q?=20channel=20actions=20=F0=9F=9F=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/index.cjs.js | 6 ++++++ dist/index.esm.js | 6 ++++++ src/module/actions.ts | 12 +++++++++--- test/helpers/index.cjs.js | 6 ++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/dist/index.cjs.js b/dist/index.cjs.js index 4f64a8ca..984ef9a6 100644 --- a/dist/index.cjs.js +++ b/dist/index.cjs.js @@ -959,6 +959,8 @@ function pluginActions (Firebase) { fetch: function (_a, parameters) { var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {} }; } + if (!isWhat.isPlainObject(parameters)) + parameters = {}; /* COMPATIBILITY START * this ensures backward compatibility for people who passed pathVariables and * clauses directly at the root of the `parameters` object. Can be removed in @@ -1071,6 +1073,8 @@ function pluginActions (Firebase) { var _this = this; var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {} }; } + if (!isWhat.isPlainObject(parameters)) + parameters = {}; /* COMPATIBILITY START * this ensures backward compatibility for people who passed pathVariables and * clauses directly at the root of the `parameters` object. Can be removed in @@ -1237,6 +1241,8 @@ function pluginActions (Firebase) { var _this = this; var getters = _a.getters, state = _a.state, commit = _a.commit, dispatch = _a.dispatch; if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {}, includeMetadataChanges: false }; } + if (!isWhat.isPlainObject(parameters)) + parameters = {}; /* COMPATIBILITY START * this ensures backward compatibility for people who passed pathVariables and * clauses directly at the root of the `parameters` object. Can be removed in diff --git a/dist/index.esm.js b/dist/index.esm.js index dfbd2d77..f1713d05 100644 --- a/dist/index.esm.js +++ b/dist/index.esm.js @@ -953,6 +953,8 @@ function pluginActions (Firebase) { fetch: function (_a, parameters) { var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {} }; } + if (!isPlainObject(parameters)) + parameters = {}; /* COMPATIBILITY START * this ensures backward compatibility for people who passed pathVariables and * clauses directly at the root of the `parameters` object. Can be removed in @@ -1065,6 +1067,8 @@ function pluginActions (Firebase) { var _this = this; var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {} }; } + if (!isPlainObject(parameters)) + parameters = {}; /* COMPATIBILITY START * this ensures backward compatibility for people who passed pathVariables and * clauses directly at the root of the `parameters` object. Can be removed in @@ -1231,6 +1235,8 @@ function pluginActions (Firebase) { var _this = this; var getters = _a.getters, state = _a.state, commit = _a.commit, dispatch = _a.dispatch; if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {}, includeMetadataChanges: false }; } + if (!isPlainObject(parameters)) + parameters = {}; /* COMPATIBILITY START * this ensures backward compatibility for people who passed pathVariables and * clauses directly at the root of the `parameters` object. Can be removed in diff --git a/src/module/actions.ts b/src/module/actions.ts index dc866a79..0a1f039e 100644 --- a/src/module/actions.ts +++ b/src/module/actions.ts @@ -241,6 +241,7 @@ export default function (Firebase: any): AnyObject { {state, getters, commit, dispatch}, parameters:any = {clauses: {}, pathVariables: {}} ) { + if (!isPlainObject(parameters)) parameters = {} /* COMPATIBILITY START * this ensures backward compatibility for people who passed pathVariables and * clauses directly at the root of the `parameters` object. Can be removed in @@ -343,8 +344,9 @@ export default function (Firebase: any): AnyObject { // orderBy: ['done_date', 'desc'] fetchAndAdd ( {state, getters, commit, dispatch}, - parameters = {clauses: {}, pathVariables: {}} + parameters: any = {clauses: {}, pathVariables: {}} ) { + if (!isPlainObject(parameters)) parameters = {} /* COMPATIBILITY START * this ensures backward compatibility for people who passed pathVariables and * clauses directly at the root of the `parameters` object. Can be removed in @@ -479,7 +481,11 @@ export default function (Firebase: any): AnyObject { } }) }, - openDBChannel ({getters, state, commit, dispatch}, parameters = {clauses: {}, pathVariables: {}, includeMetadataChanges: false}) { + openDBChannel ( + {getters, state, commit, dispatch}, + parameters: any = {clauses: {}, pathVariables: {}, includeMetadataChanges: false} + ) { + if (!isPlainObject(parameters)) parameters = {} /* COMPATIBILITY START * this ensures backward compatibility for people who passed pathVariables and * clauses directly at the root of the `parameters` object. Can be removed in @@ -700,7 +706,7 @@ export default function (Firebase: any): AnyObject { streamingStop ) state._sync.unsubscribe[identifier] = unsubscribe - + return initialPromise }, closeDBChannel ({getters, state, commit, dispatch}, { clearModule = false, _identifier = null } = { clearModule: false, _identifier: null }) { diff --git a/test/helpers/index.cjs.js b/test/helpers/index.cjs.js index ceef2f4c..2e9dcb0d 100644 --- a/test/helpers/index.cjs.js +++ b/test/helpers/index.cjs.js @@ -1479,6 +1479,8 @@ function pluginActions (Firebase) { fetch: function (_a, parameters) { var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {} }; } + if (!isWhat.isPlainObject(parameters)) + parameters = {}; /* COMPATIBILITY START * this ensures backward compatibility for people who passed pathVariables and * clauses directly at the root of the `parameters` object. Can be removed in @@ -1591,6 +1593,8 @@ function pluginActions (Firebase) { var _this = this; var state = _a.state, getters = _a.getters, commit = _a.commit, dispatch = _a.dispatch; if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {} }; } + if (!isWhat.isPlainObject(parameters)) + parameters = {}; /* COMPATIBILITY START * this ensures backward compatibility for people who passed pathVariables and * clauses directly at the root of the `parameters` object. Can be removed in @@ -1757,6 +1761,8 @@ function pluginActions (Firebase) { var _this = this; var getters = _a.getters, state = _a.state, commit = _a.commit, dispatch = _a.dispatch; if (parameters === void 0) { parameters = { clauses: {}, pathVariables: {}, includeMetadataChanges: false }; } + if (!isWhat.isPlainObject(parameters)) + parameters = {}; /* COMPATIBILITY START * this ensures backward compatibility for people who passed pathVariables and * clauses directly at the root of the `parameters` object. Can be removed in From 1ecc84d8d00699feb8b4fc1b26a3a0a8ec56520d Mon Sep 17 00:00:00 2001 From: Mesqueeb Date: Tue, 10 Dec 2019 19:00:43 +0900 Subject: [PATCH 6/9] =?UTF-8?q?Fix=20error=20handling=20=F0=9F=9F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/index.cjs.js | 137 +++++++++++++++++++++++--------------- dist/index.esm.js | 137 +++++++++++++++++++++++--------------- src/module/actions.ts | 66 +++++++++++------- src/module/errors.ts | 7 +- test/helpers/index.cjs.js | 137 +++++++++++++++++++++++--------------- 5 files changed, 297 insertions(+), 187 deletions(-) diff --git a/dist/index.cjs.js b/dist/index.cjs.js index 984ef9a6..4111297f 100644 --- a/dist/index.cjs.js +++ b/dist/index.cjs.js @@ -94,6 +94,18 @@ function pluginState () { }; } +var errorMessages = { + 'user-auth': "\n Error trying to set userId.\n Please double check if you have correctly authenticated the user with Firebase Auth before calling `openDBChannel` or `fetchAndAdd`.\n\n If you still get this error, try passing your firebase instance to the plugin as described in the documentation:\n https://mesqueeb.github.io/vuex-easy-firestore/extra-features.html#pass-firebase-dependency\n ", + 'delete-missing-id': "\n Missing id of the doc you want to delete!\n Correct usage:\n dispatch('delete', id)\n ", + 'delete-missing-path': "\n Missing path to the prop you want to delete!\n Correct usage:\n dispatch('delete', 'path.to.prop')\n\n Use `.` for sub props!\n ", + 'missing-id': "\n This action requires an id to be passed!\n ", + 'patch-missing-id': "\n Missing an id of the doc you want to patch!\n Correct usage:\n\n // pass `id` as a prop:\n dispatch('module/set', {id: '123', name: 'best item name'})\n // or\n dispatch('module/patch', {id: '123', name: 'best item name'})\n ", + 'missing-path-variables': "\n A path variable was passed without defining it!\n In VuexEasyFirestore you can create paths with variables:\n eg: `groups/{groupId}/user/{userId}`\n\n `userId` is automatically replaced with the userId of the firebase user.\n `groupId` or any other variable that needs to be set after authentication needs to be passed upon the `openDBChannel` action.\n\n // (in module config) Example path:\n firestorePath: 'groups/{groupId}/user/{userId}'\n\n // Then before openDBChannel:\n // retrieve the value\n const groupId = someIdRetrievedAfterSignin\n // pass as argument into openDBChannel:\n dispatch('moduleName/openDBChannel', {groupId})\n ", + 'patch-no-ref': "\n Something went wrong during the PATCH mutation:\n The document it's trying to patch does not exist.\n ", + 'only-in-collection-mode': "\n The action you dispatched can only be used in 'collection' mode.\n ", + 'initial-doc-failed': "\n Initial doc insertion failed. Further `set` or `patch` actions will also fail. Requires an internet connection when the initial doc is inserted. Check the error returned by Firebase:\n ", + 'sync-error': "\n Something went wrong while trying to synchronise data to Cloud Firestore.\n The data is kept in queue, so that it will try to sync again upon the next 'set' or 'patch' action.\n ", +}; /** * execute Error() based on an error id string * @@ -103,6 +115,10 @@ function pluginState () { * @returns {string} the error id */ function error (errorId, error) { + var logData = errorMessages[errorId] || errorId; + console.error("[vuex-easy-firestore] Error! " + logData); + if (error) + console.error(error); return errorId; } @@ -887,20 +903,25 @@ function pluginActions (Firebase) { var initialDocPrepared = getters.prepareInitialDocForInsert(initialDoc); // 2. Create a reference to the SF doc. var initialDocRef = getters.dbRef; - return Firebase.firestore().runTransaction(function (transaction) { - // This code may get re-run multiple times if there are conflicts. - return transaction.get(initialDocRef) - .then(function (foundInitialDoc) { - if (!foundInitialDoc.exists) { - transaction.set(initialDocRef, initialDocPrepared); + return new Promise(function (resolve, reject) { + Firebase.firestore().runTransaction(function (transaction) { + // This code may get re-run multiple times if there are conflicts. + return transaction.get(initialDocRef) + .then(function (foundInitialDoc) { + if (!foundInitialDoc.exists) { + transaction.set(initialDocRef, initialDocPrepared); + } + }); + }).then(function (_) { + if (state._conf.logging) { + var message = 'Initial doc succesfully inserted'; + console.log("%c [vuex-easy-firestore] " + message + "; for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: SeaGreen'); } + resolve(); + }).catch(function (error$1) { + error('initial-doc-failed', error$1); + reject(error$1); }); - }).then(function (_) { - if (state._conf.logging) { - console.log('[vuex-easy-firestore] Initial doc succesfully inserted.'); - } - }).catch(function (error$1) { - return error('initial-doc-failed'); }); }, handleSyncStackDebounce: function (_a, payloadToResolve) { @@ -952,6 +973,7 @@ function pluginActions (Firebase) { }).catch(function (error$1) { state._sync.patching = 'error'; state._sync.syncStack.debounceTimer = null; + error('sync-error', error$1); return reject(error$1); }); }); @@ -1102,7 +1124,7 @@ function pluginActions (Firebase) { console.log("%c fetch for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: goldenrod'); } return getters.dbRef.get().then(function (_doc) { return __awaiter(_this, void 0, void 0, function () { - var id, doc; + var message, id, doc; return __generator(this, function (_a) { switch (_a.label) { case 0: @@ -1110,11 +1132,16 @@ function pluginActions (Firebase) { // No initial doc found in docMode if (state._conf.sync.preventInitialDocInsertion) throw 'preventInitialDocInsertion'; - if (state._conf.logging) - console.log('[vuex-easy-firestore] inserting initial doc'); - return [4 /*yield*/, dispatch('insertInitialDoc')]; + if (state._conf.logging) { + message = 'inserting initial doc'; + console.log("%c [vuex-easy-firestore] " + message + "; for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: MediumSeaGreen'); + } + return [4 /*yield*/, dispatch('insertInitialDoc') + // an error in await here is (somehow) caught in the catch down below + ]; case 1: _a.sent(); + // an error in await here is (somehow) caught in the catch down below return [2 /*return*/, _doc]; case 2: id = getters.docModeId; @@ -1124,7 +1151,8 @@ function pluginActions (Firebase) { } }); }); }).catch(function (error$1) { - return error(error$1); + error(error$1); + throw error$1; }); } // 'collection' mode: @@ -1367,7 +1395,7 @@ function pluginActions (Firebase) { }); }; var unsubscribe = dbRef.onSnapshot({ includeMetadataChanges: includeMetadataChanges }, function (querySnapshot) { return __awaiter(_this, void 0, void 0, function () { - var message, resp; + var message, error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: @@ -1393,45 +1421,48 @@ function pluginActions (Firebase) { } gotFirstLocalResponse = true; } - return [3 /*break*/, 9]; + return [3 /*break*/, 12]; case 1: - if (!!getters.collectionMode) return [3 /*break*/, 7]; - if (!!querySnapshot.data()) return [3 /*break*/, 5]; - if (!!state._conf.sync.preventInitialDocInsertion) return [3 /*break*/, 3]; + if (!!getters.collectionMode) return [3 /*break*/, 10]; + if (!!querySnapshot.data()) return [3 /*break*/, 8]; + if (!!state._conf.sync.preventInitialDocInsertion) return [3 /*break*/, 6]; if (state._conf.logging) { message = gotFirstServerResponse - ? '[vuex-easy-firestore] recreating doc after remote deletion' - : '[vuex-easy-firestore] inserting initial doc'; - console.log(message); + ? 'recreating doc after remote deletion' + : 'inserting initial doc'; + console.log("%c [vuex-easy-firestore] " + message + "; for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: MediumSeaGreen'); } + _a.label = 2; + case 2: + _a.trys.push([2, 4, , 5]); return [4 /*yield*/, dispatch('insertInitialDoc') // if the initial document was successfully inserted ]; - case 2: - resp = _a.sent(); + case 3: + _a.sent(); // if the initial document was successfully inserted - if (!resp) { - if (initialPromise.isPending) { - streamingStart(); - } - if (refreshedPromise.isPending) { - refreshedPromise.resolve(); - } + if (initialPromise.isPending) { + streamingStart(); } - else { - // we close the channel ourselves. Firestore does not, as it leaves the - // channel open as long as the user has read rights on the document, even - // if it does not exist. But since the dev enabled `insertInitialDoc`, - // it makes some sense to close as we can assume the user should have had - // write rights - streamingStop('failedRecreatingDoc'); + if (refreshedPromise.isPending) { + refreshedPromise.resolve(); } - return [3 /*break*/, 4]; - case 3: + return [3 /*break*/, 5]; + case 4: + error_1 = _a.sent(); + // we close the channel ourselves. Firestore does not, as it leaves the + // channel open as long as the user has read rights on the document, even + // if it does not exist. But since the dev enabled `insertInitialDoc`, + // it makes some sense to close as we can assume the user should have had + // write rights + streamingStop(error_1); + return [3 /*break*/, 5]; + case 5: return [3 /*break*/, 7]; + case 6: streamingStop('preventInitialDocInsertion'); - _a.label = 4; - case 4: return [3 /*break*/, 6]; - case 5: + _a.label = 7; + case 7: return [3 /*break*/, 9]; + case 8: processDocument(querySnapshot.data()); if (initialPromise.isPending) { streamingStart(); @@ -1441,9 +1472,9 @@ function pluginActions (Firebase) { if (refreshedPromise.isPending) { refreshedPromise.resolve(); } - _a.label = 6; - case 6: return [3 /*break*/, 8]; - case 7: + _a.label = 9; + case 9: return [3 /*break*/, 11]; + case 10: processCollection(querySnapshot.docChanges()); if (initialPromise.isPending) { streamingStart(); @@ -1451,11 +1482,11 @@ function pluginActions (Firebase) { if (refreshedPromise.isPending) { refreshedPromise.resolve(); } - _a.label = 8; - case 8: + _a.label = 11; + case 11: gotFirstServerResponse = true; - _a.label = 9; - case 9: return [2 /*return*/]; + _a.label = 12; + case 12: return [2 /*return*/]; } }); }); }, streamingStop); diff --git a/dist/index.esm.js b/dist/index.esm.js index f1713d05..fd9fc841 100644 --- a/dist/index.esm.js +++ b/dist/index.esm.js @@ -88,6 +88,18 @@ function pluginState () { }; } +var errorMessages = { + 'user-auth': "\n Error trying to set userId.\n Please double check if you have correctly authenticated the user with Firebase Auth before calling `openDBChannel` or `fetchAndAdd`.\n\n If you still get this error, try passing your firebase instance to the plugin as described in the documentation:\n https://mesqueeb.github.io/vuex-easy-firestore/extra-features.html#pass-firebase-dependency\n ", + 'delete-missing-id': "\n Missing id of the doc you want to delete!\n Correct usage:\n dispatch('delete', id)\n ", + 'delete-missing-path': "\n Missing path to the prop you want to delete!\n Correct usage:\n dispatch('delete', 'path.to.prop')\n\n Use `.` for sub props!\n ", + 'missing-id': "\n This action requires an id to be passed!\n ", + 'patch-missing-id': "\n Missing an id of the doc you want to patch!\n Correct usage:\n\n // pass `id` as a prop:\n dispatch('module/set', {id: '123', name: 'best item name'})\n // or\n dispatch('module/patch', {id: '123', name: 'best item name'})\n ", + 'missing-path-variables': "\n A path variable was passed without defining it!\n In VuexEasyFirestore you can create paths with variables:\n eg: `groups/{groupId}/user/{userId}`\n\n `userId` is automatically replaced with the userId of the firebase user.\n `groupId` or any other variable that needs to be set after authentication needs to be passed upon the `openDBChannel` action.\n\n // (in module config) Example path:\n firestorePath: 'groups/{groupId}/user/{userId}'\n\n // Then before openDBChannel:\n // retrieve the value\n const groupId = someIdRetrievedAfterSignin\n // pass as argument into openDBChannel:\n dispatch('moduleName/openDBChannel', {groupId})\n ", + 'patch-no-ref': "\n Something went wrong during the PATCH mutation:\n The document it's trying to patch does not exist.\n ", + 'only-in-collection-mode': "\n The action you dispatched can only be used in 'collection' mode.\n ", + 'initial-doc-failed': "\n Initial doc insertion failed. Further `set` or `patch` actions will also fail. Requires an internet connection when the initial doc is inserted. Check the error returned by Firebase:\n ", + 'sync-error': "\n Something went wrong while trying to synchronise data to Cloud Firestore.\n The data is kept in queue, so that it will try to sync again upon the next 'set' or 'patch' action.\n ", +}; /** * execute Error() based on an error id string * @@ -97,6 +109,10 @@ function pluginState () { * @returns {string} the error id */ function error (errorId, error) { + var logData = errorMessages[errorId] || errorId; + console.error("[vuex-easy-firestore] Error! " + logData); + if (error) + console.error(error); return errorId; } @@ -881,20 +897,25 @@ function pluginActions (Firebase) { var initialDocPrepared = getters.prepareInitialDocForInsert(initialDoc); // 2. Create a reference to the SF doc. var initialDocRef = getters.dbRef; - return Firebase.firestore().runTransaction(function (transaction) { - // This code may get re-run multiple times if there are conflicts. - return transaction.get(initialDocRef) - .then(function (foundInitialDoc) { - if (!foundInitialDoc.exists) { - transaction.set(initialDocRef, initialDocPrepared); + return new Promise(function (resolve, reject) { + Firebase.firestore().runTransaction(function (transaction) { + // This code may get re-run multiple times if there are conflicts. + return transaction.get(initialDocRef) + .then(function (foundInitialDoc) { + if (!foundInitialDoc.exists) { + transaction.set(initialDocRef, initialDocPrepared); + } + }); + }).then(function (_) { + if (state._conf.logging) { + var message = 'Initial doc succesfully inserted'; + console.log("%c [vuex-easy-firestore] " + message + "; for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: SeaGreen'); } + resolve(); + }).catch(function (error$1) { + error('initial-doc-failed', error$1); + reject(error$1); }); - }).then(function (_) { - if (state._conf.logging) { - console.log('[vuex-easy-firestore] Initial doc succesfully inserted.'); - } - }).catch(function (error$1) { - return error('initial-doc-failed'); }); }, handleSyncStackDebounce: function (_a, payloadToResolve) { @@ -946,6 +967,7 @@ function pluginActions (Firebase) { }).catch(function (error$1) { state._sync.patching = 'error'; state._sync.syncStack.debounceTimer = null; + error('sync-error', error$1); return reject(error$1); }); }); @@ -1096,7 +1118,7 @@ function pluginActions (Firebase) { console.log("%c fetch for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: goldenrod'); } return getters.dbRef.get().then(function (_doc) { return __awaiter(_this, void 0, void 0, function () { - var id, doc; + var message, id, doc; return __generator(this, function (_a) { switch (_a.label) { case 0: @@ -1104,11 +1126,16 @@ function pluginActions (Firebase) { // No initial doc found in docMode if (state._conf.sync.preventInitialDocInsertion) throw 'preventInitialDocInsertion'; - if (state._conf.logging) - console.log('[vuex-easy-firestore] inserting initial doc'); - return [4 /*yield*/, dispatch('insertInitialDoc')]; + if (state._conf.logging) { + message = 'inserting initial doc'; + console.log("%c [vuex-easy-firestore] " + message + "; for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: MediumSeaGreen'); + } + return [4 /*yield*/, dispatch('insertInitialDoc') + // an error in await here is (somehow) caught in the catch down below + ]; case 1: _a.sent(); + // an error in await here is (somehow) caught in the catch down below return [2 /*return*/, _doc]; case 2: id = getters.docModeId; @@ -1118,7 +1145,8 @@ function pluginActions (Firebase) { } }); }); }).catch(function (error$1) { - return error(error$1); + error(error$1); + throw error$1; }); } // 'collection' mode: @@ -1361,7 +1389,7 @@ function pluginActions (Firebase) { }); }; var unsubscribe = dbRef.onSnapshot({ includeMetadataChanges: includeMetadataChanges }, function (querySnapshot) { return __awaiter(_this, void 0, void 0, function () { - var message, resp; + var message, error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: @@ -1387,45 +1415,48 @@ function pluginActions (Firebase) { } gotFirstLocalResponse = true; } - return [3 /*break*/, 9]; + return [3 /*break*/, 12]; case 1: - if (!!getters.collectionMode) return [3 /*break*/, 7]; - if (!!querySnapshot.data()) return [3 /*break*/, 5]; - if (!!state._conf.sync.preventInitialDocInsertion) return [3 /*break*/, 3]; + if (!!getters.collectionMode) return [3 /*break*/, 10]; + if (!!querySnapshot.data()) return [3 /*break*/, 8]; + if (!!state._conf.sync.preventInitialDocInsertion) return [3 /*break*/, 6]; if (state._conf.logging) { message = gotFirstServerResponse - ? '[vuex-easy-firestore] recreating doc after remote deletion' - : '[vuex-easy-firestore] inserting initial doc'; - console.log(message); + ? 'recreating doc after remote deletion' + : 'inserting initial doc'; + console.log("%c [vuex-easy-firestore] " + message + "; for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: MediumSeaGreen'); } + _a.label = 2; + case 2: + _a.trys.push([2, 4, , 5]); return [4 /*yield*/, dispatch('insertInitialDoc') // if the initial document was successfully inserted ]; - case 2: - resp = _a.sent(); + case 3: + _a.sent(); // if the initial document was successfully inserted - if (!resp) { - if (initialPromise.isPending) { - streamingStart(); - } - if (refreshedPromise.isPending) { - refreshedPromise.resolve(); - } + if (initialPromise.isPending) { + streamingStart(); } - else { - // we close the channel ourselves. Firestore does not, as it leaves the - // channel open as long as the user has read rights on the document, even - // if it does not exist. But since the dev enabled `insertInitialDoc`, - // it makes some sense to close as we can assume the user should have had - // write rights - streamingStop('failedRecreatingDoc'); + if (refreshedPromise.isPending) { + refreshedPromise.resolve(); } - return [3 /*break*/, 4]; - case 3: + return [3 /*break*/, 5]; + case 4: + error_1 = _a.sent(); + // we close the channel ourselves. Firestore does not, as it leaves the + // channel open as long as the user has read rights on the document, even + // if it does not exist. But since the dev enabled `insertInitialDoc`, + // it makes some sense to close as we can assume the user should have had + // write rights + streamingStop(error_1); + return [3 /*break*/, 5]; + case 5: return [3 /*break*/, 7]; + case 6: streamingStop('preventInitialDocInsertion'); - _a.label = 4; - case 4: return [3 /*break*/, 6]; - case 5: + _a.label = 7; + case 7: return [3 /*break*/, 9]; + case 8: processDocument(querySnapshot.data()); if (initialPromise.isPending) { streamingStart(); @@ -1435,9 +1466,9 @@ function pluginActions (Firebase) { if (refreshedPromise.isPending) { refreshedPromise.resolve(); } - _a.label = 6; - case 6: return [3 /*break*/, 8]; - case 7: + _a.label = 9; + case 9: return [3 /*break*/, 11]; + case 10: processCollection(querySnapshot.docChanges()); if (initialPromise.isPending) { streamingStart(); @@ -1445,11 +1476,11 @@ function pluginActions (Firebase) { if (refreshedPromise.isPending) { refreshedPromise.resolve(); } - _a.label = 8; - case 8: + _a.label = 11; + case 11: gotFirstServerResponse = true; - _a.label = 9; - case 9: return [2 /*return*/]; + _a.label = 12; + case 12: return [2 /*return*/]; } }); }); }, streamingStop); diff --git a/src/module/actions.ts b/src/module/actions.ts index 0a1f039e..b2c714a4 100644 --- a/src/module/actions.ts +++ b/src/module/actions.ts @@ -173,21 +173,29 @@ export default function (Firebase: any): AnyObject { const initialDocPrepared = getters.prepareInitialDocForInsert(initialDoc) // 2. Create a reference to the SF doc. - var initialDocRef = getters.dbRef - return Firebase.firestore().runTransaction(transaction => { - // This code may get re-run multiple times if there are conflicts. - return transaction.get(initialDocRef) - .then(foundInitialDoc => { - if (!foundInitialDoc.exists) { - transaction.set(initialDocRef, initialDocPrepared) + const initialDocRef = getters.dbRef + return new Promise((resolve, reject) => { + Firebase.firestore().runTransaction(transaction => { + // This code may get re-run multiple times if there are conflicts. + return transaction.get(initialDocRef) + .then(foundInitialDoc => { + if (!foundInitialDoc.exists) { + transaction.set(initialDocRef, initialDocPrepared) + } + }) + }).then(_ => { + if (state._conf.logging) { + const message = 'Initial doc succesfully inserted' + console.log( + `%c [vuex-easy-firestore] ${message}; for Firestore PATH: ${getters.firestorePathComplete} [${state._conf.firestorePath}]`, + 'color: SeaGreen' + ) } + resolve() + }).catch(error => { + logError('initial-doc-failed', error) + reject(error) }) - }).then(_ => { - if (state._conf.logging) { - console.log('[vuex-easy-firestore] Initial doc succesfully inserted.') - } - }).catch(error => { - return logError('initial-doc-failed', error) }) }, handleSyncStackDebounce ({state, commit, dispatch, getters}, payloadToResolve) { @@ -377,8 +385,15 @@ export default function (Firebase: any): AnyObject { if (!_doc.exists) { // No initial doc found in docMode if (state._conf.sync.preventInitialDocInsertion) throw 'preventInitialDocInsertion' - if (state._conf.logging) console.log('[vuex-easy-firestore] inserting initial doc') + if (state._conf.logging) { + const message = 'inserting initial doc' + console.log( + `%c [vuex-easy-firestore] ${message}; for Firestore PATH: ${getters.firestorePathComplete} [${state._conf.firestorePath}]`, + 'color: MediumSeaGreen' + ) + } await dispatch('insertInitialDoc') + // an error in await here is (somehow) caught in the catch down below return _doc } const id = getters.docModeId @@ -386,7 +401,8 @@ export default function (Firebase: any): AnyObject { dispatch('applyHooksAndUpdateState', {change: 'modified', id, doc}) return doc }).catch(error => { - return logError(error) + logError(error) + throw error }) } // 'collection' mode: @@ -649,27 +665,29 @@ export default function (Firebase: any): AnyObject { if (!state._conf.sync.preventInitialDocInsertion) { if (state._conf.logging) { const message = gotFirstServerResponse - ? '[vuex-easy-firestore] recreating doc after remote deletion' - : '[vuex-easy-firestore] inserting initial doc' - console.log(message) + ? 'recreating doc after remote deletion' + : 'inserting initial doc' + console.log( + `%c [vuex-easy-firestore] ${message}; for Firestore PATH: ${getters.firestorePathComplete} [${state._conf.firestorePath}]`, + 'color: MediumSeaGreen' + ) } - const resp = await dispatch('insertInitialDoc') - // if the initial document was successfully inserted - if (!resp) { + try { + await dispatch('insertInitialDoc') + // if the initial document was successfully inserted if (initialPromise.isPending) { streamingStart() } if (refreshedPromise.isPending) { refreshedPromise.resolve() } - } - else { + } catch (error) { // we close the channel ourselves. Firestore does not, as it leaves the // channel open as long as the user has read rights on the document, even // if it does not exist. But since the dev enabled `insertInitialDoc`, // it makes some sense to close as we can assume the user should have had // write rights - streamingStop('failedRecreatingDoc') + streamingStop(error) } } // we are not allowed to (re)create the doc: close the channel and reject diff --git a/src/module/errors.ts b/src/module/errors.ts index 8189e509..ff8079f0 100644 --- a/src/module/errors.ts +++ b/src/module/errors.ts @@ -56,7 +56,7 @@ const errorMessages = { The action you dispatched can only be used in 'collection' mode. `, 'initial-doc-failed': ` - Initial doc insertion failed. Further \`set\` or \`patch\` actions will also fail. Requires an internet connection when the initial doc is inserted. Please connect to the internet and refresh the page. + Initial doc insertion failed. Further \`set\` or \`patch\` actions will also fail. Requires an internet connection when the initial doc is inserted. Check the error returned by Firebase: `, 'sync-error': ` Something went wrong while trying to synchronise data to Cloud Firestore. @@ -74,8 +74,7 @@ const errorMessages = { */ export default function (errorId: string, error?: any): string { const logData = errorMessages[errorId] || errorId - const log = `[vuex-easy-firestore] Error! ${logData}` - Error(log) - if (error) Error(error) + console.error(`[vuex-easy-firestore] Error! ${logData}`) + if (error) console.error(error) return errorId } diff --git a/test/helpers/index.cjs.js b/test/helpers/index.cjs.js index 2e9dcb0d..3da24efd 100644 --- a/test/helpers/index.cjs.js +++ b/test/helpers/index.cjs.js @@ -719,6 +719,18 @@ function pluginState () { }; } +var errorMessages = { + 'user-auth': "\n Error trying to set userId.\n Please double check if you have correctly authenticated the user with Firebase Auth before calling `openDBChannel` or `fetchAndAdd`.\n\n If you still get this error, try passing your firebase instance to the plugin as described in the documentation:\n https://mesqueeb.github.io/vuex-easy-firestore/extra-features.html#pass-firebase-dependency\n ", + 'delete-missing-id': "\n Missing id of the doc you want to delete!\n Correct usage:\n dispatch('delete', id)\n ", + 'delete-missing-path': "\n Missing path to the prop you want to delete!\n Correct usage:\n dispatch('delete', 'path.to.prop')\n\n Use `.` for sub props!\n ", + 'missing-id': "\n This action requires an id to be passed!\n ", + 'patch-missing-id': "\n Missing an id of the doc you want to patch!\n Correct usage:\n\n // pass `id` as a prop:\n dispatch('module/set', {id: '123', name: 'best item name'})\n // or\n dispatch('module/patch', {id: '123', name: 'best item name'})\n ", + 'missing-path-variables': "\n A path variable was passed without defining it!\n In VuexEasyFirestore you can create paths with variables:\n eg: `groups/{groupId}/user/{userId}`\n\n `userId` is automatically replaced with the userId of the firebase user.\n `groupId` or any other variable that needs to be set after authentication needs to be passed upon the `openDBChannel` action.\n\n // (in module config) Example path:\n firestorePath: 'groups/{groupId}/user/{userId}'\n\n // Then before openDBChannel:\n // retrieve the value\n const groupId = someIdRetrievedAfterSignin\n // pass as argument into openDBChannel:\n dispatch('moduleName/openDBChannel', {groupId})\n ", + 'patch-no-ref': "\n Something went wrong during the PATCH mutation:\n The document it's trying to patch does not exist.\n ", + 'only-in-collection-mode': "\n The action you dispatched can only be used in 'collection' mode.\n ", + 'initial-doc-failed': "\n Initial doc insertion failed. Further `set` or `patch` actions will also fail. Requires an internet connection when the initial doc is inserted. Check the error returned by Firebase:\n ", + 'sync-error': "\n Something went wrong while trying to synchronise data to Cloud Firestore.\n The data is kept in queue, so that it will try to sync again upon the next 'set' or 'patch' action.\n ", +}; /** * execute Error() based on an error id string * @@ -728,6 +740,10 @@ function pluginState () { * @returns {string} the error id */ function error (errorId, error) { + var logData = errorMessages[errorId] || errorId; + console.error("[vuex-easy-firestore] Error! " + logData); + if (error) + console.error(error); return errorId; } @@ -1407,20 +1423,25 @@ function pluginActions (Firebase) { var initialDocPrepared = getters.prepareInitialDocForInsert(initialDoc); // 2. Create a reference to the SF doc. var initialDocRef = getters.dbRef; - return Firebase.firestore().runTransaction(function (transaction) { - // This code may get re-run multiple times if there are conflicts. - return transaction.get(initialDocRef) - .then(function (foundInitialDoc) { - if (!foundInitialDoc.exists) { - transaction.set(initialDocRef, initialDocPrepared); + return new Promise(function (resolve, reject) { + Firebase.firestore().runTransaction(function (transaction) { + // This code may get re-run multiple times if there are conflicts. + return transaction.get(initialDocRef) + .then(function (foundInitialDoc) { + if (!foundInitialDoc.exists) { + transaction.set(initialDocRef, initialDocPrepared); + } + }); + }).then(function (_) { + if (state._conf.logging) { + var message = 'Initial doc succesfully inserted'; + console.log("%c [vuex-easy-firestore] " + message + "; for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: SeaGreen'); } + resolve(); + }).catch(function (error$1) { + error('initial-doc-failed', error$1); + reject(error$1); }); - }).then(function (_) { - if (state._conf.logging) { - console.log('[vuex-easy-firestore] Initial doc succesfully inserted.'); - } - }).catch(function (error$1) { - return error('initial-doc-failed'); }); }, handleSyncStackDebounce: function (_a, payloadToResolve) { @@ -1472,6 +1493,7 @@ function pluginActions (Firebase) { }).catch(function (error$1) { state._sync.patching = 'error'; state._sync.syncStack.debounceTimer = null; + error('sync-error', error$1); return reject(error$1); }); }); @@ -1622,7 +1644,7 @@ function pluginActions (Firebase) { console.log("%c fetch for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: goldenrod'); } return getters.dbRef.get().then(function (_doc) { return __awaiter(_this, void 0, void 0, function () { - var id, doc; + var message, id, doc; return __generator(this, function (_a) { switch (_a.label) { case 0: @@ -1630,11 +1652,16 @@ function pluginActions (Firebase) { // No initial doc found in docMode if (state._conf.sync.preventInitialDocInsertion) throw 'preventInitialDocInsertion'; - if (state._conf.logging) - console.log('[vuex-easy-firestore] inserting initial doc'); - return [4 /*yield*/, dispatch('insertInitialDoc')]; + if (state._conf.logging) { + message = 'inserting initial doc'; + console.log("%c [vuex-easy-firestore] " + message + "; for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: MediumSeaGreen'); + } + return [4 /*yield*/, dispatch('insertInitialDoc') + // an error in await here is (somehow) caught in the catch down below + ]; case 1: _a.sent(); + // an error in await here is (somehow) caught in the catch down below return [2 /*return*/, _doc]; case 2: id = getters.docModeId; @@ -1644,7 +1671,8 @@ function pluginActions (Firebase) { } }); }); }).catch(function (error$1) { - return error(error$1); + error(error$1); + throw error$1; }); } // 'collection' mode: @@ -1887,7 +1915,7 @@ function pluginActions (Firebase) { }); }; var unsubscribe = dbRef.onSnapshot({ includeMetadataChanges: includeMetadataChanges }, function (querySnapshot) { return __awaiter(_this, void 0, void 0, function () { - var message, resp; + var message, error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: @@ -1913,45 +1941,48 @@ function pluginActions (Firebase) { } gotFirstLocalResponse = true; } - return [3 /*break*/, 9]; + return [3 /*break*/, 12]; case 1: - if (!!getters.collectionMode) return [3 /*break*/, 7]; - if (!!querySnapshot.data()) return [3 /*break*/, 5]; - if (!!state._conf.sync.preventInitialDocInsertion) return [3 /*break*/, 3]; + if (!!getters.collectionMode) return [3 /*break*/, 10]; + if (!!querySnapshot.data()) return [3 /*break*/, 8]; + if (!!state._conf.sync.preventInitialDocInsertion) return [3 /*break*/, 6]; if (state._conf.logging) { message = gotFirstServerResponse - ? '[vuex-easy-firestore] recreating doc after remote deletion' - : '[vuex-easy-firestore] inserting initial doc'; - console.log(message); + ? 'recreating doc after remote deletion' + : 'inserting initial doc'; + console.log("%c [vuex-easy-firestore] " + message + "; for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: MediumSeaGreen'); } + _a.label = 2; + case 2: + _a.trys.push([2, 4, , 5]); return [4 /*yield*/, dispatch('insertInitialDoc') // if the initial document was successfully inserted ]; - case 2: - resp = _a.sent(); + case 3: + _a.sent(); // if the initial document was successfully inserted - if (!resp) { - if (initialPromise.isPending) { - streamingStart(); - } - if (refreshedPromise.isPending) { - refreshedPromise.resolve(); - } + if (initialPromise.isPending) { + streamingStart(); } - else { - // we close the channel ourselves. Firestore does not, as it leaves the - // channel open as long as the user has read rights on the document, even - // if it does not exist. But since the dev enabled `insertInitialDoc`, - // it makes some sense to close as we can assume the user should have had - // write rights - streamingStop('failedRecreatingDoc'); + if (refreshedPromise.isPending) { + refreshedPromise.resolve(); } - return [3 /*break*/, 4]; - case 3: + return [3 /*break*/, 5]; + case 4: + error_1 = _a.sent(); + // we close the channel ourselves. Firestore does not, as it leaves the + // channel open as long as the user has read rights on the document, even + // if it does not exist. But since the dev enabled `insertInitialDoc`, + // it makes some sense to close as we can assume the user should have had + // write rights + streamingStop(error_1); + return [3 /*break*/, 5]; + case 5: return [3 /*break*/, 7]; + case 6: streamingStop('preventInitialDocInsertion'); - _a.label = 4; - case 4: return [3 /*break*/, 6]; - case 5: + _a.label = 7; + case 7: return [3 /*break*/, 9]; + case 8: processDocument(querySnapshot.data()); if (initialPromise.isPending) { streamingStart(); @@ -1961,9 +1992,9 @@ function pluginActions (Firebase) { if (refreshedPromise.isPending) { refreshedPromise.resolve(); } - _a.label = 6; - case 6: return [3 /*break*/, 8]; - case 7: + _a.label = 9; + case 9: return [3 /*break*/, 11]; + case 10: processCollection(querySnapshot.docChanges()); if (initialPromise.isPending) { streamingStart(); @@ -1971,11 +2002,11 @@ function pluginActions (Firebase) { if (refreshedPromise.isPending) { refreshedPromise.resolve(); } - _a.label = 8; - case 8: + _a.label = 11; + case 11: gotFirstServerResponse = true; - _a.label = 9; - case 9: return [2 /*return*/]; + _a.label = 12; + case 12: return [2 /*return*/]; } }); }); }, streamingStop); From 31b9af4ab5ca13e376621e259081fcfe23ba579c Mon Sep 17 00:00:00 2001 From: Mesqueeb Date: Tue, 10 Dec 2019 22:07:36 +0900 Subject: [PATCH 7/9] =?UTF-8?q?remove=20unrequired=20transaction=20?= =?UTF-8?q?=F0=9F=9F=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/module/actions.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/module/actions.ts b/src/module/actions.ts index b2c714a4..b3694d3f 100644 --- a/src/module/actions.ts +++ b/src/module/actions.ts @@ -173,17 +173,9 @@ export default function (Firebase: any): AnyObject { const initialDocPrepared = getters.prepareInitialDocForInsert(initialDoc) // 2. Create a reference to the SF doc. - const initialDocRef = getters.dbRef return new Promise((resolve, reject) => { - Firebase.firestore().runTransaction(transaction => { - // This code may get re-run multiple times if there are conflicts. - return transaction.get(initialDocRef) - .then(foundInitialDoc => { - if (!foundInitialDoc.exists) { - transaction.set(initialDocRef, initialDocPrepared) - } - }) - }).then(_ => { + getters.dbRef.set(initialDocPrepared) + .then(() => { if (state._conf.logging) { const message = 'Initial doc succesfully inserted' console.log( @@ -660,7 +652,7 @@ export default function (Firebase: any): AnyObject { // 'doc' mode: if (!getters.collectionMode) { // if the document doesn't exist yet - if (!querySnapshot.data()) { + if (!querySnapshot.exists) { // if it's ok to insert an initial document if (!state._conf.sync.preventInitialDocInsertion) { if (state._conf.logging) { From 78ece0b8091c6031a479b7b3cffd99e0afd716a3 Mon Sep 17 00:00:00 2001 From: Mesqueeb Date: Tue, 10 Dec 2019 22:09:04 +0900 Subject: [PATCH 8/9] =?UTF-8?q?update=20dependencies=20=F0=9F=A7=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 56 +++++++++++++++++++++++------------------------ package.json | 16 +++++++------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8fc53820..bf0d413e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vuex-easy-firestore", - "version": "1.34.5", + "version": "1.35.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3093,9 +3093,9 @@ "dev": true }, "arg": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.1.tgz", - "integrity": "sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.2.tgz", + "integrity": "sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg==", "dev": true }, "argparse": { @@ -4922,11 +4922,11 @@ "dev": true }, "copy-anything": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-1.5.0.tgz", - "integrity": "sha512-veu74zguz1JFaRbGNEoB+/qBsgQ7h6y9dxEfGgf1xdOdX+13BYREGa6jn490rzeTiIAKYQHgrFwk2nuIF4YZXA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-1.5.1.tgz", + "integrity": "sha512-CGc6y1tkkmRPg4CJUCe/lTUpGe4NTv8R9Ka+YwhjQFI7ohFYLDifS5OiHiPfW9gPmUVhO0XnagXP7RZjZFPy1w==", "requires": { - "is-what": "^3.2.4" + "is-what": "^3.3.1" } }, "copy-concurrently": { @@ -9077,9 +9077,9 @@ "dev": true }, "is-what": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.3.1.tgz", - "integrity": "sha512-seFn10yAXy+yJlTRO+8VfiafC+0QJanGLMPTBWLrJm/QPauuchy0UXh8B6H5o9VA8BAzk0iYievt6mNp6gfaqA==" + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.4.0.tgz", + "integrity": "sha512-oFdBRuSY9PocqPoUUseDXek4I+A1kWGigZGhuG+7GEkp0tRkek11adc0HbTEVsNvtojV7rp0uhf5LWtGvHzoOQ==" }, "is-windows": { "version": "1.0.2", @@ -9805,9 +9805,9 @@ } }, "merge-anything": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-2.4.1.tgz", - "integrity": "sha512-dYOIAl9GFCJNctSIHWOj9OJtarCjsD16P8ObCl6oxrujAG+kOvlwJuOD9/O9iYZ9aTi1RGpGTG9q9etIvuUikQ==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-2.4.4.tgz", + "integrity": "sha512-l5XlriUDJKQT12bH+rVhAHjwIuXWdAIecGwsYjv2LJo+dA1AeRTmeQS+3QBpO6lEthBMDi2IUMpLC1yyRvGlwQ==", "requires": { "is-what": "^3.3.1" } @@ -13952,9 +13952,9 @@ "dev": true }, "ts-node": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.4.1.tgz", - "integrity": "sha512-5LpRN+mTiCs7lI5EtbXmF/HfMeCjzt7DH9CZwtkr6SywStrNQC723wG+aOWFiLNn7zT3kD/RnFqi3ZUfr4l5Qw==", + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.5.4.tgz", + "integrity": "sha512-izbVCRV68EasEPQ8MSIGBNK9dc/4sYJJKYA+IarMQct1RtEot6Xp0bXuClsbUSnKpg50ho+aOAx8en5c+y4OFw==", "dev": true, "requires": { "arg": "^4.1.0", @@ -14032,9 +14032,9 @@ } }, "typescript": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", - "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz", + "integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==", "dev": true }, "uc.micro": { @@ -14670,19 +14670,19 @@ } }, "vuex": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.1.1.tgz", - "integrity": "sha512-ER5moSbLZuNSMBFnEBVGhQ1uCBNJslH9W/Dw2W7GZN23UQA69uapP5GTT9Vm8Trc0PzBSVt6LzF3hGjmv41xcg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.1.2.tgz", + "integrity": "sha512-ha3jNLJqNhhrAemDXcmMJMKf1Zu4sybMPr9KxJIuOpVcsDQlTBYLLladav2U+g1AvdYDG5Gs0xBTb0M5pXXYFQ==", "dev": true }, "vuex-easy-access": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/vuex-easy-access/-/vuex-easy-access-3.1.7.tgz", - "integrity": "sha512-5VbwUSjN3mLeFpxYXP3AGiVe2wqJs4uI5o7OHD5xQyTRhI9VAMcCSrI1eJcx68S3Df+4J8K+CiYNeABKLCA6ew==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/vuex-easy-access/-/vuex-easy-access-3.1.8.tgz", + "integrity": "sha512-R1+a8aRHZlwZHgabjJ2OBOVJr7ukga0tiKeB22dzED8CVfCqX6Jy48IMknrhqKbiCYa+CGfdHMe8j5Urwijt7g==", "requires": { "find-and-replace-anything": "^2.0.4", - "is-what": "^3.2.4", - "merge-anything": "^2.4.0" + "is-what": "^3.3.1", + "merge-anything": "^2.4.2" } }, "watchpack": { diff --git a/package.json b/package.json index 9d480576..980b5a22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vuex-easy-firestore", - "version": "1.35.0", + "version": "1.35.1", "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", @@ -39,13 +39,13 @@ "homepage": "https://github.com/mesqueeb/vuex-easy-firestore#readme", "dependencies": { "compare-anything": "^0.1.3", - "copy-anything": "^1.5.0", + "copy-anything": "^1.5.1", "filter-anything": "^1.1.4", "find-and-replace-anything": "^2.0.4", "flatten-anything": "^1.4.1", - "is-what": "^3.3.1", - "merge-anything": "^2.4.1", - "vuex-easy-access": "^3.1.7" + "is-what": "^3.4.0", + "merge-anything": "^2.4.4", + "vuex-easy-access": "^3.1.8" }, "devDependencies": { "@vue/eslint-config-standard": "^4.0.0", @@ -59,11 +59,11 @@ "mock-cloud-firestore": "^0.9.3", "rollup-plugin-node-resolve": "^4.2.4", "rollup-plugin-typescript2": "^0.22.1", - "ts-node": "^8.4.1", - "typescript": "^3.6.4", + "ts-node": "^8.5.4", + "typescript": "^3.7.3", "vue": "^2.6.10", "vuepress": "^1.2.0", - "vuex": "^3.1.1" + "vuex": "^3.1.2" }, "peerDependencies": { "firebase": "^6.3.0" From d7d53d4996a6bf45304138ab83ed7bc1b09fb599 Mon Sep 17 00:00:00 2001 From: Mesqueeb Date: Tue, 10 Dec 2019 22:16:38 +0900 Subject: [PATCH 9/9] =?UTF-8?q?v1.35.1=20=F0=9F=90=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/index.cjs.js | 14 +++----------- dist/index.esm.js | 14 +++----------- test/helpers/index.cjs.js | 14 +++----------- test/initialDoc.js | 26 +++++++++++--------------- 4 files changed, 20 insertions(+), 48 deletions(-) diff --git a/dist/index.cjs.js b/dist/index.cjs.js index 4111297f..1efa4c66 100644 --- a/dist/index.cjs.js +++ b/dist/index.cjs.js @@ -902,17 +902,9 @@ function pluginActions (Firebase) { var initialDoc = (getters.storeRef) ? getters.storeRef : {}; var initialDocPrepared = getters.prepareInitialDocForInsert(initialDoc); // 2. Create a reference to the SF doc. - var initialDocRef = getters.dbRef; return new Promise(function (resolve, reject) { - Firebase.firestore().runTransaction(function (transaction) { - // This code may get re-run multiple times if there are conflicts. - return transaction.get(initialDocRef) - .then(function (foundInitialDoc) { - if (!foundInitialDoc.exists) { - transaction.set(initialDocRef, initialDocPrepared); - } - }); - }).then(function (_) { + getters.dbRef.set(initialDocPrepared) + .then(function () { if (state._conf.logging) { var message = 'Initial doc succesfully inserted'; console.log("%c [vuex-easy-firestore] " + message + "; for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: SeaGreen'); @@ -1424,7 +1416,7 @@ function pluginActions (Firebase) { return [3 /*break*/, 12]; case 1: if (!!getters.collectionMode) return [3 /*break*/, 10]; - if (!!querySnapshot.data()) return [3 /*break*/, 8]; + if (!!querySnapshot.exists) return [3 /*break*/, 8]; if (!!state._conf.sync.preventInitialDocInsertion) return [3 /*break*/, 6]; if (state._conf.logging) { message = gotFirstServerResponse diff --git a/dist/index.esm.js b/dist/index.esm.js index fd9fc841..d7264786 100644 --- a/dist/index.esm.js +++ b/dist/index.esm.js @@ -896,17 +896,9 @@ function pluginActions (Firebase) { var initialDoc = (getters.storeRef) ? getters.storeRef : {}; var initialDocPrepared = getters.prepareInitialDocForInsert(initialDoc); // 2. Create a reference to the SF doc. - var initialDocRef = getters.dbRef; return new Promise(function (resolve, reject) { - Firebase.firestore().runTransaction(function (transaction) { - // This code may get re-run multiple times if there are conflicts. - return transaction.get(initialDocRef) - .then(function (foundInitialDoc) { - if (!foundInitialDoc.exists) { - transaction.set(initialDocRef, initialDocPrepared); - } - }); - }).then(function (_) { + getters.dbRef.set(initialDocPrepared) + .then(function () { if (state._conf.logging) { var message = 'Initial doc succesfully inserted'; console.log("%c [vuex-easy-firestore] " + message + "; for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: SeaGreen'); @@ -1418,7 +1410,7 @@ function pluginActions (Firebase) { return [3 /*break*/, 12]; case 1: if (!!getters.collectionMode) return [3 /*break*/, 10]; - if (!!querySnapshot.data()) return [3 /*break*/, 8]; + if (!!querySnapshot.exists) return [3 /*break*/, 8]; if (!!state._conf.sync.preventInitialDocInsertion) return [3 /*break*/, 6]; if (state._conf.logging) { message = gotFirstServerResponse diff --git a/test/helpers/index.cjs.js b/test/helpers/index.cjs.js index 3da24efd..42a0e780 100644 --- a/test/helpers/index.cjs.js +++ b/test/helpers/index.cjs.js @@ -1422,17 +1422,9 @@ function pluginActions (Firebase) { var initialDoc = (getters.storeRef) ? getters.storeRef : {}; var initialDocPrepared = getters.prepareInitialDocForInsert(initialDoc); // 2. Create a reference to the SF doc. - var initialDocRef = getters.dbRef; return new Promise(function (resolve, reject) { - Firebase.firestore().runTransaction(function (transaction) { - // This code may get re-run multiple times if there are conflicts. - return transaction.get(initialDocRef) - .then(function (foundInitialDoc) { - if (!foundInitialDoc.exists) { - transaction.set(initialDocRef, initialDocPrepared); - } - }); - }).then(function (_) { + getters.dbRef.set(initialDocPrepared) + .then(function () { if (state._conf.logging) { var message = 'Initial doc succesfully inserted'; console.log("%c [vuex-easy-firestore] " + message + "; for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: SeaGreen'); @@ -1944,7 +1936,7 @@ function pluginActions (Firebase) { return [3 /*break*/, 12]; case 1: if (!!getters.collectionMode) return [3 /*break*/, 10]; - if (!!querySnapshot.data()) return [3 /*break*/, 8]; + if (!!querySnapshot.exists) return [3 /*break*/, 8]; if (!!state._conf.sync.preventInitialDocInsertion) return [3 /*break*/, 6]; if (state._conf.logging) { message = gotFirstServerResponse diff --git a/test/initialDoc.js b/test/initialDoc.js index 85710928..54ce78fc 100644 --- a/test/initialDoc.js +++ b/test/initialDoc.js @@ -1,6 +1,6 @@ import test from 'ava' import wait from './helpers/wait' -import {store} from './helpers/index.cjs.js' +import { store } from './helpers/index.cjs.js' import * as Firebase from 'firebase/app' import 'firebase/firestore' @@ -15,7 +15,7 @@ test('initialDoc through openDBRef & fetchAndAdd', async t => { // doc doesn't exist yet t.is(docR.exists, false) try { - await store.dispatch('initialDoc/openDBChannel', {randomId}) + await store.dispatch('initialDoc/openDBChannel', { randomId }) } catch (error) { console.error(error) t.fail() @@ -40,7 +40,7 @@ test('initialDoc through openDBRef & fetchAndAdd', async t => { docR = await Firebase.firestore().doc(path).get() t.is(docR.exists, false) try { - store.dispatch('initialDoc/fetchAndAdd', {pathVariables: {randomId: randomId2}}) + store.dispatch('initialDoc/fetchAndAdd', { pathVariables: { randomId: randomId2 } }) } catch (error) { t.fail() } @@ -63,17 +63,12 @@ test('preventInitialDoc through openDBRef & fetchAndAdd', async t => { docR = await Firebase.firestore().doc(path).get() // doc doesn't exist yet t.is(docR.exists, false) - // WHY DOES THIS GIVE 1 unhandled rejection: - // try { - // store.dispatch('preventInitialDoc/openDBChannel', {randomId}) - // } catch (error) { - // t.is(error, 'preventInitialDocInsertion') - // } - // THIS WORKS: - store.dispatch('preventInitialDoc/openDBChannel', {randomId}) - .catch(error => { - t.is(error, 'preventInitialDocInsertion') - }) + try { + await store.dispatch('preventInitialDoc/openDBChannel', { randomId }) + } catch (error) { + console.error(error) + t.is(error, 'preventInitialDocInsertion') + } const testFullPath = store.getters['preventInitialDoc/firestorePathComplete'] t.is(testFullPath.split('/').pop(), randomId) const fullPath = store.getters['preventInitialDoc/firestorePathComplete'] @@ -91,8 +86,9 @@ test('preventInitialDoc through openDBRef & fetchAndAdd', async t => { docR = await Firebase.firestore().doc(path).get() t.is(docR.exists, false) try { - store.dispatch('preventInitialDoc/fetchAndAdd', {randomId: randomId2}) + await store.dispatch('preventInitialDoc/fetchAndAdd', { randomId: randomId2 }) } catch (error) { + console.error(error) t.is(error, 'preventInitialDocInsertion') } const fullPath2 = store.getters['preventInitialDoc/firestorePathComplete']