47-Nodejs Course 2023: Database Models: Casting Data
Hasan Zohdy
Posted on November 7, 2022
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;
};
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 = {};
}
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,
);
}
}
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;
}
}
}
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",
};
}
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 }
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;
}
}
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
- Event Driven Architecture: A Practical Guide in Javascript
- Best Practices For Case Styles: Camel, Pascal, Snake, and Kebab Case In Node And Javascript
- After 6 years of practicing MongoDB, Here are my thoughts on MongoDB vs MySQL
Packages & Libraries
- Collections: Your ultimate Javascript Arrays Manager
- Supportive Is: an elegant utility to check types of values in JavaScript
- Localization: An agnostic i18n package to manage localization in your project
React Js Packages
Courses (Articles)
Posted on November 7, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.