How can i do the pagination without modifying the limit? - javascript

How can I get the same elements what the user asked?
for example if I do this, the user asked 30
query = query.limit(30)
const qSnap = await query.get(); // 30 objects
qSnap.docs.forEach((doc) => { // i will get fewer than 30
const item = doc.data().data.tools.find(t => t.trait === 'color1' && t.value == 'red');
console.log(item)
})
i need to filter because i have this structure:
{
name:carla
data: "{
sign: "carly",
tools: [{trait:color1, value:red}, {trait:color2, value:white}] }"
},{
name:dany
data: "{
sign: "dan",
tools: "[{trait:color1, value:blue}, {trait:color2, value:black}]
}"
}
or how can i enhacement my structure to dont have this problem?

Taking Stewart's answer and changing it a bit (I couldn't do that in a comment, sorry)
const toolsFilter = {
trait: 'color1',
value:'red'
}
const qSnap = await query.where('tools','array-contains-any', toolsFilter)
.limit(30)
.get();
qSnap.docs.forEach((doc) => {
const item = doc.data().data;
console.log(item)
}))

The array-contains operations checks if an array, contains a specific (complete) value. It can't check if an array of objects, contains an item with a specific value for a property. The only way is to query the entire object inside the array.
In this example structure:
data: {
sign: "carly",
tools: [{trait:color1, value:red}, {trait:color2, value:white}] }
}
You want to query objects inside a map of an array. See Firestore screenshot below for better visualization:
To be able to query objects inside a map of an array, you must query the whole object inside of it. See example query below:
// As you can see here, you need to be able to jump inside the `data.tools`
// Then query the `tools` array by using objects.
const toolsRef = db.collection("someCollection").where("data.tools", "array-contains", {trait: "color1", value: "red"})
Here's a complete code for reference:
const toolsRef = db.collection("someCollection").where("data.tools", "array-contains", {trait: "color1", value: "red"})
query = toolsRef.limit(30)
query.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// Do anything with the result.
console.log(doc.id, " => ", doc.data());
});
})
.catch((error) => {
console.log("Error getting documents: ", error);
});
The result should be something like this:
5nwzwpxr7BmctvznEypl => {
name: 'carla',
data: { tools: [ [Object], [Object] ], sign: 'carly' }
}
For more information, See Array Memberships.
UPDATE:
Firestore does not have a way to search from the document's fields which have a JSON object encoded inside. You should parse the JSON object first to get/filter the necessary data. e.g.:
query = query.limit(30);
const qSnap = await query.get();
qSnap.docs.forEach((doc) => {
const data = JSON.parse(doc.data().data);
const items = data.tools.filter(t => t.trait === "color1" && t.value === "red");
console.log(items);
})
However, the above snippet that is similar to yours could lead into a problem which is not the same as the limit() you set on your query. To enhance your structure, I would suggest to put it in document fields like I gave on my original answer above.
document
(fields)
|- name(string): "carla"
|- data(map)
|- sign(string): "carly"
|- tools(array)
|- (map)
| - trait(string): "color1"
| - value(string): "red"
|- (map)
- trait(string): "color2"
- value(string): "white"
This structure is the same as your JSON object encoded inside the data field. The advantage of using this structure is you can now query using Firestore which I showed to you on my original post. This would result in 30 documents without using a client side filtering. It will be only fewer than 30 if the query can't find matched documents.
To do this, you just need to construct your JSON object and set the data to the document. See e.g. below:
db.collection("someCollection").add({
data: {
sign: "carly",
tools: [{trait: "color1", value:"red"}, {trait:"color2", value:"white"}]
},
name: "carla"
})
.then((docRef) => {
console.log("Document written with ID: ", docRef.id);
})
.catch((error) => {
console.error("Error adding document: ", error);
});

You need to filter the data first, them limit it. Using this synatax.
const toolsFilter = {
trait: 'color1',
value:'red'
}
const qSnap = await query.where('tools','array-contains',toolsFilter)
.limit(30)
.get();
qSnap.docs.forEach((doc) => {
const item = doc.data().data;
console.log(item)
}))
See the Firebase online documentation for more info on query syntax etc.. https://firebase.google.com/docs/firestore/query-data/queries
Also this explains pagination using query cursors https://firebase.google.com/docs/firestore/query-data/query-cursors

Related

How can I add objects to an array of Objects which has same keys in firestore?

