Automattic / mongoose

MongoDB object modeling designed to work in an asynchronous environment.
https://mongoosejs.com
MIT License
26.95k stars 3.84k forks source link

Feature: Soft-Delete #7304

Open mhombach opened 5 years ago

mhombach commented 5 years ago

Do you want to request a feature or report a bug? feature

What is the current behavior? Deleting a document in mongoose really deletes it from the database.

What is the expected behavior? We should be able to enable an option to let documents be "soft-deleted". Meaning, there should be an attribute like _deleted on each document and deleting a document by the normal mongoose way will jsut mark the document as deleted. Find-queries will, by default, all skip/exclude documents that are marked as "deleted". By options, one can still hard-delete documents or find-query including or only deleted documents. There are 2-3 plugins for that but they are not maintained in any way, nor do they have a considerable amount of downloads and their last commit is over 3 years old. I, and many others that will be working with mongoose in professional environments, will love that feature.

Please mention your node.js, mongoose and MongoDB version. All latest versions

vkarpov15 commented 5 years ago

How about an official plugin? Like the ones on http://plugins.mongoosejs.io

msrumon commented 3 years ago

https://github.com/Automattic/mongoose/issues/7304#issuecomment-446616920: Is it under development or what? 'Cause I don't see it in the provided link.

vkarpov15 commented 3 years ago

@msrumon we don't have an official plugin yet, nor do we have plans to build one currently.

axelvaindal commented 3 years ago

Hello,

I've tried writing this plugin to fit our internal use. I've something working but I've questions that are TypeScript related. We have extended the Document type to match the new contract with soft deletion with the following code:

type TWithSoftDeleted = {
  isDeleted: boolean;
  deletedAt: Date | null;
};

type TDocument = TWithSoftDeleted & Document;

Our goal is then to use this document type to validate the procedure made in the plugin but we have errors in the typing. I've tried to use the documentation here related to TypeScript, but it was not 100% clear what the expected types are 😕

const excludeDeletedInFindQueries = async function (
    this: Query<any, TDocument>, // is this correct ?
    next: HookNextFunction
  ) {
    // Property 'includeSoftDeleted' does not exist on type 'QueryOptions'. How to make it available ?
    const { includeSoftDeleted } = this.getOptions();

    if (includeSoftDeleted !== true) {
      this.where({ isDeleted: { $in: [null, false] } });
    }

    next();
  };

Do you have any insight regarding this?

vkarpov15 commented 3 years ago

@axelvaindal unfortunately right now you would have to manually cast:

const { includeSoftDeleted } = q.getOptions() as { includeSoftDeleted?: boolean };

We'll figure out a workaround for this for future versions.

axelvaindal commented 3 years ago

Hello @vkarpov15 👋

I'm trying to update my code to use mongoose v6, but it seems typings have changed from the previous 5.X version. I've removed the @types/mongoose and tried to rely only on internal typings from now on.

Is there a way to find relevant typings for plugins apart from reading index.d.ts in the repository? More specifically, I'm trying to apply pre hooks to these queries:

  const findQueries = [
    "count",
    "find",
    "findOne",
    "findOneAndDelete",
    "findOneAndRemove",
    "findOneAndUpdate",
    "update",
    "updateOne",
    "updateMany",
  ];

Then, I try to exclude deleted in these queries but I can't manage to correctly type the hook, and the previous HookNextFunction type is no longer functioning. Could you help me correct the typings here? Thanks in advance for your help 🙏

const excludeDeletedInQueries = async function (
    this: FilterQuery<any>, // is this correct?
    next: any // what should be used here?
  ) {
    const { includeSoftDeleted } = this.getOptions();

    if (includeSoftDeleted !== true) {
      this.where({ isDeleted: { $in: [null, false] } });
    }

    next();
  };

  const excludeDeletedInAggregateMiddleware = async function (
    this: Aggregate<any>, // is this correct?
    next: any // what should be used here?
  ) {
    this.pipeline().unshift({ $match: { isDeleted: { $in: [null, false] } } });
    next();
  };

softDeletedExcludedQueries.forEach((query) => {
    schema.pre(query, excludeDeletedInQueries);
  });

  schema.pre("aggregate", excludeDeletedInAggregateMiddleware);
vkarpov15 commented 2 years ago

HookNextFunction looks like this:

  interface HookNextFunction {
    (error?: Error): any;
  }

We removed it in Mongoose 6 because it isn't terribly useful. next: (error?: Error) => any should work.

Your plugin implementation looks correct :+1:

GO-DIE commented 2 years ago

Soft delete plugin => https://www.npmjs.com/package/soft-delete-mongoose-plugin

A simple and friendly soft delete plugin for mongoose,implementation using TS. Methods were added and overridden on mongoose model to realize soft deletion logic.

aprilmintacpineda commented 1 year ago

Would be good if mongoose have something like https://sequelize.org/docs/v6/core-concepts/paranoid, so we can do:

const userSchema = new mongoose.Schema(
  {
    // schema
  },
  {
    timestamps: true,
    paranoid: true
  }
);

And whenever we perform any kinds of delete, i.e., deleteMany, deleteOne, findByIdAndDelete, findOneAndDelete, etc, it will use deletedAt field, and when we query, we have the option to either include or exclude documents with deletedAt -- should default to exclude!

sibelius commented 9 months ago

user land ?