I recently had an issue with Alamofire (More generally with asynchronous calls)
I have two models, Listings and Users. Listings contains a user's email, and I would also like to get the user's first and last name (I understand I could solve this in the backend as well, however, I would like to see if there is a frontend solution as this problem comes up for something more complicated as well)
Currently I'm making a GET request to get all listings, and I'm looping through them, and making another GET request to get firstname, lastname.
I need to wait to get the result of this get request, or at the minimum append it to my listings dictionary. Likewise, before I do anything else (Move on to the next screen of my app), I'd like to have all the listings be linked to a firstname, lastname. Because theres a loop, this specifically seems to cause some issues (ie if it was just two nested GET requests, it could be in a callback). Is there an easy way to get around this. I've attached psuedocode below:
GET Request to grab listings:
for each listing:
GET request to grab first_name, last_name
Once all listings have gotten first_name, last_name -> Load next page
The answer for your question is called a dispatch group
Dispatch groups can be entered and left only to execute some code when no code is currently inside the dispatch group.
GET Request to grab listings{
var downloadGroup = dispatch_group_create()
//Create a dispatch group
for each listing{
dispatch_group_enter(downloadGroup)
//Enter the dispatch group
GET request to grab first_name, last_name (Async){
dispatch_group_leave(downloadGroup)
//Leave the dispatch group
}
}
dispatch_group_notify(downloadGroup, dispatch_get_main_queue()) {
//Run this code when all GET requests are finished
}
}
As demonstrated by this code.
Source and interesting reading material about dispatching: Grand Central Dispatch Tutorial for Swift by Ray Wenderlich
With Scala-style futures and promises you can do something like this:
let future: Future<[Listing]> = fetchListings().flatMap { listings in
listings.traverse { listing in
fetchUser(listing.userId).map { user in
listing.userName = "\(user.firstName) \(user.lastName)"
return listing
}
}
}
The result of the above expression is a future whose value is an array of listings.
Print the listing's user name, once the above expression is finished:
future.onSuccess { listings in
listings.forEach {
print($0.userName)
}
}
Scala-style future and promise libraries: BrightFutures or FutureLib
Below a ready-to-use code example which you can paste into a playgrounds file to experiment with any of the above libraries (works in FutureLib, BrightFutures might require slight modifications).
import FutureLib
import Foundation
import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
class Listing {
let userId: Int
init(userId: Int) {
self.userId = userId
userName = ""
}
var userName: String
}
struct User {
let id: Int
let firstName: String
let lastName: String
init (_ id: Int, firstName: String, lastName: String) {
self.id = id
self.firstName = firstName
self.lastName = lastName
}
}
func fetchListings() -> Future<[Listing]> {
NSLog("start fetching listings...")
return Promise.resolveAfter(1.0) {
NSLog("finished fetching listings.")
return (1...10).map { Listing(userId: $0) }
}.future!
}
// Given a user ID, fetch a user:
func fetchUser(id: Int) -> Future<User> {
NSLog("start fetching user[\(id)]...")
return Promise.resolveAfter(1.0) {
NSLog("finished fetching user[\(id)].")
return User(id, firstName: "first\(id)", lastName: "last\(id)")
}.future!
}
let future: Future<[Listing]> = fetchListings().flatMap { listings in
listings.traverse { listing in
fetchUser(listing.userId).map { user in
listing.userName = "\(user.firstName) \(user.lastName)"
return listing
}
}
}
future.onSuccess { listings in
listings.forEach {
print($0.userName)
}
}
Here's a possible solution:
GET Request to grab listings:
var n = the number of listings
var i = 0 //the number of retrieved
for each listing:
GET request to grab first_name, last_name, callback: function(response){
assign first/last name using response.
i+=1
if(i==n)
load_next_page()
}
So what this does is keep a counter of how many firstname/lastname records you've fetched. Make sure that you handle cases where a call to get the name fails.
Or, as suggested in a comment on the question, you could use promises. They make async code like this much nicer.
Related
I am not too confident working with Firestore and have trouble with more complex API calls to get data. Usually I use SQL backends in my apps.
For the section that I am working on, I would like to combine three collections to get an array of ToDos with the involved users and the category the current user labelled this ToDo with. Every involved person can label the ToDo like they prefer, which makes things a little more complicated. Broken down the collections are structured as follows.
todo: Firestore Database Document
{
title: string,
involved: string[], //user ids
involvedCategory: string[] //category ids mapped by index to involved
}
(I tried to have an array of objects here instead of the two arrays, but it seems I would not be able to query the array for the current user´s ID, like mentioned here, so this is a workaround)
category: Firestore Database Document
{
title: string,
color: string
}
user: Firebase Authentication User
{
uid: string,
displayName: string,
photoURL: string,
...
}
THE GOAL
An array of ToDo items like this:
{
id: string,
title: string,
involved: User[],
category?: {
title: string,
color: string
}
}
As I am working with TypeScript, I created an interface to use a converter with. My code looks like this so far:
import {
DocumentData,
FirestoreDataConverter,
WithFieldValue,
QueryDocumentSnapshot,
SnapshotOptions,
query,
collection,
where,
} from 'firebase/firestore'
import { store } from '../firebase'
import { useCollectionData } from 'react-firebase-hooks/firestore'
import { User } from 'firebase/auth'
import { useCategories } from './categories'
import { useAuth } from '../contexts/AuthContext'
interface ToDo {
id: string
title: string
involved: User[]
category?: {
title: string
color: string
}
}
const converter: FirestoreDataConverter<ToDo> = {
toFirestore(todo: WithFieldValue<ToDo>): DocumentData {
return {} //not implemented yet
},
fromFirestore(
snapshot: QueryDocumentSnapshot,
options: SnapshotOptions
): ToDo {
const data = snapshot.data(options)
return {
id: snapshot.id,
title: data.title,
category: undefined, //?
involved: [], //?
}
},
}
export function useToDos() {
const { currentUser } = useAuth()
const { categories } = useCategories() //needed in converter
const ref = query(
collection(store, 'habits'),
where('involved', 'array-contains', currentUser.uid)
).withConverter(converter)
const [data] = useCollectionData(ref)
return {
todos: data,
}
}
Is there any way I can do this? I have a Hook that returns all of the user´s categories, but I obviously can´t call that outside the
useToDos-Hook. And creating the const in the hook does not help, either, as it results in an infinite re-render.
I know this is a long one, but does anyone have tips how I could approach this? Thanks in advance ^^
UPDATE:
I had to make two small adjustments to #ErnestoC ´s solution in case anyone is doing something similar:
First, I changed the calls for currentUser.id to currentUser.uid.
Afterwards I got the very missleading Firestore Error: PERMISSION_DENIED: Missing or insufficient permissions, which made me experiment a lot with my security rules. But that is not where the error originated. Debugging the code line by line, I noticed the category objects resolved by the promise where not correct and had a weird path with multiple spaces at the beginning and the end of their ids. When I removed them before saving them in the promises array, it worked. Although I do not see where the spaces came from in the first place.
promises.push(
getDoc(
doc(
store,
'categories',
docSnap.data().involvedCategory[userCatIndex].replaceAll(' ', '')
)
)
)
The general approach, given that Firestore is a NoSQL database that does not support server-side JOINS, is to perform all the data combinations on the client side or in the backend with a Cloud Function.
For your scenario, one approach is to first query the ToDo documents by the array membership of the current user's ID in the involved array.
Afterwards, you fetch the corresponding category document the current user assigned to that ToDo (going by index mapping between the two arrays). Finally, you should be able to construct your ToDo objects with the data.
const toDoArray = [];
const promises = [];
//Querying the ToDo collection
const q = query(collection(firestoreDB, 'habits'), where('involved', 'array-contains', currentUser.id));
const querySnap = await getDocs(q);
querySnap.forEach((docSnap) => {
//Uses index mapping
const userCatIndex = docSnap.data().involved.indexOf(currentUser.id);
//For each matching ToDo, get the corresponding category from the categories collection
promises.push(getDoc(doc(firestoreDB, 'categories', docSnap.data().involvedCategory[userCatIndex])));
//Pushes object to ToDo class/interface
toDoArray.push(new ToDo(docSnap.id, docSnap.data().title, docSnap.data().involved))
});
//Resolves all promises of category documents, then adds the data to the existing ToDo objects.
await Promise.all(promises).then(categoryDocs => {
categoryDocs.forEach((userCategory, i) => {
toDoArray[i].category = userCategory.data();
});
});
console.log(toDoArray);
Using the FirestoreDataConverter interface would not be that different, as you would need to still perform an additional query for the category data, and then add the data to your custom objects. Let me know if this was helpful.
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)
}
}
Perhaps I am mis-using onDisonnect(), but I looked at the example code on the firebase.blog and am doing my best.
When a user submits a user name, I call the code below, which adds the username to a firebase db. Then on disconnection, I want the username to be deleted from the db. This would mean that the db would only show users that are connected to the app at that moment in time.
I am doing it this way so I can then call the data and then map through the array to display currently logged-in users.
I have made two attempts in deleting the name, which you can see in the code below under con.onDisconnect().remove();, neither of which work the way I need. That said, if I log in once again from the same computer, the first user name replaces the second user name!
Here is my code
setName = e => {
e.preventDefault()
let name = this.state.name;
let connectedRef = firebase.database().ref('.info/connected');
connectedRef.on('value', function (snap) {
if (snap.val() === true) {
// Connected
let con = myConnectionsRef.push();
myConnectionsRef.set({
name
})
// On disconnect
con.onDisconnect().remove();
myConnectionsRef.orderByChild('name').equalTo(name).once('child_added', function (snapshot) {
snapshot.ref.remove();
// var nameRef = firebase.database().ref('users/'+name);
// nameRef.remove()
})
}
});
Where am I going wrong? Is there a better way to use onDisconnect? From the example on the fb forum, it isn't clear where I would put that block of code, hence why I am attempting to do it this way.
Thanks.
If I understand correctly what is your goal, you don't need to do
myConnectionsRef.orderByChild('name').equalTo(name).once('child_added', function (snapshot) {
snapshot.ref.remove();
// var nameRef = firebase.database().ref('users/'+name);
// nameRef.remove()
})
as the onDisconnect().remove() call will take care of that.
Also, as explained in the blog article you refer to (as well as shown in the doc):
The onDisconnect() call shall be before the call to set() itself. This is to
avoid a race condition where you set the user's presence to true and
the client disconnects before the onDisconnect() operation takes
effect, leaving a ghost user.
So the following code should do the trick:
setName = e => {
e.preventDefault()
let name = this.state.name;
const connectedRef = firebase.database().ref('.info/connected');
const usersRef = firebase.database().ref('users');
connectedRef.on('value', function (snap) {
if (snap.val() === true) {
// Connected
const con = usersRef.child(name); //Here we define a Reference
// When I disconnect, remove the data at the Database location corresponding to the Reference defined above
con.onDisconnect().remove();
// Add this name to the list of users
con.set(true); //Here we write data (true) to the Database location corresponding to the Reference defined above
}
});
The users node will display the list of connected users by name, as follows:
- users
- James: true
- Renaud: true
I have a get route on my server-side that responds with two random records from MongoDB. I currently have a couple records hard-wired as excluded records that will never be returned to the client.
app.get("/api/matchups/:excludedrecords", (req, res) => {
const ObjectId = mongoose.Types.ObjectId;
Restaurant.aggregate([
{
$match: {
_id: { $nin: [ObjectId("5b6b5188ed2749054c277f95"), ObjectId("50mb5fie7v2749054c277f36")] }
}
},
{ $sample: { size: 2 } }
]).
This works, but I don't want to hard-wire the excluded records, I want to dynamically pass the ObjectIds from the client side. I want the user to be able to exclude multiple records from the random query. I have an action creator that pushes the ObjectId the user wishes to exclude through a reducer so that it becomes part of the store, and the store is an array that includes all the ObjectIds of the records the user wishes to exclude. Here's my action that fetches the random records, taking the excluded records from the store as an argument:
export function fetchRecords(excludedrecords) {
const excludedarray = JSON.stringify(excludedrecords); // Don't currently have this, but feel like I need to.
const request =
axios.get(`http://localhost:3000/api/matchups/${excludedarray}`);
return {
type: "FETCH_MATCHUP_DATA",
payload: request
};
}
I have the sense I need to stringify the array on the client side and parse it on the server side, but I'm not sure how. I've started something like:
app.get("/api/matchups/:excludedrecords", (req, res) => {
const excludedRecords = JSON.parse(req.params.excludedrecords);
const ObjectId = mongoose.Types.ObjectId;
Restaurant.aggregate([
{
$match: {
_id: { $nin: [excludedRecords] } //
}
},
But how do I get ObjectId() to wrap around each record number that is passed in params? I've tried inserting the number on the client side into a template string, like ObjectId('${excludedrecord}'), which results in me passing an array that looks like what I want, but when it gets stringified and parsed it doesn't quite work out.
Sorry if this question is a bit messy.
First of all you should pass the array of string as a body of the http request and not as a part of the url. You do not pass an array parameter as part of the url or in the query string.
Second, you must transcode the to $nin: [ObjectId(excludedRecord1), ObjectId(excludedRecord2)]
at the server side.
Hope it helps!
I'm working with some arrays in node and I want to send if as one JSON object to the front-end. I use express to do this. I have a model called User where I find users based on their email. That email is provided in an array. I do get the user object but I can't create one JSON object out of them!
I have tried some middleware but that didn't give me any result! https://www.npmjs.com/package/node.extend
var users = {};
for (var i = 0; i <emails.length; i++) {
User.findOne({
'email': project.students[i]
}, function (err, user) {
if (err) {
res.send(err);
}
// Fill the users object with each user found based on the email
});
}
console.log(users); // Should be one JSONObject
Thanks for the help!
You should be able to do this in a single query. It looks like you're using mongoose, so try something like this:
User.find({ email: { $in: emails } }, function(err, results) {
if (err) return res.send(err);
res.send(results);
});
It's also worth noting that javascript is single threaded. This means that a lot of operations happen asynchronously, meaning you have to wait for the operation to get done before you can move on. Your console logging statement above doesn't wait for the database operation to complete. You have to wait for the callback function to execute.
UPDATE: Also just noticed that you are looping over emails but then using project.students[i] within each iteration. I can't see the rest of your code, but this is just buggy code. You should be either looping over project.students or using emails[i] within each iteration.
UPDATE 2: It appears that you are wanting to send more than just an array of user with the response. So the first goal is to use a single query using the $in operator (see example above - you should be able to pass a list of emails to mongoose). Anything mongo-related, you always want to reduce the amount of queries to the database if you care at all about performance. The second task is to reformat your users and other data accordingly:
var finalResponse = { token: "12341234", users: null };
User.find({ email: { $in: emails } }, function(err, results) {
if (err) return res.send(err);
if (!results.length) return res.send(finalResponse);
// OPTION 1: Array of users (recommended)
finalResponse.users = results;
// OPTION 2: users object, keyed by the users email
finalResponse.users = {};
results.forEach(function(user) {
finalResponse.users[user.email] = user;
});
// FINALLY, send the response
resp.send(finalResponse);
});