From 0720e074b358690860b3afc4fb49d2142a51f510 Mon Sep 17 00:00:00 2001 From: Luca Ban Date: Mon, 18 Mar 2019 14:54:02 +0900 Subject: [PATCH] =?UTF-8?q?Fix=20for=20initialDocInsert=20=E2=80=BC?= =?UTF-8?q?=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/index.cjs.js | 118 +++++++++++++++++++++++--------------- dist/index.esm.js | 118 +++++++++++++++++++++++--------------- package.json | 2 +- src/module/actions.ts | 27 +++++++-- test/helpers/index.cjs.js | 118 +++++++++++++++++++++++--------------- test/initialDoc.js | 3 +- 6 files changed, 243 insertions(+), 143 deletions(-) diff --git a/dist/index.cjs.js b/dist/index.cjs.js index 74b70972..908933a4 100644 --- a/dist/index.cjs.js +++ b/dist/index.cjs.js @@ -808,9 +808,24 @@ function pluginActions (Firebase) { return; // 1. Prepare for insert var initialDoc = (getters.storeRef) ? getters.storeRef : {}; - var doc = getters.prepareInitialDocForInsert(initialDoc); - // 2. insert - return getters.dbRef.set(doc); + 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); + } + }); + }).then(function () { + if (state._conf.logging) { + console.log('[vuex-easy-firestore] Initial doc succesfully inserted.'); + } + }).catch(function (error) { + console.error('[vuex-easy-firestore] Initial doc succesfully 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.', error); + }); }, handleSyncStackDebounce: function (_a) { var state = _a.state, commit = _a.commit, dispatch = _a.dispatch, getters = _a.getters; @@ -957,19 +972,24 @@ function pluginActions (Firebase) { return getters.dbRef.get().then(function (_doc) { return __awaiter(_this, void 0, void 0, function () { var id, doc; return __generator(this, function (_a) { - 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'); - dispatch('insertInitialDoc'); - return [2 /*return*/, _doc]; + switch (_a.label) { + case 0: + if (!!_doc.exists) return [3 /*break*/, 2]; + // 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')]; + case 1: + _a.sent(); + return [2 /*return*/, _doc]; + case 2: + id = getters.docModeId; + doc = getters.cleanUpRetrievedDoc(_doc.data(), id); + dispatch('applyHooksAndUpdateState', { change: 'modified', id: id, doc: doc }); + return [2 /*return*/, doc]; } - id = getters.docModeId; - doc = getters.cleanUpRetrievedDoc(_doc.data(), id); - dispatch('applyHooksAndUpdateState', { change: 'modified', id: id, doc: doc }); - return [2 /*return*/, doc]; }); }); }).catch(function (error) { console.error('[vuex-easy-firestore]', error); @@ -1055,6 +1075,7 @@ function pluginActions (Firebase) { }); }, openDBChannel: function (_a, pathVariables) { + var _this = this; var getters = _a.getters, state = _a.state, commit = _a.commit, dispatch = _a.dispatch; dispatch('setUserId'); // set state for pathVariables @@ -1093,38 +1114,45 @@ function pluginActions (Firebase) { if (state._conf.logging) { console.log("%c openDBChannel for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: lightcoral'); } - var unsubscribe = dbRef.onSnapshot(function (querySnapshot) { - var 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) - return reject('preventInitialDocInsertion'); - if (state._conf.logging) - console.log('[vuex-easy-firestore] inserting initial doc'); - dispatch('insertInitialDoc'); - return resolve(); + 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) + return [2 /*return*/, reject('preventInitialDocInsertion')]; + if (state._conf.logging) + console.log('[vuex-easy-firestore] inserting initial doc'); + return [4 /*yield*/, dispatch('insertInitialDoc')]; + case 1: + _a.sent(); + return [2 /*return*/, resolve()]; + case 2: + if (source === 'local') + return [2 /*return*/, resolve()]; + id = getters.docModeId; + doc = getters.cleanUpRetrievedDoc(querySnapshot.data(), id); + dispatch('applyHooksAndUpdateState', { change: 'modified', id: id, doc: doc }); + return [2 /*return*/, resolve()]; + case 3: + // 'collection' mode: + querySnapshot.docChanges().forEach(function (change) { + var changeType = change.type; + // Don't do anything for local modifications & removals + if (source === 'local') + return resolve(); + var id = change.doc.id; + var doc = getters.cleanUpRetrievedDoc(change.doc.data(), id); + dispatch('applyHooksAndUpdateState', { change: changeType, id: id, doc: doc }); + }); + return [2 /*return*/, resolve()]; } - if (source === 'local') - return resolve(); - var id = getters.docModeId; - var doc = getters.cleanUpRetrievedDoc(querySnapshot.data(), id); - dispatch('applyHooksAndUpdateState', { change: 'modified', id: id, doc: doc }); - return resolve(); - } - // 'collection' mode: - querySnapshot.docChanges().forEach(function (change) { - var changeType = change.type; - // Don't do anything for local modifications & removals - if (source === 'local') - return resolve(); - var id = change.doc.id; - var doc = getters.cleanUpRetrievedDoc(change.doc.data(), id); - dispatch('applyHooksAndUpdateState', { change: changeType, id: id, doc: doc }); }); - return resolve(); - }, function (error) { + }); }, function (error) { state._sync.patching = 'error'; return reject(error); }); diff --git a/dist/index.esm.js b/dist/index.esm.js index 863307c1..abdb8021 100644 --- a/dist/index.esm.js +++ b/dist/index.esm.js @@ -802,9 +802,24 @@ function pluginActions (Firebase) { return; // 1. Prepare for insert var initialDoc = (getters.storeRef) ? getters.storeRef : {}; - var doc = getters.prepareInitialDocForInsert(initialDoc); - // 2. insert - return getters.dbRef.set(doc); + 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); + } + }); + }).then(function () { + if (state._conf.logging) { + console.log('[vuex-easy-firestore] Initial doc succesfully inserted.'); + } + }).catch(function (error) { + console.error('[vuex-easy-firestore] Initial doc succesfully 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.', error); + }); }, handleSyncStackDebounce: function (_a) { var state = _a.state, commit = _a.commit, dispatch = _a.dispatch, getters = _a.getters; @@ -951,19 +966,24 @@ function pluginActions (Firebase) { return getters.dbRef.get().then(function (_doc) { return __awaiter(_this, void 0, void 0, function () { var id, doc; return __generator(this, function (_a) { - 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'); - dispatch('insertInitialDoc'); - return [2 /*return*/, _doc]; + switch (_a.label) { + case 0: + if (!!_doc.exists) return [3 /*break*/, 2]; + // 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')]; + case 1: + _a.sent(); + return [2 /*return*/, _doc]; + case 2: + id = getters.docModeId; + doc = getters.cleanUpRetrievedDoc(_doc.data(), id); + dispatch('applyHooksAndUpdateState', { change: 'modified', id: id, doc: doc }); + return [2 /*return*/, doc]; } - id = getters.docModeId; - doc = getters.cleanUpRetrievedDoc(_doc.data(), id); - dispatch('applyHooksAndUpdateState', { change: 'modified', id: id, doc: doc }); - return [2 /*return*/, doc]; }); }); }).catch(function (error) { console.error('[vuex-easy-firestore]', error); @@ -1049,6 +1069,7 @@ function pluginActions (Firebase) { }); }, openDBChannel: function (_a, pathVariables) { + var _this = this; var getters = _a.getters, state = _a.state, commit = _a.commit, dispatch = _a.dispatch; dispatch('setUserId'); // set state for pathVariables @@ -1087,38 +1108,45 @@ function pluginActions (Firebase) { if (state._conf.logging) { console.log("%c openDBChannel for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: lightcoral'); } - var unsubscribe = dbRef.onSnapshot(function (querySnapshot) { - var 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) - return reject('preventInitialDocInsertion'); - if (state._conf.logging) - console.log('[vuex-easy-firestore] inserting initial doc'); - dispatch('insertInitialDoc'); - return resolve(); + 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) + return [2 /*return*/, reject('preventInitialDocInsertion')]; + if (state._conf.logging) + console.log('[vuex-easy-firestore] inserting initial doc'); + return [4 /*yield*/, dispatch('insertInitialDoc')]; + case 1: + _a.sent(); + return [2 /*return*/, resolve()]; + case 2: + if (source === 'local') + return [2 /*return*/, resolve()]; + id = getters.docModeId; + doc = getters.cleanUpRetrievedDoc(querySnapshot.data(), id); + dispatch('applyHooksAndUpdateState', { change: 'modified', id: id, doc: doc }); + return [2 /*return*/, resolve()]; + case 3: + // 'collection' mode: + querySnapshot.docChanges().forEach(function (change) { + var changeType = change.type; + // Don't do anything for local modifications & removals + if (source === 'local') + return resolve(); + var id = change.doc.id; + var doc = getters.cleanUpRetrievedDoc(change.doc.data(), id); + dispatch('applyHooksAndUpdateState', { change: changeType, id: id, doc: doc }); + }); + return [2 /*return*/, resolve()]; } - if (source === 'local') - return resolve(); - var id = getters.docModeId; - var doc = getters.cleanUpRetrievedDoc(querySnapshot.data(), id); - dispatch('applyHooksAndUpdateState', { change: 'modified', id: id, doc: doc }); - return resolve(); - } - // 'collection' mode: - querySnapshot.docChanges().forEach(function (change) { - var changeType = change.type; - // Don't do anything for local modifications & removals - if (source === 'local') - return resolve(); - var id = change.doc.id; - var doc = getters.cleanUpRetrievedDoc(change.doc.data(), id); - dispatch('applyHooksAndUpdateState', { change: changeType, id: id, doc: doc }); }); - return resolve(); - }, function (error) { + }); }, function (error) { state._sync.patching = 'error'; return reject(error); }); diff --git a/package.json b/package.json index 836f8e5e..e4bf63f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vuex-easy-firestore", - "version": "1.30.0", + "version": "1.30.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", diff --git a/src/module/actions.ts b/src/module/actions.ts index 19ecbe30..d535cc1a 100644 --- a/src/module/actions.ts +++ b/src/module/actions.ts @@ -150,10 +150,25 @@ export default function (Firebase: any): AnyObject { // 1. Prepare for insert const initialDoc = (getters.storeRef) ? getters.storeRef : {} - const doc = getters.prepareInitialDocForInsert(initialDoc) + const initialDocPrepared = getters.prepareInitialDocForInsert(initialDoc) - // 2. insert - return getters.dbRef.set(doc) + // 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) + } + }) + }).then(function() { + if (state._conf.logging) { + console.log('[vuex-easy-firestore] Initial doc succesfully inserted.') + } + }).catch(function(error) { + console.error('[vuex-easy-firestore] Initial doc succesfully 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.', error) + }) }, handleSyncStackDebounce ({state, commit, dispatch, getters}) { if (!getters.signedIn) return false @@ -290,7 +305,7 @@ export default function (Firebase: any): AnyObject { // 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') - dispatch('insertInitialDoc') + await dispatch('insertInitialDoc') return _doc } const id = getters.docModeId @@ -415,7 +430,7 @@ export default function (Firebase: any): AnyObject { if (state._conf.logging) { console.log(`%c openDBChannel for Firestore PATH: ${getters.firestorePathComplete} [${state._conf.firestorePath}]`, 'color: lightcoral') } - const unsubscribe = dbRef.onSnapshot(querySnapshot => { + const unsubscribe = dbRef.onSnapshot(async querySnapshot => { const source = querySnapshot.metadata.hasPendingWrites ? 'local' : 'server' // 'doc' mode: if (!getters.collectionMode) { @@ -423,7 +438,7 @@ export default function (Firebase: any): AnyObject { // No initial doc found in docMode if (state._conf.sync.preventInitialDocInsertion) return reject('preventInitialDocInsertion') if (state._conf.logging) console.log('[vuex-easy-firestore] inserting initial doc') - dispatch('insertInitialDoc') + await dispatch('insertInitialDoc') return resolve() } if (source === 'local') return resolve() diff --git a/test/helpers/index.cjs.js b/test/helpers/index.cjs.js index f59d1269..04996d35 100644 --- a/test/helpers/index.cjs.js +++ b/test/helpers/index.cjs.js @@ -1309,9 +1309,24 @@ function pluginActions (Firebase) { return; // 1. Prepare for insert var initialDoc = (getters.storeRef) ? getters.storeRef : {}; - var doc = getters.prepareInitialDocForInsert(initialDoc); - // 2. insert - return getters.dbRef.set(doc); + 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); + } + }); + }).then(function () { + if (state._conf.logging) { + console.log('[vuex-easy-firestore] Initial doc succesfully inserted.'); + } + }).catch(function (error) { + console.error('[vuex-easy-firestore] Initial doc succesfully 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.', error); + }); }, handleSyncStackDebounce: function (_a) { var state = _a.state, commit = _a.commit, dispatch = _a.dispatch, getters = _a.getters; @@ -1458,19 +1473,24 @@ function pluginActions (Firebase) { return getters.dbRef.get().then(function (_doc) { return __awaiter(_this, void 0, void 0, function () { var id, doc; return __generator(this, function (_a) { - 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'); - dispatch('insertInitialDoc'); - return [2 /*return*/, _doc]; + switch (_a.label) { + case 0: + if (!!_doc.exists) return [3 /*break*/, 2]; + // 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')]; + case 1: + _a.sent(); + return [2 /*return*/, _doc]; + case 2: + id = getters.docModeId; + doc = getters.cleanUpRetrievedDoc(_doc.data(), id); + dispatch('applyHooksAndUpdateState', { change: 'modified', id: id, doc: doc }); + return [2 /*return*/, doc]; } - id = getters.docModeId; - doc = getters.cleanUpRetrievedDoc(_doc.data(), id); - dispatch('applyHooksAndUpdateState', { change: 'modified', id: id, doc: doc }); - return [2 /*return*/, doc]; }); }); }).catch(function (error) { console.error('[vuex-easy-firestore]', error); @@ -1556,6 +1576,7 @@ function pluginActions (Firebase) { }); }, openDBChannel: function (_a, pathVariables) { + var _this = this; var getters = _a.getters, state = _a.state, commit = _a.commit, dispatch = _a.dispatch; dispatch('setUserId'); // set state for pathVariables @@ -1594,38 +1615,45 @@ function pluginActions (Firebase) { if (state._conf.logging) { console.log("%c openDBChannel for Firestore PATH: " + getters.firestorePathComplete + " [" + state._conf.firestorePath + "]", 'color: lightcoral'); } - var unsubscribe = dbRef.onSnapshot(function (querySnapshot) { - var 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) - return reject('preventInitialDocInsertion'); - if (state._conf.logging) - console.log('[vuex-easy-firestore] inserting initial doc'); - dispatch('insertInitialDoc'); - return resolve(); + 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) + return [2 /*return*/, reject('preventInitialDocInsertion')]; + if (state._conf.logging) + console.log('[vuex-easy-firestore] inserting initial doc'); + return [4 /*yield*/, dispatch('insertInitialDoc')]; + case 1: + _a.sent(); + return [2 /*return*/, resolve()]; + case 2: + if (source === 'local') + return [2 /*return*/, resolve()]; + id = getters.docModeId; + doc = getters.cleanUpRetrievedDoc(querySnapshot.data(), id); + dispatch('applyHooksAndUpdateState', { change: 'modified', id: id, doc: doc }); + return [2 /*return*/, resolve()]; + case 3: + // 'collection' mode: + querySnapshot.docChanges().forEach(function (change) { + var changeType = change.type; + // Don't do anything for local modifications & removals + if (source === 'local') + return resolve(); + var id = change.doc.id; + var doc = getters.cleanUpRetrievedDoc(change.doc.data(), id); + dispatch('applyHooksAndUpdateState', { change: changeType, id: id, doc: doc }); + }); + return [2 /*return*/, resolve()]; } - if (source === 'local') - return resolve(); - var id = getters.docModeId; - var doc = getters.cleanUpRetrievedDoc(querySnapshot.data(), id); - dispatch('applyHooksAndUpdateState', { change: 'modified', id: id, doc: doc }); - return resolve(); - } - // 'collection' mode: - querySnapshot.docChanges().forEach(function (change) { - var changeType = change.type; - // Don't do anything for local modifications & removals - if (source === 'local') - return resolve(); - var id = change.doc.id; - var doc = getters.cleanUpRetrievedDoc(change.doc.data(), id); - dispatch('applyHooksAndUpdateState', { change: changeType, id: id, doc: doc }); }); - return resolve(); - }, function (error) { + }); }, function (error) { state._sync.patching = 'error'; return reject(error); }); diff --git a/test/initialDoc.js b/test/initialDoc.js index 0a11fade..1fba1403 100644 --- a/test/initialDoc.js +++ b/test/initialDoc.js @@ -15,8 +15,9 @@ test('initialDoc through openDBRef & fetchAndAdd', async t => { // doc doesn't exist yet t.is(docR.exists, false) try { - store.dispatch('initialDoc/openDBChannel', {randomId}) + await store.dispatch('initialDoc/openDBChannel', {randomId}) } catch (error) { + console.error(error) t.fail() } const testFullPath = store.getters['initialDoc/firestorePathComplete']