Update Cloud Firestore Collection Based on "Master Collection" - swift

I am creating an iOS application to help users who collect figurines keep track of their figurines and see realtime price information.
I have two collections:
The "master" collection
This is essentially a catalog of thousands of figurines users can select to add to their collection. It has information about each item, such as Artist, Original Price, Barcode, Year, Comments, etc.
The "user" collection
This contains documents for each user. Each of these "user" documents has a sub-collection with documents for each figurine they have added to their collection.
Currently, if a user wants to add a new item to their personal collection, they go to the "master" collection UITableView and swipe to add an item. The item document is basically copied from the "master" collection and a new document in the user sub-collection is created. They can then go to a UITableView that displays the items they have added. They have the option to add their own information about the item, such as comments, date bought, date sold, price bought, etc.
My question: How can I sync information between the document in the user collection/sub-collection and the document in the main collection? Say the "Market Price" for an item has gone up, so I update the main collection database. How can I get this price change to display in all users' collections?
I assume by using Cloud Functions, but I wanted to post here in case there was an easier method.

This is definitely a good use for Cloud Functions. Create an onUpdate trigger for your /users{uid}/figurines/{figurineId} collection and then use that to copy the updated data to the master.
You can either copy specific fields or you can check for differences by examining event.data.data() and event.data.previous.data()
const admin = require('firebase-admin');
const functions = require('firebase-functions');
admin.initializeApp(functions.config().firebase);
const db = admin.firestore();
exports.updateMaster = functions.firestore
.document('users/{uid}/figurines/{figurineId}')
.onUpdate(event => {
var newData = event.data.data();
var oldData = event.data.previous.data();
var updateData = {};
if (oldData.price != newData.price) {updateData.price = newData.price}
// Add any more changes that you may want to compare / update below...
// Then...
return db
.collection('master')
.doc(event.params.figurineId)
.update(updateData).then(response => {
console.log('We updated figurine', event.params.figurineId);
}).catch(err => {
console.error('There was an error', err);
});
});

Related

How do I only return my primary keys from a Firebase RTDB and not the rest of the data stored when using a get() in Flutter?

My menu item tree looks is shown below:
menuItem
J1
-description:"Tasty milk shake!"
-img:"assets/images/milkshake.JPG"
-itemName:"Milk Shake"
-price:20
-varieties
-var1:"Chocolate"
-var2:"Vanilla"
-var3:"Strawberry"
I want to get just the item IDs (J1, J2, J3 ect.) but not all the information such as 'itemName'
final DatabaseReference _dbRef = FirebaseDatabase.instance.ref();
final items = await _dbRef.child('menuItem').get();
if (items.exists) {
String? itemID = items.value.toString();
}
items.values returns all the information for an item and items.key returns only 'menuItem'.
How can I just get the IDs only?
With the Realtime Database queries done via the Client SDKs are deep: They always return the entire subtree.
This is a key difference with Firestore for which queries are shallow: They only return documents in a particular collection or collection group and do not return subcollection data.
However, with the Realtime Database REST API you can use a query parameter named shallow, which "limits the depth of the data returned at a location". I've never used it but it seems that it will fulfill your requirement.
Another solution would to denormalise your data and maintain, in parallel to the menu items, a list of menu IDs in a specific DB node.
As Renaud explained in his answer, all read operations in the Firebase Realtime Database SDKs return complete branches of the tree, and can't be used to just read the keys.
That said, you can use just the keys from the data you read with:
final DatabaseReference _dbRef = FirebaseDatabase.instance.ref();
final items = await _dbRef.child('menuItem').get();
items.forEach((child) => {
console.log(child.key);
})
The above will still retrieve the entire menuItem branch of your database, but only show the keys under that node (so J1 from the sample you shared).

How to approach dynamically generated Firestore queries depending on nested user created Sub Collections?

