Firestore insert object field - object

I want to save a value inside a field in firestore, like this image.
As you can see in the picture, I want shopId to be dynamic, then it will contain date and time fields with a boolean value.
My objective is to track a user's Checkin history on different shops. Hope you understand what i'm saying here.
now this is my current code:
checkInShop() {
let currentUser = firebase.auth().currentUser;
if (currentUser) {
const db = firebase.firestore();
const batch = db.batch();
const docRef = db.collection("userCheckInHistory").doc(currentUser.uid);
const timestamp = firebase.firestore.FieldValue.serverTimestamp();
batch.set(docRef, {
`${this.state.shopId}`: {
`${timestamp}`: true
}
});
// Commit the batch
batch.commit().then(function() {
AlertIOS.alert(`ShopId: Sent!`);
});
}
};
Is this possible to firestore?

you can do:
docRef.set({ [this.state.shopId]: { [timestamp]: true } });
you probably want to set merge to true in this case as well:
docRef.set({ [this.state.shopId]: { [timestamp]: true } }, { merge: true });
if you have to use template literals:
docRef.set(
{ [`${this.state.shopId}`]: { [`${timestamp}`]: true } },
{ merge: true }
);

Related

Handling concurrent request that finds and update the same resource in Node Js & Mongo DB?

I have a function in node that runs after a clicking the checkout button. It checks the availability of the items in cart and if the item is available it will deduct it from the inventory.
I'm currently testing with two users clicking the checkout button at the same time. Both users have the exact same content in their cart (10 apples each) which gives a total of 20 apples, but there are only 10 apples in inventory.
If there is no item in cart it should return an error to the user but both orders are going through.
NOTE: This works if there is a 1 second delay between the clicks.
What can i do to prevent this?
// Check if items in inventory
const availability = await checkInventory(store, cart, seller);
if (!availability.success) {
return res.status(400).json({
success: false,
type: 'unavailable',
errors: availability.errors,
});
}
// Deduct Inventory
const inventory = await deductInventory(store, seller, cart);
if (!inventory) {
return next(new ErrorResponse('Server Error', 500));
}
checkInventory
exports.checkInventory = asyncHandler(async (store, cart, seller) => {
let isAvailable = true;
const unavailableProducts = [];
const inventory = await Inventory.find({
$and: [
{
store: store,
user: seller,
},
],
});
const products = inventory[0].products;
cart.forEach((item) => {
const product = products.find(
(product) => product._id.toString() === item.productId
);
if (!item.hasvariation) {
if (product.stock < item.qty) {
isAvailable = false;
unavailableProducts.push(
`${item.title} is not available, only ${product.stock} left available`
);
}
}
if (item.hasvariation) {
const variation = product.variations.find(
(variation) => variation._id.toString() === item.variationId
);
const option = variation.options.find(
(option) => option._id.toString() === item.optionId
);
if (option.stock < item.qty) {
isAvailable = false;
unavailableProducts.push(
`${item.title} is not available, only ${product.stock} left available`
);
}
}
});
return {
success: isAvailable,
errors: unavailableProducts,
};
});
deductInventory
exports.deductInventory = asyncHandler(async (store, seller, cart) => {
const inventory = await Inventory.findOne({
$and: [
{
store: store,
user: seller,
},
],
});
const products = inventory.products;
cart.forEach((item) => {
const product = products.find(
(product) => product._id.toString() === item.productId
);
if (!item.hasvariation) {
product.stock = product.stock - item.qty;
}
if (item.hasvariation) {
const variation = product.variations.find(
(variation) => variation._id.toString() === item.variationId
);
const option = variation.options.find(
(option) => option._id.toString() === item.optionId
);
option.stock = option.stock - item.qty;
}
});
const saveInventory = await Inventory.findOneAndUpdate(
{
$and: [
{
store: store,
user: seller,
},
],
},
{
$set: { products: products },
},
{ new: true, runValidator: true }
);
if (!saveInventory) {
return {
success: false,
errors: ['Server Error'],
};
}
return {
success: true,
};
});
The problem is that the 2 checkout calls run at (almost) the same time and your routine is not thread-safe. Both calls read a copy of the inventory data in memory. So both calls get a products.stock=10 and based on that local info you check and set the products counter by calculating the new amount in your function (stock-qty) and use an update query to set it as a fixed value (so both calls update the products.stock to 0). Resulting in your concurrency issues.
What you should do is let mongodb handle the concurrency for you.
There are several ways to handle concurrency but you could for example use the $inc to decrease the stock amount directly in mongo. That way the stock amount in the db can never be wrong.
result = await update({stock: {$ge: 10}}, {$inc: {stock : -10}})
As I added a filter to the query the order amount can not be lower than 0 plus you can now check the result of the update call to see if the update modified any documents. If it did not (result.nModified==0) you know the inventory was too low and you can report that back to the user.
https://docs.mongodb.com/manual/reference/operator/update/inc/
https://docs.mongodb.com/manual/reference/method/db.collection.update/#std-label-writeresults-update

