Action-Based Structure Design for Data Layer

Action-Based Structure Design for Data Layer

Action-Based Structure Design for Data Layer

Overview

การออกแบบ Data Layer โดยใช้ Action เป็นหลัก ที่ align กับ Abstraction Layer (shared-api-core) โดยแยก command/query และ Entry ทำหน้าที่เป็น Data Access Provider สำหรับ Action นั้นๆ

Core Principles

1. Action-Centric Organization

  • Action = Business operation ที่มีความหมายเดียวกันทั้ง Abstraction Layer และ Data Layer
  • Entry = Data Access Provider ที่ implement interface และ delegate ทุก method ไปยัง task functions
  • 1:1 Mapping ระหว่าง Abstraction Layer Action กับ Data Layer Action

2. Command/Query Segregation

  • Command: Write operations (Create, Update, Delete)
  • Query: Read operations (Get, List, Search)
  • แยกตามประเภทของ Action ไม่ใช่ประเภทของ method ใน Entry

3. Flat File Organization per Action

  • ไฟล์ทุกไฟล์อยู่ใน action folder โดยตรง ไม่มี method sub-folder
  • db.logic.ts และ data.logic.ts เป็น shared resource ของทุก method ใน action
  • ใช้ comment separator แบ่ง section ตาม method แทนการแยก folder

4. Type File Convention

  • แต่ละ action มี 1 ไฟล์ สำหรับเก็บ internal types และ interfaces ทั้งหมด
  • Pattern: internal.type.ts — ชื่อบอก intent ชัดเจนว่าเป็น internal ของ action folder เท่านั้น
  • ไม่ export ออกนอก action folderindex.ts ไม่ re-export อะไรจาก internal.type.ts
  • SonarQube: ใช้ sonar.cpd.exclusions=**/*.type.ts เพื่อ exclude CPD (Copy-Paste Detection) เพราะ type definitions มักมีโครงสร้างคล้ายกันข้ามไฟล์ตามธรรมชาติ

5. Public Types ของ Data Layer

Data Layer ไม่มี public types ของตัวเอง — นี่คือการออกแบบโดยตั้งใจ:

Public types (Input/Output ของทุก method)
    อยู่ที่ Abstraction Layer (shared-api-core)
    ไม่ใช่ Data Layer

เหตุผล: Data Layer เป็น implementation ของ interface ที่ Abstraction Layer กำหนด consumer (Service Layer) ควร depend on Abstraction ไม่ใช่ Data Layer

// ✅ Service Layer import types จาก Abstraction Layer โดยตรง
import type {
  ReturnRequestInput,
  ReturnRequestOutput,
  Repository,
} from '@feedos-frgm-system/shared-api-core/document-process-api/command/return-request-lv40'

// ✅ import concrete class จาก Data Layer สำหรับ DI เท่านั้น
import { ReturnRequestLv40V1Entry } from '@feedos-frgm-system/api-data/document-process-api'

// ❌ ไม่ควร import types จาก Data Layer
import type { ReturnRequestInput } from '@feedos-frgm-system/api-data/document-process-api/...'

ผลที่ได้: index.ts ของทุก action export เฉพาะ Entry class ไม่มี type export ใดๆ


Folder Structure Standard

Abstraction Layer Structure (Reference)

shared-api-core/
├── document-process-api/
│   ├── command/
│   │   ├── return-request-lv40/          # Business Action
│   │   ├── approve-document/             # Business Action
│   │   └── validate-document-lv30/       # Business Action
│   └── query/
│       ├── get-document-status/          # Business Action
│       ├── list-documents/               # Business Action
│       └── search-documents/             # Business Action

Data Layer Structure (Implementation)

store-prisma/src/
├── document-process-api/
│   ├── command/                              # Write Actions Group
│   │   ├── return-request-lv40/              # 🎯 Action Folder
│   │   │   ├── index.ts                      # ✅ Entry Export Only (Entry class เท่านั้น)
│   │   │   ├── internal.type.ts              # ✅ Internal types & interfaces ทั้งหมดของ action
│   │   │   ├── entry.ts                      # ✅ Data Access Provider (delegates to tasks)
│   │   │   ├── returnRequest.task.ts         # ✅ Orchestration + Unit of Work
│   │   │   ├── checkStatus.task.ts           # ✅ Orchestration
│   │   │   ├── flows.ts                      # ✅ Flow functions (ถ้ามี workflow ซับซ้อน)
│   │   │   ├── db.logic.ts                   # ✅ DAF functions ทั้งหมดของ action นี้
│   │   │   ├── data.logic.ts                 # ✅ Pure transforms ทั้งหมดของ action นี้
│   │   │   └── __tests__/                    # ✅ Action Tests
│   │   │       └── returnRequestLv40.test.ts # 1 file ต่อ 1 action
│   │   ├── approve-document/                 # 🎯 Another Action
│   │   │   ├── index.ts
│   │   │   ├── internal.type.ts
│   │   │   ├── entry.ts
│   │   │   ├── approveDocument.task.ts
│   │   │   ├── flows.ts
│   │   │   ├── db.logic.ts
│   │   │   ├── data.logic.ts
│   │   │   └── __tests__/
│   │   │       └── approveDocument.test.ts
│   ├── query/                                # Read Actions Group
│   │   ├── get-document-status/              # 🎯 Action Folder
│   │   │   ├── index.ts
│   │   │   ├── internal.type.ts
│   │   │   ├── entry.ts
│   │   │   ├── getDocumentStatus.task.ts
│   │   │   ├── db.logic.ts
│   │   │   ├── data.logic.ts
│   │   │   └── __tests__/
│   │   │       └── getDocumentStatus.test.ts
│   │   ├── list-documents/                   # 🎯 Another Action
│   │   │   ├── entry.ts
│   │   │   ├── listDocuments.task.ts
│   │   │   ├── db.logic.ts
│   │   │   ├── data.logic.ts
│   │   │   ├── __tests__/
│   │   │   │   └── listDocuments.test.ts
│   │   │   └── index.ts
│   └── index.ts                              # ✅ API-Level Entry Re-exports

กฎที่ไม่ต้องคิด:

ไฟล์จำนวนชื่อ
index1 ต่อ actionindex.ts
internal types1 ต่อ actioninternal.type.ts
entry1 ต่อ actionentry.ts
task1 ต่อ method{methodName}.task.ts
flows1 ต่อ actionflows.ts (มีเมื่อมี workflow)
db logic1 ต่อ actiondb.logic.ts
data logic1 ต่อ actiondata.logic.ts
test1 ต่อ action{ActionName}.test.ts

internal.type.ts มีเสมอ 1 ไฟล์ต่อ action — types ทุกตัวของ action รวมอยู่ที่นี่ที่เดียว


Key Design Decisions

1. Entry as Data Access Provider

entry.ts ทำหน้าที่เดียวคือ implement interface และ delegate ไปยัง task functions ไม่มี business logic โดยตรง

// entry.ts - Data Access Provider
import { Repository } from '@feedos-frgm-system/shared-api-core/document-process-api/command/return-request-lv40';
import { returnRequestTask } from './returnRequest.task';
import { checkStatusTask } from './checkStatus.task';

export class ReturnRequestLv40V1Entry implements Repository {
  constructor(
    private readonly client: PrismaClient,
    private readonly telemetryService: TelemetryMiddlewareService,
  ) {}

  async returnRequest(
    context: UnifiedHttpContext,
    props: ReturnRequestInput,
  ): Promise<Result<ReturnRequestOutput, BaseFailure>> {
    return returnRequestTask({
      context,
      telemetryService: this.telemetryService,
      client: this.client,
      props,
    });
  }

  async checkStatus(
    context: UnifiedHttpContext,
    props: CheckStatusInput,
  ): Promise<Result<CheckStatusOutput, BaseFailure>> {
    return checkStatusTask({
      context,
      telemetryService: this.telemetryService,
      client: this.client,
      props,
    });
  }
}

2. internal.type.ts — Internal Types, Interfaces & Custom Errors

แต่ละ action มี 1 ไฟล์ รวม types, interfaces และ custom error classes ทั้งหมดที่ใช้ภายใน action folder

Custom error classes ของ DAF และ Flow เก็บใน internal.type.ts เพราะ:

  • มีโครงสร้างคล้ายกันทุก action (extends BaseFailure, constructor pattern เหมือนกัน) → SonarQube CPD flag ถ้าอยู่ใน source file
  • sonar.cpd.exclusions=**/*.type.ts exclude ไปแล้ว — ไม่มี false positive
