GraphQL resolver logic - Call query/mutation from mutation? - javascript

I am creating a workout/exercise logger, where user's can add a log of their set of an exercise to their account. User's will also be able to see their history of workouts and exercises (with set data). I am using mongoDB to store this data, with GraphQL and mongoose to query and mutate the data. I have seperated workout and exercise into their own types, as a workout object will only hold exercises and the sets that were recorded in the last 4 hours (duration of workout), while the exercise object will hold all sets that were ever logged by the user.
Type Definitions
type Workout {
id: ID!
workoutName: String!
username: String!
createdAt: String
exercises: [Exercise]!
notes: String
}
type Exercise {
id: ID!
exerciseName: String!
username: String!
sets: [Set]!
}
type Set {
id: ID!
reps: Int!
weight: Float!
createdAt: String!
notes: String
}
My problem lies in my resolver code for adding a set (mutation). This resolver should:
Query the database whether the exercise has ever been done by the user, by checking the name of the exercise with a match in the database, if there is a match, add the set data the user inputted to it, otherwise, create a new exercise entry first and then add the set to it.
Query the database whether there is a workout that has been done in the last 4 hours. If there isn't a match, create a new workout entry in the database. If there is a workout, check for a matching exercise name on the workout object to add the set data to or create a new exercise entry for it.
I realise that this mutation will be fairly large and will combine both the querying and mutation of data together. So I'm wondering if I can call seperate queries/mutations from my addSet resolver similar to a function call? Or is there another method that I should be going about this?
addSet Resolver
async addSet(_, { exerciseName, reps, weight, notes }, context) {
const user = checkAuth(context); // Authenticates and gets logged in user's details
if (exerciseName.trim() === '') {
throw new UserInputError('Empty Field', {
// Attached payload of errors - can be used on client side
errors: {
body: 'Choose an exercise'
}
})
} else {
exerciseName = exerciseName.toLowerCase();
console.log(exerciseName);
}
if ((isNaN(reps) || reps === null) || (isNaN(weight) || reps === null)) {
throw new UserInputError('Empty Fields', {
// Attached payload of errors - can be used on client side
errors: {
reps: 'Enter the number of reps you did for this set',
weight: 'Enter the amount of weight you did for this set'
}
})
}
// TODO: Check to see if the exercise has been done before by the user. If it has, then update the entry by adding the set data to it. If not create a new entry for the
// exercise and then add the data to it - Completed and working.
const exerciseExists = await Exercise.findOne({ exerciseName: exerciseName, username: user.username });
if (exerciseExists) {
console.log("This exercise exists");
exerciseExists.sets.unshift({
reps,
weight,
username: user.username,
createdAt: Date.now(),
notes
})
await exerciseExists.save();
//return exerciseExists;
} else {
console.log("I don't exist");
const newExercise = new Exercise({
exerciseName,
user: user.id,
username: user.username,
});
const exercise = await newExercise.save();
console.log("new exercise entry");
exercise.sets.unshift({
reps,
weight,
username: user.username,
createdAt: Date.now(),
notes
})
await exercise.save();
//return exercise;
}
// TODO: Get the most recent workout from the user and check if the time it was done was from the last 4 hours. If it wasn't, create a new workout entry for the user.
// If it was within the ast 4 hours, check to see if the workout has an exercise that matches with one the user inputted. If there isn't an exercise, create a new entry
// and add the set data to it, otherwise update the existing entry for the exercise.
const workoutExists = await Workout.findOne({ username: username }).sort({ createdAt: -1 }); // Gets most recent workout by user
const now = Date.now();
if (now > workoutExists.createdAt + 14400000) { // Checks to see if the workout was less than 4 hours ago
console.log("workout was from another session");
// rest of code not implemented yet
} else {
console.log("workout is still in progress");
// rest of code not implemented yet
}
},

Related

Mongoose - Deleting documents is unresponsive

