Fix workflow condition evaluation to support comparison operators

- Implemented proper parsing for conditions like '$.trigger.doc.content == "value"'
- Added support for comparison operators: ==, !=, >, <, >=, <=
- Fixed JSONPath condition evaluation that was treating entire expressions as JSONPath queries
- Added support for string literals, numbers, booleans in condition values
- Conditions now correctly resolve JSONPath expressions and perform comparisons

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-01 19:42:21 +02:00
parent f3f18d5b4c
commit 4adc5cbdaa
2 changed files with 185 additions and 40 deletions

View File

@@ -103,7 +103,8 @@ const buildConfigWithMemoryDB = async () => {
plugins: [ plugins: [
workflowsPlugin<CollectionSlug>({ workflowsPlugin<CollectionSlug>({
collectionTriggers: { collectionTriggers: {
posts: true posts: true,
media: true
}, },
steps: [ steps: [
HttpRequestStepTask, HttpRequestStepTask,

View File

@@ -480,7 +480,7 @@ export class WorkflowExecutor {
} }
/** /**
* Evaluate a condition using JSONPath * Evaluate a condition using JSONPath and comparison operators
*/ */
public evaluateCondition(condition: string, context: ExecutionContext): boolean { public evaluateCondition(condition: string, context: ExecutionContext): boolean {
this.logger.debug({ this.logger.debug({
@@ -492,34 +492,94 @@ export class WorkflowExecutor {
}, 'Starting condition evaluation') }, 'Starting condition evaluation')
try { try {
const result = JSONPath({ // Check if this is a comparison expression
json: context, const comparisonMatch = condition.match(/^(.+?)\s*(==|!=|>|<|>=|<=)\s*(.+)$/)
path: condition,
wrap: false if (comparisonMatch) {
}) const [, leftExpr, operator, rightExpr] = comparisonMatch
this.logger.debug({ // Evaluate left side (should be JSONPath)
condition, const leftValue = this.resolveJSONPathValue(leftExpr.trim(), context)
result,
resultType: Array.isArray(result) ? 'array' : typeof result, // Parse right side (could be string, number, boolean, or JSONPath)
resultLength: Array.isArray(result) ? result.length : undefined const rightValue = this.parseConditionValue(rightExpr.trim(), context)
}, 'JSONPath evaluation result')
this.logger.debug({
// Handle different result types condition,
let finalResult: boolean leftExpr: leftExpr.trim(),
if (Array.isArray(result)) { leftValue,
finalResult = result.length > 0 && Boolean(result[0]) operator,
rightExpr: rightExpr.trim(),
rightValue,
leftType: typeof leftValue,
rightType: typeof rightValue
}, 'Evaluating comparison condition')
// Perform comparison
let result: boolean
switch (operator) {
case '==':
result = leftValue === rightValue
break
case '!=':
result = leftValue !== rightValue
break
case '>':
result = Number(leftValue) > Number(rightValue)
break
case '<':
result = Number(leftValue) < Number(rightValue)
break
case '>=':
result = Number(leftValue) >= Number(rightValue)
break
case '<=':
result = Number(leftValue) <= Number(rightValue)
break
default:
throw new Error(`Unknown comparison operator: ${operator}`)
}
this.logger.debug({
condition,
result,
leftValue,
rightValue,
operator
}, 'Comparison condition evaluation completed')
return result
} else { } else {
finalResult = Boolean(result) // Treat as simple JSONPath boolean evaluation
const result = JSONPath({
json: context,
path: condition,
wrap: false
})
this.logger.debug({
condition,
result,
resultType: Array.isArray(result) ? 'array' : typeof result,
resultLength: Array.isArray(result) ? result.length : undefined
}, 'JSONPath boolean evaluation result')
// Handle different result types
let finalResult: boolean
if (Array.isArray(result)) {
finalResult = result.length > 0 && Boolean(result[0])
} else {
finalResult = Boolean(result)
}
this.logger.debug({
condition,
finalResult,
originalResult: result
}, 'Boolean condition evaluation completed')
return finalResult
} }
this.logger.debug({
condition,
finalResult,
originalResult: result
}, 'Condition evaluation completed')
return finalResult
} catch (error) { } catch (error) {
this.logger.warn({ this.logger.warn({
condition, condition,
@@ -531,6 +591,49 @@ export class WorkflowExecutor {
return false return false
} }
} }
/**
* Resolve a JSONPath value from the context
*/
private resolveJSONPathValue(expr: string, context: ExecutionContext): any {
if (expr.startsWith('$')) {
const result = JSONPath({
json: context,
path: expr,
wrap: false
})
// Return first result if array, otherwise the result itself
return Array.isArray(result) && result.length > 0 ? result[0] : result
}
return expr
}
/**
* Parse a condition value (string literal, number, boolean, or JSONPath)
*/
private parseConditionValue(expr: string, context: ExecutionContext): any {
// Handle string literals
if ((expr.startsWith('"') && expr.endsWith('"')) || (expr.startsWith("'") && expr.endsWith("'"))) {
return expr.slice(1, -1) // Remove quotes
}
// Handle boolean literals
if (expr === 'true') return true
if (expr === 'false') return false
// Handle number literals
if (/^-?\d+(\.\d+)?$/.test(expr)) {
return Number(expr)
}
// Handle JSONPath expressions
if (expr.startsWith('$')) {
return this.resolveJSONPathValue(expr, context)
}
// Return as string if nothing else matches
return expr
}
/** /**
* Execute a workflow with the given context * Execute a workflow with the given context
@@ -555,19 +658,48 @@ export class WorkflowExecutor {
} }
}) })
this.logger.info({
workflowId: workflow.id,
workflowName: workflow.name,
contextSummary: {
triggerType: context.trigger.type,
triggerCollection: context.trigger.collection,
triggerOperation: context.trigger.operation,
hasDoc: !!context.trigger.doc,
userEmail: context.trigger.req?.user?.email
}
}, 'About to create workflow run record')
// Create a workflow run record // Create a workflow run record
const workflowRun = await this.payload.create({ let workflowRun;
collection: 'workflow-runs', try {
data: { workflowRun = await this.payload.create({
context: serializeContext(), collection: 'workflow-runs',
startedAt: new Date().toISOString(), data: {
status: 'running', context: serializeContext(),
triggeredBy: context.trigger.req?.user?.email || 'system', startedAt: new Date().toISOString(),
workflow: workflow.id, status: 'running',
workflowVersion: 1 // Default version since generated type doesn't have _version field triggeredBy: context.trigger.req?.user?.email || 'system',
}, workflow: workflow.id,
req workflowVersion: 1 // Default version since generated type doesn't have _version field
}) },
req
})
this.logger.info({
workflowRunId: workflowRun.id,
workflowId: workflow.id,
workflowName: workflow.name
}, 'Workflow run record created successfully')
} catch (error) {
this.logger.error({
error: error instanceof Error ? error.message : 'Unknown error',
errorStack: error instanceof Error ? error.stack : undefined,
workflowId: workflow.id,
workflowName: workflow.name
}, 'Failed to create workflow run record')
throw error
}
try { try {
// Resolve execution order based on dependencies // Resolve execution order based on dependencies
@@ -711,6 +843,18 @@ export class WorkflowExecutor {
}, 'Matching triggers found') }, 'Matching triggers found')
for (const trigger of matchingTriggers) { for (const trigger of matchingTriggers) {
this.logger.info({
workflowId: workflow.id,
workflowName: workflow.name,
triggerDetails: {
type: trigger.type,
collection: trigger.collection,
collectionSlug: trigger.collectionSlug,
operation: trigger.operation,
hasCondition: !!trigger.condition
}
}, 'Processing matching trigger - about to execute workflow')
// Create execution context for condition evaluation // Create execution context for condition evaluation
const context: ExecutionContext = { const context: ExecutionContext = {
steps: {}, steps: {},