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
│ ├── employeeWithRelations.ts
│ └── overtimeWithRelations.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 & {
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 " ] {
const result = await prismaClient . employee . create ( {
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
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 " ] {
const result = await prismaClient . employee . findMany ( {
const data = Helpers . fromObjectToSchema (EmployeeSchema . SchemaArray)(result)
export function findManyWithRelations ( prismaClient : PrismaClient ) : EmployeeRepository[ " findManyWithRelations " ] {
const result = await prismaClient . employee . findMany ( {
const data = Helpers . fromObjectToSchema (EmployeeWithRelationsSchema . SchemaArray)(result)
export function findById ( prismaClient : PrismaClient ) : EmployeeRepository[ " findById " ] {
const result = await prismaClient . employee . findUnique ( {
return Helpers . fromObjectToSchema (EmployeeSchema . Schema)(result)
export function findByIdWithRelations ( prismaClient : PrismaClient ) : EmployeeRepository[ " findByIdWithRelations " ] {
const result = await prismaClient . employee . findUnique ( {
return Helpers . fromObjectToSchema (EmployeeWithRelationsSchema . Schema)(result)
Updating Employee functions
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 ( {
return Helpers . fromObjectToSchema (EmployeeSchema . Schema)(result)
export function updatePartial ( prismaClient : PrismaClient ) : EmployeeRepository[ " updatePartial " ] {
return async ( id , data ) => {
const result = await prismaClient . employee . update ( {
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
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 " ] {
const result = await prismaClient . employee . update ( {
return Helpers . fromObjectToSchema (EmployeeSchema . Schema)(result)
export function hardRemoveById ( prismaClient : PrismaClient ) : EmployeeRepository[ " hardRemove " ] {
const result = await prismaClient . employee . delete ( {
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 {
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)
ด้วยนะ