I'm trying to use Mongoose (MongoDB JS library) to create a basic database, but I can't figure out how to delete the documents / items, I'm not sure what the technical term for them is.
Everything seems to work fine, when I use Item.findById(result[i].id), it returns a valid id of the item, but when I use Item.findByIdAndDelete(result[i].id), the function doesn't seem to start at all.
This is a snippet the code that I have: (Sorry in advance for bad indentation)
const testSchema = new schema({
item: {
type: String,
required: true
},
detail: {
type: String,
required: true
},
quantity: {
type: String,
required: true
}
})
const Item = mongoose.model("testitems", testSchema)
Item.find()
.then((result) => {
for (i in result) {
Item.findByIdAndDelete(result[i].id), function(err, result) {
if (err) {
console.log(err)
}
else {
console.log("Deleted " + result)
}
}
}
mongoose.connection.close()
})
.catch((err) => {
console.log(err)
})
I'm not sure what I'm doing wrong, and I haven't been able to find anything on the internet.
Any help is appreciated, thanks.
_id is a special field on MongoDB documents that by default is the type ObjectId. Mongoose creates this field for you automatically. So a sample document in your testitems collection might look like:
{
_id: ObjectId("..."),
item: "xxx",
detail: "yyy",
quantity: "zzz"
}
However, you retrieve this value with id. The reason you get a value back even though the field is called _id is because Mongoose creates a virtual getter for id:
Mongoose assigns each of your schemas an id virtual getter by default which returns the document's _id field cast to a string, or in the case of ObjectIds, its hexString. If you don't want an id getter added to your schema, you may disable it by passing this option at schema construction time.
The key takeaway is that when you get this value with id it is a string, not an ObjectId. Because the types don't match, MongoDB will not delete anything.
To make sure the values and types match, you should use result[i]._id.

MongoDB update many using the values of the user

I've written a function to execute hourly which looks up a user and finds some values and then pushes those values into a history collection that records the hourly updated values. I've written this so far as a test just finding a user by their ID but now I need to roll this out to my entire database of 50,000+ users.
From what I've read using updateMany is a lot more performant but I'm not entirely sure how to retrieve the document detail of the record that is being updated at the time.
Here is my code so far, which you can see I'm first looking up the user and then grabbing their valuation details which I'd like to then push into a history collection.
exports.updateUserValuationHistoric = () => {
User.find({ _id: "609961fdd989613914ef7216" })
.populate('UserValuationHistory')
.exec((err, userDoc) => {
if (err){
console.log('[ERROR]: ', err)
}
const updatedValuationHistory = {
totalValuation: userDoc[0].valuation.totalValuation,
comicsValuation: userDoc[0].valuation.comicsValuation,
collectiblesValuation: userDoc[0].valuation.collectiblesValuation,
omiValuation: userDoc[0].valuation.omiValuation
}
UserValuationHistory.findOneAndUpdate(
{ user: userDoc[0]._id },
{ $push: {
'history': updatedValuationHistory
}},
{upsert: true, new: true}
)
.exec((error, updated) => {
if (error){
console.log('[ERROR]: Unable to update the user valuation history.')
} else {
console.log('[SUCCESS]: User historic valuation has been updated.')
}
})
})
}
Any help is greatly appreciated!
User model:
https://pastebin.com/7MWBVHf3
Historic model:
https://pastebin.com/nkTGztJY

How to update firestore collection based on other docs?