// internal.type.ts — internal types, interfaces และ custom errors ของ return-request-lv40

// ─── returnRequest task ──────────────────────────────────────────

export interface ReturnRequestTaskInput {
  context: UnifiedHttpContext;
  telemetryService: TelemetryMiddlewareService;
  client: PrismaClient;
  props: ReturnRequestInput;
}

// ─── checkStatus task ────────────────────────────────────────────

export interface CheckStatusTaskInput {
  context: UnifiedHttpContext;
  telemetryService: TelemetryMiddlewareService;
  client: PrismaClient;
  props: CheckStatusInput;
}

// ─── flows ───────────────────────────────────────────────────────

export interface GetAllActivityFlowInput {
  client: PrismaClient | Prisma.TransactionClient;
  context: UnifiedHttpContext;
  applicationId: string;
}

export interface ManagePackSizeFlowInput {
  client: PrismaClient | Prisma.TransactionClient;
  context: UnifiedHttpContext;
  applicationId: string;
}

export interface ManageAttachmentFlowInput {
  client: PrismaClient | Prisma.TransactionClient;
  context: UnifiedHttpContext;
  props: AttachmentProps;
}

// ─── flows errors ────────────────────────────────────────────────

export class ActivityNotFoundFailure extends BaseFailure {
  constructor(message?: string, details?: unknown) {
    super('ACTIVITY_NOT_FOUND', message ?? 'ACTIVITY_NOT_FOUND', 404, details);
  }
}