How to access a different schema in a virtual method?

I want to write a virtual (get) method for my MongoDb collection (Parts) which needs to access a different schema: I want it to assert if a document is 'obsolete' according to a timestamp available in a different (Globals) collection:
const partsSchema = new Schema({
...
updatedAt: {
type: Date,
},
...
}, {
toObject: { virtuals: true },
toJSON: { virtuals: true },
});
partsSchema.virtual('obsolete').get(async function() {
const timestamp = await Globals.findOne({ key: 'obsolescenceTimestamp' }).exec();
return this.updatedAt < timestamp.value;
});
But when I do a find, I always get a {} in the obsolete field, and not a boolean value...
const p = await parts.find();
...
"obsolete": {},
...
Is there some way to accomplish my goal?
You can do this, but there are a few obstacles you need to hurdle. As #Mohammad Yaser Ahmadi points out, these getters are best suited for synchronous operations, but you can use them in the way you're using them in your example.
So let's consider what's happening here:
partsSchema.virtual('obsolete').get(async function() {
const timestamp = await Globals.findOne({ key: 'obsolescenceTimestamp' }).exec();
return this.updatedAt < timestamp.value;
});
Since the obsolete getter is an async function, you will always get a Promise in the obsolete field when you query your parts collection. In other words, when you do this:
const p = await parts.find();
You will get this:
...
"obsolete": Promise { <pending> },
...
So besides getting the query results for parts.find(), you also need to resolve the obsolete field to get that true or false result.
Here is how I would write your code:
partsSchema.virtual('obsolete').get(async function() {
const Globals = mongoose.model('name_of_globals_schema');
const timestamp = await Globals.findOne({ key: 'obsolescenceTimestamp' });
return this.updatedAt < timestamp.value;
});
Then when querying it...
parts.findOne({_id: '5f76aee6d1922877dd769da9'})
.then(async part => {
const obsolete = await part.obsolete;
console.log("If obsolete:", obsolete);
})

How to get all updated documents' values after updateMany()?

