MongoDB Mutation Upsert - Can't Get Id of New Record on First Submit - mongodb

I'm executing an upsert mutation on MongoDB to create a new document or update an existing document. If the document exists, the mutation returns the id as expected. If a new document is created, the mutation returns null (in both Apollo sandbox and via console.log) in the initial return then in subsequent returns it will return the id. I need it to return the id of the newly created document immediately (on the first return) so I can use that id in subsequent actions.
Starting from the beginning here's the setup:
TYPEDEF
updateHourByEmployeeIdByJobDate(
jobDate: String
startTime: String
endTime: String
hoursWorked: String
employee: String
): Hour
RESOLVER
updateHourByEmployeeIdByJobDate: async (
parent,
{ jobDate, startTime, endTime, hoursWorked, employee },
context
) => {
// if (context.user) {
console.log("resolver hours update = ");
return Hour.findOneAndUpdate(
{ employee, jobDate },
{
jobDate,
startTime,
endTime,
hoursWorked,
employee,
},
{
upsert: true,
}
);
// }
// throw new AuthenticationError("You need to be logged in!");
},
MUTATION
//UPDATE HOUR RECORD - CREATES DOCUMENT IF IT DOESN'T EXIST OR UPDATES IF IT DOES EXIST VIA THE UPSERT OPTION ON THE RESOLVER
export const UPDATE_HOURS_BYEMPLOYEEID_BYJOBDATE = gql`
mutation UpdateHourByEmployeeIdByJobDate(
$jobDate: String
$startTime: String
$endTime: String
$hoursWorked: String
$employee: String
) {
updateHourByEmployeeIdByJobDate(
jobDate: $jobDate
startTime: $startTime
endTime: $endTime
hoursWorked: $hoursWorked
employee: $employee
) {
_id
}
}
`;
FRONT-END EXECUTION
const [ mostRecentHourUpdateId, setMostRecentHoursUpdateId ] = useState();
const [updateHours] = useMutation(UPDATE_HOURS_BYEMPLOYEEID_BYJOBDATE, {
onCompleted: (data) => {
console.log('mutation result #1 = ', data)
setMostRecentHoursUpdateId(data?.updateHourByEmployeeIdByJobDate?._id);
console.log('mutation result #2 = ', mostRecentHourUpdateId)
},
});
//section update database - this mutation is an upsert...it either updates or creates a record
const handleUpdateDatabase = async (data) => {
console.log(data);
try {
// eslint-disable-next-line
const { data2 } = await updateHours({
variables: {
jobDate: moment(data.date).format("MMMM DD YYYY"), //"January 20 2023"
startTime: `${data.startTime}:00 (MST)`, //"12:00:00 (MST)"
endTime: `${data.endTime}:00 (MST)`, //"13:00:00 (MST)"
hoursWorked: data.hours.toString(), //"3.0"
employee: userId, //"6398fb54494aa98f85992da3"
},
});
console.log('handle update database function = data = ', data2); //fix
} catch (err) {
console.error(err);
}
singleHoursRefetch();
};
I've tried using onComplete as part of the mutation request as well as useEffect not to mention running the mutation in Apollo Sandbox. Same result in all scenarios. The alternative is to re-run the useQuery to get the most recent / last record created but this seems like a challenging solution (if at some point the records are sorted differently) and/or seems like I should be able to get access to the newly created record immediately as part of the mutation results.

You'll want to use { returnNewDocument: true }
Like this:
const getData = async () => {
const returnedRecord = await Hour.findOneAndUpdate( { employee, jobDate }, { jobDate, startTime, endTime, hoursWorked, employee, }, { upsert: true, returnNewDocument: true } );
// do something with returnedRecord
}
getData();
For more information:
https://www.mongodb.com/docs/manual/reference/method/db.collection.findOneAndUpdate/

Related

Use Promise.all() inside mongodb transaction is not working propertly in Nestjs

