Implementing the Persistence Layer - Part 2: Repositories

Implementing the Persistence Layer - Part 2: Repositories

Clean Architecture Series - Frameworks Layer

The repository pattern provide a layer of abstraction between the application code and the data storage. Instead of directly interacting with the database, your application interacts with the repository, which handles all the database operations.

Policy Repository

At this point we need to use the "IPolicyRepository" interface located in the Domain layer which defines the PolicyRepository behavior.

Go to frameworks folder and create a new folder named "repositories" with a subfolder named "policy" into the "persistence" folder. And create a file named "policy.repository.ts" into the "repositories" folder:

// .src/frameworks/persistence/repositories/policy/policy.repository.ts
import { IPolicyRepository } from "@/domain/policy/policy-repository.interface";
import { PolicyEntity } from "@/domain/policy/policy.entity";
import { IDatabase } from "../../db/database.interface";
import { PolicyMapper } from "../../mappers/policy.mapper";

export class PolicyRepository implements IPolicyRepository {
  constructor(private db: IDatabase) {}

  async save(policy: PolicyEntity): Promise<void> {
    // Mapping the PolicyEntity to PolicyModel
    const policyModel = PolicyMapper.toPersistence(policy);
    // Get columns name from the PolicyModel
    const columns = Object.keys(policyModel);
    // Get columns values from the PolicyModel
    const values = Object.values(policyModel);
    // Placeholder to set the column values
    const placeholder = Array(columns.length).fill("?");
    // Preparing the SQL statement
    const sql = `INSERT INTO policy (${columns.join(",")}) VALUES (${placeholder.join(",")})`;
    // Execute the statement into DB
    await this.db.query(sql, values);
  }
}

Let's explain the PolicyRepository Class:

  • constructor: Rather than directly creating an instance of the Database, we define an interface IDatabase for the database connection and use Constructor Dependency Injection to manage it.

  • save: Encapsulate the logic to create the SQL statement with the PolicyEntity information and execute the query into the database. Also, this method map the PolicyEntity to PolicyModel to get the db object representation before persist it.

\By implementing IPolicyRepository interface we must implement all methods. In our case, we only have the "save" method.*

The Policy Mapper

The PolicyMapper is used to convert data between database and domain layer. It helps us in decoupling the source and target objects by providing a separate mapper method that handles the conversion logic.

Go to frameworks folder and create a new folder named "mappers" into the "persistence" folder. And create a file named "policy.mapper.ts":

// ./src/frameworks/persistence/mappers/policy.mapper.ts

import { PolicyEntity } from "@/domain/policy/policy.entity";
import { PolicyModel } from "../db/sqlite/policy.model";

export class PolicyMapper {
  static toDomain(data: PolicyModel): PolicyEntity {
    const { start_date, price: _, ...rest } = data;
    return new PolicyEntity({
      ...rest,
      startDate: start_date,
    });
  }

  static toPersistence(policy: PolicyEntity): PolicyModel {
    const { startDate: start_date, ...rest } = policy.unmarshalled();
    return {
      ...rest,
      start_date,
    };
  }
}

toDomain: This method converts PolicyModel object to a PolicyEntity object by applying the conversion logic for startDate (taking the start date information from the start_date column)

toPersistence: This method converts PolicyEntity object to a PolicyModel object by applying the conversion logic for start_date (taking the start date information from the startDate property)

In conclusion, Using a mapper we hide the logic to map between DB and Domain make the conversion logic reusable and testable.

See you in the next article.

Did you find this article valuable?

Support Max Martínez Cartagena by becoming a sponsor. Any amount is appreciated!