I am building an order form that limits how many items you can order based on the stock of the item. I have a menu collection which has items
// menu
{ id: "lasagna", name: "Lasagna", price: 10, stock: 15 }
{ id: "carrot-soup", name: "Carrot Soup", price: 10, stock: 15 }
{ id: "chicken-pot-pie", name: "Chicken Pot Pie", price: 10, stock: 15 }
And an orders collection
// orders
{ id: <auto>, name: "Sarah", cart: {lasagna: 1, carrot-soup: 3}, ... }
{ id: <auto>, name: "Wendy", cart: {chicken-pot-pie: 2, carrot-soup: 1}, ... }
{ id: <auto>, name: "Linda", cart: {lasagna: 3}, ... }
4 carrot-soup has been ordered so the stock should be updated
// updated stock
{ id: "carrot-soup", name: "Carrot Soup", stock: 11 }
Orders are inserted from my Form component
function Form(props) {
// ...
// send order to firestore
const onSubmit = async _event => {
try {
const order = { cart, name, email, phone, sms }
dispatch({ action: "order-add" })
const id = await addDocument(store, "orders", order)
dispatch({ action: "order-add-success", payload: { ...order, id } })
}
catch (err) {
dispatch({ action: "order-add-error", payload: err })
}
}
return <form>...</form>
}
This is my database addDocument function
import { addDoc, collection, serverTimeStamp } from "firebase/firestore"
async function addDocument(store, coll, data) {
const docRef = await addDoc(collection(store, coll), { ...data, timestamp: serverTimestamp() })
return docRef.id
}
How should I decrement the stock field in my menu collection?
Ideally the client should have only read access to menu but to update the stock the client would need write access.
Another possibility is to have the client query the orders, sum the items, and subtract them from the read-only menu. But giving the client read access to other people's orders seems wrong too.
I am new to firestore and don't see a good way to design this.
You should deffinitely use a cloud function to update the stock. Create a function onCreate and onDelete functions trigger. If users can change data you would also need to onWrite function trigger.
Depending on the amount of data you have you woould need to create a custom queue system to update the stock. Belive me! It took me almost 2 years to figure out to solve this. I have even spoken with the Firebase engeeners at the last Firebase Summit in Madrid.
Usualy you would use a transaction to update the state. I would recommend you to do so if you don't have to much data to store.
In my case the amount of data was so large that those transactions would randomly fail so the stock wasn't correct at all. You can see my StackOverflow answer here. The first time I tought I had an answer. You know it took me years to solve this because I asked the same question on a Firebase Summit in Amsterdam. I asked one of the Engeeners who worked on the Realtime Database before they went to Google.
There is a solution to store the stock in chunks but even that would cause random errors with our data. Each time we improved our solution the random errors reduced but still remained.
The solution we are still using is to have a custom queue and work each change one by one. The downside of this is that it takes some time to calculate a lot of data changes but it is 100% acurate.
Just in case we still have a "recalculator" who recalculates one day again and checks if everything worked as it should.
Sorry for the long aswer. For me it looks like you are building a similar system like we have. If you plan to create a warehouse management system like we did I would rather point you to the right direction.
In the end it depends on the amount of data you have and how often or fast you change it.
Here is a solution based on Tarik Huber's advice.
First I include functions and admin
const functions = require("firebase-functions")
const admin = require("firebase-admin")
admin.initializeApp()
Then I create increment and decrement helpers
const menuRef = admin.firestore().collection("menu")
const increment = ([ id, n ]) =>
menuRef.doc(id).update({
stock: admin.firestore.FieldValue.increment(n)
})
const decrement = ([ id, n ]) =>
increment([ id, n * -1 ])
Here is the onCreate and onDelete hooks
exports.updateStockOnCreate =
functions
.firestore
.document("orders/{orderid}")
.onCreate(snap => Promise.all(Object.entries(snap.get("cart") ?? {}).map(decrement)))
exports.updateStockOnDelete =
functions
.firestore
.document("orders/{orderid}")
.onDelete(snap => Promise.all(Object.entries(snap.get("cart") ?? {}).map(increment)))
To handle onUpdate I compare the cart before and after using a diff helper
exports.updateStockOnUpdate =
functions
.firestore
.document("orders/{orderid}")
.onUpdate(snap => Promise.all(diff(snap.before.get("cart"), snap.after.get("cart")).map(increment)))
Here is the diff helper
function diff (before = {}, after = {}) {
const changes = []
const keys = new Set(Object.keys(before).concat(Object.keys(after)))
for (const k of keys) {
const delta = (before[k] ?? 0) - (after[k] ?? 0)
if (delta !== 0)
changes.push([k, delta])
}
return changes
}

Attempt to search for a specific string

I'm trying to find a specific row in the database based on the user's message, namely: catalystname.
Within the schema I have successfully indexed the given string as text:
const { Schema } = mongoose;
const scheduleMessageSchema = new Schema({
_id: { type: Schema.Types.Oid, auto: true },
catalystname: String,
catalystdesc: String,
catalystquest: String,
date: String,
});
scheduleMessageSchema.index({catalystname: 'text'});
module.exports = mongoose.model('dbcatalyst', scheduleMessageSchema);
My search code:
const Catal = require("../src/models/dbcatalyst.js")
module.exports.run = async (client, message, args) => {
message.content = args.slice(0).join(" ")
Catal.find({$text: {$search: message.content}})
.exec(function(docs){
let embedlogs3 = new Discord.RichEmbed()
.setAuthor(`1`, message.author.displayAvatarURL)
.setDescription(`${docs}`)
.setColor("#33ffff")
message.channel.send(embedlogs3)
/*/ ${collected.first().content}/*/
});
}
And started searching for the required line in the message. The bot successfully copes with its task, but displays the entire document in full instead of 1 line.
_id: 5e243704961eb23c106bfb02,
catalystname: 'Чёрный Коготь',
catalystdesc: '0',
catalystquest: '0',
date: '1579430157018',
__v: 0
}
Can I somehow output exactly the string? catalystname
Looking at the Mongoose documentation, it seems the callback takes two parameters:
err An error or null
docs The returned document(s)
Change your callback to
Catal.find({$text: {$search: message.content}})
.exec(function(err, docs){
...
});
and you should receive an array of matching documents.

