Firestore add Document ID to Data via functions - node.js

I'm looking to add the Firestore ID to the DocumentData, so that I can easily utilize the ID when referring to rows in a table, without having to use document.data().property everytime I call a property of a document. Instead, I want to be able to call document.id.... document.property... and so on.
Is there an easy way to do this? Possibly with a Cloud Function that adds the auto-generated ID to the document data?
Thanks!
Example:
export const getSpaces = async () => {
const spaceDocs = await getDocs(spacesCollection)
spaceDocs.docs.forEach((spaceDoc) => {
const spaceID = spaceDoc.id
const spaceData = spaceDoc.data()
console.log(spaceID)
spaces.value.push(spaceData)
})
}
Now, the spaces array has objects containing the data of the documents. But, I loose the ability to reference the ID of a document.
Alternatively, I can add the entire document to the array, but following that, I'll have to access the properties by always including the data() in between. I.e. space.data().name
I'm certain, theres a better way

You don't need Cloud Functions to add the document ID to the data of that document. If you look at the third code snippet in the documentation on adding a document, you can see how to get the ID before writing the document.
In some cases, it can be useful to create a document reference with an auto-generated ID, then use the reference later. For this use case, you can call doc() [without any arguments]:
const newCityRef = db.collection('cities').doc(); // 👈 Generates a reference, but doesn't write yet
// Later...
const res = await newCityRef.set({
newCityRef.id, // 👈 Writes the document ID
// ...
});
As others have commented, you don't need to store the ID in the document. You can also add it to your data when you read the documents, with:
spaceDocs.docs.forEach((spaceDoc) => {
const spaceID = spaceDoc.id
const spaceData = spaceDoc.data()
console.log(spaceID)
spaces.value.push({ id: spaceID, ...spaceData })
})
With this change your spaces contains both the document ID and the data of each document.

A DocumentSnapshot has id property that you are looking for. If you add something within the document, then you'll need to access the data first with doc.data().field_name.

Working on another problem, I came across the solution. Quite simple actually:
When declaring the new Object, you can add ...doc.data() to add all the properties of the DocumentData to the newly created Object, after initialising it with an id. This works for me anyways. Case closed.
onSnapshot(profilesCollectionRef, (querySnapshot) => {
const profilesHolder = [];
querySnapshot.forEach((doc) => {
const profile = {
id: doc.id,
...doc.data(),
}
profilesHolder.push(profile);
console.log(profile);
});
profiles.value = profilesHolder;
});
onSnapshot(doc(db, "profiles", userId), (doc) => {
const newProfile = {
id: doc.id,
...doc.data(),
}
myProfile.value = newProfile as Profile;
});

Related

Perform check on record before performing update with Prisma

I'm creating the backend for a simple app which allows users to create, update, and delete products. Using Express as my framework, with Postgres as my DB and Prisma, which I'm new to, as my ORM. Users and products have a one-to-many relationship. Prisma's documentation states that when updating a record, you should use the update method - so to update the name of a product with a given ID, your code would look something like this:
export const updateProduct = async (req, res) => {
const [productID, newProductName, userID] = [req.params.id, req.body.name, res.locals.user.id];
const product = await prisma.product.update({
where: {
id: productID,
},
data: {
name: newProductName
}
});
res.status(200);
res.json(product);
};
However, there's a problem here - I'm not checking to see that the product with the provided ID belongs to the user that has sent the request to update it. I have the ID of the user who has sent the request in the variable userID, and each product in the DB has a field belongsToID which is set to the ID of the user that the product belongs to. I should theoretically therefore be able to modify my query to get the product with the specified ID and a matching belongsToID like so:
export const updateProduct = async (req, res) => {
const [productID, newProductName, userID] = [req.params.id, req.body.name, res.locals.user.id];
const product = await prisma.product.update({
where: {
id: productID,
belongsToID: userID
},
data: {
name: newProductName
}
});
res.status(200);
res.json(product);
};
That, however, does not work - I get the following error: Type '{ id: any; belongsToID: any; }' is not assignable to type 'ProductWhereUniqueInput'. Object literal may only specify known properties, and 'belongsToId' does not exist in type 'ProductWhereUniqueInput'.ts(2322).
It appears that when trying to do a 'findUnique', Prisma doesn't allow non-unique fields to be used in the query (even if the combination of both fields is unique, as is the case here). I do get that logically, my query doesn't make much sense - the ID alone is already enough to find a unique entry without the second field, so in that sense, the second field is totally redundant. Yet how else am I meant to check that the belongsToID is what it should be before updating the record? Is there somewhere else within the object passed to .update where I can provide a check to be performed on the retrieved record before performing the update?
I believe that creating an index would allow me to query for both fields at once - but why should I have to create an index when the ID (which is already indexed) alone is all I need to retrieve the record I need? What I really need is a way to perform a check on a retrieved record before performing the update when using Prisma.table_name.update(), not a way to query for something with a unique combination of fields.

