So let's make sure we can now update
a product that we fetched and loaded here.
Now, for that, in our admin.controller,
we already do have the updateProduct action,
which is triggered
if a POST request is being sent to this post route here.
Please note that there,
we also got the id of the product that should be updated.
So therefore, in here, we can actually get
the request and response, as we always do.
We can now use our model
to update our product by that id that we got.
And now in here, in the end, what I wanna do
is I wanna create a new product with my Product constructor,
but now the product data which I'll pass into that Product
will also include an id
so that for this product.model,
in the constructor where I'm setting an id,
I do get this id,
and it has to be _id therefore
because that's what I'm looking for
in the productData that's received in the constructor.
But that's not a problem.
Here, in this object which I pass to new Product,
I now set my title equal to req.body.title,
summary equal to req.body.summary.
And since I'll actually repeat all those req.body fields,
I'll just use ...req.body
to spread all those req.body fields into new Product.
And I'll add the _id field which is req.params.id
because I get the id in my request parameters
because of that placeholder we defined on our routes.
Now, what will be missing here is my image.
Of course, we might choose an image here
when we update a product,
but unlike for creating a product,
this should not be mandatory.
It should be optional.
It's optional to edit any of these fields.
Now, all the fields, except for the image,
still need to have some value.
But for the image,
since we already do have an image stored on the server,
I am fine with that submitted form
not having an image at all in case of updating.
Therefore, what I actually wanna do in my template
is I want to go to product-form
and only enforce the image to be there
if we are creating a new product.
That means that here on this image,
I do wanna add the required attribute
but only if we are creating a new product,
not if we are updating.
To achieve this, I'll add a little if check here with EJS
where we, of course, open a parenthesis here
and then close it thereafter.
And between those EJS tags,
I have required this HTML attribute which will be added,
but it will only be added if this condition is met
and the condition could be if imageRequired.
Of course, we now have to ensure that this imageRequired key
is provided to the product form snippet
from both new-product and update-product.ejs.
In new-product.ejs,
I will therefore pass that as another key-value pair
to this included snippet, imageRequired, and here it's true.
So I'll set this to true here
because for a new product, it is required.
For update-product, on the other hand,
there, I'll set imageRequired to false
because there, it's not required.
There, it's optional.
With that, we add this frontend validation
only if we are adding a new product.
So if I reload this, now we don't see any change here,
but now we could submit this even if no image was chosen.
If, on the other hand, we add a new product,
that would not be possible.
But now back to the backend code in the admin.controller,
there, I don't provide the image to new Product.
Now, as I just said, that's fine in case of updating.
But, of course, I still can choose a new image
in that case as well.
It's just optional.
Therefore, we also have to be prepared
to add a new image if we have one.
Still, I will not add one here in the constructor.
I will simply not set any image field here in this object
which I pass to the constructor.
Instead, in a second step,
I will check if req.file is truthy,
so if we do have a file.
If we don't add a file to the request,
Multer won't be able to extract one,
and req.file would be undefined and therefore falsy.
So if it's truthy, then Multer was able to extract the file.
And then, of course, we wanna use that new image here
for our product.
So that's why we then have to replace the old image
with the new one,
something we'll do in a second.
Either way, what I wanna do
is on my model, my product.model,
I wanna ensure that we now can call the safe method.
But in that safe method we now check whether we have an id,
which means we are saving a product
that was already stored in the database before,
or if we don't have an id,
which means we're about to add a new product,
so what we did before thus far.
Hence here, I will check if this.id is a thing,
if it's truthy, which means yes, we do already have an id,
we wanna update a product in the database.
In that case, we'll have to write some new logic.
Else, if we don't have an id,
then we're adding a new product,
then I wanna execute the old logic,
so I'll move that into the else block.
Now, here, if we're updating an existing image,
what I wanna do is in the end I want to call
db.getDb collection products
updateOne instead of insertOne.
Now, for updating, I want to identify a product by its id.
And here I will use this id
which is not a mongodb.ObjectId though.
I want to have a mongodb.ObjectId.
So here I'll get my productId by using new mongodb.ObjectId
and passing this id into that just as before.
And then I'll use this productId
which is now such a mongodb.ObjectId here
as a filtering condition.
And then the second value which we pass to updateOne
is a object where we describe the update.
We do that with help of the special $set property
you learned about before in the course,
which itself takes the object
where we now can set all the key-value pairs we want
to new values.
And in the end here, I wanna take my productData
because that is a combination of key-value pairs
with all the keys I have in the database
and the appropriate new values.
So that is what I use here as a value for set,
to update all the data
for an existing product in the database.
And, of course, we wanna await this as well.
This might fail as well,
for example, because we failed to create an objectId,
but we'll handle this in the admin.controller later.
Of course, you would also correctly notice
that at the moment
we always overwrite the stored image with undefined
because at the moment I'm not passing any image data
into my product,
but we'll change that soon because here I plan
on replacing the existing image with a new one,
and that is actually something we have to do.
Back in product-model, let's say at the end,
though, again, it doesn't matter,
I'll add a new method, replaceImage, which gets a newImage
and which will replace the old image with that new one.
And now here I will set this image equal to newImage.
Here I expect to get the name of the image
as it was created by Multer.
And I also wanna update my image path and URL.
For that, I'll copy these two lines.
Alternatively, we create a new helper method
that does this updating for us.
Actually, let's maybe do that.
Let's cut these lines from the constructor,
add a new method
maybe after these static methods
to have some kind of organization here.
This method could be called updateImageData,
and in there I wanna set this imagePath and this imageUrl,
and instead of productData.image,
we refer to this.image in both cases.
And then it's updateImageData
which can be called from inside the constructor.
So here I call this.updateImageData
in the constructor method
after setting the image.
The order is important.
And then down here in replaceImage,
I also call this.updateImageData.
And now with that, we update the image data
in that product object based on our class
whenever we call replaceImage.
Now, back in admin-controller, if we now got a new image,
we can reach out to product and call replaceImage,
and pass our new image which is req.file.filename,
so the new image name to be precise, to replaceImage.
Now, of course, we could have this scenario
that user did not choose a new image.
And therefore back in product-model, when we save our data,
I still wanna check if we maybe don't have an image data
because no new data was provided,
and we didn't fetch the old data from the database,
and in that case,
I don't wanna overwrite the existing image data
in the database with undefined.
Therefore, in this branch of the if-else block
where we do start updating our data,
before we call updateOne,
I wanna check if this.image is a thing,
if this is truthy, which means we have some image data.
And if that's not the case, hence the exclamation mark,
then I don't wanna overwrite the image data
in the database with undefined.
We can achieve this by using a specific keyword
which we haven't seen before in this course,
and that's the delete keyword.
You can execute delete entered productData.image.
And what this does is it deletes that key-value pair
from that productData object.
Just setting it to null or undefined wouldn't be enough
because then we would just overwrite
the data in the database for the image field with null.
Therefore, I instead called delete here
or I used the delete keyword
to delete this key-value pair so that it's gone entirely.
And then the updating object which we passed to set
will simply not have image field,
and hence MongoDB won't even try to update the image field.
And that's what I wanna do here
to ensure that I'm not accidentally overwriting
the image in the database,
the image data in the database I should say.
There would be alternatives to achieving this,
but this is an approach that should work.
Now, back in the admin-controller,
we can use our product to call save now
and, of course, await this since it's an async operation.
For that, of course, turn that into a async function
with the async keyword.
And then, of course, also be prepared for this to fail
by wrapping it with try-catch
as we did it before all the time.
And then here we get this third parameter,
this next function,
so that in the catch case,
we can call next and forward our error.
In that case, I also wanna return here
so that we don't continue
because otherwise if we make it past try-catch,
my default way of handling the success here
would be to call res.redirect,
and redirect to /admin/products
now that the product was updated.
And this should do the trick.
If I now try this, and I reload this page,
and I add exclamation mark after a keyword,
and I maybe change the price to 109.99 and I click Save,
I get an error.
I get an error that an argument must be passed in, yeah.
That means that we have a problem
with the id of the product.
And let's see where this is coming from.
If we go back to the Update Product page for this product,
we can see that if I inspect this,
actually the problem is that for this form,
in the form element here,
the submit URL has undefined as a product id.
That's the problem.
Now, why is the product id missing?
Let's see.
If we have a look at the admin.controller
and there at the getUpdateProduct function,
which is responsible for rendering that Update Product page,
there I am providing my product,
but yeah.
In Product.findById,
if we have a look at that method in our model,
we do find a product by id, of course,
but in the end what I returned there
is my product as I get it from the database,
so the document, the product document data in the end.
Now, please note that in update-product.ejs,
I'm referring to product.id here when I construct this path
for the form submission: .id, not ._id.
Now, .id only exists if in my product model,
I pass productData into the constructor,
and hence I added this id here.
But this is not happening with findById.
There, I just get some product from the database,
and then in the end I just return that.
It's never converted to an instance of this class,
and hence it doesn't have an id field.
So one workaround here
would be to instead return new Product
and pass this product document
which we get from the database
into this constructor function
so that what we actually return now
is a instance based on this product class
and therefore an object that will have an id field.
With that, if I reload here,
and I now add an exclamation mark and change that price,
and I click Save,
this is looking a bit better,
but now it looks like my title is gone.
And indeed, if I find all products,
I see that I reset all the data except for the image name.
At least that worked, I did not overwrite that,
but I lost all the other data.
And the reason for my data missing and so on
is that I forgot to add an important middleware
to this post route.
We might have an image in there.
We use enctype multipart/form-data for the form,
so we should add our imageUploadMiddleware,
otherwise this will fail as it did before.
That is why I accidentally erased all the data
because without that middleware,
without Multer being involved, no body data is parsed
because the default body parser for form data does not work
if the enctype is multipart/form-data
as it is for this product form.
So this middleware, the imageUploadMiddleware,
needs to be added to this update post route.
And if we do that, and we then go back and we update this,
as I said, I erased everything but for the image,
so I have to reenter it.
So let's set some new values here.
This is a great keyboard!
Now also updated!
If we now save that,
the data is back here and also here in the database.
So now this was updated here,
let's now also try updating this with a new image.
For this, I'll pick the existing image,
but you should notice that the name here changes
as soon as I updated it,
so I'll confirm this image and click Save.
And now if I find my products again, indeed,
this is a slightly different name as you can tell.
The characters here are different,
and therefore this was updated.
We are not deleting the old image.
You could, of course, try to write logic for this as well.
Here, I'm fine with keeping those images
because it would also make sense
that we wanna store images forever to give users or admins
a way of rolling back to older images
or to run our own analytics software on those images
where we might train some machine learning models
or anything like that.
So I'll keep those images around here.
But now with that,
we are able to update with or without image,
now also without overwriting all my data.