diff --git a/.github/workflows/docker-image-dockerhub.yml b/.github/workflows/docker-image-dockerhub.yml new file mode 100644 index 00000000000..3752ddc7ed4 --- /dev/null +++ b/.github/workflows/docker-image-dockerhub.yml @@ -0,0 +1,72 @@ +name: Docker Image CI - Docker Hub + +on: + workflow_dispatch: + inputs: + node_version: + description: 'Node.js version to build this image with.' + type: choice + required: true + default: '20' + options: + - '20' + tag_version: + description: 'Tag version of the image to be pushed.' + type: string + required: true + default: 'latest' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Set default values + id: defaults + run: | + echo "node_version=${{ github.event.inputs.node_version || '20' }}" >> $GITHUB_OUTPUT + echo "tag_version=${{ github.event.inputs.tag_version || 'latest' }}" >> $GITHUB_OUTPUT + + - name: Checkout + uses: actions/checkout@v4.1.1 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # ------------------------- + # Build and push main image + # ------------------------- + - name: Build and push main image + uses: docker/build-push-action@v5.3.0 + with: + context: . + file: ./docker/Dockerfile + build-args: | + NODE_VERSION=${{ steps.defaults.outputs.node_version }} + platforms: linux/amd64,linux/arm64 + push: true + tags: | + flowiseai/flowise:${{ steps.defaults.outputs.tag_version }} + + # ------------------------- + # Build and push worker image + # ------------------------- + - name: Build and push worker image + uses: docker/build-push-action@v5.3.0 + with: + context: . + file: docker/worker/Dockerfile + build-args: | + NODE_VERSION=${{ steps.defaults.outputs.node_version }} + platforms: linux/amd64,linux/arm64 + push: true + tags: | + flowiseai/flowise-worker:${{ steps.defaults.outputs.tag_version }} diff --git a/.github/workflows/docker-image-ecr.yml b/.github/workflows/docker-image-ecr.yml new file mode 100644 index 00000000000..1fc28fb1d8b --- /dev/null +++ b/.github/workflows/docker-image-ecr.yml @@ -0,0 +1,73 @@ +name: Docker Image CI - AWS ECR + +on: + workflow_dispatch: + inputs: + environment: + description: 'Environment to push the image to.' + required: true + default: 'dev' + type: choice + options: + - dev + - prod + node_version: + description: 'Node.js version to build this image with.' + type: choice + required: true + default: '20' + options: + - '20' + tag_version: + description: 'Tag version of the image to be pushed.' + type: string + required: true + default: 'latest' + +jobs: + docker: + runs-on: ubuntu-latest + environment: ${{ github.event.inputs.environment }} + steps: + - name: Set default values + id: defaults + run: | + echo "node_version=${{ github.event.inputs.node_version || '20' }}" >> $GITHUB_OUTPUT + echo "tag_version=${{ github.event.inputs.tag_version || 'latest' }}" >> $GITHUB_OUTPUT + + - name: Checkout + uses: actions/checkout@v4.1.1 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Login to Amazon ECR + uses: aws-actions/amazon-ecr-login@v1 + + # ------------------------- + # Build and push main image + # ------------------------- + - name: Build and push main image + uses: docker/build-push-action@v5.3.0 + with: + context: . + file: Dockerfile + build-args: | + NODE_VERSION=${{ steps.defaults.outputs.node_version }} + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ${{ format('{0}.dkr.ecr.{1}.amazonaws.com/flowise:{2}', + secrets.AWS_ACCOUNT_ID, + secrets.AWS_REGION, + steps.defaults.outputs.tag_version) }} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml deleted file mode 100644 index 7faf472f59b..00000000000 --- a/.github/workflows/docker-image.yml +++ /dev/null @@ -1,114 +0,0 @@ -name: Docker Image CI - -on: - workflow_dispatch: - inputs: - registry: - description: 'Container Registry to push the image to.' - type: choice - required: true - default: 'aws_ecr' - options: - - 'docker_hub' - - 'aws_ecr' - environment: - description: 'Environment to push the image to.' - required: true - default: 'dev' - type: choice - options: - - dev - - prod - image_type: - description: 'Type of image to build and push.' - type: choice - required: true - default: 'main' - options: - - 'main' - - 'worker' - node_version: - description: 'Node.js version to build this image with.' - type: choice - required: true - default: '20' - options: - - '20' - tag_version: - description: 'Tag version of the image to be pushed.' - type: string - required: true - default: 'latest' - -jobs: - docker: - runs-on: ubuntu-latest - environment: ${{ github.event.inputs.environment }} - steps: - - name: Set default values - id: defaults - run: | - echo "registry=${{ github.event.inputs.registry || 'aws_ecr' }}" >> $GITHUB_OUTPUT - echo "image_type=${{ github.event.inputs.image_type || 'main' }}" >> $GITHUB_OUTPUT - echo "node_version=${{ github.event.inputs.node_version || '20' }}" >> $GITHUB_OUTPUT - echo "tag_version=${{ github.event.inputs.tag_version || 'latest' }}" >> $GITHUB_OUTPUT - - - name: Checkout - uses: actions/checkout@v4.1.1 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3.0.0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.0.0 - - # ------------------------ - # Login Steps (conditional) - # ------------------------ - - name: Login to Docker Hub - if: steps.defaults.outputs.registry == 'docker_hub' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Configure AWS Credentials - if: steps.defaults.outputs.registry == 'aws_ecr' - uses: aws-actions/configure-aws-credentials@v3 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ secrets.AWS_REGION }} - - - name: Login to Amazon ECR - if: steps.defaults.outputs.registry == 'aws_ecr' - uses: aws-actions/amazon-ecr-login@v1 - - # ------------------------- - # Build and push (conditional tags) - # ------------------------- - - name: Build and push - uses: docker/build-push-action@v5.3.0 - with: - context: . - file: | - ${{ - steps.defaults.outputs.image_type == 'worker' && 'docker/worker/Dockerfile' || - (steps.defaults.outputs.registry == 'docker_hub' && './docker/Dockerfile' || 'Dockerfile') - }} - build-args: | - NODE_VERSION=${{ steps.defaults.outputs.node_version }} - platforms: linux/amd64,linux/arm64 - push: true - tags: | - ${{ - steps.defaults.outputs.registry == 'docker_hub' && - format('flowiseai/flowise{0}:{1}', - steps.defaults.outputs.image_type == 'worker' && '-worker' || '', - steps.defaults.outputs.tag_version) || - format('{0}.dkr.ecr.{1}.amazonaws.com/flowise{2}:{3}', - secrets.AWS_ACCOUNT_ID, - secrets.AWS_REGION, - steps.defaults.outputs.image_type == 'worker' && '-worker' || '', - steps.defaults.outputs.tag_version) - }} diff --git a/Dockerfile b/Dockerfile index a824b7f8090..70041f41d47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,34 +5,41 @@ # docker run -d -p 3000:3000 flowise FROM node:20-alpine -RUN apk add --update libc6-compat python3 make g++ -# needed for pdfjs-dist -RUN apk add --no-cache build-base cairo-dev pango-dev -# Install Chromium -RUN apk add --no-cache chromium - -# Install curl for container-level health checks -# Fixes: https://github.com/FlowiseAI/Flowise/issues/4126 -RUN apk add --no-cache curl - -#install PNPM globaly -RUN npm install -g pnpm +# Install system dependencies and build tools +RUN apk update && \ + apk add --no-cache \ + libc6-compat \ + python3 \ + make \ + g++ \ + build-base \ + cairo-dev \ + pango-dev \ + chromium \ + curl && \ + npm install -g pnpm ENV PUPPETEER_SKIP_DOWNLOAD=true ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser ENV NODE_OPTIONS=--max-old-space-size=8192 -WORKDIR /usr/src +WORKDIR /usr/src/flowise # Copy app source COPY . . -RUN pnpm install +# Install dependencies and build +RUN pnpm install && \ + pnpm build + +# Give the node user ownership of the application files +RUN chown -R node:node . -RUN pnpm build +# Switch to non-root user (node user already exists in node:20-alpine) +USER node EXPOSE 3000 -CMD [ "pnpm", "start" ] +CMD [ "pnpm", "start" ] \ No newline at end of file diff --git a/docker/worker/Dockerfile b/docker/worker/Dockerfile index 655b3de4785..8a2c749d44e 100644 --- a/docker/worker/Dockerfile +++ b/docker/worker/Dockerfile @@ -7,7 +7,7 @@ RUN apk add --no-cache build-base cairo-dev pango-dev # Install Chromium and curl for container-level health checks RUN apk add --no-cache chromium curl -#install PNPM globaly +#install PNPM globally RUN npm install -g pnpm ENV PUPPETEER_SKIP_DOWNLOAD=true diff --git a/package.json b/package.json index 08d32de876c..9ee93d127b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "10", + "version": "3.0.11", "private": true, "homepage": "https://flowiseai.com", "workspaces": [ diff --git a/packages/components/nodes/agentflow/Condition/Condition.ts b/packages/components/nodes/agentflow/Condition/Condition.ts index 6913aac1947..7ae1be06291 100644 --- a/packages/components/nodes/agentflow/Condition/Condition.ts +++ b/packages/components/nodes/agentflow/Condition/Condition.ts @@ -317,7 +317,7 @@ class Condition_Agentflow implements INode { } } - // If no condition is fullfilled, add isFulfilled to the ELSE condition + // If no condition is fulfilled, add isFulfilled to the ELSE condition const dummyElseConditionData = { type: 'string', value1: '', diff --git a/packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts b/packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts index 29d1b74e584..5e238410219 100644 --- a/packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts +++ b/packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts @@ -103,7 +103,7 @@ class ChatHuggingFace_ChatModels implements INode { type: 'string', rows: 4, placeholder: 'AI assistant:', - description: 'Sets the stop sequences to use. Use comma to seperate different sequences.', + description: 'Sets the stop sequences to use. Use comma to separate different sequences.', optional: true, additionalParams: true } diff --git a/packages/components/nodes/documentloaders/Json/Json.ts b/packages/components/nodes/documentloaders/Json/Json.ts index f94138a4c0c..042c81ef833 100644 --- a/packages/components/nodes/documentloaders/Json/Json.ts +++ b/packages/components/nodes/documentloaders/Json/Json.ts @@ -47,7 +47,7 @@ class Json_DocumentLoaders implements INode { constructor() { this.label = 'Json File' this.name = 'jsonFile' - this.version = 3.0 + this.version = 3.1 this.type = 'Document' this.icon = 'json.svg' this.category = 'Document Loaders' @@ -66,6 +66,14 @@ class Json_DocumentLoaders implements INode { type: 'TextSplitter', optional: true }, + { + label: 'Separate by JSON Object (JSON Array)', + name: 'separateByObject', + type: 'boolean', + description: 'If enabled and the file is a JSON Array, each JSON object will be extracted as a chunk', + optional: true, + additionalParams: true + }, { label: 'Pointers Extraction (separated by commas)', name: 'pointersName', @@ -73,7 +81,10 @@ class Json_DocumentLoaders implements INode { description: 'Ex: { "key": "value" }, Pointer Extraction = "key", "value" will be extracted as pageContent of the chunk. Use comma to separate multiple pointers', placeholder: 'key1, key2', - optional: true + optional: true, + hide: { + separateByObject: true + } }, { label: 'Additional Metadata', @@ -122,6 +133,7 @@ class Json_DocumentLoaders implements INode { const pointersName = nodeData.inputs?.pointersName as string const metadata = nodeData.inputs?.metadata const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string + const separateByObject = nodeData.inputs?.separateByObject as boolean const output = nodeData.outputs?.output as string let omitMetadataKeys: string[] = [] @@ -153,7 +165,7 @@ class Json_DocumentLoaders implements INode { if (!file) continue const fileData = await getFileFromStorage(file, orgId, chatflowid) const blob = new Blob([fileData]) - const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined, metadata) + const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined, metadata, separateByObject) if (textSplitter) { let splittedDocs = await loader.load() @@ -176,7 +188,7 @@ class Json_DocumentLoaders implements INode { splitDataURI.pop() const bf = Buffer.from(splitDataURI.pop() || '', 'base64') const blob = new Blob([bf]) - const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined, metadata) + const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined, metadata, separateByObject) if (textSplitter) { let splittedDocs = await loader.load() @@ -306,13 +318,20 @@ class TextLoader extends BaseDocumentLoader { class JSONLoader extends TextLoader { public pointers: string[] private metadataMapping: Record - - constructor(filePathOrBlob: string | Blob, pointers: string | string[] = [], metadataMapping: Record = {}) { + private separateByObject: boolean + + constructor( + filePathOrBlob: string | Blob, + pointers: string | string[] = [], + metadataMapping: Record = {}, + separateByObject: boolean = false + ) { super(filePathOrBlob) this.pointers = Array.isArray(pointers) ? pointers : [pointers] if (metadataMapping) { this.metadataMapping = typeof metadataMapping === 'object' ? metadataMapping : JSON.parse(metadataMapping) } + this.separateByObject = separateByObject } protected async parse(raw: string): Promise { @@ -323,14 +342,24 @@ class JSONLoader extends TextLoader { const jsonArray = Array.isArray(json) ? json : [json] for (const item of jsonArray) { - const content = this.extractContent(item) - const metadata = this.extractMetadata(item) - - for (const pageContent of content) { - documents.push({ - pageContent, - metadata - }) + if (this.separateByObject) { + if (typeof item === 'object' && item !== null && !Array.isArray(item)) { + const metadata = this.extractMetadata(item) + const pageContent = this.formatObjectAsKeyValue(item) + documents.push({ + pageContent, + metadata + }) + } + } else { + const content = this.extractContent(item) + const metadata = this.extractMetadata(item) + for (const pageContent of content) { + documents.push({ + pageContent, + metadata + }) + } } } @@ -370,6 +399,30 @@ class JSONLoader extends TextLoader { return metadata } + /** + * Formats a JSON object as readable key-value pairs + */ + private formatObjectAsKeyValue(obj: any, prefix: string = ''): string { + const lines: string[] = [] + + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key + + if (value === null || value === undefined) { + lines.push(`${fullKey}: ${value}`) + } else if (Array.isArray(value)) { + lines.push(`${fullKey}: ${JSON.stringify(value)}`) + } else if (typeof value === 'object') { + // Recursively format nested objects + lines.push(this.formatObjectAsKeyValue(value, fullKey)) + } else { + lines.push(`${fullKey}: ${value}`) + } + } + + return lines.join('\n') + } + /** * If JSON pointers are specified, return all strings below any of them * and exclude all other nodes expect if they match a JSON pointer. diff --git a/packages/components/nodes/documentloaders/Playwright/Playwright.ts b/packages/components/nodes/documentloaders/Playwright/Playwright.ts index c3b090e8b10..f0507459347 100644 --- a/packages/components/nodes/documentloaders/Playwright/Playwright.ts +++ b/packages/components/nodes/documentloaders/Playwright/Playwright.ts @@ -190,11 +190,14 @@ class Playwright_DocumentLoaders implements INode { async function playwrightLoader(url: string): Promise { try { let docs = [] + + const executablePath = process.env.PLAYWRIGHT_EXECUTABLE_PATH + const config: PlaywrightWebBaseLoaderOptions = { launchOptions: { args: ['--no-sandbox'], headless: true, - executablePath: process.env.PLAYWRIGHT_EXECUTABLE_FILE_PATH + executablePath: executablePath } } if (waitUntilGoToOption) { diff --git a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts index 5409ef4f02b..9b4ada91661 100644 --- a/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts +++ b/packages/components/nodes/documentloaders/Puppeteer/Puppeteer.ts @@ -181,11 +181,14 @@ class Puppeteer_DocumentLoaders implements INode { async function puppeteerLoader(url: string): Promise { try { let docs: Document[] = [] + + const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH + const config: PuppeteerWebBaseLoaderOptions = { launchOptions: { args: ['--no-sandbox'], headless: 'new', - executablePath: process.env.PUPPETEER_EXECUTABLE_FILE_PATH + executablePath: executablePath } } if (waitUntilGoToOption) { diff --git a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts index 808b7ef0d76..d1a372b0c94 100644 --- a/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts +++ b/packages/components/nodes/documentloaders/Unstructured/UnstructuredFile.ts @@ -4,15 +4,11 @@ import { UnstructuredLoaderOptions, UnstructuredLoaderStrategy, SkipInferTableTypes, - HiResModelName, - UnstructuredLoader as LCUnstructuredLoader + HiResModelName } from '@langchain/community/document_loaders/fs/unstructured' import { getCredentialData, getCredentialParam, handleEscapeCharacters } from '../../../src/utils' import { getFileFromStorage, INodeOutputsValue } from '../../../src' import { UnstructuredLoader } from './Unstructured' -import { isPathTraversal } from '../../../src/validator' -import sanitize from 'sanitize-filename' -import path from 'path' class UnstructuredFile_DocumentLoaders implements INode { label: string @@ -44,17 +40,6 @@ class UnstructuredFile_DocumentLoaders implements INode { optional: true } this.inputs = [ - /** Deprecated - { - label: 'File Path', - name: 'filePath', - type: 'string', - placeholder: '', - optional: true, - warning: - 'Use the File Upload instead of File path. If file is uploaded, this path is ignored. Path will be deprecated in future releases.' - }, - */ { label: 'Files Upload', name: 'fileObject', @@ -455,7 +440,6 @@ class UnstructuredFile_DocumentLoaders implements INode { } async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { - const filePath = nodeData.inputs?.filePath as string const unstructuredAPIUrl = nodeData.inputs?.unstructuredAPIUrl as string const strategy = nodeData.inputs?.strategy as UnstructuredLoaderStrategy const encoding = nodeData.inputs?.encoding as string @@ -560,37 +544,8 @@ class UnstructuredFile_DocumentLoaders implements INode { docs.push(...loaderDocs) } } - } else if (filePath) { - if (!filePath || typeof filePath !== 'string') { - throw new Error('Invalid file path format') - } - - if (isPathTraversal(filePath)) { - throw new Error('Invalid path characters detected in filePath - path traversal not allowed') - } - - const parsedPath = path.parse(filePath) - const sanitizedFilename = sanitize(parsedPath.base) - - if (!sanitizedFilename || sanitizedFilename.trim() === '') { - throw new Error('Invalid filename after sanitization') - } - - const sanitizedFilePath = path.join(parsedPath.dir, sanitizedFilename) - - if (!path.isAbsolute(sanitizedFilePath)) { - throw new Error('File path must be absolute') - } - - if (sanitizedFilePath.includes('..')) { - throw new Error('Invalid file path - directory traversal not allowed') - } - - const loader = new LCUnstructuredLoader(sanitizedFilePath, obj) - const loaderDocs = await loader.load() - docs.push(...loaderDocs) } else { - throw new Error('File path or File upload is required') + throw new Error('File upload is required') } if (metadata) { diff --git a/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts b/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts index 4946fa8bbc2..5ee12705e38 100644 --- a/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts +++ b/packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts @@ -96,7 +96,7 @@ class AWSBedrockEmbedding_Embeddings implements INode { { label: 'Max AWS API retries', name: 'maxRetries', - description: 'This will limit the nubmer of AWS API for Titan model embeddings call retries. Used to avoid throttling.', + description: 'This will limit the number of AWS API for Titan model embeddings call retries. Used to avoid throttling.', type: 'number', optional: true, default: 5, diff --git a/packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts b/packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts index b19eb2346ae..02862c74045 100644 --- a/packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts +++ b/packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts @@ -39,7 +39,7 @@ class SubQuestionQueryEngine_LlamaIndex implements INode { this.icon = 'subQueryEngine.svg' this.category = 'Engine' this.description = - 'Breaks complex query into sub questions for each relevant data source, then gather all the intermediate reponses and synthesizes a final response' + 'Breaks complex query into sub questions for each relevant data source, then gather all the intermediate responses and synthesizes a final response' this.baseClasses = [this.type, 'BaseQueryEngine'] this.tags = ['LlamaIndex'] this.inputs = [ diff --git a/packages/components/nodes/multiagents/Supervisor/Supervisor.ts b/packages/components/nodes/multiagents/Supervisor/Supervisor.ts index f67abf00b19..2babee9aa02 100644 --- a/packages/components/nodes/multiagents/Supervisor/Supervisor.ts +++ b/packages/components/nodes/multiagents/Supervisor/Supervisor.ts @@ -21,6 +21,7 @@ import { ChatOpenAI } from '../../chatmodels/ChatOpenAI/FlowiseChatOpenAI' import { ChatAnthropic } from '../../chatmodels/ChatAnthropic/FlowiseChatAnthropic' import { addImagesToMessages, llmSupportsVision } from '../../../src/multiModalUtils' import { ChatGoogleGenerativeAI } from '../../chatmodels/ChatGoogleGenerativeAI/FlowiseChatGoogleGenerativeAI' +import { AzureChatOpenAI } from '../../chatmodels/AzureChatOpenAI/FlowiseAzureChatOpenAI' const sysPrompt = `You are a supervisor tasked with managing a conversation between the following workers: {team_members}. Given the following user request, respond with the worker to act next. @@ -242,7 +243,7 @@ class Supervisor_MultiAgents implements INode { } } }) - } else if (llm instanceof ChatOpenAI) { + } else if (llm instanceof ChatOpenAI || llm instanceof AzureChatOpenAI) { let prompt = ChatPromptTemplate.fromMessages([ ['system', systemPrompt], new MessagesPlaceholder('messages'), diff --git a/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts b/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts index b1d1f6dd3c3..9e562962a4a 100644 --- a/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts +++ b/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts @@ -11,7 +11,7 @@ return [ tool_calls: [ { id: "12345", - name: "calulator", + name: "calculator", args: { number1: 333382, number2: 1932, diff --git a/packages/components/nodes/tools/ReadFile/ReadFile.ts b/packages/components/nodes/tools/ReadFile/ReadFile.ts deleted file mode 100644 index eb703a1de90..00000000000 --- a/packages/components/nodes/tools/ReadFile/ReadFile.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { z } from 'zod' -import path from 'path' -import { StructuredTool, ToolParams } from '@langchain/core/tools' -import { Serializable } from '@langchain/core/load/serializable' -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, getUserHome } from '../../../src/utils' -import { SecureFileStore, FileSecurityConfig } from '../../../src/SecureFileStore' - -abstract class BaseFileStore extends Serializable { - abstract readFile(path: string): Promise - abstract writeFile(path: string, contents: string): Promise -} - -class ReadFile_Tools implements INode { - label: string - name: string - version: number - description: string - type: string - icon: string - category: string - baseClasses: string[] - inputs: INodeParams[] - warning: string - - constructor() { - this.label = 'Read File' - this.name = 'readFile' - this.version = 2.0 - this.type = 'ReadFile' - this.icon = 'readfile.svg' - this.category = 'Tools' - this.warning = 'This tool can be used to read files from the disk. It is recommended to use this tool with caution.' - this.description = 'Read file from disk' - this.baseClasses = [this.type, 'Tool', ...getBaseClasses(ReadFileTool)] - this.inputs = [ - { - label: 'Workspace Path', - name: 'workspacePath', - placeholder: `C:\\Users\\User\\MyProject`, - type: 'string', - description: 'Base workspace directory for file operations. All file paths will be relative to this directory.', - optional: true - }, - { - label: 'Enforce Workspace Boundaries', - name: 'enforceWorkspaceBoundaries', - type: 'boolean', - description: 'When enabled, restricts file access to the workspace directory for security. Recommended: true', - default: true, - optional: true - }, - { - label: 'Max File Size (MB)', - name: 'maxFileSize', - type: 'number', - description: 'Maximum file size in megabytes that can be read', - default: 10, - optional: true - }, - { - label: 'Allowed Extensions', - name: 'allowedExtensions', - type: 'string', - description: 'Comma-separated list of allowed file extensions (e.g., .txt,.json,.md). Leave empty to allow all.', - placeholder: '.txt,.json,.md,.py,.js', - optional: true - } - ] - } - - async init(nodeData: INodeData): Promise { - const workspacePath = nodeData.inputs?.workspacePath as string - const enforceWorkspaceBoundaries = nodeData.inputs?.enforceWorkspaceBoundaries !== false // Default to true - const maxFileSize = nodeData.inputs?.maxFileSize as number - const allowedExtensions = nodeData.inputs?.allowedExtensions as string - - // Parse allowed extensions - const allowedExtensionsList = allowedExtensions ? allowedExtensions.split(',').map((ext) => ext.trim().toLowerCase()) : [] - - let store: BaseFileStore - - if (workspacePath) { - // Create secure file store with workspace boundaries - const config: FileSecurityConfig = { - workspacePath, - enforceWorkspaceBoundaries, - maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, // Convert MB to bytes - allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined - } - store = new SecureFileStore(config) - } else { - // Fallback to current working directory with security warnings - if (enforceWorkspaceBoundaries) { - const fallbackWorkspacePath = path.join(getUserHome(), '.flowise') - console.warn(`[ReadFile] No workspace path specified, using ${fallbackWorkspacePath} with security restrictions`) - store = new SecureFileStore({ - workspacePath: fallbackWorkspacePath, - enforceWorkspaceBoundaries: true, - maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, - allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined - }) - } else { - console.warn('[ReadFile] SECURITY WARNING: Workspace boundaries disabled - unrestricted file access enabled') - store = SecureFileStore.createUnsecure() - } - } - - return new ReadFileTool({ store }) - } -} - -interface ReadFileParams extends ToolParams { - store: BaseFileStore -} - -/** - * Class for reading files from the disk. Extends the StructuredTool - * class. - */ -export class ReadFileTool extends StructuredTool { - static lc_name() { - return 'ReadFileTool' - } - - schema = z.object({ - file_path: z.string().describe('name of file') - }) as any - - name = 'read_file' - - description = 'Read file from disk' - - store: BaseFileStore - - constructor({ store }: ReadFileParams) { - super(...arguments) - - this.store = store - } - - async _call({ file_path }: z.infer) { - return await this.store.readFile(file_path) - } -} - -module.exports = { nodeClass: ReadFile_Tools } diff --git a/packages/components/nodes/tools/ReadFile/readfile.svg b/packages/components/nodes/tools/ReadFile/readfile.svg deleted file mode 100644 index c7cba0efa25..00000000000 --- a/packages/components/nodes/tools/ReadFile/readfile.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/components/nodes/tools/WriteFile/WriteFile.ts b/packages/components/nodes/tools/WriteFile/WriteFile.ts deleted file mode 100644 index bc3609bebb5..00000000000 --- a/packages/components/nodes/tools/WriteFile/WriteFile.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { z } from 'zod' -import path from 'path' -import { StructuredTool, ToolParams } from '@langchain/core/tools' -import { Serializable } from '@langchain/core/load/serializable' -import { INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, getUserHome } from '../../../src/utils' -import { SecureFileStore, FileSecurityConfig } from '../../../src/SecureFileStore' - -abstract class BaseFileStore extends Serializable { - abstract readFile(path: string): Promise - abstract writeFile(path: string, contents: string): Promise -} - -class WriteFile_Tools implements INode { - label: string - name: string - version: number - description: string - type: string - icon: string - category: string - baseClasses: string[] - inputs: INodeParams[] - warning: string - - constructor() { - this.label = 'Write File' - this.name = 'writeFile' - this.version = 2.0 - this.type = 'WriteFile' - this.icon = 'writefile.svg' - this.category = 'Tools' - this.warning = 'This tool can be used to write files to the disk. It is recommended to use this tool with caution.' - this.description = 'Write file to disk' - this.baseClasses = [this.type, 'Tool', ...getBaseClasses(WriteFileTool)] - this.inputs = [ - { - label: 'Workspace Path', - name: 'workspacePath', - placeholder: `C:\\Users\\User\\MyProject`, - type: 'string', - description: 'Base workspace directory for file operations. All file paths will be relative to this directory.', - optional: true - }, - { - label: 'Enforce Workspace Boundaries', - name: 'enforceWorkspaceBoundaries', - type: 'boolean', - description: 'When enabled, restricts file access to the workspace directory for security. Recommended: true', - default: true, - optional: true - }, - { - label: 'Max File Size (MB)', - name: 'maxFileSize', - type: 'number', - description: 'Maximum file size in megabytes that can be written', - default: 10, - optional: true - }, - { - label: 'Allowed Extensions', - name: 'allowedExtensions', - type: 'string', - description: 'Comma-separated list of allowed file extensions (e.g., .txt,.json,.md). Leave empty to allow all.', - placeholder: '.txt,.json,.md,.py,.js', - optional: true - } - ] - } - - async init(nodeData: INodeData): Promise { - const workspacePath = nodeData.inputs?.workspacePath as string - const enforceWorkspaceBoundaries = nodeData.inputs?.enforceWorkspaceBoundaries !== false // Default to true - const maxFileSize = nodeData.inputs?.maxFileSize as number - const allowedExtensions = nodeData.inputs?.allowedExtensions as string - - // Parse allowed extensions - const allowedExtensionsList = allowedExtensions ? allowedExtensions.split(',').map((ext) => ext.trim().toLowerCase()) : [] - - let store: BaseFileStore - - if (workspacePath) { - // Create secure file store with workspace boundaries - const config: FileSecurityConfig = { - workspacePath, - enforceWorkspaceBoundaries, - maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, // Convert MB to bytes - allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined - } - store = new SecureFileStore(config) - } else { - // Fallback to current working directory with security warnings - if (enforceWorkspaceBoundaries) { - const fallbackWorkspacePath = path.join(getUserHome(), '.flowise') - console.warn(`[WriteFile] No workspace path specified, using ${fallbackWorkspacePath} with security restrictions`) - store = new SecureFileStore({ - workspacePath: fallbackWorkspacePath, - enforceWorkspaceBoundaries: true, - maxFileSize: maxFileSize ? maxFileSize * 1024 * 1024 : undefined, - allowedExtensions: allowedExtensionsList.length > 0 ? allowedExtensionsList : undefined - }) - } else { - console.warn('[WriteFile] SECURITY WARNING: Workspace boundaries disabled - unrestricted file access enabled') - store = SecureFileStore.createUnsecure() - } - } - - return new WriteFileTool({ store }) - } -} - -interface WriteFileParams extends ToolParams { - store: BaseFileStore -} - -/** - * Class for writing data to files on the disk. Extends the StructuredTool - * class. - */ -export class WriteFileTool extends StructuredTool { - static lc_name() { - return 'WriteFileTool' - } - - schema = z.object({ - file_path: z.string().describe('name of file'), - text: z.string().describe('text to write to file') - }) as any - - name = 'write_file' - - description = 'Write file to disk' - - store: BaseFileStore - - constructor({ store, ...rest }: WriteFileParams) { - super(rest) - - this.store = store - } - - async _call({ file_path, text }: z.infer) { - await this.store.writeFile(file_path, text) - return `File written to ${file_path} successfully.` - } -} - -module.exports = { nodeClass: WriteFile_Tools } diff --git a/packages/components/nodes/tools/WriteFile/writefile.svg b/packages/components/nodes/tools/WriteFile/writefile.svg deleted file mode 100644 index 0df04ea44c3..00000000000 --- a/packages/components/nodes/tools/WriteFile/writefile.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/components/package.json b/packages/components/package.json index 9f1ab13f738..f3894f996c4 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "flowise-components", - "version": "3.0.10", + "version": "3.0.11", "description": "Flowiseai Components", "main": "dist/src/index", "types": "dist/src/index.d.ts", diff --git a/packages/components/src/SecureFileStore.ts b/packages/components/src/SecureFileStore.ts deleted file mode 100644 index 88981ecbff2..00000000000 --- a/packages/components/src/SecureFileStore.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { Serializable } from '@langchain/core/load/serializable' -import { NodeFileStore } from 'langchain/stores/file/node' -import { isUnsafeFilePath, isWithinWorkspace } from './validator' -import * as path from 'path' -import * as fs from 'fs' - -/** - * Security configuration for file operations - */ -export interface FileSecurityConfig { - /** Base workspace path - all file operations are restricted to this directory */ - workspacePath: string - /** Whether to enforce workspace boundaries (default: true) */ - enforceWorkspaceBoundaries?: boolean - /** Maximum file size in bytes (default: 10MB) */ - maxFileSize?: number - /** Allowed file extensions (if empty, all extensions allowed) */ - allowedExtensions?: string[] - /** Blocked file extensions */ - blockedExtensions?: string[] -} - -/** - * Secure file store that enforces workspace boundaries and validates file operations - */ -export class SecureFileStore extends Serializable { - lc_namespace = ['flowise', 'components', 'stores', 'file'] - - private config: Required - private nodeFileStore: NodeFileStore - - constructor(config: FileSecurityConfig) { - super() - - // Set default configuration - this.config = { - workspacePath: config.workspacePath, - enforceWorkspaceBoundaries: config.enforceWorkspaceBoundaries ?? true, - maxFileSize: config.maxFileSize ?? 10 * 1024 * 1024, // 10MB default - allowedExtensions: config.allowedExtensions ?? [], - blockedExtensions: config.blockedExtensions ?? [ - '.exe', - '.bat', - '.cmd', - '.sh', - '.ps1', - '.vbs', - '.scr', - '.com', - '.pif', - '.dll', - '.sys', - '.msi', - '.jar' - ] - } - - // Validate workspace path - if (!this.config.workspacePath || !path.isAbsolute(this.config.workspacePath)) { - throw new Error('Workspace path must be an absolute path') - } - - // Ensure workspace directory exists - if (!fs.existsSync(this.config.workspacePath)) { - throw new Error(`Workspace directory does not exist: ${this.config.workspacePath}`) - } - - // Initialize the underlying NodeFileStore with workspace path - this.nodeFileStore = new NodeFileStore(this.config.workspacePath) - } - - /** - * Validates a file path against security policies - */ - private validateFilePath(filePath: string): void { - // Check for unsafe path patterns - if (isUnsafeFilePath(filePath)) { - throw new Error(`Unsafe file path detected: ${filePath}`) - } - - // Enforce workspace boundaries if enabled - if (this.config.enforceWorkspaceBoundaries) { - if (!isWithinWorkspace(filePath, this.config.workspacePath)) { - throw new Error(`File path outside workspace boundaries: ${filePath}`) - } - } - - // Check file extension - const ext = path.extname(filePath).toLowerCase() - - // Check blocked extensions - if (this.config.blockedExtensions.includes(ext)) { - throw new Error(`File extension not allowed: ${ext}`) - } - - // Check allowed extensions (if specified) - if (this.config.allowedExtensions.length > 0 && !this.config.allowedExtensions.includes(ext)) { - throw new Error(`File extension not in allowed list: ${ext}`) - } - } - - /** - * Validates file size - */ - private validateFileSize(content: string): void { - const sizeInBytes = Buffer.byteLength(content, 'utf8') - if (sizeInBytes > this.config.maxFileSize) { - throw new Error(`File size exceeds maximum allowed size: ${sizeInBytes} > ${this.config.maxFileSize}`) - } - } - - /** - * Reads a file with security validation - */ - async readFile(filePath: string): Promise { - this.validateFilePath(filePath) - - try { - return await this.nodeFileStore.readFile(filePath) - } catch (error) { - // Provide generic error message to avoid information leakage - throw new Error(`Failed to read file: ${path.basename(filePath)}`) - } - } - - /** - * Writes a file with security validation - */ - async writeFile(filePath: string, contents: string): Promise { - this.validateFilePath(filePath) - this.validateFileSize(contents) - - try { - // Ensure the directory exists - const dir = path.dirname(path.resolve(this.config.workspacePath, filePath)) - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }) - } - - await this.nodeFileStore.writeFile(filePath, contents) - } catch (error) { - // Provide generic error message to avoid information leakage - throw new Error(`Failed to write file: ${path.basename(filePath)}`) - } - } - - /** - * Gets the workspace configuration - */ - getConfig(): Readonly> { - return { ...this.config } - } - - /** - * Creates a secure file store with workspace enforcement disabled (for backward compatibility) - * WARNING: This should only be used when absolutely necessary and with proper user consent - */ - static createUnsecure(basePath?: string): SecureFileStore { - const workspacePath = basePath || process.cwd() - return new SecureFileStore({ - workspacePath, - enforceWorkspaceBoundaries: false, - maxFileSize: 50 * 1024 * 1024, // 50MB for insecure mode - blockedExtensions: [] // No extension restrictions in insecure mode - }) - } -} diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 7c526681f5e..fef9adac407 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -1500,9 +1500,29 @@ export const executeJavaScriptCode = async ( const sbx = await Sandbox.create({ apiKey: process.env.E2B_APIKEY, timeoutMs }) + // Determine which libraries to install + const librariesToInstall = new Set(libraries) + + // Auto-detect required libraries from code + // Extract required modules from import/require statements + const importRegex = /(?:import\s+.*?\s+from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g + let match + while ((match = importRegex.exec(code)) !== null) { + const moduleName = match[1] || match[2] + // Extract base module name (e.g., 'typeorm' from 'typeorm/something') + const baseModuleName = moduleName.split('/')[0] + librariesToInstall.add(baseModuleName) + } + // Install libraries - for (const library of libraries) { - await sbx.commands.run(`npm install ${library}`) + for (const library of librariesToInstall) { + // Validate library name to prevent command injection. + const validPackageNameRegex = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/ + if (validPackageNameRegex.test(library)) { + await sbx.commands.run(`npm install ${library}`) + } else { + console.warn(`[Sandbox] Skipping installation of invalid module: ${library}`) + } } // Separate imports from the rest of the code for proper ES6 module structure diff --git a/packages/components/src/validator.ts b/packages/components/src/validator.ts index 26cad65d935..5a72144f0cd 100644 --- a/packages/components/src/validator.ts +++ b/packages/components/src/validator.ts @@ -69,36 +69,3 @@ export const isUnsafeFilePath = (filePath: string): boolean => { return dangerousPatterns.some((pattern) => pattern.test(filePath)) } - -/** - * Validates if a file path is within the allowed workspace boundaries - * @param {string} filePath The file path to validate - * @param {string} workspacePath The workspace base path - * @returns {boolean} True if path is within workspace, false otherwise - */ -export const isWithinWorkspace = (filePath: string, workspacePath: string): boolean => { - if (!filePath || !workspacePath) { - return false - } - - try { - const path = require('path') - - // Resolve both paths to absolute paths - const resolvedFilePath = path.resolve(workspacePath, filePath) - const resolvedWorkspacePath = path.resolve(workspacePath) - - // Normalize paths to handle different separators - const normalizedFilePath = path.normalize(resolvedFilePath) - const normalizedWorkspacePath = path.normalize(resolvedWorkspacePath) - - // Check if the file path starts with the workspace path - const relativePath = path.relative(normalizedWorkspacePath, normalizedFilePath) - - // If relative path starts with '..' or is absolute, it's outside workspace - return !relativePath.startsWith('..') && !path.isAbsolute(relativePath) - } catch (error) { - // If any error occurs during path resolution, deny access - return false - } -} diff --git a/packages/server/README.md b/packages/server/README.md index af29ebea246..c3c6c9a16d6 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -41,7 +41,7 @@ cd Flowise/packages/server pnpm install ./node_modules/.bin/cypress install pnpm build -#Only for writting new tests on local dev -> pnpm run cypress:open +#Only for writing new tests on local dev -> pnpm run cypress:open pnpm run e2e ``` diff --git a/packages/server/marketplaces/agentflowsv2/SQL Agent.json b/packages/server/marketplaces/agentflowsv2/SQL Agent.json index 02af729b82a..70d1bba5cf8 100644 --- a/packages/server/marketplaces/agentflowsv2/SQL Agent.json +++ b/packages/server/marketplaces/agentflowsv2/SQL Agent.json @@ -284,7 +284,7 @@ "inputAnchors": [], "inputs": { "customFunctionInputVariables": "", - "customFunctionJavascriptFunction": "const { DataSource } = require('typeorm');\n\nconst HOST = 'localhost';\nconst USER = 'testuser';\nconst PASSWORD = 'testpwd';\nconst DATABASE = 'abudhabi';\nconst PORT = 5555;\n\nlet sqlSchemaPrompt = '';\n\nconst AppDataSource = new DataSource({\n type: 'postgres',\n host: HOST,\n port: PORT,\n username: USER,\n password: PASSWORD,\n database: DATABASE,\n synchronize: false,\n logging: false,\n});\n\nasync function getSQLPrompt() {\n try {\n await AppDataSource.initialize();\n const queryRunner = AppDataSource.createQueryRunner();\n\n // Get all user-defined tables (excluding system tables)\n const tablesResult = await queryRunner.query(`\n SELECT table_name\n FROM information_schema.tables\n WHERE table_schema = 'public' AND table_type = 'BASE TABLE'\n `);\n\n for (const tableRow of tablesResult) {\n const tableName = tableRow.table_name;\n\n const schemaInfo = await queryRunner.query(`\n SELECT column_name, data_type, is_nullable\n FROM information_schema.columns\n WHERE table_name = '${tableName}'\n `);\n\n const createColumns = [];\n const columnNames = [];\n\n for (const column of schemaInfo) {\n const name = column.column_name;\n const type = column.data_type.toUpperCase();\n const notNull = column.is_nullable === 'NO' ? 'NOT NULL' : '';\n columnNames.push(name);\n createColumns.push(`${name} ${type} ${notNull}`);\n }\n\n const sqlCreateTableQuery = `CREATE TABLE ${tableName} (${createColumns.join(', ')})`;\n const sqlSelectTableQuery = `SELECT * FROM ${tableName} LIMIT 3`;\n\n let allValues = [];\n try {\n const rows = await queryRunner.query(sqlSelectTableQuery);\n\n allValues = rows.map(row =>\n columnNames.map(col => row[col]).join(' ')\n );\n } catch (err) {\n allValues.push('[ERROR FETCHING ROWS]');\n }\n\n sqlSchemaPrompt +=\n sqlCreateTableQuery +\n '\\n' +\n sqlSelectTableQuery +\n '\\n' +\n columnNames.join(' ') +\n '\\n' +\n allValues.join('\\n') +\n '\\n\\n';\n }\n\n await queryRunner.release();\n } catch (err) {\n console.error(err);\n throw err;\n }\n}\n\nasync function main() {\n await getSQLPrompt();\n}\n\nawait main();\n\nreturn sqlSchemaPrompt;\n", + "customFunctionJavascriptFunction": "const { DataSource } = require('typeorm');\nconst { Pool } = require('pg');\n\nconst HOST = 'localhost';\nconst USER = 'testuser';\nconst PASSWORD = 'testpwd';\nconst DATABASE = 'abudhabi';\nconst PORT = 5555;\n\nlet sqlSchemaPrompt = '';\n\nconst AppDataSource = new DataSource({\n type: 'postgres',\n host: HOST,\n port: PORT,\n username: USER,\n password: PASSWORD,\n database: DATABASE,\n synchronize: false,\n logging: false,\n});\n\nasync function getSQLPrompt() {\n try {\n await AppDataSource.initialize();\n const queryRunner = AppDataSource.createQueryRunner();\n\n // Get all user-defined tables (excluding system tables)\n const tablesResult = await queryRunner.query(`\n SELECT table_name\n FROM information_schema.tables\n WHERE table_schema = 'public' AND table_type = 'BASE TABLE'\n `);\n\n for (const tableRow of tablesResult) {\n const tableName = tableRow.table_name;\n\n const schemaInfo = await queryRunner.query(`\n SELECT column_name, data_type, is_nullable\n FROM information_schema.columns\n WHERE table_name = '${tableName}'\n `);\n\n const createColumns = [];\n const columnNames = [];\n\n for (const column of schemaInfo) {\n const name = column.column_name;\n const type = column.data_type.toUpperCase();\n const notNull = column.is_nullable === 'NO' ? 'NOT NULL' : '';\n columnNames.push(name);\n createColumns.push(`${name} ${type} ${notNull}`);\n }\n\n const sqlCreateTableQuery = `CREATE TABLE ${tableName} (${createColumns.join(', ')})`;\n const sqlSelectTableQuery = `SELECT * FROM ${tableName} LIMIT 3`;\n\n let allValues = [];\n try {\n const rows = await queryRunner.query(sqlSelectTableQuery);\n\n allValues = rows.map(row =>\n columnNames.map(col => row[col]).join(' ')\n );\n } catch (err) {\n allValues.push('[ERROR FETCHING ROWS]');\n }\n\n sqlSchemaPrompt +=\n sqlCreateTableQuery +\n '\\n' +\n sqlSelectTableQuery +\n '\\n' +\n columnNames.join(' ') +\n '\\n' +\n allValues.join('\\n') +\n '\\n\\n';\n }\n\n await queryRunner.release();\n } catch (err) {\n console.error(err);\n throw err;\n }\n}\n\nasync function main() {\n await getSQLPrompt();\n}\n\nawait main();\n\nreturn sqlSchemaPrompt;\n", "customFunctionUpdateState": "" }, "outputAnchors": [ @@ -913,7 +913,7 @@ "variableValue": "

