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

Multiple Collections Help #32

Closed adamelevate closed 5 years ago

adamelevate commented 6 years ago

I've spent a few days trying to get this working, but I'm not understanding where I'm going wrong.

I added this to main.js

let app;
firebase.auth().onAuthStateChanged(function(user) {
  if(!app){
    if (user) {
      console.log('user', user);
      // user is logged in
      store.dispatch('userData/openDBChannel')
        .then( () => {

        })
        .catch(console.error)

      }
    /* eslint-disable no-new */
    app = new Vue ({
      el:'#app',
      router,
      store,
      render: h => h(App)
    })

  }
});

The User loads just fine, at first I was loading another collection inside the ".then" after dispatch and it was calling the collection immediately when the site loaded. I needed to delay this for when the component loads, so I moved it inside the component at the "created()" function, but it never got my second collection. What am I doing wrong?

Neat library by the way, it will save a lot of redundant code :)

mesqueeb commented 6 years ago

@adamelevate I will help you set it up correctly.

I am not sure what is the error, from the code you posted everything looks ok. I would need

You can comment here or also contact me @mesqueeb on twitter DM or Telegram for easier communication.

mesqueeb commented 6 years ago

Possible solution: I think your conditions are set up weird. Might cause problems, because the listener launches immediately without user, then again when the first user is confirmed, but then your app would be already there so the openDBChannel would not be launched.

please try:

// App can be created even with empty store
/* eslint-disable no-new */
app = new Vue ({
  el:'#app',
  router,
  store,
  render: h => h(App)
})
firebase.auth().onAuthStateChanged(function(user) {
  // let a signed in user just update the store
  if (user) {
    console.log('user', user);
    // user is logged in
    store.dispatch('userData/openDBChannel')
      .then( () => {

      })
      .catch(console.error)
  } else {
    // else reset the store state (i'll need to write some helpers for that)
  }
});

Why do you want to wait with rendering the app?

adamelevate commented 6 years ago

Ah thanks man, I'm sure I'm doing it hella wrong, new to firebase and all.

I don't want to wait but I followed a quick tutorial to get auth working, I didn't see your example in another issue until this am, but I don't exactly follow it (totaln00b). The userData collection does work tho, I'm open to change however :)

main.js
import '@babel/polyfill'
import Vue from 'vue'
import './plugins/vuetify'
import App from './App.vue'
import router from './router'
import store from './store'
import * as firebase from 'firebase'
import "@firebase/firestore"

Vue.config.productionTip = false

// save a reference to the firestore database
// to access it in the future
let app;
firebase.auth().onAuthStateChanged(function(user) {
  if(!app){
    if (user) {
      // user is logged in
      store.dispatch('userData/openDBChannel')
        .then(()=>{
            ******* was putting "locksData/openDBChannel" to grab locks collection but it
            was loading on created, not when "locks" component is created
            ******
        })
        .catch(console.error)
      }
    /* eslint-disable no-new */
    app = new Vue ({
      el:'#app',
      router,
      store,
      render: h => h(App)
    })

  }
});
store.js
import Vue from "vue";
import Vuex from "vuex";
import * as firebase from 'firebase'
import {db} from './firebase';
import createEasyFirestore from 'vuex-easy-firestore'

Vue.use(Vuex);

const state = {
  userId: '',
  customerId: '',
};

const actions = {
  login({ commit, state }, { email, password }) {
    return new Promise((resolve, reject) => {
      firebase.auth().signInWithEmailAndPassword(email, password)
        .then((response) => {
          console.log('response from state', response);
          commit('SET_USER_ID', response.user);
          resolve(response.user);
        })
        .catch((err) => {
          console.log('error', err);
          reject(err);
        });
    });
  },
  logout({ commit, state }) {
    return new Promise((resolve, reject) => {
      firebase.auth().signOut()
        .then(() => {
          commit('LOGOUT');
          resolve();
        })
        .catch((err) => {
          reject(err);
        });
    });
  },
  checkUserStatus({ commit, state }) {
    return new Promise((resolve, reject) => {
      firebase.auth().onAuthStateChanged((user) => {
        if (user) {
          commit('SET_USER_ID', user);
          resolve(user.uid);
        } else {
          reject('User is not logged in.');
        }
      });
    });
  },
  saveUser({ commit, state }, {user}) {
    state.user = user;
  },
};

const mutations = {
  LOGOUT(state, user) {
    state.userData = null;
    state.locksData = null;
    state.lockData = null;
    state.userId = null;
    store.dispatch('resetStore', null)
  },
  SET_USER_ID(state, user) {
    state.user = state.userData.data;
    state.userId = user.uid;
  },
};

