Something went wrong. Try again.
🔥 🔥 🔥 A Free & Self-hostable Airtable Alternative nocodb.com
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293import type { Edge, Node } from '@vue-flow/core'import type { WorkflowNodeDefinition, WorkflowType } from 'nocodb-sdk'import { GeneralNodeID, INIT_WORKFLOW_NODES } from 'nocodb-sdk'import { generateRandomUUID } from '~/utils/generateName'
/** * Filter nodes and edges based on edit permission * Removes Plus and Trigger nodes when user doesn't have edit permission */const filterNodesByPermission = (nodes: Array<Node>, edges: Array<Edge>, hasEditPermission: boolean) => { if (hasEditPermission) { return { nodes, edges } }
// Filter out Plus and Trigger nodes when user doesn't have edit permission const filteredNodes = nodes.filter((node) => node.type !== GeneralNodeID.PLUS && node.type !== GeneralNodeID.TRIGGER)
// Get IDs of filtered out nodes const filteredNodeIds = new Set( nodes.filter((node) => node.type === GeneralNodeID.PLUS || node.type === GeneralNodeID.TRIGGER).map((n) => n.id), )
// Filter out edges connected to filtered nodes const filteredEdges = edges.filter((edge) => !filteredNodeIds.has(edge.source) && !filteredNodeIds.has(edge.target))
return { nodes: filteredNodes, edges: filteredEdges }}
/** * Get source nodes/edges based on permission * Don't use draft if user doesn't have edit permission */const getSourceNodesAndEdges = (workflow: WorkflowType, hasEditPermission: boolean) => { const sourceNodes = hasEditPermission ? workflow?.draft?.nodes || workflow?.nodes || INIT_WORKFLOW_NODES : workflow?.nodes || INIT_WORKFLOW_NODES
const sourceEdges = hasEditPermission ? workflow?.draft?.edges || workflow?.edges || [] : workflow?.edges || []
return { sourceNodes: sourceNodes as Array<Node>, sourceEdges: sourceEdges as Array<Edge> }}
const generateUniqueNodeId = (nodes: Node[]): string => { let candidateId = generateRandomUUID()
// Keep incrementing until we find an ID that doesn't exist while (nodes.some((n) => n.id === candidateId)) { candidateId = generateRandomUUID() }
return candidateId}
/** * Generate a unique trigger ID for webhook triggers * Format: trg_{8 alphanumeric characters} */const generateTriggerId = (): string => { const uuid = generateRandomUUID() // Use first 8 characters of UUID (removing hyphens) const shortId = uuid.replace(/-/g, '').substring(0, 8) return `trg_${shortId}`}
interface UIWorkflowNodeDefinition extends WorkflowNodeDefinition { input?: number output?: number}
function transformNode(backendNode: WorkflowNodeDefinition): UIWorkflowNodeDefinition { const inputPorts = backendNode.ports.filter((p: any) => p.direction === 'input') const outputPorts = backendNode.ports.filter((p: any) => p.direction === 'output')
return { ...backendNode, input: inputPorts.length > 0 ? inputPorts.length : undefined, output: outputPorts.length > 0 ? outputPorts.length : undefined, }}
/** * Find all parent nodes (upstream nodes) for a given node * @param nodeId - The node ID to find parents for * @param edges - All edges in the workflow * @returns array of parent node IDs in execution order */const findAllParentNodes = (nodeId: string, edges: Edge[]): string[] => { const parents: string[] = [] const visited = new Set<string>()
const traverse = (currentId: string) => { if (visited.has(currentId)) return visited.add(currentId)
// Find edges that point to this node const parentEdges = edges.filter((edge) => edge.target === currentId)
for (const edge of parentEdges) { if (edge.source && edge.source !== currentId) { // First traverse to parents of this parent (to maintain execution order) traverse(edge.source) // Then add this parent if (!parents.includes(edge.source)) { parents.push(edge.source) } } } }
traverse(nodeId) return parents}
/** * Recursively prefix all variable keys (including children) with the node reference */const prefixVariableKeysRecursive = (variable: any, prefix: string): any => { return { ...variable, key: `${prefix}.${variable.key}`, children: variable.children?.map((child: any) => prefixVariableKeysRecursive(child, prefix)), }}
/** * Generate a unique node title based on the node type title * E.g., 'NocoDB', 'NocoDB1', 'NocoDB2', etc. */const generateUniqueNodeTitle = (nodeMeta: UIWorkflowNodeDefinition, nodes: Node[]): string => { const baseTitle = nodeMeta.title
// Get all existing node titles that start with this base title const existingTitles = nodes.map((n) => n.data?.title).filter((title): title is string => typeof title === 'string')
// Check if base title is available (without number) if (!existingTitles.includes(baseTitle)) { return baseTitle }
// Find the highest number used with this base title let maxNumber = 0 const regex = new RegExp(`^${baseTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\d+)$`)
existingTitles.forEach((title) => { const match = title.match(regex) if (match && match[1]) { const num = parseInt(match[1], 10) if (num > maxNumber) { maxNumber = num } } })
// Return the next available number return `${baseTitle}${maxNumber + 1}`}
const findAllChildNodes = (nodeId: string, edges: Edge[]): Set<string> => { const children = new Set<string>() const visited = new Set<string>()
const traverse = (currentId: string) => { if (visited.has(currentId)) return visited.add(currentId)
const childEdges = edges.filter((edge) => edge.source === currentId)
for (const edge of childEdges) { if (edge.target) { children.add(edge.target) traverse(edge.target) } } }
traverse(nodeId) return children}
/** * Find which output port from an iterate node leads to a target node * Recursively follows edges to determine the port path * @param iterateNodeId - The iterate node ID * @param targetNodeId - The target node ID we're trying to reach * @param edges - All edges in the workflow * @returns The port ID ('body' or 'output') or null if no path found */const findIterateNodePortForPath = (iterateNodeId: string, targetNodeId: string, edges: Edge[]): string | null => { // Find all edges from the iterate node const iterateEdges = edges.filter((e) => e.source === iterateNodeId)
// For each output port from the iterate node for (const edge of iterateEdges) { const portId = edge.sourceHandle
// Check if this edge leads to the target node (directly or indirectly) const visited = new Set<string>() const queue = [edge.target]
while (queue.length > 0) { const currentNodeId = queue.shift()!
if (currentNodeId === targetNodeId) { // Found a path from this port to the target node return portId as string }
if (visited.has(currentNodeId)) continue visited.add(currentNodeId)
// Add all child nodes to the queue const childEdges = edges.filter((e) => e.source === currentNodeId) for (const childEdge of childEdges) { queue.push(childEdge.target) } } }
return null}
/** * Update variable references in a string when a node is renamed * Replaces $('oldName') with $('newName') and $("oldName") with $("newName") * @param content - The string content to update * @param oldTitle - The old node title * @param newTitle - The new node title * @returns Updated string with replaced variable references */const updateVariableReferences = (content: string, oldTitle: string, newTitle: string): string => { if (!ncIsString(content)) return content
// Escape special regex characters in both titles const escapedOldTitle = oldTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const escapedNewTitle = newTitle.replace(/\$/g, '$$$$') // Escape $ for replacement string
// Replace both single and double quoted references const singleQuoteRegex = new RegExp(`\\$\\('${escapedOldTitle}'\\)`, 'g') const doubleQuoteRegex = new RegExp(`\\$\\("${escapedOldTitle}"\\)`, 'g')
let updated = content.replace(singleQuoteRegex, `$('${escapedNewTitle}')`) updated = updated.replace(doubleQuoteRegex, `$("${escapedNewTitle}")`)
return updated}/** * Recursively update variable references in an object * @param obj - The object to update * @param oldTitle - The old node title * @param newTitle - The new node title * @returns Updated object with replaced variable references */const updateVariableReferencesInObject = (obj: any, oldTitle: string, newTitle: string): any => { if (ncIsNullOrUndefined(obj)) return obj
if (ncIsString(obj)) { return updateVariableReferences(obj, oldTitle, newTitle) }
if (ncIsArray(obj)) { return obj.map((item) => updateVariableReferencesInObject(item, oldTitle, newTitle)) }
if (ncIsObject(obj)) { const updated: any = {} for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { updated[key] = updateVariableReferencesInObject(obj[key], oldTitle, newTitle) } } return updated }
return obj}
export { filterNodesByPermission, getSourceNodesAndEdges, generateUniqueNodeId, generateTriggerId, transformNode, findAllParentNodes, prefixVariableKeysRecursive, generateUniqueNodeTitle, findAllChildNodes, findIterateNodePortForPath, updateVariableReferences, updateVariableReferencesInObject,}
export type { UIWorkflowNodeDefinition }