Add, update and read sub-collection in firestore using Angular (#angular/fire) - javascript

I am trying to learn firebase. I am making a project like Linkedin using Angular and Firebase.
I am using the #angular/fire package in my project.
So far I have done authentication and adding some basic info of a user. I have users collection where each documentId have information like name, email and etc
Now my next goal is to create a work experience section. Since a user can have multiple work experience.
I have to decided to proceed with sub-collection under each user document id instead of creating a separate collection for work-experience.
Now I am having a bit trouble how to add sub-collection in my users collection.
A user can only add/update one experience at a time. My UI will be something similar to Linkedin. When the 'Add Experience' button is clicked a modal will pop up and there will be a form inside the form with fields such as jobTitle, companyName and etc. So once the submit is clicked I want to save that data under the work-experience sub-collection with a unique documentId.
Currently I am adding my basic info like this
Adding
addUser(user: any): Observable<void> {
const ref = doc(this.firestore, 'users', user.uid);
return from(setDoc(ref, user));
}
Updating
updateUser(user: any): Observable<void> {
const ref = doc(this.firestore, 'users', user.uid);
return from(updateDoc(ref, { ...user }));
}
Now I want to add sub-collection called work-experience under the users collection.

From your other question I think you need to add a sub-collection which contains a document with ID work-experience.
If my assumption is correct, do as follows to create the DocumentReference to this Document:
const workExperienceDocRef = doc(this.firestore, `users/${user.uid}/sub-sections/work-experience`);
Note: Make sure to use back ticks.
And then you can set the data of this doc as follows, for example:
return from(setDoc(workExperienceDocRef, {experiences: [{years: "2015-2018", company: "comp1"}, {years: "2018-2021", company: "comp2"}]}));
If you want to create a sub-collection for the user's document, do as follows:
const userSubCollection = collection(this.firestore, `users/${user.uid}/sub-collection`);
Then you can use the addDoc() method, which will automatically generate the document ID.:
const docRef = await addDoc(userSubCollection, {
foo: "bar",
bar: "foo"
});

Related

MongoDB - Insert 2 documents, each in another collection but share ObjectId

I have this question:
I want to insert 2 documents into 2 collections.
One is for user and one is for company.
Both inserts are requested via api.
In created(Inserted) company I want to know, which user[Created/inserted] create this company. And in user i want to have _id of company that he inserted.
User
{
_id: "userGeneratedId",
companyId : Company._id
}
Company
{
_id: "companyGeneratedId",
registeredByID : user._id
}
How can this be done?
Thank you,
Dufino
There are two ways to go about this
The first and easy way
Add new fields to your user and company model. maybe call it userSaveId or whatever you choose. Now you will insert same unique id to these new fields fields, so that when you are retrieving a company, you can just retrieve it based on that ID.
The second way this could be done is by performing 4 operations. 2 insert operations and two update operations. Note that this would slightly increase the response time of your operations.
Suppose you have inserted a user and company, get the IDs of both the user document and company document as such:
const user = User.save(yourData);
const company = Company.save(yourCompanyData);
Afterwards get the ids and use it to update the documents that are already stored as such:
const userId = user._id;
const companyId = company._id;
User.updateOne({_id: userId}, {$set: {companyId: companyId}});
Company.updateOne({_id: companyId}, {$set: {registeredByID: userId}});
So the complete code would be this:
const user = User.save(yourData);
const company = Company.save(yourCompanyData);
const userId = user._id;
const companyId = company._id;
User.updateOne({_id: userId}, {$set: {companyId: companyId}});
Company.updateOne({_id: companyId}, {$set: {registeredByID: userId}});

Supabase login get user session and profile at the same time

Is there a way to get the user session and profile at the same time? The way I did it would be get the user session first after login then fetch the user profile using the id.
const [authsession, setSession] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
const userSession = supabase.auth.session();
setSession(userSession);
if (userSession) {
getProfile(userSession.user.id);
} else {
setSession((s) => ({ ...s, profile: null }));
}
supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
if (session) {
getProfile(session.user.id);
} else {
setSession((s) => ({ ...s, profile: null }));
}
});
}, []);
const getProfile = async (id) => {
setLoading(true);
setError(false);
try {
const { data } = await supabase
.from("profiles")
.select("*")
.eq("id", id)
.single();
setSession((s) => ({ ...s, profile: data }));
} catch (error) {
setError(true);
} finally {
setLoading(false);
}
};
(In support of #chipilov's answer, but too long for a comment.)
Solution
When you get the user session at login time, Supabase will additionally return the JSON stored in auth.users.raw_user_meta_data. So, save the profile info there at signup time wit supabase.auth.signUp() or later with supabase.auth.updateUser(), you are all set.
Should you really store profile data in auth.users?
People seem to freak out a bit at the prospect of writing into auth.users, probably fearing that they might mess with Supabase internals. However, raw_user_meta_data is meant for this purpose: Supabase itself does not save anything into that column, only the additional user metadata that you may provide at signup or when updating a user.
Supabase maintainers do not recommend to write with own server-side routines to auth.users (source). But we don't do that here, relying only on the Supabase-provided functions supabase.auth.signUp() and supabase.auth.updateUser().
In the Supabase docs, they even provide an example where this "additional user metadata" is used for profile information:
const { data, error } = await supabase.auth.signUp(
{
email: 'example#email.com',
password: 'example-password',
options: {
data: {
first_name: 'John',
age: 27,
}
}
}
)
(Source: Supabase Documentation: JavaScript [Client Library] v2.0: AUTH: Create a new user, example "Sign up with additional user metadata")
How to access this profile data server-side?
The OP uses a table public.profiles to maintain profile information. For additional profile information that you generate and write to server-side, this is the recommended practice. Such a table is also recommended to make user data from auth.users accessible through the API:
GaryAustin1 (Supabase maintainer): "You could use the user meta data [field to store your additional metadata] and update it server side. The recommendation though normally is to have your own copy of key user info from the auth.users table […] [in] your own user table. This table is updated with a trigger function on auth.users inserts/updates/deletes." (source)
Instead of a set of trigger functions, you may also opt for a VIEW with SECURITY definer privileges to make auth.users data available in the public schema and thus via the API. Compared to triggers, it does not introduce redundancy, and I also think it's simpler. Such a view can also include your auth.users.raw_user_meta_data JSON split nicely into columns, as explored in this question. For security, be sure to include row access filtering via the WHERE clause of this view (instructions), because VIEWs cannot have their own row-level security (RLS) policies.
How to modify this profile data server-side?
Your own user table in the public schema can be used to store that additional profile data. It would be connected to auth.users with a foreign key relation. In the VIEW proposed above, you can then include both data from this own table and columns from auth.users.
Of course, user information from your own table will then not be returned automatically on login. If you cannot live with that, then I propose to alternatively use auth.users.raw_user_meta_data to save your additional metadata. I know I disagree with a Supabase maintainer here, but really, you're not messing with Supabase internals, just writing into a field that nothing in Supabase depends on.
You would use PostgreSQL functions (with SECURITY definer set) to provide access to specific JSON properties of auth.users.raw_user_meta_data from within the public schema. These functions can even be exposed to the API automatically (see).
If however you also need to prevent write access by users to some of your user metadata, then the own table solution from above is usually better. You could use triggers on auth.users.raw_user_meta_data, but it seems rather complex.
The session.user object contains a property called user_metadata - this property contains the data which is present in the raw_user_meta_data column of the of auth.users table.
Hence, you can setup a DB trigger on your custom user profile table which copies the data from that table to the raw_user_meta_data column of the of auth.users table in JSON format anytime the data in the user profile table changes (i.e. you will need the trigger to be run on INSERT/UPDATE and probably DELETE statements). This way the profile data will be automatically delivered to the client with the sign-in or token refresh events.
IMPORTANT: This approach has potential drawbacks:
this data will also be part of the JWT sent to the user. This might be a problem because the JWT is NOT easy to revoke BEFORE its expiration time and you might end up in a situation where a JWT which expires in 1 hour (and will be refreshed in 1 hour) still contains profile data which has already been updated on the server. The implications of this really depends on what you put in your profile data and how you use it in the client and/or the backend;
since this data is in the JWT, the data will be re-sent to the client with each refresh of the session (which is every 1 hour by default). So, (a) if the profile data is big, you would be sending this large piece of data on every token refresh even if it has NOT changed and (b) if the data changes often and you need to ensure that the client is up to date in (near) real time you will need to increase the token refresh rate;

Firestore query with only a field inside an array

This is the thing I want to accomplish: I'm building a web shop. The web shop has a React Front-end. The front-end fetches 5 collections from Firestore and displays all the items from the collection array on the shop page. A user selects an item on the shop page. I send the item fields such as (price, name, quantity, id) to my express server and the server makes a checkout session of the item fields. The user goes to a Stripe checkout form and is sent back to my front-end by Stripe when the payment is complete. I listen for that event on my server and when then want to update the quantity field of the item in Firestore.
But how do I query Firestore for this item? Is there a way to query Firestore with only this id field (or name field)? Some something like:
db
.collection('collections')
.where('id', '===', 1)
Or do I need to save the document id (of the collection) as a field inside the item map and also send that to Stripe? Or is there a better way to do this? I can't find anything online about this.
Here is a screenshot of Firestore.
Please forgive my beginner question. I'm still learning React, Firestore and Node.js.
First be sure you are sticking to the Firestore terminology correctly. There are collections and there are documents.
Collections you access via a path such as:
collRef = db.collection("products")
collRef = db.collection("products").where("quanity_on_hand", ">", "0")
collRef = db.collection("products").doc("12345").collection("purchase_history")
The latter instance can also be accessed via collRef = db.collection("products/12345/purchase_history").
In all the above cases you will get back a CollectionReference.
Documents you access such as:
docRef = db.collection("products").doc("12345")
docRef = db.doc("products/12345")
This returns you a DocumentReference for the document whose ID is "12345" in the collection "products".
So for your code example above, you want to use docRef = db.doc("collections/1") to get back the DocumentReference for the item you are after. (Or, alternatively, you could use: docRef = db.collection("collections").doc("1")
If you stick with the code that you have above, you'd get back a CollectionReference then you'd need to fetch the data with .get(), then extract the resulting documents (that will just be a single document), then work with that. Oh...and you will need to put an "id" field into all of your documents because the document's ID value (the "name" of the document) is not part of the document by default so if you want to use .where("id", "==", "1"), then you need to add an "id" field to your document and populate it correctly.
If you go with docRef = db.doc("collections/1"), you are querying for the document directly and will get back a reference to just that one. No need for extra fields, nor extracting a single document from a result set.

Firebase auth onCreate add dynamic custom claim

I am trying to add some custom claims to my users, mainly role and org fields.
exports.userSignUp = functions.auth.user().onCreate(async user => {
console.log(user);
let uid = user.uid;
await admin.auth().setCustomUserClaims(uid, { project_worker: true });
});
This is my current code. What I would like to do is be able to pass this onCreate method information to dynamically set a custom claim:
setCustomUserClaims(uid, { project_worker: true, org: userInput });
Is this possible? From what I have read you cannot pass data to the onCreate function, it just pulls the user information from firebase auth.
Is there another way I could go about doing this?

How to notify the front end that it needs to refresh after setting a custom claim with Firebase cloud functions onCreate listener

I'm trying to initialize a user upon registration with a isUSer role using custom claims and the onCreate listener. I've got it to set the correct custom claim but the front end is aware of it only after a full page refresh.
I've been following this article, https://firebase.google.com/docs/auth/admin/custom-claims?authuser=0#logic, to notify the front end that it needs to refresh the token in order to get the latest changes on the custom claims object, but to be honest I don't quite fully understand what's going on in the article.
Would someone be able to help me successfully do this with the firestore database ?
This is my current cloud function:
exports.initializeUserRole = functions.auth.user().onCreate(user => {
return admin.auth().setCustomUserClaims(user.uid, {
isUser: true
}).then(() => {
return null;
});
});
I've tried adapting the real-time database example provided in the article above to the firestore database but I've been unsuccessful.
exports.initializeUserRole = functions.auth.user().onCreate(user => {
return admin.auth().setCustomUserClaims(user.uid, {
isUser: true
}).then(() => {
// get the user with the updated claims
return admin.auth().getUser(user.uid);
}).then(user => {
user.metadata.set({
refreshTime: new Date().getTime()
});
return null;
})
});
I thought I could simply set refreshTime on the user metadata but there's no such property on the metadata object.
In the linked article, does the metadataRef example provided not actually live on the user object but instead somewhere else in the database ?
const metadataRef = admin.database().ref("metadata/" + user.uid);
If anyone could at least point me in the right direction on how to adapt the real-time database example in the article to work with the firestore database that would be of immense help.
If my description doesn't make sense or is missing vital information let me know and I'll amend it.
Thanks.
The example is using data stored in the Realtime Database at a path of the form metadata/[userID]/refreshTime.
To do the same thing in Firestore you will need to create a Collection named metadata and add a Document for each user. The Document ID will be the value of user.uid. Those documents will need a timestamp field named refreshTime.
After that, all you need to do is update that field on the corresponding Document after the custom claim has been set for the user. On the client side, you will subscribe to changes for the user's metadata Document and update in response to that.
Here is an example of how I did it in one of my projects. My equivalent of the metadata collection is named userTokens. I use a transaction to prevent partial database changes in the case that any of the steps fail.
Note: My function uses some modern JavaScript syntax that is being transpiled with Babel before uploading.
exports.initializeUserData = functions.auth.user().onCreate(async user => {
await firestore.collection('userTokens').doc(user.uid).set({ accountStatus: 'pending' })
const tokenRef = firestore.collection('userTokens').doc(user.uid)
const userRef = firestore.collection('users').doc(user.uid)
const permissionsRef = firestore.collection('userPermissions').doc(user.email)
await firestore.runTransaction(async transaction => {
const permissionsDoc = await transaction.get(permissionsRef)
const permissions = permissionsDoc.data();
const customClaims = {
admin: permissions ? permissions.admin : false,
hasAccess: permissions ? permissions.hasAccess : false,
};
transaction.set(userRef, { name: user.displayName, email: user.email, getEmails: customClaims.hasAccess })
await admin.auth().setCustomUserClaims(user.uid, customClaims)
transaction.update(tokenRef, { accountStatus: 'ready', refreshTime: admin.firestore.FieldValue.serverTimestamp() })
});
})

Categories