DEV Community

Cover image for 44-Nodejs Course 2023: Database Recycle Bin
Hasan Zohdy
Hasan Zohdy

Posted on

44-Nodejs Course 2023: Database Recycle Bin

We've proceeded with a nice progress in the saving process, but we still have a problem, what if we want to delete a document from the database but we actually don't want to delete it?

Well, we can use the recycle bin concept, we can just move the document to the recycle bin and then we can restore it.

Recycle Bin

Here is how it works, when we call the delete method, we'll actually not deleting it, but we'll move it to another collection which will be called collectionNameTrash where collectionName is the name of the collection we want to delete the document from.

For example if we want to delete a document from the users collection, we'll move it to the usersTrash collection.

Now let's try it in the destroy method first.

// src/core/database/model/model.ts


  /**
   * Delete the document form database
   */
  public async destroy() {
    if (!this.data._id) return;

    // Trash The data
    this.data.deletedAt = new Date();

    // create a copy of the current data in the collectionTrash
    // create a copy of the current data in the collectionTrash
    await queryBuilder.create(this.getCollectionName() + "Trash", {
      document: this.data,
      id: this.data.id,
      _id: this.data._id,
    });

    // perform the actual delete
    await queryBuilder.deleteOne(this.getCollectionName(), {
      _id: this.data._id,
    });
  }
Enter fullscreen mode Exit fullscreen mode

We performed here before the actual delete from the database a new query to create a copy of the document in the trash collection which will be added in the Model's collection name with the Trash suffix.

The content that we added in the collection, contains the entire document data, the id and the _id of the document.

But why would we add the id and the _id of the document?

Well, we'll need them later to restore the document.

Restoring The Document

Now let's go to the restoring process, we'll create a new method called restore which will be a static method that can be called directly from the model class itself.

// src/core/database/model/crud-model.ts
// ...
  /**
   * Update model by the given id
   */
  public static async restore<T>(
    this: ChildModel<T>,
    id: PrimaryIdType,
  ): Promise<T | null> {
    // find the document in the trash collection
    const document = await queryBuilder.first(this.collectionName + "Trash", {
      [this.primaryIdColumn]: id,
    });

    // if no document, then it means there is no document in the trash
    // table with that id
    if (!document) return null;

    // if we got here, create a new model and inject to it
    // the document object
    const model = this.self(document.document);

    // add restoredAt column with current date
    model.set("restoredAt", new Date());

    // perform save
    await model.save();

    // return the model
    return model;
  }
Enter fullscreen mode Exit fullscreen mode

The restore method receives one parameter, the id of the document that we want to restore from the trash, if the document exists in the trash collection, get it.

Now if the document exists, create a new model, inject the document which contains the entire data of the deleted document, after that add the restoredAt column with the current date.

Now we can perform the save, and return the model.

But there is a catch here, remember in the save method we have a condition that checks if the document has an _id or not, if it has an _id then it means that the document already exists in the database, so we'll perform an update, but if it doesn't have an _id then we'll perform an insert.

So we need to remove the _id from the document before we perform the save, so we'll do that in the restore method.

So we'll add another condition to check if it is being restored.

We'll created a property called isRestored and method to mark the model as restored.

// src/core/database/model/model.ts
// ...

  /**
   * A flag to determine whether the model is restored or not
   */
  protected isRestored = false;
  /**
   * Mark the model as restored
   */
  public markAsRestored() {
    this.isRestored = true;
    return this;
  }
Enter fullscreen mode Exit fullscreen mode

Now let's call it in the restore method.

// src/core/database/model/crud-model.ts
// ...
  /**
   * Update model by the given id
   */
  public static async restore<T>(
    this: ChildModel<T>,
    id: PrimaryIdType,
  ): Promise<T | null> {
    // find the document in the trash collection
    const document = await queryBuilder.first(this.collectionName + "Trash", {
      [this.primaryIdColumn]: id,
    });

    // if no document, then it means there is no document in the trash
    // table with that id
    if (!document) return null;

    // if we got here, create a new model and inject to it
    // the document object
    const model = this.self(document.document);

    // add restoredAt column with current date
    model.set("restoredAt", new Date());

    // mark the model as restored
    model.markAsRestored();

    // perform save
    await model.save();

    // return the model
    return model;
  }
Enter fullscreen mode Exit fullscreen mode

Now let's go to the save method and add the condition.

// src/core/database/model/model.ts


  /**
   * Perform saving operation either by updating or creating a new record in database
   */
  public async save(mergedData: Document = {}) {
    this.merge(mergedData);

    // check if the data contains the primary id column
    // but is not being restored
    if (this.data._id && !this.isRestored) {
      // perform an update operation
      // check if the data has changed
      // if not changed, then do not do anything
      if (areEqual(this.originalData, this.data)) return;

      this.data.updatedAt = new Date();

      await queryBuilder.update(
        this.getCollectionName(),
        {
          _id: this.data._id,
        },
        this.data,
      );

      // unset the isRestored flag
      if (this.isRestored) {
        this.isRestored = false;
      }
    } else {
      // creating a new document in the database
      const generateNextId =
        this.getStaticProperty("generateNextId").bind(Model);

      // if the column does not exist, then create it
      if (!this.data.id) {
        this.data.id = await generateNextId();
      }

      const now = new Date();

      // if the column does not exist, then create it
      if (!this.data.createdAt) {
        this.data.createdAt = now;
      }

      // if the column does not exist, then create it
      if (!this.data.updatedAt) {
        this.data.updatedAt = now;
      }

      this.data = await queryBuilder.create(
        this.getCollectionName(),
        this.data,
      );
    }
  }
Enter fullscreen mode Exit fullscreen mode

And that's it, now we can restore the document.

import User from './models/users';

const user = await User.create({
    name: 'John Doe',
});

await user.destroy();

const restoredUser = await User.restore(user.data.id);

console.log(restoredUser.data); // { id: 1, name: 'John Doe', createdAt: '2021-01-01', updatedAt: '2021-01-01', restoredAt: '2021-01-01', deletedAt: '2021-01-01', _id: '5ff5c1c0e4b0e8b8b8b8b8b8' }
Enter fullscreen mode Exit fullscreen mode

And that's it!

🎨 Conclusion

In this article, we learned how to implement the recycle bin and move the model to it and learnt how to restore it again.

🚀 Project Repository

You can find the latest updates of this project on Github

😍 Join our community

Join our community on Discord to get help and support (Node Js 2023 Channel).

🎞️ Video Course (Arabic Voice)

If you want to learn this course in video format, you can find it on Youtube, the course is in Arabic language.

💰 Bonus Content 💰

You may have a look at these articles, it will definitely boost your knowledge and productivity.

General Topics

Packages & Libraries

React Js Packages

Courses (Articles)

Top comments (0)