Best practice way to update a parent document field that is computed from sub collection documents in google cloud firestore

Here's an abbreviated version of the data model for my firestore collections:
// /posts/{postId}
interface Post {
id: string;
lastCommentedAt: Timestamp | null
}
// /posts/{postId}/comments/{commentId}
interface Comment {
id: string;
createdAt: Timestamp;
}
So I have a collection of posts, and within each post is a subcollection of comments.
I want to do the following:
When a comment is created, update the lastCommentedAt field of the parent Post document with the comment's createdAt value
When a comment is deleted, the parent Post's lastCommentedAt field may no longer be valid so we need to get all of the Post's comments and get the most recent comment to update lastCommentedAt.
I see a couple ways to do this:
In my client code, I can do the above logic inside of functions like createComment(post, comment), and deleteComment(post, comment)
Especially in the case of deleting it seems like it is not ideal to require the client to fetch all comments for the post and iterate through them just to delete one
Would I need to use transactions for this since someone could be deleting a comment at the same time someone was creating a new one?
In my cloud functions I could create triggers on /posts/{postId}/comments/{commentId} for create and delete and do this logic on the backend
Is there risk of race conditions here as well? Again maybe I should use a transaction?
The use case for this lastCommentedAt field is that I want to be able to query for posts and sort them by the ones that have recent comments.
Edit: possible implementation for deleteComment using a batched write. Is it actually safe to do a query for documents before the writes though?
async function deleteComment(post, comment) {
const batch = writeBatch(firestore);
const postRef = doc("posts", post.id);
const commentRef = doc("posts", post.id, "comments", comment.id);
const commentsCollection = collection("posts", post.id, "comments");
const recentCommentSnapshot = await getDocs(
query(
commentsCollection,
where("id", "!=", comment.id),
orderBy("createdAt", "desc"),
limit(1)
)
);
let lastCommentedAt = null;
if (recentCommentSnapshot.docs.length > 0) {
lastCommentedAt = recentCommentSnapshot.docs[0].data().createdAt;
}
batch.delete(commentRef);
batch.update(postRef, { lastCommentedAt });
await batch.commit();
}
For adding comments you can use batched writes to ensure the comment is added and the parent document is update with current timestamp.
const batch = db.batch();
const postDocRef = db.collection('posts').doc('postId');
const commentRef = postDocRef.collection("comments").doc();
batch.update(postDocRef, { lastCommentedAt: FieldValue.serverTimestamp() });
batch.set(commentRef, { createdAt: FieldValue.serverTimestamp() });
await batch.commit();
When deleting comments you might have to query the latest comment using createdAt field and then update parent document.
const getLastComment = (postId: string) => {
const postRef = db.collection('posts').doc(postId);
return await postRef.orderBy('createdAt', 'desc').limit(1).get();
}

How to get document one time and using it every where

