How to handle GeoFirestore query's read operations? - geofirestore

I'm making an app where i can localize people around me. So i have a Firestore database that save all the user's location (users/{usersId}) and i make a query to find all people around me. My problem is that if i have 1000 people around me the query count 1000 read operation. Is there a solution to handle the number of read operation?
Here's how i make my geoquery:
var db = firebase.firestore();
const geoFirestore = new GeoFirestore(db);
const collectionRef = geoFirestore.collection('users');
const geoQuery = collectionRef.near({
center: user.coordinates,
radius: user.distance,
});
geoQuery.get().then((value) => {
value.docs.forEach( async (element) => {
....
})
})

Thx for your answers,
Here is the index i created

Related

How to use Firestore Security Rules to let users only access their own resources

I'm pretty new to Firebase and am trying to make a simple todo list app with different users (Mainly to get the hang of Firestore and security rules).
I've created two accounts with two different emails, as follows:
And this is picture of one of the documents with userID field to identify the owner. (Notice it matches one of the Auth users.
I am trying to create security rules so that each user can only view their todos by checking request.auth.uid and the todo.userID as per the following screenshot:
The write rule is working, I tested it with different UIDs and it properly rejected the request. However, I keep getting permission-denied when trying to read the document. I tried so many different things, went through the documentation but still could not resolve this. Any idea on how to resolve this?
Thank you in advance
RESOLVED
Code before solution
const collectionRef = collection(db, "todos");
export function subscribeToDatebase(setTodos, setErr, uid) {
function snapshot(snapshot) {
const todosCollection = snapshot.docs;
const todos = [];
todosCollection.forEach((t, i) =>
todos.push({
firebaseID: t.id,
...t.data(),
timeStamp: t.data().timeStamp,
})
);
setTodos(todos);
}
function error(err) {
setErr(err.code);
}
const unSub = onSnapshot(collectionRef, snapshot, error);
return unSub;
}
Code after solution
const collectionRef = collection(db, "todos");
export function subscribeToDatebase(setTodos, setErr, uid) {
function snapshot(snapshot) {
const todosCollection = snapshot.docs;
const todos = [];
todosCollection.forEach((t, i) =>
todos.push({
firebaseID: t.id,
...t.data(),
timeStamp: t.data().timeStamp,
})
);
setTodos(todos);
}
function error(err) {
setErr(err.code);
}
// This is the missing line
const q = query(collectionRef, where("userID", "==", uid));
// Changing the reference to the query
const unSub = onSnapshot(q, snapshot, error);
return unSub;
}
Firebase security rules don't filter data, but merely make sure that the app is only trying to read data it is allowed to read.
It's hard to be certain without seeing the code that fails, but my educated guess is that you're not actually querying for uid in the read operations.
So to match with your rules, you need a query that only tries to read the user's own data, like this (in the v8 JavaScript syntax):
todosCollectionRef.where('userID', '==', firebase.auth.currentUser.uid)
Also see the documentation on query based rules

Is there any way to combine range queries for different fields in cloud firestore?

I know that the firestore does not support range queries on different fields (as per the doc).
But I ran into a situation where I needed to check the quantity > 0 and order by discount percentage.
Like this
db.collection("Inventory").where("availableQuantity", ">", 0).orderBy("discountPercentage", "desc").get();
Can anyone help me with achieving this functionality?
Or suggest me a different data model to store the data.
here's the current data model
Let me suggest a different data model to enable you run your query.
add a bool field called availableQuantityGreaterThan0.
Keep it updated using cloud functions. Then run your query like this:
db.collection("Inventory")
.where("availableQuantityGreaterThan0", "==", true)
.orderBy("discountPercentage", "desc").get();
Example cloud function to keep the field updated:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const db = admin.firestore();
exports.onUpdate = functions.firestore
.document("/Inventory/{inventory_id}")
.onUpdate((change, context) => {
const params = context.params;
const inventoryId = params.inventory_id;
const inventory = change.after.data();
const availableQuantityGreaterThan0 = inventory.availableQuantity > 0;
return db.doc("/Inventory/" + inventoryId)
.set({ availableQuantityGreaterThan0 }, { merge: true });
});
Do this for onCreate also.

Firestore + Vue: When refreshing the page, how to avoid delay in data display?

Every time I refresh the page, a new query is made in the Firestore database to show (sometimes) the same data to the user.
It is a bookcase. The code below only reads data that has already been added:
export default {
name: "Shelf",
date: () => ({ shelfName: "", books: [] }),
async mounted() {
const auth = getAuth();
const db = getFirestore();
const userID = auth.currentUser.uid;
const userRef = doc(db, "users", userID);
const userSnap = await getDoc(userRef);
userSnap.exists()
? (this.shelfName = "Shelf: " + userSnap.data().name)
: (this.shelfName = "Your Shelf");
const userBooksRef = query(collection(db, "users", userID, "addedBooks"));
const querySnapshot = await getDocs(userBooksRef);
querySnapshot.forEach((doc) => {
this.books.push({
id: doc.id,
addedIn: doc.data().addedIn,
readIn: doc.data().readIn,
});
});
this.books.map(async (book) => {
const booksRef = doc(db, "books", book.id);
const bookSnap = await getDoc(booksRef);
book.authors = bookSnap.data().authors;
book.title = bookSnap.data().title;
book.thumbnail = bookSnap.data().thumbnail;
});
},
};
I'm not sure if I should look for a caching solution or if there is some other type of solution.
Below I show the books collection. It's important to say that within the users collection there is a collection called addedBooks that reference the id of each book below:
When refreshing the page, you're destroying the previous instance of your application and loading a new one. As such, your app will re-execute all of its queries just like it did the first time opening the app.
As Estus mentioned in the comments, this problem is often solved by storing your data in state, and then putting that state in localstorage. When your page is refreshed, you first load the state from localstorage (no delay), and then update the state with the current data from your query when it becomes available. You can implement this process manually as well without using a state management library.
When storing data in localstorage, make sure you update it every time the data changes, and clear it when you no longer need it.

How to await a write function inside a get function with Firebase Cloud Function for Flutter app

So, I don't really know how to write JS, I am developing a mobile app in Flutter, and I would be grateful for some help and clarifications regarding Future/Promises in JS.
I got a collection of posts for each user, and I want to create an .onCreate function which when a user posts a new post (a new document is created inside the 'posts/userId/user_posts' collection), then it gets all the user's followers (from a collection 'user_followers/userUid') and for each follower, it writes the postUid and postOwnerUid to that follower's newsFeed collection ('user_news_feed/followerId').
This is what I got right now, but I am walking blind, since I really don't know JS and I don't know how can I await a write function while inside a get function.
And how do I prevent Cloud Timeouts? If for instance the user has 1000 followers, how can I prevent Firebase from shutting down my function and making sure all the followers are notified?
exports.writeToUserNewsFeed = functions.firestore
.document('posts/{userId}/user_posts/{postId}')
.onCreate((snap, context) => {
const postData = snap.data();
const postUid = postData['post_uid'];
const userUid = postData['user_uid'];
const postCreationDate = postData['post_creation_date'];
var docRef = db.collection('user_followers').doc(userUid).collection('followers');
docRef.get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
db.collection('user_news_feed')
.doc(doc.data['uid'])
.collection('feed')
.document(postUid)
.set({
'post_uid': postUid,
'user_uid': userUid,
'post_uid': postCreationDate,
});
});
});
});
As explained in the doc, in a background Cloud Function like an onCreate() for Firestore, you need to return a Promise when all the asynchronous work is completed. So in your case, one possibility is to use Promise.all() because you don't know upfront how many documents are in the followers subcollection. Since Promise.all() returns a single Promise you can include it in the Promise chain that you need to return in the Cloud Function.
exports.writeToUserNewsFeed = functions.firestore
.document('posts/{userId}/user_posts/{postId}')
.onCreate((snap, context) => {
const postData = snap.data();
const postUid = postData['post_uid'];
const userUid = postData['user_uid'];
const postCreationDate = postData['post_creation_date'];
var followersColRef = db.collection('user_followers').doc(userUid).collection('followers');
return followersColRef.get().then((querySnapshot) => { // <= See return here
const promises = [];
querySnapshot.forEach((doc) => {
promises.push(
db.collection('user_news_feed')
.doc(doc.data['uid'])
.collection('feed')
.doc(postUid)
.set({
'post_uid': postUid,
'user_uid': userUid,
'post_uid': postCreationDate,
})
);
});
return Promise.all(promises); // <= See return here
})
.catch(error => {
console.log(error);
return null;
})
});
Note that instead of using Promise.all() you could also use a batched write but there is a limit of 500 operations for a batched write.

