DEV Community

Cover image for 49-Nodejs Course 2023: Database Models: Dirty Columns
Hasan Zohdy
Hasan Zohdy

Posted on

49-Nodejs Course 2023: Database Models: Dirty Columns

As we are now done with casting, there is a catch here, we don't need to make any cast to any column, unless the column's value has changed, so we need to know which columns have changed, and this is what we call dirty columns.

Dirty Columns

Dirty columns are the columns that have changed, so we need to know which columns have changed, and this is what we call dirty columns.

Data are changed through multiple methods, set, replace and merge methods.

We also have the originalData property that holds the original data, so we can compare the original data with the current data to know which columns have changed.

// src/core/database/model/model.ts
import {
  areEqual,
  except,
  get,
  merge,
  only,
  set,
} from "@mongez/reinforcements";

  /**
   * Check if the given column is a dirty column
   */
  public isDirty(column: string) {
    return areEqual(get(this.originalData, column), get(this.data, column));
  }
Enter fullscreen mode Exit fullscreen mode

Here we checked for the column in both originalData and data and compared them, if they are equal, then the column is not dirty, otherwise, it is dirty.

We used areEqual function from @mongez/reinforcements package, it is a deep equal function, so it will compare the values deeply either objects, arrays, strings and so on.

But what about the new models, the original data are the same as the current data, so we need to check also if the model is relatively new one or not, if so then all columns are dirty.

// src/core/database/model/model.ts
  /**
   * Check if the given column is a dirty column
   */
  public isDirty(column: string) {
    if (this.isNewModel()) return true;

    return areEqual(get(this.originalData, column), get(this.data, column));
  }
Enter fullscreen mode Exit fullscreen mode

We used isNewModel method to check if the model is new or not, if so then all columns are dirty.

Now let's implement the 'isNewModel' method.

// src/core/database/model/model.ts
  /**
   * Check if the model is new or not
   */
  public isNewModel() {
    return !this.data._id || (this.data._id && this.isRestored);
  }
Enter fullscreen mode Exit fullscreen mode

Actually, we can also use it in the save method as well.


  /**
   * 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
    if (!this.isNewModel()) {
      // 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();

      this.castData();

      await queryBuilder.update(
        this.getCollectionName(),
        {
          _id: this.data._id,
        },
        this.data,
      );
    } else {
      // creating a new document in the database
      const generateNextId =
        this.getStaticProperty("generateNextId").bind(Model);

      // check for default values and merge it with the data
      this.checkDefaultValues();

      // 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.castData();

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

Now we'll go back to the cast method and check if the column is a dirty column, if so then we'll cast it.

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

  /**
   * Cast data before saving
   */
  protected castData() {
    for (const column in this.casts) {
      if (!this.isDirty(column)) continue;

      let value = this.get(column);

      if (value === undefined) continue;

      const castType = this.casts[column];

      if (typeof castType === "function") {
        value = castType(column, value, this);
      } else {
        value = this.castValue(value, castType);
      }

      this.set(column, value);
    }
  }
Enter fullscreen mode Exit fullscreen mode

And That's it.

Update the original data on save

The original data is being captured from the constructor, but we need to replace it again once the user perform a save so we can make it the latest stable data for the current model.

// 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
    if (!this.isNewModel()) {
      // 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();

      this.castData();

      await queryBuilder.update(
        this.getCollectionName(),
        {
          _id: this.data._id,
        },
        this.data,
      );
    } else {
      // creating a new document in the database
      const generateNextId =
        this.getStaticProperty("generateNextId").bind(Model);

      // check for default values and merge it with the data
      this.checkDefaultValues();

      // 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.castData();

      this.data = await queryBuilder.create(
        this.getCollectionName(),
        this.data,
      );
    }

    // update the original data to equal current data
    this.originalData = this.data;
  }
Enter fullscreen mode Exit fullscreen mode

Keeping Initial Data

What if we want to keep the very first data that were injected in the model? well, in that case we can create a new property called initialData and assign it the value of the data that were injected in the constructor.

// src/core/database/model/model.ts
export default abstract class Model extends CrudModel {
  /**
   * Model Document Initial data
   */
  public initialData: Partial<ModelDocument> = {};

  /**
   * Constructor
   */
  public constructor(public originalData: Partial<ModelDocument> = {}) {
    //
    super();
    this.data = { ...this.originalData };

    this.initialData = { ...this.originalData };
  }
Enter fullscreen mode Exit fullscreen mode

In that sense, now the initial value is kept in our model untouched.

🎨 Conclusion

In this lesson, we learned how to implement dirty columns, and how to check if a column is dirty or not.

We also made an update to the save method to update the original data once the user perform a save so if we want to change the data again after the save method, we can now tell that this column is dirty or not after the last save.

🚀 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)

Oldest comments (0)