Prisma layer
ใน Effect จะนิยมใช้ Layer
ในการจัดการกับ Dependencies ทั้งหลาย
เดี๋ยวค่อยๆดูกันไปนะครับ
เราจะทำ Prisma layer ก่อน
import { PrismaClient } from "@prisma/client"
import { Context , Layer } from "effect"
const prismaClient = new PrismaClient ()
export class PrismaClientContext extends Context . Tag ( "Repository/PrismaClientLayer" ) < PrismaClientContext , PrismaClient > () {
static Live = Layer . succeed ( this , prismaClient)
export default PrismaClientContext
จาก code ด้านบนเราสร้าง Layer
ที่เป็น class
นะครับ โดยจะ Extends มาจาก Context.Tag(<unique tag>)<[Name of the class], [Record of services]>()
ซึ่งเราต้องใส่ tag เข้าไปด้วย อย่าให้ซ้ำกับ Tag ของ service อื่นๆนะครับ
ตัวนี้จะช่วยให้เรารู้ว่า function ไหนต้องการ Dependencies อะไร
ส่วน static Live
ก็คือสร้าง Service จริงๆที่เราจะใช้งานใน production แต่จะยังไม่ได้ใช้ในตอนนี้ เก็บไว้ตรงนี้ก่อน
Schema convert helpers
เราได้ทำ Helper functions สำหรับใช้แปลง object ให้กลายเป็น Effect Schema ไปแล้ว
แต่ว่ามันก็อาจจะมีตอนที่แปลงได้ไม่สำเร็จ ซึ่ง Helpers เหล่านั้นมันจะ throw Error
ออกมาเลย ซึ่งเราไม่ได้ต้องการให้มัน Throw Error
เราก็จะใช้อีก function หนึ่งให้มันแปลง object เป็น Effect Schema โดยเอา Effect มาครอบไว้ จะได้ใช้ร่วมกับ Effect ส่วนอื่นๆได้แบบ seamless
src/schema/helpers.ts
import * as S from "effect/Schema"
export const fromObjectToSchema = S . decodeUnknownSync
export const fromSchemaToObject = S . encodeSync
export const fromObjectToSchemaEffect = S . decode
export const fromSchemaToObjectEffect = S . encode
Employee and Overtime Repository Errors
เราจะมา define Error กันก่อนในทุกๆ operations ของ Repositories
ทำไมต้องสร้าง Error ด้วย นั่นก็เพราะว่า PrismaClient เป็น Third party library ที่ใช้ Promise-based แต่เราใช้ Effect-based เราไม่สามารถไปแก้ code ของ PrismaClient ได้
ฉนั้นเราจะเขียน function มา wrap function ของ PrismaClient ในทุกๆ operations ที่เราเขียน
file and folders
เนื่องจากว่า Error เป็น class เราก็เลยสร้าง function ที่ช่วยให้เอาสร้าง Error class ได้ง่ายๆ
src/types/errors.helpers.ts
export function createErrorFactory < T extends new ( err : ErrorMsg ) => any > ( Self : T ) {
return ( msg : string ) => ( error ?: unknown ) => new Self ( { error , msg } )
src/types/errors/employee-errors.ts
import { Data } from "effect"
import { createErrorFactory , type ErrorMsg } from "../error.helpers.js"
export class CreateEmployeeError extends Data . TaggedError ( "CreateEmployeeError" ) < ErrorMsg > {
static new = ( msg ?: string ) => ( error ?: unknown ) => new CreateEmployeeError ( { error , msg } )
export class FindEmployeeByIdError extends Data . TaggedError ( "FindEmployeeByIdError" ) < ErrorMsg > {
static new = createErrorFactory ( this )
export class FindManyEmployeeError extends Data . TaggedError ( "FindManyEmployeeError" ) < ErrorMsg > {
static new = createErrorFactory ( this )
export class UpdateEmployeeError extends Data . TaggedError ( "UpdateEmployeeError" ) < ErrorMsg > {
static new = createErrorFactory ( this )
export class RemoveEmployeeError extends Data . TaggedError ( "RemoveEmployeeError" ) < ErrorMsg > {
static new = createErrorFactory ( this )
Employee Repository interface
เราจะมาทำ Interface สำหรับ Employee Repository กัน
เราไม่ได้เขียนใหม่นะ เราแค่แก้ types ให้ไปเป็น Effect
import type { Effect } from "effect"
import type { NoSuchElementException } from "effect/Cause"
import type { ParseError } from "effect/ParseResult"
import type { Branded , EmployeeSchema , EmployeeWithRelationsSchema } from "../../schema/index.js"
import type * as Errors from "../errors/employee-errors.js"
type Employee = EmployeeSchema . Employee
export type EmployeeRepository = {
create : ( data : EmployeeSchema . CreateEmployeeEncoded ) => Effect . Effect < Employee , Errors . CreateEmployeeError | ParseError >
findById : ( id : Branded . EmployeeId ) => Effect . Effect < Employee , Errors . FindEmployeeByIdError | ParseError | NoSuchElementException >
findByIdWithRelations : ( id : Branded . EmployeeId ) => Effect . Effect < EmployeeWithRelationsSchema . EmployeeWithRelations , Errors . FindEmployeeByIdError | ParseError | NoSuchElementException >
findMany : () => Effect . Effect < EmployeeSchema . EmployeeArray , Errors . FindManyEmployeeError >
findManyWithRelations : () => Effect . Effect < EmployeeWithRelationsSchema . EmployeeWithRelationsArray , Errors . FindManyEmployeeError >
update : ( id : Branded . EmployeeId , data : EmployeeSchema . UpdateEmployeeEncoded ) => Effect . Effect < Employee , Errors . UpdateEmployeeError | ParseError >
updatePartial : ( id : Branded . EmployeeId , data : Partial < EmployeeSchema . UpdateEmployeeEncoded > ) => Effect . Effect < Employee , Errors . UpdateEmployeeError >
remove : ( id : Branded . EmployeeId ) => Effect . Effect < Employee , Errors . RemoveEmployeeError >
hardRemove : ( id : Branded . EmployeeId ) => Effect . Effect < Employee , Errors . RemoveEmployeeError >
จะเห็นว่า Error ที่ใส่จะมี
Errors ของ operations นั้นๆ
ParseError
อันนี้คือ เรา Parse Effect Schema ไม่ผ่าน
NoSuchElementException
อันนี้จะเป็น Error ที่ใช้บอกว่าไม่มี element นั้นๆ ผมใช้อันนี้แทน null
ใครจะใช้ Option
แทนก็ได้
ตรงนี้สำหรับมือใหม่อาจจะยากที่จะบอกว่า Errors มันเป็นอะไรได้บ้าง success type เป็นอะไรได้บ้าง
เราอาจจะใช้วิธีสร้าง function นั้นๆขึ้นมาก่อน แล้วดูว่า return type เป็นอะไร แล้วค่อยเอามาใส่ใน interface อีกทีนึงก็ได้
แต่ถ้าทีมขนาดใหญ่อาจจะทำแบบนั้นได้ยาก
Employee Repository Create service
เรามาสร้าง function ที่เอาไว้ create Employee กัน โดยใช้ interface ที่ได้แก้ไปใช้ Effect
และ function นี้ก็เปลี่ยนมาใช้ Effect เช่นกัน
src/repositories/employees/creates.ts
export function create ( prismaClient : PrismaClient ) : EmployeeRepository[ " create " ] {
const result = await prismaClient . employee . create ( {
return Helpers . fromObjectToSchema (EmployeeSchema . Schema)(result)
export function create ( prismaClient : PrismaClient ) : EmployeeRepository[ " create " ] {
return data => Effect . tryPromise ( {
catch : Errors . CreateEmployeeError . new () ,
try : () => prismaClient . employee . create ( {
Effect . andThen (Helpers . fromObjectToSchemaEffect (EmployeeSchema . Schema)) ,
Effect . withSpan ( "create.employee.repository" ) ,
เนื่องจากเราเรียก function จาก third-party lib เราก็เลยจะใช้ Effect.tryPromise
เข้าไปด้วย
การทำงานก็ยังเหมือนเดิม แต่ผมแยกการ parse schema ออกไปอีก function หนึ่ง แล้วเอามาใช้ที่ Effect.andThen()
การทำงานของ function create()
นี้ก็จะประกอบไปด้วย function เล็กๆหลายๆ functions มาทำงานต่อๆกันเป็นลำดับ
ถ้ามี function อื่นๆอีกก็ใช้ Effect.andThen()
ไปเรื่อยๆ
ส่วน Effect.withSpan()
เป็นการสร้าง tracing span เอาไว้เดี๋ยวจะอธิบายอีกครั้งในภายหลังว่ามันเอาไว้ทำอะไร ในหัวข้อ Telemetry
Employee Repository Find, Update, Remove
มาทำ employee repository ที่เหลือกัน
find
src/repositories/employees/finds.ts
import type { PrismaClient } from "@prisma/client"
import type { EmployeeRepository } from "../../types/repositories/employee.js"
import { Effect } from "effect"
import { EmployeeSchema , EmployeeWithRelationsSchema , Helpers } from "../../schema/index.js"
import * as Errors from "../../types/errors/employee-errors.js"
export function findMany ( prismaClient : PrismaClient ) : EmployeeRepository[ " findMany " ] {
return () => Effect . tryPromise ( {
catch : Errors . FindManyEmployeeError . new () ,
try : () => prismaClient . employee . findMany ( {
Effect . andThen (Helpers . fromObjectToSchema (EmployeeSchema . SchemaArray)) ,
Effect . withSpan ( "find-many.employee.repository" ) ,
export function findManyWithRelations ( prismaClient : PrismaClient ) : EmployeeRepository[ " findManyWithRelations " ] {
return () => Effect . tryPromise ( {
catch : Errors . FindManyEmployeeError . new () ,
try : () => prismaClient . employee . findMany ( {
Effect . andThen (Helpers . fromObjectToSchema (EmployeeWithRelationsSchema . SchemaArray)) ,
Effect . withSpan ( "find-many-with-relations.employee.repository" ) ,
export function findById ( prismaClient : PrismaClient ) : EmployeeRepository[ " findById " ] {
return id => Effect . tryPromise ( {
catch : Errors . FindEmployeeByIdError . new () ,
try : () => prismaClient . employee . findUnique ( {
Effect . andThen (Effect . fromNullable) ,
Effect . andThen (Helpers . fromObjectToSchema (EmployeeSchema . Schema)) ,
Effect . withSpan ( "find-by-id.employee.repository" ) ,
export function findByIdWithRelations ( prismaClient : PrismaClient ) : EmployeeRepository[ " findByIdWithRelations " ] {
return id => Effect . tryPromise ( {
catch : Errors . FindEmployeeByIdError . new () ,
try : () => prismaClient . employee . findUnique ( {
Effect . andThen (Effect . fromNullable) ,
Effect . andThen (Helpers . fromObjectToSchemaEffect (EmployeeWithRelationsSchema . Schema)) ,
Effect . withSpan ( "find-by-id-with-relations.employee.repository" ) ,
update
src/repositories/employees/updates.ts
import type { PrismaClient } from "@prisma/client"
import type { EmployeeRepository } from "../../types/repositories/employee.js"
import { Effect } from "effect"
import { EmployeeSchema , Helpers } from "../../schema/index.js"
import * as Errors from "../../types/errors/employee-errors.js"
export function update ( prismaClient : PrismaClient ) : EmployeeRepository[ " update " ] {
return ( id , data ) => Effect . tryPromise ( {
catch : Errors . UpdateEmployeeError . new () ,
try : () => prismaClient . employee . update ( {
Effect . andThen (Helpers . fromObjectToSchema (EmployeeSchema . Schema)) ,
Effect . withSpan ( "update.overtime.repository" ) ,
export function updatePartial ( prismaClient : PrismaClient ) : EmployeeRepository[ " updatePartial " ] {
return ( id , data ) => Effect . tryPromise ( {
catch : Errors . UpdateEmployeeError . new () ,
try : () => prismaClient . employee . update ( {
Effect . andThen (Helpers . fromObjectToSchema (EmployeeSchema . Schema)) ,
Effect . withSpan ( "update-partial.overtime.repository" ) ,
remove
src/repositories/employees/removes.ts
import type { PrismaClient } from "@prisma/client"
import type { EmployeeRepository } from "../../types/repositories/employee.js"
import { Effect } from "effect"
import { EmployeeSchema , Helpers } from "../../schema/index.js"
import * as Errors from "../../types/errors/employee-errors.js"
export function remove ( prismaClient : PrismaClient ) : EmployeeRepository[ " remove " ] {
return id => Effect . tryPromise ( {
catch : Errors . RemoveEmployeeError . new () ,
try : () => prismaClient . employee . update ( {
Helpers . fromObjectToSchema (EmployeeSchema . Schema) ,
Effect . withSpan ( "remove.overtime.repository" ) ,
export function hardRemoveById ( prismaClient : PrismaClient ) : EmployeeRepository[ " hardRemove " ] {
return id => Effect . tryPromise ( {
catch : Errors . RemoveEmployeeError . new () ,
try : () => prismaClient . employee . delete ( {
Helpers . fromObjectToSchema (EmployeeSchema . Schema) ,
Effect . withSpan ( "hard-remove.overtime.repository" ) ,
Employee Repository Context
เมื่อมี functions ครบแล้วเราจะมาสร้าง Context หรือ Layer สำหรับ EmployeeRepository กัน
หาก service ไหนต้องการติดต่อกับ Employee database ก็ต้องผ่าน Employee Repository นี้ ก็ให้เอา Context ตัวนี้ไปใช้ แทนที่จะเอา interface ไปใช้
src/repositories/employees/index.ts
import type { PrismaClient } from "@prisma/client"
import type * as Types from "../../types/repositories/employee.js"
import { Context , Effect , Layer } from "effect"
import PrismaClient Context from "../prisma.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 {
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) ,
export class EmployeeRepository Context extends Context . Tag ( "repository/Employee" ) < EmployeeRepository Context , Types . EmployeeRepository > () {
static Live = Layer . effect ( this , Effect . gen ( function* () {
const prismaClient = yield * PrismaClient Context
return initEmployeeRepository (prismaClient)
จาก code ด้านบน
ได้สร้าง class EmployeeRepositoryContext
ขึ้นมา ซึ่งจะเห็นว่าเราใส่ Tag ด้วย "repository/Employee"
จะเป็นคำว่าอะไรก็ได้ ขอแค่ไม่ซ้ำกับ Context อื่นๆ ก็พอ
ใน class มี method Live
ที่จะใช้สร้าง Context EmployeeRepositoryContext
ตัวนี้แหละ
โดยเราจะสร้างผ่าน Layer.effect(<class name>, <Effect value>)
รับ parameters 2 ตัว
ในส่วนของ class name
จะใช้ this
ที่หมายถึง class EmployeeRepositoryContext
Effect ที่ return interface Types.EmployeeRepository
ซึ่งจะถูกกำหนดไว้ด้านหลัง Context.Tag()
เรามาลงรายละเอียดกันสักนิด
จากตัวอย่างจะใช้ Effect.gen()
ซึ่งไม่จำเป็นต้องใช้ก็ได้นะ แล้วแต่ว่าใครชอบแบบไหน ส่วนตัวผมไม่ค่อยได้ใช้เลยย แต่ใส่ให้ดูว่าใช้วิธีนี้ก็ได้เช่นกัน
เอามาดูกันต่อละ Effect.gen()
เราจะต้องใส่ generator function เท่านั้น function* () {}
ที่มันมี *
นั่นแหละ
ภายใน function ก็เขียนเหมือน generator function ปกติ แต่จะมี yield*
ที่ต้องใช้กับอะไรก็ตามที่เป็น Effect เท่านั้นเลย
ในตัวอย่างก็จะใช้กับ PrismaClientContext
สุดท้ายจากตัวอย่างนี้เราก็แค่ต้อง return object ที่มี function ต่างๆตาม interface EmployeeRepository
โดยส่ง prismaClient
ที่ด้จาก PrismaClientContext
เข้าไป
Additional info
ส่วนล่างนี้ถ้างงก็ข้ามไปก่อนก็ได้นะ
จากการเขียน method Live
ด้านบน
เพิ่มเติมให้อีกนิดนึงว่าเราสามารถเขียนอีกแบบนึงได้ เช่น
export function create ( prismaClient : PrismaClient ) : EmployeeRepository[ " create " ] {
return data => Effect . tryPromise ( {
catch : Errors . CreateEmployeeError . new () ,
try : () => prismaClient . employee . create ( {
Effect . andThen (Helpers . fromObjectToSchemaEffect (EmployeeSchema . Schema)) ,
Effect . withSpan ( "create.employee.repository" ) ,
export function create2 ( data : EmployeeSchema . CreateEmployeeEncoded ) : Effect . Effect < EmployeeSchema . Employee , Errors . CreateEmployeeError | ParseError , PrismaClientContext > {
return PrismaClientContext . pipe (
Effect . andThen ( prismaClient => Effect . tryPromise ( {
catch : Errors . CreateEmployeeError . new () ,
try : () => prismaClient . employee . create ( {
Effect . andThen (Helpers . fromObjectToSchemaEffect (EmployeeSchema . Schema)) ,
Effect . withSpan ( "create.employee.repository" ) ,
จะเห็นว่าเอา PrismaClientContext
มาใช้ตรงๆเลย ไม่ได้รับเป็น argument เข้ามา และก็ได้ผลลัพธ์เหมือนกัน
แต่ผมไม่แนะนำให้ทำแบบนี้ มันจะเกิดสิ่งที่เรียกว่า Requirement Leakage
มันจะทำให้ตอนเขียน test ต้อง mockup dependencies หลายตัวโดยที่ไม่จำเป็น