47-Nodejs Course 2023: Database Models: Casting Data

hassanzohdy

Hasan Zohdy

Posted on November 7, 2022

47-Nodejs Course 2023: Database Models: Casting Data

We added a feature previously where we can set the default value, but what about casting data?

What is Casting Data?

Casting data is when you want to change the type of data that is being stored in the database. For example, if you have a field that is a string, but you want to store it as a number, you can cast it to a number.

So we can change or mutate the data before it is stored in the database.

That's the whole idea.

Types of Casting

There are multiple types, as mostly all data types in JavaScript can be listed here, but we'll do a better thing.

We'll add standard casting types, and we'll add custom casting types, also we'll add a cast handler type as well so we can later add custom casting types.

Now let's define it's type in the types file.

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

/**
 * Custom cast handler
 */
export type CustomCast = (column: string, value: any, model: Model) => any;

/**
 * Cast types
 */
export type CastType =
  | "string"
  | "bool"
  | "boolean"
  | "number"
  | "int"
  | "integer"
  | "float"
  | "date"
  | "object"
  | "array"
  | CustomCast;

/**
 * Model Casts
 */
export type Casts = {
  [column: string]: CastType;
};
Enter fullscreen mode Exit fullscreen mode

The CastType has a pre-defined types such as string and number, but also has a CustomCast type which is a function that takes the column name, the value, and the model, and returns the casted value.

Custom Cast handler will allow us to make custom casting types, for example we can use it to generate hashed password for the user.

But let's start with the standard types first.

// src/core/database/model/model.ts
import { Document, ModelDocument, Casts } from "./types";

export default abstract class Model extends CrudModel {
  // ...
  /**
   * Cast types
   */
  protected casts: Casts = {};
}
Enter fullscreen mode Exit fullscreen mode

We defined a property called casts which is an object that has the column name as the key, and the cast type as the value.

Now the question that remains is how do we use it?

Using Casting

It's pretty obvious, we'll use it in the save method directly before the create or update process.

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

export default abstract class Model extends CrudModel {
  // ...

  /**
   * Cast types
   */
  protected casts: Casts = {};

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

      // 👇🏻 cast data
      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;
      }

      // 👇🏻 cast data
      this.castData();

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

What we did here, We added a method called castData which will cast the data before it is saved in the database.

Now let's define the castData method.

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

export default abstract class Model extends CrudModel {
  // ...

  /**
   * Cast types
   */
  protected casts: Casts = {};

  /**
   * Cast data
   */
  protected castData() {
    // loop through the casts
    for (const column in this.casts) {
      // get the cast type
      const castType = this.casts[column];

      // get the value
      const value = this.data[column];

      // check if the value is undefined
      if (value === undefined) continue;

      // check if the cast type is a function
      if (typeof castType === "function") {
        // cast the value
        this.data[column] = castType(column, value, this);
      } else {
        // cast the value
        this.data[column] = this.castValue(castType, value);
      }
    }
  }

  /**
   * Cast value
   */
  protected castValue(castType: CastType, value: any) {
    switch (castType) {
      case "string":
        return String(value);
      case "bool":
      case "boolean":
        return Boolean(value);
      case "number":
        return Number(value);
      case "int":
      case "integer":
        return parseInt(value);
      case "float":
        return parseFloat(value);
      case "date":
        if (typeof value === "string") {
          // parse the date string
          return new Date(value);
        } else if (typeof value === "number") {
          // parse the date number
          return new Date(value * 1000);
        } else if (value instanceof Date) {
          // return the date
          return value;
        } else {
          // return the current date
          return new Date();
        }
      case "object":
        if (!value) return {};

        if (typeof value === "string") {
          return JSON.parse(value);
        }

        return value;
      case "array":
        if (!value) return [];

        if (typeof value === "string") {
          return JSON.parse(value);
        }

        return value;
      default:
        return value;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Woooohooo, what is the heck is going on here?

Let's break it down.

First, we loop through the casts, and get the cast type and the value.

Then we check if the value is undefined, if it is, then we continue to the next iteration.

Then we check if the cast type is a function, if it is, then we call the function and pass the column name, the value, and the model itself.

If the cast type is not a function, then we call the castValue method and pass the cast type and the value which will perform our standard casting.

Now let's understand the castValue method.

Here where is the best time to use the switch statement.

We check the cast type, and based on the cast type, we cast the value.

For example, if the cast type is date, then we check the value type, if it is a string, then we parse it as a date, if it is a number, then we parse it as a date, if it is a date, then we return it, and if it is none of the above, then we return the current date.

As you can see there are some casts are alias to each other like bool and boolean, int and integer, these are just for convenience, in that case we define multiple cases for the same cast type.

Now let's give it a try.

// src/app/users/models/user.ts
import { Model } from "core/database";
import { Casts, Document } from "core/database/model/types";

export default class User extends Model {
  /**
   * Collection name
   */
  public static collectionName = "users";

  /**
   * {@inheritDoc}
   */
  protected casts: Casts = {
    isActive: "boolean",
    isVerified: "boolean",
    joinDate: "date",
  };
}
Enter fullscreen mode Exit fullscreen mode

We defined here three casts, isActive will be casted to a boolean, email will be casted to a string, and joinDate will be casted to a date.

Now let's try to save a new user.

// src/app/users/routes.ts
import User from './models/user';

const user = await User.create({
  isVerified: "",
  joinDate: "2021-01-01",
  isActive: "1",
});

await user.save();

console.log(user.data); // { isVerified: false, joinDate: 2021-01-01T00:00:00.000Z, isActive: true }
Enter fullscreen mode Exit fullscreen mode

And That's it!

Actually, i just remembered one thing, the boolean casting we need to enhance it a little bit.


  /**
   * Cast value
   */
  protected castValue(castType: CastType, value: any) {
    switch (castType) {
      case "string":
        return String(value);
      case "bool":
      case "boolean":
        // if the value is `true` in string, then turn it into boolean true
        if (value === "true") return true;
        // if the value is `false` in string or zero, then turn it into boolean false
        if (value === "false" || value === "0" || value === 0) return false;
        return Boolean(value);
      case "number":
        return Number(value);
      case "int":
      case "integer":
        return parseInt(value);
      case "float":
        return parseFloat(value);
      case "date":
        if (typeof value === "string") {
          // parse the date string
          return new Date(value);
        } else if (typeof value === "number") {
          // parse the date number
          return new Date(value * 1000);
        } else if (value instanceof Date) {
          // return the date
          return value;
        } else {
          // return the current date
          return new Date();
        }
      case "object":
        if (!value) return {};

        if (typeof value === "string") {
          return JSON.parse(value);
        }

        return value;
      case "array":
        if (!value) return [];

        if (typeof value === "string") {
          return JSON.parse(value);
        }

        return value;
      default:
        return value;
    }
  }
Enter fullscreen mode Exit fullscreen mode

We added a little check here about the value, if its true in string, then we need to cast it as true as boolean value, also if the value is false in string or zero, then we need to cast it as false as boolean value.

🎨 Conclusion

In this article, we learned how to cast values in our models, and we learned how to use the switch statement in TypeScript in practice.

In our next article, we will learn how to use the custom cast and create password cast.

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

💖 💪 🙅 🚩
hassanzohdy
Hasan Zohdy

Posted on November 7, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related