I have a Collection called Notes which has a document that is an UID of the user that consists of collection of an Array of objects which look something like this
obj {[
{title:"Note 1",desc:"description 1"},
{title:"Note 2",desc:"description 2"},
{title:"Note 3",desc:"description 3"},
]}
this is the actual Firestore collection where is now allowing me to store objects with the same key using the code I wrote but If I'm trying to add it manually then I
I'm using React with Firebase and using Firestore as a database
the problem I'm facing is that if I add more objects to the array mentioned above with the same key it's not creating a duplicate Firestore is not letting me add more objects with the same key. Other than that it is running just fine.
Below is the code for adding a new Notes Collection
// this function create a new Notes Collection
const createNotes = async (title, description) => {
const uid = auth.currentUser.uid; // For document name
const notesRef = doc(db, "Notes", uid);
const data = {
note: [{ title: title, description: description }],
};
try {
await setDoc(notesRef, data);
} catch (error) {
console.log(error);
}
};
Below is the code for updating the array of objects, this was the only way I could find to add multiple objects into an array on firestore
const updateNotes = async (title, description) => {
const uid = auth.currentUser.uid;
const notesRef = doc(db, "Notes", uid);
const data = {
note: arrayUnion({ title: title, description: description }),
};
try {
await updateDoc(notesRef, data, { merge: true });
} catch (error) {
console.log(error);
}
};
Is there a solution to this?
According to your last comment:
If suppose I have added an array field such as {title:”a”,desc:”b”} in the array and if again try to add the same object it will not create a duplicate of the same object how will I be able to have multiple same objects in an array?
Please note that you cannot add duplicate elements in an array using arrayUnion. According to the official documentation:
arrayUnion() adds elements to an array but only elements not already present.
So if you need to have duplicate elements, then the single option that you is to read the document, add the duplicate element in the array, and then write the document back to Firestore.

How to update a nested object child element, where some condition(s) is/are true

