DEV Community

Hasan Zohdy
Hasan Zohdy

Posted on

31-Nodejs Course 2023: Database Models: Create Base Model

So we got to know about database models in the previous article, now let's create a base model that we'll use to create other models.

Model Structure

Before we getting started, let's illustrate how the model structure works, we have 4 main parts:

  • Static Variables: These variables will be access directly without creating an instance of the model, for example, the collection name.
  • Static Methods: These methods will be access directly without creating an instance of the model, for example, the find method.
  • Instance Variables: These variables will be access after creating an instance of the model, for example, the name variable.
  • Instance Methods: These methods will be access after creating an instance of the model, for example, the save method.

Static variables will be mainly for the meta data of the model which will be used to collect information about the model like the collection name, the initial id value and so on.

The static method will be used for quick access and operations that are not related to a specific instance of the model, for example, the find method.

Base Model

Go to src/core/database folder and create model folder inside it then create model.ts file.

// src/core/database/model/model.ts
export default abstract class Model {
  /**
   * Collection Name
   */
  public static collectionName = "";
}
Enter fullscreen mode Exit fullscreen mode

Nothing fancy here, just a base model class that we'll extend in other models.

We're making it abstract because we don't want to use it directly, we'll use it as a base class for other models to inherit from.

The collection name will be a static property that we'll override in the child classes so we can access the collection name from the model class directly without creating an instance of it.

You might wonder that we did a file called model.ts inside model directory not index.ts that's because we're going to create more files inside the model directory so we'll make the index.ts file for the exports only.

Now let's create the index.file and export the base model from it.

// src/core/database/model/index.ts
export { default as Model } from './model';
Enter fullscreen mode Exit fullscreen mode

Then go to database/index.ts file and export all from the model/index.ts fike.

// src/core/database/index.ts
export * from './model';
Enter fullscreen mode Exit fullscreen mode

This will allow us to access the Model class by importing it like this:

import { Model } from 'core/database';
Enter fullscreen mode Exit fullscreen mode

Users Model

Now let's create a new file called user.ts in src/app/users/models directory.

// src/app/users/models/user.ts
import { Model } from 'core/database';

export default class User extends Model {
  /**
   * Collection Name
   */
  public static collectionName = "users";
}
Enter fullscreen mode Exit fullscreen mode

Now we have a base model and a user model that extends it then we added the collection name to the user model.

Pretty easy, nothing fancy here but still these are just an empty files without soul (not database yet).

We can now get the collection name directly like this:

import User from 'app/users/models/user';

console.log(User.collectionName); // users
Enter fullscreen mode Exit fullscreen mode

Let's create its soul.

Database Connection

Heading back to our base model, let's define a database connection property.

// src/core/database/model/model.ts
import connection, { Connection } from './../connection';

export default abstract class Model {
  /**
   * Collection Name
   */
  public static collectionName = "";

  /**
   * Database Connection
   */
  public static connection: Connection = connection;
}
Enter fullscreen mode Exit fullscreen mode

We'll use our own connection so we can use it to access the connection (If needed) and we can of course access the database handler directly from it.

Collection Query

Now let's define a collection method to return the collection query handler.

// src/core/database/model/model.ts
import { Collection } from "mongodb";
import connection, { Connection } from './../connection';

export default abstract class Model {
  /**
   * Collection Name
   */
  public static collectionName = "";

  /**
   * Database Connection
   */
  public static connection: Connection = connection;

  /**
   * Get Collection query
   */
  public static query(): Collection {
    return this.connection.database.collection(this.collectionName);
  }
}
Enter fullscreen mode Exit fullscreen mode

In javascript, accessing static properties can be used with this keyword.

We need to make sure that the connection's database property is always exists as it may throw an error as it is possibly undefined.

// src/core/database/connection.ts
  /**
   * Database instance
   */
  // 👇🏻Replace the ?: with !:
  public database!: Database;
Enter fullscreen mode Exit fullscreen mode

So far so good, now we're don with static methods, let's implement the non static ones that we need.

At this point, we can now access the users collection like this:

import User from 'app/users/models/user';

// get the collection query
const usersCollection = User.query();

await usersCollection.find().toArray();
Enter fullscreen mode Exit fullscreen mode

Accessing Child Static Property From Parent Class