export class ProcessActivityFailure extends BaseFailure {
  constructor(message?: string, details?: unknown) {
    super('PROCESS_ACTIVITY_FAIL', message ?? 'PROCESS_ACTIVITY_FAIL', 500, details);
  }
}

export class PackSizeNotFoundFailure extends BaseFailure {
  constructor(message?: string, details?: unknown) {
    super('PACK_SIZE_NOT_FOUND', message ?? 'PACK_SIZE_NOT_FOUND', 404, details);
  }
}

// ─── db.logic ────────────────────────────────────────────────────

export interface UpdateStatusInput {
  id: string;
  status: string;
}

export type RawApplication = {
  ID: string;
  STATUS: string;
  CURRENT_ACTOR_ID: string;
  UPDATED_AT: Date;
};

export type RawActivity = {
  ID: string;
  PROCESS_ID: string;
  ACTIVITY_LEVEL: string;
};

// ─── db.logic errors ─────────────────────────────────────────────

export class ReturnRequestDAFFail extends BaseFailure {
  constructor(message?: string, details?: unknown) {
    super('RETURN_REQUEST_DAF_FAIL', message ?? 'RETURN_REQUEST_DAF_FAIL', 500, details);
  }
}

export class CreateActivityLogDAFFail extends BaseFailure {
  constructor(message?: string, details?: unknown) {
    super('CREATE_ACTIVITY_LOG_DAF_FAIL', message ?? 'CREATE_ACTIVITY_LOG_DAF_FAIL', 500, details);
  }
}

// ─── data.logic ──────────────────────────────────────────────────

export interface ValidationResult {
  isValid: boolean;
  errors: string[];
}

ทุก source file import จาก internal.type.ts ที่เดียว — ทั้ง types และ error classes:

// returnRequest.task.ts
import type { ReturnRequestTaskInput } from './internal.type';

// flows.ts
import type { GetAllActivityFlowInput, ManagePackSizeFlowInput } from './internal.type';
import { ActivityNotFoundFailure, ProcessActivityFailure } from './internal.type';

// db.logic.ts
import type { UpdateStatusInput, RawApplication } from './internal.type';
import { ReturnRequestDAFFail } from './internal.type';

DAF function ใช้ try/catch แปลง Prisma error เป็น custom error ของเรา:

// db.logic.ts
export async function updateStatusDAF(
  client: PrismaClient | Prisma.TransactionClient,
  data: UpdateStatusInput,
): Promise<Either<BaseFailure, RawApplication>> {
  try {
    const raw = await client.aPPLICATION.update({
      where: { ID: data.id },
      data: { STATUS: data.status, UPDATED_AT: new Date() },
    });
    return right(raw);
  } catch (error) {
    const baseFail = toBaseFailure(error);
    return left(new ReturnRequestDAFFail(baseFail.message, { error: baseFail }));
  }
}

ทำไม 1 ไฟล์ แทนที่จะแยกต่อ source file:

  • ไม่มี cross-file import ของ types และ error classes
  • Custom error classes มีโครงสร้างซ้ำกัน — internal.type.ts ที่ exclude จาก CPD รองรับได้ทันที
  • หา type และ error ได้จากที่เดียว ไม่ต้องเดาว่าอยู่ที่ไหน

SonarQube CPD:

# sonar-project.properties
sonar.cpd.exclusions=**/*.type.ts,**/__tests__/**/*.test.ts

ครอบคลุมทั้ง type definitions และ custom error classes ที่มีโครงสร้างซ้ำกันตามธรรมชาติ และ test files ที่ต้องการ explicit มากกว่า DRY

การเปรียบเทียบ 3 แนวทางสำหรับจัดการ Types

แนวทางที่ 1: Strict Co-location — แยก .type.ts ต่อ source file

return-request-lv40/
├── entry.ts
├── entry.type.ts                  ← types ของ entry เท่านั้น
├── returnRequest.task.ts
├── returnRequest.task.type.ts     ← types ของ task นี้เท่านั้น
├── flows.ts
├── flows.type.ts                  ← types ของ flows เท่านั้น
├── db.logic.ts
├── db.logic.type.ts
├── data.logic.ts
└── data.logic.type.ts

ปัญหาที่เกิดขึ้นจริง:

// returnRequest.task.ts ต้องการ FlowInput เพื่อส่งให้ flow
// แต่ FlowInput อยู่ใน flows.type.ts
import type { ReturnRequestTaskInput } from './returnRequest.task.type'
import type { GetAllActivityFlowInput } from './flows.type'  // ← cross-file import

// หรือถ้าไม่อยากข้ามไฟล์ ก็ต้อง duplicate type
// returnRequest.task.type.ts
export interface GetAllActivityFlowInput { ... }  // ← ซ้ำกับ flows.type.ts ❌

แนวทางที่ 2: Inline — เขียน types ไว้ใน source file โดยตรง

