Skip to content

Instantly share code, notes, and snippets.

@jordanell
Last active July 8, 2021 19:48
Show Gist options
  • Save jordanell/d2f6bd69a40fbe9e4976a5baf8cc1d2a to your computer and use it in GitHub Desktop.
Save jordanell/d2f6bd69a40fbe9e4976a5baf8cc1d2a to your computer and use it in GitHub Desktop.
Sequelize paranoid delete cascade
import paranoidDeleteCascade from './helpers/paranoidDeleteCascade';
// Patch the paranoid delete functionality of Sequelize
sequelize.addHook('afterDestroy', paranoidDeleteCascade(db));
import isArray from 'lodash/isArray';
import map from 'lodash/map';
const paranoidDeleteCascade = (models) =>
async (instance, options, next) => {
// Only operate on paranoid models
if (!instance.$modelOptions.paranoid) {
return next();
}
const modelName = instance.$modelOptions.name.singular;
await Promise.all(
// Go over all associations of the instance model, and delete if needed
map(models[modelName].associations, async (association) => {
try {
// Only delete if cascade is set up correctly
if (association.options.onDelete !== 'CASCADE') {
return true;
}
let relationModel = association.target;
const getOptions = { transaction: options.transaction };
// Handle "through" cases
if (association.through) {
relationModel = association.through.model;
// Include the id of the through model instance
getOptions.include = [{
model: relationModel,
}];
}
// Load id(s) of association
const instances = await instance[`get${association.as}`](getOptions);
if (isArray(instances)) {
// Association has no results so nothing to delete
if (instances.length === 0) {
return true;
}
// Delete all individually as bulk delete doesn't cascase in sequelize
return await Promise.all(instances.map(i => i.destroy(Object.assign({}, options, { individualHooks: true }))));
}
// Association is not set, so nothing to delete
if (!instances) {
return true;
}
return await instances.destroy(options);
} catch (error) {
// If we had issues deleting, we have bigger problems
Promise.resolve(true);
}
return true;
})
);
return next();
};
export default paranoidDeleteCascade;
@alexnault
Copy link

alexnault commented Mar 3, 2020

With this hook, transactions won't rollback the effect of model.destroy.

To fix this, we should pass down the transaction to the underlying instance.destroy like so:

return await Promise.all(instances.map(i => i.destroy({ transaction: options.transaction })));
// [...]
return await instances.destroy({ transaction: options.transaction });

https://sequelize.org/master/manual/hooks.html

@AntoineGrandchamp
Copy link

Transactions also need to be passed to the helper that load associations.

const getOptions = { transaction: options.transaction };

@ccolonna
Copy link

ccolonna commented Mar 23, 2020

Sorry, how can i use that. I tried to mount it like this:

import paranoidDeleteCascade from "./paranoidDeleteCascade";

const Sequelize = require("sequelize");
require("dotenv").config();

export const sequelizeConnection = new Sequelize(
  process.env.DB_NAME,
  process.env.DB_USER,
  process.env.DB_PASSWORD,
  {
    port: process.env.DB_PORT,
    host: process.env.DB_HOST, //change this with docker host
    dialect: process.env.DB_DIALECT
  }
);

sequelizeConnection.addHook("afterDestroy", paranoidDeleteCascade(sequelizeConnection));

is that okei ? It seems not to work


Here an example of association

ProductPrice.belongsTo(Product, {
  foreignKey: "productId",
  onDelete: "CASCADE",
  hooks: "true"
});

Product.hasMany(ProductPrice, {
  as: "productPrices",
  onDelete: "CASCADE",
  hooks: true,
  constraints: true
});

@AntoineGrandchamp
Copy link

@Christian-Nja, on this line, you need to pass an object containing yours models to paranoidDeleteCascade.

sequelizeConnection.addHook("afterDestroy", paranoidDeleteCascade(models));

@ccolonna
Copy link

ccolonna commented Mar 23, 2020

@AntoineGrandchamp thank you! I inserted my model like this:

sequelizeConnection.addHook("afterDestroy",  paranoidDeleteCascade({
    product: Product,
    productDescription: ProductDescription,
  })
);

the relations are:

ProductDescription.belongsTo(Product, {
  foreignKey: "productId"
});

Product.hasMany(ProductDescription, {
  as: "productDescriptions",
  onDelete: "CASCADE",
  hooks: true
});

And the models:

const Sequelize = require("sequelize");
import { sequelizeConnection } from "../mySqlDBConnection";

export const Product = sequelizeConnection.define(
  "product",
  {
    id: {
      defaultValue: Sequelize.UUIDV4,
      type: Sequelize.UUIDV4,
      primaryKey: true,
      allowNull: false
    },
    shopCode: {
      type: Sequelize.STRING,
      allowNull: false
    },
    name: {
      type: Sequelize.STRING,
      allowNull: false
    },
    barCode: {
      type: Sequelize.STRING
    },
    quantity: {
      type: Sequelize.INTEGER,
      allowNull: false
    },
    state: {
      type: Sequelize.STRING,
      allowNull: false
    },
    deleteReason: {
      type: Sequelize.STRING
    }
  },
  {
    tableName: "product",
    freezeTableName: true,
    paranoid: true,
    timestamps: true
  },
  {
    indexes: [
      {
        unique: true,
        fields: ["id", "barCode", "shopCode"]
      }
    ]
  }
);
const Sequelize = require("sequelize");
import { sequelizeConnection } from "../mySqlDBConnection";

export const ProductDescription = sequelizeConnection.define(
  "product_description",
  {
    id: {
      defaultValue: Sequelize.UUIDV4,
      type: Sequelize.UUIDV4,
      primaryKey: true,
      allowNull: false
    },
    description: {
      type: Sequelize.STRING
    },
    importance: {
      type: Sequelize.INTEGER
    },
    productId: {
      type: Sequelize.INTEGER
    },
    deleteReason: {
      type: Sequelize.STRING
    }
  },
  {
    tableName: "product_description",
    freezeTableName: true,
    paranoid: true,
    timestamps: true
  },
  {
    indexes: [
      {
        unique: true,
        fields: ["id"]
      }
    ]
  }
);

But it still doesn't work ...

Copy link

ghost commented Mar 6, 2021

I confirm that this does work, great idea man, it might put some load on your server though specially if you're soft deleting large amount of data with a lot of associations.
One hint though, you need to the "individualHooks: true," property to the destroy options for the "afterDestroy" hook to run, otherwise it'll the "afterDestroy" won't fire but the "afterBulkDestroy" will fire instead.
I'm using sequelize version 5.22.3.
await db[model].destroy({ where: { id: item.id }, transaction: transaction, individualHooks: true, });

@jordanell
Copy link
Author

Thanks for the feedback @gostavee and @AntoineGrandchamp . I've incorporated your suggestions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment