Build a JSON Database in Node.js (Part 7): Delete Records Softly & Hardly

36 min read·Dec 5, 2025

Article banner

In this part, we'll implement a method for deleting records, including soft deletion by default.

Ready? Let's build!

Delete records

Within the Model class, let's define a new method named delete() responsible for deleting records from the associated model's table:

model.js
// ...

export default class Model {
  // ...

  delete(filters = {}) {
    //
  }
}

This method takes as a single parameter:

  • filters <Object>: The list of filters identical to the filters.where object used in the find() method.

Example

Let's consider the following query:

await Emails.delete({
  folder: { $eq: 'bin' },
  read: { $eq: false }
});

Which in plain English translates to:

"Delete all the records from the emails table where folder equals 'bin' and read to false."

And in SQL to:

DELETE FROM emails WHERE folder = 'bin' AND read = FALSE;

Check the method's parameters

Within this method, let's throw an error if the filters parameter isn't an object.

model.js
// ...

export default class Model {
  // ...

  delete(filters = {}) {
    if (!isPlainObject(filters)) {
      throw new Error('Model: delete(): "filters" must be an object');
    }
  }
}

Filter records

Let's iterate on the records of the model's table in reverse order to keep the indices of the not-yet-visited records unchanged and use the #evalFilters() method to check if the current record matches the list of filters.

model.js
// ...

export default class Model {
  // ...

  delete(filters = {}) {
    // ...

    for (let i = this.#table.length - 1 ; i >= 0 ; i--) {
      let record = this.#table[i];
      
      if (this.#evalFilters(record, filters)) {
        //
      }
    }
  }
}

Let's:

  1. Declare a deletedCount variable to keep track of the number of records deleted.
  2. Remove the current record from the table if the list of filters are a match.
  3. Increment the deletions counter by 1.
model.js
// ...

export default class Model {
  // ...

  delete(filters = {}) {
    // ...
    let deletedCount = 0;

    for (let i = this.#table.length - 1 ; i >= 0 ; i--) {
      let record = this.#table[i];
      
      if (this.#evalFilters(record, filters)) {
        this.#table.splice(i, 1);
        deletedCount++;
      }
    }
  }
}

Finally, let's:

  1. Declare the delete() method as asynchronous.
  2. Use the #saveToDisk() method to persist the changes on disk if at least one record has been deleted.
  3. Return the deletions counter.
model.js
// ...

export default class Model {
  // ...

  async delete(filters = {}) {
    // ...

    if (deletedCount > 0) {
      await this.#saveToDisk();
    }
    return deletedCount;
  }
}

Enable soft deletions

Soft deletion is the practice of marking records as deleted instead of physically and irreversibly removing them, which in turn allows developers to undo deletions whenever needed and keep data for audits/history.

Update the define() method

To implement this mechanism, let's first update the define() method of the RowStackDB class by adding an is_deleted property to the validation schema object, with a default value set to false.

engine.js
// ...

export default class RowStackDB {
  // ...

  define(name, attributes) {
    // ...
    this.#schemas[name] = Joi.object({
      ...schema,
      is_deleted: Joi.boolean().default(false).required()
    });
  }

  // ...
}

Update the create() method

Let's update the create() method of the Model class by adding an is_deleted property (set to false by default) to the validated object to account for the same addition in the validation schema object.

model.js
// ...

export default class Model {
  // ...

  async create(record = {}) {
    // ...

    const { value, error } = this.#schema.validate({
      ...record,
      is_deleted: false
    }, {
      abortEarly: false
    });
    // ...
  }
}

Update the find() method

Let's update the find() method of the Model class by filtering out the records whose is_deleted property is set to true, and remove this property from the objects in the results set to keep it "hidden".

model.js
// ...

export default class Model {
  // ...

  find(filters = {}) {
    let results = this.#table
      .map(record => ({ ...record }))
      .filter(result => !result.is_deleted && this.#evalFilters(result, filters.where))
      .map(({ is_deleted, ...result }) => result);

    // ...
  }
}

Update the update() method

Let's update the update() method of the Model class by filtering out the records whose is_deleted property is set to true.

model.js
// ...

export default class Model {
  // ...

  async update(filters = {}, values = {}) {
    // ...

    for (let record of this.#table) {
      if (!record.is_deleted && this.#evalFilters(record, filters)) {
        // ...
      }
    }

    // ...
  }
}

Update the delete() method

Let's:

  1. Update the signature of the delete() method by adding a new parameter named options with a default property named soft set to true used to mark records as "softly deleted" by default.
  2. Throw an error if the options parameter is not an object.
model.js
// ...

export default class Model {
  // ...

  async delete(filters = {}, options = { soft: true }) {
    if (!isPlainObject(filters)) {
      throw new Error('Model: delete(): "filters" must be an object');
    } else if (!isPlainObject(options)) {
      throw new Error('Model: delete(): "options" must be an object');
    }

    // ...
  }
}

Finally, let's:

  1. Filter out the records whose is_deleted property is set to true.
  2. Physically remove the matching records from the table if the options.soft property is set to false.
  3. Otherwise, set their is_deleted property to true.
model.js
// ...

export default class Model {
  // ...

  async delete(filters = {}, options = { soft: true }) {
    // ...

    for (let i = this.#table.length - 1 ; i >= 0 ; i--) {
      let record = this.#table[i];
      
      if (!record.is_deleted && this.#evalFilters(record, filters)) {
        if (options.soft === false) {
          this.#table.splice(i, 1);
        } else {
          record.is_deleted = true;
        }
        deletedCount++;
      }
    }

    // ...
  }
}

Conclusion

Congratulations!

You now have a fully functional database engine with an API that allows you to define tables and perform CRUD operations on them.