mesqueeb / vuex-easy-firestore

Easy coupling of firestore and a vuex module. 2-way sync with 0 boilerplate!
https://mesqueeb.github.io/vuex-easy-firestore
MIT License
234 stars 28 forks source link

Request: modifiedHook to be triggered on local change #158

Closed bpkennedy closed 5 years ago

bpkennedy commented 5 years ago

Heya! So, I'm at a point where one vuex module needs to know when an item has changed on the server and then ask another vuex module to do some work. I am trying to use the modifiedHook but it doesn't seem to fire or register a "modified" trigger for me. My current, hacky workaround:

  serverChange: {
    convertTimestamps: {
      created_at: '%convertTimestamp%',
      updated_at: '%convertTimestamp%'
    }
  },
  sync: {
    patchHook: async function (updateStore, doc, store) {
      await updateStore(doc)
      // I don't like this! Using because the modifiedHook for serverChanges doesn't seem
      // to be working...
      setTimeout(async () => {
        await store.dispatch('profilesData/refreshProfiles', null, { root: true })  
      }, 1000)
    }
  },

But I know this is either a misunderstanding on my part or maybe a bug? The module I want to listen to (users) is a 'collection' - I've tried adding a modifiedHook like so:

export default {
  firestorePath: 'users',
  firestoreRefType: 'collection',
  moduleName: 'profilesData',
  statePropName: 'profiles',
  namespaced: true,
  serverChange: {
    convertTimestamps: {
      created_at: '%convertTimestamp%',
      updated_at: '%convertTimestamp%'
    },
    modifiedHook: async function (updateStore, doc, id, store) {
      console.log('I am in here') // <--- this never gets called when an item in the users collection is patched.
      await updateStore(doc)
    }
  },
  state,
  getters,
  actions,
  mutations
}

And in the other module (at path users/{userId}) which is a 'doc' type, I try making the patch that I thought should trigger the server change in users collection:

async updateUser ({ dispatch }, userData) {
    try {
      await dispatch('patch', userData) // <--- this would have done a PATCH to a resource in the users collection
    } catch (error) {
      console.log(error)
    }
  },

I have verified in Firestore that the data is getting changed as I would expect - just the modifiedHook never prints that console.log.

mesqueeb commented 5 years ago

Modified hooks are ONLY triggered on a change on your server and NOT when that change was invoked from your front end. Eg. A change in Firestore console or another client with your same app.

Can you try and manually making a change in Firestore console in the browser and confirm if the hook fires or not?

Sent with GitHawk

bpkennedy commented 5 years ago

Ah, that would make sense. I will try that over my lunch in about 6 hours.

If that turns out to be the issue, I'll consider this ticket resolved, but would then have a separate question on how to wait until a module's local change has fully propagated to firestore before running a follow up action in a different module.

bpkennedy commented 5 years ago

Indeed, modifiedHook is called when the change is not from my client. Sweet! Can you suggest anything for the use case of the client needing to wait until the server is updated?

mesqueeb commented 5 years ago

No, currently there is no feedback on this in my library. I believe that there are almost no use cases where this is needed.

Please tell me more about your use case and why you need this. Maybe I can teach you a work around for your case or add the functionality to my library if your use case in common enough.

Sent with GitHawk

bpkennedy commented 5 years ago

I seem to be having trouble remembering the use case I had! I'd say that means it's not that important. šŸ˜„ . If I come up to it in the future I can re-open this or open a new and describe for you/I/others to discuss and figure out if it's worth any effort to change. Cool?

mesqueeb commented 5 years ago

Great!

I'll close this issue, but anyone with similar requirements, let me know down below!

RichardCr commented 5 years ago

Hi @mesqueeb, I think I have a use case for this. Although I am pretty new, so I might be missing something. In Firebase I have a 'boards' collection, and each board doc has a number of subcollections. The owner/editor permissions are on each board doc, and all the subcollections rely on this board existing in firebase so they can do permissions checks against it (I can't use permissions wildcards). The permissions setup is pretty similar to this, (where comments permissions references the story parent doc permissions). When I create a new board this.$store.dispatch('boards/set', newBoard) I want to ensure that the board exists on firebase before running openDBChannel on all the subcollections (which triggers the permissions checks on the subCollections in firebase, which causes errors if the parent board doesn't yet exist to check for ownership). I was hoping to use hooks or something to wait for the board to be created in firebase, before running the openDBChannel's on the subCollections. Does this make sense? My two thoughts of how to approach this at the moment is to either poll for the board doc's existence (messy), or try and work out how to insert some sort of callback into your code based on where your code loads the log 'Initial doc successfully inserted' Happy to buy you many coffees if you have any idea how to approach around this! :smile:

mesqueeb commented 5 years ago

Hi @RichardCr ! Thanks for your detailed use case explained to me. It's already pretty late where I am, and I have an early day tomorrow: driving school šŸ˜„ But I will ponder how to approach this and then I'll write up an answer hopefully tomorrow evening.