const userData = {
  firestorePath: 'users/{userId}/',
  firestoreRefType: 'doc', // collection or 'doc'
  moduleName: 'userData',
  statePropName: 'data',
}

const locksData = {
  firestorePath: 'locks/',
  firestoreRefType: 'collection', // collection or 'doc'
  moduleName: 'locksData',
  statePropName: 'data',
  sync:{
    where:[['cId', '==', '{customerId}']]
  }
}
const lockData = {
  firestorePath: 'locks/{lockId}/data',
  firestoreRefType: 'collection', // collection or 'doc'
  moduleName: 'lockData',
  statePropName: 'data',
}

// do the magic 🧙🏻‍♂️
const easyFirestore = createEasyFirestore([userData, locksData, lockData])

const store = new Vuex.Store({
  plugins: [easyFirestore],
  state,
  mutations,
  actions
})

export default store
Locks.vue
<template>
  <v-layout id="locks" row wrap xs12 md10>
    <v-flex  v-for="(lock, index) in locks" :key="index"  xs12 sm6 md3>
      <v-card raised :to="{name:'Lock', params: {lockId: lock.id}}">
      <v-img :src="lock.pUrl" max-height="200"></v-img>
      <v-card-title>{{lock.addr}}</v-card-title>
      <p>customer ID: {{lock.cId}}</p>

      </v-card>
    </v-flex>

  </v-layout>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
  name:'locks',
  computed:{
    ...mapGetters({
      user:'userData/storeRef'
    })
  },
  watch:{
    user: function(val){
      console.log('val', val);
      val != undefined ? this.getLocks : null;
    }
  },
  methods:{
    getLocks(){
    let vThis = this;
    console.log('user', vThis.user.cId);
    this.$store.dispatch('locksData/openDBChannel',{customerId: vThis.user.cId})
    .then()
    .catch(console.error)
    }
  },

}
</script>
mesqueeb commented 6 years ago

@adamelevate I'm not sure what the problem is, your code looks ok. Can you make sure you are using the latest version of vuex-easy-firestore: npm i vuex-easy-firestore@latest

Edit: try firestorePath: 'locks' instead of 'locks/'

adamelevate commented 6 years ago

I think it's working now, mostly just bad code on my part... this seems to work just fine using a "customerID" or cId

import { mapGetters } from 'vuex'
export default {
  name:'locks',
  computed:{
    ...mapGetters({
      locks: 'locksData/storeRef',
      user:'userData/storeRef'
      // ...
    })
  },
  watch:{
    user: function(val){
      console.log('user changed val', val);
      if(val != undefined){
        this.getLocks(val.cId)
      }
    }
  },
  methods:{
    getLocks(cId){
      let vThis = this;
      this.$store.dispatch('locksData/openDBChannel',{customerId: cId})
      .then()
      .catch(console.error)
    }
  },

}
adamelevate commented 6 years ago

I'm still unable to get a single document... what am I doing wrong, lol?

I turned off all security rules in firestore, I have 100 locks in a collection and I can verify that the id exists and works with the firestore simulator

image

store.js

const lockData = {
  firestorePath: 'locks/{lockId}',
  firestoreRefType: 'doc', // collection or 'doc'
  moduleName: 'lockData',
  statePropName: 'data',
}

lock.vue

import { mapGetters } from 'vuex'
import { db } from '@/firebase'
export default {
  name:'lock',
  computed:{
    ...mapGetters({
      lock: 'lockData/storeRef',
      user:'userData/storeRef'
      // ...
    })
  },
  watch:{
    user: function(val){
      //make sure "signed in" is true
      if(val != undefined){
        this.getLock(val.cId)
      }
    }
  },
  methods:{
    getLock(cId){
      this.$store.dispatch('lockData/openDBChannel', {lockId: 'lockid#string'})
      .then()
      .catch(console.error)
    }
  },

}

returning "id: null"

Update

Syntax error, "firestorePath: 'locks/{lockId}'" should have a trailing /...

mesqueeb commented 6 years ago

@adamelevate it’s not clear where the issue is from your explanation and snippets. There’s nothing more i can say to help besides checking all params see if everything is passed correctly.

I'm retrieving single documents just fine in my apps.

Also save the Vuex store on the window and just debug in console.log manually doing store.dispatch and checking store.state etc.

Vuex easy Firestore is only a firebase Vuex plugin, so to avoid confusion don't use vue files to check the results. Separate the problems to find out where the bug is. Maybe it's just related to your vue file html etc.

Ps i'd be more than happy to look at your repo, you can delete any sensitive information and just send me the minimum stuff.

mesqueeb commented 6 years ago

