Skip to content
CodeSook
CodeSook

04. Convert Employee Service to Effect


Start with Employee Service type

เราจะเริ่มแก้ type ของ Employee service กันก่อน

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"
export type EmployeeService = {
create: (data: EmployeeSchema.CreateEmployee) => Effect.Effect<EmployeeSchema.Employee, Errors.CreateEmployeeError | ParseError>
findOneById: (id: Branded.EmployeeId) => Effect.Effect<EmployeeWithRelationsSchema.EmployeeWithRelations, Errors.FindEmployeeByIdError | ParseError | NoSuchElementException>
findMany: () => Effect.Effect<EmployeeWithRelationsSchema.EmployeeWithRelationsArray, Errors.FindManyEmployeeError>
update: (id: Branded.EmployeeId, data: EmployeeSchema.UpdateEmployee) => Effect.Effect<EmployeeSchema.Employee, Errors.UpdateEmployeeError | ParseError>
removeById: (id: Branded.EmployeeId) => Effect.Effect<EmployeeSchema.Employee, Errors.RemoveEmployeeError>
}

Employee Service Context

src/services
├── employee
│ ├── create.ts.del
│ ├── finds.ts.del
│ ├── index.ts
│ ├── removes.ts.del
│ └── updates.ts.del
└── overtime/

สำหรับ Services เราก็จะทำ Context เหมือนกัน

src/services/employee/index.ts
src/services/employee/index.ts
import type { EmployeeService } from "../../types/services/employee.js"
import { Context, Effect, Layer } from "effect"
import { EmployeeRepositoryContext } from "../../repositories/employees/index.js"
export function initEmployeeService(employeeRepository: EmployeeRepository): EmployeeService {
return {
create: Creates.create(employeeRepository),
findMany: Finds.findMany(employeeRepository),
findOneById: Finds.findOneById(employeeRepository),
removeById: Removes.removeById(employeeRepository),
update: Updates.update(employeeRepository),
}
}
export class EmployeeServiceContext extends Context.Tag("service/Employee")<EmployeeServiceContext, EmployeeService>() {
static Live = Layer.effect(
this,
Effect.all({
repo: EmployeeRepositoryContext,
}).pipe(
Effect.andThen(({ repo }) => {
return {
create: data => repo.create(data).pipe(
Effect.withSpan("create.employee.service"),
),
findMany: () => repo.findManyWithRelations().pipe(
Effect.withSpan("find-many.employee.service"),
),
findOneById: id => repo.findByIdWithRelations(id).pipe(
Effect.withSpan("find-by-id.employee.service"),
),
removeById: id => repo.remove(id).pipe(
Effect.withSpan("remove-by-id.employee.service"),
),
update: (id, data) => repo.update(id, data).pipe(
Effect.withSpan("update.employee.service"),
),
} satisfies EmployeeService
}),
),
)
}

จาก code ด้านบน เราไม่ได้ใช้ function initEmployeeService() แล้ว ซึ่งจะใช้ก็ได้เช่นกัน ผมอยากให้เห็นหลายๆวิธี
พอไม่ได้ใช้ function initEmployeeService() แล้ว ทำให้เราใช้ function ที่อยู่ที่ไฟล์อื่นไม่ได้แล้ว ต้อง implement กันใน method Live() กันตรงๆ ซึ่ง functions ที่เราใช้ใน EmployeeService ไม่ได้มีอะไรซับซ้อนเลย แค่ดึง data จาก Database(Repositories) แล้วส่งต่อเลยในทันที

รอบนี้เราใช้ Effect.all() แทนที่จะเป็น Effect.gen() ซึ่งปกติผมชอบท่านี้มากกว่า แล้วก็ใส่ Repositories ที่ต้องการลงมาใน Effect.all() ได้เลย ในที่นี้มีแค่ EmployeeRepository อันเดียว
จากนั้นก็จะเอา repository ไปใช้งานใน function ต่างๆต่อไป

Test Service Example

ตัวอย่างการสร้าง Test Service ที่จะเอาไว้ inject ตอนเขียน Test

export class EmployeeServiceContext extends Context.Tag("service/Employee")<EmployeeServiceContext, EmployeeService>() {
static Live = Layer.effect(
this,
Effect.all({
repo: EmployeeRepositoryContext,
}).pipe(
Effect.andThen(({ repo }) => {
return {
create: data => repo.create(data).pipe(
Effect.withSpan("create.employee.service"),
),
findMany: () => repo.findManyWithRelations().pipe(
Effect.withSpan("find-many.employee.service"),
),
findOneById: id => repo.findByIdWithRelations(id).pipe(
Effect.withSpan("find-by-id.employee.service"),
),
removeById: id => repo.remove(id).pipe(
Effect.withSpan("remove-by-id.employee.service"),
),
update: (id, data) => repo.update(id, data).pipe(
Effect.withSpan("update.employee.service"),
),
} satisfies EmployeeService
}),
),
)
static Test = Layer.succeed(this, EmployeeServiceContext.of({
create: (data: EmployeeSchema.CreateEmployeeEncoded) => Effect.succeed(EmployeeSchema.Schema.make({
...data,
_tag: "Employee",
createdAt: new Date("2024-12-30"),
deletedAt: null,
id: Branded.EmployeeId.make(1),
updatedAt: new Date("2024-12-30"),
})),
findMany: () => Effect.succeed([]),
findOneById: () => Effect.fail(Errors.FindEmployeeByIdError.new()()),
removeById: () => Effect.fail(Errors.RemoveEmployeeError.new()()),
update: () => Effect.fail(Errors.UpdateEmployeeError.new()()),
}))
}