Cloud Functions for Firebase - get database value 'synchronously' - firebase

My realtime db has this structure
- userId1
- meta
- name
- data
- dataId1
- description
- dataId2...
- userId2....
I'm trying to monitor additions to dataIdx but want to get the name field too. I tried the following, but was misusing the parent method. So I thought about the line commented out, but .once is asynchronous, which starts to make my code more complex, while in the examples there are calls to set that are more or less synchronous
exports.sendNotification = functions.database.ref('/{userId}/presents/{dataId}')
.onWrite(event => {
var eventSnapshot = event.data;
var person = eventSnapshot.parent().parent().child("meta").child("name").val();
// var person = admin.database().ref('/' + event.params.userId + '/meta/name').once().val();
let p = eventSnapshot.child("description").val();
console.log(`${person} added ${p}`);
What would be the correct way to do this

This is what I came up with using chained promises to build my data
exports.sendNotification = functions.database.ref('/{userId}/presents/{presentId}')
.onWrite(event => {
let p = eventSnapshot.child("description").val();
return event.data.ref.parent.parent.child('meta/name').once("value")
.then(snapshot => {
let person = snapshot.val();
var payload = {
data: {
person: person,
...
}
};

Related

How to efficiently query from Firebase Realtime Database using query or specific path

We have infrequently property modification("price") on Firebase Realtime Database which is structure like this:
../currencies/<currency>/value/
"price":343
every player that log in will listen to only one and specific currency. our client will pick the right currency path based on the player preferences.
so if player is set to currency USD the firebase client will listen to this path
../currencies/USD/value/
"price":343
Those currencies prices are changed infrequently
due to this structure our server side need to modify and denormalized the data to all currencies when it changes (we can have ten's of currencies)
because of that we add to the leaf even more properties which are identical at all currencies
which I find redudent like:
../currencies/USD/value/
"price":343
"currency-source":"fx" . //this property will be copied to all
currencies vals because the client listen to only one path and it needs this data aswell
instead if manitain this on path's perhaps we can use some query where each client will be able to pick it's currency based on property name?
something like that:
../currencies/value/
"USD_price":343
"EUR_price":343
...
thoughts about design? and if sounds better how can be achieved with Firebase Realtime Database query?
I don't know which language (which Client SDK) you use in your app, but here is a solution with the JavaScript SDK.
Let's imagine you have a Relatime Database structure as follows
"parentnode" : {
"currencies" : {
"EUR" : {
"value" : {
"price" : 201
}
},
"USD" : {
"value" : {
"price" : 343
}
},
"value" : {
"EUR_price" : 201,
"USD_price" : 343,
"currency-source" : "fx"
}
}
}
Under a parentnode you have a currencies node which corresponds to the examples in your question.
In case you want to listen to /currencies/<currency>/value, you would do as follows:
var db = firebase.database();
var currency = 'EUR';
var ref = db.ref().child('parentnode/currencies/' + currency);
ref.on('value', function(snapshot) {
var data = snapshot.val();
console.log(data);
});
In case you want to listen to /currencies/value/<currency>_price and get the price and the currency-source values, you would do as follows:
var db = firebase.database();
var currency = 'EUR';
var ref = db.ref().child('parentnode/currencies/value');
ref.on('value', function(snapshot) {
var data = snapshot.val();
var price = data[currency + '_price'];
var source = data['currency-source'];
console.log(price);
console.log(source);
});
As mentioned in your comment, the second approach implies that "we will download all the data leaf under /currencies/value/".
I can think of two other possible approaches. Choosing one over the other really depends on your functional requirements, i.e. what you do with these values in your front-end.
1/ Set two listeners
The idea is to set one listener for 'parentnode/currencies/value/' + currency + '_price' and one for 'parentnode/currencies/value/currency-source', as follows:
var currency = 'EUR';
var ref2 = db
.ref()
.child('parentnode/currencies/value/' + currency + '_price');
ref2.on('value', function(snapshot) {
var data = snapshot.val();
console.log(data);
});
var ref3 = db.ref().child('parentnode/currencies/value/currency-source');
ref3.on('value', function(snapshot) {
var data = snapshot.val();
console.log(data);
});
2/ Query the currency-source value within the listener
With this second approach, in the listener to 'parentnode/currencies/value/' + currency + '_price', we query the database with the once() method to get the value of currency-source:
var ref4 = db
.ref()
.child('parentnode/currencies/value/' + currency + '_price');
ref4.on('value', function(snapshot) {
var data = snapshot.val();
console.log(data);
db.ref()
.child('parentnode/currencies/value/currency-source')
.once('value')
.then(function(dataSnapshot) {
console.log(dataSnapshot.val());
});
});
Note that if you do not need to set a listener, i.e. you want to fetch the data once (e.g. by triggering the fetch from a button, or on page loading, etc..) you should only use the once() method and then you can chain the two calls as follows:
var currency = 'EUR';
var ref5 = db.ref().child('parentnode/currencies/value/' + currency + '_price');
var ref6 = db.ref().child('parentnode/currencies/value/currency-source');
ref5
.once('value')
.then(function(dataSnapshot) {
console.log(dataSnapshot.val());
return ref6.once('value');
})
.then(function(dataSnapshot) {
console.log(dataSnapshot.val());
});

firebase firestore adding new document inside a transaction - transaction.add is not a function

I was assuming that it was possible to do something like:
transaction.add(collectionRef,{
uid: userId,
name: name,
fsTimestamp: firebase.firestore.Timestamp.now(),
});
But apparently it is not:
transaction.add is not a function
The above message is displayed inside the chrome console.
I see that we can use the set method of the transaction to add a new document transactionally. see: https://firebase.google.com/docs/firestore/manage-data/transactions
The thing is if I use set instead of add(which is not supported anyways), the id of the document should be created by me manually, firestore won't create it.
see: https://firebase.google.com/docs/firestore/manage-data/add-data
Do you see any downside of this not having an add method that generates the id for you automatically?
For example, is it possible that the id generated by the firestore itself is somehow optimized considering various concerns including performance?
Which library/method do you use to create your document IDs in react-native while using transaction.set?
Thanks
If you want to generate a unique ID for later use in creating a document in a transaction, all you have to do is use CollectionReference.doc() with no parameters to generate a DocumentReference which you can set() later in a transaction.
(What you're proposing in your answer is way more work for the same effect.)
// Create a reference to a document that doesn't exist yet, it has a random id
const newDocRef = db.collection('coll').doc();
// Then, later in a transaction:
transaction.set(newDocRef, { ... });
after some more digging I found in the source code of the firestore itself the below class/method for id generation:
export class AutoId {
static newId(): string {
// Alphanumeric characters
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let autoId = '';
for (let i = 0; i < 20; i++) {
autoId += chars.charAt(Math.floor(Math.random() * chars.length));
}
assert(autoId.length === 20, 'Invalid auto ID: ' + autoId);
return autoId;
}
}
see: https://github.com/firebase/firebase-js-sdk/blob/73a586c92afe3f39a844b2be86086fddb6877bb7/packages/firestore/src/util/misc.ts#L36
I extracted the method (except the assert statement) and put it inside a method in my code. Then I used the set method of the transaction as below:
generateFirestoreId(){
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let autoId = '';
for (let i = 0; i < 20; i++) {
autoId += chars.charAt(Math.floor(Math.random() * chars.length));
}
//assert(autoId.length === 20, 'Invalid auto ID: ' + autoId);
return autoId;
}
then,
newDocRef = db.collection("PARENTCOLL").doc(PARENTDOCID).collection('SUBCOLL').doc(this.generateFirestoreId());
transaction.set(newDocRef,{
uid: userId,
name: name,
fsTimestamp: firebase.firestore.Timestamp.now(),
});
Since I am using the same algo for the id generation as the firestore itself I feel better.
Hope this helps/guides someone.
Cheers.
Based on the answer from Doug Stevenson, this is how I got it worked with #angular/fire:
// Create a reference to a document and provide it a random id, e.g. by using uuidv4
const newDocRef = this.db.collection('coll').doc(uuidv4()).ref;
// In the transaction:
transaction.set(newDocRef, { ... });
To complete Stefan's answer. For those using Angularfire, earlier to version 5.2 using CollectionReference.doc() results in an error "CollectionReference.doc() requires its first argument to be of type non-empty string".
This workaround worked for me:
const id = this.afs.createId();
const ref = this.afs.collection(this.collectionRef).doc(id);
transaction.set(ref, { ... });
Credit: https://github.com/angular/angularfire/issues/1974#issuecomment-448449448
I'd like to add an answer solving the id problem. There's no need to generate your own ids. The documentReference is updated after the transaction.set() is called, so in order to access the Firestore's id you need to just do the following:
const docRef = collectionRef.doc();
const result = await transaction.set(docRef, input);
const id = docRef.id;
First of all, firestore transaction object has 4 (get,set,update,delete) methods and doesnt has "add" method. However, the "set" method can be used instead.
import { collection,doc,runTransaction } from "firebase/firestore";
On the other hand documentReference must be created for "set" method.
Steps :
1-) collection method create a collectionReference object.
const collectionRef = collection(FirebaseDb,"[colpath]");
2-) doc method create a documentReference object with unique random id for specified collectionReference.
const documentRef = doc(collectionRef);
3-) add operation can be performed with the transaction set method
try {
await runTransaction(FirebaseDb,async (transaction) => {
await transaction.set(documentRef, {
uid: userId,
name: name,
fsTimestamp: firebase.firestore.Timestamp.now(),
});
})
} catch (e) {
console.error("Error : ", e);
}

firebase realtime database find node by child value

I have the following structure:
root
-LKMsdf2_Qxbwtv4238D
details
uid: john
-YKMWrmj_QxbwtvSBM5A
details
uid: tony
-R45Wrmj_Qxbf321BMd4
details
uid: karina
How can I find the ref key under 'root' by its uid:
e.g: by uid:karina I need to get the ref key -R45Wrmj_Qxbf321BMd4
is there a way to use some wildcard like /root/{recordid}/details/uid or something?
======== Thanks for the hints! ==== here is my final solution ================
findEntry = function(targetUid) {
var entriesRef = db.ref('root');
return entriesRef.once('value')
.then((snapshot)=>{
var id = []; // found id
snapshot.forEach((childSnapshot)=>{
var childKey = childSnapshot.key;
var childData = childSnapshot.val();
var found = (childData.uid === targetUid);
if (found) {
console.log('Found for uid:' + targetUid + ': ' + childKey);
id = childKey;
}
return found; // true - breaks the forEach, false - continue
});
if (!id) {
console.log('Not Found for uid:' + targetUid);
}
return id;
});
}
No, the best you can do is child (key) search and equality (see the example here https://firebase.google.com/docs/reference/js/firebase.database.Query#equalTo)
// Find all dinosaurs whose height is exactly 25 meters.
var ref = firebase.database().ref("dinosaurs");
ref.orderByChild("height").equalTo(25).on("child_added", function(snapshot) {
console.log(snapshot.key);
});
There isn't a way to query deeper than that.
You could have other structures for reverse lookups, flatten out your data or solve it in a different way.

How to get a data from firestore while using batch?

I want to perform a batch transaction in firestore. I am storing last key in other collection.
i need to get the last key then increase by 1, then create two documents using this key. How can i do this?
let lastDealKeyRef = this.db.collection('counters').doc('dealCounter')
let dealsRef = this.db.collection('deals').doc(id)
let lastDealKey = batch.get(lastDealKeyRef) // here is the problem..
batch.set(dealsRef, dealData)
let contentRef = this.db.collection('contents').doc('deal' + id)
batch.set(contentRef, {'html': '<p>Hello World</p>' + lastDealKey })
batch.commit().then(function () {
console.log('done') })
If you want to read/write data in a single operation you should be using a transaction.
// Set up all references
let lastDealKeyRef = this.db.collection('counters').doc('dealCounter');
let dealsRef = this.db.collection('deals').doc(id);
let contentRef = this.db.collection('contents').doc('deal' + id);
// Begin a transaction
db.runTransaction(function(transaction) {
// Get the data you want to read
return transaction.get(lastDealKeyRef).then(function(lastDealDoc) {
let lastDealData = lastDealDoc.data();
// Set all data
let setDeals = transaction.set(dealsRef, dealData);
let setContent = transaction.set(contentRef, {'html': '<p>Hello World</p>' + lastDealKey });
// Return a promise
return Promise.all([setDeals, setContent]);
});
}).then(function() {
console.log("Transaction success.");
}).catch(function(err) {
console.error("Transaction failure: " + err);
});
You can read more about transactions and batches here:
https://firebase.google.com/docs/firestore/manage-data/transactions

How to load multiple data via service and wait for it in Angular2

I use Ionic 2 with Angular 2 in my project. In the root component you can click a "Add" button to add a new Report via a complex form and a lot of preprovided data (there are some selects that are feeded with data fetched from sqlite database)
Now in my "CreateReportComponent" i have the following constructor to load the data and assign it to local array variable:
selectEmployeeOptions: Employee[];
constructor(private dbService: DatabaseService) {
dbService.getAllEmployees().then(employees => {
this.selectEmployeeOptions = employees;
});
// load more data like tasks etc.
});
But when I want to modify this data in my component, the array is empty. I tried to do it in ngOnInit() but this seems to be to early as well.
I want to to something like this, before the component gets displayed:
dbService.getAllEmployees().then(employees => {
this.selectEmployeeOptions = employees;
// modify data
this.selectEmployeeTitleOptions = employees.map((item) => {
return item.title;
});
console.log(JSON.stringify(this.selectEmployeeTitleOptions)) // --> empty
});
But selectEmployeeTitleOptions is empty...
The function in the databaseService looks like this:
getAllEmployees(): Promise<Emplyoee[]> {
let query = "SELECT * FROM employees";
let employeeList = [];
this.database.executeSql(query, []).then((data) => {
if(data.rows.length > 0) {
let e = new Employee();
e.id = data.rows.item(i).id;
e.firstname = data.rows.item(i).firstname;
e.lastname = data.rows.item(i).lastname;
employeeList.push(e);
}
}, (error) => {
// handle error
});
return Promise.resolve(employeeList);
}
I read that there is the Resolve pattern (https://blog.thoughtram.io/angular/2016/10/10/resolving-route-data-in-angular-2.html) But I need to make multiple calls and not only for contacts as in the example.
So the question: How to wait for multiple calls to database?
i think something go wrong here
getAllEmployees(): Promise<Emplyoee[]> {
let query = "SELECT * FROM employees";
let employeeList = [];
this.database.executeSql(query, []).then((data) => {
if(data.rows.length > 0) {
let e = new Employee();
e.id = data.rows.item(i).id;
e.firstname = data.rows.item(i).firstname;
e.lastname = data.rows.item(i).lastname;
employeeList.push(e);
}
}, (error) => {
// handle error
});
return Promise.resolve(employeeList);
}
first return Promise.resolve(employeeList); will return empty array, because it is async process.
you need loop through data.rows, then format return data like this.
getAllEmployees(): Promise<Employee[]> {
let query = "SELECT * FROM employees";
return this.database.executeSql(query, []).then((data) => {
let arr = [];
for(let i = ; i < data.rows.length; ++i) {
let emp = data.rows.item(i);
let e = new Employee();
e.id = emp.id;
e.firstname = emp.firstname;
e.lastname = emp.lastname;
arr.push(e);
}
return arr;
});
}
note that .then() return a promise object.
What you are looking for is forkJoin method that returns Observable that you should switch to instead of using Promises, for reference about why you should do this check here.
Short information about fork join from its GitHub page:
Runs all observable sequences in parallel and collect their last elements.
This way you can safely make parallel requests to your API.
For more information regarding forkJoin go here.
Additionally you should call services using ngOnInit as you mentioned before. For more information about Angular 2 lifecycle hooks see the docs.
You can use Promise.all
https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
You push all promises to an array, and then go
let foo : [Promise<Emplyoee[]>,Promise<void>] = [getAllEmployees(), method2()];
Promise.all(foo).then((results:any[]) => {
let employeearray: any = results[0];
/* and on an on */
});

Resources