فهرست منبع

feat(runtime): support map & datetime type (#815)

Louis Young 4 ماه پیش
والد
کامیت
b29ad158ec

+ 2 - 0
packages/materials/form-materials/src/components/condition-row/constants.ts

@@ -67,6 +67,8 @@ export const defaultRules: IRules = {
     [Op.GTE]: 'date-time',
     [Op.LT]: 'date-time',
     [Op.LTE]: 'date-time',
+    [Op.IS_EMPTY]: null,
+    [Op.IS_NOT_EMPTY]: null,
   },
 };
 

+ 14 - 0
packages/runtime/interface/src/node/condition/constant.ts

@@ -4,18 +4,32 @@
  */
 
 export enum ConditionOperator {
+  /** Equal */
   EQ = 'eq',
+  /** Not Equal */
   NEQ = 'neq',
+  /** Greater Than */
   GT = 'gt',
+  /** Greater Than or Equal */
   GTE = 'gte',
+  /** Less Than */
   LT = 'lt',
+  /** Less Than or Equal */
   LTE = 'lte',
+  /** In */
   IN = 'in',
+  /** Not In */
   NIN = 'nin',
+  /** Contains */
   CONTAINS = 'contains',
+  /** Not Contains */
   NOT_CONTAINS = 'not_contains',
+  /** Is Empty */
   IS_EMPTY = 'is_empty',
+  /** Is Not Empty */
   IS_NOT_EMPTY = 'is_not_empty',
+  /** Is True */
   IS_TRUE = 'is_true',
+  /** Is False */
   IS_FALSE = 'is_false',
 }

+ 2 - 0
packages/runtime/interface/src/schema/constant.ts

@@ -15,5 +15,7 @@ export enum WorkflowVariableType {
   Boolean = 'boolean',
   Object = 'object',
   Array = 'array',
+  Map = 'map',
+  DateTime = 'date-time',
   Null = 'null',
 }

+ 36 - 0
packages/runtime/js-core/src/infrastructure/utils/runtime-type.test.ts

@@ -44,6 +44,42 @@ describe('WorkflowRuntimeType', () => {
       });
     });
 