Note: This is not a duplicate question, please read till the end and see the included image.
I have a nested object and an array field inside my collection/document in Firestore.
Main categories
Drinks
Snacks
Items for Drinks are
(Water, Energy, Milk, ...)
Items for Snacks are
(Chips, Biscuits, Corn, ..)
The user may subscribe to both categories for multiple items with an expiration date:
Drinks->Energy
Drinks->Milk
Snack->Chips
I want to update the [expDate] field where [name] is equal to drinks and [type] is equal to [energy]
I have explored Firestore documentation more importantly compound queries in Cloud Firestore and read so many article(s) and questions on stackeoverflow but I couldn't find my answer, below is part of my code which I tr.
db.collection(this.dbName)
.where("name", "==", "drinks")
.where("subscriptions.data.type", "==", "energy")
.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
let data = doc.data();
if (data.email == email) {
let subscriptions = data.subscriptions;
subscriptions.forEach((subscription) => {
if (subscription.name == productName) {
let prodTypes = subscription.data;
prodTypes.forEach((prodType) => {
if (prodType.type == itemType) {
let docRef = fb.db.collection(this.dbName).doc(email);
fb.db
.collection(this.dbName)
.doc(email)
.update(docRef, {
subscriptions: [
{
data: [
{
expDate: expiration,
type: itemType,
},
],
name: productName,
},
],
});
}
});
}
});
}
});
})
.catch((error) => {
console.log("Error getting documents: ", error);
});
I don't get any console log result for the above query.
This query won't work:
db.collection(this.dbName)
.where("name", "==", "drinks")
.where("subscriptions.data.type", "==", "energy")
This returns documents that have a:
field name at their root with value "drinks" AND
have a field type nested under data nestedundersubscriptions**under the root** with the value"energy"`
Neither of those fields is present in the document you've shown, so the query won't return that document. If you add the fields under the root of the document, you'll see that the query returns it.
It looks like you're trying to return documents where the subscriptions array contains specific value in its items. For this you'll need to use the array-contains operator. This operation can test if an array field contains a specific complete value, like:
db.collection(this.dbName)
.where("subscriptions", "array-contains", { ... })
But here you have to specify the entire object that must exist in the array. You can't check whether one property if the item exists with a specific value, and you also can't check for a nested array as you seem to have here.
The solution, as is usually the case when dealing with NoSQL databases, is to change/augment your data model to allow the use-case. Since you want to query for documents that have a specific name and type, add top-level fields for all names and types that exist in this document:
{
names: ["drinks", "snacks"],
types: ["energy", "water"]
subscriptions: [
...
]
}
Now you can use (one of) the new top-level fields in your query:
db.collection(this.dbName)
.where("names", "array-contains", "drinks")
You can't add a second array-contains clause though, as a query can only have one such clause in Firestore. So you'll have to filter the types in your application code after using a query to retrieve only documents that contain drinks.
With my understanding firebase compound queries are not working for this kind of cases, I resolved the issue by changing the object with javascript map helper and update the document with return data of the function (changeExpDate), as follow:
changeExpDate(data, name, type, expDate) {
data = [data];
data.map((obj) => {
let subscriptions = obj.subscriptions;
for (var i = 0; i < subscriptions.length; i++) {
let items = obj.subscriptions[i].data;
for (var j = 0; j < items.length; j++) {
if (obj.subscriptions[i].name == name) {
if (items[j].type == type) {
obj.subscriptions[i].data[j].expDate = expDate;
}
}
}
}
});
return data;
}
By considering your conditions, got with following code:
let v = this.changeExpDate(
data, //The document objcet
productName, //Product Name
itemType, //Item type
expiration //Expiration date
);
try {
fb.db
.collection(this.dbName)
.doc(email)
.update(v[0])
.then(function () {
console.log("Updated!");
})
.catch(function (error) {
console.error("Error Updating: ", error);
});
} catch (error) {
console.error(error);
}

How to populate an array inside a map function in js and send it to the server?

This is my ObjectIds array -
obj_ids = [
"5ee71cc94be8d0180c1b63db",
"5ee71c884be8d0180c1b63d9",
"5ee71c494be8d0180c1b63d6",
"5ee71bfd4be8d0180c1b63d4"
]
I am using these objectids to serach whether they exist in the db or not and based on that I want to send the response to server.
This is the code I am trying but I dont know how to populate the array and send it to the server.
var msg = [];
obj_ids.map((ele) => {
Lead.find({ _id: ele._id }, async function (error, docs) {
if (docs.length) {
msg.push(
`Lead already exist for Lead id - ${ele._id} assgined to ${docs[0].salesPerson}`
);
} else {
msg.push(`Lead doesn't exist for Lead id: ${ele._id}`);
const newDuty = new AssignedDuty({
duty: ele._id,
salesPerson: req.body.salesPerson,
});
await newDuty.save();
}
});
});
res.json(msg);
By doing this approach I am getting an empty array. I cannot put res.json(msg) inside the loop. If it is possible by using async-await, please guide me through.
You don't need to make multiple queries to find whether given object ids exist in the database.
Using $in operator, you can make one query that will return all the documents where the _id is equal to one of the object id in the list.
const docs = await Lead.find({
_id: {
$in: [
"5ee71cc94be8d0180c1b63db",
"5ee71c884be8d0180c1b63d9",
"5ee71c494be8d0180c1b63d6",
"5ee71bfd4be8d0180c1b63d4"
]
}
});
After this query, you can check which object id is present in the docs array and which is absent.
For details on $in operator, see $in comparison operator
Your code can be simplified as shown below:
const obj_ids = [
"5ee71cc94be8d0180c1b63db",
"5ee71c884be8d0180c1b63d9",
"5ee71c494be8d0180c1b63d6",
"5ee71bfd4be8d0180c1b63d4"
];
const docs = await Lead.find({
_id: { $in: obj_ids }
});
const msg = [];
obj_ids.forEach(async (id) => {
const doc = docs.find(d => d._id == id);
if (doc) {
msg.push(
`Lead already exist for Lead id - ${doc._id} assgined to ${doc.salesPerson}`
);
}
else {
msg.push(`Lead doesn't exist for Lead id: ${id}`);
const newDuty = new AssignedDuty({
duty: id,
salesPerson: req.body.salesPerson
});
await newDuty.save();
}
});
res.json(msg);

How do I get documents from Firebase / Firestore ordered by property value

I have 6 documents in a "categories" collection that look like:
{
color: "#eee",
index: 6,
name: "Restaurants",
}
I want to retrieve these documents ordered by the index property, ordered from least to most. I am able to get all the documents, but I'm not sure how to order the results by index. I am also not sure whether I should query them from Firestore and then only order them once they are on the client.
Today my query looks like:
var db = firebase.firestore();
db.collection("categories").get().then((querySnapshot) => {
let categories = []
querySnapshot.forEach((doc) => {
categories.push(doc.data())
});
this.setState({categories: categories});
});
Thank you!
Hi astrojams1 great question. Modify your query as so:
db.collection("categories").orderBy("index")
.get()
.then((querySnapshot) => {
let categories = []
querySnapshot.forEach((doc) => {
categories.push(doc.data())
});
this.setState({categories: categories});
});