// returnRequest.task.ts
export interface ReturnRequestTaskInput { ... }  // ← type ปนกับ implementation
export async function returnRequestTask(input: ReturnRequestTaskInput) { ... }

ปัญหาที่เกิดขึ้นจริง:

// db.logic.ts มี interface ที่โครงสร้างคล้ายกัน
export interface UpdateStatusInput { id: string; status: string; }

// data.logic.ts ก็มี interface คล้ายกัน
export interface TransformInput { id: string; status: string; }

// SonarQube CPD flag ทั้งคู่ว่าเป็น duplicate ❌
// แต่ exclude ทั้ง source file ไม่ได้ เพราะมี implementation logic รวมอยู่ด้วย

แนวทางที่ 3: internal.type.ts — 1 ไฟล์ต่อ action ✅

return-request-lv40/
├── internal.type.ts   ← types ทั้งหมดของ action อยู่ที่นี่
├── entry.ts
├── returnRequest.task.ts
├── flows.ts
├── db.logic.ts
└── data.logic.ts
// ทุก source file import จากที่เดียว ไม่มี cross-file, ไม่มี duplicate
import type { ReturnRequestTaskInput, GetAllActivityFlowInput } from './internal.type'

ตารางเปรียบเทียบ

Strict co-locationInlineinternal.type.ts
SonarQube CPD✅ (exclude *.type.ts)❌ flag source files✅ (exclude *.type.ts)
Cross-file type import⚠️ หลีกเลี่ยงยาก✅ ไม่มี✅ ไม่มี
Duplicate type❌ เสี่ยงสูง❌ เสี่ยงสูง✅ ไม่มี
หา typeกระจายหลายไฟล์ต้อง searchที่เดียว
File count ต่อ actionsource × 2เท่าเดิม+1 เท่านั้น
Zero-decision❌ ต้องคิดว่า type ไหนของใคร❌ ต้องคิดว่า inline หรือแยก✅ ทุก type ไป internal.type.ts
Source file อ่านง่าย✅ ไม่มี type ปน❌ type ปนกับ implementation✅ ไม่มี type ปน

3. Task as Orchestration + Unit of Work

{methodName}.task.ts มี 2 บทบาทในไฟล์เดียวกัน:

  • Orchestration: เรียก flows, DAF และ data.logic ตามลำดับ
  • Unit of Work: จัดการ transaction boundary ด้วย $transaction + try/catch
// returnRequest.task.ts
import type { ReturnRequestTaskInput } from './internal.type';
import { getAllActivityFlow, managePackSizeFlow, manageAttachmentFlow } from './flows';
import { updateStatusDAF, createActivityLogDAF } from './db.logic';
import { transformReturnRequestToOutput } from './data.logic';  // ← task เรียก data.logic ได้โดยตรง

export async function returnRequestTask(
  input: ReturnRequestTaskInput,
): Promise<Result<ReturnRequestOutput, BaseFailure>> {
  const { client, context, props } = input;

  // ── Zone 1: Pre-transaction (reads / validations) ──────────
  const act = await getAllActivityFlow({ client, context, ...props });
  if (act.isLeft()) return act;

  const pack = await managePackSizeFlow({ client, context, ...props });
  if (pack.isLeft()) return pack;

  // ── Zone 2: Unit of Work (transaction boundary) ────────────
  // Prisma rollback = throw inside $transaction scope
  try {
    const txResult = await client.$transaction(async (tx) => {
      const upd = await updateStatusDAF(tx, { id: props.id, status: 'RETURNED' });
      const att = await manageAttachmentFlow({ client: tx, context, ...props });
      const log = await createActivityLogDAF(tx, { ...act.value, ...pack.value });

      // ← ใช้ variable รับผลลัพธ์ เพราะต้องส่งต่อให้ Result.ok() ด้านนอก
      const output = transformReturnRequestToOutput(upd, att.value, log);
      return output;
    });
    return Result.ok(txResult);
  } catch (error) {
    return Result.fail(new TransactionFailure(error.message));
  }
}

Zone 1 (Pre-transaction): read-only operations, validation, data preparation

Zone 2 (Unit of Work): write operations ทั้งหมดที่ต้อง atomic — throw ใน $transaction = rollback อัตโนมัติ

หมายเหตุ: transformReturnRequestToOutput อยู่ใน data.logic.ts และถูกเรียกจาก task โดยตรง ภายใน $transaction scope — ผลลัพธ์รับผ่าน variable txResult ก่อนส่งออก Result.ok() เสมอ

4. flows.ts — Cognitive Complexity Management

Flow functions แยกออกจาก task เพื่อแก้ปัญหา S3776 Cognitive Complexity (SonarQube limit: 15)

// ❌ ก่อนมี flows.ts — task.ts complexity สูง
async function returnRequestTask(input) {
  const act = await findActivityDAF(...)         // +1
  if (act.isLeft()) return act                   // +1
  const proc = await findManyProcessActivityDAF(...) // +1
  if (proc.isLeft()) return proc                 // +1
  // ... อีก 8+ operations
  // complexity = 12+ ⚠️ ใกล้ limit
}