I using firebase admin and use nodejs get data by phone number from firebase. When i get success , i want get only document one time and use it every time. It possible ?
Picture :
First i get document by phone look like.
Example:
const phone = await admin.firestore().collection('users').where('phone_number', '==', phone).get()
After that , i want using document phone every time in my code look like :
await handleLogic(phone)
Then
async function handleLogic(phone) {
//inside here i need call await admin.firestore().collection('users').where('phone_number', '==', somePhone).get() or re use phone in parameter ?
phone.ref.collection("subcollection").get()
.then(() => {
let data = {
created_at: timeNowFirebase(),
};
phone.ref.collection(subcollection).doc(someId)
.set(data);
}
I have question : Inside function handleLogic(phone), i need re call admin.firestore().collection('users').where('phone_number', '==', phone).get() or only use parameter phone and used phone.ref.collection(subcollection).doc(someId).set(data); . It will set subcollection into document my phone correct ?
Yes you can store the document ID (or even the DocumentReference as you are currently) in memory/cache instead of querying the document with phone number every time. The doc ID never changes and this seems to be a good way to prevent additional requests to the database.
// storing in memory for example
const phoneToUserId = {};
async function handleLogic(phone) {
if (!phoneToUserId[phone]) {
// run a query to get userID from phone number
const user = await admin.firestore().collection('users').where('phone_number', '==', somePhone).get()
phoneToUserId[phone] = user.docs[0].id
}
// get a reference to sub-collection
const subCol = admin.firestore().collection(`users/${phoneToUserId[phone]}/subcollection`)
// query data
}
However do note that you'll have to update that object whenever user updates their phone number or delete their document.

How can I dynamically generate Mongoose discriminators (at runtime?)

TL;DR: Is there a safe way to dynamically define a mongoose discriminator at runtime?
I have an app with a MongoDB collection where users have some control over the underlying schema.
I could add one or two fixed, required fields and just use mongoose.Mixed for the remainder that users can change, but I'd like to make use of Mongoose's validation and discriminators if I can.
So, what I've got is a second collection Grid where the users can define the shape they'd like their data to take, and in my main model Record, I've added a function to dynamically generate a discriminator from the definition in the second collection.
The code for my Record model looks like this:
const mongoose = require("mongoose")
const recordSchema = new mongoose.Schema({
fields: {
type: Array,
required: true
}
}, {
discriminatorKey: "grid"
})
const Record = mongoose.model("Record", recordSchema)
module.exports = grid => {
// Generate a mongoose-compatible schema from the grid's field definitions
const schema = grid.fields.map(field => {
if(field.type === "string") return { [field.name]: String }
if(field.type === "number") return { [field.name]: Number }
if(field.type === "checkbox") return { [field.name]: Boolean }
return { [field.name]: mongoose.Mixed }
})
return Record.discriminator(grid._id, new mongoose.Schema(schema))
}
This is inside an Express app, and I use the model in my middleware handlers something like this:
async (req, res) => {
const grid = await Grid.findById(req.params.id)
const Record = await GenerateRecordModel(grid)
const records = await Record.find({})
res.json({
...grid,
records
})
}
This works great on the first request, but after that I get an error Discriminator with name “ ” already exists.
I guess this is because only one discriminator with its name per model can exist.
I could give every discriminator a unique name whenever the function is called:
return Record.discriminator(uuidv4(), new mongoose.Schema(schema), grid._id)
But I imagine that this isn't a good idea because discriminators seem to persist beyond the lifetime of the request, so am I laying the groundwork for a memory leak?
I can see two ways forward:
COMPLICATED? Define all discriminators when the app boots up, rather than just when a HTTP request comes in, and write piles of extra logic to handle the user creating, updating or deleting the definitions over in the Grid collection.
SIMPLER? Abandon using discriminators, just use mongoose.Mixed so anything goes as far as mongoose is concerned, and write any validation myself.
Any ideas?

Get field from subcollection's parent document

I am creating a message application with Firestore
At the moment i have created a function that gets triggered when a user writes a message in a messageRoom. Each messageRoom document has a subcollection called messages which contains the messages of the room. The messageRoom document itself contains data related to the messageRoom itself e.g. users who are part of the messageRoom. The messageRoom contains a field called members which holds info about the user who are a part of the messageRoom
I want to get access to the data in the messageRoom document and not the messages subcollection. At the moment the following code does not work. An error tells me that the members field is undefined. What have i done wrong?
exports.sendNotification = functions
.firestore
.document('messageRooms/{messageRoomId}/messages/{messageId}')
.onCreate(
async (snapshot) => {
/*
Does something with the documents in the subcollection
*/
//Get the DocumentReference of the parent doc
const parentDocId = snapshot.ref.parent.id;
admin.firestore().collection('messageRooms').doc(parentDocId).get('members').get().then(doc => {
if (doc.exists) {
console.log("Document data:", doc.data());
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
return; //return added to prevent crash
}).catch(error => {
console.log("Error getting document:", error);
});
}
);
I think the problems is here:
//Get the DocumentReference of the parent doc
const parentDocId = snapshot.ref.parent.id;
snapshot is DocumentSnapshot, so ref property give us DocumentReference. Property parent of document reference is CollectionReference (Google Doc). So you have to find not parent but "grandparent" :) I may say. Maybe try:
const parentDocId = snapshot.ref.parent.parent.id;
However I would not use .id property here just to get reference in .doc in next line. parent property already provides reference (doc). I don't think that argument in get is changing anything, so I would use .data().messages to get to the field. In the end I would use:
const parentDocRef = snapshot.ref.parent.parent;
parentDocRef.get().then(doc => { console.log(doc.data().members)})
I hope it will help!

Resources