Firebase query object key in array with Javascript - firebase

I have a db structure as:
user = {
name: 'xxx',
address: 'XXX YY ZZZ',
updates: [
{
created: '09/03/2022',
content: { message: 'Message #1132'},
....
}
]
}
I want to query any user that have updates.content.message contains the word 'Message' with firebase web version.
Thanks a lot!

There's no native like query on Google Cloud Firestore. But you can manage to create a workaround using nodejs/javascript. See code below:
var collection = "user";
// search input. you can change this to any word like in the comment you said that you want to find document that contains "test".
search = "Message";
// gets the length of the search input.
searchLength = search.length;
// logically change the last character of the search input to imitate the `like` query.
searchFront = search.slice(0, searchLength-1);
searchEnd = search.slice(searchLength-1, search.length);
// will result to "Messagf".
end = searchFront + String.fromCharCode(searchEnd.charCodeAt(0) + 1);
const query = db
.collection(collection)
.where('updates.content.message', '>=', search).where('updates.content.message', '<', end)
.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.id, ' => ', doc.data());
});
});
The above code will search the updates.content.message that contains the word 'Message'. I leave some comments on the code for a better explanation. I'm not sure though if you're using Firestore v8 or v9 but the above code is written on v8.
Result:
drh13VT3T6n5bBHNARYD => {
address: 'XXX YY ZZZ',
name: 'xxx',
updates: { created: '09/03/2022', content: { message: 'Message #1132' } }
}

Related

Firebase query `TypeError` using `firestore.FieldPath.documentId()`

