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;
@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