2
0
Files
bot/packages/bot-engine/blocks/logic/condition/executeCondition.ts
2024-03-29 08:06:16 +01:00

185 lines
6.5 KiB
TypeScript

import { isNotDefined, isDefined } from '@typebot.io/lib'
import { Comparison, Condition, Variable } from '@typebot.io/schemas'
import { findUniqueVariableValue } from '@typebot.io/variables/findUniqueVariableValue'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import {
LogicalOperator,
ComparisonOperators,
defaultConditionItemContent,
} from '@typebot.io/schemas/features/blocks/logic/condition/constants'
export const executeCondition =
(variables: Variable[]) =>
(condition: Condition): boolean => {
if (!condition.comparisons) return false
return (condition.logicalOperator ??
defaultConditionItemContent.logicalOperator) === LogicalOperator.AND
? condition.comparisons.every(executeComparison(variables))
: condition.comparisons.some(executeComparison(variables))
}
const executeComparison =
(variables: Variable[]) =>
(comparison: Comparison): boolean => {
if (!comparison?.variableId) return false
const inputValue =
variables.find((v) => v.id === comparison.variableId)?.value ?? null
const value =
comparison.value === 'undefined' || comparison.value === 'null'
? null
: findUniqueVariableValue(variables)(comparison.value) ??
parseVariables(variables)(comparison.value)
if (isNotDefined(comparison.comparisonOperator)) return false
switch (comparison.comparisonOperator) {
case ComparisonOperators.CONTAINS: {
const contains = (a: string | null, b: string | null) => {
if (b === '' || !b || !a) return false
return a
.toLowerCase()
.trim()
.normalize()
.includes(b.toLowerCase().trim().normalize())
}
return compare(contains, inputValue, value, 'some')
}
case ComparisonOperators.NOT_CONTAINS: {
const notContains = (a: string | null, b: string | null) => {
if (b === '' || !b || !a) return true
return !a
.toLowerCase()
.trim()
.normalize()
.includes(b.toLowerCase().trim().normalize())
}
return compare(notContains, inputValue, value)
}
case ComparisonOperators.EQUAL: {
return compare(
(a, b) => {
if (typeof a === 'string' && typeof b === 'string')
return a.normalize() === b.normalize()
return a === b
},
inputValue,
value
)
}
case ComparisonOperators.NOT_EQUAL: {
return compare(
(a, b) => {
if (typeof a === 'string' && typeof b === 'string')
return a.normalize() !== b.normalize()
return a !== b
},
inputValue,
value
)
}
case ComparisonOperators.GREATER: {
if (isNotDefined(inputValue) || isNotDefined(value)) return false
if (typeof inputValue === 'string') {
if (typeof value === 'string')
return parseDateOrNumber(inputValue) > parseDateOrNumber(value)
return Number(inputValue) > value.length
}
if (typeof value === 'string') return inputValue.length > Number(value)
return inputValue.length > value.length
}
case ComparisonOperators.LESS: {
if (isNotDefined(inputValue) || isNotDefined(value)) return false
if (typeof inputValue === 'string') {
if (typeof value === 'string')
return parseDateOrNumber(inputValue) < parseDateOrNumber(value)
return Number(inputValue) < value.length
}
if (typeof value === 'string') return inputValue.length < Number(value)
return inputValue.length < value.length
}
case ComparisonOperators.IS_SET: {
return isDefined(inputValue) && inputValue.length > 0
}
case ComparisonOperators.IS_EMPTY: {
return isNotDefined(inputValue) || inputValue.length === 0
}
case ComparisonOperators.STARTS_WITH: {
const startsWith = (a: string | null, b: string | null) => {
if (b === '' || !b || !a) return false
return a
.toLowerCase()
.trim()
.normalize()
.startsWith(b.toLowerCase().trim().normalize())
}
return compare(startsWith, inputValue, value)
}
case ComparisonOperators.ENDS_WITH: {
const endsWith = (a: string | null, b: string | null) => {
if (b === '' || !b || !a) return false
return a
.toLowerCase()
.trim()
.normalize()
.endsWith(b.toLowerCase().trim().normalize())
}
return compare(endsWith, inputValue, value)
}
case ComparisonOperators.MATCHES_REGEX: {
const matchesRegex = (a: string | null, b: string | null) => {
if (b === '' || !b || !a) return false
const regex = preprocessRegex(b)
if (!regex) return false
return new RegExp(regex.pattern, regex.flags).test(a)
}
return compare(matchesRegex, inputValue, value, 'some')
}
case ComparisonOperators.NOT_MATCH_REGEX: {
const matchesRegex = (a: string | null, b: string | null) => {
if (b === '' || !b || !a) return false
const regex = preprocessRegex(b)
if (!regex) return true
return !new RegExp(regex.pattern, regex.flags).test(a)
}
return compare(matchesRegex, inputValue, value)
}
}
}
const compare = (
compareStrings: (a: string | null, b: string | null) => boolean,
a: Exclude<Variable['value'], undefined>,
b: Exclude<Variable['value'], undefined>,
type: 'every' | 'some' = 'every'
): boolean => {
if (!a || typeof a === 'string') {
if (!b || typeof b === 'string') return compareStrings(a, b)
return type === 'every'
? b.every((b) => compareStrings(a, b))
: b.some((b) => compareStrings(a, b))
}
if (!b || typeof b === 'string') {
return type === 'every'
? a.every((a) => compareStrings(a, b))
: a.some((a) => compareStrings(a, b))
}
if (type === 'every')
return a.every((a) => b.every((b) => compareStrings(a, b)))
return a.some((a) => b.some((b) => compareStrings(a, b)))
}
const parseDateOrNumber = (value: string): number => {
const parsed = Number(value)
if (isNaN(parsed)) {
const time = Date.parse(value)
return time
}
return parsed
}
const preprocessRegex = (regex: string) => {
const match = regex.match(/^\/([^\/]+)\/([gimuy]*)$/)
if (!match) return null
return { pattern: match[1], flags: match[2] }
}