Skip to content

Commit

Permalink
Introduced new block logic (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
girl-loves-coding authored Oct 8, 2024
1 parent 52d7d75 commit 9dd5786
Show file tree
Hide file tree
Showing 61 changed files with 9,930 additions and 7,030 deletions.
4 changes: 3 additions & 1 deletion packages/blocks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
"test": "jest"
},
"dependencies": {
"@data-river/shared": "workspace:*"
"@data-river/shared": "workspace:*",
"lodash": "^4.17.21"
},
"devDependencies": {
"@types/jest": "^29.5.13",
"@types/lodash": "^4.17.10",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"typescript": "^5.6.2"
Expand Down
39 changes: 22 additions & 17 deletions packages/blocks/src/block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,26 @@ export abstract class Block implements IBlock {
const missingFields: string[] = [];
const invalidFields: string[] = [];

for (const [key, value] of Object.entries(inputs)) {
for (const [key, config] of Object.entries(this.inputConfigs)) {
if (
key in this.inputConfigs &&
value !== undefined &&
value !== null &&
value !== ""
config.required &&
(!(key in inputs) ||
inputs[key] === undefined ||
inputs[key] === null ||
inputs[key] === "")
) {
cleanedInputs[key] = value;
}
}

for (const [key, config] of Object.entries(this.inputConfigs)) {
if (config.required && !(key in cleanedInputs)) {
missingFields.push(key);
} else if (
key in cleanedInputs &&
typeof cleanedInputs[key] !== config.type
key in inputs &&
inputs[key] !== undefined &&
inputs[key] !== null &&
inputs[key] !== ""
) {
invalidFields.push(key);
if (typeof inputs[key] !== config.type) {
invalidFields.push(key);
} else {
cleanedInputs[key] = inputs[key];
}
}
}

Expand Down Expand Up @@ -104,15 +105,19 @@ export abstract class Block implements IBlock {

async safeExecute(
inputs: Record<string, unknown>,
config: Record<string, unknown>,
): Promise<Record<string, unknown>> {
this.logger.group(`Block Id:${this.id} Type:${this.type}`);

// Merge inputs with config
const mergedInputs = { ...config, ...inputs };

this.logger.debug("safeExecute", {
inputs,
mergedInputs,
inputConfigs: this.inputConfigs,
});

const inputValidation = this.validateInputs(inputs);
const inputValidation = this.validateInputs(mergedInputs);
if (!inputValidation.valid) {
throw new BlockValidationError(
`Invalid inputs for block ${this.id}`,
Expand All @@ -122,7 +127,7 @@ export abstract class Block implements IBlock {
);
}

const outputs = await this.execute(inputs);
const outputs = await this.execute(mergedInputs);

const outputValidation = this.validateOutputs(outputs);
if (!outputValidation.valid) {
Expand Down
2 changes: 2 additions & 0 deletions packages/blocks/src/blockFactory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { StartBlock } from "../startBlock";
import { EndBlock } from "../endBlock";
import { InputBlock } from "../inputBlock";
import { OutputBlock } from "../outputBlock";
import { LogicBlock } from "..";

type BlockConstructor = new (config: IBlockConfig, logger: ILogger) => IBlock;

Check warning on line 10 in packages/blocks/src/blockFactory/index.ts

View workflow job for this annotation

GitHub Actions / build

'config' is defined but never used

Check warning on line 10 in packages/blocks/src/blockFactory/index.ts

View workflow job for this annotation

GitHub Actions / build

'logger' is defined but never used

Expand All @@ -13,6 +14,7 @@ const blockRegistry: Record<string, BlockConstructor> = {
"blocks/[email protected]": EndBlock,
"blocks/[email protected]": InputBlock,
"blocks/[email protected]": OutputBlock,
"blocks/[email protected]": LogicBlock,
};

export function createBlock(config: IBlockConfig, logger: ILogger): IBlock {
Expand Down
71 changes: 71 additions & 0 deletions packages/blocks/src/logicBlock/conditionEvaluator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ICondition } from "@data-river/shared/interfaces/ICondition";
import { ILogger } from "@data-river/shared/interfaces/ILogger";
import _ from "lodash";
import { ValueResolver } from "@/utils/valueResolver";
import { DateHandler } from "./dateHandler";
import { Comparator } from "@/utils/comparator";

export class ConditionEvaluator {
constructor(private logger: ILogger) {}

Check warning on line 9 in packages/blocks/src/logicBlock/conditionEvaluator.ts

View workflow job for this annotation

GitHub Actions / build

'logger' is defined but never used

evaluateCondition(
condition: ICondition,
inputs: Record<string, unknown>,
): boolean {
const leftValue = ValueResolver.resolveValue(condition.left, inputs);
const rightValue = ValueResolver.resolveValue(condition.right, inputs);

const leftDate = DateHandler.parseDateIfValid(leftValue);
const rightDate = DateHandler.parseDateIfValid(rightValue);

if (leftDate && rightDate) {
return DateHandler.compareDates(leftDate, rightDate, condition.operator);
}

switch (condition.operator) {
case "==":
return Comparator.looseEquals(leftValue, rightValue);
case "===":
return Comparator.strictEquals(leftValue, rightValue);
case "!=":
return !Comparator.looseEquals(leftValue, rightValue);
case "!==":
return !Comparator.strictEquals(leftValue, rightValue);
case ">":
return Comparator.compare(leftValue, rightValue) > 0;
case ">=":
return Comparator.compare(leftValue, rightValue) >= 0;
case "<":
return Comparator.compare(leftValue, rightValue) < 0;
case "<=":
return Comparator.compare(leftValue, rightValue) <= 0;
case "contains":
return (
_.isString(leftValue) &&
String(leftValue).includes(String(rightValue))
);
case "not_contains":
return (
_.isString(leftValue) &&
!String(leftValue).includes(String(rightValue))
);
case "starts_with":
return (
_.isString(leftValue) &&
String(leftValue).startsWith(String(rightValue))
);
case "ends_with":
return (
_.isString(leftValue) &&
String(leftValue).endsWith(String(rightValue))
);
case "is_empty":
return Comparator.isEmpty(leftValue);
case "is_not_empty":
return !Comparator.isEmpty(leftValue);
default:
this.logger.warn(`Unknown operator: ${condition.operator}`);
return false;
}
}
}
35 changes: 35 additions & 0 deletions packages/blocks/src/logicBlock/dateHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import _ from "lodash";

export class DateHandler {
static parseDateIfValid(value: unknown): Date | null {
if (_.isDate(value)) {
return value;
}
if (_.isString(value) || _.isNumber(value)) {
const timestamp = _.toNumber(new Date(value));
return isNaN(timestamp) ? null : new Date(timestamp);
}
return null;
}

static compareDates(a: Date, b: Date, operator: string): boolean {
const aTime = a.getTime();
const bTime = b.getTime();
switch (operator) {
case ">":
return aTime > bTime;
case ">=":
return aTime >= bTime;
case "<":
return aTime < bTime;
case "<=":
return aTime <= bTime;
case "==":
return aTime === bTime;
case "!=":
return aTime !== bTime;
default:
return false;
}
}
}
64 changes: 49 additions & 15 deletions packages/blocks/src/logicBlock/index.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,63 @@
import { IBlockConfig } from "@data-river/shared/interfaces";
import { ILogger } from "@data-river/shared/interfaces/ILogger";
import { Block } from "../block";
import { Block } from "@/block";
import { CustomEvaluatorStrategy } from "@/utils/customEvaluatorStrategy";
import { CustomEvaluatorFunction } from "@data-river/shared/types";
import { ICondition } from "@data-river/shared/interfaces";
import { ConditionEvaluator } from "./conditionEvaluator";

export interface LogicBlockConfig {
conditions: ICondition[];
logicOperator: "AND" | "OR";
customEvaluatorCode?: string;
}

export class LogicBlock extends Block {
condition: string;
conditions: ICondition[];
logicOperator: "AND" | "OR";
customEvaluator?: CustomEvaluatorFunction;
private conditionEvaluator: ConditionEvaluator;

constructor(config: IBlockConfig & { condition?: string }, logger: ILogger) {
constructor(config: IBlockConfig, logger: ILogger) {
super(config, logger);
this.condition = config.condition || "";
const magicConfig = config.config as unknown as LogicBlockConfig;
this.conditions = magicConfig.conditions || [];
this.logicOperator = magicConfig.logicOperator || "AND";
this.conditionEvaluator = new ConditionEvaluator(logger);
if (magicConfig.customEvaluatorCode) {
this.setCustomEvaluator(magicConfig.customEvaluatorCode);
}
}

setCustomEvaluator(code: string, timeout?: number) {
this.customEvaluator = CustomEvaluatorStrategy.createEvaluator(
code,
timeout,
);
}

async execute(
inputs: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const conditionResult = evaluateCondition(this.condition, inputs);
this.logger.debug(`Condition result: ${conditionResult}`);
return { conditionResult };
const conditionResults = this.conditions.map((condition) =>
this.evaluateCondition(condition, inputs),
);

const result =
this.logicOperator === "AND"
? conditionResults.every((result) => result)
: conditionResults.some((result) => result);

this.logger.debug(`Condition result: ${result}`);
return { result };
}
}

function evaluateCondition(
condition: string,
inputs: Record<string, unknown>,
): boolean {
// Implement condition evaluation logic
// For POC, randomly return true or false
return Math.random() >= 0.5;
private evaluateCondition(
condition: ICondition,
inputs: Record<string, unknown>,
): boolean {
return this.customEvaluator
? this.customEvaluator(condition, inputs)
: this.conditionEvaluator.evaluateCondition(condition, inputs);
}
}
28 changes: 28 additions & 0 deletions packages/blocks/src/utils/comparator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import _ from "lodash";

export class Comparator {
static looseEquals(a: unknown, b: unknown): boolean {
return _.eq(a, b);
}

static strictEquals(a: unknown, b: unknown): boolean {
return _.isEqual(a, b);
}

static compare(a: unknown, b: unknown): number {
if (_.isNumber(a) && _.isNumber(b)) {
return a - b;
}
if (_.isBoolean(a) && _.isBoolean(b)) {
return a === b ? 0 : a ? 1 : -1;
}
if (_.isString(a) && _.isString(b)) {
return a === b ? 0 : a > b ? 1 : -1;
}
return String(a) > String(b) ? 1 : -1;
}

static isEmpty(value: unknown): boolean {
return _.isEmpty(value);
}
}
71 changes: 71 additions & 0 deletions packages/blocks/src/utils/conditionEvaluator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ICondition } from "@data-river/shared/interfaces";
import { ILogger } from "@data-river/shared/interfaces/ILogger";
import _ from "lodash";
import { ValueResolver } from "./valueResolver";
import { DateHandler } from "../logicBlock/dateHandler";
import { Comparator } from "./comparator";

export class ConditionEvaluator {
constructor(private logger: ILogger) {}

Check warning on line 9 in packages/blocks/src/utils/conditionEvaluator.ts

View workflow job for this annotation

GitHub Actions / build

'logger' is defined but never used

evaluateCondition(
condition: ICondition,
inputs: Record<string, unknown>,
): boolean {
const leftValue = ValueResolver.resolveValue(condition.left, inputs);
const rightValue = ValueResolver.resolveValue(condition.right, inputs);

const leftDate = DateHandler.parseDateIfValid(leftValue);
const rightDate = DateHandler.parseDateIfValid(rightValue);

if (leftDate && rightDate) {
return DateHandler.compareDates(leftDate, rightDate, condition.operator);
}

switch (condition.operator) {
case "==":
return Comparator.looseEquals(leftValue, rightValue);
case "===":
return Comparator.strictEquals(leftValue, rightValue);
case "!=":
return !Comparator.looseEquals(leftValue, rightValue);
case "!==":
return !Comparator.strictEquals(leftValue, rightValue);
case ">":
return Comparator.compare(leftValue, rightValue) > 0;
case ">=":
return Comparator.compare(leftValue, rightValue) >= 0;
case "<":
return Comparator.compare(leftValue, rightValue) < 0;
case "<=":
return Comparator.compare(leftValue, rightValue) <= 0;
case "contains":
return (
_.isString(leftValue) &&
String(leftValue).includes(String(rightValue))
);
case "not_contains":
return (
_.isString(leftValue) &&
!String(leftValue).includes(String(rightValue))
);
case "starts_with":
return (
_.isString(leftValue) &&
String(leftValue).startsWith(String(rightValue))
);
case "ends_with":
return (
_.isString(leftValue) &&
String(leftValue).endsWith(String(rightValue))
);
case "is_empty":
return Comparator.isEmpty(leftValue);
case "is_not_empty":
return !Comparator.isEmpty(leftValue);
default:
this.logger.warn(`Unknown operator: ${condition.operator}`);
return false;
}
}
}
Loading

0 comments on commit 9dd5786

Please sign in to comment.