Desired Behaviour
After updating a property value in multiple documents, access the value of that updated property for each document updated.
(for context, I am incrementing multiple users' notification counts, and then sending each updated value back to the respective user, if they are logged in, via socket.io)
What I've Tried
let collection = mongo_client.db("users").collection("users");
let filter = { "user_email": { $in: users_to_notify } };
let update = { $inc: { "new_notifications": 1 } };
let result = await collection.updateMany(filter, update);
// get the `new_notifications` value for all updated documents here
There doesn't seem to be a returnOriginal type option applicable to the updateMany() method.
https://docs.mongodb.com/manual/reference/method/db.collection.updateMany
http://mongodb.github.io/node-mongodb-native/3.2/api/Collection.html#updateMany
That is understandable, because returnOriginal only seems to make sense when updating one document, eg in the options for the findOneAndUpdate() method.
Question
If all of the above assumptions are true, what would be the best way to get the new_notifications value for all updated documents after the updateMany() method has finished?
Is it just a matter of making another database call to get the updated values?
for example - this works:
let collection = mongo_client.db("users").collection("users");
let filter = { "user_email": { $in: users_to_notify } };
let update = { $inc: { "new_notifications": 1 } };
await collection.updateMany(filter, update);
// make another database call to get updated values
var options = { projection: { user_email: 1, username: 1, new_notifications: 1 } };
let docs = collection.find(filter, options).toArray();
/*expected result:
[{
"user_email": "user_1#somedomain.com",
"username": "user_1",
"new_notifications": 17
},
{
"user_email": "user_2#somedomain.com",
"username": "user_2",
"new_notifications": 5
}]*/
// create an object where each doc's user_email is a key with the value of new_notifications
var new_notifications_object = {};
// iterate over docs and add each property and value
for (let obj of docs) {
new_notifications_object[obj.user_email] = obj.new_notifications;
}
/*expected result:
{
"user_1#somedomain.com": 17,
"user_2#somedomain.com": 3
}*/
// iterare over logged_in_users
for (let key of Object.keys(logged_in_users)) {
// get logged in user's email and socket id
let user_email = logged_in_users[key].user_email;
let socket_id = key;
// if the logged in user's email is in the users_to_notify array
if (users_to_notify.indexOf(user_email) !== -1) {
// emit to the socket's personal room
let notifications_count = new_notifications_object[user_email];
io.to(socket_id).emit('new_notification', { "notifications_count": notifications_count });
}
}
updateMany() does not return the updated documents but some counts. You have to just run another query for getting those updates.
Would suggest you do a find({}) first, then findByIdAndUpdate() in a loop of the found items.
let updatedRecord = [];
let result = await Model.find(query);
if (result.length > 0) {
result.some(async e => {
updatedRecord.push(await Model.findByIdAndUpdate(e._id, updateObj, { new: true }));
});
}
return updatedRecord;

Mongoose - updating object inside an array

I have a Schema that looks like this:
const RefSchema = {
active: Boolean,
items: [{}],
};
const TopLevelSchema = new mongoose.Schema({
refs: [RefSchema],
...
}, { timestamps: true });
I'm making an API call to update this one of the refs using its id (below its rid) and some data that's inside the API call:
async function updateRef(id, rid, data) {
// First get the TopLevelSchema by the ID - this is OK
const instance = await this.findById(id).exec();
// Prepare the data:
const $set = _.mapKeys(data, (v, k) => `refs.$.${k}`);
// Update the data
await instance.update(
{ 'refs.id': rid },
{ $set },
);
What's happening is that the data (and e.g. I'm passing { active: true }) is not updated.
What am I doing wrong?
There is no need to first get the TopLevelSchema etc. You can update the child like this:
async function updateRef(rid, data) {
let $set = _.mapKeys(data, (v, k) => `refs.$.${k}`)
await TopLevelSchema.updateOne(
{ 'refs._id' : mongoose.Types.ObjectId(rid) },
{ $set })
}
are you using custom ids? because you should do { '_id': rid } instead { 'refs.id': rid }

how to let updatemany validate mongoose schema on every modified document ?

I 've got an issue with mongoose schema validation, while trying to validate the schema of documents modified inside a Model::updateMany (or update + mutli:true) request.
I've got the schema below:
var BusinessesSchema = new Schema({
label: {
type: String,
required: true
},
openingDate: {
type: Date,
required: true
},
endingDate: {
type: Date,
validate: function(value) {
if (this.constructor.name === 'Query') {
// Looks like this is a validation for update request
var doc = null;
switch (this.op) {
case 'update':
case 'updateMany': {
doc = this._update.$set;
break;
}
case 'findOneAndUpdate': {
doc = this._update;
break;
}
default:
// keep null, will throw an error
}
return doc.openingDate < value;
}
else {
return this.openingDate < value;
}
}
}
});
I would like to access modified documents value ("this") inside endingDate::validate function to make sure that for each modified document ending Date is greater than beginning one .
Even, when using pre-hook (for update & updateMany, as below), I still do not find any way to access the modified documents value to perform my check when multi is set (or when calling updateMany).
BusinessesSchema.pre('update', function(next) {
this.options.runValidators = true;
this.options.context = 'query';
next();
});
BusinessesSchema.pre('updateMany', function(next) {
this.options.runValidators = true;
this.options.context = 'query';
next();
});
I probably missed something, and would really appreciate help here.
Can't do that. Best you can do with updateMany is to capture the query context, from this, and analyze the update. Something like this:
Schema.pre('updateMany', function (next) {
const update = this.getUpdate();
if (update.$set && update.$set && !validateUpdate(update.$set)) {
throw new Error('Invalid Update');
}
next();
});
If you wanna do validation on the resulting document before the update using the save hook, you can use a cursor:
Schema.pre('save', function(next) {
if (!validDoc(this)) {
throw new Error('Invalid Doc');
}
next();
}
Schema.statics.updateManyWithValidation = async function(criteria, update) {
const cursor = Model.find(criteria).cursor();
let doc = null;
do {
doc = await cursor.next();
Object.assign(doc, update);
await doc.save();
} while(doc != null);
}
Now, bear in mind this is a much more expensive operation since you're fetching the documents, applying the changes, and then saving them individually.

Resources