What is the best way to keep track of changes of a document's property in MongoDB?

I would like to know how to keep track of the values of a document in MongoDB.
It's a MongoDB Database with a Node and Express backend.
Say I have a document, which is part of the Patients collection.
{
"_id": "4k2lK49938d82kL",
"firstName": "John",
"objective": "Burn fat"
}
Then I edit the "objective" property, so the document results like this:
{
"_id": "4k2lK49938d82kL",
"firstName": "John",
"objective": "Gain muscle"
}
What's the best/most efficient way to keep track of that change? In other words, I would like to know that the "objective" property had the value "Burn fat" in the past, and access it in the future.
Thanks a lot!
Maintaining/tracking history in the same document is not all recommended. As the document size will keep on increasing leading to
probably if there are too many updates, 16mb document size limit
Performance degrades
Instead, you should maintain a separate collection for history. You might have use hibernates' Javers or envers for auditing for your relational databases. if not you can check how they work. A separate table (xyz_AUD) is maintained for each table (xyz). For each row (with primary key abc) in xyz table, there exist multiple rows in xyz_AUD table, where each row is version of that row.
Moreover, Javers also support MongoDB auditing. If you are using java you can directly use it. No need to write your own logic.
Refer - https://nullbeans.com/auditing-using-spring-boot-mongodb-and-javers/
One more thing, Javers Envers Hibernate are java libraries. But I'm sure for other programming languages also, similar libraries will be present.
There is a mongoose plugin as well -
https://www.npmjs.com/package/mongoose-audit (quite oudated 4 years)
https://github.com/nassor/mongoose-history#readme (better)
Maybe you can change the type of "objective" to array and track the changes in it. the last one of the array is the latest value.
Maintain it as a sub-document like below
{
"_id": "4k2lK49938d82kL",
"firstName": "John",
"objective": {
obj1: "Gain muscle",
obj2: "Burn fat"
}
}
You can also maintain it as an array field but remember, mongodb doesn't allow you to maintain uniqueness in an array field and if you plan to index the "objective" field, you'll have to create a multi key index
I think the simplest solution would be to use and update an array:
const patientSchema = new Schema({
firstName: { type: String, required: true },
lastName: { type: String, required: true },
objective: { type: String, required: true }
notes: [{
date: { type: Date, default: Date.now() },
note: { type: String, required: true }
}],
});
Then when you want to update the objective...
const updatePatientObjective = async (req, res) => {
try {
// check if _id and new objective exist in req.body
const { _id, objective, date } = req.body;
if (!_id || !objective) throw "Unable to update patient's objective.";
// make sure provided _id is valid
const existingPatient = await Patient.findOne({ _id });
if (!existingPatient) throw "Unable to locate that patient.";
// pull out objective as previousObjective
const { objective: previousObjective } = existingPatient;
// update patient's objective while pushing
// the previous objective into the notes sub document
await existingPatient.updateOne({
// update current objective
$set { objective },
// push an object with a date and note (previouseObjective)
// into a notes array
$push: {
notes: {
date,
note: previousObjective
},
},
}),
);
// send back response
res
.status(201)
.json({ message: "Successfully updated your objective!" });
} catch (err) {
return res.status(400).json({ err: err.toString() });
}
};
Document will look like:
firstName: "John",
lastName: "Smith",
objective: "Lose body fat.",
notes: [
{
date: 2019-07-19T17:45:43-07:00,
note: "Gain muscle".
},
{
date: 2019-08-09T12:00:38-07:00,
note: "Work on cardio."
}
{
date: 2019-08-29T19:00:38-07:00,
note: "Become a fullstack web developer."
}
...etc
]
Alternatively, if you're worried about document size, then create a separate schema for patient history and reference the user's id (or just store the patient's _id as a string instead of referencing an ObjectId, whichever you prefer):
const patientHistorySchema = new Schema({
_id: { type: Schema.Types.ObjectId, ref: "Patient", required: true },
objective: { type: String, required: true }
});
Then create a new patient history document when the objective is updated...
PatientHistory.create({ _id, objective: previousObjective });
And if you need to access to the patient history documents...
PatientHistory.find({ _id });

Categories