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.