+    describe('datetime values', () => {
+      it('should return DateTime for valid ISO 8601 string with milliseconds and Z', () => {
+        const result = WorkflowRuntimeType.getWorkflowType('2025-09-11T12:05:49.000Z');
+        expect(result).toBe(WorkflowVariableType.DateTime);
+      });
+
+      it('should return DateTime for valid ISO 8601 string without milliseconds', () => {
+        const result = WorkflowRuntimeType.getWorkflowType('2025-09-11T12:05:49Z');
+        expect(result).toBe(WorkflowVariableType.DateTime);
+      });
+
+      it('should return DateTime for valid ISO 8601 string without Z suffix', () => {
+        const result = WorkflowRuntimeType.getWorkflowType('2025-09-11T12:05:49.000');
+        expect(result).toBe(WorkflowVariableType.DateTime);
+      });
+
+      it('should return String for invalid datetime strings', () => {
+        const invalidDateTimes = [
+          '2025-13-11T12:05:49.000Z', // Invalid month
+          '2025-09-32T12:05:49.000Z', // Invalid day
+          '2025-09-11T25:05:49.000Z', // Invalid hour
+          '2025-09-11T12:65:49.000Z', // Invalid minute
+          '2025-09-11T12:05:65.000Z', // Invalid second
+          '2025-09-11 12:05:49.000Z', // Missing T separator
+          'not-a-date', // Completely invalid
+          '2025-09-11', // Date only
+          '12:05:49', // Time only
+        ];
+
+        invalidDateTimes.forEach((invalidDateTime) => {
+          const result = WorkflowRuntimeType.getWorkflowType(invalidDateTime);
+          expect(result).toBe(WorkflowVariableType.String);
+        });
+      });
+    });
+
     describe('boolean values', () => {
       it('should return Boolean for true', () => {
         const result = WorkflowRuntimeType.getWorkflowType(true);

+ 14 - 5
packages/runtime/js-core/src/infrastructure/utils/runtime-type.ts

@@ -14,6 +14,15 @@ export namespace WorkflowRuntimeType {
 
     // 处理基本类型
     if (typeof value === 'string') {
+      // Check if string is a valid ISO 8601 datetime format
+      const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/;
+      if (iso8601Regex.test(value)) {
+        const date = new Date(value);
+        // Validate that the date is actually valid
+        if (!isNaN(date.getTime())) {
+          return WorkflowVariableType.DateTime;
+        }
+      }
       return WorkflowVariableType.String;
     }
 
@@ -50,17 +59,17 @@ export namespace WorkflowRuntimeType {
   };
 
   export const isTypeEqual = (
-    leftType: WorkflowVariableType,
-    rightType: WorkflowVariableType
+    typeA: WorkflowVariableType,
+    typeB: WorkflowVariableType
   ): boolean => {
     // 处理 Number 和 Integer 等价的情况
     if (
-      (leftType === WorkflowVariableType.Number && rightType === WorkflowVariableType.Integer) ||
-      (leftType === WorkflowVariableType.Integer && rightType === WorkflowVariableType.Number)
+      (typeA === WorkflowVariableType.Number && typeB === WorkflowVariableType.Integer) ||
+      (typeA === WorkflowVariableType.Integer && typeB === WorkflowVariableType.Number)
     ) {
       return true;
     }
-    return leftType === rightType;
+    return typeA === typeB;
   };
 
   export const getArrayItemsType = (types: WorkflowVariableType[]): WorkflowVariableType => {

+ 56 - 0
packages/runtime/js-core/src/nodes/condition/handlers/datetime.ts

@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { isNil } from 'lodash-es';
+import { ConditionOperator } from '@flowgram.ai/runtime-interface';
+
+import { ConditionHandler } from '../type';
+
+// Convert ISO string to Date object for comparison
+const parseDateTime = (value: string | Date): Date => {
+  if (value instanceof Date) {
+    return value;
+  }
+  return new Date(value);
+};
+
+export const conditionDateTimeHandler: ConditionHandler = (condition) => {
+  const { operator } = condition;
+  const leftValue = condition.leftValue as string;
+
+  // Handle empty checks first
+  if (operator === ConditionOperator.IS_EMPTY) {
+    return isNil(leftValue);
+  }
+  if (operator === ConditionOperator.IS_NOT_EMPTY) {
+    return !isNil(leftValue);
+  }
+
+  // For comparison operations, parse both datetime values
+  const leftTime = parseDateTime(leftValue).getTime();
+  const rightValue = condition.rightValue as string;
+  const rightTime = parseDateTime(rightValue).getTime();
+
+  if (operator === ConditionOperator.EQ) {
+    return leftTime === rightTime;
+  }
+  if (operator === ConditionOperator.NEQ) {
+    return leftTime !== rightTime;
+  }
+  if (operator === ConditionOperator.GT) {
+    return leftTime > rightTime;
+  }
+  if (operator === ConditionOperator.GTE) {
+    return leftTime >= rightTime;
+  }
+  if (operator === ConditionOperator.LT) {
+    return leftTime < rightTime;
+  }
+  if (operator === ConditionOperator.LTE) {
+    return leftTime <= rightTime;
+  }
+
+  return false;
+};

+ 4 - 0
packages/runtime/js-core/src/nodes/condition/handlers/index.ts

@@ -10,6 +10,8 @@ import { conditionStringHandler } from './string';
 import { conditionObjectHandler } from './object';
 import { conditionNumberHandler } from './number';
 import { conditionNullHandler } from './null';
+import { conditionMapHandler } from './map';
+import { conditionDateTimeHandler } from './datetime';
 import { conditionBooleanHandler } from './boolean';
 import { conditionArrayHandler } from './array';
 
@@ -19,6 +21,8 @@ export const conditionHandlers: ConditionHandlers = {
   [WorkflowVariableType.Integer]: conditionNumberHandler,
   [WorkflowVariableType.Boolean]: conditionBooleanHandler,
   [WorkflowVariableType.Object]: conditionObjectHandler,
+  [WorkflowVariableType.Map]: conditionMapHandler,
   [WorkflowVariableType.Array]: conditionArrayHandler,
+  [WorkflowVariableType.DateTime]: conditionDateTimeHandler,
   [WorkflowVariableType.Null]: conditionNullHandler,
 };

+ 22 - 0
packages/runtime/js-core/src/nodes/condition/handlers/map.ts

@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { isNil } from 'lodash-es';
+import { ConditionOperator } from '@flowgram.ai/runtime-interface';
+
+import { ConditionHandler } from '../type';
+
+export const conditionMapHandler: ConditionHandler = (condition) => {
+  const { operator } = condition;
+  const leftValue = condition.leftValue as object;
+  // Switch case share scope, so we need to use if else here
+  if (operator === ConditionOperator.IS_EMPTY) {
+    return isNil(leftValue);
+  }
+  if (operator === ConditionOperator.IS_NOT_EMPTY) {
+    return !isNil(leftValue);
+  }
+  return false;
+};

+ 14 - 0
packages/runtime/js-core/src/nodes/condition/rules.ts

@@ -56,6 +56,20 @@ export const conditionRules: ConditionRules = {
     [ConditionOperator.IS_EMPTY]: WorkflowVariableType.Null,
     [ConditionOperator.IS_NOT_EMPTY]: WorkflowVariableType.Null,
   },
+  [WorkflowVariableType.Map]: {
+    [ConditionOperator.IS_EMPTY]: WorkflowVariableType.Null,
+    [ConditionOperator.IS_NOT_EMPTY]: WorkflowVariableType.Null,
+  },
+  [WorkflowVariableType.DateTime]: {
+    [ConditionOperator.EQ]: WorkflowVariableType.DateTime,
+    [ConditionOperator.NEQ]: WorkflowVariableType.DateTime,
+    [ConditionOperator.GT]: WorkflowVariableType.DateTime,
+    [ConditionOperator.GTE]: WorkflowVariableType.DateTime,
+    [ConditionOperator.LT]: WorkflowVariableType.DateTime,
+    [ConditionOperator.LTE]: WorkflowVariableType.DateTime,
+    [ConditionOperator.IS_EMPTY]: WorkflowVariableType.Null,
+    [ConditionOperator.IS_NOT_EMPTY]: WorkflowVariableType.Null,
+  },
   [WorkflowVariableType.Array]: {
     [ConditionOperator.IS_EMPTY]: WorkflowVariableType.Null,
     [ConditionOperator.IS_NOT_EMPTY]: WorkflowVariableType.Null,