dialogflow chatbot how to go to firestore database and return with support relevant questions?

I am using "firestore" database for my "dialogflow" chat bot which I already created for an online grocery store. The problem is: I want my chatbot to initially ask questions from users to find the proper item title in my database and then return to user by asking 3-4 support relevant questions about that item. the questions must be the item attributes (Brand, color, size...) and will vary from one item to another. so the chatbot will stream down the user to find the best item.
can you please help me to find the answer? I already created the codes but they don't work and I do not know what is wrong with that. If you already created this and have the index.js file, I appreciate to propose me here.
index.js:
'use strict';
const functions = require('firebase-functions');
// Import admin SDK
const admin = require('firebase-admin');
const {
WebhookClient
} = require('dialogflow-fulfillment');
process.env.DEBUG = 'dialogflow:*'; // enables lib debugging statements
admin.initializeApp(functions.config().firebase);
// here we get the database in a variable
const db = admin.firestore();
const data = {...};
// Add a new document in collection "dialogflow" with document ID 'agent'
const dialogflowAgentRef = db.collection('dialogflow').doc('agent').set(data);
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({
request,
response
});
function writeToDb(agent) {
// Get parameter from Dialogflow with the string to add to the database doc
const databaseEntry = agent.parameters.databaseEntry;
// Get the database collection 'dialogflow' and document 'agent' and store
// the document {entry: "<value of database entry>"} in the 'agent' document
const dialogflowAgentRef = db.collection('dialogflow').doc('agent').where('title', '==', title);
return db.runTransaction(t => {
t.set(dialogflowAgentRef, {
entry: databaseEntry
});
return Promise.resolve('Write complete');
}).then(doc => {
agent.add(`Wrote "${databaseEntry}" to the Firestore database.`);
}).catch(err => {
console.log(`Error writing to Firestore: ${err}`);
agent.add(`Failed to write "${databaseEntry}" to the Firestore database.`);
});
}
function readFromDb(agent) {
// Get the database collection 'dialogflow' and document 'agent'
const dialogflowAgentDoc = db.collection('dialogflow/agent/rss/channel/item'); // .doc('agent')
// Get the value of 'entry' in the document and send it to the user
return dialogflowAgentDoc.get()
.then(doc => {
if (!doc.exists) {
agent.add('No data found in the database!');
} else {
agent.add(doc.data().entry);
}
return Promise.resolve('Read complete');
}).catch(() => {
agent.add('Error reading entry from the Firestore database.');
agent.add('Please add a entry to the database first by saying, "Write <your phrase> to the database"');
});
}
// Map from Dialogflow intent names to functions to be run when the intent is matched
let intentMap = new Map();
intentMap.set('ReadFromFirestore', readFromDb);
intentMap.set('WriteToFirestore', writeToDb);
agent.handleRequest(intentMap);
});
There are a number of issues with your code as you've shown it that could cause problems reading and writing with the Firestore database.
It looks like you're trying to find an existing collection to write to with the line
const dialogflowAgentRef = db.collection('dialogflow').doc('agent').where('title', '==', title);
but title isn't defined anywhere, which I suspect causes an error. Furthermore, doc() returns a DocumentReference, but there is no where() method in a DocumentReference.
Remember that you need to structure Firestore using alternating collections and documents. So your "firebase" collection can contain a document named "agent", and that document may have subcollections.
When you're trying to read with
const dialogflowAgentDoc = db.collection('dialogflow/agent/rss/channel/item');
You're getting a collection, but then trying to treat it as a document. The comment suggests that you're trying to read a specific doc from this collection (which makes sense), but you're loading that document by a hard-coded string "agent", rather than trying to get the agent from the parameters passed to you from Dialogflow.
Finally - the paths in the read and write sections don't match. Using hard-coded paths are fine when testing, but make sure you're using matching paths and that they reflect the collection/doc/collection/doc/... path requirement.
So in both cases, you might have a reference that looks something like
const docTitle = agent.parameters.title;
const docRef = db.collection('dialogflow').doc(title);
Which, if you have defined a "title" parameter in your Intents in Dialogflow, will use this to reference the doc, which you can then read or write.
thanks for the answer I already changed my database to real time firebase instead of firestore. still having problem with support relevant questions. I want to go to my real time database to find the item by search using "oederByChild" and "equalTo" methods as I found these in people questions and answer in this website. still cannot find and item title through my database child. here is the codes are written:
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const {
WebhookClient
} = require('dialogflow-fulfillment');
process.env.DEBUG = 'dialogflow:*'; // enables lib debugging statements
admin.initializeApp(functions.config().firebase);
const db = admin.database();
// const ref = db.ref('server/saving-data/fireblog');
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({
request,
response
});
console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
// Get the database collection 'dialogflow' and document 'agent' and store
// the document {entry: "<value of database entry>"} in the 'agent' document
function writeToDb(agent) {
const databaseEntry = agent.parameters.databaseEntry;
const acc = db.ref('rss/channel/item/4/title'); //**This worked! */
acc.set({
entry: databaseEntry
});
return Promise.resolve('write complete')
.then(_acc => {
agent.add(`Wrote ${databaseEntry} to the realtime database.`);
return false;
}).catch(err => {
console.log(`Error writing to Firestore: ${err}`);
agent.add(`Failed to write "${databaseEntry}" to the Firestore database.`);
});
}
// and this is when we want to write to in the same child, keeping the old values:
//const acc = db.ref('/rss/channel/item/5/color'); //**This worked! */
//const result = acc.child(databaseEntry).set({entry: databaseEntry});
//agent.add(`Wrote ${databaseEntry} to the realtime database.`);
//console.log(result.key);
//});
// to read data
function readFromDb(agent) {
const any = agent.parameters.any;
agent.add(`Thank you...`);
var rootRef = db.ref();
var childref = rootRef.child("rss/channel/item");
return childref.orderByChild("title").equalTo("Icebreaker").once("value").then(function(snapshot){ //has been taken from the bus example: https://stackoverflow.com/questions/51917390/dialogflow-how-do-i-pass-a-parameter-through-in-a-firebase-query
var colored = snapshot.child("color/__text").val();
var sized = snapshot.child("size/__text").val();
agent.add(`Your search result for ` + any + ` Throughout the database is ` + colored +
` Color and ` + sized + ` Size`);
return Promise.resolve('Read complete');
}).catch(() => {
agent.add('Error reading entry from the Firestore database.');
agent.add('Please add a entry to the database first by saying, "Write <your phrase> to the database"');
});
}
// Map from Dialogflow intent names to functions to be run when the intent is matched
let intentMap = new Map();
intentMap.set('IWannaBuy', readFromDb);
intentMap.set('WriteToFirebase', writeToDb);
agent.handleRequest(intentMap);
});
enter code here
[this is how my database is][1]
[1]: https://i.stack.imgur.com/QdFy5.png

Resources