Handling Users with MongoDB Stitch App within Atlas Cluster - reactjs

I have an MongoDB Stitch app, that users the Email/Password authentication. This creates users within the Stitch App that I can authenticate on the page. I also have an MongoDB Atlas Cluster for my database. In the cluster I have a DB with the name of the project, then a collection underneath that for 'Matches'. So when I insert the 'Matches' into the collection, I can send the authenticated user id from Stitch, so that I have a way to query all Matches for a particular User. But how can I add additional values to the 'User' collection in stitch? That user section is sort of prepackaged in Stitch with whatever authentication type you choose (email/password). But for my app I want to be able to store something like a 'MatchesWon' or 'GamePreference' field on the 'User' collection.
Should I create a collection for 'Users' the same way I did for 'Matches' in my Cluster and just insert the user id that is supplied from Stitch and handle the fields in that collection? Seems like I would be duplicating the User data, but I'm not sure I understand another way to do it. Still learning, I appreciate any feedback/advice.

There isn't currently a way to store your own data on the internal user objects. Instead, you can use authentication triggers to manage users. The following snippet is taken from these docs.
exports = function(authEvent){
// Only run if this event is for a newly created user.
if (authEvent.operationType !== "CREATE") { return }
// Get the internal `user` document
const { user } = authEvent;
const users = context.services.get("mongodb-atlas")
.db("myApplication")
.collection("users");
const isLinkedUser = user.identities.length > 1;
if (isLinkedUser) {
const { identities } = user;
return users.updateOne(
{ id: user.id },
{ $set: { identities } }
)
} else {
return users.insertOne({ _id: user.id, ...user })
.catch(console.error)
}
};

MongoDB innovates at a very fast pace - and while in 2019 there wasn't a way to do this elegantly, now there is. You can now enable custom user data on MongoDB realm! (https://docs.mongodb.com/realm/users/enable-custom-user-data/)
https://docs.mongodb.com/realm/sdk/node/advanced/access-custom-user-data
const user = context.user;
user.custom_data.primaryLanguage == "English";
--
{
id: '5f1f216e82df4a7979f9da93',
type: 'normal',
custom_data: {
_id: '5f20d083a37057d55edbdd57',
userID: '5f1f216e82df4a7979f9da93',
primaryLanguage: 'English',
},
data: { email: 'test#test.com' },
identities: [
{ id: '5f1f216e82df4a7979f9da90', provider_type: 'local-userpass' }
]
}
--
const customUserData = await user.refreshCustomData()
console.log(customUserData);

Related

How can I map data of multiple collections in snapshot?

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.

Stripe webhook checkout.session items to Supabase

Im using next.js and Stripe webhooks to insert checkout sessions to Supabase that will create a customer's order history. I'm able to get the information about the whole order written to a table called 'orders', but am wondering what the best way to add individual items within each checkout session to another table called 'order_items' is. This way I can map through main orders and then the children items. Appreciate any help provided. Here is what I have for getting orders associated with a customer:
const upsertOrderRecord = async (session: Stripe.Checkout.Session, customerId: string) => {
const { data: customerData, error: noCustomerError } = await supabaseAdmin
.from<Customer>('customers')
.select('id')
.eq('stripe_customer_id', customerId)
.single();
if (noCustomerError) throw noCustomerError;
const { id: uuid } = customerData || {};
const sessionData: Session = {
id: session.id,
amount_total: session.amount_total ?? undefined,
user_id: uuid ?? undefined
};
const { error } = await supabaseAdmin.from<Session>('orders').insert([sessionData], { upsert: true });
if (error) throw error;
console.log(`Product inserted/updated: ${session.id}`);
};
The Checkout Session object contains a line_items field which is a list of each item included in the purchase.
However this field is not included in the object by default, and therefore won't be a part of your webhook payload. Instead you'll need to make an API call in your webhook handle to retrieve the Checkout Session object, passing the expand parameter to include the line_items field:
const session = await stripe.checkout.sessions.retrieve('cs_test_xxx', {
expand: ['line_items']
});

pull operation in mongoose is taking several attempts to delete a sub document