Hi I am working in a NestJS project using mongodb and mongoose.
I have to create transactions with a lot of promises inside, so i think it was a good idea to use Promise.all() inside my transaction for performace issues.
Unfortunately when i started working with my transactions i have a first issue, i was using
session.startTransaction(); and my code was throwing the following error:
Given transaction number 2 does not match any in-progress transactions. The active transaction number is 1, the error was thrown sometimes, not always but it was a problem
So i read the following question Mongoose `Promise.all()` Transaction Error, and i started to use withTransaction(), this solved the problem, but now mi code does not work propertly.
the code basically takes an array of bookings and then creates them, also needs to create combos of the bookings, what I need is that if a creation of a booking or a combo fail nothing should be inserted, for perfomance I use Promise.all().
But when i execute the function sometimes it creates more bookings than expected, if bookingsArray is from size 2, some times it creates 3 bookings and i just don't know why, this occurs very rarely but it is a big issue.
If i remove the Promise.all() from the transaction it works perfectly, but without Promise.all() the query is slow, so I wanted to know if there is any error in my code, or if you just cannot use Promise.all() inside a mongodb transaction in Nestjs
Main function with the transaction and Promise.all(), this one sometimes create the wrong number of bookings
async createMultipleBookings(
userId: string,
bookingsArray: CreateBookingDto[],
): Promise<void> {
const session = await this.connection.startSession();
await session.withTransaction(async () => {
const promiseArray = [];
for (let i = 0; i < bookingsArray.length; i++) {
promiseArray.push(
this.bookingRepository.createSingleBooking(
userId,
bookingsArray[i],
session,
),
);
}
promiseArray.push(
this.bookingRepository.createCombosBookings(bookingsArray, session),
);
await Promise.all(promiseArray);
});
session.endSession();
}
Main function with the transaction and withot Promise.all(), works fine but slow
async createMultipleBookings(
userId: string,
bookingsArray: CreateBookingDto[],
): Promise<void> {
const session = await this.connection.startSession();
await session.withTransaction(async () => {
for (let i = 0; i < bookingsArray.length; i++) {
await this.bookingRepository.createSingleBooking(
userId,
bookingsArray[i],
session,
);
}
await this.bookingRepository.createCombosBookings(bookingsArray, session);
});
session.endSession();
}
Functions called inside the main function
async createSingleBooking(
userId: string,
createBookingDto: CreateBookingDto,
session: mongoose.ClientSession | null = null,
) {
const product = await this.productsService.getProductById(
createBookingDto.productId,
session,
);
const user = await this.authService.getUserByIdcustomAttributes(
userId,
['profile', 'name'],
session,
);
const laboratory = await this.laboratoryService.getLaboratoryById(
product.laboratoryId,
session,
);
if (product.state !== State.published)
throw new BadRequestException(
`product ${createBookingDto.productId} is not published`,
);
const bookingTracking = this.createBookingTraking();
const value = product.prices.find(
(price) => price.user === user.profile.role,
);
const bookingPrice: Price = !value
? {
user: user.profile.role,
measure: Measure.valorACotizar,
price: null,
}
: value;
await new this.model({
...createBookingDto,
userId,
canceled: false,
productType: product.productType,
bookingTracking,
bookingPrice,
laboratoryId: product.laboratoryId,
userName: user.name,
productName: product.name,
laboratoryName: laboratory.name,
facultyName: laboratory.faculty,
createdAt: new Date(),
}).save({ session });
await this.productsService.updateProductOutstanding(
createBookingDto.productId,
session,
);
}
async createCombosBookings(
bookingsArray: CreateBookingDto[],
session: mongoose.ClientSession,
): Promise<void> {
const promiseArray = [];
for (let i = 1; i < bookingsArray.length; i++) {
promiseArray.push(
this.combosService.createCombo(
{
productId1: bookingsArray[0].productId,
productId2: bookingsArray[i].productId,
},
session,
),
);
}
await Promise.all(promiseArray);
}
also this is how i create the connection element:
export class BookingService {
constructor(
#InjectModel(Booking.name) private readonly model: Model<BookingDocument>,
private readonly authService: AuthService,
private readonly bookingRepository: BookingRepository,
#InjectConnection()
private readonly connection: mongoose.Connection,
) {}

MongoDB and triggers

I have a post call that inserts records in an Atlas mongodb database, the Atlas service has a trigger active to increment a correlative field (no_hc), then I make a query to retrieve that correlative field with the _id generated in the insert of the document. I put the code below so that they can tell me what I am doing wrong since this code sometimes returns null the field no_hc. Thanks since now
I add information about what I need to execute. I have a collection on which every time a document is added the mongodb server executes a trigger to increment a certain field, the problem is that the findoneandupsert query returns the inserted document with the autoincrement field set to null, since apparently the trigger is executed after the result of findoneandupsert is returned, how can I solve this issue?
router.post('/admisionUpsert', (req, res) => {
admisionUpsert(req.body)
.then(data => res.json(data))
.catch(err => res.status(400).json({ error: err + ' Unable to add ' }));
})
async function admisionUpsert(body) {
let nohc = body.no_hc
delete body['no_hc'];
let id;
let noprot
await Paciente.findOneAndUpdate(
{ no_hc: nohc },
{
apellido: body.apellido,
nombre: body.nombre,
sexo: body.sexo,
no_doc: body.no_doc,
fec_nac: body.fec_nac,
calle: body.calle,
no_tel: body.no_tel,
email: body.email
}, {
new: true,
upsert: true
}
)
.then(data => { id = data._id })
console.log("id pac", id)
Paciente.findOne({ _id: id }).then(data => { nohc = data.no_hc })
console.log("nohc pac", nohc)
await Protocolo.findOneAndUpdate(
{ no_prot: "" },
{
cod_os: body.cod_os,
no_os: body.no_os,
plan_os: body.plan_os,
no_hc: nohc,
sexo: body.sexo,
medico: body.medico,
diag: body.diag,
fec_prot: body.fec_prot,
medicacion: body.medicacion,
demora: body.demora
}, {
new: true,
upsert: true
}
)
.then(data => { id = data._id })
console.log("id prot", id)
Protocolo.findOne({ _id: id }).then(data => { noprot = data.no_prot })
console.log("noprot prot", noprot)
let practicasProtocolo = []
body.practicas_solicitadas.map((practica, index1) => {
practicasProtocolo.push({
no_prot: noprot,
cod_ana: practica.codigo,
cod_os: practica.cod_os,
estado_administrativo: practica.estado_administrtivo,
estado_muestra: practica.estado_muestra,
estado_proceso: practica.estado_proceso,
})
practica.parametro.map((parametro, index) => {
par = parametro.codigo
tipo_dato = parametro.tipo_dato
if ((tipo_dato === "numerico") || (tipo_dato === "frase") || (tipo_dato === "codigo")) {
practicasProtocolo[index1][par] = null;
}
})
})
console.log(practicasProtocolo)
practicasProtocolo.map(async (practica, index) => {
await new Practica(practica).save()
})
return { nohc: nohc, noprot: noprot }
}

Using the $inc function across the MongoDB documents

I am currently working on a moderation system for my Discord bot and came across an unexpected issue. I've been using the $inc function to increase the values for a single document, though I have sadly not achieved to use the $inc function across multiple different documents, meaning I would like to increase ($inc) the value of the new document according to the numbers of the previous document.
Example: Cases
Current code:
async run(client, message, args, Discord) {
const targetMention = message.mentions.users.first()
const userid = args[0]
const targetId = client.users.cache.find(user => user.id === userid)
const username = targetMention.tag
if(targetMention){
args.shift()
const userId = targetMention.id
const WarnedBy = message.author.tag
const reason = args.join(' ')
if(!reason) {
message.delete()
message.reply('You must state the reason behind the warning you are attempting to apply.').then(message => {
message.delete({ timeout: 6000})
});
return;
}
const warningApplied = new Discord.MessageEmbed()
.setColor('#ffd200')
.setDescription(`A warning has been applied to ${targetMention.tag} :shield:`)
let reply = await message.reply(warningApplied)
let replyID = reply.id
message.reply(replyID)
const warning = {
UserId: userId,
WarnedBy: WarnedBy,
Timestamp: new Date().getTime(),
Reason: reason,
}
await database().then(async database => {
try{
await warnsSchema.findOneAndUpdate({
Username: username,
MessageID: replyID
}, {
$inc: {
Case: 1
},
WarnedBy: WarnedBy,
$push: {
warning: warning
}
}, {
upsert: true
})
} finally {
database.connection.close()
}
})
}
if(targetId){
args.shift()
const userId = message.member.id
const WarnedBy = message.author.tag
const reason = args.join(' ')
if(!reason) {
message.delete()
message.reply('You must state the reason behind the warning you are attempting to apply.').then(message => {
message.delete({ timeout: 6000})
});
return;
}
const warning = {
userId: userId,
WarnedBy: WarnedBy,
timestamp: new Date().getTime(),
reason: reason
}
await database().then(async database => {
try{
await warnsSchema.findOneAndUpdate({
userId,
}, {
$inc: {
Case: 1
},
WarnedBy: WarnedBy,
$push: {
warning: warning
}
}, {
upsert: true
})
} finally {
database.connection.close()
}
const warningApplied = new Discord.MessageEmbed()
.setColor('#ffd200')
.setDescription(`A warning has been applied to ${targetId.tag} :shield:`)
message.reply(warningApplied)
message.delete();
})
}
}
Schema attached to the Code:
const warnsSchema = database.Schema({
Username: {
type: String,
required: true
},
MessageID: {
type: String,
required: true
},
Case: {
type: Number,
required: true
},
warning: {
type: [Object],
required: true
}
})
module.exports = database.model('punishments', warnsSchema)
Answer to my own question. For all of those who are attempting to do exactly the same as me, there is an easier way to get this to properly work. The $inc (increase) function will not work as the main property of a document. An easier way to implement this into your database would be by creating a .json file within your Discord bot files and adding a line such as the following:
{
"Number": 0
}
After that, you'd want to "npm i fs" in order to read directories in live time.
You can proceed to add a function to either increase or decrease the value of the "Number".
You must make sure to import the variable to your current coding document by typing:
const {Number} = require('./config.json')
config.json can be named in any way, it just serves as an example.
Now you'd be able to console.log(Number) in order to make sure the number is what you expected it to be, as well as you can now increase it by typing Number+=[amount]
Hope it was helpful.

Google Calendar API (Saving events in MongoDB, Express JS)

I can't figure out how to save fetched events from Calendar API. I was able to print out array of events in console. I would require save multiple events at once and have verification if they already exist in database with unique id.
Here's my event.js scheme in express js.
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const EventSchema = new Schema({
id: {
type: String,
required: false,
unique:true
},
start: {
type: String
},
end: {
type: String
},
status: {
type: String
},
creator: {
type: Array
},
description: {
type: String
}
});
module.exports = Event = mongoose.model('events', EventSchema);
Here's my event.js router in express js.
router.post("/google/get", async (req, res, next) => {
const {
google
} = require('googleapis')
const {
addWeeks
} = require('date-fns')
const {
OAuth2
} = google.auth
const oAuth2Client = new OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET
)
oAuth2Client.setCredentials({
refresh_token: process.env.GOOGLE_REFRESH_TOKEN,
})
const calendar = google.calendar({
version: 'v3',
auth: oAuth2Client
})
calendar.events.list({
calendarId: 'MY CALENDAR ID',
timeMin: new Date().toISOString(),
timeMax: addWeeks(new Date(), 1).toISOString(),
singleEvents: true,
orderBy: 'startTime',
},
function (err, response) {
if (err) {
console.log("The API returned an error: " + err)
return
}
var events = response.data.items
events.forEach(function (event) {
var start = event.start.dateTime || event.start.date
console.log("%s - %s", start, event.summary)
})
}
)
In Mongoose, in order to save something to a database, all you need to do is to instantiate the model that you created. Your event schema exports Event as a model that you can then treat as a regular object. So you would do something along the lines of:
let currentEvent = new Event({id, start, end, status, creator, description});
currentEvent.save();
Once that is done, it should be stored in your MongoDB. I assume that as the code for this is not visible it is already set up and working. You can just run the above inside of your for loop with some minor tweaks to grab each value correctly and it should sort your issue out!
As for your unique ID and making sure that it doesn't already exist in your database, you can use the same model to find values by checking the id against your database and seeing if it exists. As follows:
Event.findById(id, (err, event) => {
if(event == null) {
let currentEvent = new Event({id, start, end, status, creator, description});
currentEvent.save();
} else {
alert("Error, this event already exists")
}
});
I believe something like this should work, however I might have it wrong with how to check if the event exists, I can't remember if it returns null or something different, so just console log the value of event and check to see what it returns if there isn't an event that exists with that ID, and just re-run your if statement with that instead.

Mongoose findOneAndUpdate not working if I dont specify the field to update

I currently have the following mongoose function in a hapi.js api call
server.route({
method: "PUT",
path:"/api/blockinfo/{hash}",
handler: async (request, h) => {
try{
var jsonPayload = JSON.parse(request.payload)
console.log(jsonPayload)
var result = await BlockModel.findOneAndUpdate(request.params.hash, {$set: { height : jsonPayload[Object.keys(jsonPayload)[0]]}}, {new: true});
return h.response(result);
}catch{
return h.response(error).code(500);
}
}
})
Its goal is basically to update a value using a PUT. In the case above, it will update the field height, and it will work just fine.
But what if I want to update an arbitrary field?
For example my object format is the following:
{"_id":"5cca9f15b1b535292eb4e468", "hash":"d6e0fdb404cb9779a34894b4809f492f1390216ef9d2dc0f2ec91f95cbfa89c9", "height":301651, "size":883, "time":1556782336, "__v":0}
In the case above I updated the height value using the $set, but what if I decide to input 2 random fields to update, for example, size and time.
This would be my put in postman:
{
"size": 300,
"time": 2
}
Well obviously it wont work in the code above because those fields are missing in the set.
SO how do i make that set to recognize automatically whatever it needs to update?
I tried to simplify it with the following code but it wont update anything
server.route({
method: "PUT",
path:"/api/blockinfo/{hash}",
handler: async (request, h) => {
try{
var result = await BlockModel.findOneAndUpdate(request.params.hash, request.payload, {new: true});
return h.response(result);
}catch{
return h.response(error).code(500);
}
}
})
Schema
const BlockModel = Mongoose.model("blocks", {
hash: String,
height: Number,
size: Number,
time: Number
});
Problem is with your hash key. First parameter/argument in findOneAndUpdate function should be the key value pair. And here you are directly putting the key.
So it should be
handler: async (request, h) => {
try {
const { hash } = request.params
var result = await BlockModel.findOneAndUpdate({ hash }, request.payload, { new: true })
return h.response(result)
} catch (err) {
return h.response(error).code(500)
}
}
Update:
You are defining mongoose model in incorrect way. Schema is not just an object. It should be mongoose object. Something like this
const schema = new Mongoose.Schema({
hash: String,
height: Number,
size: Number,
time: Number
})
export default Mongoose.model("blocks", schema)
handler: async (request, h) => {
try{
var result = await BlockModel.findOneAndUpdate(request.params.hash, JSON.parse(request.payload ), {new: true});
return h.response(result);
}catch{
return h.response(error).code(500);
}
}
SInce we are updating a JSON, the payload must be in JSON format
You have not added $set in your simplified code, adding that it should work.
Send payload as an object with the required fields.
server.route({
method: "PUT",
path:"/api/blockinfo/{hash}",
handler: async (request, h) => {
try{
var result = await BlockModel.findOneAndUpdate(request.params.hash, { $set: request.payload } , {new: true});
return h.response(result);
}catch{
return h.response(error).code(500);
}
}
})