Skip to content
CodeSook
CodeSook

10. Implement Employee Repository


Let’s create interface for Employee Repository

อย่างที่บอกไปว่า prismaClient สามารถแทน repository ได้เลย ถ้า app เราเล็กๆ เราก็ข้ามส่วนนี้ได้
แต่ว่าเราอยากได้อะไรที่มันยืดหยุ่นขึ้นไปอีก เพราะ app ที่ทำ มันมักจะมีอะไรที่ซับซ้อนกว่าตัวอย่างนี้มากๆๆ

เราจะมาสร้าง interface หรือ type กันก่อน

ตรงนี้สำคัญมาก

เราจะต้องเห็นภาพรวมก่อนว่าจะทำอะไรบ้าง จะมีอะไรบ้าง
โดยคนที่สร้าง interface เหล่านี้จะเป็นคนที่ต้องทำงานในส่วนของ Business logic หรือก็คือคนที่เขียน services นั่นแหละ
เค้าต้องบอกว่าอยากได้อะไรบ้าง โดยอิงจาก Schema ที่ได้ทำไว้

ส่วนคนที่ทำงานในส่วนของ Repository ก็ต้องมีหน้าที่เอา data ไปเก็บ หรือ query ออกมาให้ได้ตามที่ Business (services) ต้องการ

Files and Folders

types folder
src
├── configure/
├── controllers/
├── index.ts
├── repositories
├── employees
├── creates.ts
├── finds.ts
├── index.ts
├── removes.ts
└── updates.ts
├── overtimes/
└── prisma.ts
9 collapsed lines
├── schema/
├── branded.ts
├── employee.ts
├── employeeWithRelations.ts
├── general.ts
├── helpers.ts
├── index.ts
├── overtime.ts
└── overtimeWithRelations.ts
├── services/
└── types
├── repositories
├── employee.ts
└── overtime.ts
└── services/
types/repositories/employee.ts
types/repositories/employee.ts
import type { Branded, EmployeeSchema, EmployeeWithRelationsSchema } from "../../schema/index.js"
type Employee = EmployeeSchema.Employee
type EmployeeArray = EmployeeSchema.EmployeeArray
export type EmployeeWithoutId = Omit<Employee, "id">
export type CreateEmployeeDto = Omit<Employee, "id" | "createdAt" | "updatedAt" | "deletedAt" | "_tag">
export type UpdateEmployeeDto = CreateEmployeeDto & {
id?: Employee["id"]
}
export type EmployeeRepository = {
create: (data: CreateEmployeeDto) => Promise<Employee>
findById: (id: Branded.EmployeeId) => Promise<Employee | null>
findByIdWithRelations: (id: Branded.EmployeeId) => Promise<EmployeeWithRelationsSchema.EmployeeWithRelations | null>
findMany: () => Promise<EmployeeArray>
findManyWithRelations: () => Promise<EmployeeWithRelationsSchema.EmployeeWithRelationsArray>
update: (id: Branded.EmployeeId, data: UpdateEmployeeDto) => Promise<Employee | null>
updatePartial: (id: Branded.EmployeeId, data: Partial<UpdateEmployeeDto>) => Promise<Employee | null>
remove: (id: Branded.EmployeeId) => Promise<Employee | null>
hardRemove: (id: Branded.EmployeeId) => Promise<Employee | null>
}

Create Employee functions

repositories/employees/creates.ts
import type { PrismaClient } from "@prisma/client"
import type { EmployeeRepository } from "../../types/repositories/employee.js"
import { EmployeeSchema, Helpers } from "../../schema/index.js"
export function create(prismaClient: PrismaClient): EmployeeRepository["create"] {
return async (data) => {
const result = await prismaClient.employee.create({
data,
})
return Helpers.fromObjectToSchema(EmployeeSchema.Schema)(result)
}
}

จะเห็นว่าเราสร้าง function ชื่อว่า create() แล้วรับ parameter เป็น prismaClient มา
ตรงนี้เราเรียกว่า dependency injection
เราจะไม่ได้เอา prismaClient ที่สร้างไว้ก่อนหน้านี้มาใช้งาน ถ้าทำแบบนั้นการเขียน Test ของเราจะยากไปเลย

ตอน app ใช้งานบน production server เราก็ส่ง prismaClient ที่เราสร้างไว้เข้ามา
แต่ตอนที่ Test เราจะใช้ prismaClient อีกตัวนึง แล้วก็ mock data ตามต้องการได้เลย ทำให้สะดวกต่อการเขียน test มากกว่า

แล้ว function create() return function อีกทีนึง ซึ่ง function จะถูกเอาไปใช้ที่ services อีกทีนึง

Finding Employee functions

อันนี้ก็จะทำเหมือนกันเลย ทำ Dependency Injection กับ prismaClient เหมือนกัน

ตอน query เราต้องอย่าลืมว่า column deletedAt ต้องเป็น null ด้วยนะ

ในที่นี้เราไม่มี function ที่เอาไว้ query data ที่เป็น soft deleted นะ เพราะไม่ได้มีใน requirements
ถ้าอยากได้ก็ทำไว้ได้แหละ แต่มันจะไม่ได้อยู่ใน interface ที่ business ออกแบบมา ก็ทำไว้เฉยๆ เดี๋ยว tree-shaking มันก็ลบไปให้เองตอน build bundle

