690406:2310 Done Task BE-ERR-01
CI / CD Pipeline / build (push) Failing after 4m53s
CI / CD Pipeline / deploy (push) Has been skipped

This commit is contained in:
2026-04-06 23:10:56 +07:00
parent c95e0f537e
commit 961ee72343
24 changed files with 1329 additions and 268 deletions
@@ -1,4 +1,9 @@
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import {
NotFoundException,
ValidationException,
WorkflowException,
} from '../../../common/exceptions';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WorkflowDslSchema, WorkflowDsl } from './workflow-dsl.schema';
@@ -36,16 +41,18 @@ export class WorkflowDslParser {
return await this.workflowDefRepo.save(definition);
} catch (error: unknown) {
if (error instanceof SyntaxError) {
throw new BadRequestException(`Invalid JSON: ${error.message}`);
throw new ValidationException(`Invalid JSON: ${error.message}`);
}
const err = error as {
name?: string;
errors?: unknown;
errors?: unknown[];
message?: string;
};
if (err.name === 'ZodError') {
throw new BadRequestException(
`Invalid workflow DSL: ${JSON.stringify(err.errors)}`
throw new WorkflowException(
'INVALID_WORKFLOW_DSL',
`Invalid workflow DSL: ${JSON.stringify(err.errors)}`,
'Workflow DSL ไม่ถูกต้อง'
);
}
throw error;
@@ -66,16 +73,20 @@ export class WorkflowDslParser {
// 1. Validate initial state
if (!stateSet.has(dsl.initialState)) {
throw new BadRequestException(
`Initial state "${dsl.initialState}" not found in states array`
throw new WorkflowException(
'DSL_INVALID_INITIAL_STATE',
`Initial state "${dsl.initialState}" not found in states array`,
'Initial State ไม่พบใน States Array'
);
}
// 2. Validate final states
dsl.finalStates.forEach((state) => {
if (!stateSet.has(state)) {
throw new BadRequestException(
`Final state "${state}" not found in states array`
throw new WorkflowException(
'DSL_INVALID_FINAL_STATE',
`Final state "${state}" not found in states array`,
'Final State ไม่พบใน States Array'
);
}
});
@@ -86,15 +97,19 @@ export class WorkflowDslParser {
dsl.transitions.forEach((transition, index) => {
// Check 'from' state
if (!stateSet.has(transition.from)) {
throw new BadRequestException(
`Transition ${index}: 'from' state "${transition.from}" not found in states array`
throw new WorkflowException(
'DSL_INVALID_TRANSITION_FROM',
`Transition ${index}: 'from' state "${transition.from}" not found in states array`,
'Transition อ้างอิง State ที่ไม่พบ'
);
}
// Check 'to' state
if (!stateSet.has(transition.to)) {
throw new BadRequestException(
`Transition ${index}: 'to' state "${transition.to}" not found in states array`
throw new WorkflowException(
'DSL_INVALID_TRANSITION_TO',
`Transition ${index}: 'to' state "${transition.to}" not found in states array`,
'Transition อ้างอิง State ที่ไม่พบ'
);
}
@@ -120,8 +135,10 @@ export class WorkflowDslParser {
dsl.transitions.forEach((transition) => {
const key = `${transition.from}-${transition.trigger}-${transition.to}`;
if (transitionKeys.has(key)) {
throw new BadRequestException(
`Duplicate transition: ${transition.from} --[${transition.trigger}]--> ${transition.to}`
throw new WorkflowException(
'DSL_DUPLICATE_TRANSITION',
`Duplicate transition: ${transition.from} --[${transition.trigger}]--> ${transition.to}`,
'DSL มี Transition ซ้ำซ้อน'
);
}
transitionKeys.add(key);
@@ -158,9 +175,7 @@ export class WorkflowDslParser {
});
if (!definition) {
throw new BadRequestException(
`Workflow definition ${definitionId} not found`
);
throw new NotFoundException('Workflow definition', String(definitionId));
}
try {
@@ -171,8 +186,10 @@ export class WorkflowDslParser {
`Failed to parse stored DSL for definition ${definitionId}`,
error
);
throw new BadRequestException(
`Invalid stored DSL: ${error instanceof Error ? error.message : String(error)}`
throw new WorkflowException(
'INVALID_STORED_DSL',
`Invalid stored DSL: ${error instanceof Error ? error.message : String(error)}`,
'DSL ที่บันทึกไว้ไม่ถูกต้อง'
);
}
}
@@ -1,6 +1,10 @@
// File: src/modules/workflow-engine/workflow-dsl.service.ts
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import {
ValidationException,
WorkflowException,
} from '../../common/exceptions';
// ==========================================
// 1. Interfaces for RAW DSL (Input from User)
@@ -86,8 +90,11 @@ export class WorkflowDslService {
for (const rawState of dsl.states) {
if (rawState.initial) {
if (initialFound) {
throw new BadRequestException(
`DSL Error: Multiple initial states found (at "${rawState.name}").`
throw new WorkflowException(
'DSL_MULTIPLE_INITIAL_STATES',
`DSL Error: Multiple initial states found (at "${rawState.name}")`,
'DSL มี Initial State หลายค่า แต่ละ Workflow ต้องมีเพียง Initial State เดียว',
['ตรวจสอบโครงสร้าง DSL และแก้ไข Initial State']
);
}
compiled.initialState = rawState.name;
@@ -104,8 +111,11 @@ export class WorkflowDslService {
for (const [action, rule] of Object.entries(rawState.on)) {
// Validation: Target state must exist
if (!definedStates.has(rule.to)) {
throw new BadRequestException(
`DSL Error: State "${rawState.name}" transitions via "${action}" to unknown state "${rule.to}".`
throw new WorkflowException(
'DSL_UNKNOWN_TRANSITION_TARGET',
`DSL Error: State "${rawState.name}" transitions via "${action}" to unknown state "${rule.to}"`,
'DSL อ้างอิง State ที่ไม่พบ',
['ตรวจสอบชื่อ State ที่กำหนดใน Transition']
);
}
@@ -133,7 +143,12 @@ export class WorkflowDslService {
}
if (!initialFound) {
throw new BadRequestException('DSL Error: No initial state defined.');
throw new WorkflowException(
'DSL_NO_INITIAL_STATE',
'DSL Error: No initial state defined',
'DSL ไม่มีการกำหนด Initial State',
['เพิ่ม initial: true ใน State หนึ่ง']
);
}
return compiled;
@@ -153,15 +168,21 @@ export class WorkflowDslService {
// 1. Validate State Existence
if (!stateConfig) {
throw new BadRequestException(
`Runtime Error: Current state "${currentState}" is invalid.`
throw new WorkflowException(
'WORKFLOW_INVALID_CURRENT_STATE',
`Runtime Error: Current state "${currentState}" is invalid`,
'Workflow อยู่ในสถานะที่ไม่รู้จัก',
['ตรวจสอบ DSL ของ Workflow']
);
}
// 2. Check if terminal
if (stateConfig.terminal) {
throw new BadRequestException(
`Runtime Error: Cannot transition from terminal state "${currentState}".`
throw new WorkflowException(
'WORKFLOW_TERMINAL_STATE',
`Runtime Error: Cannot transition from terminal state "${currentState}"`,
'ไม่สามารถดำเนินการจาก State สุดท้ายได้',
['เอกสารสิ้นสุดกระบวนการแล้ว']
);
}
@@ -169,8 +190,11 @@ export class WorkflowDslService {
const transition = stateConfig.transitions[action];
if (!transition) {
const allowed = Object.keys(stateConfig.transitions).join(', ');
throw new BadRequestException(
`Invalid Action: "${action}" is not allowed from "${currentState}". Allowed: [${allowed}]`
throw new WorkflowException(
'WORKFLOW_INVALID_ACTION',
`Invalid Action: "${action}" is not allowed from "${currentState}". Allowed: [${allowed}]`,
`ไม่สามารถดำเนินการ "${action}" ในสถานะปัจจุบัน ทำได้: [${allowed}]`,
['เลือกการดำเนินการที่อนุญาตจากรายการ']
);
}
@@ -181,8 +205,11 @@ export class WorkflowDslService {
if (transition.condition) {
const isMet = this.evaluateCondition(transition.condition, context);
if (!isMet) {
throw new BadRequestException(
'Condition Failed: The criteria for this transition are not met.'
throw new WorkflowException(
'WORKFLOW_CONDITION_NOT_MET',
'Condition Failed: The criteria for this transition are not met',
'เงื่อนไขสำหรับการดำเนินการนี้ไม่ผ่าน',
['ตรวจสอบเงื่อนไขที่กำหนดใน Workflow']
);
}
}
@@ -199,12 +226,12 @@ export class WorkflowDslService {
private validateSchemaStructure(dsl: unknown) {
if (!dsl || typeof dsl !== 'object') {
throw new BadRequestException('DSL must be a JSON object.');
throw new ValidationException('DSL must be a JSON object');
}
const d = dsl as Record<string, unknown>;
if (!d.workflow || !d.states || !Array.isArray(d.states)) {
throw new BadRequestException(
'DSL Error: Missing required fields (workflow, states).'
throw new ValidationException(
'DSL Error: Missing required fields (workflow, states)'
);
}
}
@@ -226,15 +253,23 @@ export class WorkflowDslService {
if (requiredRoles.length > 0) {
const hasRole = requiredRoles.some((r) => userRoles.includes(r));
if (!hasRole) {
throw new BadRequestException(
`Access Denied: Required roles [${requiredRoles.join(', ')}]`
throw new WorkflowException(
'WORKFLOW_ROLE_REQUIRED',
`Access Denied: Required roles [${requiredRoles.join(', ')}]`,
`ต้องมี Role: [${requiredRoles.join(', ')}] จึงจะดำเนินการนี้ได้`,
['ติดต่อผู้ดูแลระบบเพื่อขอสิทธิ์']
);
}
}
// Check Specific User
if (req.userId && String(req.userId) !== String(userId)) {
throw new BadRequestException('Access Denied: User mismatch.');
throw new WorkflowException(
'WORKFLOW_USER_MISMATCH',
'Access Denied: User mismatch',
'ผู้ใช้ไม่ได้รับอนุญาตให้ดำเนินการนี้',
['ตรวจสอบว่าเล็็กชื่ออีเมลที่ป้อนให้เข้าสู่ระบบ']
);
}
}
@@ -1,14 +1,9 @@
// File: src/modules/workflow-engine/workflow-engine.service.ts
import {
BadRequestException,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { NotFoundException, WorkflowException } from '../../common/exceptions';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
// Entities
import { WorkflowDefinition } from './entities/workflow-definition.entity';
import { WorkflowHistory } from './entities/workflow-history.entity';
@@ -104,9 +99,7 @@ export class WorkflowEngineService {
): Promise<WorkflowDefinition> {
const definition = await this.workflowDefRepo.findOne({ where: { id } });
if (!definition) {
throw new NotFoundException(
`Workflow Definition with ID "${id}" not found`
);
throw new NotFoundException('Workflow Definition', id);
}
if (dto.dsl) {
@@ -115,8 +108,11 @@ export class WorkflowEngineService {
definition.dsl = dto.dsl as unknown as Record<string, unknown>;
definition.compiled = compiled as unknown as Record<string, unknown>;
} catch (error: unknown) {
throw new BadRequestException(
`Invalid DSL: ${error instanceof Error ? error.message : String(error)}`
throw new WorkflowException(
'INVALID_WORKFLOW_DSL',
`Invalid DSL: ${error instanceof Error ? error.message : String(error)}`,
'Workflow DSL ไม่ถูกต้อง กรุณาตรวจสอบโครงสร้าง',
['ตรวจสอบ syntax ของ DSL', 'ดูตัวอย่าง DSL ที่ถูกต้อง']
);
}
}
@@ -149,9 +145,7 @@ export class WorkflowEngineService {
async getDefinitionById(id: string): Promise<WorkflowDefinition> {
const definition = await this.workflowDefRepo.findOne({ where: { id } });
if (!definition) {
throw new NotFoundException(
`Workflow Definition with ID "${id}" not found`
);
throw new NotFoundException('Workflow Definition', id);
}
return definition;
}
@@ -197,9 +191,7 @@ export class WorkflowEngineService {
});
if (!definition) {
throw new NotFoundException(
`Workflow "${workflowCode}" not found or inactive.`
);
throw new NotFoundException('Workflow', workflowCode);
}
// 2. หา Initial State จาก Compiled Structure
@@ -209,8 +201,11 @@ export class WorkflowEngineService {
const initialState = compiled.initialState;
if (!initialState) {
throw new BadRequestException(
`Workflow "${workflowCode}" has no initial state defined.`
throw new WorkflowException(
'WORKFLOW_NO_INITIAL_STATE',
`Workflow "${workflowCode}" has no initial state defined`,
'Workflow ไม่มี Initial State ที่กำหนด',
['ตรวจสอบ DSL ของ Workflow นี้']
);
}
@@ -242,9 +237,7 @@ export class WorkflowEngineService {
});
if (!instance) {
throw new NotFoundException(
`Workflow Instance "${instanceId}" not found`
);
throw new NotFoundException('Workflow Instance', instanceId);
}
return instance;
@@ -276,14 +269,15 @@ export class WorkflowEngineService {
});
if (!instance) {
throw new NotFoundException(
`Workflow Instance "${instanceId}" not found.`
);
throw new NotFoundException('Workflow Instance', instanceId);
}
if (instance.status !== WorkflowStatus.ACTIVE) {
throw new BadRequestException(
`Workflow is not active (Status: ${instance.status}).`
throw new WorkflowException(
'WORKFLOW_NOT_ACTIVE',
`Workflow is not active (Status: ${instance.status})`,
'Workflow ไม่อยู่ในสถานะ Active',
['ตรวจสอบสถานะของ Workflow']
);
}
@@ -427,7 +421,12 @@ export class WorkflowEngineService {
case 'RETURN': {
const targetStep = returnToSequence || currentSequence - 1;
if (targetStep < 1) {
throw new BadRequestException('Cannot return beyond the first step');
throw new WorkflowException(
'WORKFLOW_INVALID_RETURN_TARGET',
'Cannot return beyond the first step',
'ไม่สามารถส่งคืนไปเกินกว่าขั้นตอนแรกได้',
['ตรวจสอบลำดับขั้นตอนที่ต้องการส่งคืน']
);
}
return {
nextStepSequence: targetStep,