Home > database >  Sequelize update via PATCH, how to process each possible result
Sequelize update via PATCH, how to process each possible result

Time:11-05

I'm creating a rest api for CRUD operations using Sequelize and MySql. I'm using a controller to run an update on a PATCH request to update fields of a product. It technically works, but I feel like there is a more elegant way to handle this.

Sequelize's update method will return an array of objects depending on the results. Array[0] is the number of rows affected by the update (should just be one in my case, as I'm updating by id). Array[1] will return an object with details about the update as well as all the old values and new values. Here's how I'm handling that currently:

//products.controller.js

//Update a single product using id (PUT/PATCH)
const patch = (req, res) => {
  const id = req.params.id;

  Product.update(req.body, { where: { id }, individualHooks: true })
    .then((rowsAffected) => {
      //Item not found
      if (Object.entries(rowsAffected[1]).length === 0) {
        res.status(404).send({
          success: false,
          status: 404, //Not found
          message: `Product with id ${id} not found.  Update failed.`,
        });

        return;
      }

      //if rowsAffected[0] === 1 then success
      if (rowsAffected[0] === 1) { //row changed
        res.status(200).send({
          success: true,
          status: 200,
          message: `Product updated.`,
          id: id,
          payload: req.body,
        });
      } else {
        // if rowsAffected[0] !== 1 then it failed.
        res.status(200).send({
          success: false,
          status: 200, //Not Modified
          message: `No fields have changed. Product not updated.`,
        });
      }
    })
    .catch((err) => {
      res.status(500).send({
        success: false,
        status: 500,
        message:
          err.message || "Something went wrong while updating the product.",
      });
    });
}

As you can see, first I'm checking to see if the the update function returns the product details (meaning it successfully found it in the database). If not then sending 404. Then I check the affected rows. If 1 then success, if 0 then nothing changed. Finally I'm catching any server errors.

I feel like there is a better way rather than having to break down the update function's return (like Object.entries(rowsAffected[1]).length === 0)

CodePudding user response:

This is ok if this is the only way you can check the effects of the update. What I can suggest is putting an abstraction above it.

First thing that checking (rowsAffected[0] === 1) does not make much sense, since the update is idempotent and you end up with the same resource state no matter what the actual values are. If you insist, then I would not pair success: false with a 200 ok status, because failure is failure and it requires an error message and 4xx or 5xx status. So either delete it or convert it into a proper error. Hard to find such a status code, but maybe using 409 conflict is ok in these cases https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409 though I would just remove this part of the code. I keep it for the sake of the example.

As of the success and status properties in the body, they don't make much sense either, because they travel in the header, and it is evident from the HTTP standard that 2xx means success, 4xx and 5xx means error. So I would remove those too.

If you don't want to support detailed error codes and exception types and parameters, then just send the error messages and the body can be even a string instead of an object.

Sending the err.message to the consumers is a bad idea by unexpected errors. You don't know what you send out. You need to log them and send something general instead. Communicating errors is always a higher abstraction level stuff, many times. As of the Product with id ${id} not found. Update failed. here adding the id is not necessary, because the request contains it.

So atm. the code looks like this:

const patch = (req, res) => {
  const id = req.params.id;

  Product.update(req.body, { where: { id }, individualHooks: true })
    .then((rowsAffected) => {

      if (Object.entries(rowsAffected[1]).length === 0) {
        res.status(404).send({message: `Product not found.  Update failed.`});
        return;
      }

      //if rowsAffected[0] === 1 then success
      if (rowsAffected[0] === 1) { //row changed
        res.status(200).send({
          message: `Product updated.`,
          id: id,
          payload: req.body,
        });
      } else {
        res.status(409).send({message: "No fields have changed. Product not updated."});
      }
      
    })
    .catch((err) => {
      res.status(500).send({message: "Something went wrong while updating the product."});
    });
}

We can go further by mapping status codes to status messages and extracting the possibly repeating parts of the story into separate functions.

const patch = (req, res) => {
  const id = req.params.id;

  const statusMessages = {
    200: "Product updated."
    404: "Product not found. Update failed."
    409: "No fields have changed. Product not updated.",
    500: "Something went wrong while updating the product."
  };

  Product.update(req.body, { where: { id }, individualHooks: true })
    .then(updateStatusVerification)
    .then(successHandler(res, statusMessages, () => {
        return {
            id: id,
            payload: req.body,
        };
    }))
    .catch(apiErrorHandler(res, statusMessages));
}

function successHandler(res, statusMessages, callback){
    return function (){
        let body = callback();
        body.message = statusMessages[200];
        res.status(200).send(body);
    };
}

function apiErrorHandler(res, statusMessages){
    return function (err){
        let statusCode = 500;
        if (err instanceof NotFoundError)
            statusCode = 404;
        else if (err instanceof NotUpdatedError)
        statusCode = 409;
        res.status(statusCode).send({
            message: statusMessages[statusCode]
        });
    };
}

function updateStatusVerification(rowsAffected){
    return new Promise((resolve, reject) => {
        if (Object.entries(rowsAffected[1]).length === 0)
            reject(new NotFoundError);
        else if (rowsAffected[0] !== 1)
            reject(new NotUpdatedError);
        else
            resolve();
    });
}

class ApiError extends Error {}
class NotFoundError extends ApiError {}
class NotUpdatedError extends ApiError {}

We can move the status messages to the documentation. So you will end up with something like this and some utility functions:

const patch = (req, res) => {
  const id = req.params.id;
  statusMessages = docs.product.update.statusMessages;

  Product.update(req.body, { where: { id }, individualHooks: true })
    .then(updateStatusVerification)
    .then(successHandler(res, statusMessages, () => {
        return {
            id: id,
            payload: req.body,
        };
    }))
    .catch(apiErrorHandler(res, statusMessages));
}

We can go even further if this is a frequent pattern:

const patch = (req, res) => {
  const id = req.params.id;
  handleUpdate(
      Product.update(req.body, { where: { id }, individualHooks: true }),
      () => {id: id, payload: req.body},
      docs.product.update.statusMessages
  );
}
  
function handleUpdate(dbUpdatePromise, successCallback, statusMessages){
    dbUpdatePromise.then(updateStatusVerification)
    .then(successHandler(res, statusMessages, successCallback))
    .catch(apiErrorHandler(res, statusMessages));
}

So it can be as abstract as you like, it really depends on your needs and what the current usage allows. You can decide how many and what kind of layers you need based on actual use cases and repetitions.

  • Related