My Firestore data structure looks like this:
db.firestore.FieldPath.documentId(), '==', '20210106.0' does not work, but I am not sure why. I need to read it as a float, so I can use => or =< as Start Date and End Date in my query.
In the console I get this error message: TypeError: Cannot read property 'FieldPath' of undefined'
Here is my code:
actions: {
getFireBaseOrders(state){
db.collection(`ordersOptimized`).where(
db.firestore.FieldPath.documentId(),
'==',
'20210106.0').onSnapshot((res) => {
const changes = res.docChanges();
changes.forEach((change) => {
if (change.type === "added") {
let payload = change.doc.data();
state.commit("firebaseOrders", payload);
}
});
});
},
What am I missing? How do I make the condition work?
If you want to listen to changes occuring to the Firestore document with ID 20210106.0, just do as follows:
db.collection("ordersOptimized").doc("20210106.0").get()
.onSnapshot(function(doc) {
// ....
// Based on your database screenshot you should loop over the
// JavaScript object returned by doc.data()
// Something like
for (const [key, value] of Object.entries(doc.data())) {
console.log(`${key}: ${value}`);
}
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
});
Since 20210106.0 is the ID of a document in the ordersOptimizedcollection, only one document with this ID can exist in this collection. Therefore you should not use a Query (i.e. db.collection('...').where('...')) in order to listen to changes to this document.
On this other hand, if you want to listen to ALL the documents of the ordersOptimized collection, see the corresponding doc.

Create documents in different firestore collections, with same reference ID

My question is actually twofold, so I m not sure I should ask both in one post or create another post. Anyway, here it is:
I am creating users in firestore database. I do not want to put all details in a single document because it will be requested a lot, and all details will be retrieved, even if not needed. So I decided to create a collection members_full with all details of users I may not need often, and another collection called members_header to keep the few most important details. On creation of a new user, I want reference ID in both collections to be the same for a specific user.
- members_full -+
|
+ --- abnGMbre --- +
|
+ --- mother : 'His mom'
+ --- Father: 'daddy'
- members_header+
|
+ ---- abnGMbre -- +
|
+ ---- fullname: 'john Doe'
+ ---- pictURL: 'path to his profile pic'
I want something looking like the above.
So this is what I did in the cloud function:
/** Create / Update a member
* ------------------------- */
exports.updateMember = functions.https.onCall( (data, context) =>{
// root member and secretaries are allowed to update members
const authParams:any = {
uid: context.auth.uid,
email: context.auth.token.email,
};
// Check if user is allowed to perform operation
return checkPermission(authParams, ['root', 'secretary']).then(res => {
if(res==false){
return { // Permission denied
status: STATUS.permission_denied,
}
}
// set object to add/ update
const member:any = data;
// Check if uid of member object is present (true:update, false: create)
var fullRef : admin.firestore.DocumentReference;
var headRef : admin.firestore.DocumentReference;
var countRef: admin.firestore.DocumentReference;
var createNewMember = false;
if(member.uid!==undefined && member.uid!==null){ // update
fullRef = fsDB.collection('members_full').doc(member.uid);
headRef = fsDB.collection('members_header').doc(member.uid);
} else {
fullRef = fsDB.collection('members_full').doc();
headRef = fsDB.collection('members_header').doc(fullRef.id);
countRef = fsDB.collection('counters').doc('members');
createNewMember = true;
}
return fsDB.runTransaction(t => {
return t.get(fullRef).then(doc => {
// Update full details
t.set(fullRef, {
surname : member.surname ,
firstName : member.firstName ,
birthDate : member.birthDate ,
birthPlace : member.birthPlace ,
email : member.email ,
phone : member.phone ,
occupation : member.occupation ,
father : member.father ,
mother : member.mother ,
spouse : member.spouse ,
children : member.children ,
addressHome : member.addressHome ,
addressLocal: member.addressLocal,
contactHome : member.contactHome ,
contactLocal: member.contactLocal,
comment : member.comment ,
regDate : member.regDate ,
});
// Update header details
t.set(headRef, {
fullName : member.fullName ,
gender : member.gender ,
active : member.active ,
picURL : member.picURL ,
});
// Increment number of members
if(createNewMember ){
t.update(countRef, {count: admin.firestore.FieldValue.increment(1)});
}
}).then(() => {
return { status : STATUS.ok }
}).catch(err => {
return {
status: STATUS.fail,
message: err.message,
error: err
}
});
}).then(() => {
return { status : STATUS.ok }
}).catch(error =>{
return {
status: STATUS.fail,
message: error.message,
debug: 'run transaction err',
error: error
}
});
}).catch(err => {
return {
status: STATUS.fail,
message: err.message,
debug: 'check permission err',
error: err
}
});
});
/** Check if authenticated user's roles are among the ones allowed
* --------------------------------------------------------------- */
function checkPermission(authParams:any, allowedRoles:any[]):Promise<boolean>{
// Check if authenticated user as any of the roles in array 'allowedRoles'
return new Promise((resolve, reject) => {
// If one of allowed roles is root, check against global variables
if(allowedRoles.indexOf('root')>=0 &&
( root_auth.email.localeCompare(authParams.email)==0 ||
root_auth.uid.localeCompare(authParams.uid)==0)){
resolve(true);
}
// Get autID
const uid = authParams.uid;
// Get corresponding user in collection roles
admin.firestore().collection('userRoles').doc(uid).get().then(snap => {
// Get roles of user and compare against all roles in array 'allowedRoles'
const memRoles = snap.data().roles;
var found = false;
var zz = memRoles.length;
for(let z=0; z<zz; z++){
if(allowedRoles.indexOf(memRoles[z])){
found = true;
break;
}
}
resolve(found);
}).catch(err => {
reject(err);
});
});
}
When I call this cloud function, it only writes in document members_full, and increment number of members. It does not create entry in members_header.
My first question: where did I go wrong? the way I' m getting ID from the first document to create second document, isn't it valid?
The second question, will it be better to create subcollections rather than having 2 collections? if yes, how to do I do that in a transaction?
Help much appreciated
You need to chain the method calls in the Transaction. It is not extremely clear in the documentation, but if you look at the reference document for a Transaction (https://firebase.google.com/docs/reference/node/firebase.firestore.Transaction) you will see that the update() and set() methods return a Transaction, which is
the "Transaction instance. [and is] used for chaining method calls".
So you should adapt your code along these lines:
return fsDB.runTransaction(t => {
return t.get(fullRef)
.then(doc => {
t.set(fullRef, {
surname : member.surname ,
firstName : member.firstName
//....
})
.set(headRef, {
//....
gender : member.gender
//....
})
.update(countRef, {count: admin.firestore.FieldValue.increment(1)});
});
});
You also need to correctly chain all the different promises, as follows:
return checkPermission(authParams, ['root', 'secretary'])
.then(res => {
//...
return fsDB.runTransaction(t => {
//.....
});
.then(t => {
return { status : STATUS.ok }
})
.catch(error => {...})
However, you may use a batched write instead of a transaction, since it appears that you don't use the document returned by t.get(fullRef) in the transaction.
For your second question, IMHO there is no reason to use sub-collections instead of two (root) collections.

AngularFire2 db.collection Adds key 2 Times

I followed this guide to create a upload-component for Angular5 with AngularFire2.
At the end of the Video he showed a code snippet that allows adding a url path to any other database url.
this.snapshot = this.task.snapshotChanges().pipe(
tap(snap => {
console.log(snap);
if (snap.bytesTransferred === snap.totalBytes) {
// Update firestore on completion
this.db.collection('photos').add({ path, size: snap.totalBytes }).then();
}
})
);
This creates a url entry to photos, but it does it 2 times. Any idea how this can be? Each upload he creates 2 random keys with exactly same content inside.
Try something like this. Add to collection in downloadURL()
import { AngularFireStorage } from 'angularfire2/storage';
constructor(private storage: AngularFireStorage)
uploadFile(file){
const filePath = 'images/' + this.docRef + '/';
const task = this.storage.upload(filePath, file);
task.percentageChanges().subscribe(per => {
console.log(per);
});
task.downloadURL().subscribe(url => {
console.log(url);
this.db.collection('photos').add('required data')
}
}

Firebase denormalization data consistency issue

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.
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.
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);
})
})
}
It does show offline in the console, but the changes do not appear in Firebase database.
Is there anyone who could help me? :(
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

child_added for new items only with Firebase Cloud Functions

I have this code below which works (no syntax or any other errors) except that the output of this code displays all the results under /server/name:
i.e:
We have a new event:
{ des: 'test123', name: 'Test', nice: 'wew' } lol
Here is the Code in functions/index.js:
exports.sendFollowerNotification = functions.database.ref('/server/name').onWrite(event => {
admin.database().ref("/server/name").limitToLast(1).on('child_added', function(snapshot) {
console.log('We have a new event:', snapshot.val());
});
Here is the DB:
Update 2:
exports.sendFollowerNotification = functions.database.ref('/server/name/{num}').onWrite(event => {
console.log('We have a new event:', event.data.val(), 'lol');
});
Output in Logs:
Update 3:
And
Currently, the trigger is attached higher in the path than you want.
Instead,if you're planning on having multiple lists of events to listen to and this is just list 1 of many, use a wildcard:
exports.sendFollowerNotification = functions.database.ref('/server/name/{num}/{notification}').onWrite(event => {
...
})
You can choose wildcard names that better fit your specific code.

Resources