repositories/employees/finds.ts
repositories/employees/finds.ts
import type { PrismaClient } from "@prisma/client"
import type { EmployeeRepository } from "../../types/repositories/employee.js"
import { EmployeeSchema, EmployeeWithRelationsSchema, Helpers } from "../../schema/index.js"
export function findMany(prismaClient: PrismaClient): EmployeeRepository["findMany"] {
return async () => {
const result = await prismaClient.employee.findMany({
where: {
deletedAt: null,
},
})
const data = Helpers.fromObjectToSchema(EmployeeSchema.SchemaArray)(result)
return data
}
}
export function findManyWithRelations(prismaClient: PrismaClient): EmployeeRepository["findManyWithRelations"] {
return async () => {
const result = await prismaClient.employee.findMany({
include: {
overtimes: true,
},
where: {
deletedAt: null,
},
})
const data = Helpers.fromObjectToSchema(EmployeeWithRelationsSchema.SchemaArray)(result)
return data
}
}
export function findById(prismaClient: PrismaClient): EmployeeRepository["findById"] {
return async (id) => {
const result = await prismaClient.employee.findUnique({
where: {
deletedAt: null,
id,
},
})
if (result === null)
return null
return Helpers.fromObjectToSchema(EmployeeSchema.Schema)(result)
}
}
export function findByIdWithRelations(prismaClient: PrismaClient): EmployeeRepository["findByIdWithRelations"] {
return async (id) => {
const result = await prismaClient.employee.findUnique({
include: {
overtimes: true,
},
where: {
deletedAt: null,
id,
},
})
if (result === null)
return null
return Helpers.fromObjectToSchema(EmployeeWithRelationsSchema.Schema)(result)
}
}

Updating Employee functions

repositories/employees/updates.ts
repositories/employees/updates.ts
import type { PrismaClient } from "@prisma/client"
import type { EmployeeRepository } from "../../types/repositories/employee.js"
import { EmployeeSchema, Helpers } from "../../schema/index.js"
export function update(prismaClient: PrismaClient): EmployeeRepository["update"] {
return async (id, data) => {
const result = await prismaClient.employee.update({
data,
where: {
deletedAt: null,
id,
},
})
return Helpers.fromObjectToSchema(EmployeeSchema.Schema)(result)
}
}
export function updatePartial(prismaClient: PrismaClient): EmployeeRepository["updatePartial"] {
return async (id, data) => {
const result = await prismaClient.employee.update({
data,
where: {
deletedAt: null,
id,
},
})
return Helpers.fromObjectToSchema(EmployeeSchema.Schema)(result)
}
}

จาก code ด้านบนเรามี function update 2 อัน อันแรก update() ต้องส่ง data ทั้งหมดเข้ามา แล้วเราจะ update ทั้งหมด
ส่วนอันล่าง updatePartial() ก็ไม่จำเป็นต้องส่ง data มาทั้งหมด
ซึ่งก็ทำเผื่อไว้ ไม่ได้ใช้หรอก อยากให้เห็นว่าถ้าจะต้องทำจะทำแบบไหน

Remove Employee functions

ตรงนี้เราจะทำ 2 function

  • remove() อันนี้จะทำ soft delete การทำงานจะไม่ใช่การ delete จริงๆ แต่เป็นการ update column deletedAt ต่างหาก
  • hardRemove() อันนี้ไม่ได้อยู่ใน requirements แต่เพิ่มเข้ามาให้เห็นว่าถ้าจะลบจริงๆ ทำยังไง
repositories/employees/removes.ts
repositories/employees/removes.ts
import type { PrismaClient } from "@prisma/client"
import type { EmployeeRepository } from "../../types/repositories/employee.js"
import { EmployeeSchema, Helpers } from "../../schema/index.js"
export function remove(prismaClient: PrismaClient): EmployeeRepository["remove"] {
return async (id) => {
const result = await prismaClient.employee.update({
data: {
deletedAt: new Date(),
},
where: {
id,
},
})
return Helpers.fromObjectToSchema(EmployeeSchema.Schema)(result)
}
}
export function hardRemoveById(prismaClient: PrismaClient): EmployeeRepository["hardRemove"] {
return async (id) => {
const result = await prismaClient.employee.delete({
where: {
id,
},
})
return Helpers.fromObjectToSchema(EmployeeSchema.Schema)(result)
}
}

export all repositories functions

เราจะเอาทุกอย่างมา export ที่ index.ts พร้อมกับทำ Dependencies injection ด้วย

import type { PrismaClient } from "@prisma/client"
import type * as Types from "../../types/repositories/employee.js"
import * as Creates from "./creates.js"
import * as Finds from "./finds.js"
import * as Removes from "./removes.js"
import * as Updates from "./updates.js"
export default function initEmployeeRepository(prismaClient: PrismaClient): Types.EmployeeRepository {
return {
create: Creates.create(prismaClient),
findById: Finds.findById(prismaClient),
findByIdWithRelations: Finds.findByIdWithRelations(prismaClient),
findMany: Finds.findMany(prismaClient),
findManyWithRelations: Finds.findManyWithRelations(prismaClient),
hardRemove: Removes.hardRemoveById(prismaClient),
remove: Removes.remove(prismaClient),
update: Updates.update(prismaClient),
updatePartial: Updates.updatePartial(prismaClient),
}
}

จาก code ด้านบนจะเห็นว่าเราใช้ import * as .. from .. เรียกว่า namespace import ผมชอบใช้วิธีนี้ แทนการสร้าง object แล้วมี
properties ด้านใน การทำแบบนี้จะทำให้เราใช้ความสามารถ Tree-shaking ของ Bundler ได้ด้วย
และยังทำให้เราสร้าง function ที่ชื่อเดิม แต่อยู่คนละ namespace ได้ด้วย

การเขียนแบบนี้จะเห็นว่าคล้ายกับการใช้ Class มากๆ ใครจะลองใช้ Class ก็ไม่ว่ากัน แต่อยากให้ระวังเรื่องการ .bind(this) ด้วยนะ