/Countries/Lebanon/Governorates/Mount Lebanon/Districts/Chouf/Cities/Wadi al-Zayneh/Data/Products/Main Categories/Restaurants & Bakeries/Sub Categories/Snack/Sub Categories/Abo Arab Cafe
So as you can see, this is a snippet from my current Firestore structure. So many deeply nested collections. The issue is, I want to keep going deeper as long as a collection called 'Sub Categories' is found which in that case I would render them in the UI. And when eventually I reach a level where 'Sub Categories' is not found, I will render a different UI and show the actual products (The last document "Abo Arab Cafe" contains all the products as maps). The pattern of how many Sub Categories there are is unexpectable and can be modified by the end user.
How can I keep checking for Sub Categories? How to manage my queries in a way that they are dynamically generated at each level at the client-side?
I use Flutter. Here is my current queries structure:
import 'package:cloud_firestore/cloud_firestore.dart';
class FirebaseServices {
final FirebaseFirestore _db = FirebaseFirestore.instance;
CollectionReference mainCategoryCollectionReference() {
CollectionReference mainCategoryCollectionReference = _db.collection(
'/Countries/Lebanon/Governorates/Mount Lebanon/Districts/Chouf/Cities/Wadi al-Zayneh/Data/Products/Main Categories');
return mainCategoryCollectionReference;
}
CollectionReference subCategoryCollectionReference(
String parentSelectedCategory) {
CollectionReference mainCategoryCollectionReference = _db.collection(
'/Countries/Lebanon/Governorates/Mount Lebanon/Districts/Chouf/Cities/Wadi al-Zayneh/Data/Products/Main Categories/$parentSelectedCategory/Sub Categories');
return mainCategoryCollectionReference;
}
bool checkIfSubCategoriesExist(CollectionReference collectionReference) {
bool subCategoriesExist;
collectionReference.get().then((value) => {
subCategoriesExist = value.docs.isNotEmpty,
print('SubCategoriesExist: $subCategoriesExist')
});
return subCategoriesExist;
}
}
This works only if I know for certain how many levels of deepness there are, but since this can be modified by the user, it won't work.
Sorry for the very long question I had no idea how to explain it properly and clearly. Thank you in advance!
The structure is all wrong, there is no point in the structure being this deeply nested. The structure of the database needs to match what has to appear in the UI.
Assuming this is a worldwide application since you are using countries then you have to do the following:
Collection
Document
Fields
Countries
Random ID
countryName - arrayOfDistrict- arrayOfGovernorates
3 Fields under each document id, containing information about the country.
Then regarding Resturants:
Collection
Document
Fields
SubCollection
subCollectionId
Fields
Resturant
Random ID
resturant_name- resturant_location - info_about_resturant
Menu
randomId
dish_name - price -...
The problem with your db structure is that it is very nested instead of making a flat structure and that right now you are harcoding the whole path.
Using the above structure, you can create a dropdown with list of countries if the user chooses Lebanon, then you get the districts and the governorates. Then you can do a call to get the resturants that are inside each district, since in the documents inside Resturant collection you can get location of each resturant and name.
After that on click of each resturant, you will get the data inside the subcollection that will contain the full menu.
I think I found the solution with the help of a friend!
Since the checkIfSubCategoriesExist function is always checking on the very last reached level(using the collectionReference argument) whether Sub Categories exists or not, he suggested that in case it does exist, I can append to its argument collectionReference the new "Sub Categories" String to the path as a variable! This way I can query on it and voila!

MongoDB pre save set field based on lookup logic

People register for an event. There are two collections in the database. One for new registrations coming in and one for the registrations of previous years. Both contain an email field as unique identifier.
I would like to know if its possible to check if a newly registered person has registered before in previous years. If so add a field, for example: returningCustomer: true. Otherwise add returningCustomer: false
I am using Mongoose and have a User model for new registrations. I don't have a model (yet) for previously registered users. Would that be neccesary? If it is possible to check if a person has registered before and a field can be added before saving, it might be handy to save the user to the returning customers collection immediatly as well.
I know it is possible to access the current document and collection using a pre save hook, but how about doing a lookup in another collection, write a bit of logic and add a field to the current document pre save?
userSchema.pre('save', function (doc, next) {
const exists = otherCollection.find({ email: doc.email });
exists ? doc.returningCustomer = true : doc.returningCustomer = false;
next();
});
You should have a model for the collection you want to lookup.
Then you can query the other collection before saving the current collection.
CurrentModel.pre('save', async function (next) {
const doc = await OtherModel.find({ field: this.field });
doc.length ? this.returningCustomer = false : this.returningCustomer = true;
next();
});

Query Firestore documents with role based security via Flutter

