Files
admin 02400fd88c
CI / CD Pipeline / build (push) Failing after 7m53s
CI / CD Pipeline / deploy (push) Has been skipped
690412:1716 Done Task-FE-AI-03
2026-04-12 17:16:37 +07:00

4.9 KiB

Code Snippets

Version: 1.8.6 Last Updated: 2026-04-10 Location: specs/05-Engineering-Guidelines/05-06-code-snippets.md


Backend DTO Pattern

// [dto-new] → Create DTO with validator
@IsUUID()
@ApiProperty({ description: 'Project UUID (public)' })
projectUuid!: string;

@IsOptional()
@IsInt()
@ApiProperty({ required: false, description: 'Internal project ID' })
projectId?: number; // resolved internally, never from client

Frontend Form Pattern

// [form-rhf-zod] → Create form schema + hook
const schema = z.object({
  projectUuid: z.string().uuid('รหัสโครงการไม่ถูกต้อง'),
  title: z.string().min(3, 'กรุณากรอกหัวข้ออย่างน้อย 3 ตัวอักษร'),
});
const form = useForm({ resolver: zodResolver(schema) });

UUID Safe Pattern

// [uuid-safe] → Check UUID before use
const safeUuid = (val: string | number): string => {
  if (typeof val === 'number') {
    Logger.warn(`UUID received as number: ${val}`);
    return String(val); // or throw error per policy
  }
  return val;
};

Backend Error Handling Pattern

// [backend-error] → Standard error handling
if (!entity) {
  this.logger.warn(`Entity not found: ${uuid}`, 'Service.findOne');
  throw new NotFoundException(`Resource with UUID ${uuid} not found`);
}

Frontend Query Pattern

// [frontend-query] → TanStack Query v5 standard
const { data, error, isLoading } = useQuery({
  queryKey: ['correspondence', uuid],
  queryFn: () => api.get(`/correspondences/${uuid}`),
});
// v5: onError removed from useQuery — handle error via return value
if (error) toast.error('ไม่สามารถโหลดข้อมูลได้');

Redis Cache Pattern

// [redis-cache] → Cache-Aside Pattern
const cacheKey = `correspondence:${uuid}`;
const cached = await this.cacheManager.get(cacheKey);
if (cached) return cached;
const entity = await this.repo.findOneBy({ uuid });
if (entity) {
  await this.cacheManager.set(cacheKey, entity, 300); // 5 minutes
}
return entity;

Workflow Transition Pattern

// [workflow-transition] → Pattern สำหรับการเปลี่ยนสถานะเอกสารอย่างปลอดภัย
// ใช้ใน: WorkflowEngineService

async transitionStatus(
  publicId: string, // รับ UUIDv7 เท่านั้น (ADR-019)
  targetStatus: DocumentStatus,
  actor: RequestWithUser
): Promise<Document> {
  // 1. ค้นหา Entity ด้วย publicId และตรวจสอบการมีอยู่
  const document = await this.repo.findOne({
    where: { publicId },
    relations: ['currentAssignee'],
  });

  if (!document) {
    this.logger.warn(`ไม่พบเอกสาร UUID: ${publicId}`, 'WorkflowService');
    throw new NotFoundException(ErrorCode.DOC_NOT_FOUND);
  }

  // 2. ตรวจสอบว่า transition นี้ถูกต้องตาม DSL (ADR-001)
  await this.workflowEngine.validateTransition(document.workflowState, targetStatus);

  // 3. Business Logic Validation: ตรวจสอบสิทธิ์ผู้รับผิดชอบ
  if (document.currentAssignee?.publicId !== actor.user.publicId) {
    throw new ForbiddenException(ErrorCode.UNAUTHORIZED_TRANSITION);
  }

  // 4. ปรับปรุงสถานะและใช้ @VersionColumn ใน Entity เพื่อทำ Optimistic Locking
  try {
    document.status = targetStatus;
    document.updatedBy = actor.user.publicId;

    const savedDoc = await this.repo.save(document);

    // 5. บันทึก Audit Log
    this.logger.log(
      `เอกสาร ${publicId} เปลี่ยนสถานะเป็น ${targetStatus} โดย ${actor.user.publicId}`
    );

    // 6. ส่งงานเข้า Queue (BullMQ) สำหรับการส่ง Notification/Email (ADR-008)
    await this.notificationQueue.add('status-change', {
      docId: savedDoc.publicId,
      status: targetStatus,
      recipientPublicId: savedDoc.creatorPublicId, // ใช้ publicId ตาม ADR-019
    });

    return savedDoc;
  } catch (error) {
    if (error instanceof OptimisticLockVersionMismatchError) {
      // ป้องกันการแก้ไขซ้ำซ้อนในเวลาเดียวกัน (Race Condition)
      throw new ConflictException('ข้อมูลถูกแก้ไขโดยผู้อื่นไปก่อนหน้าแล้ว กรุณารีเฟรชหน้าจอ');
    }
    throw error;
  }
}

Reference