Skip to main content

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:

ColumnTypeKeterangan
idUUIDPK, default uuid()
tenant_idUUIDNOT NULL
keyVARCHAR(50)UNIQUE, slug dari name
nameVARCHAR(100)NOT NULL
descriptionVARCHAR(255)NULL
is_activeBOOLEANDEFAULT true
created_byUUIDNULL
created_by_nameVARCHAR(100)NULL
created_atTIMESTAMPTZDEFAULT now()
updated_byUUIDNULL
updated_by_nameVARCHAR(100)NULL
updated_atTIMESTAMPTZNULL, @updatedAt
deleted_byUUIDNULL
deleted_by_nameVARCHAR(100)NULL
deleted_atTIMESTAMPTZNULL

Relasi:

  • JobTitle.family_idJobFamily.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:
    1. createTenantClient(tenantId)
    2. generateUniqueSlug(client, dto.name) → key
    3. Transaction: jobFamily.create({ tenantId, key, name, description, isActive: true, createdBy, createdAt })
    4. 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:
    1. createTenantClient(tenantId)
    2. JobFamilyValidation.validateNotFound(client, { id })
    3. generateUniqueSlug(client, dto.name) → key (dipanggil selalu; jika dto.name undefined, base slug bisa "undefined")
    4. Transaction: jobFamily.update({ where: { id }, data: { key, name: dto.name, description: dto.description ?? null, isActive: dto.is_active ?? true, updatedBy, updatedByName, updatedAt } })
    5. 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:
    1. createTenantClient(tenantId)
    2. JobFamilyValidation.validateNotFound(client, { id })
    3. 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)
  • 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 kolom orderIndex; gunakan id, created_at, atau updated_at agar 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

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.