In the model it self when we create a new instance (object) of it, we need to access the collection of the model to make internal operations, like saving the user's data to the database, so we need to access the query method from the child class.

We can't access the static property of a child class from the parent class if we created an instance of that child, so we need to do a trick to access it.

To access the child class static members, we'll use this.constructor feature.

// src/core/database/model/model.ts
import connection, { Connection } from './../connection';

export default abstract class Model {
  /**
   * Collection Name
   */
  public static collectionName = "";

  /**
   * Database Connection
   */
  public static connection: Connection = connection;

  /**
   * Get Collection Name
   */
  public getCollectionName(): string {
    return this.constructor.collectionName;
  }
}
Enter fullscreen mode Exit fullscreen mode

But Typescript will start complaining, because it doesn't know that the constructor property is a class.

To fix this, we'll use the typeof keyword to tell Typescript that the constructor property is a class.

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

export default abstract class Model {
  // ...

  /**
   * Get Collection Name
   */
  public getCollectionName(): string {
    return (this.constructor as typeof Model).collectionName;
  }
}
Enter fullscreen mode Exit fullscreen mode

We can wrap it in a private method so we can use it in other methods by passing only the static property that we need to get.

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

export default abstract class Model {
  // ...

  /**
   * Get Static Property
   */
  protected getStaticProperty<T>(property: keyof typeof Model): T {
    return (this.constructor as any)[property];
  }

  /**
   * Get Collection Name
   */
  public getCollectionName(): string {
    return this.getStaticProperty('collectionName');
  }
}
Enter fullscreen mode Exit fullscreen mode

The keyof typeof Model will tell Typescript that the property parameter is a key of the Model class so we can use the auto complete feature to get the static property that we need to get.

Getting Connection, Database and Collection Query

Now we can use the getStaticProperty method to get the connection, database and collection query.

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

export default abstract class Model {
  // ...
  /**
   * Get Collection Name
   */
  public getCollectionName(): string {
    return this.getStaticProperty("collectionName");
  }

  /**
   * Get database connection
   */
  public getConnection(): Connection {
    return this.getStaticProperty("connection");
  }

  /**
   * Get database instance
   */
  public getDatabase(): Database {
    return this.getConnection().database;
  }

  /**
   * Get Collection Query
   */
  public getQuery(): Collection {
    return this.getStaticProperty("query");
  }
}
Enter fullscreen mode Exit fullscreen mode

So our final model class will look like this:

import { Collection } from "mongodb";
import { Database } from "../database";
import connection, { Connection } from "./../connection";

export default abstract class Model {
  /**
   * Collection Name
   */
  public static collectionName = "";

  /**
   * Database Connection
   */
  public static connection: Connection = connection;

  /**
   * Get Collection query
   */
  public static query(): Collection {
    return this.connection.database.collection(this.collectionName);
  }

  /**
   * Get Collection Name
   */
  public getCollectionName(): string {
    return this.getStaticProperty("collectionName");
  }

  /**
   * Get database connection
   */
  public getConnection(): Connection {
    return this.getStaticProperty("connection");
  }

  /**
   * Get database instance
   */
  public getDatabase(): Database {
    return this.getConnection().database;
  }

  /**
   * Get Collection Query
   */
  public getQuery(): Collection {
    return this.getStaticProperty("query");
  }

  /**
   * Get Static Property
   */
  protected getStaticProperty<T>(property: keyof typeof Model): T {
    return (this.constructor as any)[property];
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's give it a try

import User from 'app/users/models/user';

// get the collection query
const user = new User;

// get the collection name
user.getCollectionName();

// get the database connection
user.getConnection();

// get the database instance
user.getDatabase();

// get the collection query
user.getQuery();
Enter fullscreen mode Exit fullscreen mode

We can split it into 4 parts:

  • Public Static Variables/Properties: to define the collection name and the connection.
  • Public Static Methods: to define the collection query.
  • Public Methods: to get the collection name, connection, database and collection query.
  • Protected Methods: to get the static property of the child class.

The protected method is intended to not be used outside the model or one of its children as its an internal operation.

🎨 Conclusion

We've learned couple of good things here today, how and why to use static and non static methods, how to access static properties from a child class in the parent class, and how to use the this.constructor feature.

Then we created base model that allows us to get the collection query of the current model's collection name.

Then we added another feature to access the collection query or any other static methods when we create a new instance of the model.

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