What is Firebase Firestore 'Reference' data type good for?

I'm just exploring the new Firebase Firestore and it contains a data type called reference. It is not clear to me what this does.
Is it like foreign key?
Can it be used to point to a collection that is located somewhere else?
If reference is an actual reference, can I use it for queries? For example can I have a reference that points directly to the user, instead of storing the userId in a text field? And can I use this user reference for querying?
Adding below what worked for me using references in Firestore.
As the other answers say, it's like a foreign key. The reference attribute doesn't return the data of the reference doc though. For example, I have a list of products, with a userRef reference as one of the attributes on the product. Getting the list of products, gives me the reference of the user that created that product. But it doesn't give me the details of the user in that reference. I've used other back end as a services with pointers before that have a "populate: true" flag that gives the user details back instead of just the reference id of the user, which would be great to have here (hopefully a future improvement).
Below is some example code that I used to set the reference as well as get the collection of products list then get the user details from the user reference id given.
Set a reference on a collection:
let data = {
name: 'productName',
size: 'medium',
userRef: db.doc('users/' + firebase.auth().currentUser.uid)
};
db.collection('products').add(data);
Get a collection (products) and all references on each document (user details):
db.collection('products').get()
.then(res => {
vm.mainListItems = [];
res.forEach(doc => {
let newItem = doc.data();
newItem.id = doc.id;
if (newItem.userRef) {
newItem.userRef.get()
.then(res => {
newItem.userData = res.data()
vm.mainListItems.push(newItem);
})
.catch(err => console.error(err));
} else {
vm.mainListItems.push(newItem);
}
});
})
.catch(err => { console.error(err) });
References are very much like foreign keys.
The currently released SDKs cannot store references to other projects. Within a project, references can point to any other document in any other collection.
You can use references in queries like any other value: for filtering, ordering, and for paging (startAt/startAfter).
Unlike foreign keys in a SQL database, references are not useful for performing joins in a single query. You can use them for dependent lookups (which seem join like), but be careful because each hop will result in another round trip to the server.
For those looking for a Javascript solution to querying by reference - the concept is that, you need to use a 'document reference' object in the query statement
teamDbRef = db.collection('teams').doc('CnbasS9cZQ2SfvGY2r3b'); /* CnbasS9cZQ2SfvGY2r3b being the collection ID */
//
//
db.collection("squad").where('team', '==', teamDbRef).get().then((querySnapshot) => {
//
}).catch(function(error) {
//
});
(Kudos to the answer here: https://stackoverflow.com/a/53141199/1487867)
According to the #AskFirebase https://youtu.be/Elg2zDVIcLo?t=276
the primary use-case for now is a link in Firebase console UI
A lot of answers mentioned it is just a reference to another document but does not return data for that reference but we can use it to fetch data separately.
Here is an example of how you could use it in the firebase JavaScript SDK 9, modular version.
let's assume your firestore have a collection called products and it contains the following document.
{
name: 'productName',
size: 'medium',
userRef: 'user/dfjalskerijfs'
}
here users have a reference to a document in the users collection. we can use the following code segment to get the product and then retrieve the user from the reference.
import { collection, getDocs, getDoc, query, where } from "firebase/firestore";
import { db } from "./main"; // firestore db object
let productsWithUser = []
const querySnaphot = await getDocs(collection(db, 'products'));
querySnapshot.forEach(async (doc) => {
let newItem = {id: doc.id, ...doc.data()};
if(newItem.userRef) {
let userData = await getDoc(newItem.userRef);
if(userData.exists()) {
newItem.userData = {userID: userData.id, ...userData.data()}
}
productwithUser.push(newItem);
} else {
productwithUser.push(newItem);
}
});
here collection, getDocs, getDoc, query, where are firestore related modules we can use to get data whenever necessary. we use user reference returned from the products document directly to fetch the user document for that reference using the following code,
let userData = await getDoc(newItem.userRef);
to read more on how to use modular ver SDK refer to official documentation to learn more.
If you don't use Reference data type, you need to update every document.
For example, you have 2 collections "categories" and "products" and you stored the category name "Fruits" in categories to every document of "Apple" and "Lemon" in products as shown below. But, if you update the category name "Fruits" in categories, you also need to update the category name "Fruits" in every document of "Apple" and "Lemon" in products:
collection | document | field
categories > 67f60ad3 > name: "Fruits"
collection | document | field
products > 32d410a7 > name: "Apple", category: "Fruits"
58d16c57 > name: "Lemon", category: "Fruits"
But, if you store the reference of "Fruits" in categories to every document of "Apple" and "Lemon" in products, you don't need to update every document of "Apple" and "Lemon" when you update the category name "Fruits" in categories:
collection | document | field
products > 32d410a7 > name: "Apple", category: categories/67f60ad3
58d16c57 > name: "Lemon", category: categories/67f60ad3
This is the goodness of Reference data type.
Belatedly, there are two advantages from this blog:
if I expect that I'll want to order restaurant reviews by rating, or publish date, or most upvotes, I can do that within a reviews subcollection without needing a composite index. In the larger top level collection, I'd need to create a separate composite index for each one of those, and I also have a limit of 200 composite indexes.
I wouldn't have 200 composite indices but there are some constraints.
Also, from a security rules standpoint, it's fairly common to restrict child documents based on some data that exists in their parent, and that's significantly easier to do when you have data set up in subcollections.
One example would be restricting to insert a child collection if the user doesn't have the privilege in the parent's field.
2022 UPDATE
let coursesArray = [];
const coursesCollection = async () => {
const queryCourse = query(
collection(db, "course"),
where("status", "==", "active")
)
onSnapshot(queryCourse, (querySnapshot) => {
querySnapshot.forEach(async (courseDoc) => {
if (courseDoc.data().userId) {
const userRef = courseDoc.data().userId;
getDoc(userRef)
.then((res) => {
console.log(res.data());
})
}
coursesArray.push(courseDoc.data());
});
setCourses(coursesArray);
});
}
UPDATE 12/18/22 - I put this in a package.
Original Blog Post
What this package does is use RXJS to loop through each field in a document. If that document type is a Reference type, then it grabs that foreign document type. The collection version grabs the foreign key value for each reference field in all documents in your collection. You can also input the fields manually that you wish to parse to speed up the searching (see my post). This is definitely not as efficient as doing manual aggregations with Firebase Functions, as you will get charged for lots of reads for each document you read, but it could come in handy for people that want a quick way to join data on the frontend.
This could also come in handy if you cache data and really only need to do this once.
J
install
npm i j-firebase
import
import { expandRef, expandRefs } from 'j-firebase';
https://github.com/jdgamble555/j-firebase
Original Post
Automatic JOINS:
DOC
expandRef<T>(obs: Observable<T>, fields: any[] = []): Observable<T> {
return obs.pipe(
switchMap((doc: any) => doc ? combineLatest(
(fields.length === 0 ? Object.keys(doc).filter(
(k: any) => {
const p = doc[k] instanceof DocumentReference;
if (p) fields.push(k);
return p;
}
) : fields).map((f: any) => docData<any>(doc[f]))
).pipe(
map((r: any) => fields.reduce(
(prev: any, curr: any) =>
({ ...prev, [curr]: r.shift() })
, doc)
)
) : of(doc))
);
}
COLLECTION
expandRefs<T>(
obs: Observable<T[]>,
fields: any[] = []
): Observable<T[]> {
return obs.pipe(
switchMap((col: any[]) =>
col.length !== 0 ? combineLatest(col.map((doc: any) =>
(fields.length === 0 ? Object.keys(doc).filter(
(k: any) => {
const p = doc[k] instanceof DocumentReference;
if (p) fields.push(k);
return p;
}
) : fields).map((f: any) => docData<any>(doc[f]))
).reduce((acc: any, val: any) => [].concat(acc, val)))
.pipe(
map((h: any) =>
col.map((doc2: any) =>
fields.reduce(
(prev: any, curr: any) =>
({ ...prev, [curr]: h.shift() })
, doc2
)
)
)
) : of(col)
)
);
}
Simply put this function around your observable and it will automatically expand all reference data types providing automatic joins.
Usage
this.posts = expandRefs(
collectionData(
query(
collection(this.afs, 'posts'),
where('published', '==', true),
orderBy(fieldSort)
), { idField: 'id' }
)
);
Note: You can also now input the fields you want to expand as a second argument in an array.
['imageDoc', 'authorDoc']
This will increase the speed!
Add .pipe(take(1)).toPromise(); at the end for a promise version!
See here for more info. Works in Firebase 8 or 9!
Simple!
J

Categories