// ✅ หลังมี flows.ts — task.ts complexity ต่ำ
async function returnRequestTask(input) {
  const act = await getAllActivityFlow(input)     // +1
  if (act.isLeft()) return act                   // +1
  const pack = await managePackSizeFlow(input)   // +1
  if (pack.isLeft()) return pack                 // +1
  try { ... } catch { ... }                      // +2
  // complexity = 6 ✅ ปลอดภัย
}

Flow functions กระจาย complexity ออกไปแต่ละ function และ test แยกกันได้ชัดเจน

// flows.ts — flow functions ทั้งหมดของ action นี้

// ─── getAllActivityFlow ──────────────────────────────────────────

export interface GetAllActivityFlowInput {
  client: PrismaClient | Prisma.TransactionClient;
  context: UnifiedHttpContext;
  applicationId: string;
}

export async function getAllActivityFlow(
  input: GetAllActivityFlowInput,
): Promise<Result<ActivityData, BaseFailure>> {
  const { client, applicationId } = input;

  const act = await findActivityDAF(client, { appId: applicationId });
  if (act.isLeft()) return Result.fail(new ActivityNotFoundFailure());

  const proc = await findManyProcessActivityDAF(client, { processId: act.value.PROCESS_ID });
  if (proc.isLeft()) return Result.fail(new ProcessActivityFailure());

  return Result.ok(mapToAllActivityItem(proc.value, act.value));
}

// ─── managePackSizeFlow ─────────────────────────────────────────

export interface ManagePackSizeFlowInput {
  client: PrismaClient | Prisma.TransactionClient;
  context: UnifiedHttpContext;
  applicationId: string;
}

export async function managePackSizeFlow(
  input: ManagePackSizeFlowInput,
): Promise<Result<PackSizeData, BaseFailure>> {
  const { client, applicationId } = input;

  const pack = await getPackSizeDAF(client, { appId: applicationId });
  if (pack.isLeft()) return Result.fail(new PackSizeNotFoundFailure());

  return Result.ok(calculatePackSize(pack.value));
}

// ─── manageAttachmentFlow ───────────────────────────────────────

export async function manageAttachmentFlow(
  input: ManageAttachmentFlowInput,
): Promise<Result<AttachmentData, BaseFailure>> {
  // ...
}

หลักการ: Flow function รับ PrismaClient | Prisma.TransactionClient เสมอ เพราะ caller (task.ts) ตัดสินใจว่าจะส่ง client ปกติหรือ transaction client

flows.ts มีเมื่อ: task มี named workflow ที่มี error handling ของตัวเอง

flows.ts ไม่มีเมื่อ: method ทำแค่ query/transform ตรงๆ ไม่มี workflow ซับซ้อน (เช่น checkStatus.task.ts)

5. db.logic.ts — Shared DAF Functions

DAF functions ของทุก method ใน action รวมอยู่ในไฟล์เดียว ใช้ comment separator แบ่ง section

ทำไม DAF ต้องแปลง External Error เป็น Custom Error

DAF เป็น boundary ระหว่าง External Library (Prisma) กับ system ของเรา — ต้องไม่ให้ Prisma error รั่วออกไปยัง layer บน

❌ ถ้าไม่แปลง — Prisma error รั่วออกไป
task.ts → flows.ts → db.logic.ts → PrismaClientKnownRequestError

                              layer บนต้องรู้จัก Prisma เพื่อจัดการ error

✅ หลังแปลง — layer บนเห็นแค่ BaseFailure
task.ts → flows.ts → db.logic.ts → ReturnRequestDAFFail (BaseFailure)

                              layer บนไม่รู้จัก Prisma เลย

ข้อดี:

  • Layer Separation — เปลี่ยน ORM แก้แค่ db.logic.ts ไม่กระทบ layer อื่น
  • Consistent Error Contract — ทุก error มีโครงสร้าง code, message, statusCode, details เหมือนกัน
  • Meaningful Error CodeRETURN_REQUEST_DAF_FAIL บอกได้ทันทีว่าเกิดที่ไหน แทน Prisma code P2025
  • ไม่ Leak Implementation Details — Prisma schema หรือ version ไม่หลุดออก HTTP response
// db.logic.ts — Database Access Functions
// import custom errors จาก internal.type.ts
import { ReturnRequestDAFFail, CreateActivityLogDAFFail } from './internal.type';

// ─── returnRequest ──────────────────────────────────────────────

export async function getApplicationDAF(
  client: PrismaClient | Prisma.TransactionClient,
  id: string,
): Promise<Either<BaseFailure, RawApplication>> {
  try {
    const raw = await client.aPPLICATION.findUnique({
      where: { ID: id },
      select: { ID: true, STATUS: true, CURRENT_ACTOR_ID: true },
    });
    return right(raw);
  } catch (error) {
    const baseFail = toBaseFailure(error);
    return left(new ReturnRequestDAFFail(baseFail.message, { error: baseFail }));
  }
}

