How to filter list of id on firebase cloud firestore? - firebase

I have two collections. (applyJobs and Jobs and users). When users apply for a job, I store that record inside applyJobs collection. Like this:
applyId:****,
jobId:*****,
userId:*****
Now, I want to show all apply for jobs by a user.
First: Get logged user id, I store locally logged user id. So, I can get loggged user id.
Second: I filter Apply Jobs by that id. like this, var ref = _db.collection('applyJobs').where('userId',isEqualTo: uid);. I here I didn't call users collection to get uid. because I already store uid on locally. Is it best practice?
Third: I store result here List<ApplyJobsModelClass>. I want to get all jobs by a list of id. How do I filter it?
This is way I tried it. But this is not list of IDs. only one id.
streamApplyJob(List<String> jobId) {
Collection('jobs').document(jobId);
}
And I tried this way too.
Stream<List<JobModel>> streamApplyJob(List<String> jobId) {
var ref = _db.collection('jobs').where('jobId',isEqualTo: jobId);
return ref.snapshots().map((list) =>
list.documents.map((doc) => JobModel.fromFirestore(doc)).toList());
}
tried to get length, but result is 0
db.streamApplyJob(jobIds).listen((v)=>{
print(v.length)
});
Full Code
Database side
///Get a stream of apply jobs
Stream<List<ApplyJobModel>> streamApplyJobs(String uid) {
var ref = _db.collection('applyJobs').where('userId',isEqualTo: uid);
return ref.snapshots().map((list) =>
list.documents.map((doc) => ApplyJobModel.fromFirestore(doc)).toList());
}
///Get a stream of a single document
Stream<List<JobModel>> streamApplyJob(List<String> jobId) {
var ref = _db.collection('jobs').where('jobId',isEqualTo: jobId);
return ref.snapshots().map((list) =>
list.documents.map((doc) => JobModel.fromFirestore(doc)).toList());
}
calling
List<String> jobIds = [];
void getData() {
db.streamApplyJobs(widget.uid).listen((listApplies) => {
for (int i = 0; i < listApplies.length; i++)
{jobIds.add(listApplies[i].jobId)},
});
db.streamApplyJob(jobIds).listen((v)=>{
print(v.length)
});
}
Solution(It's working now)- Is it best practice or are there other best way to do this?
Future<List<JobModel>> getJobs() async {
await db.streamJobs(true).listen((jobs) {
setState(() {
jobModel = jobs;
});
});
return jobModel;
}
Future getData() async {
await getJobs();
db.streamApplyJobs(widget.uid).listen((apply) => {
for (int j = 0; j < jobModel.length; j++)
{
for (int i = 0; i < apply.length; i++)
{
if (apply[i].jobId == jobModel[j].jobId)
{
jobModelNew.add(jobModel[j]),
}
}
}
});
}

I want to get all jobs by a list of id. How do I filter it?
There currently is no way to pass in a list of IDs to a Firestore query and get documents matching all those IDs. See Google Firestore - how to get document by multiple ids in one round trip? (which talks about doing this with document IDs), and Firebase Firestore - OR query (which talks about filtering for multiple values on a single field).
Unless your use-case happens to match the workaround mentioned in that second answer, you'll have to perform a separate query for each value, and merge the results in your application code.

Not sure if it is documented anywhere officially, but this is possible now!
.where(admin.firestore.FieldPath.documentId(), "in", [array, of, ids])
Found here: https://stackoverflow.com/a/52252264/10562805

Please take a look at this example. It binds a CollectionReference to a List.
Let me know if this is helpful.

Related

Flutter: How to loop through each field in `DocumentSnapshot.data()`

I have a collection where each user has their own document. In the document I create a map for a new entry. The document would look something like this:
In my app, I want to get the document and then turn it into a list where each map (titled by the time) would be an entry. I tried looping through it but the problem is that there is not a forEach() in the DocumentSnapshot.data() and it is a type of _JsonDocumentSnapshot.
How can I get this as a List where 2021-05-30 20:49:59.671705, 2021-05-30 20:50:00:600294, ... are individual elements in a list. I plan on building each entry on its own using something such as a ListView.builder() so I would like to have a list of all those entries.
Edit:
My code looks something like this:
journal is just a DocumentReference.
journal.snapshots().forEach((element) {
var data = element.data();
print(element.data());
(data ?? {}).forEach((key, value) {
return JournalEntryData(
date: key,
entryText: value['entryText'],
feeling: value['feeling']);
});
});
The problem was that the type of data which was set to element.data() was Object? hence the forEach() method wasn't defined. I tried casting the element.data() as a List which caused errors so I didn't go further down that path. What I should have done was to cast element.data() as a Map which worked. I also should have used journal.get() and not snapshots() as it is a stream so the await keyword would result in the journal.snapshots().forEach() to never end.
The final code looked something like this:
Future<List<JournalEntryData>> getJournalEntries() async {
List<JournalEntryData> entries = [];
await journal.get().then((document) {
Map data = (document.data() as Map);
data.forEach((key, value) {
print('adding entry');
entries.add(JournalEntryData(
date: key,
entryText: value['entryText'],
feeling: value['feeling'],
));
});
});
return entries;
}
and I would do the following to get the entries:
var entries = await getJournalEntries();
Hey for most of my problems like these I choose to go with this approach;
final result = await _db
.collection("collectionName")
.get();
List<Object> toReturn = [];
for (int i = 0; i < result.docs.length; i++) {
// add data to list you want to return.
toReturn.add();
}
Hopefully, this will be helpful to you.
List collectionElements = [];
void messagesStream() async {
await for (var snapshot in _firestore.collection('your collection').snapshots().where(
(snapshot) => snapshot.docs
.every((element) => element.author == currentUser.id),
)) {
collectionElements.add(snapshot);
}
print(collectionElements);
}
Each element would be added to the collectionElements list.
If you want to access only one field in your snapshot this works fine:
final entryText= element.data()['entryText'];
print(entryText); // this would print "lorem ipsum..."

How can I update map data that's in a array in firebase? (Flutter)

I'm using flutter web and firebase for a project and got stuck on a problem. I'm trying to update a map in an array in firestore.
using this:
var val = [];
val.add({'groupUID': groupUID, 'inviteStatus': status});
var userInviteUID;
await users
.document(uid)
.get()
.then((value) => userInviteUID = value.data['inviteUID']);
await invites
.document(userInviteUID)
.updateData({'invites': FieldValue.arrayUnion(val)});
I got this result:
firestore structure
What I want to do is just change the 1 to a 2 in the map. I thought that it would update since its the same value but it just adds it to the array.
I looked around on stack and saw some ways to do it like copying the entire array and changing it where I need to, then adding it back.
But I wanted to know if there was a way to avoid that by adding some modifications to my code. Also let me know if there's a better structure I should use. Appreciate the help!
UPDATE:
var ref = invites.document(userData.inviteUID);
ref.get().then((value) async {
var invitesList = value.data['invites'];
switch (status) {
case 1:
break;
case 2:
var index;
invitesList.asMap().forEach((key, value) {
if (value['groupUID'] == groupUID) index = key;
});
invitesList.removeAt(index);
await invites
.document(userData.inviteUID)
.updateData({'invites': FieldValue.arrayUnion(invitesList)});
break;
default:
}
So I looked at some print statements and seen that the elements with the matching group uid is removed, but looking at firebase, the array isn't overwritten anything...any ideas?
FINAL UPDATE:
var ref = invites.document(userData.inviteUID);
ref.get().then((value) async {
var invitesList = value.data['invites'];
switch (status) {
case 1:
break;
case 2:
var index;
invitesList.asMap().forEach((key, value) {
if (value['groupUID'] == groupUID) index = key;
});
invitesList.removeAt(index);
await invites
.document(userData.inviteUID)
.setData({'invites': FieldValue.arrayUnion(invitesList)});
break;
default:
}
Fixed it by changing updateData to setData.
I looked around on stack and saw some ways to do it like copying the entire array and changing it where I need to, then adding it back.
That's exactly how you are supposed to modify the contents of arrays in Firestore documents. Firestore doesn't support updating array elements by index.
setData creates a new document if it already doesn't exist but if the document exists, the data will be overwritten. To prevent this from happening, you could use SetOptions(merge: true) if you wish to append the data:
set(someData, SetOptions(merge: true))
You can also use update method and provide it the updated data, the pseudo code could look like this:
List<Map<String, dynamic>> updatedList = [...];
Map<String, dynamic> updatedData = {
'existing_map': updatedList,
};
var collection = FirebaseFirestore.instance.collection('collection');
collection
.doc('doc_id')
.update(updatedData);

Firebase Realtime DB: Order query results by number of values for a key

I have a Firebase web Realtime DB with users, each of whom has a jobs attribute whose value is an object:
{
userid1:
jobs:
guid1: {},
guid2: {},
userid2:
jobs:
guid1: {},
guid2: {},
}
I want to query to get the n users with the most jobs. Is there an orderby trick I can use to order the users by the number of values the given user has in their jobs attribute?
I specifically don't want to store an integer count of the number of jobs each user has because I need to update users' jobs attribute as a part of atomic updates that update other user attributes concurrently and atomically, and I don't believe transactions (like incrementing/decrementing counters) can be a part of those atomic transactions.
Here's an example of the kind of atomic update I'm doing. Note I don't have the user that I'm modifying in memory when I run the following update:
firebase.database().ref('/').update({
[`/users/${user.guid}/pizza`]: true,
[`/users/${user.guid}/jobs/${job.guid}/scheduled`]: true,
})
Any suggestions on patterns that would work with this data would be hugely appreciated!
Realtime Database transactions run on a single node in the JSON tree, so it would be quite difficult to integrate the update of a jobCounter node within your atomic update to several nodes (i.e. to /users/${user.guid}/pizza and /users/${user.guid}/jobs/${job.guid}/scheduled). We would need to update at /users/${user.guid} level and calculate the counter value, etc...
An easier approach is to use a Cloud Function to update a user's jobCounter node each time there is a change to one of the jobs nodes that implies a change in the counter. In other words, if a new job node is added or removed, the counter is updated. If an existing node is only modified, the counter is not updated, since there were no change in the number of jobs.
exports.updateJobsCounter = functions.database.ref('/users/{userId}/jobs')
.onWrite((change, context) => {
if (!change.after.exists()) {
//This is the case when no more jobs exist for this user
const userJobsCounterRef = change.before.ref.parent.child('jobsCounter');
return userJobsCounterRef.transaction(() => {
return 0;
});
} else {
if (!change.before.val()) {
//This is the case when the first job is created
const userJobsCounterRef = change.before.ref.parent.child('jobsCounter');
return userJobsCounterRef.transaction(() => {
return 1;
});
} else {
const valObjBefore = change.before.val();
const valObjAfter = change.after.val();
const nbrJobsBefore = Object.keys(valObjBefore).length;
const nbrJobsAfter = Object.keys(valObjAfter).length;
if (nbrJobsBefore !== nbrJobsAfter) {
//We update the jobsCounter node
const userJobsCounterRef = change.after.ref.parent.child('jobsCounter');
return userJobsCounterRef.transaction(() => {
return nbrJobsAfter;
});
} else {
//No need to update the jobsCounter node
return null;
}
}
}
});

Async Await with four nested loops

I am currently trying to return a JSON object array that requires me to do one asynchronous function and then four nested asynchronous map functions in order to populate an array of entities. Basically, each user has an array of orders, each order has an array of items, each item has an array of options and each option has an array of values. I am using loopback4 framework and therefore cannot do res.send once all things have been populated. The function seems to return on the first await, but any await after that, it does not wait on it and instead runs to the end of the function. I have tried using Promises and .thens(), but cannot seem to figure out how to populate each entity fully nested, and then return the array of populated entities. I keep getting an empty array. Below is only one nest of maps, but I cannot get it to even populate up to the first nest and return this, so I decided not to go any further. This is the code:
async getUserOrders2(#param.path.number('id') id: number): Promise<any> {
if ( !this.user) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
}
else if (this.user.id != id) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
}
else {
let restaurantId = this.user.restaurantId
let orderFrameArray = new Array<OrderFrame>()
return this.restaurantRepository.orders(restaurantId as string).find()
.then(async orders => {
orders.map(async (val, key)=> {
let orderFrame = new OrderFrame(val)
orderFrame.itemArray = await this.orderRepository.orderItems(val.id).find()
orderFrameArray.push(orderFrame)
})
orderFrameArray = await Promise.all(orderFrameArray)
return orderFrameArray
})
}
}
The function is returning before the orderFrameArray has been populated. I need four nested map loops and this first one is not working, so I am not sure how to do the rest. Any help would be extremely appreciated.
Based on #Tomalaks solution I tried the following, but its still only returning the top level array and nothing is nested:
async getUserOrders2(#param.path.number('id') id: number): Promise<any> {
if ( !this.user) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
}
else if (this.user.id != id) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
}
else {
let restaurantId = this.user.restaurantId
let orderFrameArray = new Array<OrderFrame>()
return this.restaurantRepository.orders(restaurantId as string).find()
.then(orders => {Promise.all(orders.map(
order => {
let orderFrame = new OrderFrame(order)
orderFrame.itemArray = new Array<Item>()
this.orderRepository.orderItems(order.id).find()
.then(orderItems => Promise.all(orderItems.map(
orderItem => {
let itemFrame = new Item(orderItem)
itemFrame.options = new Array<Option>()
this.orderItemRepository.orderItemOptions(orderItem.id).find()
.then(orderItemOptions => Promise.all(orderItemOptions.map(
orderItemOption => {
let optionFrame = new Option(orderItemOption)
optionFrame.values = new Array<Value>()
this.orderItemOptionRepository.orderItemOptionValues(orderItemOption.id).find()
.then(orderItemOptionValues => Promise.all(orderItemOptionValues.map(
orderItemOptionValue => {
let valueFrame = new Value(orderItemOptionValue)
optionFrame.values.push(valueFrame)})))
itemFrame.options.push(optionFrame)})))
orderFrame.itemArray.push(itemFrame)})))
orderFrameArray.push(orderFrame)}))
return orderFrameArray})
}
}
I apologize for the formatting I wasn't sure how best to format it. Is there something else I'm doing wrong?
Thanks to everyone for their response. The answer that was posted by #Tomalak was correct. I just had to surround the entire function in brackets, and put a .then to return the populated entity I had made
You only need to use async when you are using await in the same function. If there's await in a nested function, the parent function does not need async.
However, in your case, there is no function that should be made async in the first place.
There is no benefit in awaiting any results in your function, because no code inside depends on any intermediary result. Just return the promises as you get them.
There's no need for intermediary result variables like orderFrameArray, you're making things harder than they are with your approach of awaiting individual orders and pushing them to a top-level variable.
Using await in a loop like you do inside your .map() call is bad for performance. You are basically serializing database access this way – the next query will only be sent after the current one has returned. This kind of daisy-chaining nullifies the database's ability to process multiple concurrent requests.
getUserOrders2 is not Promise<any>, it's Promise<Array<OrderFrame>>.
throw terminates the function anyway, you can do multiple checks for error conditions without using else if. This reduces nesting.
So a fully asynchronous function would look like this:
getUserOrders2(#param.path.number('id') id: number): Promise<Array<OrderFrame>> {
if (!this.user) throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
if (this.user.id != id) throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
return this.restaurantRepository
.orders(this.user.restaurantId).find().then(
orders => Promise.all(orders.map(
order => this.orderRepository.orderItems(order.id).find().then(
order => new OrderFrame(order)
)
))
);
}
The async/await equivalent of this function would be more complex.
You then would await the result in the calling code, as you would have to do anyway:
async test() {
const orders = await foo.getUserOrders2(someUserId);
// ...
}
// or
test() {
foo.getUserOrders2(someUserId).then(orders => {
// ...
});
}

Firestore get value of Field.increment after update without reading the document data

Is there a way to retrieve the updated value of a document field updated using firestore.FieldValue.increment without asking for the document?
var countersRef = db.collection('system').doc('counters');
await countersRef.update({
nextOrderCode: firebase.firestore.FieldValue.increment(1)
});
// Get the updated nextOrderCode without asking for the document data?
This is not cost related, but for reliability. For example if I want to create a code that increases for each order, there is no guaranty that if >= 2 orders happen at the same time, will have different codes if I read the incremental value right after the doc update resolves, because if >= 2 writes happen before the first read, then at least 2 docs will have the same code even if the nextOrderCode will have proper advance increment.
Update
Possible now, check other answer.
It's not possible. You will have to read the document after the update if you want to know the value.
If you need to control the value of the number to prevent it from being invalid, you will have to use a transaction instead to make sure that the increment will not write an invalid value. FieldValue.increment() would not be a good choice for this case.
We can do it by using Firestore Transactions, like incremental worked before Field.increment feature:
try {
const orderCodesRef = admin.firestore().doc('system/counters/order/codes');
let orderCode = null;
await admin.firestore().runTransaction(async transaction => {
const orderCodesDoc = await transaction.get(orderCodesRef);
if(!orderCodesDoc.exists) {
throw { reason: 'no-order-codes-doc' };
}
let { next } = orderCodesDoc.data();
orderCode = next++;
transaction.update(orderCodesRef, { next });
});
if(orderCode !== null) {
newOrder.code = orderCode;
const orderRef = await admin.firestore().collection('orders').add(newOrder);
return success({ orderId: orderRef.id });
} else {
return fail('no-order-code-result');
}
} catch(error) {
console.error('commitOrder::ERROR', error);
throw errors.CantWriteDatabase({ error });
}
Had the same question and looks like Firestore Python client
doc_ref.update() returns WriteResult that has transform_results attribute with the updated field value

Resources