251128:1700 Backend to T3.1.1
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
// File: src/modules/workflow-engine/entities/workflow-history.entity.ts
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { WorkflowInstance } from './workflow-instance.entity';
|
||||
|
||||
@Entity('workflow_histories')
|
||||
export class WorkflowHistory {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@ManyToOne(() => WorkflowInstance, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'instance_id' })
|
||||
instance!: WorkflowInstance;
|
||||
|
||||
@Column({ name: 'instance_id' })
|
||||
instanceId!: string;
|
||||
|
||||
@Column({ name: 'from_state', length: 50 })
|
||||
fromState!: string;
|
||||
|
||||
@Column({ name: 'to_state', length: 50 })
|
||||
toState!: string;
|
||||
|
||||
@Column({ length: 50 })
|
||||
action!: string;
|
||||
|
||||
@Column({ name: 'action_by_user_id', nullable: true })
|
||||
actionByUserId?: number; // User ID ของผู้ดำเนินการ
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
comment?: string;
|
||||
|
||||
@Column({ type: 'json', nullable: true })
|
||||
metadata?: Record<string, any>; // เก็บข้อมูลเพิ่มเติม เช่น Snapshot ของ Context ณ ตอนนั้น
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// File: src/modules/workflow-engine/entities/workflow-instance.entity.ts
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { WorkflowDefinition } from './workflow-definition.entity';
|
||||
|
||||
export enum WorkflowStatus {
|
||||
ACTIVE = 'ACTIVE',
|
||||
COMPLETED = 'COMPLETED',
|
||||
CANCELLED = 'CANCELLED',
|
||||
TERMINATED = 'TERMINATED',
|
||||
}
|
||||
|
||||
@Entity('workflow_instances')
|
||||
@Index(['entityType', 'entityId']) // Index สำหรับค้นหาตามเอกสาร
|
||||
@Index(['currentState']) // Index สำหรับ Filter ตามสถานะ
|
||||
export class WorkflowInstance {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
// เชื่อมโยงกับ Definition ที่ใช้ตอนสร้าง Instance นี้
|
||||
@ManyToOne(() => WorkflowDefinition)
|
||||
@JoinColumn({ name: 'definition_id' })
|
||||
definition!: WorkflowDefinition;
|
||||
|
||||
@Column({ name: 'definition_id' })
|
||||
definitionId!: string;
|
||||
|
||||
// Polymorphic Relation: เชื่อมกับเอกสารได้หลายประเภท (RFA, CORR, etc.)
|
||||
@Column({ name: 'entity_type', length: 50 })
|
||||
entityType!: string;
|
||||
|
||||
@Column({ name: 'entity_id', length: 50 })
|
||||
entityId!: string; // รองรับทั้ง ID แบบ Int และ UUID (เก็บเป็น String)
|
||||
|
||||
@Column({ name: 'current_state', length: 50 })
|
||||
currentState!: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: WorkflowStatus,
|
||||
default: WorkflowStatus.ACTIVE,
|
||||
})
|
||||
status!: WorkflowStatus;
|
||||
|
||||
// Context เฉพาะของ Instance นี้ (เช่น ตัวแปรที่ส่งต่อระหว่าง State)
|
||||
@Column({ type: 'json', nullable: true })
|
||||
context?: Record<string, any>;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// File: src/modules/workflow-engine/workflow-dsl.service.ts
|
||||
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
export interface WorkflowState {
|
||||
initial?: boolean;
|
||||
@@ -17,7 +17,7 @@ export interface TransitionRule {
|
||||
export interface RequirementRule {
|
||||
role?: string;
|
||||
user?: string;
|
||||
condition?: string; // e.g. "amount > 5000" (Advanced)
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
export interface EventRule {
|
||||
@@ -36,11 +36,12 @@ export interface CompiledWorkflow {
|
||||
export class WorkflowDslService {
|
||||
/**
|
||||
* คอมไพล์ DSL Input ให้เป็น Standard Execution Tree
|
||||
* @param dsl ข้อมูลดิบจาก User (JSON/Object)
|
||||
* @returns CompiledWorkflow Object ที่พร้อมใช้งาน
|
||||
*/
|
||||
compile(dsl: any): CompiledWorkflow {
|
||||
// 1. Basic Structure Validation
|
||||
if (!dsl || typeof dsl !== 'object') {
|
||||
throw new BadRequestException('DSL must be a valid JSON object.');
|
||||
}
|
||||
|
||||
if (!dsl.states || !Array.isArray(dsl.states)) {
|
||||
throw new BadRequestException(
|
||||
'DSL syntax error: "states" array is required.',
|
||||
@@ -55,7 +56,6 @@ export class WorkflowDslService {
|
||||
|
||||
const stateMap = new Set<string>();
|
||||
|
||||
// 2. First Pass: Collect all state names and normalize structure
|
||||
for (const rawState of dsl.states) {
|
||||
if (!rawState.name) {
|
||||
throw new BadRequestException(
|
||||
@@ -71,7 +71,6 @@ export class WorkflowDslService {
|
||||
transitions: {},
|
||||
};
|
||||
|
||||
// Normalize transitions "on:"
|
||||
if (rawState.on) {
|
||||
for (const [action, rule] of Object.entries(rawState.on)) {
|
||||
const rawRule = rule as any;
|
||||
@@ -86,15 +85,11 @@ export class WorkflowDslService {
|
||||
compiled.states[rawState.name] = normalizedState;
|
||||
}
|
||||
|
||||
// 3. Second Pass: Validate Integrity
|
||||
this.validateIntegrity(compiled, stateMap);
|
||||
|
||||
return compiled;
|
||||
}
|
||||
|
||||
/**
|
||||
* ตรวจสอบความสมบูรณ์ของ Workflow Logic
|
||||
*/
|
||||
private validateIntegrity(compiled: CompiledWorkflow, stateMap: Set<string>) {
|
||||
let hasInitial = false;
|
||||
|
||||
@@ -107,19 +102,13 @@ export class WorkflowDslService {
|
||||
hasInitial = true;
|
||||
}
|
||||
|
||||
// ตรวจสอบ Transitions
|
||||
if (state.transitions) {
|
||||
for (const [action, rule] of Object.entries(state.transitions)) {
|
||||
// 1. ปลายทางต้องมีอยู่จริง
|
||||
if (!stateMap.has(rule.to)) {
|
||||
throw new BadRequestException(
|
||||
`DSL Error: State "${stateName}" transitions via "${action}" to unknown state "${rule.to}".`,
|
||||
);
|
||||
}
|
||||
// 2. Action name convention (Optional but recommended)
|
||||
if (!/^[A-Z0-9_]+$/.test(action)) {
|
||||
// Warning or Strict Error could be here
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,18 +118,11 @@ export class WorkflowDslService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ประเมินผล (Evaluate) การเปลี่ยนสถานะ
|
||||
* @param compiled ข้อมูล Workflow ที่ Compile แล้ว
|
||||
* @param currentState สถานะปัจจุบัน
|
||||
* @param action การกระทำ
|
||||
* @param context ข้อมูลประกอบ (User roles, etc.)
|
||||
*/
|
||||
evaluate(
|
||||
compiled: CompiledWorkflow,
|
||||
currentState: string,
|
||||
action: string,
|
||||
context: any,
|
||||
context: any = {}, // Default empty object
|
||||
): { nextState: string; events: EventRule[] } {
|
||||
const stateConfig = compiled.states[currentState];
|
||||
|
||||
@@ -164,7 +146,6 @@ export class WorkflowDslService {
|
||||
);
|
||||
}
|
||||
|
||||
// Check Requirements (RBAC Logic inside Engine)
|
||||
if (transition.requirements && transition.requirements.length > 0) {
|
||||
this.checkRequirements(transition.requirements, context);
|
||||
}
|
||||
@@ -175,22 +156,19 @@ export class WorkflowDslService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ตรวจสอบเงื่อนไขสิทธิ์ (Requirements)
|
||||
*/
|
||||
private checkRequirements(requirements: RequirementRule[], context: any) {
|
||||
const userRoles = context.roles || [];
|
||||
const userId = context.userId;
|
||||
const safeContext = context || {};
|
||||
const userRoles = safeContext.roles || [];
|
||||
const userId = safeContext.userId;
|
||||
|
||||
const isAllowed = requirements.some((req) => {
|
||||
// กรณีเช็ค Role
|
||||
if (req.role) {
|
||||
return userRoles.includes(req.role);
|
||||
}
|
||||
// กรณีเช็ค Specific User
|
||||
if (req.user) {
|
||||
return userId === req.user;
|
||||
}
|
||||
// Future: Add Condition Logic Evaluation here
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
// File: src/modules/workflow-engine/workflow-engine.controller.ts
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Query,
|
||||
Patch,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common'; // เพิ่ม Patch, Param
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { WorkflowEngineService } from './workflow-engine.service';
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { CreateWorkflowDefinitionDto } from './dto/create-workflow-definition.dto';
|
||||
import { EvaluateWorkflowDto } from './dto/evaluate-workflow.dto';
|
||||
import { GetAvailableActionsDto } from './dto/get-available-actions.dto'; // [NEW]
|
||||
import { UpdateWorkflowDefinitionDto } from './dto/update-workflow-definition.dto'; // [NEW]
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { GetAvailableActionsDto } from './dto/get-available-actions.dto';
|
||||
import { UpdateWorkflowDefinitionDto } from './dto/update-workflow-definition.dto';
|
||||
import { WorkflowEngineService } from './workflow-engine.service';
|
||||
|
||||
@ApiTags('Workflow Engine (DSL)')
|
||||
@Controller('workflow-engine')
|
||||
@UseGuards(JwtAuthGuard) // Protect all endpoints
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class WorkflowEngineController {
|
||||
constructor(private readonly workflowService: WorkflowEngineService) {}
|
||||
|
||||
@@ -42,24 +43,20 @@ export class WorkflowEngineController {
|
||||
@Get('actions')
|
||||
@ApiOperation({ summary: 'Get available actions for current state' })
|
||||
async getAvailableActions(@Query() query: GetAvailableActionsDto) {
|
||||
// [UPDATED] ใช้ DTO แทนแยก Query
|
||||
return this.workflowService.getAvailableActions(
|
||||
query.workflow_code,
|
||||
query.current_state,
|
||||
);
|
||||
}
|
||||
|
||||
// [OPTIONAL/RECOMMENDED] เพิ่ม Endpoint สำหรับ Update (PATCH)
|
||||
@Patch('definitions/:id')
|
||||
@ApiOperation({
|
||||
summary: 'Update workflow status or details (e.g. Deactivate)',
|
||||
summary: 'Update workflow status or details (DSL Re-compile)',
|
||||
})
|
||||
async updateDefinition(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateWorkflowDefinitionDto, // [NEW] ใช้ Update DTO
|
||||
@Param('id', ParseUUIDPipe) id: string, // เพิ่ม ParseUUIDPipe เพื่อ Validate ID
|
||||
@Body() dto: UpdateWorkflowDefinitionDto,
|
||||
) {
|
||||
// *หมายเหตุ: คุณต้องไปเพิ่ม method update() ใน Service ด้วยถ้าจะใช้ Endpoint นี้
|
||||
// return this.workflowService.update(id, dto);
|
||||
return { message: 'Update logic not implemented yet', id, ...dto };
|
||||
return this.workflowService.update(id, dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,23 @@
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { WorkflowDefinition } from './entities/workflow-definition.entity';
|
||||
import { WorkflowHistory } from './entities/workflow-history.entity'; // [New]
|
||||
import { WorkflowInstance } from './entities/workflow-instance.entity'; // [New]
|
||||
import { WorkflowDslService } from './workflow-dsl.service';
|
||||
import { WorkflowEngineController } from './workflow-engine.controller';
|
||||
import { WorkflowEngineService } from './workflow-engine.service';
|
||||
import { WorkflowDslService } from './workflow-dsl.service'; // [New] ต้องสร้างไฟล์นี้ตามแผน Phase 6A
|
||||
import { WorkflowEngineController } from './workflow-engine.controller'; // [New] ต้องสร้างไฟล์นี้ตามแผน Phase 6A
|
||||
import { WorkflowDefinition } from './entities/workflow-definition.entity'; // [New] ต้องสร้างไฟล์นี้ตามแผน Phase 6A
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// เชื่อมต่อกับตาราง workflow_definitions
|
||||
TypeOrmModule.forFeature([WorkflowDefinition]),
|
||||
TypeOrmModule.forFeature([
|
||||
WorkflowDefinition,
|
||||
WorkflowInstance, // [New]
|
||||
WorkflowHistory, // [New]
|
||||
]),
|
||||
],
|
||||
controllers: [WorkflowEngineController], // เพิ่ม Controller สำหรับรับ API
|
||||
providers: [
|
||||
WorkflowEngineService, // Service หลัก
|
||||
WorkflowDslService, // [New] Service สำหรับ Compile/Validate DSL
|
||||
],
|
||||
exports: [WorkflowEngineService], // Export ให้ module อื่นใช้เหมือนเดิม
|
||||
controllers: [WorkflowEngineController],
|
||||
providers: [WorkflowEngineService, WorkflowDslService],
|
||||
exports: [WorkflowEngineService],
|
||||
})
|
||||
export class WorkflowEngineModule {}
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
// File: src/modules/workflow-engine/workflow-engine.service.ts
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
// Entities
|
||||
import { WorkflowDefinition } from './entities/workflow-definition.entity';
|
||||
import { WorkflowDslService, CompiledWorkflow } from './workflow-dsl.service';
|
||||
import { WorkflowHistory } from './entities/workflow-history.entity';
|
||||
import {
|
||||
WorkflowInstance,
|
||||
WorkflowStatus,
|
||||
} from './entities/workflow-instance.entity';
|
||||
|
||||
// Services & Interfaces
|
||||
import { CreateWorkflowDefinitionDto } from './dto/create-workflow-definition.dto';
|
||||
import { EvaluateWorkflowDto } from './dto/evaluate-workflow.dto';
|
||||
import { UpdateWorkflowDefinitionDto } from './dto/update-workflow-definition.dto';
|
||||
import { CompiledWorkflow, WorkflowDslService } from './workflow-dsl.service';
|
||||
|
||||
// Interface สำหรับ Backward Compatibility (Logic เดิม)
|
||||
// Legacy Interface (Backward Compatibility)
|
||||
export enum WorkflowAction {
|
||||
APPROVE = 'APPROVE',
|
||||
REJECT = 'REJECT',
|
||||
@@ -35,11 +44,16 @@ export class WorkflowEngineService {
|
||||
constructor(
|
||||
@InjectRepository(WorkflowDefinition)
|
||||
private readonly workflowDefRepo: Repository<WorkflowDefinition>,
|
||||
@InjectRepository(WorkflowInstance)
|
||||
private readonly instanceRepo: Repository<WorkflowInstance>,
|
||||
@InjectRepository(WorkflowHistory)
|
||||
private readonly historyRepo: Repository<WorkflowHistory>,
|
||||
private readonly dslService: WorkflowDslService,
|
||||
private readonly dataSource: DataSource, // ใช้สำหรับ Transaction
|
||||
) {}
|
||||
|
||||
// =================================================================
|
||||
// [NEW] DSL & Workflow Engine (Phase 6A)
|
||||
// [PART 1] Definition Management (Phase 6A)
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
@@ -48,8 +62,10 @@ export class WorkflowEngineService {
|
||||
async createDefinition(
|
||||
dto: CreateWorkflowDefinitionDto,
|
||||
): Promise<WorkflowDefinition> {
|
||||
// 1. Compile & Validate DSL
|
||||
const compiled = this.dslService.compile(dto.dsl);
|
||||
|
||||
// 2. Check latest version
|
||||
const latest = await this.workflowDefRepo.findOne({
|
||||
where: { workflow_code: dto.workflow_code },
|
||||
order: { version: 'DESC' },
|
||||
@@ -57,6 +73,7 @@ export class WorkflowEngineService {
|
||||
|
||||
const nextVersion = latest ? latest.version + 1 : 1;
|
||||
|
||||
// 3. Save new version
|
||||
const entity = this.workflowDefRepo.create({
|
||||
workflow_code: dto.workflow_code,
|
||||
version: nextVersion,
|
||||
@@ -65,9 +82,16 @@ export class WorkflowEngineService {
|
||||
is_active: dto.is_active ?? true,
|
||||
});
|
||||
|
||||
return this.workflowDefRepo.save(entity);
|
||||
const saved = await this.workflowDefRepo.save(entity);
|
||||
this.logger.log(
|
||||
`Created Workflow Definition: ${saved.workflow_code} v${saved.version}`,
|
||||
);
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* อัปเดต Definition (Re-compile DSL)
|
||||
*/
|
||||
async update(
|
||||
id: string,
|
||||
dto: UpdateWorkflowDefinitionDto,
|
||||
@@ -95,33 +119,9 @@ export class WorkflowEngineService {
|
||||
return this.workflowDefRepo.save(definition);
|
||||
}
|
||||
|
||||
async evaluate(dto: EvaluateWorkflowDto): Promise<any> {
|
||||
const definition = await this.workflowDefRepo.findOne({
|
||||
where: { workflow_code: dto.workflow_code, is_active: true },
|
||||
order: { version: 'DESC' },
|
||||
});
|
||||
|
||||
if (!definition) {
|
||||
throw new NotFoundException(
|
||||
`No active workflow definition found for "${dto.workflow_code}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const compiled: CompiledWorkflow = definition.compiled;
|
||||
const result = this.dslService.evaluate(
|
||||
compiled,
|
||||
dto.current_state,
|
||||
dto.action,
|
||||
dto.context || {},
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Workflow Evaluated: ${dto.workflow_code} [${dto.current_state}] --${dto.action}--> [${result.nextState}]`,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึง Action ที่ทำได้ ณ State ปัจจุบัน
|
||||
*/
|
||||
async getAvailableActions(
|
||||
workflowCode: string,
|
||||
currentState: string,
|
||||
@@ -140,25 +140,206 @@ export class WorkflowEngineService {
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// [LEGACY] Backward Compatibility for Correspondence/RFA Modules
|
||||
// คืนค่า Logic เดิมเพื่อไม่ให้ Module อื่น Error (TS2339)
|
||||
// [PART 2] Runtime Engine (Phase 3.1)
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* เริ่มต้น Workflow Instance ใหม่สำหรับเอกสาร
|
||||
*/
|
||||
async createInstance(
|
||||
workflowCode: string,
|
||||
entityType: string,
|
||||
entityId: string,
|
||||
initialContext: Record<string, any> = {},
|
||||
): Promise<WorkflowInstance> {
|
||||
// 1. หา Definition ล่าสุด
|
||||
const definition = await this.workflowDefRepo.findOne({
|
||||
where: { workflow_code: workflowCode, is_active: true },
|
||||
order: { version: 'DESC' },
|
||||
});
|
||||
|
||||
if (!definition) {
|
||||
throw new NotFoundException(
|
||||
`Workflow "${workflowCode}" not found or inactive.`,
|
||||
);
|
||||
}
|
||||
|
||||
// 2. หา Initial State จาก Compiled Structure
|
||||
const compiled: CompiledWorkflow = definition.compiled;
|
||||
const initialState = Object.keys(compiled.states).find(
|
||||
(key) => compiled.states[key].initial,
|
||||
);
|
||||
|
||||
if (!initialState) {
|
||||
throw new BadRequestException(
|
||||
`Workflow "${workflowCode}" has no initial state defined.`,
|
||||
);
|
||||
}
|
||||
|
||||
// 3. สร้าง Instance
|
||||
const instance = this.instanceRepo.create({
|
||||
definition,
|
||||
entityType,
|
||||
entityId,
|
||||
currentState: initialState,
|
||||
status: WorkflowStatus.ACTIVE,
|
||||
context: initialContext,
|
||||
});
|
||||
|
||||
const savedInstance = await this.instanceRepo.save(instance);
|
||||
this.logger.log(
|
||||
`Started Workflow Instance: ${workflowCode} for ${entityType}:${entityId}`,
|
||||
);
|
||||
return savedInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* ดำเนินการเปลี่ยนสถานะ (Transition) ของ Instance จริงแบบ Transactional
|
||||
*/
|
||||
async processTransition(
|
||||
instanceId: string,
|
||||
action: string,
|
||||
userId: number,
|
||||
comment?: string,
|
||||
payload: Record<string, any> = {},
|
||||
) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// 1. Lock Instance เพื่อป้องกัน Race Condition (Pessimistic Write Lock)
|
||||
const instance = await queryRunner.manager.findOne(WorkflowInstance, {
|
||||
where: { id: instanceId },
|
||||
relations: ['definition'],
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
|
||||
if (!instance) {
|
||||
throw new NotFoundException(
|
||||
`Workflow Instance "${instanceId}" not found.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (instance.status !== WorkflowStatus.ACTIVE) {
|
||||
throw new BadRequestException(
|
||||
`Workflow is not active (Status: ${instance.status}).`,
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Evaluate Logic ผ่าน DSL Service
|
||||
const compiled: CompiledWorkflow = instance.definition.compiled;
|
||||
const context = { ...instance.context, userId, ...payload }; // Merge Context
|
||||
|
||||
// * DSL Service จะ throw error ถ้า action ไม่ถูกต้อง หรือสิทธิ์ไม่พอ
|
||||
const evaluation = this.dslService.evaluate(
|
||||
compiled,
|
||||
instance.currentState,
|
||||
action,
|
||||
context,
|
||||
);
|
||||
|
||||
const fromState = instance.currentState;
|
||||
const toState = evaluation.nextState;
|
||||
|
||||
// 3. อัปเดต Instance
|
||||
instance.currentState = toState;
|
||||
instance.context = context; // อัปเดต Context ด้วย
|
||||
|
||||
// เช็คว่าเป็น Terminal State หรือไม่?
|
||||
if (compiled.states[toState].terminal) {
|
||||
instance.status = WorkflowStatus.COMPLETED;
|
||||
}
|
||||
|
||||
await queryRunner.manager.save(instance);
|
||||
|
||||
// 4. บันทึก History (Audit Trail)
|
||||
const history = this.historyRepo.create({
|
||||
instanceId: instance.id,
|
||||
fromState,
|
||||
toState,
|
||||
action,
|
||||
actionByUserId: userId,
|
||||
comment,
|
||||
metadata: {
|
||||
events: evaluation.events,
|
||||
payload,
|
||||
},
|
||||
});
|
||||
await queryRunner.manager.save(history);
|
||||
|
||||
// 5. Trigger Events (Integration Point)
|
||||
// ในอนาคตสามารถ Inject NotificationService มาเรียกตรงนี้ได้
|
||||
if (evaluation.events && evaluation.events.length > 0) {
|
||||
this.logger.log(
|
||||
`Triggering ${evaluation.events.length} events for instance ${instanceId}`,
|
||||
);
|
||||
// await this.eventHandler.handle(evaluation.events);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
this.logger.log(
|
||||
`Transition: ${instanceId} [${fromState}] --${action}--> [${toState}] by User:${userId}`,
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
nextState: toState,
|
||||
events: evaluation.events,
|
||||
isCompleted: instance.status === WorkflowStatus.COMPLETED,
|
||||
};
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Transition Failed for ${instanceId}: ${(err as Error).message}`,
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (Utility) Evaluate แบบไม่บันทึกผล (Dry Run) สำหรับ Test หรือ Preview
|
||||
*/
|
||||
async evaluate(dto: EvaluateWorkflowDto): Promise<any> {
|
||||
const definition = await this.workflowDefRepo.findOne({
|
||||
where: { workflow_code: dto.workflow_code, is_active: true },
|
||||
order: { version: 'DESC' },
|
||||
});
|
||||
|
||||
if (!definition) {
|
||||
throw new NotFoundException(`Workflow "${dto.workflow_code}" not found`);
|
||||
}
|
||||
|
||||
return this.dslService.evaluate(
|
||||
definition.compiled,
|
||||
dto.current_state,
|
||||
dto.action,
|
||||
dto.context || {},
|
||||
);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// [PART 3] Legacy Support (Backward Compatibility)
|
||||
// รักษา Logic เดิมไว้เพื่อให้ Module อื่น (Correspondence/RFA) ทำงานต่อได้
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* คำนวณสถานะถัดไปแบบ Linear Sequence (Logic เดิม)
|
||||
* ใช้สำหรับ CorrespondenceService และ RfaService ที่ยังไม่ได้ Refactor
|
||||
* @deprecated แนะนำให้เปลี่ยนไปใช้ processTransition แทนเมื่อ Refactor เสร็จ
|
||||
*/
|
||||
processAction(
|
||||
currentSequence: number,
|
||||
totalSteps: number,
|
||||
action: string, // รับเป็น string เพื่อความยืดหยุ่น
|
||||
action: string,
|
||||
returnToSequence?: number,
|
||||
): TransitionResult {
|
||||
// Map string action to enum logic
|
||||
switch (action) {
|
||||
case WorkflowAction.APPROVE:
|
||||
case WorkflowAction.ACKNOWLEDGE:
|
||||
case 'APPROVE': // Case sensitive handling fallback
|
||||
case 'APPROVE':
|
||||
case 'ACKNOWLEDGE':
|
||||
if (currentSequence >= totalSteps) {
|
||||
return {
|
||||
@@ -193,7 +374,6 @@ export class WorkflowEngineService {
|
||||
};
|
||||
|
||||
default:
|
||||
// กรณีส่ง Action อื่นมา ให้ถือว่าเป็น Approve (หรือจะ Throw Error ก็ได้)
|
||||
this.logger.warn(
|
||||
`Unknown legacy action: ${action}, treating as next step.`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user