i am developing a todolist application so that for each user i store the activities of a user in an array. schema of user look like this.
const activitySchema=new mongoose.Schema({
activity:String
})
const Activity=new mongoose.model("activity",activitySchema)
const userSchema=new mongoose.Schema({
username:String,
password:String,
activities:[],
});
i used pull command to delete a specific activity from user actvities list
app.post("/delete",(req,res)=>{
User.updateOne( {username: req.user.username}, { $pull: { activities: { _id: req.body.activity} }
}, function(err, model){
if(err)
console.log(err);
else {
console.log(model)
res.redirect("/")
}
})
})
this code is working after user clicks for 5-10 times on delete button but not immediately. the log output showing matched document:1 and modified document:0. kindly assist
For Modifying the document you need to save the model after making every change to the model.
activity.save().then(()=>{ console.log("Document Saved"))
Similar is the for the other document where you are making a change, You can read more about it if you want Mongoose

Updating state when database gets updated

I got a schema looking something like this:
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
//Create Schema
const PhoneNumbersSchema = new Schema({
phone_numbers: {
phone_number: 072382838232
code: ""
used: false
},
});
module.exports = PhoneNumbers = mongoose.model(
"phonenumbers",
PhoneNumbersSchema
);
And then I got an end-point that gets called from a 3rd party application that looks like this:
let result = await PhoneNumbers.findOneAndUpdate(
{ country_name: phoneNumberCountry },
{ $set: {"phone_numbers.$[elem1].services.$[elem2].sms_code": 393} },
{ arrayFilters: [ { "elem1.phone_number": simNumberUsed }, { "elem2.service_name": "steam" } ] },
Basically the end-point updates the "code" from the phone numbers in the database.
In react this is how I retrieve my phone numbers from the state:
const phonenumbers_database = useSelector((state) => {
console.log(state);
return state.phonenumbers ? state.phonenumbers.phone_numbers_details : [];
});
Every time the code gets changed in my database from the API call I would like to update "phonenumbers_database" in my state automatically.
How would I be able to do that?
MongoDB can actually watch for changes to a collection or a DB by opening a Change Stream.
First, you would open up a WebSocket from your React app to the server using something like Socket.io, and then watch for changes on your model:
PhoneNumbers
.watch()
.on('change', data => socket.emit('phoneNumberUpdated', data));
Your third party app will make the changes to the database to your API, and then the changes will be automatically pushed back to the client.
You could do a polling and check the Database every N secs or by using change streams
After that, to notify your frontend app, you need to use WebSockets, check on Socket IO

Comparing results from two API calls and returning their difference in MEAN app

EDIT: Since I wasn't able to find a correct solution, I changed the
application's structure a bit and posted another question:
Mongoose - find documents not in a list
I have a MEAN app with three models: User, Task, and for keeping track of which task is assigned to which user I have UserTask, which looks like this:
const mongoose = require("mongoose");
const autopopulate = require("mongoose-autopopulate");
const UserTaskSchema = mongoose.Schema({
completed: { type: Boolean, default: false },
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
autopopulate: true
},
taskId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Task",
autopopulate: true
}
});
UserTaskSchema.plugin(autopopulate);
module.exports = mongoose.model("UserTask", UserTaskSchema);
In my frontend app I have AngularJS services and I already have functions for getting all users, all tasks, and tasks which are assigned to a particular user (by getting all UserTasks with given userId. For example:
// user-task.service.js
function getAllUserTasksForUser(userId) {
return $http
.get("http://localhost:3333/userTasks/byUserId/" + userId)
.then(function(response) {
return response.data;
});
}
// task-service.js
function getAllTasks() {
return $http.get("http://localhost:3333/tasks").then(function(response) {
return response.data;
});
}
Then I'm using this data in my controllers like this:
userTaskService
.getAllUserTasksForUser($routeParams.id)
.then(data => (vm.userTasks = data));
...and because of autopopulate plugin I have complete User and Task objects inside the UserTasks that I get. So far, so good.
Now I need to get all Tasks which are not assigned to a particular User. I guess I should first get all Tasks, then all UserTasks for a given userId, and then make some kind of difference, with some "where-not-in" kind of filter.
I'm still a newbie for all the MEAN components, I'm not familiar with all those then()s and promises and stuff... and I'm really not sure how to do this. I tried using multiple then()s but with no success. Can anyone give me a hint?
You can do at server/API side that will more efficient.
In client side, if you want to do then try below
var userid = $routeParams.id;
userTaskService
.getAllTasks()
.then((data) => {
vm.userTasks = data.filter(task => task.userId !== userid)
});

Resources