{{ $flow.state.sqlQuery }}

" } ], - "customFunctionJavascriptFunction": "const { DataSource } = require('typeorm');\n\n// Configuration\nconst HOST = 'localhost';\nconst USER = 'testuser';\nconst PASSWORD = 'testpwd';\nconst DATABASE = 'abudhabi';\nconst PORT = 5555;\n\nconst sqlQuery = $sqlQuery;\n\nconst AppDataSource = new DataSource({\n type: 'postgres',\n host: HOST,\n port: PORT,\n username: USER,\n password: PASSWORD,\n database: DATABASE,\n synchronize: false,\n logging: false,\n});\n\nlet formattedResult = '';\n\nasync function runSQLQuery(query) {\n try {\n await AppDataSource.initialize();\n const queryRunner = AppDataSource.createQueryRunner();\n\n const rows = await queryRunner.query(query);\n console.log('rows =', rows);\n\n if (rows.length === 0) {\n formattedResult = '[No results returned]';\n } else {\n const columnNames = Object.keys(rows[0]);\n const header = columnNames.join(' ');\n const values = rows.map(row =>\n columnNames.map(col => row[col]).join(' ')\n );\n\n formattedResult = query + '\\n' + header + '\\n' + values.join('\\n');\n }\n\n await queryRunner.release();\n } catch (err) {\n console.error('[ERROR]', err);\n formattedResult = `[Error executing query]: ${err}`;\n }\n\n return formattedResult;\n}\n\nasync function main() {\n formattedResult = await runSQLQuery(sqlQuery);\n}\n\nawait main();\n\nreturn formattedResult;\n", + "customFunctionJavascriptFunction": "const { DataSource } = require('typeorm');\nconst { Pool } = require('pg');\n\n// Configuration\nconst HOST = 'localhost';\nconst USER = 'testuser';\nconst PASSWORD = 'testpwd';\nconst DATABASE = 'abudhabi';\nconst PORT = 5555;\n\nconst sqlQuery = $sqlQuery;\n\nconst AppDataSource = new DataSource({\n type: 'postgres',\n host: HOST,\n port: PORT,\n username: USER,\n password: PASSWORD,\n database: DATABASE,\n synchronize: false,\n logging: false,\n});\n\nlet formattedResult = '';\n\nasync function runSQLQuery(query) {\n try {\n await AppDataSource.initialize();\n const queryRunner = AppDataSource.createQueryRunner();\n\n const rows = await queryRunner.query(query);\n console.log('rows =', rows);\n\n if (rows.length === 0) {\n formattedResult = '[No results returned]';\n } else {\n const columnNames = Object.keys(rows[0]);\n const header = columnNames.join(' ');\n const values = rows.map(row =>\n columnNames.map(col => row[col]).join(' ')\n );\n\n formattedResult = query + '\\n' + header + '\\n' + values.join('\\n');\n }\n\n await queryRunner.release();\n } catch (err) {\n console.error('[ERROR]', err);\n formattedResult = `[Error executing query]: ${err}`;\n }\n\n return formattedResult;\n}\n\nasync function main() {\n formattedResult = await runSQLQuery(sqlQuery);\n}\n\nawait main();\n\nreturn formattedResult;\n", "customFunctionUpdateState": "" }, "outputAnchors": [ diff --git a/packages/server/package.json b/packages/server/package.json index 50fc00d03fc..0427f186c99 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "flowise", - "version": "3.0.10", + "version": "3.0.11", "description": "Flowiseai Server", "main": "dist/index", "types": "dist/index.d.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 6d1edef7ede..75e532fcd1f 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "flowise-ui", - "version": "3.0.10", + "version": "3.0.11", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://flowiseai.com", "author": { diff --git a/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx b/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx index f965e0e72f4..054f409c9e7 100644 --- a/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx +++ b/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx @@ -58,7 +58,7 @@ const NavGroup = ({ item }) => { const renderNonPrimaryGroups = () => { let nonprimaryGroups = item.children.filter((child) => child.id !== 'primary') - // Display chilren based on permission and display + // Display children based on permission and display nonprimaryGroups = nonprimaryGroups.map((group) => { const children = group.children.filter((menu) => shouldDisplayMenu(menu)) return { ...group, children }