export async function updateStatusDAF(
  client: PrismaClient | Prisma.TransactionClient,
  data: UpdateStatusInput,
): Promise<Either<BaseFailure, RawApplication>> {
  try {
    const raw = await client.aPPLICATION.update({
      where: { ID: data.id },
      data: { STATUS: data.status, UPDATED_AT: new Date() },
    });
    return right(raw);
  } catch (error) {
    const baseFail = toBaseFailure(error);
    return left(new ReturnRequestDAFFail(baseFail.message, { error: baseFail }));
  }
}

export async function findActivityDAF(
  client: PrismaClient | Prisma.TransactionClient,
  input: { appId: string },
): Promise<Either<BaseFailure, RawActivity>> {
  try {
    const raw = await client.aCTIVITY.findFirst({
      where: { APPLICATION_ID: input.appId },
    });
    return right(raw);
  } catch (error) {
    const baseFail = toBaseFailure(error);
    return left(new ReturnRequestDAFFail(baseFail.message, { error: baseFail }));
  }
}

// ─── checkStatus ────────────────────────────────────────────────

export async function getDocumentStatusDAF(
  client: PrismaClient | Prisma.TransactionClient,
  id: string,
): Promise<Either<BaseFailure, RawDocumentStatus>> {
  try {
    const raw = await client.dOCUMENT.findUnique({
      where: { ID: id },
      select: { ID: true, STATUS: true, UPDATED_AT: true },
    });
    return right(raw);
  } catch (error) {
    const baseFail = toBaseFailure(error);
    return left(new ReturnRequestDAFFail(baseFail.message, { error: baseFail }));
  }
}

// ─── shared ─────────────────────────────────────────────────────

export async function findManyProcessActivityDAF(
  client: PrismaClient | Prisma.TransactionClient,
  input: { processId: string },
): Promise<Either<BaseFailure, RawProcessActivity[]>> {
  try {
    const raw = await client.pROCESS_ACTIVITY.findMany({
      where: { PROCESS_ID: input.processId },
    });
    return right(raw);
  } catch (error) {
    const baseFail = toBaseFailure(error);
    return left(new ReturnRequestDAFFail(baseFail.message, { error: baseFail }));
  }
}

6. data.logic.ts — Shared Pure Functions

// data.logic.ts — Pure Data Transformations

// ─── returnRequest ──────────────────────────────────────────────

export function transformReturnRequestToOutput(
  upd: RawApplication,
  att: AttachmentData,
  log: RawActivityLog,
): ReturnRequestOutput {
  return {
    id: upd.ID,
    status: upd.STATUS,
    attachments: att.items,
    logId: log.ID,
  };
}

export function validateReturnRequestInput(input: ReturnRequestInput): ValidationResult {
  const errors: string[] = [];
  if (!input.id) errors.push('id is required');
  if (!input.reason) errors.push('reason is required');
  return { isValid: errors.length === 0, errors };
}

// ─── checkStatus ────────────────────────────────────────────────

export function transformDocumentStatusToOutput(raw: RawDocumentStatus): CheckStatusOutput {
  return {
    id: raw.ID,
    status: raw.STATUS,
    updatedAt: raw.UPDATED_AT,
  };
}

Naming Convention Standards

Action Folder Naming

Command Actions (Write Operations)

  • Pattern: {command-name} (kebab-case)
  • Examples:
    • return-request-lv40
    • approve-document
    • validate-document-lv30
    • requestReturn — camelCase
    • lv40-return-request — ลำดับคำผิด

Query Actions (Read Operations)

  • Pattern: {query-name} (kebab-case)
  • Examples:
    • get-document-status
    • list-documents
    • search-documents-by-criteria
    • documentStatus — camelCase
    • getAllDocuments — camelCase

File Naming Standards

ไฟล์Patternตัวอย่าง
Indexindex.tsindex.ts
Internal typesinternal.type.tsinternal.type.ts
Entryentry.tsentry.ts
Task{methodName}.task.tsreturnRequest.task.ts
Flowsflows.tsflows.ts
DB Logicdb.logic.tsdb.logic.ts
Data Logicdata.logic.tsdata.logic.ts

Examples:

  • returnRequest.task.ts — task สำหรับ returnRequest method
  • checkStatus.task.ts — task สำหรับ checkStatus method
  • internal.type.ts — internal types ทั้งหมดของ action นี้
  • task.ts — ไม่บอกว่าของ method ไหน
  • types.ts — ชื่อนี้ convention ทั่วไปหมายถึง public types ที่ index.ts ควร re-export

Entry Class Naming

  • Pattern: {ActionName}{Version}Entry
  • Examples:
    • ReturnRequestLv40V1Entry
    • ApproveDocumentEntry
    • GetDocumentStatusEntry
    • ReturnRequestLv40V1Repo — ใช้ชื่อ Repo (เปลี่ยนเป็น Entry)
    • ReturnRequestEntryRepository — ยาวเกินไป