Also try swapping out the ID for a hard coded ID in the Firestore path. Etc.

mesqueeb commented 6 years ago

@adamelevate Did everything work out alright?

Syntax error, "firestorePath: 'locks/{lockId}'" should have a trailing /...

This is weird, I never had this error and do not have trailing / in my paths.

How can I help you?

adamelevate commented 6 years ago

After lots of headbanging, because n00b, everything works when getting docs and collections. I have a lot to learn about permissions and quite spartan documentation available. I'm still having issues pushing multiple docs, the console shows a batch of 20 but in my db it only adds 1... then after 3 tries it stops running altogether. I know I'm waaaay off topic here, should I open a new issue or close this one? Thanks!!!

mesqueeb commented 6 years ago

@adamelevate We can continue here. I’m glad you have it sorted out. What was the original problem eventually? Your firestore rules?

Let’s try to find the last bugs as well then.

So first of all i’m not sure what you mean with “pushing multiple docs”. As always please provide me with code related to what you want to do, then I can see if I can find any mistakes in your code or if not try replicate the bug on my side.

adamelevate commented 6 years ago

Ah ok, well I'm trying to load this array of objects that I'm generating, and it shows that it works in the console, but they don't show in firestore.

page.vue

buildArrays(){
      let arr = []
      for (var i = 0; i < 10; i++) { 
        // seed the data
        let tempArr = this.genForm 

        // add a generated code
        tempArr['code'] =  this.makeId(); 
        arr.push(tempArr);
      }
      this.$store.dispatch('secretsData/insertBatch', arr)
      .then().catch(console.error)
    }

store.js

const secretsData = {
  firestorePath: 'secrets/',
  firestoreRefType: 'collection', // collection or 'doc'
  moduleName: 'secretsData',
  statePropName: 'data',
}

Image below... At the top is the form and submit button, where I input my variables... at the top I have {mapgetter} for 'secrets/storeRef' that is listing all the keys made, and at the bottom is your console.log for showing the inserts... they aren't making it to the db tho. Permissions in db have been set to 'secret/{id}{allow read,write: if request.auth.uid != null'

image

mesqueeb commented 5 years ago

@adamelevate I have tested a similar setup and insertBatch works as expected for me.

I did notice in your code: firestorePath: 'secrets/' but your permissions: 'secret/{id}{allow read,write: if request.auth.uid != null'

says "secret" with no s.

Could you try these security rules for me and see if it works in this case? Then we know for sure it's related to the security rules or not.

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write;
    }
  }
}
adamelevate commented 5 years ago

It was a mistype on my part when entering it into the comments, the names are correct in production. I am dumb, the way I wrote the for loop was outputting the same generated ID, meaning firebase was not allowing duplicates but also not telling me they were duplicates. I think I can close this now, it all works. Thanks for your hard work, it's very helpful when I use it correctly :)

mesqueeb commented 5 years ago

@adamelevate Ah yes, I see now you have the same ID's for each item in your batchInsert! : D I also overlooked that! hehehe.

I'm glad it worked out. A final piece of advice: On inserts you do not need to add an ID manually. You can do this, but it's not required.

Meaning you could also pass:

dispatch('secretsData/insertBatch', [
    {code: 111},
    {code: 222},
    {code: 333},
    {code: 444},
  ])

and all these would automatically receive an ID and work just fine. However, if you need to use the ID's later, it might be easier to assign them manually like you do.

Finally, when assigning ID's manually the recommended way to do so is: store.getters['yourModule/dbRef'].doc().id

There is a getter called dbRef created on each vuex-easy-firestore module with the reference of your firestorePath. So with adding .doc().id to that you will "create" a new ID you can pass.

You could wrap it like so:

function newId () {
  return store.getters['yourModule/dbRef'].doc().id
}

and just do newId() to get a new ID each time! (I'll have to clarify this in the docs)

mesqueeb commented 5 years ago

I just wanted to comment on the topic of how to work with multiple sub-collections in Firestore. In my opinion it is usually better to work with a flatter database and filter on certain fields (using where()).

Eg. when you have a collection of user [lists] with each a sub-collection with [items].

That's why in such a case it's usually better to have some basic "lists" info on a single document with maybe just the names of the lists. And have a single "items" collection which is not nested, with all items. Then query for items .where('list', '==', 'something').

Thinking about the best database structure is very important. And I want to emphasize that if you need to use a lot of pathVariables and also open and closeDBChannel a lot with vuex-easy-firestore, there is a big chance you do not have the best database structure.

I highly recommend everyone to watch these specific two videos on Firestore database structure by the Firebase team. They helped me a lot:

Keep the discussion going and good luck!