This commit is contained in:
@@ -34,13 +34,18 @@ export class WorkflowDslParser {
|
||||
|
||||
// Step 5: Save to database
|
||||
return await this.workflowDefRepo.save(definition);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SyntaxError) {
|
||||
throw new BadRequestException(`Invalid JSON: ${error.message}`);
|
||||
}
|
||||
if (error.name === 'ZodError') {
|
||||
const err = error as {
|
||||
name?: string;
|
||||
errors?: unknown;
|
||||
message?: string;
|
||||
};
|
||||
if (err.name === 'ZodError') {
|
||||
throw new BadRequestException(
|
||||
`Invalid workflow DSL: ${JSON.stringify(error.errors)}`
|
||||
`Invalid workflow DSL: ${JSON.stringify(err.errors)}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
@@ -161,12 +166,14 @@ export class WorkflowDslParser {
|
||||
try {
|
||||
const dsl = definition.dsl;
|
||||
return WorkflowDslSchema.parse(dsl);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(
|
||||
`Failed to parse stored DSL for definition ${definitionId}`,
|
||||
error
|
||||
);
|
||||
throw new BadRequestException(`Invalid stored DSL: ${error?.message}`);
|
||||
throw new BadRequestException(
|
||||
`Invalid stored DSL: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,10 +186,12 @@ export class WorkflowDslParser {
|
||||
const dsl = WorkflowDslSchema.parse(rawDsl);
|
||||
this.validateStateMachine(dsl);
|
||||
return { valid: true };
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
valid: false,
|
||||
errors: [error?.message || 'Unknown validation error'],
|
||||
errors: [
|
||||
error instanceof Error ? error.message : 'Unknown validation error',
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
IsBoolean,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import type { RawWorkflowDSL } from '../workflow-dsl.service';
|
||||
|
||||
export class CreateWorkflowDefinitionDto {
|
||||
@ApiProperty({ example: 'RFA', description: 'รหัสของ Workflow' })
|
||||
@@ -17,7 +18,7 @@ export class CreateWorkflowDefinitionDto {
|
||||
@ApiProperty({ description: 'นิยาม Workflow' })
|
||||
@IsObject()
|
||||
@IsNotEmpty()
|
||||
dsl!: any; // เพิ่ม !
|
||||
dsl!: RawWorkflowDSL; // เพิ่ม !
|
||||
|
||||
@ApiProperty({ description: 'เปิดใช้งานทันทีหรือไม่', default: true })
|
||||
@IsBoolean()
|
||||
|
||||
@@ -38,14 +38,14 @@ export class WorkflowDefinition {
|
||||
type: 'json',
|
||||
comment: 'Raw DSL ที่ User/Admin เขียน (เก็บไว้เพื่อดูหรือแก้ไข)',
|
||||
})
|
||||
dsl!: any; // ควรตรงกับ RawWorkflowDSL interface
|
||||
dsl!: Record<string, unknown>; // RawWorkflowDSL | WorkflowDsl
|
||||
|
||||
@Column({
|
||||
type: 'json',
|
||||
comment:
|
||||
'Compiled JSON Structure ที่ผ่านการ Validate และ Optimize สำหรับ Runtime Engine แล้ว',
|
||||
})
|
||||
compiled!: any; // ควรตรงกับ CompiledWorkflow interface
|
||||
compiled!: Record<string, unknown>; // CompiledWorkflow | WorkflowDsl
|
||||
|
||||
@Column({ default: true, comment: 'สถานะการใช้งาน (Soft Disable)' })
|
||||
is_active!: boolean;
|
||||
|
||||
@@ -33,7 +33,7 @@ export interface RawEvent {
|
||||
type: 'notify' | 'webhook' | 'assign' | 'auto_action';
|
||||
target?: string;
|
||||
template?: string;
|
||||
payload?: any;
|
||||
payload?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
@@ -147,7 +147,7 @@ export class WorkflowDslService {
|
||||
compiled: CompiledWorkflow,
|
||||
currentState: string,
|
||||
action: string,
|
||||
context: any = {}
|
||||
context: Record<string, unknown> = {}
|
||||
): { nextState: string; events: RawEvent[] } {
|
||||
const stateConfig = compiled.states[currentState];
|
||||
|
||||
@@ -197,11 +197,12 @@ export class WorkflowDslService {
|
||||
// Private Helpers
|
||||
// --------------------------------------------------------
|
||||
|
||||
private validateSchemaStructure(dsl: any) {
|
||||
private validateSchemaStructure(dsl: unknown) {
|
||||
if (!dsl || typeof dsl !== 'object') {
|
||||
throw new BadRequestException('DSL must be a JSON object.');
|
||||
}
|
||||
if (!dsl.workflow || !dsl.states || !Array.isArray(dsl.states)) {
|
||||
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).'
|
||||
);
|
||||
@@ -210,15 +211,15 @@ export class WorkflowDslService {
|
||||
|
||||
private checkRequirements(
|
||||
req: CompiledTransition['requirements'],
|
||||
context: any
|
||||
context: Record<string, unknown>
|
||||
) {
|
||||
// [FIX] Early return if no requirements defined
|
||||
if (!req) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userRoles: string[] = context.roles || [];
|
||||
const userId: string | number = context.userId;
|
||||
const userRoles: string[] = (context.roles as string[]) || [];
|
||||
const userId: string | number = context.userId as string | number;
|
||||
|
||||
// Check Roles (OR logic inside array) - with null-safety
|
||||
const requiredRoles = req.roles || [];
|
||||
@@ -242,7 +243,10 @@ export class WorkflowDslService {
|
||||
* NOTE: In production, use a safe parser like 'json-logic-js' or vm2
|
||||
* For this phase, we use a simple Function constructor with restricted scope.
|
||||
*/
|
||||
private evaluateCondition(expression: string, context: any): boolean {
|
||||
private evaluateCondition(
|
||||
expression: string,
|
||||
context: Record<string, unknown>
|
||||
): boolean {
|
||||
try {
|
||||
// Simple guard against malicious code (basic)
|
||||
if (expression.includes('process') || expression.includes('require')) {
|
||||
@@ -253,8 +257,10 @@ export class WorkflowDslService {
|
||||
// "context" is available inside the expression
|
||||
const func = new Function('context', `return ${expression};`);
|
||||
return !!func(context);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Condition Error: "${expression}" -> ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(
|
||||
`Condition Error: "${expression}" -> ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
return false; // Fail safe
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,11 @@ import {
|
||||
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';
|
||||
import {
|
||||
CompiledWorkflow,
|
||||
RawEvent,
|
||||
WorkflowDslService,
|
||||
} from './workflow-dsl.service';
|
||||
import { WorkflowEventService } from './workflow-event.service'; // [NEW] Import Event Service
|
||||
|
||||
// Legacy Interface (Backward Compatibility)
|
||||
@@ -51,7 +55,7 @@ export class WorkflowEngineService {
|
||||
private readonly historyRepo: Repository<WorkflowHistory>,
|
||||
private readonly dslService: WorkflowDslService,
|
||||
private readonly eventService: WorkflowEventService, // [NEW] Inject Service
|
||||
private readonly dataSource: DataSource, // ใช้สำหรับ Transaction
|
||||
private readonly dataSource: DataSource // ใช้สำหรับ Transaction
|
||||
) {}
|
||||
|
||||
// =================================================================
|
||||
@@ -62,7 +66,7 @@ export class WorkflowEngineService {
|
||||
* สร้างหรืออัปเดต Workflow Definition ใหม่ (Auto Versioning)
|
||||
*/
|
||||
async createDefinition(
|
||||
dto: CreateWorkflowDefinitionDto,
|
||||
dto: CreateWorkflowDefinitionDto
|
||||
): Promise<WorkflowDefinition> {
|
||||
// 1. Compile & Validate DSL
|
||||
const compiled = this.dslService.compile(dto.dsl);
|
||||
@@ -79,16 +83,16 @@ export class WorkflowEngineService {
|
||||
const entity = this.workflowDefRepo.create({
|
||||
workflow_code: dto.workflow_code,
|
||||
version: nextVersion,
|
||||
dsl: dto.dsl,
|
||||
compiled: compiled,
|
||||
dsl: dto.dsl as unknown as Record<string, unknown>,
|
||||
compiled: compiled as unknown as Record<string, unknown>,
|
||||
is_active: dto.is_active ?? true,
|
||||
});
|
||||
|
||||
const saved = await this.workflowDefRepo.save(entity);
|
||||
this.logger.log(
|
||||
`Created Workflow Definition: ${saved.workflow_code} v${saved.version}`,
|
||||
`Created Workflow Definition: ${(saved as WorkflowDefinition).workflow_code} v${(saved as WorkflowDefinition).version}`
|
||||
);
|
||||
return saved;
|
||||
return saved as WorkflowDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,22 +100,24 @@ export class WorkflowEngineService {
|
||||
*/
|
||||
async update(
|
||||
id: string,
|
||||
dto: UpdateWorkflowDefinitionDto,
|
||||
dto: UpdateWorkflowDefinitionDto
|
||||
): Promise<WorkflowDefinition> {
|
||||
const definition = await this.workflowDefRepo.findOne({ where: { id } });
|
||||
if (!definition) {
|
||||
throw new NotFoundException(
|
||||
`Workflow Definition with ID "${id}" not found`,
|
||||
`Workflow Definition with ID "${id}" not found`
|
||||
);
|
||||
}
|
||||
|
||||
if (dto.dsl) {
|
||||
try {
|
||||
const compiled = this.dslService.compile(dto.dsl);
|
||||
definition.dsl = dto.dsl;
|
||||
definition.compiled = compiled;
|
||||
} catch (error: any) {
|
||||
throw new BadRequestException(`Invalid DSL: ${error.message}`);
|
||||
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)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +136,7 @@ export class WorkflowEngineService {
|
||||
const latestDefinitions = await this.workflowDefRepo
|
||||
.createQueryBuilder('def')
|
||||
.where(
|
||||
'def.version = (SELECT MAX(sub.version) FROM workflow_definitions sub WHERE sub.workflow_code = def.workflow_code)',
|
||||
'def.version = (SELECT MAX(sub.version) FROM workflow_definitions sub WHERE sub.workflow_code = def.workflow_code)'
|
||||
)
|
||||
.getMany();
|
||||
|
||||
@@ -143,7 +149,9 @@ 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 with ID "${id}" not found`
|
||||
);
|
||||
}
|
||||
return definition;
|
||||
}
|
||||
@@ -153,7 +161,7 @@ export class WorkflowEngineService {
|
||||
*/
|
||||
async getAvailableActions(
|
||||
workflowCode: string,
|
||||
currentState: string,
|
||||
currentState: string
|
||||
): Promise<string[]> {
|
||||
const definition = await this.workflowDefRepo.findOne({
|
||||
where: { workflow_code: workflowCode, is_active: true },
|
||||
@@ -162,7 +170,8 @@ export class WorkflowEngineService {
|
||||
|
||||
if (!definition) return [];
|
||||
|
||||
const stateConfig = definition.compiled.states[currentState];
|
||||
const compiled = definition.compiled as unknown as CompiledWorkflow;
|
||||
const stateConfig = compiled.states[currentState];
|
||||
if (!stateConfig || !stateConfig.transitions) return [];
|
||||
|
||||
return Object.keys(stateConfig.transitions);
|
||||
@@ -179,7 +188,7 @@ export class WorkflowEngineService {
|
||||
workflowCode: string,
|
||||
entityType: string,
|
||||
entityId: string,
|
||||
initialContext: Record<string, any> = {},
|
||||
initialContext: Record<string, unknown> = {}
|
||||
): Promise<WorkflowInstance> {
|
||||
// 1. หา Definition ล่าสุด
|
||||
const definition = await this.workflowDefRepo.findOne({
|
||||
@@ -189,19 +198,19 @@ export class WorkflowEngineService {
|
||||
|
||||
if (!definition) {
|
||||
throw new NotFoundException(
|
||||
`Workflow "${workflowCode}" not found or inactive.`,
|
||||
`Workflow "${workflowCode}" not found or inactive.`
|
||||
);
|
||||
}
|
||||
|
||||
// 2. หา Initial State จาก Compiled Structure
|
||||
const compiled: CompiledWorkflow = definition.compiled;
|
||||
const compiled = definition.compiled as unknown as CompiledWorkflow;
|
||||
// [FIX] ใช้ initialState จาก Root Property โดยตรง (ตามที่ Optimize ใน DSL Service)
|
||||
// เพราะ CompiledState ใน states map ไม่มี property 'initial' แล้ว
|
||||
const initialState = compiled.initialState;
|
||||
|
||||
if (!initialState) {
|
||||
throw new BadRequestException(
|
||||
`Workflow "${workflowCode}" has no initial state defined.`,
|
||||
`Workflow "${workflowCode}" has no initial state defined.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -217,7 +226,7 @@ export class WorkflowEngineService {
|
||||
|
||||
const savedInstance = await this.instanceRepo.save(instance);
|
||||
this.logger.log(
|
||||
`Started Workflow Instance: ${workflowCode} for ${entityType}:${entityId}`,
|
||||
`Started Workflow Instance: ${workflowCode} for ${entityType}:${entityId}`
|
||||
);
|
||||
return savedInstance;
|
||||
}
|
||||
@@ -234,7 +243,7 @@ export class WorkflowEngineService {
|
||||
|
||||
if (!instance) {
|
||||
throw new NotFoundException(
|
||||
`Workflow Instance "${instanceId}" not found`,
|
||||
`Workflow Instance "${instanceId}" not found`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -249,14 +258,14 @@ export class WorkflowEngineService {
|
||||
action: string,
|
||||
userId: number,
|
||||
comment?: string,
|
||||
payload: Record<string, any> = {},
|
||||
payload: Record<string, any> = {}
|
||||
) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
let eventsToDispatch: any[] = [];
|
||||
let updatedContext: any = {};
|
||||
let eventsToDispatch: RawEvent[] = [];
|
||||
let updatedContext: Record<string, unknown> = {};
|
||||
|
||||
try {
|
||||
// 1. Lock Instance เพื่อป้องกัน Race Condition (Pessimistic Write Lock)
|
||||
@@ -268,18 +277,19 @@ export class WorkflowEngineService {
|
||||
|
||||
if (!instance) {
|
||||
throw new NotFoundException(
|
||||
`Workflow Instance "${instanceId}" not found.`,
|
||||
`Workflow Instance "${instanceId}" not found.`
|
||||
);
|
||||
}
|
||||
|
||||
if (instance.status !== WorkflowStatus.ACTIVE) {
|
||||
throw new BadRequestException(
|
||||
`Workflow is not active (Status: ${instance.status}).`,
|
||||
`Workflow is not active (Status: ${instance.status}).`
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Evaluate Logic ผ่าน DSL Service
|
||||
const compiled: CompiledWorkflow = instance.definition.compiled;
|
||||
const compiled = instance.definition
|
||||
.compiled as unknown as CompiledWorkflow;
|
||||
const context = { ...instance.context, userId, ...payload }; // Merge Context
|
||||
|
||||
// * DSL Service จะ throw error ถ้า action ไม่ถูกต้อง หรือสิทธิ์ไม่พอ
|
||||
@@ -287,7 +297,7 @@ export class WorkflowEngineService {
|
||||
compiled,
|
||||
instance.currentState,
|
||||
action,
|
||||
context,
|
||||
context
|
||||
);
|
||||
|
||||
const fromState = instance.currentState;
|
||||
@@ -326,7 +336,7 @@ export class WorkflowEngineService {
|
||||
updatedContext = context;
|
||||
|
||||
this.logger.log(
|
||||
`Transition: ${instanceId} [${fromState}] --${action}--> [${toState}] by User:${userId}`,
|
||||
`Transition: ${instanceId} [${fromState}] --${action}--> [${toState}] by User:${userId}`
|
||||
);
|
||||
|
||||
// [NEW] Dispatch Events (Async) ผ่าน WorkflowEventService
|
||||
@@ -334,7 +344,7 @@ export class WorkflowEngineService {
|
||||
this.eventService.dispatchEvents(
|
||||
instance.id,
|
||||
eventsToDispatch,
|
||||
updatedContext,
|
||||
updatedContext
|
||||
);
|
||||
}
|
||||
|
||||
@@ -347,7 +357,7 @@ export class WorkflowEngineService {
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Transition Failed for ${instanceId}: ${(err as Error).message}`,
|
||||
`Transition Failed for ${instanceId}: ${(err as Error).message}`
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -369,10 +379,10 @@ export class WorkflowEngineService {
|
||||
}
|
||||
|
||||
return this.dslService.evaluate(
|
||||
definition.compiled,
|
||||
definition.compiled as unknown as CompiledWorkflow,
|
||||
dto.current_state,
|
||||
dto.action,
|
||||
dto.context || {},
|
||||
dto.context || {}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -389,7 +399,7 @@ export class WorkflowEngineService {
|
||||
currentSequence: number,
|
||||
totalSteps: number,
|
||||
action: string,
|
||||
returnToSequence?: number,
|
||||
returnToSequence?: number
|
||||
): TransitionResult {
|
||||
switch (action) {
|
||||
case WorkflowAction.APPROVE:
|
||||
@@ -430,7 +440,7 @@ export class WorkflowEngineService {
|
||||
|
||||
default:
|
||||
this.logger.warn(
|
||||
`Unknown legacy action: ${action}, treating as next step.`,
|
||||
`Unknown legacy action: ${action}, treating as next step.`
|
||||
);
|
||||
if (currentSequence >= totalSteps) {
|
||||
return {
|
||||
|
||||
@@ -9,9 +9,9 @@ export interface WorkflowEventHandler {
|
||||
handleNotification(
|
||||
target: string,
|
||||
template: string,
|
||||
payload: any,
|
||||
payload: Record<string, unknown>
|
||||
): Promise<void>;
|
||||
handleWebhook(url: string, payload: any): Promise<void>;
|
||||
handleWebhook(url: string, payload: Record<string, unknown>): Promise<void>;
|
||||
handleAutoAction(instanceId: string, action: string): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -28,19 +28,17 @@ export class WorkflowEventService {
|
||||
async dispatchEvents(
|
||||
instanceId: string,
|
||||
events: RawEvent[],
|
||||
context: Record<string, any>,
|
||||
context: Record<string, any>
|
||||
) {
|
||||
if (!events || events.length === 0) return;
|
||||
|
||||
this.logger.log(
|
||||
`Dispatching ${events.length} events for Instance ${instanceId}`,
|
||||
`Dispatching ${events.length} events for Instance ${instanceId}`
|
||||
);
|
||||
|
||||
// ทำแบบ Async ไม่รอผล (Fire-and-forget) เพื่อไม่ให้กระทบ Response Time ของ User
|
||||
Promise.allSettled(
|
||||
events.map((event) =>
|
||||
this.processSingleEvent(instanceId, event, context),
|
||||
),
|
||||
events.map((event) => this.processSingleEvent(instanceId, event, context))
|
||||
).then((results) => {
|
||||
// Log errors if any
|
||||
results.forEach((res, idx) => {
|
||||
@@ -54,7 +52,7 @@ export class WorkflowEventService {
|
||||
private async processSingleEvent(
|
||||
instanceId: string,
|
||||
event: RawEvent,
|
||||
context: any,
|
||||
context: Record<string, unknown>
|
||||
) {
|
||||
try {
|
||||
switch (event.type) {
|
||||
@@ -79,18 +77,24 @@ export class WorkflowEventService {
|
||||
|
||||
// --- Handlers ---
|
||||
|
||||
private async handleNotify(event: RawEvent, context: any) {
|
||||
private async handleNotify(
|
||||
event: RawEvent,
|
||||
_context: Record<string, unknown>
|
||||
) {
|
||||
// Mockup: ในของจริงจะเรียก NotificationService.send()
|
||||
// const recipients = this.resolveRecipients(event.target, context);
|
||||
this.logger.log(
|
||||
`[EVENT] Notify target: "${event.target}" | Template: "${event.template}"`,
|
||||
`[EVENT] Notify target: "${event.target}" | Template: "${event.template}"`
|
||||
);
|
||||
}
|
||||
|
||||
private async handleWebhook(event: RawEvent, context: any) {
|
||||
private async handleWebhook(
|
||||
event: RawEvent,
|
||||
_context: Record<string, unknown>
|
||||
) {
|
||||
// Mockup: เรียก HttpService.post()
|
||||
this.logger.log(
|
||||
`[EVENT] Webhook to: "${event.target}" | Payload: ${JSON.stringify(event.payload)}`,
|
||||
`[EVENT] Webhook to: "${event.target}" | Payload: ${JSON.stringify(event.payload)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user