Dokumentasi Teknis
Overview
Dokumentasi teknis untuk developer yang akan maintain atau develop lebih lanjut pada Job Family API module.
Arsitektur
Job Family module menggunakan pola Command & Query dengan struktur sebagai berikut:
job-family/
├── commands/ # Command handlers untuk write operations
│ ├── create.cmd.ts
│ ├── update.cmd.ts
│ └── soft-delete.cmd.ts
├── queries/ # Query handlers untuk read operations
│ ├── get-job-family.query.ts
│ └── get-job-family-by-id.query.ts
├── dto/ # Data Transfer Objects
├── helper/ # Helper (mapping, slug generation)
├── validation/ # Validators
├── job-family.controller.ts
├── job-family.service.ts
├── job-family.module.ts
└── docs/ # Dokumentasi
Tech Stack
- Framework: NestJS
- Database: PostgreSQL dengan Prisma ORM
- Validation: class-validator, class-transformer
- API Documentation: Swagger/OpenAPI
- Auth: AuthGuard (JWT), tenant dari current user
- RLS: createTenantClient untuk tenant-scoped Prisma client
Database Schema
Table: job_families
Berdasarkan Prisma model JobFamily:
| Column | Type | Keterangan |
|---|---|---|
| id | UUID | PK, default uuid() |
| tenant_id | UUID | NOT NULL |
| key | VARCHAR(50) | UNIQUE, slug dari name |
| name | VARCHAR(100) | NOT NULL |
| description | VARCHAR(255) | NULL |
| is_active | BOOLEAN | DEFAULT true |
| created_by | UUID | NULL |
| created_by_name | VARCHAR(100) | NULL |
| created_at | TIMESTAMPTZ | DEFAULT now() |
| updated_by | UUID | NULL |
| updated_by_name | VARCHAR(100) | NULL |
| updated_at | TIMESTAMPTZ | NULL, @updatedAt |
| deleted_by | UUID | NULL |
| deleted_by_name | VARCHAR(100) | NULL |
| deleted_at | TIMESTAMPTZ | NULL |
Relasi:
JobTitle.family_id→JobFamily.id(banyak job title dapat mengacu ke satu job family)
Module Structure
1. Commands (Write Operations)
CreateJobFamilyCommand
- File:
commands/create.cmd.ts - Handler:
JobFamilyService.create() - Alur:
- createTenantClient(tenantId)
- generateUniqueSlug(client, dto.name) → key
- Transaction:
jobFamily.create({ tenantId, key, name, description, isActive: true, createdBy, createdAt }) - mapToDetailResponse(result)
- Validations: Hanya DTO (class-validator). Tidak ada validasi "sudah ada" — key dibuat unik via slug + suffix.
UpdateJobFamilyCommand
- File:
commands/update.cmd.ts - Handler:
JobFamilyService.update() - Alur:
- createTenantClient(tenantId)
- JobFamilyValidation.validateNotFound(client,
{ id }) - generateUniqueSlug(client, dto.name) → key (dipanggil selalu; jika dto.name undefined, base slug bisa "undefined")
- Transaction:
jobFamily.update({ where: { id }, data: { key, name: dto.name, description: dto.description ?? null, isActive: dto.is_active ?? true, updatedBy, updatedByName, updatedAt } }) - mapToDetailResponse(result)
- Validations: validateNotFound sebelum update. Semua field di DTO optional; key selalu di-regenerate dari dto.name (sebaiknya kirim name saat update).
SoftDeleteJobFamilyCommand
- File:
commands/soft-delete.cmd.ts - Handler:
JobFamilyService.softDelete() - Alur:
- createTenantClient(tenantId)
- JobFamilyValidation.validateNotFound(client,
{ id }) - Transaction:
jobFamily.update({ where: { id }, data: { deletedAt, deletedBy, deletedByName } })
- Validations: validateNotFound. Tidak ada pengecekan "masih punya job title" — soft delete tetap dijalankan.
2. Queries (Read Operations)
GetJobFamilyQuery
- File:
queries/get-job-family.query.ts - Handler:
JobFamilyService.findAll() - Query params: search, is_active, order_by, order_direction, page, page_size
- Where:
- Tanpa search: (deletedAt null OR deletedBy null OR deletedByName null) AND is_active =
(query.is_active ?? true) - Dengan search: where.OR diganti jadi
[{ name: { contains: query.search, mode: 'insensitive' } }]— filter soft-delete tidak lagi dipakai (hanya is_active + name contains)
- Tanpa search: (deletedAt null OR deletedBy null OR deletedByName null) AND is_active =
- Default: page=1, page_size=20, order_by=id, order_direction=asc
- Return: JobFamilyPaginatedResponseDto →
{ total, page, page_size, data: array of items }. Data di-map dengan mapToDetailResponse; list hanya select id, key, name, isActive sehingga field lain (tenant_id, description, created_at, dll.) undefined untuk list.
GetJobFamilyByIdQuery
- File:
queries/get-job-family-by-id.query.ts - Handler:
JobFamilyService.findOne() - Validations: Jika tidak ketemu → NotFoundException, reason
job-family.not-found - Return: JobFamilyDetailResponseDto
3. DTOs
CreateJobFamilyDto
- File:
dto/create-job-family.dto.ts - Required: name (string)
- Optional: description (string)
UpdateJobFamilyDto
- File:
dto/update-job-family.dto.ts - Semua optional: key, name, description, is_active
QueryParamJobFamilyListDto
- File:
dto/query-param-job-family-list.dto.ts - Fields: search, is_active (boolean; transform string "true" → true), order_by, order_direction, page (min 1), page_size (min 1)
- order_by: DTO memvalidasi enum
orderIndex|id|created_at|updated_at. Catatan: Model JobFamily tidak punya kolomorderIndex; gunakanid,created_at, atauupdated_atagar aman. - order_direction:
asc|desc
JobFamilyDetailResponseDto / JobFamilyListItemDto
- File:
dto/job-family-response.dto.ts - Detail: id, tenant_id, key, name, description, is_active, created_by, created_by_name, created_at, updated_by, updated_by_name, updated_at, deleted_by, deleted_by_name, deleted_at
- List item: id, key, name, is_active
JobFamilyPaginatedResponseDto
- File:
dto/job-family-paginated-response.dto.ts - data: JobFamilyListItemDto[], total, page, page_size
4. Validation
JobFamilyValidation
- File:
validation/index.ts - validateExistence(client, where) — Jika record ada, throw BadRequestException, reason
job-family.is_exist. Tidak dipanggil di create/update saat ini. - validateNotFound(client, where) — Jika record tidak ada, throw NotFoundException, reason
job-family.is_not_found. Dipanggil di update dan soft-delete.
5. Helper
JobFamilyHelper
- File:
helper/index.ts - mapToDetailResponse(jf) — Map ke JobFamilyDetailResponseDto (response snake_case). Menggunakan jf.isActive (camelCase); untuk list, hanya id/key/name/isActive yang di-select sehingga field lain (tenant_id, description, created_at, dll.) bisa undefined.
- generateSlug(str) — Lowercase, trim, non-alphanumeric diganti
-, hapus leading/trailing-. - generateUniqueSlug(client, base, attempt) — generateSlug(base) → key; jika key sudah ada, coba
`${baseSlug}-${attempt}`(1, 2, …) sampai unik.
6. Service & Controller
JobFamilyService
- create(user, dto) → CreateJobFamilyCommand.execute
- findAll(tenantId, query) → GetJobFamilyQuery.execute
- findOne(tenantId, id) → GetJobFamilyByIdQuery.execute
- update(user, id, dto) → UpdateJobFamilyCommand.execute
- softDelete(user, id) → SoftDeleteJobFamilyCommand.execute
JobFamilyController
- Base route:
job-families(prefix tergantung global prefix API, mis./api/v1/job-families) - Guards: AuthGuard
- Endpoints:
- POST
/— create - GET
/— findAll (query params); response type di service: JobFamilyPaginatedResponseDto - GET
/:id— findOne - PATCH
/:id— update - DELETE
/:id— softDelete
- POST
Error Handling
Error Response Format
Format standar NestJS (Exception):
- NotFoundException → 404
- BadRequestException → 400
- Body: message, reason (optional)
Reason Codes
job-family.is_not_found— Record tidak ditemukan (validation, update/soft-delete)job-family.not-found— Job family not found (query get by id)job-family.is_exist— Job family sudah ada (validateExistence; saat ini tidak dipakai di flow)
Security & Tenant Isolation
- Semua akses database melalui createTenantClient(user.tenantId).
- Tenant ID dari CurrentUser (AuthGuard).
- Tidak ada cross-tenant access.
Catatan Implementasi
- Create: Key selalu di-generate dari name; tidak ada input key dari client.
- Update: Key di-regenerate dari dto.name di update.cmd; jika name tidak dikirim, base untuk slug bisa undefined — disarankan frontend selalu kirim name saat update jika ingin key konsisten.
- List: Filter soft-delete memakai OR (deletedAt null OR deletedBy null OR deletedByName null). Jika search dipakai, where.OR diganti hanya dengan name contains — perlu dipertimbangkan agar filter soft-delete tetap dipakai bersamaan dengan search.
- Soft delete: Tidak memblokir bila masih ada JobTitle yang memakai family ini; bisa ditambah validasi di kemudian hari jika dibutuhkan.