mirror of
https://github.com/nocodb/nocodb.git
synced 2026-02-01 23:48:33 +00:00
fix: workflow corrections
This commit is contained in:
139
packages/nocodb-sdk/src/lib/workflow/interface.ts
Normal file
139
packages/nocodb-sdk/src/lib/workflow/interface.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
enum VariableType {
|
||||
String = 'string',
|
||||
Number = 'number',
|
||||
Integer = 'integer',
|
||||
Boolean = 'boolean',
|
||||
Object = 'object',
|
||||
Array = 'array',
|
||||
Date = 'date',
|
||||
DateTime = 'datetime',
|
||||
}
|
||||
|
||||
enum VariableGroupKey {
|
||||
Meta = 'meta', // System fields (id, createdAt, etc.)
|
||||
Fields = 'fields', // User data fields
|
||||
}
|
||||
|
||||
interface VariableDefinition {
|
||||
// Expression reference (e.g., "fields.Title" for use in {{ $("Node").fields.Title }})
|
||||
key: string;
|
||||
|
||||
// Human-readable name for UI (e.g., "Title")
|
||||
name: string;
|
||||
|
||||
// Variable type
|
||||
type: VariableType;
|
||||
|
||||
// UI grouping
|
||||
groupKey?: VariableGroupKey;
|
||||
|
||||
// Is this variable an array?
|
||||
isArray?: boolean;
|
||||
|
||||
// Additional metadata for UI
|
||||
extra?: {
|
||||
// Entity ID for dependency tracking
|
||||
entity_id?: string;
|
||||
|
||||
// Entity type for dependency tracking ('column' | 'table' | 'view')
|
||||
entity?: 'column' | 'table' | 'view';
|
||||
|
||||
// icon for the variable
|
||||
icon?: string;
|
||||
|
||||
// UIType for fields
|
||||
uiType?: string;
|
||||
|
||||
// Table/View names for display
|
||||
tableName?: string;
|
||||
viewName?: string;
|
||||
|
||||
// Description for tooltips
|
||||
description?: string;
|
||||
|
||||
// Value for dynamically generated variables (used in data display)
|
||||
value?: any;
|
||||
|
||||
// Item schema for array variables - defines the structure of each array item
|
||||
// Used to generate "Record 1", "Record 2", etc. when displaying actual data
|
||||
itemSchema?: VariableDefinition[];
|
||||
};
|
||||
|
||||
// Nested variables for objects/arrays
|
||||
children?: VariableDefinition[];
|
||||
}
|
||||
|
||||
interface NodeExecutionResult {
|
||||
nodeId: string;
|
||||
nodeTitle: string;
|
||||
status: 'pending' | 'running' | 'success' | 'error' | 'skipped';
|
||||
output?: any;
|
||||
input?: any;
|
||||
error?: string;
|
||||
startTime: number;
|
||||
endTime?: number;
|
||||
nextNode?: string; // Next node to execute (for conditional branching)
|
||||
logs?: Array<{
|
||||
level: 'info' | 'warn' | 'error';
|
||||
message: string;
|
||||
ts?: number;
|
||||
data?: any;
|
||||
}>;
|
||||
metrics?: Record<string, number>;
|
||||
isStale?: boolean;
|
||||
inputVariables?: VariableDefinition[];
|
||||
outputVariables?: VariableDefinition[];
|
||||
}
|
||||
|
||||
interface WorkflowExecutionState {
|
||||
id: string;
|
||||
workflowId: string;
|
||||
status: 'running' | 'completed' | 'error' | 'cancelled' | 'skipped';
|
||||
startTime: number;
|
||||
endTime?: number;
|
||||
nodeResults: NodeExecutionResult[];
|
||||
currentNodeId?: string;
|
||||
triggerData?: any;
|
||||
triggerNodeTitle?: string; // Optional: which trigger node to start from
|
||||
}
|
||||
|
||||
interface NodeConfig {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface WorkflowGeneralNode {
|
||||
id: string;
|
||||
type: string;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
data: {
|
||||
config: NodeConfig;
|
||||
title: string;
|
||||
description?: string;
|
||||
testResult?: NodeExecutionResult;
|
||||
inputVariables?: VariableDefinition[];
|
||||
outputVariables?: VariableDefinition[];
|
||||
};
|
||||
targetPosition: 'top' | 'bottom' | 'left' | 'right';
|
||||
sourcePosition: 'top' | 'bottom' | 'left' | 'right';
|
||||
}
|
||||
|
||||
interface WorkflowGeneralEdge {
|
||||
id: string;
|
||||
source: string; // Source node ID
|
||||
target: string; // Target node ID
|
||||
animated: boolean;
|
||||
label?: string; // Optional label like "True" or "False"
|
||||
}
|
||||
|
||||
export {
|
||||
VariableType,
|
||||
VariableGroupKey,
|
||||
VariableDefinition,
|
||||
WorkflowGeneralEdge,
|
||||
WorkflowGeneralNode,
|
||||
NodeExecutionResult,
|
||||
WorkflowExecutionState,
|
||||
};
|
||||
918
packages/nocodb-sdk/src/lib/workflow/variable-generator.ts
Normal file
918
packages/nocodb-sdk/src/lib/workflow/variable-generator.ts
Normal file
@@ -0,0 +1,918 @@
|
||||
import UITypes from '../UITypes';
|
||||
import { isSystemColumn, parseProp } from '../helperFunctions';
|
||||
import {
|
||||
VariableDefinition,
|
||||
VariableGroupKey,
|
||||
VariableType,
|
||||
} from './interface';
|
||||
import { RelationTypes } from '../globals';
|
||||
import { ColumnType } from '~/lib';
|
||||
import { LinkToAnotherRecordType } from '~/lib/Api';
|
||||
|
||||
/**
|
||||
* Map UIType to icon name (matching NocoDB's iconMap)
|
||||
*/
|
||||
export function uiTypeToIcon(column: ColumnType): string {
|
||||
switch (column.uidt) {
|
||||
case UITypes.ID:
|
||||
case UITypes.ForeignKey:
|
||||
return 'cellSystemKey';
|
||||
case UITypes.SingleLineText:
|
||||
return 'cellText';
|
||||
case UITypes.LongText:
|
||||
return 'cellLongText';
|
||||
case UITypes.Email:
|
||||
return 'cellEmail';
|
||||
case UITypes.URL:
|
||||
return 'cellUrl';
|
||||
case UITypes.PhoneNumber:
|
||||
return 'cellPhone';
|
||||
case UITypes.Number:
|
||||
return 'cellNumber';
|
||||
case UITypes.Decimal:
|
||||
return 'cellDecimal';
|
||||
case UITypes.Currency:
|
||||
return 'cellCurrency';
|
||||
case UITypes.Percent:
|
||||
return 'cellPercent';
|
||||
case UITypes.Duration:
|
||||
return 'cellDuration';
|
||||
case UITypes.Rating:
|
||||
return 'cellRating';
|
||||
case UITypes.Checkbox:
|
||||
return 'cellCheckbox';
|
||||
case UITypes.Date:
|
||||
return 'cellDate';
|
||||
case UITypes.DateTime:
|
||||
return 'cellDatetime';
|
||||
case UITypes.Time:
|
||||
return 'cellTime';
|
||||
case UITypes.Year:
|
||||
return 'cellYear';
|
||||
case UITypes.Attachment:
|
||||
return 'cellAttachment';
|
||||
case UITypes.SingleSelect:
|
||||
return 'cellSingleSelect';
|
||||
case UITypes.MultiSelect:
|
||||
return 'cellMultiSelect';
|
||||
case UITypes.Collaborator:
|
||||
return 'cellUser';
|
||||
case UITypes.CreatedBy:
|
||||
case UITypes.LastModifiedBy:
|
||||
return 'cellSystemUser';
|
||||
case UITypes.CreatedTime:
|
||||
case UITypes.LastModifiedTime:
|
||||
return 'cellSystemDate';
|
||||
case UITypes.LinkToAnotherRecord:
|
||||
case UITypes.Links: {
|
||||
const relationType = (column.colOptions as LinkToAnotherRecordType).type;
|
||||
if (relationType === RelationTypes.HAS_MANY) {
|
||||
return 'hm_solid';
|
||||
} else if (relationType === RelationTypes.MANY_TO_MANY) {
|
||||
return 'mm_solid';
|
||||
} else if (relationType === RelationTypes.BELONGS_TO) {
|
||||
return 'bt_solid';
|
||||
} else if (relationType === RelationTypes.ONE_TO_ONE) {
|
||||
return 'oneToOneSolid';
|
||||
}
|
||||
return 'mmSolid';
|
||||
}
|
||||
case UITypes.Lookup:
|
||||
return 'cellLookup';
|
||||
case UITypes.Rollup:
|
||||
return 'cellRollup';
|
||||
case UITypes.Formula:
|
||||
return 'cellFormula';
|
||||
case UITypes.Count:
|
||||
case UITypes.AutoNumber:
|
||||
return 'cellNumber';
|
||||
case UITypes.Barcode:
|
||||
return 'cellBarcode';
|
||||
case UITypes.QrCode:
|
||||
return 'cellQrCode';
|
||||
case UITypes.GeoData:
|
||||
return 'ncMapPin';
|
||||
case UITypes.Geometry:
|
||||
return 'cellGeometry';
|
||||
case UITypes.JSON:
|
||||
return 'cellJson';
|
||||
case UITypes.Button:
|
||||
return 'cellButton';
|
||||
case UITypes.SpecificDBType:
|
||||
return 'cellDb';
|
||||
default:
|
||||
return 'cellSystemText';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert NocoDB UIType to VariableType
|
||||
*/
|
||||
export function uiTypeToVariableType(
|
||||
uiType: UITypes,
|
||||
columnMeta?: any
|
||||
): { type: VariableType; isArray: boolean } {
|
||||
switch (uiType) {
|
||||
// Number types
|
||||
case UITypes.Number:
|
||||
case UITypes.Decimal:
|
||||
case UITypes.Currency:
|
||||
case UITypes.Percent:
|
||||
case UITypes.Duration:
|
||||
case UITypes.Rating:
|
||||
case UITypes.Rollup:
|
||||
case UITypes.AutoNumber:
|
||||
case UITypes.Count:
|
||||
return { type: VariableType.Number, isArray: false };
|
||||
|
||||
// Integer type
|
||||
case UITypes.Links:
|
||||
return { type: VariableType.Integer, isArray: false };
|
||||
|
||||
// Boolean types
|
||||
case UITypes.Checkbox:
|
||||
return { type: VariableType.Boolean, isArray: false };
|
||||
|
||||
// DateTime types
|
||||
case UITypes.Date:
|
||||
case UITypes.DateTime:
|
||||
case UITypes.Time:
|
||||
case UITypes.CreatedTime:
|
||||
case UITypes.LastModifiedTime:
|
||||
return { type: VariableType.DateTime, isArray: false };
|
||||
|
||||
// Array types
|
||||
case UITypes.Attachment:
|
||||
return { type: VariableType.Object, isArray: true };
|
||||
|
||||
case UITypes.MultiSelect:
|
||||
return { type: VariableType.String, isArray: true };
|
||||
|
||||
// Object types
|
||||
case UITypes.LinkToAnotherRecord: {
|
||||
// Check relation type from columnMeta
|
||||
const isMultiple =
|
||||
columnMeta?.type === RelationTypes.HAS_MANY ||
|
||||
columnMeta?.type === RelationTypes.MANY_TO_MANY;
|
||||
return { type: VariableType.Object, isArray: isMultiple };
|
||||
}
|
||||
|
||||
case UITypes.Collaborator: {
|
||||
// Check if multi-user
|
||||
const isMultiUser = parseProp(columnMeta?.meta)?.is_multi;
|
||||
return { type: VariableType.Object, isArray: isMultiUser };
|
||||
}
|
||||
|
||||
case UITypes.LastModifiedBy:
|
||||
case UITypes.CreatedBy:
|
||||
return { type: VariableType.Object, isArray: false };
|
||||
|
||||
case UITypes.JSON:
|
||||
return { type: VariableType.Object, isArray: false };
|
||||
|
||||
// String types (default)
|
||||
case UITypes.SingleLineText:
|
||||
case UITypes.LongText:
|
||||
case UITypes.Email:
|
||||
case UITypes.URL:
|
||||
case UITypes.PhoneNumber:
|
||||
case UITypes.SingleSelect:
|
||||
case UITypes.Year:
|
||||
case UITypes.GeoData:
|
||||
case UITypes.Geometry:
|
||||
case UITypes.Barcode:
|
||||
case UITypes.QrCode:
|
||||
case UITypes.Button:
|
||||
case UITypes.SpecificDBType:
|
||||
case UITypes.ID:
|
||||
case UITypes.ForeignKey:
|
||||
default:
|
||||
return { type: VariableType.String, isArray: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to safely build a property accessor (dot notation or bracket notation)
|
||||
* Uses bracket notation if the property name contains spaces or special characters
|
||||
*/
|
||||
function buildPropertyKey(prefix: string, propertyName: string): string {
|
||||
// Check if property name needs bracket notation
|
||||
// Use bracket notation if it contains spaces, starts with a number, or has special chars
|
||||
const needsBrackets =
|
||||
/[^a-zA-Z0-9_$]/.test(propertyName) || /^\d/.test(propertyName);
|
||||
|
||||
if (needsBrackets) {
|
||||
return `${prefix}['${propertyName}']`;
|
||||
} else {
|
||||
return `${prefix}.${propertyName}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate variable definition from NocoDB column
|
||||
*/
|
||||
export function getFieldVariable(
|
||||
column: ColumnType,
|
||||
prefix: string = 'fields'
|
||||
): VariableDefinition {
|
||||
const { type, isArray } = uiTypeToVariableType(
|
||||
column.uidt as UITypes,
|
||||
column.colOptions
|
||||
);
|
||||
|
||||
const variable: VariableDefinition = {
|
||||
key: buildPropertyKey(prefix, column.title),
|
||||
name: column.title,
|
||||
type,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
isArray,
|
||||
extra: {
|
||||
entity_id: column.id,
|
||||
entity: 'column',
|
||||
uiType: column.uidt,
|
||||
icon: uiTypeToIcon(column),
|
||||
},
|
||||
};
|
||||
|
||||
if (column.uidt === UITypes.Attachment && isArray) {
|
||||
// Define the structure of each attachment item
|
||||
variable.extra = {
|
||||
...variable.extra,
|
||||
itemSchema: [
|
||||
{
|
||||
key: 'url',
|
||||
name: 'url',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellUrl',
|
||||
description: 'URL of the attachment',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'signedUrl',
|
||||
name: 'signedUrl',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellUrl',
|
||||
description: 'Signed URL of the attachment',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'title',
|
||||
name: 'title',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellText',
|
||||
description: 'Title of the attachment',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'mimetype',
|
||||
name: 'mimetype',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellText',
|
||||
description: 'MIME type of the attachment',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'size',
|
||||
name: 'size',
|
||||
type: VariableType.Number,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellNumber',
|
||||
description: 'Size of the attachment in bytes',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Array-level properties
|
||||
variable.children = [
|
||||
{
|
||||
key: `${variable.key}.length`,
|
||||
name: 'length',
|
||||
type: VariableType.Number,
|
||||
groupKey: VariableGroupKey.Meta,
|
||||
extra: {
|
||||
description: 'Number of attachments',
|
||||
icon: 'cellNumber',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: `${variable.key}.map(item => item.url).join(', ')`,
|
||||
name: 'url',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellUrl',
|
||||
description: 'Comma-separated URLs of all attachments',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: `${variable.key}.map(item => item.signedUrl).join(', ')`,
|
||||
name: 'signedUrl',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellUrl',
|
||||
description: 'Comma-separated signed URLs of all attachments',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: `${variable.key}.map(item => item.title).join(', ')`,
|
||||
name: 'title',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellText',
|
||||
description: 'Comma-separated titles of all attachments',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: `${variable.key}.map(item => item.mimetype).join(', ')`,
|
||||
name: 'mimetype',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellText',
|
||||
description: 'Comma-separated mimetypes of all attachments',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: `${variable.key}.map(item => item.size).join(', ')`,
|
||||
name: 'size',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellNumber',
|
||||
description: 'Comma-separated sizes of all attachments',
|
||||
},
|
||||
},
|
||||
];
|
||||
} else if (column.uidt === UITypes.Collaborator) {
|
||||
const isMulti = parseProp(column.meta)?.is_multi;
|
||||
|
||||
variable.extra = isMulti
|
||||
? {
|
||||
...variable.extra,
|
||||
itemSchema: [
|
||||
{
|
||||
key: 'id',
|
||||
name: 'id',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellSystemKey',
|
||||
description: 'User ID',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
name: 'email',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellEmail',
|
||||
description: 'User email',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'display_name',
|
||||
name: 'display_name',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellText',
|
||||
description: 'User display name',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {};
|
||||
|
||||
// Array-level properties
|
||||
variable.children = [
|
||||
{
|
||||
key: isMulti
|
||||
? `${variable.key}.map(item => item.id).join(', ')`
|
||||
: `${variable.key}.id`,
|
||||
name: 'id',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellSystemKey',
|
||||
description: isMulti ? 'Comma-separated user IDs' : 'User ID',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: isMulti
|
||||
? `${variable.key}.map(item => item.email).join(', ')`
|
||||
: `${variable.key}.email`,
|
||||
name: 'email',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellEmail',
|
||||
description: isMulti ? 'Comma-separated emails' : 'User email',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: isMulti
|
||||
? `${variable.key}.map(item => item.display_name || '').join(', ')`
|
||||
: `${variable.key}.display_name`,
|
||||
name: 'display_name',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellText',
|
||||
description: isMulti
|
||||
? 'Comma-separated display names'
|
||||
: 'User display name',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: `${variable.key}.length`,
|
||||
name: 'length',
|
||||
type: VariableType.Number,
|
||||
groupKey: VariableGroupKey.Meta,
|
||||
extra: {
|
||||
description: 'Number of users',
|
||||
icon: 'cellNumber',
|
||||
},
|
||||
},
|
||||
];
|
||||
} else if (column.uidt === UITypes.LinkToAnotherRecord && isArray) {
|
||||
variable.children = [
|
||||
{
|
||||
key: `${variable.key}.length`,
|
||||
name: 'length',
|
||||
type: VariableType.Number,
|
||||
groupKey: VariableGroupKey.Meta,
|
||||
extra: {
|
||||
description: 'Number of linked records',
|
||||
icon: 'cellNumber',
|
||||
},
|
||||
},
|
||||
];
|
||||
} else if (column.uidt === UITypes.MultiSelect && isArray) {
|
||||
variable.extra = {
|
||||
...variable.extra,
|
||||
itemSchema: [
|
||||
{
|
||||
key: '', // Empty key means the item itself (it's a primitive string)
|
||||
name: 'option',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
icon: 'cellText',
|
||||
description: 'Selected option',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Array-level properties
|
||||
variable.children = [
|
||||
{
|
||||
key: `${variable.key}.length`,
|
||||
name: 'length',
|
||||
type: VariableType.Number,
|
||||
groupKey: VariableGroupKey.Meta,
|
||||
extra: {
|
||||
description: 'Number of selected options',
|
||||
icon: 'cellNumber',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return variable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate system/meta variables for records
|
||||
*/
|
||||
export function getMetaVariables(prefix: string = ''): VariableDefinition[] {
|
||||
const baseKey = prefix ? `${prefix}.` : '';
|
||||
|
||||
return [
|
||||
{
|
||||
key: `${baseKey}id`,
|
||||
name: 'id',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Meta,
|
||||
extra: {
|
||||
description: 'Record ID',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate complete record variable structure
|
||||
* This is the main function for generating variables from NocoDB table columns
|
||||
*
|
||||
* @param columns - Array of column definitions
|
||||
* @param isArray - Whether this represents an array of records
|
||||
* @param outputKey - The key under which the record(s) are stored in the output (e.g., 'record', 'deleted', 'records')
|
||||
*/
|
||||
export function genRecordVariables(
|
||||
columns: Array<ColumnType>,
|
||||
isArray: boolean = false,
|
||||
outputKey?: string
|
||||
): VariableDefinition[] {
|
||||
const filteredColumns = columns.filter((col) => !isSystemColumn(col));
|
||||
const recordKey = outputKey || (isArray ? 'records' : 'record');
|
||||
const recordName = recordKey.charAt(0).toUpperCase() + recordKey.slice(1);
|
||||
|
||||
if (isArray) {
|
||||
// Generate field variables (without array prefix for itemSchema)
|
||||
const fieldVariables = filteredColumns.map((col) => {
|
||||
return getFieldVariable(col, 'fields');
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
key: recordKey,
|
||||
name: recordName,
|
||||
type: VariableType.Array,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
isArray: true,
|
||||
extra: {
|
||||
description: `List of ${recordName.toLowerCase()}`,
|
||||
icon: 'cellJson',
|
||||
// Define the structure of each record item
|
||||
itemSchema: [
|
||||
{
|
||||
key: 'id',
|
||||
name: 'ID',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
description: 'Record ID',
|
||||
icon: 'cellSystemKey',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'fields',
|
||||
name: 'Fields',
|
||||
type: VariableType.Object,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
description: 'Record fields',
|
||||
icon: 'cellJson',
|
||||
},
|
||||
children: fieldVariables,
|
||||
},
|
||||
],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
key: `${recordKey}.length`,
|
||||
name: 'Count',
|
||||
type: VariableType.Number,
|
||||
groupKey: VariableGroupKey.Meta,
|
||||
extra: {
|
||||
description: 'Number of records',
|
||||
icon: 'cellNumber',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
} else {
|
||||
// Generate field variables with record prefix
|
||||
const fieldVariables = filteredColumns.map((col) => {
|
||||
const fieldVar = getFieldVariable(col, 'fields');
|
||||
return prefixVariableKeys(fieldVar, recordKey);
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
key: recordKey,
|
||||
name: recordName,
|
||||
type: VariableType.Object,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
description: `The ${recordName.toLowerCase()}`,
|
||||
icon: 'cellJson',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
key: `${recordKey}.id`,
|
||||
name: 'ID',
|
||||
type: VariableType.String,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
description: 'Record ID',
|
||||
icon: 'cellSystemKey',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: `${recordKey}.fields`,
|
||||
name: 'Fields',
|
||||
type: VariableType.Object,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
description: 'Record fields',
|
||||
icon: 'cellJson',
|
||||
},
|
||||
children: fieldVariables,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to prefix variable keys recursively
|
||||
*/
|
||||
export function prefixVariableKeys(
|
||||
variable: VariableDefinition,
|
||||
prefix: string
|
||||
): VariableDefinition {
|
||||
return {
|
||||
...variable,
|
||||
key: `${prefix}.${variable.key}`,
|
||||
children: (variable.children ?? []).map((child) =>
|
||||
prefixVariableKeys(child, prefix)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate variables from actual output data (fallback for unknown structures)
|
||||
* This is used when we don't have column definitions but have actual output data
|
||||
*/
|
||||
export function genGeneralVariables(
|
||||
output: any,
|
||||
prefix: string = ''
|
||||
): VariableDefinition[] {
|
||||
if (output == null) return [];
|
||||
|
||||
const variables: VariableDefinition[] = [];
|
||||
|
||||
// Primitive value
|
||||
if (typeof output !== 'object') {
|
||||
return [
|
||||
{
|
||||
key: prefix || 'value',
|
||||
name: prefix || 'value',
|
||||
type: typeof output as VariableType,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Array - analyze first item
|
||||
if (Array.isArray(output)) {
|
||||
if (output.length === 0) return [];
|
||||
|
||||
const firstItem = output[0];
|
||||
const arrayName = prefix.split('.').pop() || 'items';
|
||||
|
||||
// If array items are primitives (string, number, boolean)
|
||||
if (typeof firstItem !== 'object' || firstItem === null) {
|
||||
return [
|
||||
{
|
||||
key: prefix,
|
||||
name: arrayName,
|
||||
type: VariableType.Array,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
isArray: true,
|
||||
extra: {
|
||||
description: `Array of ${typeof firstItem}s`,
|
||||
itemSchema: [
|
||||
{
|
||||
key: '',
|
||||
name: 'item',
|
||||
type: typeof firstItem as VariableType,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
description: `Individual ${typeof firstItem}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
key: `${prefix}.length`,
|
||||
name: 'length',
|
||||
type: VariableType.Number,
|
||||
groupKey: VariableGroupKey.Meta,
|
||||
extra: {
|
||||
description: 'Number of items',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// If array items are objects, analyze the structure
|
||||
const itemSchema: VariableDefinition[] = [];
|
||||
|
||||
// Analyze first item's properties
|
||||
for (const [itemKey, itemValue] of Object.entries(firstItem)) {
|
||||
const isNestedArray = Array.isArray(itemValue);
|
||||
const valueType = typeof itemValue;
|
||||
|
||||
const itemDef: VariableDefinition = {
|
||||
key: itemKey,
|
||||
name: itemKey,
|
||||
type: isNestedArray
|
||||
? VariableType.Array
|
||||
: valueType === 'object' && itemValue !== null
|
||||
? VariableType.Object
|
||||
: (valueType as VariableType),
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
isArray: isNestedArray,
|
||||
extra: {
|
||||
description: `${itemKey} property of array item`,
|
||||
},
|
||||
};
|
||||
|
||||
// For nested objects/arrays, recursively build schema
|
||||
if (typeof itemValue === 'object' && itemValue !== null) {
|
||||
if (Array.isArray(itemValue) && itemValue.length > 0) {
|
||||
// Nested array - recursively generate its itemSchema
|
||||
const nestedFirstItem = itemValue[0];
|
||||
if (typeof nestedFirstItem === 'object' && nestedFirstItem !== null) {
|
||||
const nestedItemSchema: VariableDefinition[] = [];
|
||||
for (const [nestedKey, nestedValue] of Object.entries(
|
||||
nestedFirstItem
|
||||
)) {
|
||||
nestedItemSchema.push({
|
||||
key: nestedKey,
|
||||
name: nestedKey,
|
||||
type: typeof nestedValue as VariableType,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
description: `${nestedKey} property`,
|
||||
},
|
||||
});
|
||||
}
|
||||
itemDef.extra = {
|
||||
...itemDef.extra,
|
||||
itemSchema: nestedItemSchema,
|
||||
};
|
||||
} else {
|
||||
// Array of primitives
|
||||
itemDef.extra = {
|
||||
...itemDef.extra,
|
||||
itemSchema: [
|
||||
{
|
||||
key: '',
|
||||
name: 'item',
|
||||
type: typeof nestedFirstItem as VariableType,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
itemDef.children = [
|
||||
{
|
||||
key: `${itemKey}.length`,
|
||||
name: 'length',
|
||||
type: VariableType.Number,
|
||||
groupKey: VariableGroupKey.Meta,
|
||||
extra: {
|
||||
description: 'Number of items',
|
||||
},
|
||||
},
|
||||
];
|
||||
} else if (!Array.isArray(itemValue)) {
|
||||
// Nested object - build children
|
||||
const nestedChildren: VariableDefinition[] = [];
|
||||
for (const [nestedKey, nestedValue] of Object.entries(itemValue)) {
|
||||
nestedChildren.push({
|
||||
key: `${itemKey}.${nestedKey}`,
|
||||
name: nestedKey,
|
||||
type: typeof nestedValue as VariableType,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
description: `${nestedKey} property`,
|
||||
},
|
||||
});
|
||||
}
|
||||
itemDef.children = nestedChildren;
|
||||
}
|
||||
}
|
||||
|
||||
itemSchema.push(itemDef);
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key: prefix,
|
||||
name: arrayName,
|
||||
type: VariableType.Array,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
isArray: true,
|
||||
extra: {
|
||||
description: `Array of ${arrayName}`,
|
||||
itemSchema: itemSchema,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
key: `${prefix}.length`,
|
||||
name: 'length',
|
||||
type: VariableType.Number,
|
||||
groupKey: VariableGroupKey.Meta,
|
||||
extra: {
|
||||
description: 'Number of items',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Object - recurse into properties
|
||||
for (const [key, value] of Object.entries(output)) {
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||
const isArray = Array.isArray(value);
|
||||
const valueType = typeof value;
|
||||
|
||||
const varDef: VariableDefinition = {
|
||||
key: fullKey,
|
||||
name: key,
|
||||
type: isArray
|
||||
? VariableType.Array
|
||||
: valueType === 'object'
|
||||
? VariableType.Object
|
||||
: (valueType as VariableType),
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
isArray,
|
||||
};
|
||||
|
||||
// Handle arrays with itemSchema
|
||||
if (isArray && value.length > 0) {
|
||||
const firstItem = value[0];
|
||||
|
||||
// Array of primitives
|
||||
if (typeof firstItem !== 'object' || firstItem === null) {
|
||||
varDef.extra = {
|
||||
itemSchema: [
|
||||
{
|
||||
key: '',
|
||||
name: 'item',
|
||||
type: typeof firstItem as VariableType,
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
extra: {
|
||||
description: `Individual ${typeof firstItem}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
// Array of objects - build itemSchema
|
||||
const itemSchema: VariableDefinition[] = [];
|
||||
for (const [itemKey, itemValue] of Object.entries(firstItem)) {
|
||||
itemSchema.push({
|
||||
key: itemKey,
|
||||
name: itemKey,
|
||||
type: Array.isArray(itemValue)
|
||||
? VariableType.Array
|
||||
: typeof itemValue === 'object' && itemValue !== null
|
||||
? VariableType.Object
|
||||
: (typeof itemValue as VariableType),
|
||||
groupKey: VariableGroupKey.Fields,
|
||||
isArray: Array.isArray(itemValue),
|
||||
extra: {
|
||||
description: `${itemKey} property`,
|
||||
},
|
||||
});
|
||||
}
|
||||
varDef.extra = { itemSchema };
|
||||
}
|
||||
|
||||
// Add length property
|
||||
varDef.children = [
|
||||
{
|
||||
key: `${fullKey}.length`,
|
||||
name: 'length',
|
||||
type: VariableType.Number,
|
||||
groupKey: VariableGroupKey.Meta,
|
||||
extra: {
|
||||
description: 'Number of items',
|
||||
},
|
||||
},
|
||||
];
|
||||
} else if (!isArray && valueType === 'object' && value !== null) {
|
||||
// Regular object - recurse into children
|
||||
varDef.children = genGeneralVariables(value, fullKey);
|
||||
}
|
||||
|
||||
variables.push(varDef);
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
Reference in New Issue
Block a user