Skip to content

Minor fixes #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions src/soft-delete-model.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
import {Document, SaveOptions} from "mongoose";
import { Document, SaveOptions } from "mongoose";
import * as mongoose from "mongoose";

export interface SoftDelete {
isDeleted: boolean;
deletedAt: Date | null;
}

export type SoftDeleteDocument = SoftDelete & Document;

export interface SoftDeleteModel<T extends Document> extends mongoose.Model<T> {
findDeleted(): Promise<T[]>;
restore(query: Record<string, any>): Promise<{ restored: number }>;
softDelete(query: Record<string, any>, options?: SaveOptions): Promise<{ deleted: number }>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if anyone is using the options parameter now.
this could be a breaking change

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider a major?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, the majority is important.
In this case, if we bump the package version accordingly to introduce the changes, it should be fine.


/**
* Finds the documents that matches the given query and restore them if `isDeleted` was set to true
*/
restore(query: mongoose.FilterQuery<T>, orFail?: (e: Error) => unknown): Promise<{ restored: number }>;

/**
* Finds the document with the given ID and restores it if `isDeleted` was true
*/
restoreById(id: string | mongoose.Types.ObjectId, orFail?: (e: Error) => unknown): Promise<void>;

/**
* Finds the documents with the given query and update the "isDeleted" & "deletedAt" properties
* If no document is found it will return `{ deleted: 0 }`
*/
softDelete(query: mongoose.FilterQuery<T>, orFail?: (e: Error) => unknown): Promise<{ deleted: number }>;

/**
* Finds one document with the given id and udpate if one exists and it has not been deleted yet
*/
softDeleteById(id: string | mongoose.Types.ObjectId, orFail?: (e: Error) => unknown): Promise<void>;
}
96 changes: 55 additions & 41 deletions src/soft-delete-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import mongoose, { CallbackError, SaveOptions } from 'mongoose';
import mongoose, { CallbackError, FilterQuery } from 'mongoose';
import { SoftDeleteModel } from './soft-delete-model';

export const softDeletePlugin = (schema: mongoose.Schema) => {
type Model = SoftDeleteModel<mongoose.Document>;
export function softDeletePlugin<T extends mongoose.Schema>(schema: T) {
schema.add({
isDeleted: {
type: Boolean,
Expand All @@ -9,13 +11,13 @@ export const softDeletePlugin = (schema: mongoose.Schema) => {
},
deletedAt: {
type: Date,
default: null,
default: () => new Date(),
},
});

// @ts-ignore
schema.pre('find',
async function (this, next: (err?: CallbackError) => void) {
async function(this, next: (err?: CallbackError) => void) {
if (this.getFilter().isDeleted === true) {
return next();
}
Expand All @@ -26,7 +28,7 @@ export const softDeletePlugin = (schema: mongoose.Schema) => {

// @ts-ignore
schema.pre('count',
async function (this, next: (err?: CallbackError) => void) {
async function(this, next: (err?: CallbackError) => void) {
if (this.getFilter().isDeleted === true) {
return next();
}
Expand All @@ -36,57 +38,69 @@ export const softDeletePlugin = (schema: mongoose.Schema) => {

// @ts-ignore
schema.pre('countDocuments',
async function (this, next: (err?: CallbackError) => void) {
async function(this, next: (err?: CallbackError) => void) {
if (this.getFilter().isDeleted === true) {
return next();
}
this.setQuery({ ...this.getFilter(), isDeleted: { $ne: true } });
next();
})

schema.static('findDeleted', async function () {
return this.find({ isDeleted: true });
schema.static('findDeleted', function() {
return this.find({ isDeleted: true }).exec();
});

schema.static('restore', async function (query) {

// add {isDeleted: true} because the method find is set to filter the non deleted documents only,
// so if we don't add {isDeleted: true}, it won't be able to find it
const updatedQuery = {
schema.static('restore', async function(query: FilterQuery<T>, orFail?: (e: Error) => unknown) {
const findDeletedDocsQuery = {
isDeleted: true,
...query,
isDeleted: true
};
const deletedTemplates = await this.find(updatedQuery);
if (!deletedTemplates) {
return Error('element not found');
}
let restored = 0;
for (const deletedTemplate of deletedTemplates) {
if (deletedTemplate.isDeleted) {
deletedTemplate.$isDeleted(false);
deletedTemplate.isDeleted = false;
deletedTemplate.deletedAt = null;
await deletedTemplate.save().then(() => restored++).catch((e: mongoose.Error) => { throw new Error(e.name + ' ' + e.message) });
const { modifiedCount } = await this.updateMany(findDeletedDocsQuery, {
$set: {
isDeleted: false,
deletedAt: null
}
}
return { restored };
}).orFail(orFail).exec();


return { restored: modifiedCount };
});

schema.static('softDelete', async function (query, options?: SaveOptions) {
const templates = await this.find(query);
if (!templates) {
return Error('Element not found');
}
let deleted = 0;
for (const template of templates) {
if (!template.isDeleted) {
template.$isDeleted(true);
template.isDeleted = true;
template.deletedAt = new Date();
await template.save(options).then(() => deleted++).catch((e: mongoose.Error) => { throw new Error(e.name + ' ' + e.message) });
schema.static('softDelete', async function(query, orFail?: (e: Error) => unknown) {
const queryFilter = {
$or: [
{ isDeleted: false },
{ isDeleted: { $exists: false } },
{ isDeleted: null }
],
...query,
};

const { modifiedCount } = await this.updateMany(queryFilter, {
$set: {
isDeleted: true,
deletedAt: new Date()
}
}
return { deleted };
}).orFail(orFail).exec();

return {
deleted: modifiedCount
};
});

schema.static('softDeleteById', async function(id: string | mongoose.Types.ObjectId, orFail?: (e: Error) => unknown) {
await (this as Model).softDelete(
{ _id: new mongoose.Types.ObjectId(id) },
orFail
)
});

schema.static('restoreById', async function(id: string | mongoose.Types.ObjectId, orFail?: (e: Error) => unknown) {
await (this as Model).restore(
{ _id: new mongoose.Types.ObjectId(id) },
orFail
)
});

};