Method Naming in Entry

  • Pattern: {actionVerb}{Entity} (camelCase)
  • Examples:
    • returnRequest()
    • checkStatus()
    • approveDocument()
    • return_request() — snake_case
    • doReturn() — ไม่สื่อความหมาย

Task Function Naming

  • Pattern: {methodName}Task (camelCase function)
  • Examples:
    • returnRequestTask
    • checkStatusTask
    • approveDocumentTask
    • ReturnRequestTask — PascalCase (class style)
    • taskReturnRequest — ลำดับคำผิด

Task Input Type Naming

  • Pattern: {MethodName}TaskInput (PascalCase)
  • Examples:
    • ReturnRequestTaskInput
    • CheckStatusTaskInput
    • returnRequestTaskInput — camelCase

Flow Function Naming

  • Pattern: {workflowName}Flow (camelCase function)
  • Examples:
    • getAllActivityFlow
    • managePackSizeFlow
    • manageAttachmentFlow
    • GetAllActivityFlow — PascalCase
    • flowGetAllActivity — ลำดับคำผิด

DAF (Database Access Function) Naming

  • Pattern: {verb}{Entity}DAF (camelCase)
  • Examples:
    • getApplicationDAF
    • updateStatusDAF
    • createDocumentDAF
    • findApplicationsByStatusDAF
    • getApplication — ไม่มี DAF suffix
    • applicationGet — ลำดับคำผิด

Data Logic Function Naming

  • Pattern: {operation}{Entity}{Direction?} (camelCase)
  • Examples:
    • transformApplicationToOutput
    • validateDocumentInput
    • formatStatusDisplay
    • applicationTransform — ลำดับคำผิด
    • transformApp — entity name ไม่ชัดเจน

Interface and Type Naming

  • Pattern: {MethodName}{Type} (PascalCase)
  • Examples:
    • ReturnRequestInput
    • ReturnRequestOutput
    • ReturnRequestTaskInput
    • returnRequestInput — camelCase
    • ReturnRequestInputType — Type suffix ซ้ำซ้อน

Testing Strategy

รายละเอียดทั้งหมดอยู่ใน Data Layer Test Strategy — section นี้สรุปเฉพาะ convention ที่เกี่ยวข้องกับ folder structure

หลักการ: 1 Action = 1 Test File

return-request-lv40/
└── __tests__/
    ├── fixtures/
    │   ├── application.fixture.ts   ← test data ต่อ entity (1 entity = 1 file)
    │   ├── activity.fixture.ts
    │   └── index.ts
    └── returnRequestLv40.test.ts    ← ทุก component ของ action อยู่ในไฟล์เดียว

เหตุผล: Jest spawn worker ต่อ test file — 1 test file ต่อ action ลด overhead จาก 5–6 workers เหลือ 1 worker ต่อ action

Test File Naming

  • Pattern: {ActionName}.test.ts (camelCase)
  • Examples:
    • returnRequestLv40.test.ts
    • approveDocument.test.ts
    • repository.test.ts — ไม่บอก action
    • return-request-lv40.test.ts — kebab-case

Fixture File Naming

  • Pattern: {entityName}.fixture.ts (camelCase) — 1 entity = 1 file
  • Examples:
    • application.fixture.ts
    • activity.fixture.ts
    • packsize.fixture.ts

Describe Structure — Overview

Level 1 = source file name  (entry, returnRequestTask, flows, db.logic, data.logic)
Level 2 = function name     (เฉพาะ flows, db.logic, data.logic ที่มีหลาย function)
Level 3 = scenario          (it/test)

สำหรับ mock level ต่อ component, describe examples และ implementation patterns ดูได้ที่ Data Layer Test Strategy


Call Graph

ความสัมพันธ์จริงๆ ระหว่างไฟล์ภายใน action ไม่ได้เป็น linear chain:

entry.ts
  └── {method}.task.ts (1:1 per method)
        ├── flows.ts        (workflow logic)
        │     ├── db.logic.ts
        │     └── data.logic.ts
        ├── db.logic.ts     (direct DAF calls)
        └── data.logic.ts   (direct transforms)

db.logic.ts และ data.logic.ts เป็น shared resource — task และ flow เรียกได้โดยตรง ไม่ต้องผ่านกัน


Import/Export Strategy

Entry-Only Export Principle

Data Layer export เฉพาะ Entry class เท่านั้น เพื่อซ่อน implementation details

// ✅ command/return-request-lv40/index.ts
export { ReturnRequestLv40V1Entry } from './entry';

// 🔒 Internal — ไม่ export ออกนอก action folder:
// - internal.type.ts  (internal types ทั้งหมด)
// - entry.ts
// - returnRequest.task.ts + checkStatus.task.ts
// - flows.ts
// - db.logic.ts + data.logic.ts

Static Import Strategy (2-Level Support)

// Level 1: Action-Level Import (Recommended — most performant)
import { ReturnRequestLv40V1Entry } from
  '@feedos-frgm-system/api-data/document-process-api/command/return-request-lv40';

