i don't know how to go about accessing several documents inside a document of unknown name. Here is my Firebase Firestore structure:
BUSINESS
unknownbusinessid1
PROMOTIONS
unknownpromotionid1
unknownpromotionid2
(...)
unknownbusinessid2
(...)
unknownbusinessid3
(...)
what I would like to achieve is to retrieve all promotion documents (unknownpromotionid1, unknownpromotionid2, etc.) without knowing the business ids (unknownbusinessid1, unknownbusinessid2, unknownbusinessid3, etc.)
it should be something like this:
const getItem = db.doc(`BUSINESS/$whatever`).collection("PROMOTIONS")
.get().then((snapshot) => {
snapshot.docs.map(doc => {
console.log("this is your promotion", doc)
})
return promotionsArray
})
how can this be accomplished?
thank you!
You can use a collection group query, as detailed in the docs, it allows accessing subcollections with a specific name from every documents at once:
db.collectionGroup('PROMOTIONS').get().then(snapshot => {
snapshot.forEach(doc => {
console.log("this is your promotion", doc)
}
});
Note that you will need to set an index as well a an appropriate security rule for Firestore:
match /{path=**}/PROMOTIONS/{id} {
allow read: if true;
}
Related
I am looking to fetch the data of collection named users which has documents with an id of logged-in users' uid. Further, these documents contain some data and a subCollection called posts.
which looks like -
So now, I need to fetch all four(4) of these documents along with the posts collection data together so that I can display it.
My approach -
( here I fetched the document ids - middle section of image IDs)
// Fetching Firestore Users Document IDs
const [userDocs, setUserDocs] = React.useState([]);
React.useEffect(() => {
try {
const data = firestore.collection('users')
.onSnapshot(snap => {
let docIDs = [];
snap.forEach(doc => {
docIDs.push({id: doc.id});
});
setUserDocs(docIDs);
})
}
catch(err) {
console.log(err);
}
}, [])
Now, I have tried to fetch the entire data using the following way (which isn't working)
// Fetching Firestore Posts Data
const [postData, setPostData] = useState([]);
React.useEffect(() => {
try {
userDocs.map(data => {
const data = firestore.collection('users/'+currentUser.uid+'/posts')
.onSnapshot(snap => {
let documents = [];
snap.forEach(doc => {
documents.push({...doc.data(), id: doc.id});
});
setPostData(documents);
})
})
}
catch(err) {
console.log(err);
}
}, [])
Finally, I should end up with postData array which I can map on my card component to render all posted images and captions to the UI.
I am not sure if this is the right way to achieve what I am doing here, please help me correct this error and if there's a more subtle and easy way to do it please let me know. Thank You.
I have tried to fetch the entire data
Looking at the code you wrote for fetching "the entire data" (i.e. the second snippet) it seems that you don't need to link a post document to the parent user document when fetching the post documents. In other words, I understand that you want to fetch all the posts collection independently of the user documents.
Therefore you could use a Collection Group query.
If you need, for each post document returned by the Collection Group query, to get the parent user doc (for example to display the author name) you can do as explained in this SO answer, i.e. using the parent properties.
I am building an app using Firebase Firestore as a BaaS.
But I am facing a problem when I try to create a feed/implement full-text-search on my app.
I want to be able to search through all the users posts, the problem is, the users posts are structured like this in the Firestore Database:
Posts(collection) -> UserID(Document) -> user posts(subcollection that holds all userID posts) -> actual posts(separate documents within that collection)
I want to loop through every user's user posts subcollection and fetch all data for the feed, and also to implement it with a full text search app like Algolia or ES.
I can loop through a specific user ID(code below), but being a beginner, I couldn't find a way to loop through all of them and fetch all of them.
firebase.firestore()
.collection('allPosts')
.doc('SPECIFIC_USER_ID') //-> Here I have to loop through all docs in that collection
.collection('userPosts')
.orderBy("creation", "asc")
.get()
.then((snapshot) => {
let posts = snapshot.docs.map(doc => {
const data = doc.data();
const id = doc.id;
return { id, ...data }
})
setUserPosts(posts)
})
}
Would love some help!
Collection Group Query
You can query in all collections named X using a collection group query.
var posts= db.collectionGroup('userPosts').orderBy('creation').limit(10);
posts.get().then((querySnapshot) => {
let posts = querySnapshot.map(doc => {
const data = doc.data();
const id = doc.id;
return { id, ...data }
})
setUserPosts(posts)
});
Source: https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query
Algolia implementation
You will need to use Cloud Functions to migrate fields to a dedicated collection specifically for Algolia. Many users have found nested SubCollections to be problematic with Algolia's setup.
You do this by duplicating the user Post data as a 'source' to this new public collection, and using the Firebase Algolia Extension, you can sync it directly
exports.bakePosts= functions.firestore
.document('allPosts/{userID}/userPosts/{postID}')
.onWrite((change, context) => {
// Get an object with the current document value.
// If the document does not exist, it has been deleted.
const document = change.after.exists ? change.after.data() : null;
// Get an object with the previous document value (for update or delete)
const oldDocument = change.before.data();
if(document != null)
db.collection("posts/"+ context.params.postID).set(document);
if(document == null)
db.collection("posts/"+ context.params.postID).delete();
});
Algolia Extension:
https://firebase.google.com/products/extensions/firestore-algolia-search
You can avoid most of the above if you simply submit posts to a master collection and have the userID as the 'owner' property within the document. The above also have benefits, but more related to blog posts where users may have a "work in progress" version vs Live.
The Algolia Extension has the full guide on how to set it up and if you need to customize the extensions, the source code is also available.
I have chatrooms stored in Cloud Firestore that need to be secured and I'm having a difficult time doing so. My state in React looks as such:
export class Messages extends React.Component {
constructor(props) {
super(props);
this.boardID = this.props.settings.id;
this.mainChatRef = database
.collection("boards")
.doc(boardID)
.collection("chats")
.doc("MAIN")
.collection("messages");
this.chatRoomsRef = database
.collection("boards")
.doc(boardID)
.collection("chats");
}
My query in react looks like:
latestMessages = (messageSnapshot) => {
const message = [];
messageSnapshot.forEach((doc) => {
const { text, createdAt, uid } = doc.data();
message.push({
key: doc.id,
text,
createdAt,
uid,
});
});
this.setState({
dataSource: message,
});
}
queryRooms = async () => {
const recentQuery = await this.chatRoomsRef
.where("uidConcat", "in", [
this.props.user.uid + this.state.value.objectID,
this.state.value.objectID + this.props.user.uid,
])
.get();
for (const qSnap of recentQuery.docs) {
const messagesRef = this.chatRoomsRef
.doc(qSnap.id)
.collection("messages")
.orderBy("createdAt")
.limitToLast(30);
messagesRef.onSnapshot(this.latestMessages);
}
}
My database structure:
boards(collection)
{boardId}
chats (collection)
MAIN
{chatId_1}
{chatId_2}
uidArray (has only two UIDs since it's for private chat)
uidConcat: user1_uid+user2_uid
messages(collection)
{message1}
createdAt
text
uid_creator
uidArray (has UID of user1 and user2)
I tried securing my boards as such:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /boards/{boardId} {
allow read, write: if request.time < timestamp.data(2050, 5, 1);
match /chats/MAIN/messages/{messagesId=**} {
allow read, create: if request.auth.uid !=null;
}
match /chats/{chatId} {
allow read, write: if request.auth.uid !=null;
match /messages/{messagesId=**} {
allow read: if (request.auth.uid in get(/databases/{database}/documents/boards/
{boardId}/chats/{chatId}/messages/{messageId}).data.uidArray)
&& request.query.limit <= 30 && request.query.orderBy.createdAt == 'ASC';
allow write: if request.auth.uid != null;
}
}
}
}
}
I wish to have anyone that's authenticated to always have access to the boards MAIN chat and the way I have it written it works. For private rooms I keep getting an error. I've read the documents and I know that Security rules are not filters which is why I added a bunch of AND statements in an attempt to closely resemble my securities to my written code query in React yet I still keep getting Uncaught Errors in snapshot listener: FirebaseError: Missing or insufficient permissions. I am currently writing my rules to secure at the document level within the messages collection but Ideally I'd like to secure at the document level in the chat colection and check if my request.auth.uid is in the document field uidArray as so :
match /chats/{chatId=**} {
allow read, write if request.auth.uid in resource.data.uidArray
}
I imagine securing the documents in the chat collection would be more secure but any method that secures messages is more than appreciated.
The problem is that you're attempting to do a dynamic lookup in your rule based on each row of the data in this line: get(/databases/{database}/documents/boards/{boardId}/chats/{chatId}/messages/{messageId}).data.uidArray)
The critical point to understand here is that rules do not look at the actual data when perform queries. Get operations are a bit different because there's exactly one record to fetch, but for queries, it only examines the query to ensure it cannot fetch data that doesn't match your rule; it never actually loads data to examine. This is covered in docs here and there's a good video overviewing this here. A deeper dive is here.
Role based access is a complex topic and probably not easy to answer in a Stack Overflow given how broad the solutions would be, and how specific they are to your use case. But a naïve alternative would be to list the members in /chats/{chatId} and use a rule like this:
match /chats/{chatId} {
// scoped to chats/{chatId} doc
function isChatMember(uid) {
// assumes this document contains an array of
// uids allowed to read the messages subcollection
return uid in resource.data.members;
}
match /messages/{messagesId=**} {
allow read: if isChatMember(request.auth.uid)
}
}
I have created several user accounts on mongodb and i want to sort them out by user name. I compare the user names in the database against a string provided through aaxios request with a body value that is taken from an input value, like this:
frontend
const findUsers = async () => {
try {
const response = await axios.post(`http://localhost:8080/search-users/${_id}`, { searchValue });
setReturnedUser(response.data.matchedUser);
} catch (error) {
console.log(error);
}
}
findUsers();
backend
exports.sort = (req, res) => {
let result;
User.find({ name: req.body.searchValue }).exec((error, users) => {
if (error) {
return res.status(400).json({
message: error,
});
}
result = users;
res.status(200).json({
message: 'Description added successfully',
matchedUser: result,
});
});
};
The problem with this approach is that the users are returned only after I type in the entire name.
What I want is that the users to get returned as I type in the name, so several matching users will het returned when I start typing and as I continue the list will narrow down, until only the matching user remains.
I have successfully achieved this on the react side, but that was possible only by fetching all the users from the database, which would be a very bad idea with a lot of users. The obvious solution is to do the sorting on the server.
Filtering on the client-side is possible but with some tweaks to your architecture:
Create an end-point in node that returns all the users as JSON. Add caching onto this end-point. A call to origin would only occur very infrequently. YOu can then filter the list easily.
Use something like GraphQL and Appollo within node. This will help performance
To do the filtering in node you can use a normal array.filter()
I woul do the filter in mongo as the quick approach and then change it if you notice performance issues. It is better no to do pre-optimisation. As Mongo is NoSQL it wshould be quick
Every day I am importing products from external retailers into a Google Cloud Firestore database.
During that process, products can be either new (a new document will be added to the database) or existing (an existing document will be updated in the database).
Should be noted that I am importing about 10 million products each day so I am not querying the database for each product to check if it exists already.
I am currently using set with merge, which is exactly what I need as it creates a document if it doesn't exist or updates specific fields of an existing document.
Now the question is, how could I achieve a createdAt timestamp given that provided fields will be updated, therefore the original createdAt timestamp will be lost on update? Is there any way to not update a specific field if that field already exists in the document?
I suggest that you use a Cloud Function that would create a new dedicated field when a Firestore doc is created. This field shall not be included in the object you pass to the set() method when you update the docs.
Here is a proposal for the Cloud Function code:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.productsCreatedDate = functions.firestore
.document('products/{productId}')
.onCreate((snap, context) => {
return snap.ref.set(
{
calculatedCreatedAt: admin.firestore.FieldValue.serverTimestamp()
},
{ merge: true }
)
.catch(error => {
console.log(error);
return false;
});
});
Based on Bob Snyder comment above, note that you could also do:
const docCreatedTimestamp = snap.createTime;
return snap.ref.set(
{
calculatedCreatedAt: docCreatedTimestamp
},
{ merge: true }
)
.catch(...)
in version firebase 9 the correct way is:
import { serverTimestamp } from "firebase/firestore";
....
return snap.ref.set(
{
calculatedCreatedAt: serverTimestamp();
.....