I already have some ideas it my head but as I said, it's kinda late and I wanna go over you post thoroughly again tomorrow šŸ˜‰

Sent with GitHawk

RichardCr commented 5 years ago

No Problem! Any help at any time is amazing!

mesqueeb commented 5 years ago

@RichardCr I had another look at the docs, and there are a couple of options. You'll have to determine which fits best in your app. šŸ˜„

  1. Make a transaction that will look for the document.

If the doc already exists make it throw an error. Catch this error in the client and you know for sure you can open the DBChannels. If the document doesn't exist you can create it in the transaction and when this transaction succeeds you'll also know for sure you can open the DBChannels.

(one drawback: "Transactions will fail when the client is offline.") ā†’ However, you could argue, when the user is offline, and there's no cache of existing docs, how can your security rules work in that case?

  1. First make your dispatch('boards/set', newBoard) call, but include a Firestore Server Timestamp field. Server timestamps are only resolved on the server, so once this happens your server will trigger a "modification" which is sent back to your client.

Therefore if you have your DBChannel open on boards it will trigger the server hooks. (Either the serverChange.addedHook or serverChange.modifiedHook)

--

See what fits best in your application šŸ˜‰ I guess the conclusion is: try not to limit yourself by using ONLY methods from my library. See my library as "complementary" to the Firestore SDK, so always think if there's a good solution with their native functions.

--
Vuex Easy Firestore was made with ā™„ by Luca Ban.
If this library helped you in any way you can support me by buying me a cup of coffee. ā˜•ļø
You can also reach out on twitter if you want a one-on-one coding review/lesson. šŸ¦œ

RichardCr commented 4 years ago

Thanks @mesqueeb ! I ended up just using db.collection().doc().set().then() and then redundantly running dispatch('boards/set', newBoard) inside then() to populate the local store, and all the other goodness your library does. This approach also let me set a loading spinner which was nice :) I then opened all the DBChannels in mounted() on the board component (opened by goToBoard()) once I arrived on the new Board. Code below if anyone is interested.

async newBoard (event, template = false) {
  // get a new board object and a unique ID ready
  let newBoard = this.instantiateBoard(template)
  const id = newBoard.id

  // get a firebase reference for the new board
  var db = Firebase.firestore()
  let idDocRef = db.collection('boards').doc(id)

  // start loading screen and set the new board
  this.loading= true
  await idDocRef.set(newBoard)
  .then((success) => { 
    // once new board exists in firebase, use Vuex easy firestore to set it locally, and goto the board 
    this.$store.dispatch('boards/set', newBoard)
    this.loading = false
    this.goToBoard('', id)})
  .catch((error) => {console.log(error)})
},
RichardCr commented 4 years ago

@mesqueeb Also is there a way to sponsor this repo? I keep trying to buy you coffees but for some reason my bank keeps blocking the transaction as suspicious and the wait time to talk to a human to fix this is the better part of an hour :/

mesqueeb commented 4 years ago

@RichardCr thank you so much for offering. Please wait a few hours while I try to set up GitHub sponsor on my account. šŸ˜ƒ

Sent with GitHawk

louisameline commented 4 years ago

@RichardCr you seem to be calling Firestore's API twice, doesn't it bother you to be charged twice?

mesqueeb commented 4 years ago

@RichardCr @louisameline is right about this:

using db.collection().doc().set().then() and then redundantly running dispatch('boards/set', newBoard) inside then() to populate the local store

to prevent calling the firebase api twice you can do this instead:

const newDoc = {}
const id = newBoard.id

db.collection('your/collection/path').doc(id).set(newDoc).then(() => {
  this.$store.commit('yourModule/INSERT_DOC', {...newDoc, id})
})

See the source code for INSERT_DOC.

!! Please be careful with the promise from doc().set(), because this will NOT be triggered when the user is offline. so you need to check for network connectivity before you call this.

BTW, I subscribed to Github Sponsors, and my profile is now "under review":

Nice! Thanks for submitting your profile. Our team will review it and get back to you within 7-10 business days.

I'll let you know when you can support me ;)

mesqueeb commented 4 years ago

@RichardCr i'm so excited that I got accepted for Github Sponsors ! šŸŽ‰ You can see my profile here: https://github.com/sponsors/mesqueeb

I highly appreciate your support and it enables me to work harder on vuex-easy-firestore. I got big plans for the future improving many aspects based on everything I learned these past two years!!! šŸ”„šŸ”„šŸ”„

RichardCr commented 4 years ago

@louisameline @mesqueeb. Thank you both. I was being somewhat lazy to get it all working. Now that it is, i'll be making these changes on the long journey to production. @mesqueeb congrats on being accepted. It is a worthy repo. I started sponsorship :) If I manage to release and monetise my current project I'll move up a sponsorship tier :D

mesqueeb commented 4 years ago

@RichardCr Thank you so much for your support!! šŸ’œ You're my first supporter šŸŽ‰šŸŽ‰šŸŽ‰šŸŽ‰

Feel free to open more issues whenever you're stuck.