// Level 2: API-Level Import (Convenience)
import {
  ReturnRequestLv40V1Entry,
  ApproveDocumentEntry,
  GetDocumentStatusEntry,
} from '@feedos-frgm-system/api-data/document-process-api';

API-Level Index (No Nested Barrel Exports)

// ✅ document-process-api/index.ts
// Direct re-exports เท่านั้น — ไม่มี export * from './command'

export { ReturnRequestLv40V1Entry } from './command/return-request-lv40';
export { ApproveDocumentEntry } from './command/approve-document';
export { ValidateDocumentLv30Entry } from './command/validate-document-lv30';

export { GetDocumentStatusEntry } from './query/get-document-status';
export { ListDocumentsEntry } from './query/list-documents';
export { SearchDocumentsEntry } from './query/search-documents';

Layer Separation Compliance

// ✅ Service Layer — ใช้ interface จาก Abstraction Layer
import type {
  ReturnRequestInput,
  ReturnRequestOutput,
  Repository,
} from '@feedos-frgm-system/shared-api-core/document-process-api/command/return-request-lv40';

// ✅ DI Configuration — ใช้ concrete class จาก Data Layer
import { ReturnRequestLv40V1Entry } from
  '@feedos-frgm-system/api-data/document-process-api';

const container = new Container();
container.bind<Repository>('ReturnRequestRepo').to(ReturnRequestLv40V1Entry);

Benefits of Action-Based Structure

1. Clear Alignment

  • Abstraction Layer ↔ Data Layer: 1:1 mapping ทำให้ navigate ง่าย
  • Mental Model: Developer ไม่ต้อง switch context เวลาเปลี่ยนชั้น
  • Consistent Naming: ชื่อ folder และ class เหมือนกันทุกชั้น

2. Build Time Performance

  • 1 test file per action: ลด Jest worker spawn overhead
  • 1 flows.ts per action: ลด file I/O และ module resolution
  • Flat structure: ไม่มี nested folder ทำให้ TypeScript resolve import เร็ว

3. SonarQube Compliance

  • S3776 Cognitive Complexity: flows.ts กระจาย complexity ออกจาก task
  • S104 File Lines of Code: แต่ละไฟล์มี single responsibility ทำให้ LOC ต่ำตามธรรมชาติ
  • S138 Function Lines: DAF functions และ flow functions สั้นและ focused
  • CPD (Copy-Paste Detection): sonar.cpd.exclusions=**/*.type.ts,**/__tests__/**/*.test.ts ป้องกัน false positive จาก type definitions และ test code ที่ต้องการ explicit มากกว่า DRY

4. Zero-Decision Convention

  • task: 1 file ต่อ method เสมอ — ไม่ต้องคิดว่า “method นี้ซับซ้อนพอที่จะแยกไหม”
  • flows.ts: มีเมื่อ task มี named workflow — ไม่ต้องคิดเกณฑ์ LOC หรือ error handling
  • db/data logic: 1 file ต่อ action เสมอ — share ได้ ไม่ต้อง copy

5. Maintainability

  • Easy Navigation: เปิด action folder เห็นทุก file ทันที
  • Shared DAF: method ใหม่ใช้ DAF เดิมได้เลยโดยไม่ต้อง import ข้าม folder
  • Independent Evolution: แต่ละ action พัฒนาแยกกันได้

Conclusion

การใช้ Action-Based Structure ที่ปรับปรุงแล้วทำให้:

  1. Build time เร็ว — 1 test file per action ลด Jest overhead, flat structure ลด module resolution
  2. SonarQube ผ่านflows.ts แก้ S3776, .type.ts แก้ CPD false positive, convention ป้องกัน S104/S138
  3. Zero-decision convention — developer ทุกคนทำเหมือนกันโดยไม่ต้องตัดสินใจ
  4. Shared resourcedb.logic.ts และ data.logic.ts เป็นของทั้ง action ไม่ใช่ของ method
  5. Internal types รวมที่เดียวinternal.type.ts 1 ไฟล์ต่อ action ไม่มี cross-file type import, ไม่มี duplicate
  6. Public types ชัดเจน — Data Layer ไม่มี public types ของตัวเอง consumer import types จาก Abstraction Layer เท่านั้น
  7. Navigation ง่าย — flat folder เห็นทุกไฟล์ทันที ไม่ต้องดำดิ่ง sub-folder

Structure นี้เหมาะสมกับ Data Layer ที่เน้น simplicity, build performance, และ team consistency

Supawut Thomas

Supawut Thomas

Software Developer

มีประสบการณ์พัฒนา Software ระดับ Enterprise มากกว่า 10 ปี ผ่านงานจริงหลากหลายโปรเจกต์องค์กร — เชื่อว่าความรู้ที่ดีที่สุดคือความรู้ที่มาจากประสบการณ์จริง และอยากแบ่งปันสิ่งเหล่านั้นให้เพื่อน Developer ทุกคนได้นำไปพัฒนาตัวเองได้ดีขึ้นในทุกๆ วัน