I´ve a role based data model on Firestore according to googles suggestion here: https://firebase.google.com/docs/firestore/solutions/role-based-access
Security rules are set up correctly and work fine. But now I´ve the problem on how to query for the roles.
This is my data model (one sample document):
id: "1234-5678-91234",
roles:
userId_1:"owner",
userId_2:"editor
title: "This is a sample document"
And this is my Firestore Query in Flutter which gets all documents for a specific user by its ID if the user has assigned the role "owner" for the document:
return firestore
.collection(path)
.where("roles.${user.firebaseUserId}", isEqualTo: "owner")
.snapshots().map((snapshot) {
return snapshot.documents.map((catalog) {
return SomeDocumentObject(...);
}).toList();
});
My problem now is, that I need some kind of "OR" clause - which does not exist as far as I know. The query above only retrieves documents for users with role "owner" but I need a query that also retrieves the document if the userId is associated with the role "editor".
I´ve tried "arrayContains:" which also doesn´t seem to work (cause it´s a map).
I´ve read about solutions with two independent queries which doesn´t sound like a good solution due to a lot of overhead.
Maybe someone of you have a hint for me? :)
Thanks & best,
Michael
Firestore doesn't currently have any logical OR operations. You'll have to perform two queries, one for each condition, and merge the results of both queries in the client app.
This is the final solution using RxDart, Observables and .combineLatest() - maybe it helps someone out there:
#override
Stream<List<Catalog>> catalogs(User user) {
// Retrieve all catalogs where user is owner
Observable<QuerySnapshot> ownerCatalogs = Observable(firestore
.collection(path)
.where("roles.${user.firebaseUserId}", isEqualTo: "owner")
.snapshots());
// Retrieve all catalogs where user is editor
Observable<QuerySnapshot> editorCatalogs = Observable(firestore
.collection(path)
.where("roles.${user.firebaseUserId}", isEqualTo: "editor")
.snapshots());
// Convert merged stream to list of catalogs
return Observable.combineLatest([ownerCatalogs, editorCatalogs],
(List<QuerySnapshot> snapshotList) {
List<Catalog> catalogs = [];
snapshotList.forEach((snapshot) {
snapshot.documents.forEach((DocumentSnapshot catalog) {
catalogs.add(Catalog(
id: catalog.documentID,
title: catalog.data['title'],
roles: catalog.data['roles'],
));
});
});
return catalogs;
}).asBroadcastStream();
}

create personal collections for logged in users

import {favRestaurants} from '/lib/collections';
import {Meteor} from 'meteor/meteor';
import {check} from 'meteor/check';
export default function () {
Meteor.methods({
'favRestaurants.create' (id, name, rating, priceLevel, type) {
check(id, String);
check(name, String);
check(rating, Number);
check(priceLevel, Number);
check(type, String);
const createdAt = new Date();
const restaurant = {id, name, rating, priceLevel, type, createdAt};
if(check(Meteor.user()) == null){
console.log('onlye logged in users can data');
}else{
FavRestaurants.insert(restaurant);
}
}
});
}
This is my insert method for adding data to the restaurants collections. When i console log the 'check(Meteor.user())' in the console i get null as output. By that logic you shouldn't be able to add data to the collection, although this is still possible.
I would also like to make the FavResaurants collection personal for each user. Iv'e tried to check if there is a user and then adding a collection in the main.js file on the client side.
Meteor.loggingIn(() => {
console.log('check for user method');
var restId = 0;
if(Meteor.user() != null){
console.log('created new collection for the user');
const FavRestaurants = new Mongo.Collection('favRestaurants' + restId);
}
restId++;
});
I dont get any output to console using this method, which i found in the meteor docs. Is the code in the wrong place? Any help is much appriciated.
According to the docs the Accounts.ui.config is the method i should use. But I'm not sure in code i should put it. So far the placement of this method has resulted in my application crashing.
Answering your first question, to allow only logged-in clients to access a method, you should use something like:
if (!Meteor.userId()) {
throw new Meteor.Error('403', 'Forbidden');
}
Now, I see you want a collection to store favorite restaurants for each user in client side. But as I see it, there'd be only one logged in user per client, so you don't need a separate collection for each user (as the collection is in each client), you can just refer the user with it's id, and then fetch a user's favorite restaurants by a query like:
FavRestaurants.find({user: Meteor.userId()});
Moreover, as the docs suggest, Meteor.loggingIn is a method which tells you if some user is in the process of logging in. What you are doing is over-riding it, which doesn't make sense.
You should do something like:
if (Meteor.loggingIn()) {
// Do your stuff
}
Hope it gives you more clarity.
Creating a collection per user is a bad approach.
Define your favRestaurants collection once and add a owner field in the restaurant document before insert.
Create a publish method to publish to client side the userid favrestaurant only.
One more thing, check your userid first in your Meteor method, it will avoid unnecessary proces.
Regs