4

I'm currently using Ionic CLI 3.19 with Cordova CLI 7.1.0 (@ionic-app-script 3.1.4)

The problem that I’m currently facing with is, I should update friends node values simultaneously every time the related data get changed from elsewhere. I’d like to clarify my objective with some screenshots to make it more clear.

As you can see from the image below, each child node consists of a user array that has a user id as a key of friends node. The reason why I store as an array is because each user could have many friends. In this example, Jeff Kim has one friend which is John Doe vice versa.

friends node image

When data in users node gets changed for some reason, I want the related data in friends node also want them to be updated too.

For example, when Jeff Kim changed his profile photo or statusMessage all the same uid that reside in friends node which matches with Jeff Kim’s uid need to be updated based on what user has changed.

users node image

user-service.ts

    constructor(private afAuth: AngularFireAuth, private afDB: AngularFireDatabase,){
      this.afAuth.authState.do(user => {
      this.authState = user;
        if (user) {
          this.updateOnConnect();
          this.updateOnDisconnect();
        }
      }).subscribe();
     }

    sendFriendRequest(recipient: string, sender: User) {
      let senderInfo = {
      uid: sender.uid,
      displayName: sender.displayName,
      photoURL: sender.photoURL,
      statusMessage: sender.statusMessage,
      currentActiveStatus: sender.currentActiveStatus,
      username: sender.username,
      email: sender.email,
      timestamp: Date.now(),
      message: 'wants to be friend with you.'
    }
    return new Promise((resolve, reject) => {
      this.afDB.list(`friend-requests/${recipient}`).push(senderInfo).then(() => {
      resolve({'status': true, 'message': 'Friend request has sent.'});
     }, error => reject({'status': false, 'message': error}));
  });
}

    fetchFriendRequest() {
    return this.afDB.list(`friend-requests/${this.currentUserId}`).valueChanges();
  }

    acceptFriendRequest(sender: User, user: User) {
      let acceptedUserInfo = {
      uid: sender.uid,
      displayName: sender.displayName,
      photoURL: sender.photoURL,
      statusMessage: sender.statusMessage,
      currentActiveStatus: sender.currentActiveStatus,
      username: sender.username,
      email: sender.email
     }
     this.afDB.list(`friends/${sender.uid}`).push(user); 
     this.afDB.list(`friends/${this.currentUserId}`).push(acceptedUserI
     this.removeCompletedFriendRequest(sender.uid);
}

According to this clip that I've just watched, it looks like I did something called Denormalization and the solution might be using Multi-path updates to change data with consistency. Data consistency with Multi-path updates. However, it's kinda tricky to fully understand and start writing some code.

I've done some sort of practice to make sure update data in multiple locations without calling .update method twice.

// I have changed updateUsername method from the code A to code B
// Code A
updateUsername(username: string) {
  let data = {};
  data[username] = this.currentUserId;
  this.afDB.object(`users/${this.currentUserId}`).update({'username': username});
  this.afDB.object(`usernames`).update(data);
}
// Code B
updateUsername(username: string) {
  const ref = firebase.database().ref(); 
  let updateUsername = {};
  updateUsername[`usernames/${username}`] = this.currentUserId; 
  updateUsername[`users/${this.currentUserId}/username`] = username;
  ref.update(updateUsername);
}

I'm not trying to say this is a perfect code. But I've tried to figure this out on my own and here's what I've done so far.

Assume that I'm currently signed in as Jeff.

When I run this code all the associated data with Jeff in friends node gets changed, as well as Jeff's data in users node gets updated simultaneously.

The code needs to be improved by other firebase experts and also should be tested on a real test code.

According to the following thread, once('value' (which is, in general, a bad idea for optimal performance with Firebase). I should find out why this is bad.

friend.ts

    getFriendList() {
      const subscription = this.userService.getMyFriendList().subscribe((users: any) => {
        users.map(u => {
          this.userService.testMultiPathStatusMessageUpdate({uid: u.uid, statusMessage: 'Learning Firebase:)'});
      });
      this.friends = users;
      console.log("FRIEND LIST@", users);
    });
    this.subscription.add(subscription);
  }

user-service.ts

    testMultiPathStatusMessageUpdate({uid, statusMessage}) {
      if (uid === null || uid === undefined) 
      return;

      const rootRef = firebase.database().ref();
      const query = rootRef.child(`friends/${uid}`).orderByChild('uid').equalTo(this.currentUserId);

    return query.once('value').then(snapshot => {
      let key = Object.keys(snapshot.val());
      let updates = {};
      console.log("key:", key);
      key.forEach(key => {
        console.log("checking..", key);
        updates[`friends/${uid}/${key}/statusMessage`] = statusMessage;
      });
      updates[`users/${this.currentUserId}/statusMessage`] = statusMessage;
      return rootRef.update(updates);
    });
  }

The code below works fine when updating status to online but not offline.

I don't think it's the correct approach.

    updateOnConnect() {
      return this.afDB.object('.info/connected').valueChanges()
             .do(connected => {
             let status = connected ? 'online' : 'offline'
             this.updateCurrentUserActiveStatusTo(status)
             this.testMultiPathStatusUpdate(status)
             })
             .subscribe()
    }


    updateOnDisconnect() {
      firebase.database().ref().child(`users/${this.currentUserId}`)
              .onDisconnect()
              .update({currentActiveStatus: 'offline'});
      this.testMultiPathStatusUpdate('offline');
    }


    private statusUpdate(uid, status) {
      if (uid === null || uid === undefined) 
      return;

      let rootRef = firebase.database().ref();
      let query = rootRef.child(`friends/${uid}`).orderByChild('uid').equalTo(this.currentUserId);

      return query.once('value').then(snapshot => {
        let key = Object.keys(snapshot.val());
        let updates = {};
        key.forEach(key => {
          console.log("checking..", key);
          console.log("STATUS:", status);
          updates[`friends/${uid}/${key}/currentActiveStatus`] = status;
      });
      return rootRef.update(updates);
    });
  }

    testMultiPathStatusUpdate(status: string) {
      this.afDB.list(`friends/${this.currentUserId}`).valueChanges()
      .subscribe((users: any) => {
        users.map(u => {
          console.log("service U", u.uid);
          this.statusUpdate(u.uid, status);
        })
      })
    }

enter image description here

It does show offline in the console, but the changes do not appear in Firebase database.

Is there anyone who could help me? :(

JeffMinsungKim
  • 1,940
  • 7
  • 27
  • 50
  • this is a very well formatted and written question for a "new" user :) I hope someone can help you! Welcome on Stack Overflow. – cramopy Dec 09 '17 at 20:06
  • @cramopy Thank you for your comment. I really hope so. Currently, I'm reading this [thread](https://stackoverflow.com/questions/30693785/how-to-write-denormalized-data-in-firebase/30699277#30699277) But my question is a bit tricky. Not sure the thread could help me or not. – JeffMinsungKim Dec 09 '17 at 20:11

1 Answers1

2

I think you are right doing this denormalization, and your multi-path updates is in the right direction. But assuming several users can have several friends, I miss a loop in friends' table.

You should have tables users, friends and a userFriend. The last table is like a shortcut to find user inside friends, whitout it you need to iterate every friend to find which the user that needs to be updated.

I did a different approach in my first_app_example [angular 4 + firebase]. I removed the process from client and added it into server via onUpdate() in Cloud functions.

In the code bellow when user changes his name cloud function executes and update name in every review that the user already wrote. In my case client-side does not know about denormalization.

//Executed when user.name changes
exports.changeUserNameEvent = functions.database.ref('/users/{userID}/name').onUpdate(event =>{
    let eventSnapshot = event.data;
    let userID = event.params.userID;
    let newValue = eventSnapshot.val();

    let previousValue = eventSnapshot.previous.exists() ? eventSnapshot.previous.val() : '';

    console.log(`[changeUserNameEvent] ${userID} |from: ${previousValue} to: ${newValue}`);

    let userReviews = eventSnapshot.ref.root.child(`/users/${userID}/reviews/`);
    let updateTask = userReviews.once('value', snap => {
    let reviewIDs = Object.keys(snap.val());

    let updates = {};
    reviewIDs.forEach(key => { // <---- note that I loop in review. You should loop in your userFriend table
        updates[`/reviews/${key}/ownerName`] = newValue;
    });

    return eventSnapshot.ref.root.update(updates);
    });

    return updateTask;
});

EDIT

Q: I structured friends node correctly or not

I prefer to replicate (denormalize) only the information that I need more often. Following this idea, you should just replicate 'userName' and 'photoURL' for example. You can aways access all friends' information in two steps:

 let friends: string[];
 for each friend in usrService.getFriend(userID)
    friends.push(usrService.getUser(friend))

Q: you mean I should create a Lookup table?

The clip mentioned in your question, David East gave us an example how to denormalize. Originaly he has users and events. And in denormalization he creates eventAttendees that is like a vlookup (like you sad).

Q: Could you please give me an example?

Sure. I removed some user's information and add an extra field friendshipTypes

users
    xxsxaxacdadID1
        currentActiveStatus: online
        email: zinzzkak@gmail.com
        gender: Male
        displayName: Jeff Kim
        photoURL: https://firebase....
        ...
    trteretteteeID2
        currentActiveStatus: online
        email: hahehahaheha@gmail.com
        gender: Male
        displayName: Joeh Doe
        photoURL: https://firebase....
        ...

friends
    xxsxaxacdadID1
        trteretteteeID2
            friendshipTypes: bestFriend //<--- extra information
            displayName: Jeff Kim
            photoURL: https://firebase....
    trteretteteeID2
        xxsxaxacdadID1
            friendshipTypes: justAfriend //<--- extra information
            displayName: John Doe
            photoURL: https://firebase....


userfriends
    xxsxaxacdadID1
        trteretteteeID2: true
        hgjkhgkhgjhgID3: true
    trteretteteeID2
        trteretteteeID2: true
Makah
  • 4,435
  • 3
  • 47
  • 68
  • Thank you for giving me an advice. I'm not quite sure whether I structured friends node correctly or not. And if I understand your words correctly, you mean I should create a Lookup table? If so, where should I place the Lookup table? Could you please give me an example? – JeffMinsungKim Dec 11 '17 at 05:54
  • Thank you for the in-depth answer. Because of an issue that user's active status never turn into an offline state, I thought to myself perhaps I need to fix the data structure. – JeffMinsungKim Dec 11 '17 at 13:22
  • Rather than duplicating the user data into friends node, just keep the uid only and use those ids to fetch the user data from users node. However, the problem is I need to get friends data as an array, not an object. Do you think this is a better way to solve the problem? – JeffMinsungKim Dec 11 '17 at 13:34
  • If you don't need to add fields in friends (like friendshipTypes), I think you can start whith two tables. `users`that has userinformation and `userFriends` that has the pointers (like in my example).... so in your code you first fetch all friends that a current user has, then you fetch every friend (or just the online friends). – Makah Dec 11 '17 at 15:37
  • I've just edited your answer. Please fix me if I'm wrong. :( The problem is, it doesn't get all the user as one package array list... – JeffMinsungKim Dec 11 '17 at 17:18