diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 0a747d0f4..94c929492 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,17 +1,13 @@
name: Node CI
-
on:
push:
branches:
- main
-
pull_request:
branches:
- '*'
-
permissions:
contents: read
-
jobs:
build:
strategy:
@@ -31,12 +27,22 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
check-latest: false
- cache: 'pnpm'
-
- run: npm i -g pnpm
-
- run: pnpm install
-
+ - run: ./node_modules/.bin/cypress install
- run: pnpm lint
-
- run: pnpm build
+ - name: Install dependencies
+ uses: cypress-io/github-action@v6
+ with:
+ working-directory: ./
+ runTests: false
+ - name: Cypress test
+ uses: cypress-io/github-action@v6
+ with:
+ install: false
+ working-directory: packages/server
+ start: pnpm start
+ wait-on: 'http://localhost:3000'
+ wait-on-timeout: 120
+ browser: chrome
diff --git a/packages/server/README.md b/packages/server/README.md
index 049e72617..0b93b6ac2 100644
--- a/packages/server/README.md
+++ b/packages/server/README.md
@@ -41,6 +41,19 @@ You can also specify the env variables when using `npx`. For example:
npx flowise start --PORT=3000 --DEBUG=true
```
+## 📖 Tests
+
+We use [Cypress](https://github.com/cypress-io) for our e2e testing. If you want to run the test suite in dev mode please follow this guide:
+
+```sh
+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
+pnpm run e2e
+```
+
## 📖 Documentation
[Flowise Docs](https://docs.flowiseai.com/)
diff --git a/packages/server/cypress.config.ts b/packages/server/cypress.config.ts
new file mode 100644
index 000000000..caa7a0121
--- /dev/null
+++ b/packages/server/cypress.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from 'cypress'
+
+export default defineConfig({
+ e2e: {
+ setupNodeEvents() {
+ // implement node event listeners here
+ }
+ }
+})
diff --git a/packages/server/cypress/e2e/1-apikey/apikey.cy.js b/packages/server/cypress/e2e/1-apikey/apikey.cy.js
new file mode 100644
index 000000000..2ce7a2bbc
--- /dev/null
+++ b/packages/server/cypress/e2e/1-apikey/apikey.cy.js
@@ -0,0 +1,45 @@
+describe('E2E suite for api/v1/apikey API endpoint', () => {
+ beforeEach(() => {
+ cy.visit('http://localhost:3000/apikey')
+ })
+
+ // DEFAULT TEST ON PAGE LOAD
+ it('displays 1 api key by default', () => {
+ cy.get('table.MuiTable-root tbody tr').should('have.length', 1)
+ cy.get('table.MuiTable-root tbody tr td').first().should('have.text', 'DefaultKey')
+ })
+
+ // CREATE
+ it('can add new api key', () => {
+ const newApiKeyItem = 'MafiKey'
+ cy.get('#btn_createApiKey').click()
+ cy.get('#keyName').type(`${newApiKeyItem}`)
+ cy.get('#btn_confirmAddingApiKey').click()
+ cy.get('table.MuiTable-root tbody tr').should('have.length', 2)
+ cy.get('table.MuiTable-root tbody tr').last().find('td').first().should('have.text', newApiKeyItem)
+ })
+
+ // READ
+ it('can retrieve all api keys', () => {
+ cy.get('table.MuiTable-root tbody tr').should('have.length', 2)
+ cy.get('table.MuiTable-root tbody tr').first().find('td').first().should('have.text', 'DefaultKey')
+ cy.get('table.MuiTable-root tbody tr').last().find('td').first().should('have.text', 'MafiKey')
+ })
+
+ // UPDATE
+ it('can update new api key', () => {
+ const UpdatedApiKeyItem = 'UpsertCloudKey'
+ cy.get('table.MuiTable-root tbody tr').last().find('td').eq(4).find('button').click()
+ cy.get('#keyName').clear().type(`${UpdatedApiKeyItem}`)
+ cy.get('#btn_confirmEditingApiKey').click()
+ cy.get('table.MuiTable-root tbody tr').should('have.length', 2)
+ cy.get('table.MuiTable-root tbody tr').last().find('td').first().should('have.text', UpdatedApiKeyItem)
+ })
+
+ // DELETE
+ it('can delete new api key', () => {
+ cy.get('table.MuiTable-root tbody tr').last().find('td').eq(5).find('button').click()
+ cy.get('.MuiDialog-scrollPaper .MuiDialogActions-spacing button').last().click()
+ cy.get('table.MuiTable-root tbody tr').should('have.length', 1)
+ })
+})
diff --git a/packages/server/cypress/e2e/2-variables/variables.cy.js b/packages/server/cypress/e2e/2-variables/variables.cy.js
new file mode 100644
index 000000000..bcb1ec5c4
--- /dev/null
+++ b/packages/server/cypress/e2e/2-variables/variables.cy.js
@@ -0,0 +1,49 @@
+describe('E2E suite for api/v1/variables API endpoint', () => {
+ beforeEach(() => {
+ cy.visit('http://localhost:3000/variables')
+ })
+
+ // DEFAULT TEST ON PAGE LOAD
+ it('displays no variables by default', () => {
+ cy.get('.MuiCardContent-root .MuiStack-root').last().find('div').last().should('have.text', 'No Variables Yet')
+ })
+
+ // CREATE
+ it('can add new variable', () => {
+ const newVariableName = 'MafiVariable'
+ const newVariableValue = 'shh!!! secret value'
+ cy.get('#btn_createVariable').click()
+ cy.get('#txtInput_variableName').type(`${newVariableName}`)
+ cy.get('#txtInput_variableValue').type(`${newVariableValue}`)
+ cy.get('.MuiDialogActions-spacing button').click()
+ cy.get('.MuiTable-root tbody tr').should('have.length', 1)
+ cy.get('.MuiTable-root tbody tr').last().find('th').first().find('div').first().should('have.text', newVariableName)
+ })
+
+ // READ
+ it('can retrieve all api keys', () => {
+ const newVariableName = 'MafiVariable'
+ cy.get('.MuiTable-root tbody tr').should('have.length', 1)
+ cy.get('.MuiTable-root tbody tr').last().find('th').first().find('div').first().should('have.text', newVariableName)
+ })
+
+ // UPDATE
+ it('can update new api key', () => {
+ const updatedVariableName = 'PichiVariable'
+ const updatedVariableValue = 'silence shh! value'
+ cy.get('.MuiTable-root tbody tr').last().find('td').eq(4).find('button').click()
+ cy.get('#txtInput_variableName').clear().type(`${updatedVariableName}`)
+ cy.get('#txtInput_variableValue').clear().type(`${updatedVariableValue}`)
+ cy.get('.MuiDialogActions-spacing button').click()
+ cy.get('.MuiTable-root tbody tr').should('have.length', 1)
+ cy.get('.MuiTable-root tbody tr').last().find('th').first().find('div').first().should('have.text', updatedVariableName)
+ })
+
+ // DELETE
+ it('can delete new api key', () => {
+ cy.get('.MuiTable-root tbody tr').last().find('td').eq(5).find('button').click()
+ cy.get('.MuiDialog-scrollPaper .MuiDialogActions-spacing button').last().click()
+ cy.get('.MuiTable-root tbody tr').should('have.length', 0)
+ cy.get('.MuiCardContent-root .MuiStack-root').last().find('div').last().should('have.text', 'No Variables Yet')
+ })
+})
diff --git a/packages/server/cypress/fixtures/.keep b/packages/server/cypress/fixtures/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/server/cypress/support/commands.ts b/packages/server/cypress/support/commands.ts
new file mode 100644
index 000000000..95857aea4
--- /dev/null
+++ b/packages/server/cypress/support/commands.ts
@@ -0,0 +1,37 @@
+///
+// ***********************************************
+// This example commands.ts shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
+//
+// declare global {
+// namespace Cypress {
+// interface Chainable {
+// login(email: string, password: string): Chainable
+// drag(subject: string, options?: Partial): Chainable
+// dismiss(subject: string, options?: Partial): Chainable
+// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable
+// }
+// }
+// }
diff --git a/packages/server/cypress/support/e2e.ts b/packages/server/cypress/support/e2e.ts
new file mode 100644
index 000000000..ed5730de1
--- /dev/null
+++ b/packages/server/cypress/support/e2e.ts
@@ -0,0 +1,20 @@
+// ***********************************************************
+// This example support/e2e.ts is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
diff --git a/packages/server/package.json b/packages/server/package.json
index 1fe74eb59..860f5d8fb 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -36,7 +36,11 @@
"typeorm:migration-generate": "pnpm typeorm migration:generate -d ./src/utils/typeormDataSource.ts",
"typeorm:migration-run": "pnpm typeorm migration:run -d ./src/utils/typeormDataSource.ts",
"watch": "tsc --watch",
- "version": "oclif readme && git add README.md"
+ "version": "oclif readme && git add README.md",
+ "cypress:open": "cypress open",
+ "cypress:run": "cypress run",
+ "e2e": "start-server-and-test dev http://localhost:3000 cypress:run",
+ "cypress:ci": "START_SERVER_AND_TEST_INSECURE=1 start-server-and-test start https-get://localhost:3000 cypress:run"
},
"keywords": [],
"homepage": "https://flowiseai.com",
@@ -63,6 +67,8 @@
"express-rate-limit": "^6.9.0",
"flowise-components": "workspace:^",
"flowise-ui": "workspace:^",
+ "http-errors": "^2.0.0",
+ "http-status-codes": "^2.3.0",
"lodash": "^4.17.21",
"moment": "^2.29.3",
"moment-timezone": "^0.5.34",
@@ -86,11 +92,13 @@
"@types/multer": "^1.4.7",
"@types/sanitize-html": "^2.9.5",
"concurrently": "^7.1.0",
+ "cypress": "^13.7.1",
"nodemon": "^2.0.22",
"oclif": "^3",
"rimraf": "^5.0.5",
"run-script-os": "^1.1.6",
"shx": "^0.3.3",
+ "start-server-and-test": "^2.0.3",
"ts-node": "^10.7.0",
"tsc-watch": "^6.0.4",
"typescript": "^4.8.4"
diff --git a/packages/server/src/controllers/apikey/index.ts b/packages/server/src/controllers/apikey/index.ts
new file mode 100644
index 000000000..c35a69f8b
--- /dev/null
+++ b/packages/server/src/controllers/apikey/index.ts
@@ -0,0 +1,79 @@
+import { Request, Response, NextFunction } from 'express'
+import { StatusCodes } from 'http-status-codes'
+import { ApiError } from '../../errors/apiError'
+import apikeyService from '../../services/apikey'
+
+// Get api keys
+const getAllApiKeys = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const apiResponse = await apikeyService.getAllApiKeys()
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const createApiKey = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.body.keyName === 'undefined' || req.body.keyName === '') {
+ throw new ApiError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.createApiKey - keyName not provided!`)
+ }
+ const apiResponse = await apikeyService.createApiKey(req.body.keyName)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+// Update api key
+const updateApiKey = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ new ApiError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.updateApiKey - id not provided!`)
+ }
+ if (typeof req.body.keyName === 'undefined' || req.body.keyName === '') {
+ new ApiError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.updateApiKey - keyName not provided!`)
+ }
+ const apiResponse = await apikeyService.updateApiKey(req.params.id, req.body.keyName)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+// Delete api key
+const deleteApiKey = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ new ApiError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.deleteApiKey - id not provided!`)
+ }
+ const apiResponse = await apikeyService.deleteApiKey(req.params.id)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+// Verify api key
+const verifyApiKey = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.apiKey === 'undefined' || req.params.apiKey === '') {
+ new ApiError(StatusCodes.PRECONDITION_FAILED, `Error: apikeyController.verifyApiKey - apiKey not provided!`)
+ }
+ const apiResponse = await apikeyService.verifyApiKey(req.params.apiKey)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ createApiKey,
+ deleteApiKey,
+ getAllApiKeys,
+ updateApiKey,
+ verifyApiKey
+}
diff --git a/packages/server/src/controllers/assistants/index.ts b/packages/server/src/controllers/assistants/index.ts
new file mode 100644
index 000000000..eed007520
--- /dev/null
+++ b/packages/server/src/controllers/assistants/index.ts
@@ -0,0 +1,85 @@
+import { Request, Response, NextFunction } from 'express'
+import assistantsService from '../../services/assistants'
+
+const creatAssistant = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.body === 'undefined' || req.body === '') {
+ throw new Error(`Error: assistantsController.creatAssistant - body not provided!`)
+ }
+ const apiResponse = await assistantsService.creatAssistant(req.body)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const deleteAssistant = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: assistantsController.deleteAssistant - id not provided!`)
+ }
+ const apiResponse = await assistantsService.deleteAssistant(req.params.id, req.query.isDeleteBoth)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getAllAssistants = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const apiResponse = await assistantsService.getAllAssistants()
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getAssistantById = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: assistantsController.getAssistantById - id not provided!`)
+ }
+ const apiResponse = await assistantsService.getAssistantById(req.params.id)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const updateAssistant = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: assistantsController.updateAssistant - id not provided!`)
+ }
+ if (typeof req.body === 'undefined' || req.body === '') {
+ throw new Error(`Error: assistantsController.updateAssistant - body not provided!`)
+ }
+ const apiResponse = await assistantsService.updateAssistant(req.params.id, req.body)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ creatAssistant,
+ deleteAssistant,
+ getAllAssistants,
+ getAssistantById,
+ updateAssistant
+}
diff --git a/packages/server/src/controllers/chat-messages/index.ts b/packages/server/src/controllers/chat-messages/index.ts
new file mode 100644
index 000000000..02562b3a6
--- /dev/null
+++ b/packages/server/src/controllers/chat-messages/index.ts
@@ -0,0 +1,156 @@
+import { Request, Response, NextFunction } from 'express'
+import { chatType, IReactFlowObject } from '../../Interface'
+import chatflowsService from '../../services/chatflows'
+import chatMessagesService from '../../services/chat-messages'
+import { clearSessionMemory } from '../../utils'
+import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
+import { FindOptionsWhere } from 'typeorm'
+import { ChatMessage } from '../../database/entities/ChatMessage'
+
+const createChatMessage = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.body === 'undefined' || req.body === '') {
+ throw new Error('Error: chatMessagesController.createChatMessage - request body not provided!')
+ }
+ const apiResponse = await chatMessagesService.createChatMessage(req.body)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getAllChatMessages = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ let chatTypeFilter = req.query?.chatType as chatType | undefined
+ if (chatTypeFilter) {
+ try {
+ const chatTypeFilterArray = JSON.parse(chatTypeFilter)
+ if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) {
+ chatTypeFilter = undefined
+ } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) {
+ chatTypeFilter = chatType.EXTERNAL
+ } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) {
+ chatTypeFilter = chatType.INTERNAL
+ }
+ } catch (e) {
+ return res.status(500).send(e)
+ }
+ }
+ const sortOrder = req.query?.order as string | undefined
+ const chatId = req.query?.chatId as string | undefined
+ const memoryType = req.query?.memoryType as string | undefined
+ const sessionId = req.query?.sessionId as string | undefined
+ const messageId = req.query?.messageId as string | undefined
+ const startDate = req.query?.startDate as string | undefined
+ const endDate = req.query?.endDate as string | undefined
+ const feedback = req.query?.feedback as boolean | undefined
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: chatMessageController.getAllChatMessages - id not provided!`)
+ }
+ const apiResponse = await chatMessagesService.getAllChatMessages(
+ req.params.id,
+ chatTypeFilter,
+ sortOrder,
+ chatId,
+ memoryType,
+ sessionId,
+ startDate,
+ endDate,
+ messageId,
+ feedback
+ )
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getAllInternalChatMessages = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const sortOrder = req.query?.order as string | undefined
+ const chatId = req.query?.chatId as string | undefined
+ const memoryType = req.query?.memoryType as string | undefined
+ const sessionId = req.query?.sessionId as string | undefined
+ const messageId = req.query?.messageId as string | undefined
+ const startDate = req.query?.startDate as string | undefined
+ const endDate = req.query?.endDate as string | undefined
+ const feedback = req.query?.feedback as boolean | undefined
+ const apiResponse = await chatMessagesService.getAllInternalChatMessages(
+ req.params.id,
+ chatType.INTERNAL,
+ sortOrder,
+ chatId,
+ memoryType,
+ sessionId,
+ startDate,
+ endDate,
+ messageId,
+ feedback
+ )
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+//Delete all chatmessages from chatId
+const removeAllChatMessages = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const appServer = getRunningExpressApp()
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error('Error: chatMessagesController.removeAllChatMessages - id not provided!')
+ }
+ const chatflowid = req.params.id
+ const chatflow = await chatflowsService.getChatflowById(req.params.id)
+ if (!chatflow) {
+ return res.status(404).send(`Chatflow ${req.params.id} not found`)
+ }
+ const chatId = req.query?.chatId as string
+ const memoryType = req.query?.memoryType as string | undefined
+ const sessionId = req.query?.sessionId as string | undefined
+ const chatType = req.query?.chatType as string | undefined
+ const isClearFromViewMessageDialog = req.query?.isClearFromViewMessageDialog as string | undefined
+ const flowData = chatflow.flowData
+ const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
+ const nodes = parsedFlowData.nodes
+ try {
+ await clearSessionMemory(
+ nodes,
+ appServer.nodesPool.componentNodes,
+ chatId,
+ appServer.AppDataSource,
+ sessionId,
+ memoryType,
+ isClearFromViewMessageDialog
+ )
+ } catch (e) {
+ return res.status(500).send('Error clearing chat messages')
+ }
+
+ const deleteOptions: FindOptionsWhere = { chatflowid }
+ if (chatId) deleteOptions.chatId = chatId
+ if (memoryType) deleteOptions.memoryType = memoryType
+ if (sessionId) deleteOptions.sessionId = sessionId
+ if (chatType) deleteOptions.chatType = chatType
+ const apiResponse = await chatMessagesService.removeAllChatMessages(chatId, chatflowid, deleteOptions)
+ if (apiResponse.executionError) {
+ res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ createChatMessage,
+ getAllChatMessages,
+ getAllInternalChatMessages,
+ removeAllChatMessages
+}
diff --git a/packages/server/src/controllers/chatflows/index.ts b/packages/server/src/controllers/chatflows/index.ts
new file mode 100644
index 000000000..b3c6bd50e
--- /dev/null
+++ b/packages/server/src/controllers/chatflows/index.ts
@@ -0,0 +1,170 @@
+import { Request, Response, NextFunction } from 'express'
+import chatflowsService from '../../services/chatflows'
+import { ChatFlow } from '../../database/entities/ChatFlow'
+import { createRateLimiter } from '../../utils/rateLimit'
+import { getApiKey } from '../../utils/apiKey'
+
+const checkIfChatflowIsValidForStreaming = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: chatflowsRouter.checkIfChatflowIsValidForStreaming - id not provided!`)
+ }
+ const apiResponse = await chatflowsService.checkIfChatflowIsValidForStreaming(req.params.id)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const checkIfChatflowIsValidForUploads = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: chatflowsRouter.checkIfChatflowIsValidForUploads - id not provided!`)
+ }
+ const apiResponse = await chatflowsService.checkIfChatflowIsValidForUploads(req.params.id)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const deleteChatflow = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: chatflowsRouter.deleteChatflow - id not provided!`)
+ }
+ const apiResponse = await chatflowsService.deleteChatflow(req.params.id)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getAllChatflows = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const apiResponse = await chatflowsService.getAllChatflows()
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+// Get specific chatflow via api key
+const getChatflowByApiKey = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.apiKey === 'undefined' || req.params.apiKey === '') {
+ throw new Error(`Error: chatflowsRouter.getChatflowById - apiKey not provided!`)
+ }
+ const apiKey = await getApiKey(req.params.apiKey)
+ if (!apiKey) {
+ return res.status(401).send('Unauthorized')
+ }
+ const apiResponse = await chatflowsService.getChatflowByApiKey(apiKey.id)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getChatflowById = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: chatflowsRouter.getChatflowById - id not provided!`)
+ }
+ const apiResponse = await chatflowsService.getChatflowById(req.params.id)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const saveChatflow = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.body === 'undefined' || req.body === '') {
+ throw new Error(`Error: chatflowsRouter.saveChatflow - body not provided!`)
+ }
+ const body = req.body
+ const newChatFlow = new ChatFlow()
+ Object.assign(newChatFlow, body)
+ const apiResponse = await chatflowsService.saveChatflow(newChatFlow)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const updateChatflow = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: chatflowsRouter.updateChatflow - id not provided!`)
+ }
+ const chatflow = await chatflowsService.getChatflowById(req.params.id)
+ if (!chatflow) {
+ return res.status(404).send(`Chatflow ${req.params.id} not found`)
+ }
+ const body = req.body
+ const updateChatFlow = new ChatFlow()
+ Object.assign(updateChatFlow, body)
+ updateChatFlow.id = chatflow.id
+ createRateLimiter(updateChatFlow)
+ const apiResponse = await chatflowsService.updateChatflow(chatflow, updateChatFlow)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getSinglePublicChatflow = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: chatflowsRouter.updateChatflow - id not provided!`)
+ }
+ const apiResponse = await chatflowsService.getSinglePublicChatflow(req.params.id)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getSinglePublicChatbotConfig = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: chatflowsRouter.getSinglePublicChatbotConfig - id not provided!`)
+ }
+ const apiResponse = await chatflowsService.getSinglePublicChatbotConfig(req.params.id)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ checkIfChatflowIsValidForStreaming,
+ checkIfChatflowIsValidForUploads,
+ deleteChatflow,
+ getAllChatflows,
+ getChatflowByApiKey,
+ getChatflowById,
+ saveChatflow,
+ updateChatflow,
+ getSinglePublicChatflow,
+ getSinglePublicChatbotConfig
+}
diff --git a/packages/server/src/controllers/components-credentials/index.ts b/packages/server/src/controllers/components-credentials/index.ts
new file mode 100644
index 000000000..5f16aa464
--- /dev/null
+++ b/packages/server/src/controllers/components-credentials/index.ts
@@ -0,0 +1,44 @@
+import { Request, Response, NextFunction } from 'express'
+import componentsCredentialsService from '../../services/components-credentials'
+
+// Get all component credentials
+const getAllComponentsCredentials = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const apiResponse = await componentsCredentialsService.getAllComponentsCredentials()
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+// Get component credential via name
+const getComponentByName = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.name === 'undefined' || req.params.name === '') {
+ throw new Error(`Error: componentsCredentialsController.getComponentByName - name not provided!`)
+ }
+ const apiResponse = await componentsCredentialsService.getComponentByName(req.params.name)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+// Returns specific component credential icon via name
+const getSingleComponentsCredentialIcon = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.name === 'undefined' || req.params.name === '') {
+ throw new Error(`Error: componentsCredentialsController.getSingleComponentsCredentialIcon - name not provided!`)
+ }
+ const apiResponse = await componentsCredentialsService.getSingleComponentsCredentialIcon(req.params.name)
+ return res.sendFile(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ getAllComponentsCredentials,
+ getComponentByName,
+ getSingleComponentsCredentialIcon
+}
diff --git a/packages/server/src/controllers/credentials/index.ts b/packages/server/src/controllers/credentials/index.ts
new file mode 100644
index 000000000..2bc599a2f
--- /dev/null
+++ b/packages/server/src/controllers/credentials/index.ts
@@ -0,0 +1,79 @@
+import { Request, Response, NextFunction } from 'express'
+import credentialsService from '../../services/credentials'
+
+const createCredential = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.body === 'undefined' || req.body === '') {
+ throw new Error(`Error: credentialsController.createCredential - body not provided!`)
+ }
+ const apiResponse = await credentialsService.createCredential(req.body)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const deleteCredentials = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: credentialsController.deleteCredentials - id not provided!`)
+ }
+ const apiResponse = await credentialsService.deleteCredentials(req.params.id)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getAllCredentials = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const apiResponse = await credentialsService.getAllCredentials(req.query.credentialName)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getCredentialById = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: credentialsController.getCredentialById - id not provided!`)
+ }
+ const apiResponse = await credentialsService.getCredentialById(req.params.id)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const updateCredential = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: credentialsController.updateCredential - id not provided!`)
+ }
+ if (typeof req.body === 'undefined' || req.body === '') {
+ throw new Error(`Error: credentialsController.updateCredential - body not provided!`)
+ }
+ const apiResponse = await credentialsService.updateCredential(req.params.id, req.body)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ createCredential,
+ deleteCredentials,
+ getAllCredentials,
+ getCredentialById,
+ updateCredential
+}
diff --git a/packages/server/src/controllers/feedback/index.ts b/packages/server/src/controllers/feedback/index.ts
new file mode 100644
index 000000000..08c95aff0
--- /dev/null
+++ b/packages/server/src/controllers/feedback/index.ts
@@ -0,0 +1,52 @@
+import { Request, Response, NextFunction } from 'express'
+import feedbackService from '../../services/feedback'
+
+const getAllChatMessageFeedback = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: feedbackController.getAllChatMessageFeedback - id not provided!`)
+ }
+ const chatflowid = req.params.id
+ const chatId = req.query?.chatId as string | undefined
+ const sortOrder = req.query?.order as string | undefined
+ const startDate = req.query?.startDate as string | undefined
+ const endDate = req.query?.endDate as string | undefined
+ const apiResponse = await feedbackService.getAllChatMessageFeedback(chatflowid, chatId, sortOrder, startDate, endDate)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const createChatMessageFeedbackForChatflow = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.body === 'undefined' || req.body === '') {
+ throw new Error(`Error: feedbackController.createChatMessageFeedbackForChatflow - body not provided!`)
+ }
+ const apiResponse = await feedbackService.createChatMessageFeedbackForChatflow(req.body)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const updateChatMessageFeedbackForChatflow = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.body === 'undefined' || req.body === '') {
+ throw new Error(`Error: feedbackController.updateChatMessageFeedbackForChatflow - body not provided!`)
+ }
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: feedbackController.updateChatMessageFeedbackForChatflow - id not provided!`)
+ }
+ const apiResponse = await feedbackService.updateChatMessageFeedbackForChatflow(req.params.id, req.body)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ getAllChatMessageFeedback,
+ createChatMessageFeedbackForChatflow,
+ updateChatMessageFeedbackForChatflow
+}
diff --git a/packages/server/src/controllers/fetch-links/index.ts b/packages/server/src/controllers/fetch-links/index.ts
new file mode 100644
index 000000000..586f1159e
--- /dev/null
+++ b/packages/server/src/controllers/fetch-links/index.ts
@@ -0,0 +1,31 @@
+import { Request, Response, NextFunction } from 'express'
+import fetchLinksService from '../../services/fetch-links'
+
+const getAllLinks = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.query.url === 'undefined' || req.query.url === '') {
+ throw new Error(`Error: fetchLinksController.getAllLinks - url not provided!`)
+ }
+ if (typeof req.query.relativeLinksMethod === 'undefined' || req.query.relativeLinksMethod === '') {
+ throw new Error(`Error: fetchLinksController.getAllLinks - relativeLinksMethod not provided!`)
+ }
+ if (typeof req.query.limit === 'undefined' || req.query.limit === '') {
+ throw new Error(`Error: fetchLinksController.getAllLinks - limit not provided!`)
+ }
+ const apiResponse = await fetchLinksService.getAllLinks(
+ req.query.url as string,
+ req.query.relativeLinksMethod as string,
+ req.query.limit as string
+ )
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ getAllLinks
+}
diff --git a/packages/server/src/controllers/flow-configs/index.ts b/packages/server/src/controllers/flow-configs/index.ts
new file mode 100644
index 000000000..ce5764976
--- /dev/null
+++ b/packages/server/src/controllers/flow-configs/index.ts
@@ -0,0 +1,21 @@
+import { Request, Response, NextFunction } from 'express'
+import flowConfigsService from '../../services/flow-configs'
+
+const getSingleFlowConfig = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: flowConfigsController.getSingleFlowConfig - id not provided!`)
+ }
+ const apiResponse = await flowConfigsService.getSingleFlowConfig(req.params.id)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ getSingleFlowConfig
+}
diff --git a/packages/server/src/controllers/get-upload-file/index.ts b/packages/server/src/controllers/get-upload-file/index.ts
new file mode 100644
index 000000000..773f08add
--- /dev/null
+++ b/packages/server/src/controllers/get-upload-file/index.ts
@@ -0,0 +1,37 @@
+import { Request, Response, NextFunction } from 'express'
+import path from 'path'
+import contentDisposition from 'content-disposition'
+import { getStoragePath } from 'flowise-components'
+import * as fs from 'fs'
+
+const streamUploadedImage = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (!req.query.chatflowId || !req.query.chatId || !req.query.fileName) {
+ return res.status(500).send(`Invalid file path`)
+ }
+ const chatflowId = req.query.chatflowId as string
+ const chatId = req.query.chatId as string
+ const fileName = req.query.fileName as string
+ const filePath = path.join(getStoragePath(), chatflowId, chatId, fileName)
+ //raise error if file path is not absolute
+ if (!path.isAbsolute(filePath)) return res.status(500).send(`Invalid file path`)
+ //raise error if file path contains '..'
+ if (filePath.includes('..')) return res.status(500).send(`Invalid file path`)
+ //only return from the storage folder
+ if (!filePath.startsWith(getStoragePath())) return res.status(500).send(`Invalid file path`)
+
+ if (fs.existsSync(filePath)) {
+ res.setHeader('Content-Disposition', contentDisposition(path.basename(filePath)))
+ const fileStream = fs.createReadStream(filePath)
+ fileStream.pipe(res)
+ } else {
+ return res.status(404).send(`File ${fileName} not found`)
+ }
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ streamUploadedImage
+}
diff --git a/packages/server/src/controllers/get-upload-path/index.ts b/packages/server/src/controllers/get-upload-path/index.ts
new file mode 100644
index 000000000..05e76591f
--- /dev/null
+++ b/packages/server/src/controllers/get-upload-path/index.ts
@@ -0,0 +1,17 @@
+import { Request, Response, NextFunction } from 'express'
+import { getStoragePath } from 'flowise-components'
+
+const getPathForUploads = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const apiResponse = {
+ storagePath: getStoragePath()
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ getPathForUploads
+}
diff --git a/packages/server/src/controllers/internal-predictions/index.ts b/packages/server/src/controllers/internal-predictions/index.ts
new file mode 100644
index 000000000..1fea952d7
--- /dev/null
+++ b/packages/server/src/controllers/internal-predictions/index.ts
@@ -0,0 +1,16 @@
+import { Request, Response, NextFunction } from 'express'
+import { utilBuildChatflow } from '../../utils/buildChatflow'
+
+// Send input message and get prediction result (Internal)
+const createInternalPrediction = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const apiResponse = await utilBuildChatflow(req, req.io, true)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ createInternalPrediction
+}
diff --git a/packages/server/src/controllers/ip/index.ts b/packages/server/src/controllers/ip/index.ts
new file mode 100644
index 000000000..98f4ce729
--- /dev/null
+++ b/packages/server/src/controllers/ip/index.ts
@@ -0,0 +1,21 @@
+import { Request, Response, NextFunction } from 'express'
+
+// Configure number of proxies in Host Environment
+const configureProxyNrInHostEnv = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.ip === 'undefined' || req.ip) {
+ throw new Error(`Error: ipController.configureProxyNrInHostEnv - ip not provided!`)
+ }
+ const apiResponse = {
+ ip: req.ip,
+ msg: `Check returned IP address in the response. If it matches your current IP address ( which you can get by going to http://ip.nfriedly.com/ or https://api.ipify.org/ ), then the number of proxies is correct and the rate limiter should now work correctly. If not, increase the number of proxies by 1 and restart Cloud-Hosted Flowise until the IP address matches your own. Visit https://docs.flowiseai.com/configuration/rate-limit#cloud-hosted-rate-limit-setup-guide for more information.`
+ }
+ return res.send(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ configureProxyNrInHostEnv
+}
diff --git a/packages/server/src/controllers/load-prompts/index.ts b/packages/server/src/controllers/load-prompts/index.ts
new file mode 100644
index 000000000..6fa38aef6
--- /dev/null
+++ b/packages/server/src/controllers/load-prompts/index.ts
@@ -0,0 +1,21 @@
+import { Request, Response, NextFunction } from 'express'
+import loadPromptsService from '../../services/load-prompts'
+
+const createPrompt = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.body === 'undefined' || typeof req.body.promptName === 'undefined' || req.body.promptName === '') {
+ throw new Error(`Error: loadPromptsController.createPrompt - promptName not provided!`)
+ }
+ const apiResponse = await loadPromptsService.createPrompt(req.body.promptName as string)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ createPrompt
+}
diff --git a/packages/server/src/controllers/marketplaces/index.ts b/packages/server/src/controllers/marketplaces/index.ts
new file mode 100644
index 000000000..62371a734
--- /dev/null
+++ b/packages/server/src/controllers/marketplaces/index.ts
@@ -0,0 +1,16 @@
+import { Request, Response, NextFunction } from 'express'
+import marketplacesService from '../../services/marketplaces'
+
+// Get all templates for marketplaces
+const getAllTemplates = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const apiResponse = await marketplacesService.getAllTemplates()
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ getAllTemplates
+}
diff --git a/packages/server/src/controllers/node-configs/index.ts b/packages/server/src/controllers/node-configs/index.ts
new file mode 100644
index 000000000..f8f90e1e8
--- /dev/null
+++ b/packages/server/src/controllers/node-configs/index.ts
@@ -0,0 +1,18 @@
+import { Request, Response, NextFunction } from 'express'
+import nodeConfigsService from '../../services/node-configs'
+
+const getAllNodeConfigs = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.body === 'undefined' || req.body === '') {
+ throw new Error(`Error: nodeConfigsController.getAllNodeConfigs - body not provided!`)
+ }
+ const apiResponse = await nodeConfigsService.getAllNodeConfigs(req.body)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ getAllNodeConfigs
+}
diff --git a/packages/server/src/controllers/node-icons/index.ts b/packages/server/src/controllers/node-icons/index.ts
new file mode 100644
index 000000000..b423a96fe
--- /dev/null
+++ b/packages/server/src/controllers/node-icons/index.ts
@@ -0,0 +1,30 @@
+import { Request, Response, NextFunction } from 'express'
+import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
+
+// Returns specific component node icon via name
+const getSingleNodeIcon = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const appServer = getRunningExpressApp()
+ if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentNodes, req.params.name)) {
+ const nodeInstance = appServer.nodesPool.componentNodes[req.params.name]
+ if (nodeInstance.icon === undefined) {
+ throw new Error(`Error: nodeIconController.getSingleNodeIcon - Node ${req.params.name} icon not found`)
+ }
+
+ if (nodeInstance.icon.endsWith('.svg') || nodeInstance.icon.endsWith('.png') || nodeInstance.icon.endsWith('.jpg')) {
+ const filepath = nodeInstance.icon
+ res.sendFile(filepath)
+ } else {
+ throw new Error(`Error: nodeIconController.getSingleNodeIcon - Node ${req.params.name} icon is missing icon`)
+ }
+ } else {
+ throw new Error(`Error: nodeIconController.getSingleNodeIcon - Node ${req.params.name} not found`)
+ }
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ getSingleNodeIcon
+}
diff --git a/packages/server/src/controllers/nodes/index.ts b/packages/server/src/controllers/nodes/index.ts
new file mode 100644
index 000000000..ac448d933
--- /dev/null
+++ b/packages/server/src/controllers/nodes/index.ts
@@ -0,0 +1,76 @@
+import { Request, Response, NextFunction } from 'express'
+import nodesService from '../../services/nodes'
+
+const getAllNodes = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const apiResponse = await nodesService.getAllNodes()
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getNodeByName = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.name === 'undefined' || req.params.name === '') {
+ throw new Error(`Error: nodesController.getNodeByName - name not provided!`)
+ }
+ const apiResponse = await nodesService.getNodeByName(req.params.name)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getSingleNodeIcon = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.name === 'undefined' || req.params.name === '') {
+ throw new Error(`Error: nodesController.getSingleNodeIcon - name not provided!`)
+ }
+ const apiResponse = await nodesService.getSingleNodeIcon(req.params.name)
+ return res.sendFile(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getSingleNodeAsyncOptions = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.body === 'undefined' || req.body === '') {
+ throw new Error(`Error: nodesController.getSingleNodeAsyncOptions - body not provided!`)
+ }
+ if (typeof req.params.name === 'undefined' || req.params.name === '') {
+ throw new Error(`Error: nodesController.getSingleNodeAsyncOptions - name not provided!`)
+ }
+ const apiResponse = await nodesService.getSingleNodeAsyncOptions(req.params.name, req.body)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const executeCustomFunction = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.body === 'undefined' || req.body === '') {
+ throw new Error(`Error: nodesController.executeCustomFunction - body not provided!`)
+ }
+ const apiResponse = await nodesService.executeCustomFunction(req.body)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ getAllNodes,
+ getNodeByName,
+ getSingleNodeIcon,
+ getSingleNodeAsyncOptions,
+ executeCustomFunction
+}
diff --git a/packages/server/src/controllers/openai-assistants/index.ts b/packages/server/src/controllers/openai-assistants/index.ts
new file mode 100644
index 000000000..539f2fbf6
--- /dev/null
+++ b/packages/server/src/controllers/openai-assistants/index.ts
@@ -0,0 +1,69 @@
+import { Request, Response, NextFunction } from 'express'
+import path from 'path'
+import * as fs from 'fs'
+import openaiAssistantsService from '../../services/openai-assistants'
+import { getUserHome } from '../../utils'
+import contentDisposition from 'content-disposition'
+
+// List available assistants
+const getAllOpenaiAssistants = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.query.credential === 'undefined' || req.query.credential === '') {
+ throw new Error(`Error: openaiAssistantsController.getAllOpenaiAssistants - credential not provided!`)
+ }
+ const apiResponse = await openaiAssistantsService.getAllOpenaiAssistants(req.query.credential as string)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+// Get assistant object
+const getSingleOpenaiAssistant = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: openaiAssistantsController.getSingleOpenaiAssistant - id not provided!`)
+ }
+ if (typeof req.query.credential === 'undefined' || req.query.credential === '') {
+ throw new Error(`Error: openaiAssistantsController.getSingleOpenaiAssistant - credential not provided!`)
+ }
+ const apiResponse = await openaiAssistantsService.getSingleOpenaiAssistant(req.query.credential as string, req.params.id)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+// Download file from assistant
+const getFileFromAssistant = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName)
+ //raise error if file path is not absolute
+ if (!path.isAbsolute(filePath)) return res.status(500).send(`Invalid file path`)
+ //raise error if file path contains '..'
+ if (filePath.includes('..')) return res.status(500).send(`Invalid file path`)
+ //only return from the .flowise openai-assistant folder
+ if (!(filePath.includes('.flowise') && filePath.includes('openai-assistant'))) return res.status(500).send(`Invalid file path`)
+ if (fs.existsSync(filePath)) {
+ res.setHeader('Content-Disposition', contentDisposition(path.basename(filePath)))
+ const fileStream = fs.createReadStream(filePath)
+ fileStream.pipe(res)
+ } else {
+ return res.status(404).send(`File ${req.body.fileName} not found`)
+ }
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ getAllOpenaiAssistants,
+ getSingleOpenaiAssistant,
+ getFileFromAssistant
+}
diff --git a/packages/server/src/controllers/predictions/index.ts b/packages/server/src/controllers/predictions/index.ts
new file mode 100644
index 000000000..936bfe1cc
--- /dev/null
+++ b/packages/server/src/controllers/predictions/index.ts
@@ -0,0 +1,66 @@
+import { Request, Response, NextFunction } from 'express'
+import { getRateLimiter } from '../../utils/rateLimit'
+import chatflowsService from '../../services/chatflows'
+import logger from '../../utils/logger'
+import { utilBuildChatflow } from '../../utils/buildChatflow'
+
+// Send input message and get prediction result (External)
+const createPrediction = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: predictionsController.createPrediction - id not provided!`)
+ }
+ if (typeof req.body === 'undefined' || req.body === '') {
+ throw new Error(`Error: predictionsController.createPrediction - body not provided!`)
+ }
+ const chatflow = await chatflowsService.getChatflowById(req.params.id)
+ if (!chatflow) {
+ return res.status(404).send(`Chatflow ${req.params.id} not found`)
+ }
+ let isDomainAllowed = true
+ logger.info(`[server]: Request originated from ${req.headers.origin}`)
+ if (chatflow.chatbotConfig) {
+ const parsedConfig = JSON.parse(chatflow.chatbotConfig)
+ // check whether the first one is not empty. if it is empty that means the user set a value and then removed it.
+ const isValidAllowedOrigins = parsedConfig.allowedOrigins?.length && parsedConfig.allowedOrigins[0] !== ''
+ if (isValidAllowedOrigins) {
+ const originHeader = req.headers.origin as string
+ const origin = new URL(originHeader).host
+ isDomainAllowed =
+ parsedConfig.allowedOrigins.filter((domain: string) => {
+ try {
+ const allowedOrigin = new URL(domain).host
+ return origin === allowedOrigin
+ } catch (e) {
+ return false
+ }
+ }).length > 0
+ }
+ }
+
+ if (isDomainAllowed) {
+ const apiResponse = await utilBuildChatflow(req, req.io)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } else {
+ return res.status(401).send(`This site is not allowed to access this chatbot`)
+ }
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getRateLimiterMiddleware = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ return getRateLimiter(req, res, next)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ createPrediction,
+ getRateLimiterMiddleware
+}
diff --git a/packages/server/src/controllers/prompts-lists/index.ts b/packages/server/src/controllers/prompts-lists/index.ts
new file mode 100644
index 000000000..88a048cae
--- /dev/null
+++ b/packages/server/src/controllers/prompts-lists/index.ts
@@ -0,0 +1,16 @@
+import { Request, Response, NextFunction } from 'express'
+import promptsListsService from '../../services/prompts-lists'
+
+// Prompt from Hub
+const createPromptsList = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const apiResponse = await promptsListsService.createPromptsList(req.body)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ createPromptsList
+}
diff --git a/packages/server/src/controllers/stats/index.ts b/packages/server/src/controllers/stats/index.ts
new file mode 100644
index 000000000..e1c97c490
--- /dev/null
+++ b/packages/server/src/controllers/stats/index.ts
@@ -0,0 +1,40 @@
+import { Request, Response, NextFunction } from 'express'
+import statsService from '../../services/stats'
+import { chatType } from '../../Interface'
+
+const getChatflowStats = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: statsController.getChatflowStats - id not provided!`)
+ }
+ const chatflowid = req.params.id
+ let chatTypeFilter = req.query?.chatType as chatType | undefined
+ const startDate = req.query?.startDate as string | undefined
+ const endDate = req.query?.endDate as string | undefined
+ if (chatTypeFilter) {
+ try {
+ const chatTypeFilterArray = JSON.parse(chatTypeFilter)
+ if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) {
+ chatTypeFilter = undefined
+ } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) {
+ chatTypeFilter = chatType.EXTERNAL
+ } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) {
+ chatTypeFilter = chatType.INTERNAL
+ }
+ } catch (e) {
+ return res.status(500).send(e)
+ }
+ }
+ const apiResponse = await statsService.getChatflowStats(chatflowid, chatTypeFilter, startDate, endDate, '', true)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ getChatflowStats
+}
diff --git a/packages/server/src/controllers/tools/index.ts b/packages/server/src/controllers/tools/index.ts
new file mode 100644
index 000000000..49cea2696
--- /dev/null
+++ b/packages/server/src/controllers/tools/index.ts
@@ -0,0 +1,85 @@
+import { Request, Response, NextFunction } from 'express'
+import toolsService from '../../services/tools'
+
+const creatTool = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.body === 'undefined' || req.body === '') {
+ throw new Error(`Error: toolsController.creatTool - body not provided!`)
+ }
+ const apiResponse = await toolsService.creatTool(req.body)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const deleteTool = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: toolsController.updateTool - id not provided!`)
+ }
+ const apiResponse = await toolsService.deleteTool(req.params.id)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getAllTools = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const apiResponse = await toolsService.getAllTools()
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getToolById = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: toolsController.getToolById - id not provided!`)
+ }
+ const apiResponse = await toolsService.getToolById(req.params.id)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const updateTool = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error(`Error: toolsController.updateTool - id not provided!`)
+ }
+ if (typeof req.body === 'undefined' || req.body === '') {
+ throw new Error(`Error: toolsController.deleteTool - body not provided!`)
+ }
+ const apiResponse = await toolsService.updateTool(req.params.id, req.body)
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ creatTool,
+ deleteTool,
+ getAllTools,
+ getToolById,
+ updateTool
+}
diff --git a/packages/server/src/controllers/variables/index.ts b/packages/server/src/controllers/variables/index.ts
new file mode 100644
index 000000000..4dfd7492a
--- /dev/null
+++ b/packages/server/src/controllers/variables/index.ts
@@ -0,0 +1,68 @@
+import { Request, Response, NextFunction } from 'express'
+import variablesService from '../../services/variables'
+import { Variable } from '../../database/entities/Variable'
+
+const createVariable = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.body === 'undefined') {
+ throw new Error(`Error: variablesController.createVariable - body not provided!`)
+ }
+ const body = req.body
+ const newVariable = new Variable()
+ Object.assign(newVariable, body)
+ const apiResponse = await variablesService.createVariable(newVariable)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const deleteVariable = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error('Error: variablesController.deleteVariable - id not provided!')
+ }
+ const apiResponse = await variablesService.deleteVariable(req.params.id)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const getAllVariables = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const apiResponse = await variablesService.getAllVariables()
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const updateVariable = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ if (typeof req.params.id === 'undefined' || req.params.id === '') {
+ throw new Error('Error: variablesController.updateVariable - id not provided!')
+ }
+ if (typeof req.body === 'undefined') {
+ throw new Error('Error: variablesController.updateVariable - body not provided!')
+ }
+ const variable = await variablesService.getVariableById(req.params.id)
+ if (!variable) {
+ return res.status(404).send(`Variable ${req.params.id} not found in the database`)
+ }
+ const body = req.body
+ const updatedVariable = new Variable()
+ Object.assign(updatedVariable, body)
+ const apiResponse = await variablesService.updateVariable(variable, updatedVariable)
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ createVariable,
+ deleteVariable,
+ getAllVariables,
+ updateVariable
+}
diff --git a/packages/server/src/controllers/vectors/index.ts b/packages/server/src/controllers/vectors/index.ts
new file mode 100644
index 000000000..21f8e3da9
--- /dev/null
+++ b/packages/server/src/controllers/vectors/index.ts
@@ -0,0 +1,33 @@
+import { Request, Response, NextFunction } from 'express'
+import vectorsService from '../../services/vectors'
+import { getRateLimiter } from '../../utils/rateLimit'
+
+const getRateLimiterMiddleware = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ return getRateLimiter(req, res, next)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const upsertVectorMiddleware = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ return await vectorsService.upsertVectorMiddleware(req, res)
+ } catch (error) {
+ next(error)
+ }
+}
+
+const createInternalUpsert = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ return await vectorsService.upsertVectorMiddleware(req, res, true)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ upsertVectorMiddleware,
+ createInternalUpsert,
+ getRateLimiterMiddleware
+}
diff --git a/packages/server/src/controllers/versions/index.ts b/packages/server/src/controllers/versions/index.ts
new file mode 100644
index 000000000..7a6f76c46
--- /dev/null
+++ b/packages/server/src/controllers/versions/index.ts
@@ -0,0 +1,18 @@
+import { Request, Response, NextFunction } from 'express'
+import versionsService from '../../services/versions'
+
+const getVersion = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const apiResponse = await versionsService.getVersion()
+ if (apiResponse.executionError) {
+ return res.status(apiResponse.status).send(apiResponse.msg)
+ }
+ return res.json(apiResponse)
+ } catch (error) {
+ next(error)
+ }
+}
+
+export default {
+ getVersion
+}
diff --git a/packages/server/src/errors/apiError/index.ts b/packages/server/src/errors/apiError/index.ts
new file mode 100644
index 000000000..641e2d5a1
--- /dev/null
+++ b/packages/server/src/errors/apiError/index.ts
@@ -0,0 +1,9 @@
+export class ApiError extends Error {
+ statusCode: number
+ constructor(statusCode: number, message: string) {
+ super(message)
+ this.statusCode = statusCode
+ // capture the stack trace of the error from anywhere in the application
+ Error.captureStackTrace(this, this.constructor)
+ }
+}
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index 0e32fa627..8330c570d 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -1,89 +1,34 @@
-import express, { NextFunction, Request, Response } from 'express'
-import multer from 'multer'
+import express from 'express'
+import { Request, Response } from 'express'
import path from 'path'
import cors from 'cors'
import http from 'http'
-import * as fs from 'fs'
import basicAuth from 'express-basic-auth'
-import contentDisposition from 'content-disposition'
import { Server } from 'socket.io'
import logger from './utils/logger'
import { expressRequestLogger } from './utils/logger'
-import { v4 as uuidv4 } from 'uuid'
-import OpenAI from 'openai'
-import { DataSource, FindOptionsWhere, MoreThanOrEqual, LessThanOrEqual, Between } from 'typeorm'
-import {
- IChatFlow,
- IncomingInput,
- IReactFlowNode,
- IReactFlowObject,
- INodeData,
- ICredentialReturnResponse,
- chatType,
- IChatMessage,
- IChatMessageFeedback,
- IDepthQueue,
- INodeDirectedGraph,
- IUploadFileSizeAndTypes
-} from './Interface'
-import {
- getNodeModulesPackagePath,
- getStartingNodes,
- buildFlow,
- getEndingNodes,
- constructGraphs,
- resolveVariables,
- isStartNodeDependOnInput,
- mapMimeTypeToInputField,
- findAvailableConfigs,
- isSameOverrideConfig,
- isFlowValidForStream,
- databaseEntities,
- transformToCredentialEntity,
- decryptCredentialData,
- replaceInputsWithConfig,
- getEncryptionKey,
- getMemorySessionId,
- getUserHome,
- getSessionChatHistory,
- getAllConnectedNodes,
- clearSessionMemory,
- findMemoryNode,
- deleteFolderRecursive,
- getTelemetryFlowObj,
- getAppVersion
-} from './utils'
-import { cloneDeep, omit, uniqWith, isEqual } from 'lodash'
+import { DataSource } from 'typeorm'
+import { IChatFlow } from './Interface'
+import { getNodeModulesPackagePath, getEncryptionKey } from './utils'
import { getDataSource } from './DataSource'
import { NodesPool } from './NodesPool'
import { ChatFlow } from './database/entities/ChatFlow'
-import { ChatMessage } from './database/entities/ChatMessage'
-import { ChatMessageFeedback } from './database/entities/ChatMessageFeedback'
-import { Credential } from './database/entities/Credential'
-import { Tool } from './database/entities/Tool'
-import { Assistant } from './database/entities/Assistant'
import { ChatflowPool } from './ChatflowPool'
import { CachePool } from './CachePool'
-import {
- ICommonObject,
- IMessage,
- INodeOptionsValue,
- INodeParams,
- handleEscapeCharacters,
- convertSpeechToText,
- xmlScrape,
- webCrawl,
- getStoragePath,
- IFileUpload
-} from 'flowise-components'
-import { createRateLimiter, getRateLimiter, initializeRateLimiter } from './utils/rateLimit'
-import { addAPIKey, compareKeys, deleteAPIKey, getApiKey, getAPIKeys, updateAPIKey } from './utils/apiKey'
+import { initializeRateLimiter } from './utils/rateLimit'
+import { getAPIKeys } from './utils/apiKey'
import { sanitizeMiddleware, getCorsOptions, getAllowedIframeOrigins } from './utils/XSS'
-import axios from 'axios'
-import { Client } from 'langchainhub'
-import { parsePrompt } from './utils/hub'
import { Telemetry } from './utils/telemetry'
-import { Variable } from './database/entities/Variable'
+import flowiseApiV1Router from './routes'
+import errorHandlerMiddleware from './middlewares/errors'
+
+declare global {
+ namespace Express {
+ interface Request {
+ io?: Server
+ }
+ }
+}
export class App {
app: express.Application
@@ -140,7 +85,6 @@ export class App {
const flowise_file_size_limit = process.env.FLOWISE_FILE_SIZE_LIMIT ?? '50mb'
this.app.use(express.json({ limit: flowise_file_size_limit }))
this.app.use(express.urlencoded({ limit: flowise_file_size_limit, extended: true }))
-
if (process.env.NUMBER_OF_PROXIES && parseInt(process.env.NUMBER_OF_PROXIES) > 0)
this.app.set('trust proxy', parseInt(process.env.NUMBER_OF_PROXIES))
@@ -168,6 +112,12 @@ export class App {
// Add the sanitizeMiddleware to guard against XSS
this.app.use(sanitizeMiddleware)
+ // Make io accessible to our router on req.io
+ this.app.use((req, res, next) => {
+ req.io = socketIO
+ next()
+ })
+
if (process.env.FLOWISE_USERNAME && process.env.FLOWISE_PASSWORD) {
const username = process.env.FLOWISE_USERNAME
const password = process.env.FLOWISE_PASSWORD
@@ -197,1433 +147,7 @@ export class App {
})
}
- const upload = multer({ dest: `${path.join(__dirname, '..', 'uploads')}/` })
-
- // ----------------------------------------
- // Configure number of proxies in Host Environment
- // ----------------------------------------
- this.app.get('/api/v1/ip', (request, response) => {
- response.send({
- ip: request.ip,
- msg: 'Check returned IP address in the response. If it matches your current IP address ( which you can get by going to http://ip.nfriedly.com/ or https://api.ipify.org/ ), then the number of proxies is correct and the rate limiter should now work correctly. If not, increase the number of proxies by 1 and restart Cloud-Hosted Flowise until the IP address matches your own. Visit https://docs.flowiseai.com/configuration/rate-limit#cloud-hosted-rate-limit-setup-guide for more information.'
- })
- })
-
- // ----------------------------------------
- // Components
- // ----------------------------------------
-
- // Get all component nodes
- this.app.get('/api/v1/nodes', (req: Request, res: Response) => {
- const returnData = []
- for (const nodeName in this.nodesPool.componentNodes) {
- const clonedNode = cloneDeep(this.nodesPool.componentNodes[nodeName])
- returnData.push(clonedNode)
- }
- return res.json(returnData)
- })
-
- // Get all component credentials
- this.app.get('/api/v1/components-credentials', async (req: Request, res: Response) => {
- const returnData = []
- for (const credName in this.nodesPool.componentCredentials) {
- const clonedCred = cloneDeep(this.nodesPool.componentCredentials[credName])
- returnData.push(clonedCred)
- }
- return res.json(returnData)
- })
-
- // Get specific component node via name
- this.app.get('/api/v1/nodes/:name', (req: Request, res: Response) => {
- if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) {
- return res.json(this.nodesPool.componentNodes[req.params.name])
- } else {
- throw new Error(`Node ${req.params.name} not found`)
- }
- })
-
- // Get component credential via name
- this.app.get('/api/v1/components-credentials/:name', (req: Request, res: Response) => {
- if (!req.params.name.includes('&')) {
- if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) {
- return res.json(this.nodesPool.componentCredentials[req.params.name])
- } else {
- throw new Error(`Credential ${req.params.name} not found`)
- }
- } else {
- const returnResponse = []
- for (const name of req.params.name.split('&')) {
- if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, name)) {
- returnResponse.push(this.nodesPool.componentCredentials[name])
- } else {
- throw new Error(`Credential ${name} not found`)
- }
- }
- return res.json(returnResponse)
- }
- })
-
- // Returns specific component node icon via name
- this.app.get('/api/v1/node-icon/:name', (req: Request, res: Response) => {
- if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) {
- const nodeInstance = this.nodesPool.componentNodes[req.params.name]
- if (nodeInstance.icon === undefined) {
- throw new Error(`Node ${req.params.name} icon not found`)
- }
-
- if (nodeInstance.icon.endsWith('.svg') || nodeInstance.icon.endsWith('.png') || nodeInstance.icon.endsWith('.jpg')) {
- const filepath = nodeInstance.icon
- res.sendFile(filepath)
- } else {
- throw new Error(`Node ${req.params.name} icon is missing icon`)
- }
- } else {
- throw new Error(`Node ${req.params.name} not found`)
- }
- })
-
- // Returns specific component credential icon via name
- this.app.get('/api/v1/components-credentials-icon/:name', (req: Request, res: Response) => {
- if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentCredentials, req.params.name)) {
- const credInstance = this.nodesPool.componentCredentials[req.params.name]
- if (credInstance.icon === undefined) {
- throw new Error(`Credential ${req.params.name} icon not found`)
- }
-
- if (credInstance.icon.endsWith('.svg') || credInstance.icon.endsWith('.png') || credInstance.icon.endsWith('.jpg')) {
- const filepath = credInstance.icon
- res.sendFile(filepath)
- } else {
- throw new Error(`Credential ${req.params.name} icon is missing icon`)
- }
- } else {
- throw new Error(`Credential ${req.params.name} not found`)
- }
- })
-
- // load async options
- this.app.post('/api/v1/node-load-method/:name', async (req: Request, res: Response) => {
- const nodeData: INodeData = req.body
- if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, req.params.name)) {
- try {
- const nodeInstance = this.nodesPool.componentNodes[req.params.name]
- const methodName = nodeData.loadMethod || ''
-
- const returnOptions: INodeOptionsValue[] = await nodeInstance.loadMethods![methodName]!.call(nodeInstance, nodeData, {
- appDataSource: this.AppDataSource,
- databaseEntities: databaseEntities
- })
-
- return res.json(returnOptions)
- } catch (error) {
- return res.json([])
- }
- } else {
- res.status(404).send(`Node ${req.params.name} not found`)
- return
- }
- })
-
- // execute custom function node
- this.app.post('/api/v1/node-custom-function', async (req: Request, res: Response) => {
- const body = req.body
- const functionInputVariables = Object.fromEntries(
- [...(body?.javascriptFunction ?? '').matchAll(/\$([a-zA-Z0-9_]+)/g)].map((g) => [g[1], undefined])
- )
- const nodeData = { inputs: { functionInputVariables, ...body } }
- if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, 'customFunction')) {
- try {
- const nodeInstanceFilePath = this.nodesPool.componentNodes['customFunction'].filePath as string
- const nodeModule = await import(nodeInstanceFilePath)
- const newNodeInstance = new nodeModule.nodeClass()
-
- const options: ICommonObject = {
- appDataSource: this.AppDataSource,
- databaseEntities,
- logger
- }
-
- const returnData = await newNodeInstance.init(nodeData, '', options)
- const result = typeof returnData === 'string' ? handleEscapeCharacters(returnData, true) : returnData
-
- return res.json(result)
- } catch (error) {
- return res.status(500).send(`Error running custom function: ${error}`)
- }
- } else {
- res.status(404).send(`Node customFunction not found`)
- return
- }
- })
-
- // ----------------------------------------
- // Chatflows
- // ----------------------------------------
-
- // Get all chatflows
- this.app.get('/api/v1/chatflows', async (req: Request, res: Response) => {
- const chatflows: IChatFlow[] = await getAllChatFlow()
- return res.json(chatflows)
- })
-
- // Get specific chatflow via api key
- this.app.get('/api/v1/chatflows/apikey/:apiKey', async (req: Request, res: Response) => {
- try {
- const apiKey = await getApiKey(req.params.apiKey)
- if (!apiKey) return res.status(401).send('Unauthorized')
- const chatflows = await this.AppDataSource.getRepository(ChatFlow)
- .createQueryBuilder('cf')
- .where('cf.apikeyid = :apikeyid', { apikeyid: apiKey.id })
- .orWhere('cf.apikeyid IS NULL')
- .orWhere('cf.apikeyid = ""')
- .orderBy('cf.name', 'ASC')
- .getMany()
- if (chatflows.length >= 1) return res.status(200).send(chatflows)
- return res.status(404).send('Chatflow not found')
- } catch (err: any) {
- return res.status(500).send(err?.message)
- }
- })
-
- // Get specific chatflow via id
- this.app.get('/api/v1/chatflows/:id', async (req: Request, res: Response) => {
- const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
- id: req.params.id
- })
- if (chatflow) return res.json(chatflow)
- return res.status(404).send(`Chatflow ${req.params.id} not found`)
- })
-
- // Get specific chatflow via id (PUBLIC endpoint, used when sharing chatbot link)
- this.app.get('/api/v1/public-chatflows/:id', async (req: Request, res: Response) => {
- const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
- id: req.params.id
- })
- if (chatflow && chatflow.isPublic) return res.json(chatflow)
- else if (chatflow && !chatflow.isPublic) return res.status(401).send(`Unauthorized`)
- return res.status(404).send(`Chatflow ${req.params.id} not found`)
- })
-
- // Get specific chatflow chatbotConfig via id (PUBLIC endpoint, used to retrieve config for embedded chat)
- // Safe as public endpoint as chatbotConfig doesn't contain sensitive credential
- this.app.get('/api/v1/public-chatbotConfig/:id', async (req: Request, res: Response) => {
- const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
- id: req.params.id
- })
- if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`)
- const uploadsConfig = await this.getUploadsConfig(req.params.id)
- // even if chatbotConfig is not set but uploads are enabled
- // send uploadsConfig to the chatbot
- if (chatflow.chatbotConfig || uploadsConfig) {
- try {
- const parsedConfig = chatflow.chatbotConfig ? JSON.parse(chatflow.chatbotConfig) : {}
- return res.json({ ...parsedConfig, uploads: uploadsConfig })
- } catch (e) {
- return res.status(500).send(`Error parsing Chatbot Config for Chatflow ${req.params.id}`)
- }
- }
- return res.status(200).send('OK')
- })
-
- // Save chatflow
- this.app.post('/api/v1/chatflows', async (req: Request, res: Response) => {
- const body = req.body
- const newChatFlow = new ChatFlow()
- Object.assign(newChatFlow, body)
-
- const chatflow = this.AppDataSource.getRepository(ChatFlow).create(newChatFlow)
- const results = await this.AppDataSource.getRepository(ChatFlow).save(chatflow)
-
- await this.telemetry.sendTelemetry('chatflow_created', {
- version: await getAppVersion(),
- chatflowId: results.id,
- flowGraph: getTelemetryFlowObj(JSON.parse(results.flowData)?.nodes, JSON.parse(results.flowData)?.edges)
- })
-
- return res.json(results)
- })
-
- // Update chatflow
- this.app.put('/api/v1/chatflows/:id', async (req: Request, res: Response) => {
- const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
- id: req.params.id
- })
-
- if (!chatflow) {
- res.status(404).send(`Chatflow ${req.params.id} not found`)
- return
- }
-
- const body = req.body
- const updateChatFlow = new ChatFlow()
- Object.assign(updateChatFlow, body)
-
- updateChatFlow.id = chatflow.id
- createRateLimiter(updateChatFlow)
-
- this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow)
- const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow)
-
- // chatFlowPool is initialized only when a flow is opened
- // if the user attempts to rename/update category without opening any flow, chatFlowPool will be undefined
- if (this.chatflowPool) {
- // Update chatflowpool inSync to false, to build flow from scratch again because data has been changed
- this.chatflowPool.updateInSync(chatflow.id, false)
- }
-
- return res.json(result)
- })
-
- // Delete chatflow via id
- this.app.delete('/api/v1/chatflows/:id', async (req: Request, res: Response) => {
- const results = await this.AppDataSource.getRepository(ChatFlow).delete({ id: req.params.id })
-
- try {
- // Delete all uploads corresponding to this chatflow
- const directory = path.join(getStoragePath(), req.params.id)
- deleteFolderRecursive(directory)
- } catch (e) {
- logger.error(`[server]: Error deleting file storage for chatflow ${req.params.id}: ${e}`)
- }
-
- return res.json(results)
- })
-
- // Check if chatflow valid for streaming
- this.app.get('/api/v1/chatflows-streaming/:id', async (req: Request, res: Response) => {
- const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
- id: req.params.id
- })
- if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`)
-
- /*** Get Ending Node with Directed Graph ***/
- const flowData = chatflow.flowData
- const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
- const nodes = parsedFlowData.nodes
- const edges = parsedFlowData.edges
- const { graph, nodeDependencies } = constructGraphs(nodes, edges)
-
- const endingNodeIds = getEndingNodes(nodeDependencies, graph)
- if (!endingNodeIds.length) return res.status(500).send(`Ending nodes not found`)
-
- const endingNodes = nodes.filter((nd) => endingNodeIds.includes(nd.id))
-
- let isStreaming = false
- let isEndingNodeExists = endingNodes.find((node) => node.data?.outputs?.output === 'EndingNode')
-
- for (const endingNode of endingNodes) {
- const endingNodeData = endingNode.data
- if (!endingNodeData) return res.status(500).send(`Ending node ${endingNode.id} data not found`)
-
- const isEndingNode = endingNodeData?.outputs?.output === 'EndingNode'
-
- if (!isEndingNode) {
- if (
- endingNodeData &&
- endingNodeData.category !== 'Chains' &&
- endingNodeData.category !== 'Agents' &&
- endingNodeData.category !== 'Engine'
- ) {
- return res.status(500).send(`Ending node must be either a Chain or Agent`)
- }
- }
-
- isStreaming = isEndingNode ? false : isFlowValidForStream(nodes, endingNodeData)
- }
-
- // Once custom function ending node exists, flow is always unavailable to stream
- const obj = { isStreaming: isEndingNodeExists ? false : isStreaming }
- return res.json(obj)
- })
-
- // Check if chatflow valid for uploads
- this.app.get('/api/v1/chatflows-uploads/:id', async (req: Request, res: Response) => {
- try {
- const uploadsConfig = await this.getUploadsConfig(req.params.id)
- return res.json(uploadsConfig)
- } catch (e) {
- return res.status(500).send(e)
- }
- })
-
- // ----------------------------------------
- // ChatMessage
- // ----------------------------------------
-
- // Get all chatmessages from chatflowid
- this.app.get('/api/v1/chatmessage/:id', async (req: Request, res: Response) => {
- const sortOrder = req.query?.order as string | undefined
- const chatId = req.query?.chatId as string | undefined
- const memoryType = req.query?.memoryType as string | undefined
- const sessionId = req.query?.sessionId as string | undefined
- const messageId = req.query?.messageId as string | undefined
- const startDate = req.query?.startDate as string | undefined
- const endDate = req.query?.endDate as string | undefined
- const feedback = req.query?.feedback as boolean | undefined
- let chatTypeFilter = req.query?.chatType as chatType | undefined
-
- if (chatTypeFilter) {
- try {
- const chatTypeFilterArray = JSON.parse(chatTypeFilter)
- if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) {
- chatTypeFilter = undefined
- } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) {
- chatTypeFilter = chatType.EXTERNAL
- } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) {
- chatTypeFilter = chatType.INTERNAL
- }
- } catch (e) {
- return res.status(500).send(e)
- }
- }
-
- const chatmessages = await this.getChatMessage(
- req.params.id,
- chatTypeFilter,
- sortOrder,
- chatId,
- memoryType,
- sessionId,
- startDate,
- endDate,
- messageId,
- feedback
- )
- return res.json(chatmessages)
- })
-
- // Get internal chatmessages from chatflowid
- this.app.get('/api/v1/internal-chatmessage/:id', async (req: Request, res: Response) => {
- const sortOrder = req.query?.order as string | undefined
- const chatId = req.query?.chatId as string | undefined
- const memoryType = req.query?.memoryType as string | undefined
- const sessionId = req.query?.sessionId as string | undefined
- const messageId = req.query?.messageId as string | undefined
- const startDate = req.query?.startDate as string | undefined
- const endDate = req.query?.endDate as string | undefined
- const feedback = req.query?.feedback as boolean | undefined
-
- const chatmessages = await this.getChatMessage(
- req.params.id,
- chatType.INTERNAL,
- sortOrder,
- chatId,
- memoryType,
- sessionId,
- startDate,
- endDate,
- messageId,
- feedback
- )
- return res.json(chatmessages)
- })
-
- // Add chatmessages for chatflowid
- this.app.post('/api/v1/chatmessage/:id', async (req: Request, res: Response) => {
- const body = req.body
- const results = await this.addChatMessage(body)
- return res.json(results)
- })
-
- // Delete all chatmessages from chatId
- this.app.delete('/api/v1/chatmessage/:id', async (req: Request, res: Response) => {
- const chatflowid = req.params.id
- const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
- id: chatflowid
- })
- if (!chatflow) {
- res.status(404).send(`Chatflow ${chatflowid} not found`)
- return
- }
- const chatId = req.query?.chatId as string
- const memoryType = req.query?.memoryType as string | undefined
- const sessionId = req.query?.sessionId as string | undefined
- const chatType = req.query?.chatType as string | undefined
- const isClearFromViewMessageDialog = req.query?.isClearFromViewMessageDialog as string | undefined
-
- const flowData = chatflow.flowData
- const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
- const nodes = parsedFlowData.nodes
-
- try {
- await clearSessionMemory(
- nodes,
- this.nodesPool.componentNodes,
- chatId,
- this.AppDataSource,
- sessionId,
- memoryType,
- isClearFromViewMessageDialog
- )
- } catch (e) {
- return res.status(500).send('Error clearing chat messages')
- }
-
- const deleteOptions: FindOptionsWhere = { chatflowid }
- if (chatId) deleteOptions.chatId = chatId
- if (memoryType) deleteOptions.memoryType = memoryType
- if (sessionId) deleteOptions.sessionId = sessionId
- if (chatType) deleteOptions.chatType = chatType
-
- // remove all related feedback records
- const feedbackDeleteOptions: FindOptionsWhere = { chatId }
- await this.AppDataSource.getRepository(ChatMessageFeedback).delete(feedbackDeleteOptions)
-
- // Delete all uploads corresponding to this chatflow/chatId
- if (chatId) {
- try {
- const directory = path.join(getStoragePath(), chatflowid, chatId)
- deleteFolderRecursive(directory)
- } catch (e) {
- logger.error(`[server]: Error deleting file storage for chatflow ${chatflowid}, chatId ${chatId}: ${e}`)
- }
- }
-
- const results = await this.AppDataSource.getRepository(ChatMessage).delete(deleteOptions)
- return res.json(results)
- })
-
- // ----------------------------------------
- // Chat Message Feedback
- // ----------------------------------------
-
- // Get all chatmessage feedback from chatflowid
- this.app.get('/api/v1/feedback/:id', async (req: Request, res: Response) => {
- const chatflowid = req.params.id
- const chatId = req.query?.chatId as string | undefined
- const sortOrder = req.query?.order as string | undefined
- const startDate = req.query?.startDate as string | undefined
- const endDate = req.query?.endDate as string | undefined
-
- const feedback = await this.getChatMessageFeedback(chatflowid, chatId, sortOrder, startDate, endDate)
-
- return res.json(feedback)
- })
-
- // Add chatmessage feedback for chatflowid
- this.app.post('/api/v1/feedback/:id', async (req: Request, res: Response) => {
- const body = req.body
- const results = await this.addChatMessageFeedback(body)
- return res.json(results)
- })
-
- // Update chatmessage feedback for id
- this.app.put('/api/v1/feedback/:id', async (req: Request, res: Response) => {
- const id = req.params.id
- const body = req.body
- await this.updateChatMessageFeedback(id, body)
- return res.json({ status: 'OK' })
- })
-
- // ----------------------------------------
- // stats
- // ----------------------------------------
- //
- // get stats for showing in chatflow
- this.app.get('/api/v1/stats/:id', async (req: Request, res: Response) => {
- const chatflowid = req.params.id
- let chatTypeFilter = req.query?.chatType as chatType | undefined
- const startDate = req.query?.startDate as string | undefined
- const endDate = req.query?.endDate as string | undefined
-
- if (chatTypeFilter) {
- try {
- const chatTypeFilterArray = JSON.parse(chatTypeFilter)
- if (chatTypeFilterArray.includes(chatType.EXTERNAL) && chatTypeFilterArray.includes(chatType.INTERNAL)) {
- chatTypeFilter = undefined
- } else if (chatTypeFilterArray.includes(chatType.EXTERNAL)) {
- chatTypeFilter = chatType.EXTERNAL
- } else if (chatTypeFilterArray.includes(chatType.INTERNAL)) {
- chatTypeFilter = chatType.INTERNAL
- }
- } catch (e) {
- return res.status(500).send(e)
- }
- }
-
- const chatmessages = (await this.getChatMessage(
- chatflowid,
- chatTypeFilter,
- undefined,
- undefined,
- undefined,
- undefined,
- startDate,
- endDate,
- '',
- true
- )) as Array
- const totalMessages = chatmessages.length
-
- const totalFeedback = chatmessages.filter((message) => message?.feedback).length
- const positiveFeedback = chatmessages.filter((message) => message?.feedback?.rating === 'THUMBS_UP').length
-
- const results = {
- totalMessages,
- totalFeedback,
- positiveFeedback
- }
-
- res.json(results)
- })
-
- // ----------------------------------------
- // Credentials
- // ----------------------------------------
-
- // Create new credential
- this.app.post('/api/v1/credentials', async (req: Request, res: Response) => {
- const body = req.body
- const newCredential = await transformToCredentialEntity(body)
- const credential = this.AppDataSource.getRepository(Credential).create(newCredential)
- const results = await this.AppDataSource.getRepository(Credential).save(credential)
- return res.json(results)
- })
-
- // Get all credentials
- this.app.get('/api/v1/credentials', async (req: Request, res: Response) => {
- if (req.query.credentialName) {
- let returnCredentials = []
- if (Array.isArray(req.query.credentialName)) {
- for (let i = 0; i < req.query.credentialName.length; i += 1) {
- const name = req.query.credentialName[i] as string
- const credentials = await this.AppDataSource.getRepository(Credential).findBy({
- credentialName: name
- })
- returnCredentials.push(...credentials)
- }
- } else {
- const credentials = await this.AppDataSource.getRepository(Credential).findBy({
- credentialName: req.query.credentialName as string
- })
- returnCredentials = [...credentials]
- }
- return res.json(returnCredentials)
- } else {
- const credentials = await this.AppDataSource.getRepository(Credential).find()
- const returnCredentials = []
- for (const credential of credentials) {
- returnCredentials.push(omit(credential, ['encryptedData']))
- }
- return res.json(returnCredentials)
- }
- })
-
- // Get specific credential
- this.app.get('/api/v1/credentials/:id', async (req: Request, res: Response) => {
- const credential = await this.AppDataSource.getRepository(Credential).findOneBy({
- id: req.params.id
- })
-
- if (!credential) return res.status(404).send(`Credential ${req.params.id} not found`)
-
- // Decrpyt credentialData
- const decryptedCredentialData = await decryptCredentialData(
- credential.encryptedData,
- credential.credentialName,
- this.nodesPool.componentCredentials
- )
- const returnCredential: ICredentialReturnResponse = {
- ...credential,
- plainDataObj: decryptedCredentialData
- }
- return res.json(omit(returnCredential, ['encryptedData']))
- })
-
- // Update credential
- this.app.put('/api/v1/credentials/:id', async (req: Request, res: Response) => {
- const credential = await this.AppDataSource.getRepository(Credential).findOneBy({
- id: req.params.id
- })
-
- if (!credential) return res.status(404).send(`Credential ${req.params.id} not found`)
-
- const body = req.body
- const updateCredential = await transformToCredentialEntity(body)
- this.AppDataSource.getRepository(Credential).merge(credential, updateCredential)
- const result = await this.AppDataSource.getRepository(Credential).save(credential)
-
- return res.json(result)
- })
-
- // Delete all credentials from chatflowid
- this.app.delete('/api/v1/credentials/:id', async (req: Request, res: Response) => {
- const results = await this.AppDataSource.getRepository(Credential).delete({ id: req.params.id })
- return res.json(results)
- })
-
- // ----------------------------------------
- // Tools
- // ----------------------------------------
-
- // Get all tools
- this.app.get('/api/v1/tools', async (req: Request, res: Response) => {
- const tools = await this.AppDataSource.getRepository(Tool).find()
- return res.json(tools)
- })
-
- // Get specific tool
- this.app.get('/api/v1/tools/:id', async (req: Request, res: Response) => {
- const tool = await this.AppDataSource.getRepository(Tool).findOneBy({
- id: req.params.id
- })
- return res.json(tool)
- })
-
- // Add tool
- this.app.post('/api/v1/tools', async (req: Request, res: Response) => {
- const body = req.body
- const newTool = new Tool()
- Object.assign(newTool, body)
-
- const tool = this.AppDataSource.getRepository(Tool).create(newTool)
- const results = await this.AppDataSource.getRepository(Tool).save(tool)
-
- await this.telemetry.sendTelemetry('tool_created', {
- version: await getAppVersion(),
- toolId: results.id,
- toolName: results.name
- })
-
- return res.json(results)
- })
-
- // Update tool
- this.app.put('/api/v1/tools/:id', async (req: Request, res: Response) => {
- const tool = await this.AppDataSource.getRepository(Tool).findOneBy({
- id: req.params.id
- })
-
- if (!tool) {
- res.status(404).send(`Tool ${req.params.id} not found`)
- return
- }
-
- const body = req.body
- const updateTool = new Tool()
- Object.assign(updateTool, body)
-
- this.AppDataSource.getRepository(Tool).merge(tool, updateTool)
- const result = await this.AppDataSource.getRepository(Tool).save(tool)
-
- return res.json(result)
- })
-
- // Delete tool
- this.app.delete('/api/v1/tools/:id', async (req: Request, res: Response) => {
- const results = await this.AppDataSource.getRepository(Tool).delete({ id: req.params.id })
- return res.json(results)
- })
-
- // ----------------------------------------
- // Assistant
- // ----------------------------------------
-
- // Get all assistants
- this.app.get('/api/v1/assistants', async (req: Request, res: Response) => {
- const assistants = await this.AppDataSource.getRepository(Assistant).find()
- return res.json(assistants)
- })
-
- // Get specific assistant
- this.app.get('/api/v1/assistants/:id', async (req: Request, res: Response) => {
- const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({
- id: req.params.id
- })
- return res.json(assistant)
- })
-
- // Get assistant object
- this.app.get('/api/v1/openai-assistants/:id', async (req: Request, res: Response) => {
- const credentialId = req.query.credential as string
- const credential = await this.AppDataSource.getRepository(Credential).findOneBy({
- id: credentialId
- })
-
- if (!credential) return res.status(404).send(`Credential ${credentialId} not found`)
-
- // Decrpyt credentialData
- const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
- const openAIApiKey = decryptedCredentialData['openAIApiKey']
- if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`)
-
- const openai = new OpenAI({ apiKey: openAIApiKey })
- const retrievedAssistant = await openai.beta.assistants.retrieve(req.params.id)
- const resp = await openai.files.list()
- const existingFiles = resp.data ?? []
-
- if (retrievedAssistant.file_ids && retrievedAssistant.file_ids.length) {
- ;(retrievedAssistant as any).files = existingFiles.filter((file) => retrievedAssistant.file_ids.includes(file.id))
- }
-
- return res.json(retrievedAssistant)
- })
-
- // List available assistants
- this.app.get('/api/v1/openai-assistants', async (req: Request, res: Response) => {
- const credentialId = req.query.credential as string
- const credential = await this.AppDataSource.getRepository(Credential).findOneBy({
- id: credentialId
- })
-
- if (!credential) return res.status(404).send(`Credential ${credentialId} not found`)
-
- // Decrpyt credentialData
- const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
- const openAIApiKey = decryptedCredentialData['openAIApiKey']
- if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`)
-
- const openai = new OpenAI({ apiKey: openAIApiKey })
- const retrievedAssistants = await openai.beta.assistants.list()
-
- return res.json(retrievedAssistants.data)
- })
-
- // Add assistant
- this.app.post('/api/v1/assistants', async (req: Request, res: Response) => {
- const body = req.body
-
- if (!body.details) return res.status(500).send(`Invalid request body`)
-
- const assistantDetails = JSON.parse(body.details)
-
- try {
- const credential = await this.AppDataSource.getRepository(Credential).findOneBy({
- id: body.credential
- })
-
- if (!credential) return res.status(404).send(`Credential ${body.credential} not found`)
-
- // Decrpyt credentialData
- const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
- const openAIApiKey = decryptedCredentialData['openAIApiKey']
- if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`)
-
- const openai = new OpenAI({ apiKey: openAIApiKey })
-
- let tools = []
- if (assistantDetails.tools) {
- for (const tool of assistantDetails.tools ?? []) {
- tools.push({
- type: tool
- })
- }
- }
-
- if (assistantDetails.uploadFiles) {
- // Base64 strings
- let files: string[] = []
- const fileBase64 = assistantDetails.uploadFiles
- if (fileBase64.startsWith('[') && fileBase64.endsWith(']')) {
- files = JSON.parse(fileBase64)
- } else {
- files = [fileBase64]
- }
-
- const uploadedFiles = []
- for (const file of files) {
- const splitDataURI = file.split(',')
- const filename = splitDataURI.pop()?.split(':')[1] ?? ''
- const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
- const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', filename)
- if (!fs.existsSync(path.join(getUserHome(), '.flowise', 'openai-assistant'))) {
- fs.mkdirSync(path.dirname(filePath), { recursive: true })
- }
- if (!fs.existsSync(filePath)) {
- fs.writeFileSync(filePath, bf)
- }
-
- const createdFile = await openai.files.create({
- file: fs.createReadStream(filePath),
- purpose: 'assistants'
- })
- uploadedFiles.push(createdFile)
-
- fs.unlinkSync(filePath)
- }
- assistantDetails.files = [...assistantDetails.files, ...uploadedFiles]
- }
-
- if (!assistantDetails.id) {
- const newAssistant = await openai.beta.assistants.create({
- name: assistantDetails.name,
- description: assistantDetails.description,
- instructions: assistantDetails.instructions,
- model: assistantDetails.model,
- tools,
- file_ids: (assistantDetails.files ?? []).map((file: OpenAI.Files.FileObject) => file.id)
- })
- assistantDetails.id = newAssistant.id
- } else {
- const retrievedAssistant = await openai.beta.assistants.retrieve(assistantDetails.id)
- let filteredTools = uniqWith([...retrievedAssistant.tools, ...tools], isEqual)
- filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function))
-
- await openai.beta.assistants.update(assistantDetails.id, {
- name: assistantDetails.name,
- description: assistantDetails.description ?? '',
- instructions: assistantDetails.instructions ?? '',
- model: assistantDetails.model,
- tools: filteredTools,
- file_ids: uniqWith(
- [
- ...retrievedAssistant.file_ids,
- ...(assistantDetails.files ?? []).map((file: OpenAI.Files.FileObject) => file.id)
- ],
- isEqual
- )
- })
- }
-
- const newAssistantDetails = {
- ...assistantDetails
- }
- if (newAssistantDetails.uploadFiles) delete newAssistantDetails.uploadFiles
-
- body.details = JSON.stringify(newAssistantDetails)
- } catch (error) {
- return res.status(500).send(`Error creating new assistant: ${error}`)
- }
-
- const newAssistant = new Assistant()
- Object.assign(newAssistant, body)
-
- const assistant = this.AppDataSource.getRepository(Assistant).create(newAssistant)
- const results = await this.AppDataSource.getRepository(Assistant).save(assistant)
-
- await this.telemetry.sendTelemetry('assistant_created', {
- version: await getAppVersion(),
- assistantId: results.id
- })
-
- return res.json(results)
- })
-
- // Update assistant
- this.app.put('/api/v1/assistants/:id', async (req: Request, res: Response) => {
- const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({
- id: req.params.id
- })
-
- if (!assistant) {
- res.status(404).send(`Assistant ${req.params.id} not found`)
- return
- }
-
- try {
- const openAIAssistantId = JSON.parse(assistant.details)?.id
-
- const body = req.body
- const assistantDetails = JSON.parse(body.details)
-
- const credential = await this.AppDataSource.getRepository(Credential).findOneBy({
- id: body.credential
- })
-
- if (!credential) return res.status(404).send(`Credential ${body.credential} not found`)
-
- // Decrpyt credentialData
- const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
- const openAIApiKey = decryptedCredentialData['openAIApiKey']
- if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`)
-
- const openai = new OpenAI({ apiKey: openAIApiKey })
-
- let tools = []
- if (assistantDetails.tools) {
- for (const tool of assistantDetails.tools ?? []) {
- tools.push({
- type: tool
- })
- }
- }
-
- if (assistantDetails.uploadFiles) {
- // Base64 strings
- let files: string[] = []
- const fileBase64 = assistantDetails.uploadFiles
- if (fileBase64.startsWith('[') && fileBase64.endsWith(']')) {
- files = JSON.parse(fileBase64)
- } else {
- files = [fileBase64]
- }
-
- const uploadedFiles = []
- for (const file of files) {
- const splitDataURI = file.split(',')
- const filename = splitDataURI.pop()?.split(':')[1] ?? ''
- const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
- const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', filename)
- if (!fs.existsSync(path.join(getUserHome(), '.flowise', 'openai-assistant'))) {
- fs.mkdirSync(path.dirname(filePath), { recursive: true })
- }
- if (!fs.existsSync(filePath)) {
- fs.writeFileSync(filePath, bf)
- }
-
- const createdFile = await openai.files.create({
- file: fs.createReadStream(filePath),
- purpose: 'assistants'
- })
- uploadedFiles.push(createdFile)
-
- fs.unlinkSync(filePath)
- }
- assistantDetails.files = [...assistantDetails.files, ...uploadedFiles]
- }
-
- const retrievedAssistant = await openai.beta.assistants.retrieve(openAIAssistantId)
- let filteredTools = uniqWith([...retrievedAssistant.tools, ...tools], isEqual)
- filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function))
-
- await openai.beta.assistants.update(openAIAssistantId, {
- name: assistantDetails.name,
- description: assistantDetails.description,
- instructions: assistantDetails.instructions,
- model: assistantDetails.model,
- tools: filteredTools,
- file_ids: uniqWith(
- [...retrievedAssistant.file_ids, ...(assistantDetails.files ?? []).map((file: OpenAI.Files.FileObject) => file.id)],
- isEqual
- )
- })
-
- const newAssistantDetails = {
- ...assistantDetails,
- id: openAIAssistantId
- }
- if (newAssistantDetails.uploadFiles) delete newAssistantDetails.uploadFiles
-
- const updateAssistant = new Assistant()
- body.details = JSON.stringify(newAssistantDetails)
- Object.assign(updateAssistant, body)
-
- this.AppDataSource.getRepository(Assistant).merge(assistant, updateAssistant)
- const result = await this.AppDataSource.getRepository(Assistant).save(assistant)
-
- return res.json(result)
- } catch (error) {
- return res.status(500).send(`Error updating assistant: ${error}`)
- }
- })
-
- // Delete assistant
- this.app.delete('/api/v1/assistants/:id', async (req: Request, res: Response) => {
- const assistant = await this.AppDataSource.getRepository(Assistant).findOneBy({
- id: req.params.id
- })
-
- if (!assistant) {
- res.status(404).send(`Assistant ${req.params.id} not found`)
- return
- }
-
- try {
- const assistantDetails = JSON.parse(assistant.details)
-
- const credential = await this.AppDataSource.getRepository(Credential).findOneBy({
- id: assistant.credential
- })
-
- if (!credential) return res.status(404).send(`Credential ${assistant.credential} not found`)
-
- // Decrpyt credentialData
- const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
- const openAIApiKey = decryptedCredentialData['openAIApiKey']
- if (!openAIApiKey) return res.status(404).send(`OpenAI ApiKey not found`)
-
- const openai = new OpenAI({ apiKey: openAIApiKey })
-
- const results = await this.AppDataSource.getRepository(Assistant).delete({ id: req.params.id })
-
- if (req.query.isDeleteBoth) await openai.beta.assistants.del(assistantDetails.id)
-
- return res.json(results)
- } catch (error: any) {
- if (error.status === 404 && error.type === 'invalid_request_error') return res.send('OK')
- return res.status(500).send(`Error deleting assistant: ${error}`)
- }
- })
-
- function streamFileToUser(res: Response, filePath: string) {
- const fileStream = fs.createReadStream(filePath)
- fileStream.pipe(res)
- }
-
- // Download file from assistant
- this.app.post('/api/v1/openai-assistants-file', async (req: Request, res: Response) => {
- const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', req.body.fileName)
- //raise error if file path is not absolute
- if (!path.isAbsolute(filePath)) return res.status(500).send(`Invalid file path`)
- //raise error if file path contains '..'
- if (filePath.includes('..')) return res.status(500).send(`Invalid file path`)
- //only return from the .flowise openai-assistant folder
- if (!(filePath.includes('.flowise') && filePath.includes('openai-assistant'))) return res.status(500).send(`Invalid file path`)
-
- if (fs.existsSync(filePath)) {
- res.setHeader('Content-Disposition', contentDisposition(path.basename(filePath)))
- streamFileToUser(res, filePath)
- } else {
- return res.status(404).send(`File ${req.body.fileName} not found`)
- }
- })
-
- this.app.get('/api/v1/get-upload-path', async (req: Request, res: Response) => {
- return res.json({
- storagePath: getStoragePath()
- })
- })
-
- // stream uploaded image
- this.app.get('/api/v1/get-upload-file', async (req: Request, res: Response) => {
- try {
- if (!req.query.chatflowId || !req.query.chatId || !req.query.fileName) {
- return res.status(500).send(`Invalid file path`)
- }
- const chatflowId = req.query.chatflowId as string
- const chatId = req.query.chatId as string
- const fileName = req.query.fileName as string
-
- const filePath = path.join(getStoragePath(), chatflowId, chatId, fileName)
- //raise error if file path is not absolute
- if (!path.isAbsolute(filePath)) return res.status(500).send(`Invalid file path`)
- //raise error if file path contains '..'
- if (filePath.includes('..')) return res.status(500).send(`Invalid file path`)
- //only return from the storage folder
- if (!filePath.startsWith(getStoragePath())) return res.status(500).send(`Invalid file path`)
-
- if (fs.existsSync(filePath)) {
- res.setHeader('Content-Disposition', contentDisposition(path.basename(filePath)))
- streamFileToUser(res, filePath)
- } else {
- return res.status(404).send(`File ${fileName} not found`)
- }
- } catch (error) {
- return res.status(500).send(`Invalid file path`)
- }
- })
-
- // ----------------------------------------
- // Configuration
- // ----------------------------------------
-
- this.app.get('/api/v1/flow-config/:id', async (req: Request, res: Response) => {
- const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
- id: req.params.id
- })
- if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`)
- const flowData = chatflow.flowData
- const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
- const nodes = parsedFlowData.nodes
- const availableConfigs = findAvailableConfigs(nodes, this.nodesPool.componentCredentials)
- return res.json(availableConfigs)
- })
-
- this.app.post('/api/v1/node-config', async (req: Request, res: Response) => {
- const nodes = [{ data: req.body }] as IReactFlowNode[]
- const availableConfigs = findAvailableConfigs(nodes, this.nodesPool.componentCredentials)
- return res.json(availableConfigs)
- })
-
- this.app.get('/api/v1/version', async (req: Request, res: Response) => {
- const getPackageJsonPath = (): string => {
- const checkPaths = [
- path.join(__dirname, '..', 'package.json'),
- path.join(__dirname, '..', '..', 'package.json'),
- path.join(__dirname, '..', '..', '..', 'package.json'),
- path.join(__dirname, '..', '..', '..', '..', 'package.json'),
- path.join(__dirname, '..', '..', '..', '..', '..', 'package.json')
- ]
- for (const checkPath of checkPaths) {
- if (fs.existsSync(checkPath)) {
- return checkPath
- }
- }
- return ''
- }
-
- const packagejsonPath = getPackageJsonPath()
- if (!packagejsonPath) return res.status(404).send('Version not found')
- try {
- const content = await fs.promises.readFile(packagejsonPath, 'utf8')
- const parsedContent = JSON.parse(content)
- return res.json({ version: parsedContent.version })
- } catch (error) {
- return res.status(500).send(`Version not found: ${error}`)
- }
- })
-
- // ----------------------------------------
- // Scraper
- // ----------------------------------------
-
- this.app.get('/api/v1/fetch-links', async (req: Request, res: Response) => {
- try {
- const url = decodeURIComponent(req.query.url as string)
- const relativeLinksMethod = req.query.relativeLinksMethod as string
- if (!relativeLinksMethod) {
- return res.status(500).send('Please choose a Relative Links Method in Additional Parameters.')
- }
-
- const limit = parseInt(req.query.limit as string)
- if (process.env.DEBUG === 'true') console.info(`Start ${relativeLinksMethod}`)
- const links: string[] = relativeLinksMethod === 'webCrawl' ? await webCrawl(url, limit) : await xmlScrape(url, limit)
- if (process.env.DEBUG === 'true') console.info(`Finish ${relativeLinksMethod}`)
-
- res.json({ status: 'OK', links })
- } catch (e: any) {
- return res.status(500).send('Could not fetch links from the URL.')
- }
- })
-
- // ----------------------------------------
- // Upsert
- // ----------------------------------------
-
- this.app.post(
- '/api/v1/vector/upsert/:id',
- upload.array('files'),
- (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next),
- async (req: Request, res: Response) => {
- await this.upsertVector(req, res)
- }
- )
-
- this.app.post('/api/v1/vector/internal-upsert/:id', async (req: Request, res: Response) => {
- await this.upsertVector(req, res, true)
- })
-
- // ----------------------------------------
- // Prompt from Hub
- // ----------------------------------------
- this.app.post('/api/v1/load-prompt', async (req: Request, res: Response) => {
- try {
- let hub = new Client()
- const prompt = await hub.pull(req.body.promptName)
- const templates = parsePrompt(prompt)
- return res.json({ status: 'OK', prompt: req.body.promptName, templates: templates })
- } catch (e: any) {
- return res.json({ status: 'ERROR', prompt: req.body.promptName, error: e?.message })
- }
- })
-
- this.app.post('/api/v1/prompts-list', async (req: Request, res: Response) => {
- try {
- const tags = req.body.tags ? `tags=${req.body.tags}` : ''
- // Default to 100, TODO: add pagination and use offset & limit
- const url = `https://api.hub.langchain.com/repos/?limit=100&${tags}has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false`
- axios.get(url).then((response) => {
- if (response.data.repos) {
- return res.json({ status: 'OK', repos: response.data.repos })
- }
- })
- } catch (e: any) {
- return res.json({ status: 'ERROR', repos: [] })
- }
- })
-
- // ----------------------------------------
- // Prediction
- // ----------------------------------------
-
- // Send input message and get prediction result (External)
- this.app.post(
- '/api/v1/prediction/:id',
- upload.array('files'),
- (req: Request, res: Response, next: NextFunction) => getRateLimiter(req, res, next),
- async (req: Request, res: Response) => {
- const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
- id: req.params.id
- })
- if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`)
- let isDomainAllowed = true
- logger.info(`[server]: Request originated from ${req.headers.origin}`)
- if (chatflow.chatbotConfig) {
- const parsedConfig = JSON.parse(chatflow.chatbotConfig)
- // check whether the first one is not empty. if it is empty that means the user set a value and then removed it.
- const isValidAllowedOrigins = parsedConfig.allowedOrigins?.length && parsedConfig.allowedOrigins[0] !== ''
- if (isValidAllowedOrigins) {
- const originHeader = req.headers.origin as string
- const origin = new URL(originHeader).host
- isDomainAllowed =
- parsedConfig.allowedOrigins.filter((domain: string) => {
- try {
- const allowedOrigin = new URL(domain).host
- return origin === allowedOrigin
- } catch (e) {
- return false
- }
- }).length > 0
- }
- }
-
- if (isDomainAllowed) {
- await this.buildChatflow(req, res, socketIO)
- } else {
- return res.status(401).send(`This site is not allowed to access this chatbot`)
- }
- }
- )
-
- // Send input message and get prediction result (Internal)
- this.app.post('/api/v1/internal-prediction/:id', async (req: Request, res: Response) => {
- await this.buildChatflow(req, res, socketIO, true)
- })
-
- // ----------------------------------------
- // Marketplaces
- // ----------------------------------------
-
- // Get all templates for marketplaces
- this.app.get('/api/v1/marketplaces/templates', async (req: Request, res: Response) => {
- let marketplaceDir = path.join(__dirname, '..', 'marketplaces', 'chatflows')
- let jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json')
- let templates: any[] = []
- jsonsInDir.forEach((file, index) => {
- const filePath = path.join(__dirname, '..', 'marketplaces', 'chatflows', file)
- const fileData = fs.readFileSync(filePath)
- const fileDataObj = JSON.parse(fileData.toString())
- const template = {
- id: index,
- templateName: file.split('.json')[0],
- flowData: fileData.toString(),
- badge: fileDataObj?.badge,
- framework: fileDataObj?.framework,
- categories: fileDataObj?.categories,
- type: 'Chatflow',
- description: fileDataObj?.description || ''
- }
- templates.push(template)
- })
-
- marketplaceDir = path.join(__dirname, '..', 'marketplaces', 'tools')
- jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json')
- jsonsInDir.forEach((file, index) => {
- const filePath = path.join(__dirname, '..', 'marketplaces', 'tools', file)
- const fileData = fs.readFileSync(filePath)
- const fileDataObj = JSON.parse(fileData.toString())
- const template = {
- ...fileDataObj,
- id: index,
- type: 'Tool',
- framework: fileDataObj?.framework,
- badge: fileDataObj?.badge,
- categories: '',
- templateName: file.split('.json')[0]
- }
- templates.push(template)
- })
- const sortedTemplates = templates.sort((a, b) => a.templateName.localeCompare(b.templateName))
- const FlowiseDocsQnAIndex = sortedTemplates.findIndex((tmp) => tmp.templateName === 'Flowise Docs QnA')
- if (FlowiseDocsQnAIndex > 0) {
- sortedTemplates.unshift(sortedTemplates.splice(FlowiseDocsQnAIndex, 1)[0])
- }
- return res.json(sortedTemplates)
- })
-
- // ----------------------------------------
- // Variables
- // ----------------------------------------
- this.app.get('/api/v1/variables', async (req: Request, res: Response) => {
- const variables = await getDataSource().getRepository(Variable).find()
- return res.json(variables)
- })
-
- // Create new variable
- this.app.post('/api/v1/variables', async (req: Request, res: Response) => {
- const body = req.body
- const newVariable = new Variable()
- Object.assign(newVariable, body)
- const variable = this.AppDataSource.getRepository(Variable).create(newVariable)
- const results = await this.AppDataSource.getRepository(Variable).save(variable)
- return res.json(results)
- })
-
- // Update variable
- this.app.put('/api/v1/variables/:id', async (req: Request, res: Response) => {
- const variable = await this.AppDataSource.getRepository(Variable).findOneBy({
- id: req.params.id
- })
-
- if (!variable) return res.status(404).send(`Variable ${req.params.id} not found`)
-
- const body = req.body
- const updateVariable = new Variable()
- Object.assign(updateVariable, body)
- this.AppDataSource.getRepository(Variable).merge(variable, updateVariable)
- const result = await this.AppDataSource.getRepository(Variable).save(variable)
-
- return res.json(result)
- })
-
- // Delete variable via id
- this.app.delete('/api/v1/variables/:id', async (req: Request, res: Response) => {
- const results = await this.AppDataSource.getRepository(Variable).delete({ id: req.params.id })
- return res.json(results)
- })
-
- // ----------------------------------------
- // API Keys
- // ----------------------------------------
-
- const addChatflowsCount = async (keys: any, res: Response) => {
- if (keys) {
- const updatedKeys: any[] = []
- //iterate through keys and get chatflows
- for (const key of keys) {
- const chatflows = await this.AppDataSource.getRepository(ChatFlow)
- .createQueryBuilder('cf')
- .where('cf.apikeyid = :apikeyid', { apikeyid: key.id })
- .getMany()
- const linkedChatFlows: any[] = []
- chatflows.map((cf) => {
- linkedChatFlows.push({
- flowName: cf.name,
- category: cf.category,
- updatedDate: cf.updatedDate
- })
- })
- key.chatFlows = linkedChatFlows
- updatedKeys.push(key)
- }
- return res.json(updatedKeys)
- }
- return res.json(keys)
- }
- // Get api keys
- this.app.get('/api/v1/apikey', async (req: Request, res: Response) => {
- const keys = await getAPIKeys()
- return addChatflowsCount(keys, res)
- })
-
- // Add new api key
- this.app.post('/api/v1/apikey', async (req: Request, res: Response) => {
- const keys = await addAPIKey(req.body.keyName)
- return addChatflowsCount(keys, res)
- })
-
- // Update api key
- this.app.put('/api/v1/apikey/:id', async (req: Request, res: Response) => {
- const keys = await updateAPIKey(req.params.id, req.body.keyName)
- return addChatflowsCount(keys, res)
- })
-
- // Delete new api key
- this.app.delete('/api/v1/apikey/:id', async (req: Request, res: Response) => {
- const keys = await deleteAPIKey(req.params.id)
- return addChatflowsCount(keys, res)
- })
-
- // Verify api key
- this.app.get('/api/v1/verify/apikey/:apiKey', async (req: Request, res: Response) => {
- try {
- const apiKey = await getApiKey(req.params.apiKey)
- if (!apiKey) return res.status(401).send('Unauthorized')
- return res.status(200).send('OK')
- } catch (err: any) {
- return res.status(500).send(err?.message)
- }
- })
+ this.app.use('/api/v1', flowiseApiV1Router)
// ----------------------------------------
// Serve UI static
@@ -1636,750 +160,12 @@ export class App {
this.app.use('/', express.static(uiBuildPath))
// All other requests not handled will return React app
- this.app.use((req, res) => {
+ this.app.use((req: Request, res: Response) => {
res.sendFile(uiHtmlPath)
})
- }
- /**
- * Validate API Key
- * @param {Request} req
- * @param {Response} res
- * @param {ChatFlow} chatflow
- */
- async validateKey(req: Request, chatflow: ChatFlow) {
- const chatFlowApiKeyId = chatflow.apikeyid
- if (!chatFlowApiKeyId) return true
-
- const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? ''
- if (chatFlowApiKeyId && !authorizationHeader) return false
-
- const suppliedKey = authorizationHeader.split(`Bearer `).pop()
- if (suppliedKey) {
- const keys = await getAPIKeys()
- const apiSecret = keys.find((key) => key.id === chatFlowApiKeyId)?.apiSecret
- if (!compareKeys(apiSecret, suppliedKey)) return false
- return true
- }
- return false
- }
-
- /**
- * Method that checks if uploads are enabled in the chatflow
- * @param {string} chatflowid
- */
- async getUploadsConfig(chatflowid: string): Promise {
- const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
- id: chatflowid
- })
- if (!chatflow) return `Chatflow ${chatflowid} not found`
-
- const uploadAllowedNodes = ['llmChain', 'conversationChain', 'mrklAgentChat', 'conversationalAgent']
- const uploadProcessingNodes = ['chatOpenAI', 'chatAnthropic', 'awsChatBedrock', 'azureChatOpenAI']
-
- const flowObj = JSON.parse(chatflow.flowData)
- const imgUploadSizeAndTypes: IUploadFileSizeAndTypes[] = []
-
- let isSpeechToTextEnabled = false
- if (chatflow.speechToText) {
- const speechToTextProviders = JSON.parse(chatflow.speechToText)
- for (const provider in speechToTextProviders) {
- if (provider !== 'none') {
- const providerObj = speechToTextProviders[provider]
- if (providerObj.status) {
- isSpeechToTextEnabled = true
- break
- }
- }
- }
- }
-
- let isImageUploadAllowed = false
- const nodes: IReactFlowNode[] = flowObj.nodes
-
- /*
- * Condition for isImageUploadAllowed
- * 1.) one of the uploadAllowedNodes exists
- * 2.) one of the uploadProcessingNodes exists + allowImageUploads is ON
- */
- if (!nodes.some((node) => uploadAllowedNodes.includes(node.data.name))) {
- return {
- isSpeechToTextEnabled,
- isImageUploadAllowed: false,
- imgUploadSizeAndTypes
- }
- }
-
- nodes.forEach((node: IReactFlowNode) => {
- if (uploadProcessingNodes.indexOf(node.data.name) > -1) {
- // TODO: for now the maxUploadSize is hardcoded to 5MB, we need to add it to the node properties
- node.data.inputParams.map((param: INodeParams) => {
- if (param.name === 'allowImageUploads' && node.data.inputs?.['allowImageUploads']) {
- imgUploadSizeAndTypes.push({
- fileTypes: 'image/gif;image/jpeg;image/png;image/webp;'.split(';'),
- maxUploadSize: 5
- })
- isImageUploadAllowed = true
- }
- })
- }
- })
-
- return {
- isSpeechToTextEnabled,
- isImageUploadAllowed,
- imgUploadSizeAndTypes
- }
- }
-
- /**
- * Method that get chat messages.
- * @param {string} chatflowid
- * @param {chatType} chatType
- * @param {string} sortOrder
- * @param {string} chatId
- * @param {string} memoryType
- * @param {string} sessionId
- * @param {string} startDate
- * @param {string} endDate
- * @param {boolean} feedback
- */
- async getChatMessage(
- chatflowid: string,
- chatType: chatType | undefined,
- sortOrder: string = 'ASC',
- chatId?: string,
- memoryType?: string,
- sessionId?: string,
- startDate?: string,
- endDate?: string,
- messageId?: string,
- feedback?: boolean
- ): Promise {
- const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => {
- const date = new Date(dateTimeStr)
- if (isNaN(date.getTime())) {
- return undefined
- }
- setHours === 'start' ? date.setHours(0, 0, 0, 0) : date.setHours(23, 59, 59, 999)
- return date
- }
-
- const aMonthAgo = () => {
- const date = new Date()
- date.setMonth(new Date().getMonth() - 1)
- return date
- }
-
- let fromDate
- if (startDate) fromDate = setDateToStartOrEndOfDay(startDate, 'start')
-
- let toDate
- if (endDate) toDate = setDateToStartOrEndOfDay(endDate, 'end')
-
- if (feedback) {
- const query = this.AppDataSource.getRepository(ChatMessage).createQueryBuilder('chat_message')
-
- // do the join with chat message feedback based on messageId for each chat message in the chatflow
- query
- .leftJoinAndMapOne('chat_message.feedback', ChatMessageFeedback, 'feedback', 'feedback.messageId = chat_message.id')
- .where('chat_message.chatflowid = :chatflowid', { chatflowid })
-
- // based on which parameters are available add `andWhere` clauses to the query
- if (chatType) {
- query.andWhere('chat_message.chatType = :chatType', { chatType })
- }
- if (chatId) {
- query.andWhere('chat_message.chatId = :chatId', { chatId })
- }
- if (memoryType) {
- query.andWhere('chat_message.memoryType = :memoryType', { memoryType })
- }
- if (sessionId) {
- query.andWhere('chat_message.sessionId = :sessionId', { sessionId })
- }
-
- // set date range
- query.andWhere('chat_message.createdDate BETWEEN :fromDate AND :toDate', {
- fromDate: fromDate ?? aMonthAgo(),
- toDate: toDate ?? new Date()
- })
- // sort
- query.orderBy('chat_message.createdDate', sortOrder === 'DESC' ? 'DESC' : 'ASC')
-
- const messages = await query.getMany()
- return messages
- }
-
- return await this.AppDataSource.getRepository(ChatMessage).find({
- where: {
- chatflowid,
- chatType,
- chatId,
- memoryType: memoryType ?? undefined,
- sessionId: sessionId ?? undefined,
- ...(fromDate && { createdDate: MoreThanOrEqual(fromDate) }),
- ...(toDate && { createdDate: LessThanOrEqual(toDate) }),
- id: messageId ?? undefined
- },
- order: {
- createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC'
- }
- })
- }
-
- /**
- * Method that add chat messages.
- * @param {Partial} chatMessage
- */
- async addChatMessage(chatMessage: Partial): Promise {
- const newChatMessage = new ChatMessage()
- Object.assign(newChatMessage, chatMessage)
-
- if (!newChatMessage.createdDate) newChatMessage.createdDate = new Date()
-
- const chatmessage = this.AppDataSource.getRepository(ChatMessage).create(newChatMessage)
- return await this.AppDataSource.getRepository(ChatMessage).save(chatmessage)
- }
-
- /**
- * Method that get chat messages.
- * @param {string} chatflowid
- * @param {string} sortOrder
- * @param {string} chatId
- * @param {string} startDate
- * @param {string} endDate
- */
- async getChatMessageFeedback(
- chatflowid: string,
- chatId?: string,
- sortOrder: string = 'ASC',
- startDate?: string,
- endDate?: string
- ): Promise {
- let fromDate
- if (startDate) fromDate = new Date(startDate)
-
- let toDate
- if (endDate) toDate = new Date(endDate)
- return await this.AppDataSource.getRepository(ChatMessageFeedback).find({
- where: {
- chatflowid,
- chatId,
- createdDate: toDate && fromDate ? Between(fromDate, toDate) : undefined
- },
- order: {
- createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC'
- }
- })
- }
-
- /**
- * Method that add chat message feedback.
- * @param {Partial} chatMessageFeedback
- */
- async addChatMessageFeedback(chatMessageFeedback: Partial): Promise {
- const newChatMessageFeedback = new ChatMessageFeedback()
- Object.assign(newChatMessageFeedback, chatMessageFeedback)
-
- const feedback = this.AppDataSource.getRepository(ChatMessageFeedback).create(newChatMessageFeedback)
- return await this.AppDataSource.getRepository(ChatMessageFeedback).save(feedback)
- }
-
- /**
- * Method that updates chat message feedback.
- * @param {string} id
- * @param {Partial} chatMessageFeedback
- */
- async updateChatMessageFeedback(id: string, chatMessageFeedback: Partial) {
- const newChatMessageFeedback = new ChatMessageFeedback()
- Object.assign(newChatMessageFeedback, chatMessageFeedback)
-
- await this.AppDataSource.getRepository(ChatMessageFeedback).update({ id }, chatMessageFeedback)
- }
-
- async upsertVector(req: Request, res: Response, isInternal: boolean = false) {
- try {
- const chatflowid = req.params.id
- let incomingInput: IncomingInput = req.body
-
- const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
- id: chatflowid
- })
- if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`)
-
- if (!isInternal) {
- const isKeyValidated = await this.validateKey(req, chatflow)
- if (!isKeyValidated) return res.status(401).send('Unauthorized')
- }
-
- const files = (req.files as any[]) || []
-
- if (files.length) {
- const overrideConfig: ICommonObject = { ...req.body }
- for (const file of files) {
- const fileData = fs.readFileSync(file.path, { encoding: 'base64' })
- const dataBase64String = `data:${file.mimetype};base64,${fileData},filename:${file.filename}`
-
- const fileInputField = mapMimeTypeToInputField(file.mimetype)
- if (overrideConfig[fileInputField]) {
- overrideConfig[fileInputField] = JSON.stringify([...JSON.parse(overrideConfig[fileInputField]), dataBase64String])
- } else {
- overrideConfig[fileInputField] = JSON.stringify([dataBase64String])
- }
- }
- incomingInput = {
- question: req.body.question ?? 'hello',
- overrideConfig,
- history: [],
- stopNodeId: req.body.stopNodeId
- }
- }
-
- /*** Get chatflows and prepare data ***/
- const flowData = chatflow.flowData
- const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
- const nodes = parsedFlowData.nodes
- const edges = parsedFlowData.edges
-
- let stopNodeId = incomingInput?.stopNodeId ?? ''
- let chatHistory = incomingInput?.history
- let chatId = incomingInput.chatId ?? ''
- let isUpsert = true
-
- // Get session ID
- const memoryNode = findMemoryNode(nodes, edges)
- let sessionId = undefined
- if (memoryNode) sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal)
-
- const vsNodes = nodes.filter(
- (node) =>
- node.data.category === 'Vector Stores' &&
- !node.data.label.includes('Upsert') &&
- !node.data.label.includes('Load Existing')
- )
- if (vsNodes.length > 1 && !stopNodeId) {
- return res.status(500).send('There are multiple vector nodes, please provide stopNodeId in body request')
- } else if (vsNodes.length === 1 && !stopNodeId) {
- stopNodeId = vsNodes[0].data.id
- } else if (!vsNodes.length && !stopNodeId) {
- return res.status(500).send('No vector node found')
- }
-
- const { graph } = constructGraphs(nodes, edges, { isReversed: true })
-
- const nodeIds = getAllConnectedNodes(graph, stopNodeId)
-
- const filteredGraph: INodeDirectedGraph = {}
- for (const key of nodeIds) {
- if (Object.prototype.hasOwnProperty.call(graph, key)) {
- filteredGraph[key] = graph[key]
- }
- }
-
- const { startingNodeIds, depthQueue } = getStartingNodes(filteredGraph, stopNodeId)
-
- await buildFlow(
- startingNodeIds,
- nodes,
- edges,
- filteredGraph,
- depthQueue,
- this.nodesPool.componentNodes,
- incomingInput.question,
- chatHistory,
- chatId,
- sessionId ?? '',
- chatflowid,
- this.AppDataSource,
- incomingInput?.overrideConfig,
- this.cachePool,
- isUpsert,
- stopNodeId
- )
-
- const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.data.id))
-
- this.chatflowPool.add(chatflowid, undefined, startingNodes, incomingInput?.overrideConfig)
-
- await this.telemetry.sendTelemetry('vector_upserted', {
- version: await getAppVersion(),
- chatflowId: chatflowid,
- type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
- flowGraph: getTelemetryFlowObj(nodes, edges),
- stopNodeId
- })
-
- return res.status(201).send('Successfully Upserted')
- } catch (e: any) {
- logger.error('[server]: Error:', e)
- return res.status(500).send(e.message)
- }
- }
-
- /**
- * Build Chatflow
- * @param {Request} req
- * @param {Response} res
- * @param {Server} socketIO
- * @param {boolean} isInternal
- * @param {boolean} isUpsert
- */
- async buildChatflow(req: Request, res: Response, socketIO?: Server, isInternal: boolean = false) {
- try {
- const chatflowid = req.params.id
- let incomingInput: IncomingInput = req.body
-
- let nodeToExecuteData: INodeData
-
- const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
- id: chatflowid
- })
- if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`)
-
- const chatId = incomingInput.chatId ?? incomingInput.overrideConfig?.sessionId ?? uuidv4()
- const userMessageDateTime = new Date()
-
- if (!isInternal) {
- const isKeyValidated = await this.validateKey(req, chatflow)
- if (!isKeyValidated) return res.status(401).send('Unauthorized')
- }
-
- let fileUploads: IFileUpload[] = []
- if (incomingInput.uploads) {
- fileUploads = incomingInput.uploads
- for (let i = 0; i < fileUploads.length; i += 1) {
- const upload = fileUploads[i]
- if ((upload.type === 'file' || upload.type === 'audio') && upload.data) {
- const filename = upload.name
- const dir = path.join(getStoragePath(), chatflowid, chatId)
- if (!fs.existsSync(dir)) {
- fs.mkdirSync(dir, { recursive: true })
- }
- const filePath = path.join(dir, filename)
- const splitDataURI = upload.data.split(',')
- const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
- fs.writeFileSync(filePath, bf)
-
- // Omit upload.data since we don't store the content in database
- upload.type = 'stored-file'
- fileUploads[i] = omit(upload, ['data'])
- }
-
- // Run Speech to Text conversion
- if (upload.mime === 'audio/webm') {
- let speechToTextConfig: ICommonObject = {}
- if (chatflow.speechToText) {
- const speechToTextProviders = JSON.parse(chatflow.speechToText)
- for (const provider in speechToTextProviders) {
- const providerObj = speechToTextProviders[provider]
- if (providerObj.status) {
- speechToTextConfig = providerObj
- speechToTextConfig['name'] = provider
- break
- }
- }
- }
- if (speechToTextConfig) {
- const options: ICommonObject = {
- chatId,
- chatflowid,
- appDataSource: this.AppDataSource,
- databaseEntities: databaseEntities
- }
- const speechToTextResult = await convertSpeechToText(upload, speechToTextConfig, options)
- if (speechToTextResult) {
- incomingInput.question = speechToTextResult
- }
- }
- }
- }
- }
-
- let isStreamValid = false
-
- const files = (req.files as any[]) || []
-
- if (files.length) {
- const overrideConfig: ICommonObject = { ...req.body }
- for (const file of files) {
- const fileData = fs.readFileSync(file.path, { encoding: 'base64' })
- const dataBase64String = `data:${file.mimetype};base64,${fileData},filename:${file.filename}`
-
- const fileInputField = mapMimeTypeToInputField(file.mimetype)
- if (overrideConfig[fileInputField]) {
- overrideConfig[fileInputField] = JSON.stringify([...JSON.parse(overrideConfig[fileInputField]), dataBase64String])
- } else {
- overrideConfig[fileInputField] = JSON.stringify([dataBase64String])
- }
- }
- incomingInput = {
- question: req.body.question ?? 'hello',
- overrideConfig,
- history: [],
- socketIOClientId: req.body.socketIOClientId
- }
- }
-
- /*** Get chatflows and prepare data ***/
- const flowData = chatflow.flowData
- const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
- const nodes = parsedFlowData.nodes
- const edges = parsedFlowData.edges
-
- // Get session ID
- const memoryNode = findMemoryNode(nodes, edges)
- const memoryType = memoryNode?.data.label
- let sessionId = undefined
- if (memoryNode) sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal)
-
- /* Reuse the flow without having to rebuild (to avoid duplicated upsert, recomputation, reinitialization of memory) when all these conditions met:
- * - Node Data already exists in pool
- * - Still in sync (i.e the flow has not been modified since)
- * - Existing overrideConfig and new overrideConfig are the same
- * - Flow doesn't start with/contain nodes that depend on incomingInput.question
- * TODO: convert overrideConfig to hash when we no longer store base64 string but filepath
- ***/
- const isFlowReusable = () => {
- return (
- Object.prototype.hasOwnProperty.call(this.chatflowPool.activeChatflows, chatflowid) &&
- this.chatflowPool.activeChatflows[chatflowid].inSync &&
- this.chatflowPool.activeChatflows[chatflowid].endingNodeData &&
- isSameOverrideConfig(
- isInternal,
- this.chatflowPool.activeChatflows[chatflowid].overrideConfig,
- incomingInput.overrideConfig
- ) &&
- !isStartNodeDependOnInput(this.chatflowPool.activeChatflows[chatflowid].startingNodes, nodes)
- )
- }
-
- if (isFlowReusable()) {
- nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData as INodeData
- isStreamValid = isFlowValidForStream(nodes, nodeToExecuteData)
- logger.debug(
- `[server]: Reuse existing chatflow ${chatflowid} with ending node ${nodeToExecuteData.label} (${nodeToExecuteData.id})`
- )
- } else {
- /*** Get Ending Node with Directed Graph ***/
- const { graph, nodeDependencies } = constructGraphs(nodes, edges)
- const directedGraph = graph
- const endingNodeIds = getEndingNodes(nodeDependencies, directedGraph)
- if (!endingNodeIds.length) return res.status(500).send(`Ending nodes not found`)
-
- const endingNodes = nodes.filter((nd) => endingNodeIds.includes(nd.id))
-
- let isEndingNodeExists = endingNodes.find((node) => node.data?.outputs?.output === 'EndingNode')
-
- for (const endingNode of endingNodes) {
- const endingNodeData = endingNode.data
- if (!endingNodeData) return res.status(500).send(`Ending node ${endingNode.id} data not found`)
-
- const isEndingNode = endingNodeData?.outputs?.output === 'EndingNode'
-
- if (!isEndingNode) {
- if (
- endingNodeData &&
- endingNodeData.category !== 'Chains' &&
- endingNodeData.category !== 'Agents' &&
- endingNodeData.category !== 'Engine'
- ) {
- return res.status(500).send(`Ending node must be either a Chain or Agent`)
- }
-
- if (
- endingNodeData.outputs &&
- Object.keys(endingNodeData.outputs).length &&
- !Object.values(endingNodeData.outputs ?? {}).includes(endingNodeData.name)
- ) {
- return res
- .status(500)
- .send(
- `Output of ${endingNodeData.label} (${endingNodeData.id}) must be ${endingNodeData.label}, can't be an Output Prediction`
- )
- }
- }
-
- isStreamValid = isFlowValidForStream(nodes, endingNodeData)
- }
-
- // Once custom function ending node exists, flow is always unavailable to stream
- isStreamValid = isEndingNodeExists ? false : isStreamValid
-
- let chatHistory: IMessage[] = incomingInput.history ?? []
-
- // When {{chat_history}} is used in Prompt Template, fetch the chat conversations from memory node
- for (const endingNode of endingNodes) {
- const endingNodeData = endingNode.data
-
- if (!endingNodeData.inputs?.memory) continue
-
- const memoryNodeId = endingNodeData.inputs?.memory.split('.')[0].replace('{{', '')
- const memoryNode = nodes.find((node) => node.data.id === memoryNodeId)
-
- if (!memoryNode) continue
-
- if (!chatHistory.length && (incomingInput.chatId || incomingInput.overrideConfig?.sessionId)) {
- chatHistory = await getSessionChatHistory(
- memoryNode,
- this.nodesPool.componentNodes,
- incomingInput,
- this.AppDataSource,
- databaseEntities,
- logger
- )
- }
- }
-
- /*** Get Starting Nodes with Reversed Graph ***/
- const constructedObj = constructGraphs(nodes, edges, { isReversed: true })
- const nonDirectedGraph = constructedObj.graph
- let startingNodeIds: string[] = []
- let depthQueue: IDepthQueue = {}
- for (const endingNodeId of endingNodeIds) {
- const res = getStartingNodes(nonDirectedGraph, endingNodeId)
- startingNodeIds.push(...res.startingNodeIds)
- depthQueue = Object.assign(depthQueue, res.depthQueue)
- }
- startingNodeIds = [...new Set(startingNodeIds)]
-
- const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id))
-
- logger.debug(`[server]: Start building chatflow ${chatflowid}`)
- /*** BFS to traverse from Starting Nodes to Ending Node ***/
- const reactFlowNodes = await buildFlow(
- startingNodeIds,
- nodes,
- edges,
- graph,
- depthQueue,
- this.nodesPool.componentNodes,
- incomingInput.question,
- chatHistory,
- chatId,
- sessionId ?? '',
- chatflowid,
- this.AppDataSource,
- incomingInput?.overrideConfig,
- this.cachePool,
- false,
- undefined,
- incomingInput.uploads
- )
-
- const nodeToExecute =
- endingNodeIds.length === 1
- ? reactFlowNodes.find((node: IReactFlowNode) => endingNodeIds[0] === node.id)
- : reactFlowNodes[reactFlowNodes.length - 1]
- if (!nodeToExecute) return res.status(404).send(`Node not found`)
-
- if (incomingInput.overrideConfig) {
- nodeToExecute.data = replaceInputsWithConfig(nodeToExecute.data, incomingInput.overrideConfig)
- }
-
- const reactFlowNodeData: INodeData = resolveVariables(
- nodeToExecute.data,
- reactFlowNodes,
- incomingInput.question,
- chatHistory
- )
- nodeToExecuteData = reactFlowNodeData
-
- this.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes, incomingInput?.overrideConfig)
- }
-
- logger.debug(`[server]: Running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`)
-
- const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string
- const nodeModule = await import(nodeInstanceFilePath)
- const nodeInstance = new nodeModule.nodeClass({ sessionId })
-
- let result = isStreamValid
- ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, {
- chatId,
- chatflowid,
- chatHistory: incomingInput.history,
- logger,
- appDataSource: this.AppDataSource,
- databaseEntities,
- analytic: chatflow.analytic,
- uploads: incomingInput.uploads,
- socketIO,
- socketIOClientId: incomingInput.socketIOClientId
- })
- : await nodeInstance.run(nodeToExecuteData, incomingInput.question, {
- chatId,
- chatflowid,
- chatHistory: incomingInput.history,
- logger,
- appDataSource: this.AppDataSource,
- databaseEntities,
- analytic: chatflow.analytic,
- uploads: incomingInput.uploads
- })
-
- result = typeof result === 'string' ? { text: result } : result
-
- // Retrieve threadId from assistant if exists
- if (typeof result === 'object' && result.assistant) {
- sessionId = result.assistant.threadId
- }
-
- const userMessage: Omit = {
- role: 'userMessage',
- content: incomingInput.question,
- chatflowid,
- chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
- chatId,
- memoryType,
- sessionId,
- createdDate: userMessageDateTime,
- fileUploads: incomingInput.uploads ? JSON.stringify(fileUploads) : undefined
- }
- await this.addChatMessage(userMessage)
-
- let resultText = ''
- if (result.text) resultText = result.text
- else if (result.json) resultText = '```json\n' + JSON.stringify(result.json, null, 2)
- else resultText = JSON.stringify(result, null, 2)
-
- const apiMessage: Omit = {
- role: 'apiMessage',
- content: resultText,
- chatflowid,
- chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
- chatId,
- memoryType,
- sessionId
- }
- if (result?.sourceDocuments) apiMessage.sourceDocuments = JSON.stringify(result.sourceDocuments)
- if (result?.usedTools) apiMessage.usedTools = JSON.stringify(result.usedTools)
- if (result?.fileAnnotations) apiMessage.fileAnnotations = JSON.stringify(result.fileAnnotations)
- const chatMessage = await this.addChatMessage(apiMessage)
-
- logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`)
- await this.telemetry.sendTelemetry('prediction_sent', {
- version: await getAppVersion(),
- chatflowId: chatflowid,
- chatId,
- type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
- flowGraph: getTelemetryFlowObj(nodes, edges)
- })
-
- // Prepare response
- // return the question in the response
- // this is used when input text is empty but question is in audio format
- result.question = incomingInput.question
- result.chatId = chatId
- result.chatMessageId = chatMessage.id
- if (sessionId) result.sessionId = sessionId
- if (memoryType) result.memoryType = memoryType
-
- return res.json(result)
- } catch (e: any) {
- logger.error('[server]: Error:', e)
- return res.status(500).send(e.message)
- }
+ // Error handling
+ this.app.use(errorHandlerMiddleware)
}
async stopApp() {
diff --git a/packages/server/src/middlewares/errors/index.ts b/packages/server/src/middlewares/errors/index.ts
new file mode 100644
index 000000000..3e97daf6d
--- /dev/null
+++ b/packages/server/src/middlewares/errors/index.ts
@@ -0,0 +1,19 @@
+import { NextFunction, Request, Response } from 'express'
+import { StatusCodes } from 'http-status-codes'
+import { ApiError } from '../../errors/apiError'
+
+// we need eslint because we have to pass next arg for the error middleware
+// eslint-disable-next-line
+async function errorHandlerMiddleware(err: ApiError, req: Request, res: Response, next: NextFunction) {
+ let displayedError = {
+ statusCode: err.statusCode || StatusCodes.INTERNAL_SERVER_ERROR,
+ success: false,
+ message: err.message,
+ // Provide error stack trace only in development
+ stack: process.env.NODE_ENV === 'development' ? err.stack : {}
+ }
+ res.setHeader('Content-Type', 'application/json')
+ res.status(displayedError.statusCode).json(displayedError)
+}
+
+export default errorHandlerMiddleware
diff --git a/packages/server/src/routes/apikey/index.ts b/packages/server/src/routes/apikey/index.ts
new file mode 100644
index 000000000..e82924d87
--- /dev/null
+++ b/packages/server/src/routes/apikey/index.ts
@@ -0,0 +1,17 @@
+import express from 'express'
+import apikeyController from '../../controllers/apikey'
+const router = express.Router()
+
+// CREATE
+router.post('/', apikeyController.createApiKey)
+
+// READ
+router.get('/', apikeyController.getAllApiKeys)
+
+// UPDATE
+router.put('/:id', apikeyController.updateApiKey)
+
+// DELETE
+router.delete('/:id', apikeyController.deleteApiKey)
+
+export default router
diff --git a/packages/server/src/routes/assistants/index.ts b/packages/server/src/routes/assistants/index.ts
new file mode 100644
index 000000000..494f8f9de
--- /dev/null
+++ b/packages/server/src/routes/assistants/index.ts
@@ -0,0 +1,19 @@
+import express from 'express'
+import assistantsController from '../../controllers/assistants'
+
+const router = express.Router()
+
+// CREATE
+router.post('/', assistantsController.creatAssistant)
+
+// READ
+router.get('/', assistantsController.getAllAssistants)
+router.get('/:id', assistantsController.getAssistantById)
+
+// UPDATE
+router.put('/:id', assistantsController.updateAssistant)
+
+// DELETE
+router.delete('/:id', assistantsController.deleteAssistant)
+
+export default router
diff --git a/packages/server/src/routes/chat-messages/index.ts b/packages/server/src/routes/chat-messages/index.ts
new file mode 100644
index 000000000..dbb3e9f12
--- /dev/null
+++ b/packages/server/src/routes/chat-messages/index.ts
@@ -0,0 +1,16 @@
+import express from 'express'
+import chatMessageController from '../../controllers/chat-messages'
+const router = express.Router()
+
+// CREATE
+router.post('/:id', chatMessageController.createChatMessage)
+
+// READ
+router.get('/:id', chatMessageController.getAllChatMessages)
+
+// UPDATE
+
+// DELETE
+router.delete('/:id', chatMessageController.removeAllChatMessages)
+
+export default router
diff --git a/packages/server/src/routes/chatflows-streaming/index.ts b/packages/server/src/routes/chatflows-streaming/index.ts
new file mode 100644
index 000000000..34ca4e0c7
--- /dev/null
+++ b/packages/server/src/routes/chatflows-streaming/index.ts
@@ -0,0 +1,9 @@
+import express from 'express'
+import chatflowsController from '../../controllers/chatflows'
+
+const router = express.Router()
+
+// READ
+router.get('/:id', chatflowsController.checkIfChatflowIsValidForStreaming)
+
+export default router
diff --git a/packages/server/src/routes/chatflows-uploads/index.ts b/packages/server/src/routes/chatflows-uploads/index.ts
new file mode 100644
index 000000000..b3b3ff03d
--- /dev/null
+++ b/packages/server/src/routes/chatflows-uploads/index.ts
@@ -0,0 +1,9 @@
+import express from 'express'
+import chatflowsController from '../../controllers/chatflows'
+
+const router = express.Router()
+
+// READ
+router.get('/:id', chatflowsController.checkIfChatflowIsValidForUploads)
+
+export default router
diff --git a/packages/server/src/routes/chatflows/index.ts b/packages/server/src/routes/chatflows/index.ts
new file mode 100644
index 000000000..8cd3fcc01
--- /dev/null
+++ b/packages/server/src/routes/chatflows/index.ts
@@ -0,0 +1,19 @@
+import express from 'express'
+import chatflowsController from '../../controllers/chatflows'
+const router = express.Router()
+
+// CREATE
+router.post('/', chatflowsController.saveChatflow)
+
+// READ
+router.get('/', chatflowsController.getAllChatflows)
+router.get('/:id', chatflowsController.getChatflowById)
+router.get('/apikey/:apikey', chatflowsController.getChatflowByApiKey)
+
+// UPDATE
+router.put('/:id', chatflowsController.updateChatflow)
+
+// DELETE
+router.delete('/:id', chatflowsController.deleteChatflow)
+
+export default router
diff --git a/packages/server/src/routes/components-credentials-icon/index.ts b/packages/server/src/routes/components-credentials-icon/index.ts
new file mode 100644
index 000000000..364a87ba3
--- /dev/null
+++ b/packages/server/src/routes/components-credentials-icon/index.ts
@@ -0,0 +1,14 @@
+import express from 'express'
+import componentsCredentialsController from '../../controllers/components-credentials'
+const router = express.Router()
+
+// CREATE
+
+// READ
+router.get('/:name', componentsCredentialsController.getSingleComponentsCredentialIcon)
+
+// UPDATE
+
+// DELETE
+
+export default router
diff --git a/packages/server/src/routes/components-credentials/index.ts b/packages/server/src/routes/components-credentials/index.ts
new file mode 100644
index 000000000..2f7b23646
--- /dev/null
+++ b/packages/server/src/routes/components-credentials/index.ts
@@ -0,0 +1,9 @@
+import express from 'express'
+import componentsCredentialsController from '../../controllers/components-credentials'
+const router = express.Router()
+
+// READ
+router.get('/', componentsCredentialsController.getAllComponentsCredentials)
+router.get('/:name', componentsCredentialsController.getComponentByName)
+
+export default router
diff --git a/packages/server/src/routes/credentials/index.ts b/packages/server/src/routes/credentials/index.ts
new file mode 100644
index 000000000..0229798bf
--- /dev/null
+++ b/packages/server/src/routes/credentials/index.ts
@@ -0,0 +1,18 @@
+import express from 'express'
+import credentialsController from '../../controllers/credentials'
+const router = express.Router()
+
+// CREATE
+router.post('/', credentialsController.createCredential)
+
+// READ
+router.get('/', credentialsController.getAllCredentials)
+router.get('/:id', credentialsController.getCredentialById)
+
+// UPDATE
+router.put('/:id', credentialsController.updateCredential)
+
+// DELETE
+router.delete('/:id', credentialsController.deleteCredentials)
+
+export default router
diff --git a/packages/server/src/routes/feedback/index.ts b/packages/server/src/routes/feedback/index.ts
new file mode 100644
index 000000000..3502a9fb3
--- /dev/null
+++ b/packages/server/src/routes/feedback/index.ts
@@ -0,0 +1,14 @@
+import express from 'express'
+import feedbackController from '../../controllers/feedback'
+const router = express.Router()
+
+// CREATE
+router.post('/:id', feedbackController.createChatMessageFeedbackForChatflow)
+
+// READ
+router.get('/:id', feedbackController.getAllChatMessageFeedback)
+
+// UPDATE
+router.put('/:id', feedbackController.updateChatMessageFeedbackForChatflow)
+
+export default router
diff --git a/packages/server/src/routes/fetch-links/index.ts b/packages/server/src/routes/fetch-links/index.ts
new file mode 100644
index 000000000..a02abd588
--- /dev/null
+++ b/packages/server/src/routes/fetch-links/index.ts
@@ -0,0 +1,8 @@
+import express from 'express'
+import fetchLinksController from '../../controllers/fetch-links'
+const router = express.Router()
+
+// READ
+router.get('/', fetchLinksController.getAllLinks)
+
+export default router
diff --git a/packages/server/src/routes/flow-config/index.ts b/packages/server/src/routes/flow-config/index.ts
new file mode 100644
index 000000000..1aa46517e
--- /dev/null
+++ b/packages/server/src/routes/flow-config/index.ts
@@ -0,0 +1,14 @@
+import express from 'express'
+import flowConfigsController from '../../controllers/flow-configs'
+const router = express.Router()
+
+// CREATE
+
+// READ
+router.get('/:id', flowConfigsController.getSingleFlowConfig)
+
+// UPDATE
+
+// DELETE
+
+export default router
diff --git a/packages/server/src/routes/get-upload-file/index.ts b/packages/server/src/routes/get-upload-file/index.ts
new file mode 100644
index 000000000..cb871ed30
--- /dev/null
+++ b/packages/server/src/routes/get-upload-file/index.ts
@@ -0,0 +1,8 @@
+import express from 'express'
+import getUploadFileController from '../../controllers/get-upload-file'
+const router = express.Router()
+
+// READ
+router.get('/', getUploadFileController.streamUploadedImage)
+
+export default router
diff --git a/packages/server/src/routes/get-upload-path/index.ts b/packages/server/src/routes/get-upload-path/index.ts
new file mode 100644
index 000000000..48827c9a1
--- /dev/null
+++ b/packages/server/src/routes/get-upload-path/index.ts
@@ -0,0 +1,8 @@
+import express from 'express'
+import getUploadPathController from '../../controllers/get-upload-path'
+const router = express.Router()
+
+// READ
+router.get('/', getUploadPathController.getPathForUploads)
+
+export default router
diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts
new file mode 100644
index 000000000..c989880b2
--- /dev/null
+++ b/packages/server/src/routes/index.ts
@@ -0,0 +1,78 @@
+import express from 'express'
+import apikeyRouter from './apikey'
+import assistantsRouter from './assistants'
+import chatflowsRouter from './chatflows'
+import chatflowsStreamingRouter from './chatflows-streaming'
+import chatflowsUploadsRouter from './chatflows-uploads'
+import chatMessageRouter from './chat-messages'
+import componentsCredentialsRouter from './components-credentials'
+import componentsCredentialsIconRouter from './components-credentials-icon'
+import credentialsRouter from './credentials'
+import feedbackRouter from './feedback'
+import fetchLinksRouter from './fetch-links'
+import flowConfigRouter from './flow-config'
+import internalChatmessagesRouter from './internal-chat-messages'
+import internalPredictionRouter from './internal-predictions'
+import ipRouter from './ip'
+import getUploadFileRouter from './get-upload-file'
+import getUploadPathRouter from './get-upload-path'
+import loadPromptRouter from './load-prompts'
+import marketplacesRouter from './marketplaces'
+import nodeConfigRouter from './node-configs'
+import nodeCustomFunctionRouter from './node-custom-functions'
+import nodeIconRouter from './node-icons'
+import nodeLoadMethodRouter from './node-load-methods'
+import nodesRouter from './nodes'
+import openaiAssistantsRouter from './openai-assistants'
+import openaiAssistantsFileRouter from './openai-assistants-files'
+import predictionRouter from './predictions'
+import promptListsRouter from './prompts-lists'
+import publicChatbotRouter from './public-chatbots'
+import publicChatflowsRouter from './public-chatflows'
+import statsRouter from './stats'
+import toolsRouter from './tools'
+import variablesRouter from './variables'
+import vectorRouter from './vectors'
+import verifyRouter from './verify'
+import versionRouter from './versions'
+
+const router = express.Router()
+
+router.use('/apikey', apikeyRouter)
+router.use('/assistants', assistantsRouter)
+router.use('/chatflows', chatflowsRouter)
+router.use('/chatflows-streaming', chatflowsStreamingRouter)
+router.use('/chatmessage', chatMessageRouter)
+router.use('/components-credentials', componentsCredentialsRouter)
+router.use('/components-credentials-icon', componentsCredentialsIconRouter)
+router.use('/chatflows-uploads', chatflowsUploadsRouter)
+router.use('/credentials', credentialsRouter)
+router.use('/feedback', feedbackRouter)
+router.use('/fetch-links', fetchLinksRouter)
+router.use('/flow-config', flowConfigRouter)
+router.use('/internal-chatmessage', internalChatmessagesRouter)
+router.use('/internal-prediction', internalPredictionRouter)
+router.use('/ip', ipRouter)
+router.use('/get-upload-file', getUploadFileRouter)
+router.use('/get-upload-path', getUploadPathRouter)
+router.use('/load-prompt', loadPromptRouter)
+router.use('/marketplaces', marketplacesRouter)
+router.use('/node-config', nodeConfigRouter)
+router.use('/node-custom-function', nodeCustomFunctionRouter)
+router.use('/node-icon', nodeIconRouter)
+router.use('/node-load-method', nodeLoadMethodRouter)
+router.use('/nodes', nodesRouter)
+router.use('/openai-assistants', openaiAssistantsRouter)
+router.use('/openai-assistants-file', openaiAssistantsFileRouter)
+router.use('/prediction', predictionRouter)
+router.use('/prompts-list', promptListsRouter)
+router.use('/public-chatbotConfig', publicChatbotRouter)
+router.use('/public-chatflows', publicChatflowsRouter)
+router.use('/stats', statsRouter)
+router.use('/tools', toolsRouter)
+router.use('/variables', variablesRouter)
+router.use('/vector', vectorRouter)
+router.use('/verify', verifyRouter)
+router.use('/version', versionRouter)
+
+export default router
diff --git a/packages/server/src/routes/internal-chat-messages/index.ts b/packages/server/src/routes/internal-chat-messages/index.ts
new file mode 100644
index 000000000..fe05fdbc9
--- /dev/null
+++ b/packages/server/src/routes/internal-chat-messages/index.ts
@@ -0,0 +1,14 @@
+import express from 'express'
+import chatMessagesController from '../../controllers/chat-messages'
+const router = express.Router()
+
+// CREATE
+
+// READ
+router.get('/:id', chatMessagesController.getAllInternalChatMessages)
+
+// UPDATE
+
+// DELETE
+
+export default router
diff --git a/packages/server/src/routes/internal-predictions/index.ts b/packages/server/src/routes/internal-predictions/index.ts
new file mode 100644
index 000000000..6e1c17ba3
--- /dev/null
+++ b/packages/server/src/routes/internal-predictions/index.ts
@@ -0,0 +1,8 @@
+import express from 'express'
+import internalPredictionsController from '../../controllers/internal-predictions'
+const router = express.Router()
+
+// CREATE
+router.post('/:id', internalPredictionsController.createInternalPrediction)
+
+export default router
diff --git a/packages/server/src/routes/ip/index.ts b/packages/server/src/routes/ip/index.ts
new file mode 100644
index 000000000..04de78181
--- /dev/null
+++ b/packages/server/src/routes/ip/index.ts
@@ -0,0 +1,14 @@
+import express from 'express'
+import ipController from '../../controllers/ip'
+const router = express.Router()
+
+// CREATE
+
+// READ
+router.get('/', ipController.configureProxyNrInHostEnv)
+
+// UPDATE
+
+// DELETE
+
+export default router
diff --git a/packages/server/src/routes/load-prompts/index.ts b/packages/server/src/routes/load-prompts/index.ts
new file mode 100644
index 000000000..a12afba00
--- /dev/null
+++ b/packages/server/src/routes/load-prompts/index.ts
@@ -0,0 +1,8 @@
+import express from 'express'
+import loadPromptsController from '../../controllers/load-prompts'
+const router = express.Router()
+
+// CREATE
+router.post('/', loadPromptsController.createPrompt)
+
+export default router
diff --git a/packages/server/src/routes/marketplaces/index.ts b/packages/server/src/routes/marketplaces/index.ts
new file mode 100644
index 000000000..c7eae1f3e
--- /dev/null
+++ b/packages/server/src/routes/marketplaces/index.ts
@@ -0,0 +1,8 @@
+import express from 'express'
+import marketplacesController from '../../controllers/marketplaces'
+const router = express.Router()
+
+// READ
+router.get('/templates', marketplacesController.getAllTemplates)
+
+export default router
diff --git a/packages/server/src/routes/node-configs/index.ts b/packages/server/src/routes/node-configs/index.ts
new file mode 100644
index 000000000..4c4451388
--- /dev/null
+++ b/packages/server/src/routes/node-configs/index.ts
@@ -0,0 +1,8 @@
+import express from 'express'
+import nodeConfigsController from '../../controllers/node-configs'
+const router = express.Router()
+
+// CREATE
+router.post('/', nodeConfigsController.getAllNodeConfigs)
+
+export default router
diff --git a/packages/server/src/routes/node-custom-functions/index.ts b/packages/server/src/routes/node-custom-functions/index.ts
new file mode 100644
index 000000000..9fa33d42e
--- /dev/null
+++ b/packages/server/src/routes/node-custom-functions/index.ts
@@ -0,0 +1,14 @@
+import express from 'express'
+import nodesRouter from '../../controllers/nodes'
+const router = express.Router()
+
+// CREATE
+
+// READ
+router.post('/', nodesRouter.executeCustomFunction)
+
+// UPDATE
+
+// DELETE
+
+export default router
diff --git a/packages/server/src/routes/node-icons/index.ts b/packages/server/src/routes/node-icons/index.ts
new file mode 100644
index 000000000..0781226ad
--- /dev/null
+++ b/packages/server/src/routes/node-icons/index.ts
@@ -0,0 +1,14 @@
+import express from 'express'
+import nodesController from '../../controllers/nodes'
+const router = express.Router()
+
+// CREATE
+
+// READ
+router.get('/:name', nodesController.getSingleNodeIcon)
+
+// UPDATE
+
+// DELETE
+
+export default router
diff --git a/packages/server/src/routes/node-load-methods/index.ts b/packages/server/src/routes/node-load-methods/index.ts
new file mode 100644
index 000000000..941190f1f
--- /dev/null
+++ b/packages/server/src/routes/node-load-methods/index.ts
@@ -0,0 +1,7 @@
+import express from 'express'
+import nodesRouter from '../../controllers/nodes'
+const router = express.Router()
+
+router.post('/:name', nodesRouter.getSingleNodeAsyncOptions)
+
+export default router
diff --git a/packages/server/src/routes/nodes/index.ts b/packages/server/src/routes/nodes/index.ts
new file mode 100644
index 000000000..b875036ef
--- /dev/null
+++ b/packages/server/src/routes/nodes/index.ts
@@ -0,0 +1,9 @@
+import express from 'express'
+import nodesController from '../../controllers/nodes'
+const router = express.Router()
+
+// READ
+router.get('/', nodesController.getAllNodes)
+router.get('/:name', nodesController.getNodeByName)
+
+export default router
diff --git a/packages/server/src/routes/openai-assistants-files/index.ts b/packages/server/src/routes/openai-assistants-files/index.ts
new file mode 100644
index 000000000..3f8b85010
--- /dev/null
+++ b/packages/server/src/routes/openai-assistants-files/index.ts
@@ -0,0 +1,8 @@
+import express from 'express'
+import openaiAssistantsController from '../../controllers/openai-assistants'
+const router = express.Router()
+
+// CREATE
+router.post('/', openaiAssistantsController.getFileFromAssistant)
+
+export default router
diff --git a/packages/server/src/routes/openai-assistants/index.ts b/packages/server/src/routes/openai-assistants/index.ts
new file mode 100644
index 000000000..6e3378fd9
--- /dev/null
+++ b/packages/server/src/routes/openai-assistants/index.ts
@@ -0,0 +1,15 @@
+import express from 'express'
+import openaiAssistantsController from '../../controllers/openai-assistants'
+const router = express.Router()
+
+// CREATE
+
+// READ
+router.get('/', openaiAssistantsController.getAllOpenaiAssistants)
+router.get('/:id', openaiAssistantsController.getSingleOpenaiAssistant)
+
+// UPDATE
+
+// DELETE
+
+export default router
diff --git a/packages/server/src/routes/predictions/index.ts b/packages/server/src/routes/predictions/index.ts
new file mode 100644
index 000000000..613c67601
--- /dev/null
+++ b/packages/server/src/routes/predictions/index.ts
@@ -0,0 +1,13 @@
+import express from 'express'
+import multer from 'multer'
+import path from 'path'
+import predictionsController from '../../controllers/predictions'
+
+const router = express.Router()
+
+const upload = multer({ dest: `${path.join(__dirname, '..', '..', '..', 'uploads')}/` })
+
+// CREATE
+router.post('/:id', upload.array('files'), predictionsController.getRateLimiterMiddleware, predictionsController.createPrediction)
+
+export default router
diff --git a/packages/server/src/routes/prompts-lists/index.ts b/packages/server/src/routes/prompts-lists/index.ts
new file mode 100644
index 000000000..9b92c365c
--- /dev/null
+++ b/packages/server/src/routes/prompts-lists/index.ts
@@ -0,0 +1,8 @@
+import express from 'express'
+import promptsListController from '../../controllers/prompts-lists'
+const router = express.Router()
+
+// CREATE
+router.post('/', promptsListController.createPromptsList)
+
+export default router
diff --git a/packages/server/src/routes/public-chatbots/index.ts b/packages/server/src/routes/public-chatbots/index.ts
new file mode 100644
index 000000000..5a367c683
--- /dev/null
+++ b/packages/server/src/routes/public-chatbots/index.ts
@@ -0,0 +1,14 @@
+import express from 'express'
+import chatflowsController from '../../controllers/chatflows'
+const router = express.Router()
+
+// CREATE
+
+// READ
+router.get('/:id', chatflowsController.getSinglePublicChatbotConfig)
+
+// UPDATE
+
+// DELETE
+
+export default router
diff --git a/packages/server/src/routes/public-chatflows/index.ts b/packages/server/src/routes/public-chatflows/index.ts
new file mode 100644
index 000000000..97e23ea79
--- /dev/null
+++ b/packages/server/src/routes/public-chatflows/index.ts
@@ -0,0 +1,14 @@
+import express from 'express'
+import chatflowsController from '../../controllers/chatflows'
+const router = express.Router()
+
+// CREATE
+
+// READ
+router.get('/:id', chatflowsController.getSinglePublicChatflow)
+
+// UPDATE
+
+// DELETE
+
+export default router
diff --git a/packages/server/src/routes/stats/index.ts b/packages/server/src/routes/stats/index.ts
new file mode 100644
index 000000000..ea6d8db3c
--- /dev/null
+++ b/packages/server/src/routes/stats/index.ts
@@ -0,0 +1,9 @@
+import express from 'express'
+import statsController from '../../controllers/stats'
+
+const router = express.Router()
+
+// READ
+router.get('/:id', statsController.getChatflowStats)
+
+export default router
diff --git a/packages/server/src/routes/tools/index.ts b/packages/server/src/routes/tools/index.ts
new file mode 100644
index 000000000..4d63b1615
--- /dev/null
+++ b/packages/server/src/routes/tools/index.ts
@@ -0,0 +1,19 @@
+import express from 'express'
+import toolsController from '../../controllers/tools'
+
+const router = express.Router()
+
+// CREATE
+router.post('/', toolsController.creatTool)
+
+// READ
+router.get('/', toolsController.getAllTools)
+router.get('/:id', toolsController.getToolById)
+
+// UPDATE
+router.put('/:id', toolsController.updateTool)
+
+// DELETE
+router.delete('/:id', toolsController.deleteTool)
+
+export default router
diff --git a/packages/server/src/routes/variables/index.ts b/packages/server/src/routes/variables/index.ts
new file mode 100644
index 000000000..eece55588
--- /dev/null
+++ b/packages/server/src/routes/variables/index.ts
@@ -0,0 +1,18 @@
+import express from 'express'
+import variablesController from '../../controllers/variables'
+
+const router = express.Router()
+
+// CREATE
+router.post('/', variablesController.createVariable)
+
+// READ
+router.get('/', variablesController.getAllVariables)
+
+// UPDATE
+router.put('/:id', variablesController.updateVariable)
+
+// DELETE
+router.delete('/:id', variablesController.deleteVariable)
+
+export default router
diff --git a/packages/server/src/routes/vectors/index.ts b/packages/server/src/routes/vectors/index.ts
new file mode 100644
index 000000000..21ffd97d4
--- /dev/null
+++ b/packages/server/src/routes/vectors/index.ts
@@ -0,0 +1,14 @@
+import express from 'express'
+import multer from 'multer'
+import path from 'path'
+import vectorsController from '../../controllers/vectors'
+
+const router = express.Router()
+
+const upload = multer({ dest: `${path.join(__dirname, '..', '..', '..', 'uploads')}/` })
+
+// CREATE
+router.post('/upsert/:id', upload.array('files'), vectorsController.getRateLimiterMiddleware, vectorsController.upsertVectorMiddleware)
+router.post('/internal-upsert/:id', vectorsController.createInternalUpsert)
+
+export default router
diff --git a/packages/server/src/routes/verify/index.ts b/packages/server/src/routes/verify/index.ts
new file mode 100644
index 000000000..414e6d0f3
--- /dev/null
+++ b/packages/server/src/routes/verify/index.ts
@@ -0,0 +1,8 @@
+import express from 'express'
+import apikeyController from '../../controllers/apikey'
+const router = express.Router()
+
+// READ
+router.get('/apikey/:apikey', apikeyController.verifyApiKey)
+
+export default router
diff --git a/packages/server/src/routes/versions/index.ts b/packages/server/src/routes/versions/index.ts
new file mode 100644
index 000000000..8aa60a290
--- /dev/null
+++ b/packages/server/src/routes/versions/index.ts
@@ -0,0 +1,8 @@
+import express from 'express'
+import versionsController from '../../controllers/versions'
+const router = express.Router()
+
+// READ
+router.get('/', versionsController.getVersion)
+
+export default router
diff --git a/packages/server/src/services/apikey/index.ts b/packages/server/src/services/apikey/index.ts
new file mode 100644
index 000000000..5a1bc3344
--- /dev/null
+++ b/packages/server/src/services/apikey/index.ts
@@ -0,0 +1,69 @@
+import { addAPIKey, deleteAPIKey, getAPIKeys, updateAPIKey } from '../../utils/apiKey'
+import { addChatflowsCount } from '../../utils/addChatflowsCount'
+import { getApiKey } from '../../utils/apiKey'
+
+const getAllApiKeys = async () => {
+ try {
+ const keys = await getAPIKeys()
+ const dbResponse = await addChatflowsCount(keys)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: apikeyService.getAllApiKeys - ${error}`)
+ }
+}
+
+const createApiKey = async (keyName: string) => {
+ try {
+ const keys = await addAPIKey(keyName)
+ const dbResponse = await addChatflowsCount(keys)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: apikeyService.createApiKey - ${error}`)
+ }
+}
+
+// Update api key
+const updateApiKey = async (id: string, keyName: string) => {
+ try {
+ const keys = await updateAPIKey(id, keyName)
+ const dbResponse = await addChatflowsCount(keys)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: apikeyService.updateApiKey - ${error}`)
+ }
+}
+
+const deleteApiKey = async (id: string) => {
+ try {
+ const keys = await deleteAPIKey(id)
+ const dbResponse = await addChatflowsCount(keys)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: apikeyService.deleteApiKey - ${error}`)
+ }
+}
+
+const verifyApiKey = async (paramApiKey: string): Promise => {
+ try {
+ const apiKey = await getApiKey(paramApiKey)
+ if (!apiKey) {
+ return {
+ executionError: true,
+ status: 401,
+ msg: `Unauthorized`
+ }
+ }
+ const dbResponse = 'OK'
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: apikeyService.verifyApiKey - ${error}`)
+ }
+}
+
+export default {
+ createApiKey,
+ deleteApiKey,
+ getAllApiKeys,
+ updateApiKey,
+ verifyApiKey
+}
diff --git a/packages/server/src/services/assistants/index.ts b/packages/server/src/services/assistants/index.ts
new file mode 100644
index 000000000..8b5a431f3
--- /dev/null
+++ b/packages/server/src/services/assistants/index.ts
@@ -0,0 +1,366 @@
+import OpenAI from 'openai'
+import path from 'path'
+import * as fs from 'fs'
+import { uniqWith, isEqual } from 'lodash'
+import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
+import { Assistant } from '../../database/entities/Assistant'
+import { Credential } from '../../database/entities/Credential'
+import { getUserHome, decryptCredentialData, getAppVersion } from '../../utils'
+
+const creatAssistant = async (requestBody: any): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ if (!requestBody.details) {
+ return {
+ executionError: true,
+ status: 500,
+ msg: `Invalid request body`
+ }
+ }
+ const assistantDetails = JSON.parse(requestBody.details)
+ try {
+ const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
+ id: requestBody.credential
+ })
+
+ if (!credential) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Credential ${requestBody.credential} not found`
+ }
+ }
+
+ // Decrpyt credentialData
+ const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
+ const openAIApiKey = decryptedCredentialData['openAIApiKey']
+ if (!openAIApiKey) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `OpenAI ApiKey not found`
+ }
+ }
+ const openai = new OpenAI({ apiKey: openAIApiKey })
+
+ let tools = []
+ if (assistantDetails.tools) {
+ for (const tool of assistantDetails.tools ?? []) {
+ tools.push({
+ type: tool
+ })
+ }
+ }
+
+ if (assistantDetails.uploadFiles) {
+ // Base64 strings
+ let files: string[] = []
+ const fileBase64 = assistantDetails.uploadFiles
+ if (fileBase64.startsWith('[') && fileBase64.endsWith(']')) {
+ files = JSON.parse(fileBase64)
+ } else {
+ files = [fileBase64]
+ }
+
+ const uploadedFiles = []
+ for (const file of files) {
+ const splitDataURI = file.split(',')
+ const filename = splitDataURI.pop()?.split(':')[1] ?? ''
+ const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
+ const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', filename)
+ if (!fs.existsSync(path.join(getUserHome(), '.flowise', 'openai-assistant'))) {
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
+ }
+ if (!fs.existsSync(filePath)) {
+ fs.writeFileSync(filePath, bf)
+ }
+
+ const createdFile = await openai.files.create({
+ file: fs.createReadStream(filePath),
+ purpose: 'assistants'
+ })
+ uploadedFiles.push(createdFile)
+
+ fs.unlinkSync(filePath)
+ }
+ assistantDetails.files = [...assistantDetails.files, ...uploadedFiles]
+ }
+
+ if (!assistantDetails.id) {
+ const newAssistant = await openai.beta.assistants.create({
+ name: assistantDetails.name,
+ description: assistantDetails.description,
+ instructions: assistantDetails.instructions,
+ model: assistantDetails.model,
+ tools,
+ file_ids: (assistantDetails.files ?? []).map((file: OpenAI.Files.FileObject) => file.id)
+ })
+ assistantDetails.id = newAssistant.id
+ } else {
+ const retrievedAssistant = await openai.beta.assistants.retrieve(assistantDetails.id)
+ let filteredTools = uniqWith([...retrievedAssistant.tools, ...tools], isEqual)
+ filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function))
+
+ await openai.beta.assistants.update(assistantDetails.id, {
+ name: assistantDetails.name,
+ description: assistantDetails.description ?? '',
+ instructions: assistantDetails.instructions ?? '',
+ model: assistantDetails.model,
+ tools: filteredTools,
+ file_ids: uniqWith(
+ [...retrievedAssistant.file_ids, ...(assistantDetails.files ?? []).map((file: OpenAI.Files.FileObject) => file.id)],
+ isEqual
+ )
+ })
+ }
+
+ const newAssistantDetails = {
+ ...assistantDetails
+ }
+ if (newAssistantDetails.uploadFiles) delete newAssistantDetails.uploadFiles
+
+ requestBody.details = JSON.stringify(newAssistantDetails)
+ } catch (error) {
+ return {
+ executionError: true,
+ status: 500,
+ msg: `Error creating new assistant: ${error}`
+ }
+ }
+ const newAssistant = new Assistant()
+ Object.assign(newAssistant, requestBody)
+
+ const assistant = await appServer.AppDataSource.getRepository(Assistant).create(newAssistant)
+ const dbResponse = await appServer.AppDataSource.getRepository(Assistant).save(assistant)
+
+ await appServer.telemetry.sendTelemetry('assistant_created', {
+ version: await getAppVersion(),
+ assistantId: dbResponse.id
+ })
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: assistantsService.creatTool - ${error}`)
+ }
+}
+
+const deleteAssistant = async (assistantId: string, isDeleteBoth: any): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const assistant = await appServer.AppDataSource.getRepository(Assistant).findOneBy({
+ id: assistantId
+ })
+ if (!assistant) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Assistant ${assistantId} not found`
+ }
+ }
+ try {
+ const assistantDetails = JSON.parse(assistant.details)
+ const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
+ id: assistant.credential
+ })
+
+ if (!credential) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Credential ${assistant.credential} not found`
+ }
+ }
+
+ // Decrpyt credentialData
+ const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
+ const openAIApiKey = decryptedCredentialData['openAIApiKey']
+ if (!openAIApiKey) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `OpenAI ApiKey not found`
+ }
+ }
+
+ const openai = new OpenAI({ apiKey: openAIApiKey })
+ const dbResponse = await appServer.AppDataSource.getRepository(Assistant).delete({ id: assistantId })
+ if (isDeleteBoth) await openai.beta.assistants.del(assistantDetails.id)
+ return dbResponse
+ } catch (error: any) {
+ if (error.status === 404 && error.type === 'invalid_request_error') {
+ return 'OK'
+ } else {
+ return {
+ executionError: true,
+ status: 500,
+ msg: `Error deleting assistant: ${error}`
+ }
+ }
+ }
+ } catch (error) {
+ throw new Error(`Error: assistantsService.deleteTool - ${error}`)
+ }
+}
+
+const getAllAssistants = async (): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = await appServer.AppDataSource.getRepository(Assistant).find()
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: assistantsService.getAllAssistants - ${error}`)
+ }
+}
+
+const getAssistantById = async (assistantId: string): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = await appServer.AppDataSource.getRepository(Assistant).findOneBy({
+ id: assistantId
+ })
+ if (!dbResponse) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Assistant ${assistantId} not found`
+ }
+ }
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: assistantsService.getAssistantById - ${error}`)
+ }
+}
+
+const updateAssistant = async (assistantId: string, requestBody: any): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const assistant = await appServer.AppDataSource.getRepository(Assistant).findOneBy({
+ id: assistantId
+ })
+
+ if (!assistant) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Assistant ${assistantId} not found`
+ }
+ }
+ try {
+ const openAIAssistantId = JSON.parse(assistant.details)?.id
+ const body = requestBody
+ const assistantDetails = JSON.parse(body.details)
+ const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
+ id: body.credential
+ })
+
+ if (!credential) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Credential ${body.credential} not found`
+ }
+ }
+
+ // Decrpyt credentialData
+ const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
+ const openAIApiKey = decryptedCredentialData['openAIApiKey']
+ if (!openAIApiKey) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `OpenAI ApiKey not found`
+ }
+ }
+
+ const openai = new OpenAI({ apiKey: openAIApiKey })
+
+ let tools = []
+ if (assistantDetails.tools) {
+ for (const tool of assistantDetails.tools ?? []) {
+ tools.push({
+ type: tool
+ })
+ }
+ }
+
+ if (assistantDetails.uploadFiles) {
+ // Base64 strings
+ let files: string[] = []
+ const fileBase64 = assistantDetails.uploadFiles
+ if (fileBase64.startsWith('[') && fileBase64.endsWith(']')) {
+ files = JSON.parse(fileBase64)
+ } else {
+ files = [fileBase64]
+ }
+
+ const uploadedFiles = []
+ for (const file of files) {
+ const splitDataURI = file.split(',')
+ const filename = splitDataURI.pop()?.split(':')[1] ?? ''
+ const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
+ const filePath = path.join(getUserHome(), '.flowise', 'openai-assistant', filename)
+ if (!fs.existsSync(path.join(getUserHome(), '.flowise', 'openai-assistant'))) {
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
+ }
+ if (!fs.existsSync(filePath)) {
+ fs.writeFileSync(filePath, bf)
+ }
+
+ const createdFile = await openai.files.create({
+ file: fs.createReadStream(filePath),
+ purpose: 'assistants'
+ })
+ uploadedFiles.push(createdFile)
+
+ fs.unlinkSync(filePath)
+ }
+ assistantDetails.files = [...assistantDetails.files, ...uploadedFiles]
+ }
+
+ const retrievedAssistant = await openai.beta.assistants.retrieve(openAIAssistantId)
+ let filteredTools = uniqWith([...retrievedAssistant.tools, ...tools], isEqual)
+ filteredTools = filteredTools.filter((tool) => !(tool.type === 'function' && !(tool as any).function))
+
+ await openai.beta.assistants.update(openAIAssistantId, {
+ name: assistantDetails.name,
+ description: assistantDetails.description,
+ instructions: assistantDetails.instructions,
+ model: assistantDetails.model,
+ tools: filteredTools,
+ file_ids: uniqWith(
+ [...retrievedAssistant.file_ids, ...(assistantDetails.files ?? []).map((file: OpenAI.Files.FileObject) => file.id)],
+ isEqual
+ )
+ })
+
+ const newAssistantDetails = {
+ ...assistantDetails,
+ id: openAIAssistantId
+ }
+ if (newAssistantDetails.uploadFiles) delete newAssistantDetails.uploadFiles
+
+ const updateAssistant = new Assistant()
+ body.details = JSON.stringify(newAssistantDetails)
+ Object.assign(updateAssistant, body)
+
+ await appServer.AppDataSource.getRepository(Assistant).merge(assistant, updateAssistant)
+ const dbResponse = await appServer.AppDataSource.getRepository(Assistant).save(assistant)
+ return dbResponse
+ } catch (error) {
+ return {
+ executionError: true,
+ status: 500,
+ msg: `Error updating assistant: ${error}`
+ }
+ }
+ } catch (error) {
+ throw new Error(`Error: assistantsService.updateAssistant - ${error}`)
+ }
+}
+
+export default {
+ creatAssistant,
+ deleteAssistant,
+ getAllAssistants,
+ getAssistantById,
+ updateAssistant
+}
diff --git a/packages/server/src/services/chat-messages/index.ts b/packages/server/src/services/chat-messages/index.ts
new file mode 100644
index 000000000..8b87f9db0
--- /dev/null
+++ b/packages/server/src/services/chat-messages/index.ts
@@ -0,0 +1,116 @@
+import { FindOptionsWhere } from 'typeorm'
+import path from 'path'
+import { chatType, IChatMessage } from '../../Interface'
+import { utilGetChatMessage } from '../../utils/getChatMessage'
+import { utilAddChatMessage } from '../../utils/addChatMesage'
+import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
+import { ChatMessageFeedback } from '../../database/entities/ChatMessageFeedback'
+import { getStoragePath } from 'flowise-components'
+import { deleteFolderRecursive } from '../../utils'
+import logger from '../../utils/logger'
+import { ChatMessage } from '../../database/entities/ChatMessage'
+
+// Add chatmessages for chatflowid
+const createChatMessage = async (chatMessage: Partial) => {
+ try {
+ const dbResponse = await utilAddChatMessage(chatMessage)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: chatMessagesService.createChatMessage - ${error}`)
+ }
+}
+
+// Get all chatmessages from chatflowid
+const getAllChatMessages = async (
+ chatflowId: string,
+ chatTypeFilter: chatType | undefined,
+ sortOrder: string = 'ASC',
+ chatId?: string,
+ memoryType?: string,
+ sessionId?: string,
+ startDate?: string,
+ endDate?: string,
+ messageId?: string,
+ feedback?: boolean
+): Promise => {
+ try {
+ const dbResponse = await utilGetChatMessage(
+ chatflowId,
+ chatTypeFilter,
+ sortOrder,
+ chatId,
+ memoryType,
+ sessionId,
+ startDate,
+ endDate,
+ messageId,
+ feedback
+ )
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: chatMessagesService.getAllChatMessages - ${error}`)
+ }
+}
+
+// Get internal chatmessages from chatflowid
+const getAllInternalChatMessages = async (
+ chatflowId: string,
+ chatTypeFilter: chatType | undefined,
+ sortOrder: string = 'ASC',
+ chatId?: string,
+ memoryType?: string,
+ sessionId?: string,
+ startDate?: string,
+ endDate?: string,
+ messageId?: string,
+ feedback?: boolean
+): Promise => {
+ try {
+ const dbResponse = await utilGetChatMessage(
+ chatflowId,
+ chatTypeFilter,
+ sortOrder,
+ chatId,
+ memoryType,
+ sessionId,
+ startDate,
+ endDate,
+ messageId,
+ feedback
+ )
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: chatMessagesService.getAllInternalChatMessages - ${error}`)
+ }
+}
+
+const removeAllChatMessages = async (chatId: string, chatflowid: string, deleteOptions: FindOptionsWhere): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+
+ // remove all related feedback records
+ const feedbackDeleteOptions: FindOptionsWhere = { chatId }
+ await appServer.AppDataSource.getRepository(ChatMessageFeedback).delete(feedbackDeleteOptions)
+
+ // Delete all uploads corresponding to this chatflow/chatId
+ if (chatId) {
+ try {
+ const directory = path.join(getStoragePath(), chatflowid, chatId)
+ deleteFolderRecursive(directory)
+ } catch (e) {
+ logger.error(`[server]: Error deleting file storage for chatflow ${chatflowid}, chatId ${chatId}: ${e}`)
+ }
+ }
+ const dbResponse = await appServer.AppDataSource.getRepository(ChatMessage).delete(deleteOptions)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: chatMessagesService.removeAllChatMessages - ${error}`)
+ }
+}
+
+export default {
+ createChatMessage,
+ getAllChatMessages,
+ getAllInternalChatMessages,
+ removeAllChatMessages
+}
diff --git a/packages/server/src/services/chatflows/index.ts b/packages/server/src/services/chatflows/index.ts
new file mode 100644
index 000000000..c6ee778a9
--- /dev/null
+++ b/packages/server/src/services/chatflows/index.ts
@@ -0,0 +1,278 @@
+import path from 'path'
+import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
+import { IChatFlow } from '../../Interface'
+import { ChatFlow } from '../../database/entities/ChatFlow'
+import {
+ getAppVersion,
+ getTelemetryFlowObj,
+ deleteFolderRecursive,
+ isFlowValidForStream,
+ constructGraphs,
+ getEndingNodes
+} from '../../utils'
+import logger from '../../utils/logger'
+import { getStoragePath } from 'flowise-components'
+import { IReactFlowObject } from '../../Interface'
+import { utilGetUploadsConfig } from '../../utils/getUploadsConfig'
+
+// Check if chatflow valid for streaming
+const checkIfChatflowIsValidForStreaming = async (chatflowId: string): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ //**
+ const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({
+ id: chatflowId
+ })
+ if (!chatflow) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Chatflow ${chatflowId} not found`
+ }
+ }
+
+ /*** Get Ending Node with Directed Graph ***/
+ const flowData = chatflow.flowData
+ const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
+ const nodes = parsedFlowData.nodes
+ const edges = parsedFlowData.edges
+ const { graph, nodeDependencies } = constructGraphs(nodes, edges)
+
+ const endingNodeIds = getEndingNodes(nodeDependencies, graph)
+ if (!endingNodeIds.length) {
+ return {
+ executionError: true,
+ status: 500,
+ msg: `Ending nodes not found`
+ }
+ }
+
+ const endingNodes = nodes.filter((nd) => endingNodeIds.includes(nd.id))
+
+ let isStreaming = false
+ let isEndingNodeExists = endingNodes.find((node) => node.data?.outputs?.output === 'EndingNode')
+
+ for (const endingNode of endingNodes) {
+ const endingNodeData = endingNode.data
+ if (!endingNodeData) {
+ return {
+ executionError: true,
+ status: 500,
+ msg: `Ending node ${endingNode.id} data not found`
+ }
+ }
+
+ const isEndingNode = endingNodeData?.outputs?.output === 'EndingNode'
+
+ if (!isEndingNode) {
+ if (
+ endingNodeData &&
+ endingNodeData.category !== 'Chains' &&
+ endingNodeData.category !== 'Agents' &&
+ endingNodeData.category !== 'Engine'
+ ) {
+ return {
+ executionError: true,
+ status: 500,
+ msg: `Ending node must be either a Chain or Agent`
+ }
+ }
+ }
+
+ isStreaming = isEndingNode ? false : isFlowValidForStream(nodes, endingNodeData)
+ }
+
+ // Once custom function ending node exists, flow is always unavailable to stream
+ const dbResponse = { isStreaming: isEndingNodeExists ? false : isStreaming }
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: chatflowsService.checkIfChatflowIsValidForStreaming - ${error}`)
+ }
+}
+
+// Check if chatflow valid for uploads
+const checkIfChatflowIsValidForUploads = async (chatflowId: string): Promise => {
+ try {
+ const dbResponse = await utilGetUploadsConfig(chatflowId)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: chatflowsService.checkIfChatflowIsValidForUploads - ${error}`)
+ }
+}
+
+const deleteChatflow = async (chatflowId: string): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).delete({ id: chatflowId })
+ try {
+ // Delete all uploads corresponding to this chatflow
+ const directory = path.join(getStoragePath(), chatflowId)
+ deleteFolderRecursive(directory)
+ } catch (e) {
+ logger.error(`[server]: Error deleting file storage for chatflow ${chatflowId}: ${e}`)
+ }
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: chatflowsService.getAllChatflows - ${error}`)
+ }
+}
+
+const getAllChatflows = async (): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).find()
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: chatflowsService.getAllChatflows - ${error}`)
+ }
+}
+
+const getChatflowByApiKey = async (apiKeyId: string): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow)
+ .createQueryBuilder('cf')
+ .where('cf.apikeyid = :apikeyid', { apikeyid: apiKeyId })
+ .orWhere('cf.apikeyid IS NULL')
+ .orWhere('cf.apikeyid = ""')
+ .orderBy('cf.name', 'ASC')
+ .getMany()
+ if (dbResponse.length < 1) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Chatflow not found in the database!`
+ }
+ }
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: chatflowsService.getChatflowByApiKey - ${error}`)
+ }
+}
+
+const getChatflowById = async (chatflowId: string): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({
+ id: chatflowId
+ })
+ if (!dbResponse) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Chatflow ${chatflowId} not found in the database!`
+ }
+ }
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: chatflowsService.getAllChatflows - ${error}`)
+ }
+}
+
+const saveChatflow = async (newChatFlow: ChatFlow): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const newDbChatflow = await appServer.AppDataSource.getRepository(ChatFlow).create(newChatFlow)
+ const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).save(newDbChatflow)
+ await appServer.telemetry.sendTelemetry('chatflow_created', {
+ version: await getAppVersion(),
+ chatflowId: dbResponse.id,
+ flowGraph: getTelemetryFlowObj(JSON.parse(dbResponse.flowData)?.nodes, JSON.parse(dbResponse.flowData)?.edges)
+ })
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: chatflowsService.saveChatflow - ${error}`)
+ }
+}
+
+const updateChatflow = async (chatflow: ChatFlow, updateChatFlow: ChatFlow): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const newDbChatflow = await appServer.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow)
+ const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).save(newDbChatflow)
+ // chatFlowPool is initialized only when a flow is opened
+ // if the user attempts to rename/update category without opening any flow, chatFlowPool will be undefined
+ if (appServer.chatflowPool) {
+ // Update chatflowpool inSync to false, to build flow from scratch again because data has been changed
+ appServer.chatflowPool.updateInSync(chatflow.id, false)
+ }
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: chatflowsService.updateChatflow - ${error}`)
+ }
+}
+
+// Get specific chatflow via id (PUBLIC endpoint, used when sharing chatbot link)
+const getSinglePublicChatflow = async (chatflowId: string): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({
+ id: chatflowId
+ })
+ if (dbResponse && dbResponse.isPublic) {
+ return dbResponse
+ } else if (dbResponse && !dbResponse.isPublic) {
+ return {
+ executionError: true,
+ status: 401,
+ msg: `Unauthorized`
+ }
+ }
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Chatflow ${chatflowId} not found`
+ }
+ } catch (error) {
+ throw new Error(`Error: chatflowsService.getSinglePublicChatflow - ${error}`)
+ }
+}
+
+// Get specific chatflow chatbotConfig via id (PUBLIC endpoint, used to retrieve config for embedded chat)
+// Safe as public endpoint as chatbotConfig doesn't contain sensitive credential
+const getSinglePublicChatbotConfig = async (chatflowId: string): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({
+ id: chatflowId
+ })
+ if (!dbResponse) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Chatflow ${chatflowId} not found`
+ }
+ }
+ const uploadsConfig = await utilGetUploadsConfig(chatflowId)
+ // even if chatbotConfig is not set but uploads are enabled
+ // send uploadsConfig to the chatbot
+ if (dbResponse.chatbotConfig || uploadsConfig) {
+ try {
+ const parsedConfig = dbResponse.chatbotConfig ? JSON.parse(dbResponse.chatbotConfig) : {}
+ return { ...parsedConfig, uploads: uploadsConfig }
+ } catch (e) {
+ return {
+ executionError: true,
+ status: 500,
+ msg: `Error parsing Chatbot Config for Chatflow ${chatflowId}`
+ }
+ }
+ }
+ return 'OK'
+ } catch (error) {
+ throw new Error(`Error: chatflowsService.getSinglePublicChatbotConfig - ${error}`)
+ }
+}
+
+export default {
+ checkIfChatflowIsValidForStreaming,
+ checkIfChatflowIsValidForUploads,
+ deleteChatflow,
+ getAllChatflows,
+ getChatflowByApiKey,
+ getChatflowById,
+ saveChatflow,
+ updateChatflow,
+ getSinglePublicChatflow,
+ getSinglePublicChatbotConfig
+}
diff --git a/packages/server/src/services/components-credentials/index.ts b/packages/server/src/services/components-credentials/index.ts
new file mode 100644
index 000000000..bbc4128a2
--- /dev/null
+++ b/packages/server/src/services/components-credentials/index.ts
@@ -0,0 +1,74 @@
+import { cloneDeep } from 'lodash'
+import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
+
+// Get all component credentials
+const getAllComponentsCredentials = async (): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = []
+ for (const credName in appServer.nodesPool.componentCredentials) {
+ const clonedCred = cloneDeep(appServer.nodesPool.componentCredentials[credName])
+ dbResponse.push(clonedCred)
+ }
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: componentsCredentialsService.getAllComponentsCredentials - ${error}`)
+ }
+}
+
+const getComponentByName = async (credentialName: string) => {
+ try {
+ const appServer = getRunningExpressApp()
+ if (!credentialName.includes('&')) {
+ if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentCredentials, credentialName)) {
+ return appServer.nodesPool.componentCredentials[credentialName]
+ } else {
+ throw new Error(
+ `Error: componentsCredentialsService.getSingleComponentsCredential - Credential ${credentialName} not found`
+ )
+ }
+ } else {
+ const dbResponse = []
+ for (const name of credentialName.split('&')) {
+ if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentCredentials, name)) {
+ dbResponse.push(appServer.nodesPool.componentCredentials[name])
+ } else {
+ throw new Error(`Error: componentsCredentialsService.getSingleComponentsCredential - Credential ${name} not found`)
+ }
+ }
+ return dbResponse
+ }
+ } catch (error) {
+ throw new Error(`Error: componentsCredentialsService.getSingleComponentsCredential - ${error}`)
+ }
+}
+
+// Returns specific component credential icon via name
+const getSingleComponentsCredentialIcon = async (credentialName: string) => {
+ try {
+ const appServer = getRunningExpressApp()
+ if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentCredentials, credentialName)) {
+ const credInstance = appServer.nodesPool.componentCredentials[credentialName]
+ if (credInstance.icon === undefined) {
+ throw new Error(`Credential ${credentialName} icon not found`)
+ }
+
+ if (credInstance.icon.endsWith('.svg') || credInstance.icon.endsWith('.png') || credInstance.icon.endsWith('.jpg')) {
+ const filepath = credInstance.icon
+ return filepath
+ } else {
+ throw new Error(`Credential ${credentialName} icon is missing icon`)
+ }
+ } else {
+ throw new Error(`Credential ${credentialName} not found`)
+ }
+ } catch (error) {
+ throw new Error(`Error: componentsCredentialsService.getSingleComponentsCredentialIcon - ${error}`)
+ }
+}
+
+export default {
+ getAllComponentsCredentials,
+ getComponentByName,
+ getSingleComponentsCredentialIcon
+}
diff --git a/packages/server/src/services/credentials/index.ts b/packages/server/src/services/credentials/index.ts
new file mode 100644
index 000000000..5c5d145ea
--- /dev/null
+++ b/packages/server/src/services/credentials/index.ts
@@ -0,0 +1,126 @@
+import { omit } from 'lodash'
+import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
+import { Credential } from '../../database/entities/Credential'
+import { transformToCredentialEntity, decryptCredentialData } from '../../utils'
+import { ICredentialReturnResponse } from '../../Interface'
+
+const createCredential = async (requestBody: any) => {
+ try {
+ const appServer = getRunningExpressApp()
+ const newCredential = await transformToCredentialEntity(requestBody)
+ const credential = await appServer.AppDataSource.getRepository(Credential).create(newCredential)
+ const dbResponse = await appServer.AppDataSource.getRepository(Credential).save(credential)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: credentialsService.createCredential - ${error}`)
+ }
+}
+
+// Delete all credentials from chatflowid
+const deleteCredentials = async (credentialId: string): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = await appServer.AppDataSource.getRepository(Credential).delete({ id: credentialId })
+ if (!dbResponse) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Credential ${credentialId} not found`
+ }
+ }
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: credentialsService.deleteCredential - ${error}`)
+ }
+}
+
+const getAllCredentials = async (paramCredentialName: any) => {
+ try {
+ const appServer = getRunningExpressApp()
+ let dbResponse = []
+ if (paramCredentialName) {
+ if (Array.isArray(paramCredentialName)) {
+ for (let i = 0; i < paramCredentialName.length; i += 1) {
+ const name = paramCredentialName[i] as string
+ const credentials = await appServer.AppDataSource.getRepository(Credential).findBy({
+ credentialName: name
+ })
+ dbResponse.push(...credentials)
+ }
+ } else {
+ const credentials = await appServer.AppDataSource.getRepository(Credential).findBy({
+ credentialName: paramCredentialName as string
+ })
+ dbResponse = [...credentials]
+ }
+ } else {
+ const credentials = await appServer.AppDataSource.getRepository(Credential).find()
+ for (const credential of credentials) {
+ dbResponse.push(omit(credential, ['encryptedData']))
+ }
+ }
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: credentialsService.getAllCredentials - ${error}`)
+ }
+}
+
+const getCredentialById = async (credentialId: string): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
+ id: credentialId
+ })
+ if (!credential) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Credential ${credentialId} not found`
+ }
+ }
+ // Decrpyt credentialData
+ const decryptedCredentialData = await decryptCredentialData(
+ credential.encryptedData,
+ credential.credentialName,
+ appServer.nodesPool.componentCredentials
+ )
+ const returnCredential: ICredentialReturnResponse = {
+ ...credential,
+ plainDataObj: decryptedCredentialData
+ }
+ const dbResponse = omit(returnCredential, ['encryptedData'])
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: credentialsService.createCredential - ${error}`)
+ }
+}
+
+const updateCredential = async (credentialId: string, requestBody: any): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
+ id: credentialId
+ })
+ if (!credential) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Credential ${credentialId} not found`
+ }
+ }
+ const updateCredential = await transformToCredentialEntity(requestBody)
+ await appServer.AppDataSource.getRepository(Credential).merge(credential, updateCredential)
+ const dbResponse = await appServer.AppDataSource.getRepository(Credential).save(credential)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: credentialsService.updateCredential - ${error}`)
+ }
+}
+
+export default {
+ createCredential,
+ deleteCredentials,
+ getAllCredentials,
+ getCredentialById,
+ updateCredential
+}
diff --git a/packages/server/src/services/feedback/index.ts b/packages/server/src/services/feedback/index.ts
new file mode 100644
index 000000000..9084be613
--- /dev/null
+++ b/packages/server/src/services/feedback/index.ts
@@ -0,0 +1,46 @@
+import { utilGetChatMessageFeedback } from '../../utils/getChatMessageFeedback'
+import { utilAddChatMessageFeedback } from '../../utils/addChatMessageFeedback'
+import { utilUpdateChatMessageFeedback } from '../../utils/updateChatMessageFeedback'
+import { IChatMessageFeedback } from '../../Interface'
+
+// Get all chatmessage feedback from chatflowid
+const getAllChatMessageFeedback = async (
+ chatflowid: string,
+ chatId: string | undefined,
+ sortOrder: string | undefined,
+ startDate: string | undefined,
+ endDate: string | undefined
+) => {
+ try {
+ const dbResponse = await utilGetChatMessageFeedback(chatflowid, chatId, sortOrder, startDate, endDate)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: feedbackService.getAllChatMessageFeedback - ${error}`)
+ }
+}
+
+// Add chatmessage feedback for chatflowid
+const createChatMessageFeedbackForChatflow = async (requestBody: Partial): Promise => {
+ try {
+ const dbResponse = await utilAddChatMessageFeedback(requestBody)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: feedbackService.createChatMessageFeedbackForChatflow - ${error}`)
+ }
+}
+
+// Add chatmessage feedback for chatflowid
+const updateChatMessageFeedbackForChatflow = async (chatflowId: string, requestBody: Partial): Promise => {
+ try {
+ const dbResponse = await utilUpdateChatMessageFeedback(chatflowId, requestBody)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: feedbackService.updateChatMessageFeedbackForChatflow - ${error}`)
+ }
+}
+
+export default {
+ getAllChatMessageFeedback,
+ createChatMessageFeedbackForChatflow,
+ updateChatMessageFeedbackForChatflow
+}
diff --git a/packages/server/src/services/fetch-links/index.ts b/packages/server/src/services/fetch-links/index.ts
new file mode 100644
index 000000000..58247d72f
--- /dev/null
+++ b/packages/server/src/services/fetch-links/index.ts
@@ -0,0 +1,29 @@
+import { webCrawl, xmlScrape } from 'flowise-components'
+
+const getAllLinks = async (requestUrl: string, relativeLinksMethod: string, queryLimit: string): Promise => {
+ try {
+ const url = decodeURIComponent(requestUrl)
+ if (!relativeLinksMethod) {
+ return {
+ executionError: true,
+ status: 500,
+ msg: `Please choose a Relative Links Method in Additional Parameters!`
+ }
+ }
+ const limit = parseInt(queryLimit)
+ if (process.env.DEBUG === 'true') console.info(`Start ${relativeLinksMethod}`)
+ const links: string[] = relativeLinksMethod === 'webCrawl' ? await webCrawl(url, limit) : await xmlScrape(url, limit)
+ if (process.env.DEBUG === 'true') console.info(`Finish ${relativeLinksMethod}`)
+ const dbResponse = {
+ status: 'OK',
+ links
+ }
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: fetchLinksService.getAllLinks - ${error}`)
+ }
+}
+
+export default {
+ getAllLinks
+}
diff --git a/packages/server/src/services/flow-configs/index.ts b/packages/server/src/services/flow-configs/index.ts
new file mode 100644
index 000000000..8dbcd6d3f
--- /dev/null
+++ b/packages/server/src/services/flow-configs/index.ts
@@ -0,0 +1,29 @@
+import { findAvailableConfigs } from '../../utils'
+import { IReactFlowObject } from '../../Interface'
+import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
+import chatflowsService from '../chatflows'
+
+const getSingleFlowConfig = async (chatflowId: string): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const chatflow = await chatflowsService.getChatflowById(chatflowId)
+ if (!chatflow) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Chatflow ${chatflowId} not found in the database!`
+ }
+ }
+ const flowData = chatflow.flowData
+ const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
+ const nodes = parsedFlowData.nodes
+ const dbResponse = findAvailableConfigs(nodes, appServer.nodesPool.componentCredentials)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: flowConfigService.getSingleFlowConfig - ${error}`)
+ }
+}
+
+export default {
+ getSingleFlowConfig
+}
diff --git a/packages/server/src/services/load-prompts/index.ts b/packages/server/src/services/load-prompts/index.ts
new file mode 100644
index 000000000..0cdc3ffb9
--- /dev/null
+++ b/packages/server/src/services/load-prompts/index.ts
@@ -0,0 +1,22 @@
+import { Client } from 'langchainhub'
+import { parsePrompt } from '../../utils/hub'
+
+const createPrompt = async (promptName: string): Promise => {
+ try {
+ let hub = new Client()
+ const prompt = await hub.pull(promptName)
+ const templates = parsePrompt(prompt)
+ const dbResponse = {
+ status: 'OK',
+ prompt: promptName,
+ templates: templates
+ }
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: loadPromptsService.createPrompt - ${error}`)
+ }
+}
+
+export default {
+ createPrompt
+}
diff --git a/packages/server/src/services/marketplaces/index.ts b/packages/server/src/services/marketplaces/index.ts
new file mode 100644
index 000000000..218452dd3
--- /dev/null
+++ b/packages/server/src/services/marketplaces/index.ts
@@ -0,0 +1,58 @@
+import path from 'path'
+import * as fs from 'fs'
+
+// Get all templates for marketplaces
+const getAllTemplates = async () => {
+ try {
+ let marketplaceDir = path.join(__dirname, '..', '..', '..', 'marketplaces', 'chatflows')
+ let jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json')
+ let templates: any[] = []
+ jsonsInDir.forEach((file, index) => {
+ const filePath = path.join(__dirname, '..', '..', '..', 'marketplaces', 'chatflows', file)
+ const fileData = fs.readFileSync(filePath)
+ const fileDataObj = JSON.parse(fileData.toString())
+ const template = {
+ id: index,
+ templateName: file.split('.json')[0],
+ flowData: fileData.toString(),
+ badge: fileDataObj?.badge,
+ framework: fileDataObj?.framework,
+ categories: fileDataObj?.categories,
+ type: 'Chatflow',
+ description: fileDataObj?.description || ''
+ }
+ templates.push(template)
+ })
+
+ marketplaceDir = path.join(__dirname, '..', '..', '..', 'marketplaces', 'tools')
+ jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json')
+ jsonsInDir.forEach((file, index) => {
+ const filePath = path.join(__dirname, '..', '..', '..', 'marketplaces', 'tools', file)
+ const fileData = fs.readFileSync(filePath)
+ const fileDataObj = JSON.parse(fileData.toString())
+ const template = {
+ ...fileDataObj,
+ id: index,
+ type: 'Tool',
+ framework: fileDataObj?.framework,
+ badge: fileDataObj?.badge,
+ categories: '',
+ templateName: file.split('.json')[0]
+ }
+ templates.push(template)
+ })
+ const sortedTemplates = templates.sort((a, b) => a.templateName.localeCompare(b.templateName))
+ const FlowiseDocsQnAIndex = sortedTemplates.findIndex((tmp) => tmp.templateName === 'Flowise Docs QnA')
+ if (FlowiseDocsQnAIndex > 0) {
+ sortedTemplates.unshift(sortedTemplates.splice(FlowiseDocsQnAIndex, 1)[0])
+ }
+ const dbResponse = sortedTemplates
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: marketplacesService.getAllTemplates - ${error}`)
+ }
+}
+
+export default {
+ getAllTemplates
+}
diff --git a/packages/server/src/services/node-configs/index.ts b/packages/server/src/services/node-configs/index.ts
new file mode 100644
index 000000000..7eb523e81
--- /dev/null
+++ b/packages/server/src/services/node-configs/index.ts
@@ -0,0 +1,18 @@
+import { findAvailableConfigs } from '../../utils'
+import { IReactFlowNode } from '../../Interface'
+import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
+
+const getAllNodeConfigs = async (requestBody: any) => {
+ try {
+ const appServer = getRunningExpressApp()
+ const nodes = [{ data: requestBody }] as IReactFlowNode[]
+ const dbResponse = findAvailableConfigs(nodes, appServer.nodesPool.componentCredentials)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: nodeConfigsService.getAllNodeConfigs - ${error}`)
+ }
+}
+
+export default {
+ getAllNodeConfigs
+}
diff --git a/packages/server/src/services/nodes/index.ts b/packages/server/src/services/nodes/index.ts
new file mode 100644
index 000000000..0bf097b49
--- /dev/null
+++ b/packages/server/src/services/nodes/index.ts
@@ -0,0 +1,142 @@
+import { cloneDeep } from 'lodash'
+import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
+import { INodeData } from '../../Interface'
+import { INodeOptionsValue, ICommonObject, handleEscapeCharacters } from 'flowise-components'
+import { databaseEntities } from '../../utils'
+import logger from '../../utils/logger'
+
+// Get all component nodes
+const getAllNodes = async () => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = []
+ for (const nodeName in appServer.nodesPool.componentNodes) {
+ const clonedNode = cloneDeep(appServer.nodesPool.componentNodes[nodeName])
+ dbResponse.push(clonedNode)
+ }
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: nodesService.getAllNodes - ${error}`)
+ }
+}
+
+// Get specific component node via name
+const getNodeByName = async (nodeName: string) => {
+ try {
+ const appServer = getRunningExpressApp()
+ if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentNodes, nodeName)) {
+ const dbResponse = appServer.nodesPool.componentNodes[nodeName]
+ return dbResponse
+ } else {
+ throw new Error(`Node ${nodeName} not found`)
+ }
+ } catch (error) {
+ throw new Error(`Error: nodesService.getAllNodes - ${error}`)
+ }
+}
+
+// Returns specific component node icon via name
+const getSingleNodeIcon = async (nodeName: string) => {
+ try {
+ const appServer = getRunningExpressApp()
+ if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentNodes, nodeName)) {
+ const nodeInstance = appServer.nodesPool.componentNodes[nodeName]
+ if (nodeInstance.icon === undefined) {
+ throw new Error(`Node ${nodeName} icon not found`)
+ }
+
+ if (nodeInstance.icon.endsWith('.svg') || nodeInstance.icon.endsWith('.png') || nodeInstance.icon.endsWith('.jpg')) {
+ const filepath = nodeInstance.icon
+ return filepath
+ } else {
+ throw new Error(`Node ${nodeName} icon is missing icon`)
+ }
+ } else {
+ throw new Error(`Node ${nodeName} not found`)
+ }
+ } catch (error) {
+ throw new Error(`Error: nodesService.getSingleNodeIcon - ${error}`)
+ }
+}
+
+const getSingleNodeAsyncOptions = async (nodeName: string, requestBody: any): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const nodeData: INodeData = requestBody
+ if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentNodes, nodeName)) {
+ try {
+ const nodeInstance = appServer.nodesPool.componentNodes[nodeName]
+ const methodName = nodeData.loadMethod || ''
+
+ const dbResponse: INodeOptionsValue[] = await nodeInstance.loadMethods![methodName]!.call(nodeInstance, nodeData, {
+ appDataSource: appServer.AppDataSource,
+ databaseEntities: databaseEntities
+ })
+
+ return dbResponse
+ } catch (error) {
+ return []
+ }
+ } else {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Node ${nodeName} not found`
+ }
+ }
+ } catch (error) {
+ throw new Error(`Error: nodesService.getSingleNodeAsyncOptions - ${error}`)
+ }
+}
+
+// execute custom function node
+const executeCustomFunction = async (requestBody: any) => {
+ try {
+ const appServer = getRunningExpressApp()
+ const body = requestBody
+ const functionInputVariables = Object.fromEntries(
+ [...(body?.javascriptFunction ?? '').matchAll(/\$([a-zA-Z0-9_]+)/g)].map((g) => [g[1], undefined])
+ )
+ const nodeData = { inputs: { functionInputVariables, ...body } }
+ if (Object.prototype.hasOwnProperty.call(appServer.nodesPool.componentNodes, 'customFunction')) {
+ try {
+ const nodeInstanceFilePath = appServer.nodesPool.componentNodes['customFunction'].filePath as string
+ const nodeModule = await import(nodeInstanceFilePath)
+ const newNodeInstance = new nodeModule.nodeClass()
+
+ const options: ICommonObject = {
+ appDataSource: appServer.AppDataSource,
+ databaseEntities,
+ logger
+ }
+
+ const returnData = await newNodeInstance.init(nodeData, '', options)
+ const dbResponse = typeof returnData === 'string' ? handleEscapeCharacters(returnData, true) : returnData
+
+ return dbResponse
+ } catch (error) {
+ return {
+ executionError: true,
+ status: 500,
+ msg: `Error running custom function: ${error}`
+ }
+ }
+ } else {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Node customFunction not found`
+ }
+ }
+ } catch (error) {
+ throw new Error(`Error: nodesService.executeCustomFunction - ${error}`)
+ }
+}
+
+export default {
+ getAllNodes,
+ getNodeByName,
+ getSingleNodeIcon,
+ getSingleNodeAsyncOptions,
+ executeCustomFunction
+}
diff --git a/packages/server/src/services/openai-assistants/index.ts b/packages/server/src/services/openai-assistants/index.ts
new file mode 100644
index 000000000..cffa2e3f1
--- /dev/null
+++ b/packages/server/src/services/openai-assistants/index.ts
@@ -0,0 +1,84 @@
+import OpenAI from 'openai'
+import { decryptCredentialData } from '../../utils'
+import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
+import { Credential } from '../../database/entities/Credential'
+
+// ----------------------------------------
+// Assistants
+// ----------------------------------------
+
+// List available assistants
+const getAllOpenaiAssistants = async (credentialId: string): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
+ id: credentialId
+ })
+ if (!credential) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Credential ${credentialId} not found in the database!`
+ }
+ }
+ // Decrpyt credentialData
+ const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
+ const openAIApiKey = decryptedCredentialData['openAIApiKey']
+ if (!openAIApiKey) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `OpenAI ApiKey not found`
+ }
+ }
+ const openai = new OpenAI({ apiKey: openAIApiKey })
+ const retrievedAssistants = await openai.beta.assistants.list()
+ const dbResponse = retrievedAssistants.data
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: openaiAssistantsService.getAllOpenaiAssistants - ${error}`)
+ }
+}
+
+// Get assistant object
+const getSingleOpenaiAssistant = async (credentialId: string, assistantId: string): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const credential = await appServer.AppDataSource.getRepository(Credential).findOneBy({
+ id: credentialId
+ })
+ if (!credential) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Credential ${credentialId} not found in the database!`
+ }
+ }
+ // Decrpyt credentialData
+ const decryptedCredentialData = await decryptCredentialData(credential.encryptedData)
+ const openAIApiKey = decryptedCredentialData['openAIApiKey']
+ if (!openAIApiKey) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `OpenAI ApiKey not found`
+ }
+ }
+
+ const openai = new OpenAI({ apiKey: openAIApiKey })
+ const dbResponse = await openai.beta.assistants.retrieve(assistantId)
+ const resp = await openai.files.list()
+ const existingFiles = resp.data ?? []
+ if (dbResponse.file_ids && dbResponse.file_ids.length) {
+ ;(dbResponse as any).files = existingFiles.filter((file) => dbResponse.file_ids.includes(file.id))
+ }
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: openaiAssistantsService.getSingleOpenaiAssistant - ${error}`)
+ }
+}
+
+export default {
+ getAllOpenaiAssistants,
+ getSingleOpenaiAssistant
+}
diff --git a/packages/server/src/services/prompts-lists/index.ts b/packages/server/src/services/prompts-lists/index.ts
new file mode 100644
index 000000000..62a97c8b4
--- /dev/null
+++ b/packages/server/src/services/prompts-lists/index.ts
@@ -0,0 +1,22 @@
+import axios from 'axios'
+
+const createPromptsList = async (requestBody: any) => {
+ try {
+ const tags = requestBody.tags ? `tags=${requestBody.tags}` : ''
+ // Default to 100, TODO: add pagination and use offset & limit
+ const url = `https://api.hub.langchain.com/repos/?limit=100&${tags}has_commits=true&sort_field=num_likes&sort_direction=desc&is_archived=false`
+ const resp = await axios.get(url)
+ if (resp.data.repos) {
+ return {
+ status: 'OK',
+ repos: resp.data.repos
+ }
+ }
+ } catch (error) {
+ return { status: 'ERROR', repos: [] }
+ }
+}
+
+export default {
+ createPromptsList
+}
diff --git a/packages/server/src/services/stats/index.ts b/packages/server/src/services/stats/index.ts
new file mode 100644
index 000000000..2f2448a1e
--- /dev/null
+++ b/packages/server/src/services/stats/index.ts
@@ -0,0 +1,45 @@
+import { chatType } from '../../Interface'
+import { ChatMessage } from '../../database/entities/ChatMessage'
+import { utilGetChatMessage } from '../../utils/getChatMessage'
+import { ChatMessageFeedback } from '../../database/entities/ChatMessageFeedback'
+
+// get stats for showing in chatflow
+const getChatflowStats = async (
+ chatflowid: string,
+ chatTypeFilter: chatType | undefined,
+ startDate?: string,
+ endDate?: string,
+ messageId?: string,
+ feedback?: boolean
+): Promise => {
+ try {
+ const chatmessages = (await utilGetChatMessage(
+ chatflowid,
+ chatTypeFilter,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ startDate,
+ endDate,
+ messageId,
+ feedback
+ )) as Array
+ const totalMessages = chatmessages.length
+ const totalFeedback = chatmessages.filter((message) => message?.feedback).length
+ const positiveFeedback = chatmessages.filter((message) => message?.feedback?.rating === 'THUMBS_UP').length
+ const dbResponse = {
+ totalMessages,
+ totalFeedback,
+ positiveFeedback
+ }
+
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: statsService.getChatflowStats - ${error}`)
+ }
+}
+
+export default {
+ getChatflowStats
+}
diff --git a/packages/server/src/services/telemetry/index.ts b/packages/server/src/services/telemetry/index.ts
new file mode 100644
index 000000000..4c77efd53
--- /dev/null
+++ b/packages/server/src/services/telemetry/index.ts
@@ -0,0 +1,10 @@
+import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
+
+const createEvent = async (eventInfo: any) => {
+ const appServer = getRunningExpressApp()
+ await appServer.telemetry.sendTelemetry(eventInfo.name, eventInfo.data)
+}
+
+export default {
+ createEvent
+}
diff --git a/packages/server/src/services/tools/index.ts b/packages/server/src/services/tools/index.ts
new file mode 100644
index 000000000..42a579e1f
--- /dev/null
+++ b/packages/server/src/services/tools/index.ts
@@ -0,0 +1,93 @@
+import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
+import { Tool } from '../../database/entities/Tool'
+import { getAppVersion } from '../../utils'
+
+const creatTool = async (requestBody: any): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const newTool = new Tool()
+ Object.assign(newTool, requestBody)
+ const tool = await appServer.AppDataSource.getRepository(Tool).create(newTool)
+ const dbResponse = await appServer.AppDataSource.getRepository(Tool).save(tool)
+ await appServer.telemetry.sendTelemetry('tool_created', {
+ version: await getAppVersion(),
+ toolId: dbResponse.id,
+ toolName: dbResponse.name
+ })
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: toolsService.creatTool - ${error}`)
+ }
+}
+
+const deleteTool = async (toolId: string): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = await appServer.AppDataSource.getRepository(Tool).delete({
+ id: toolId
+ })
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: toolsService.deleteTool - ${error}`)
+ }
+}
+
+const getAllTools = async (): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = await appServer.AppDataSource.getRepository(Tool).find()
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: toolsService.getAllTools - ${error}`)
+ }
+}
+
+const getToolById = async (toolId: string): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = await appServer.AppDataSource.getRepository(Tool).findOneBy({
+ id: toolId
+ })
+ if (!dbResponse) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Tool ${toolId} not found`
+ }
+ }
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: toolsService.getToolById - ${error}`)
+ }
+}
+
+const updateTool = async (toolId: string, toolBody: any): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const tool = await appServer.AppDataSource.getRepository(Tool).findOneBy({
+ id: toolId
+ })
+ if (!tool) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Tool ${toolId} not found`
+ }
+ }
+ const updateTool = new Tool()
+ Object.assign(updateTool, toolBody)
+ await appServer.AppDataSource.getRepository(Tool).merge(tool, updateTool)
+ const dbResponse = await appServer.AppDataSource.getRepository(Tool).save(tool)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: toolsService.getToolById - ${error}`)
+ }
+}
+
+export default {
+ creatTool,
+ deleteTool,
+ getAllTools,
+ getToolById,
+ updateTool
+}
diff --git a/packages/server/src/services/variables/index.ts b/packages/server/src/services/variables/index.ts
new file mode 100644
index 000000000..7153c940d
--- /dev/null
+++ b/packages/server/src/services/variables/index.ts
@@ -0,0 +1,64 @@
+import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
+import { Variable } from '../../database/entities/Variable'
+
+const createVariable = async (newVariable: Variable) => {
+ try {
+ const appServer = getRunningExpressApp()
+ const variable = await appServer.AppDataSource.getRepository(Variable).create(newVariable)
+ const dbResponse = await appServer.AppDataSource.getRepository(Variable).save(variable)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: variablesServices.createVariable - ${error}`)
+ }
+}
+
+const deleteVariable = async (variableId: string): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = await appServer.AppDataSource.getRepository(Variable).delete({ id: variableId })
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: variablesServices.createVariable - ${error}`)
+ }
+}
+
+const getAllVariables = async () => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = await appServer.AppDataSource.getRepository(Variable).find()
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: variablesServices.getAllVariables - ${error}`)
+ }
+}
+
+const getVariableById = async (variableId: string) => {
+ try {
+ const appServer = getRunningExpressApp()
+ const dbResponse = await appServer.AppDataSource.getRepository(Variable).findOneBy({
+ id: variableId
+ })
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: variablesServices.getVariableById - ${error}`)
+ }
+}
+
+const updateVariable = async (variable: Variable, updatedVariable: Variable) => {
+ try {
+ const appServer = getRunningExpressApp()
+ const tmpUpdatedVariable = await appServer.AppDataSource.getRepository(Variable).merge(variable, updatedVariable)
+ const dbResponse = await appServer.AppDataSource.getRepository(Variable).save(tmpUpdatedVariable)
+ return dbResponse
+ } catch (error) {
+ throw new Error(`Error: variablesServices.updateVariable - ${error}`)
+ }
+}
+
+export default {
+ createVariable,
+ deleteVariable,
+ getAllVariables,
+ getVariableById,
+ updateVariable
+}
diff --git a/packages/server/src/services/vectors/index.ts b/packages/server/src/services/vectors/index.ts
new file mode 100644
index 000000000..7c55a510d
--- /dev/null
+++ b/packages/server/src/services/vectors/index.ts
@@ -0,0 +1,14 @@
+import { Request, Response } from 'express'
+import { upsertVector } from '../../utils/upsertVector'
+
+const upsertVectorMiddleware = async (req: Request, res: Response, isInternal: boolean = false) => {
+ try {
+ await upsertVector(req, res, isInternal)
+ } catch (error) {
+ throw new Error(`Error: vectorsService.getRateLimiter - ${error}`)
+ }
+}
+
+export default {
+ upsertVectorMiddleware
+}
diff --git a/packages/server/src/services/versions/index.ts b/packages/server/src/services/versions/index.ts
new file mode 100644
index 000000000..391cdb1b8
--- /dev/null
+++ b/packages/server/src/services/versions/index.ts
@@ -0,0 +1,49 @@
+import path from 'path'
+import * as fs from 'fs'
+
+const getVersion = async () => {
+ try {
+ const getPackageJsonPath = (): string => {
+ const checkPaths = [
+ path.join(__dirname, '..', 'package.json'),
+ path.join(__dirname, '..', '..', 'package.json'),
+ path.join(__dirname, '..', '..', '..', 'package.json'),
+ path.join(__dirname, '..', '..', '..', '..', 'package.json'),
+ path.join(__dirname, '..', '..', '..', '..', '..', 'package.json')
+ ]
+ for (const checkPath of checkPaths) {
+ if (fs.existsSync(checkPath)) {
+ return checkPath
+ }
+ }
+ return ''
+ }
+ const packagejsonPath = getPackageJsonPath()
+ if (!packagejsonPath) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: 'Version not found'
+ }
+ }
+ try {
+ const content = await fs.promises.readFile(packagejsonPath, 'utf8')
+ const parsedContent = JSON.parse(content)
+ return {
+ version: parsedContent.version
+ }
+ } catch (error) {
+ return {
+ executionError: true,
+ status: 500,
+ msg: `Version not found: ${error}`
+ }
+ }
+ } catch (error) {
+ throw new Error(`Error: versionService.getVersion - ${error}`)
+ }
+}
+
+export default {
+ getVersion
+}
diff --git a/packages/server/src/utils/addChatMesage.ts b/packages/server/src/utils/addChatMesage.ts
new file mode 100644
index 000000000..887031bfc
--- /dev/null
+++ b/packages/server/src/utils/addChatMesage.ts
@@ -0,0 +1,19 @@
+import { ChatMessage } from '../database/entities/ChatMessage'
+import { IChatMessage } from '../Interface'
+import { getRunningExpressApp } from '../utils/getRunningExpressApp'
+
+/**
+ * Method that add chat messages.
+ * @param {Partial} chatMessage
+ */
+export const utilAddChatMessage = async (chatMessage: Partial): Promise => {
+ const appServer = getRunningExpressApp()
+ const newChatMessage = new ChatMessage()
+ Object.assign(newChatMessage, chatMessage)
+ if (!newChatMessage.createdDate) {
+ newChatMessage.createdDate = new Date()
+ }
+ const chatmessage = await appServer.AppDataSource.getRepository(ChatMessage).create(newChatMessage)
+ const dbResponse = await appServer.AppDataSource.getRepository(ChatMessage).save(chatmessage)
+ return dbResponse
+}
diff --git a/packages/server/src/utils/addChatMessageFeedback.ts b/packages/server/src/utils/addChatMessageFeedback.ts
new file mode 100644
index 000000000..0a570b1d6
--- /dev/null
+++ b/packages/server/src/utils/addChatMessageFeedback.ts
@@ -0,0 +1,16 @@
+import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback'
+import { IChatMessageFeedback } from '../Interface'
+import { getRunningExpressApp } from '../utils/getRunningExpressApp'
+
+/**
+ * Method that add chat message feedback.
+ * @param {Partial} chatMessageFeedback
+ */
+
+export const utilAddChatMessageFeedback = async (chatMessageFeedback: Partial): Promise => {
+ const appServer = getRunningExpressApp()
+ const newChatMessageFeedback = new ChatMessageFeedback()
+ Object.assign(newChatMessageFeedback, chatMessageFeedback)
+ const feedback = await appServer.AppDataSource.getRepository(ChatMessageFeedback).create(newChatMessageFeedback)
+ return await appServer.AppDataSource.getRepository(ChatMessageFeedback).save(feedback)
+}
diff --git a/packages/server/src/utils/addChatflowsCount.ts b/packages/server/src/utils/addChatflowsCount.ts
new file mode 100644
index 000000000..c0ee664a4
--- /dev/null
+++ b/packages/server/src/utils/addChatflowsCount.ts
@@ -0,0 +1,33 @@
+import { ChatFlow } from '../database/entities/ChatFlow'
+import { getRunningExpressApp } from '../utils/getRunningExpressApp'
+
+export const addChatflowsCount = async (keys: any) => {
+ try {
+ const appServer = getRunningExpressApp()
+ let tmpResult = keys
+ if (typeof keys !== 'undefined' && keys.length > 0) {
+ const updatedKeys: any[] = []
+ //iterate through keys and get chatflows
+ for (const key of keys) {
+ const chatflows = await appServer.AppDataSource.getRepository(ChatFlow)
+ .createQueryBuilder('cf')
+ .where('cf.apikeyid = :apikeyid', { apikeyid: key.id })
+ .getMany()
+ const linkedChatFlows: any[] = []
+ chatflows.map((cf) => {
+ linkedChatFlows.push({
+ flowName: cf.name,
+ category: cf.category,
+ updatedDate: cf.updatedDate
+ })
+ })
+ key.chatFlows = linkedChatFlows
+ updatedKeys.push(key)
+ }
+ tmpResult = updatedKeys
+ }
+ return tmpResult
+ } catch (error) {
+ throw new Error(`Error: addChatflowsCount - ${error}`)
+ }
+}
diff --git a/packages/server/src/utils/buildChatflow.ts b/packages/server/src/utils/buildChatflow.ts
new file mode 100644
index 000000000..811782bc9
--- /dev/null
+++ b/packages/server/src/utils/buildChatflow.ts
@@ -0,0 +1,427 @@
+import { Request } from 'express'
+import { IFileUpload, getStoragePath, convertSpeechToText, ICommonObject } from 'flowise-components'
+import { StatusCodes } from 'http-status-codes'
+import { IncomingInput, IMessage, INodeData, IReactFlowObject, IReactFlowNode, IDepthQueue, chatType, IChatMessage } from '../Interface'
+import path from 'path'
+import { ChatFlow } from '../database/entities/ChatFlow'
+import { Server } from 'socket.io'
+import { getRunningExpressApp } from '../utils/getRunningExpressApp'
+import {
+ mapMimeTypeToInputField,
+ isFlowValidForStream,
+ buildFlow,
+ getTelemetryFlowObj,
+ getAppVersion,
+ resolveVariables,
+ getSessionChatHistory,
+ findMemoryNode,
+ replaceInputsWithConfig,
+ getStartingNodes,
+ isStartNodeDependOnInput,
+ getMemorySessionId,
+ isSameOverrideConfig,
+ getEndingNodes,
+ constructGraphs
+} from '../utils'
+import { utilValidateKey } from './validateKey'
+import { databaseEntities } from '.'
+import { v4 as uuidv4 } from 'uuid'
+import { omit } from 'lodash'
+import * as fs from 'fs'
+import logger from './logger'
+import { utilAddChatMessage } from './addChatMesage'
+
+/**
+ * Build Chatflow
+ * @param {Request} req
+ * @param {Server} socketIO
+ * @param {boolean} isInternal
+ * @param {boolean} isUpsert
+ */
+export const utilBuildChatflow = async (req: Request, socketIO?: Server, isInternal: boolean = false): Promise => {
+ try {
+ const appServer = getRunningExpressApp()
+ const chatflowid = req.params.id
+ let incomingInput: IncomingInput = req.body
+ let nodeToExecuteData: INodeData
+ const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({
+ id: chatflowid
+ })
+ if (!chatflow) {
+ return {
+ executionError: true,
+ status: StatusCodes.NOT_FOUND,
+ msg: `Chatflow ${chatflowid} not found`
+ }
+ }
+
+ const chatId = incomingInput.chatId ?? incomingInput.overrideConfig?.sessionId ?? uuidv4()
+ const userMessageDateTime = new Date()
+
+ if (!isInternal) {
+ const isKeyValidated = await utilValidateKey(req, chatflow)
+ if (!isKeyValidated) {
+ return {
+ executionError: true,
+ status: StatusCodes.UNAUTHORIZED,
+ msg: `Unauthorized`
+ }
+ }
+ }
+
+ let fileUploads: IFileUpload[] = []
+ if (incomingInput.uploads) {
+ fileUploads = incomingInput.uploads
+ for (let i = 0; i < fileUploads.length; i += 1) {
+ const upload = fileUploads[i]
+ if ((upload.type === 'file' || upload.type === 'audio') && upload.data) {
+ const filename = upload.name
+ const dir = path.join(getStoragePath(), chatflowid, chatId)
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir, { recursive: true })
+ }
+ const filePath = path.join(dir, filename)
+ const splitDataURI = upload.data.split(',')
+ const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
+ fs.writeFileSync(filePath, bf)
+
+ // Omit upload.data since we don't store the content in database
+ upload.type = 'stored-file'
+ fileUploads[i] = omit(upload, ['data'])
+ }
+
+ // Run Speech to Text conversion
+ if (upload.mime === 'audio/webm') {
+ let speechToTextConfig: ICommonObject = {}
+ if (chatflow.speechToText) {
+ const speechToTextProviders = JSON.parse(chatflow.speechToText)
+ for (const provider in speechToTextProviders) {
+ const providerObj = speechToTextProviders[provider]
+ if (providerObj.status) {
+ speechToTextConfig = providerObj
+ speechToTextConfig['name'] = provider
+ break
+ }
+ }
+ }
+ if (speechToTextConfig) {
+ const options: ICommonObject = {
+ chatId,
+ chatflowid,
+ appDataSource: appServer.AppDataSource,
+ databaseEntities: databaseEntities
+ }
+ const speechToTextResult = await convertSpeechToText(upload, speechToTextConfig, options)
+ if (speechToTextResult) {
+ incomingInput.question = speechToTextResult
+ }
+ }
+ }
+ }
+ }
+
+ let isStreamValid = false
+
+ const files = (req.files as any[]) || []
+
+ if (files.length) {
+ const overrideConfig: ICommonObject = { ...req.body }
+ for (const file of files) {
+ const fileData = fs.readFileSync(file.path, { encoding: 'base64' })
+ const dataBase64String = `data:${file.mimetype};base64,${fileData},filename:${file.filename}`
+
+ const fileInputField = mapMimeTypeToInputField(file.mimetype)
+ if (overrideConfig[fileInputField]) {
+ overrideConfig[fileInputField] = JSON.stringify([...JSON.parse(overrideConfig[fileInputField]), dataBase64String])
+ } else {
+ overrideConfig[fileInputField] = JSON.stringify([dataBase64String])
+ }
+ }
+ incomingInput = {
+ question: req.body.question ?? 'hello',
+ overrideConfig,
+ history: [],
+ socketIOClientId: req.body.socketIOClientId
+ }
+ }
+
+ /*** Get chatflows and prepare data ***/
+ const flowData = chatflow.flowData
+ const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
+ const nodes = parsedFlowData.nodes
+ const edges = parsedFlowData.edges
+
+ // Get session ID
+ const memoryNode = findMemoryNode(nodes, edges)
+ const memoryType = memoryNode?.data.label
+ let sessionId = undefined
+ if (memoryNode) sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal)
+
+ /* Reuse the flow without having to rebuild (to avoid duplicated upsert, recomputation, reinitialization of memory) when all these conditions met:
+ * - Node Data already exists in pool
+ * - Still in sync (i.e the flow has not been modified since)
+ * - Existing overrideConfig and new overrideConfig are the same
+ * - Flow doesn't start with/contain nodes that depend on incomingInput.question
+ * TODO: convert overrideConfig to hash when we no longer store base64 string but filepath
+ ***/
+ const isFlowReusable = () => {
+ return (
+ Object.prototype.hasOwnProperty.call(appServer.chatflowPool.activeChatflows, chatflowid) &&
+ appServer.chatflowPool.activeChatflows[chatflowid].inSync &&
+ appServer.chatflowPool.activeChatflows[chatflowid].endingNodeData &&
+ isSameOverrideConfig(
+ isInternal,
+ appServer.chatflowPool.activeChatflows[chatflowid].overrideConfig,
+ incomingInput.overrideConfig
+ ) &&
+ !isStartNodeDependOnInput(appServer.chatflowPool.activeChatflows[chatflowid].startingNodes, nodes)
+ )
+ }
+
+ if (isFlowReusable()) {
+ nodeToExecuteData = appServer.chatflowPool.activeChatflows[chatflowid].endingNodeData as INodeData
+ isStreamValid = isFlowValidForStream(nodes, nodeToExecuteData)
+ logger.debug(
+ `[server]: Reuse existing chatflow ${chatflowid} with ending node ${nodeToExecuteData.label} (${nodeToExecuteData.id})`
+ )
+ } else {
+ /*** Get Ending Node with Directed Graph ***/
+ const { graph, nodeDependencies } = constructGraphs(nodes, edges)
+ const directedGraph = graph
+ const endingNodeIds = getEndingNodes(nodeDependencies, directedGraph)
+ if (!endingNodeIds.length) {
+ return {
+ executionError: true,
+ status: 500,
+ msg: `Ending nodes not found`
+ }
+ }
+
+ const endingNodes = nodes.filter((nd) => endingNodeIds.includes(nd.id))
+
+ let isEndingNodeExists = endingNodes.find((node) => node.data?.outputs?.output === 'EndingNode')
+
+ for (const endingNode of endingNodes) {
+ const endingNodeData = endingNode.data
+ if (!endingNodeData) {
+ return {
+ executionError: true,
+ status: 500,
+ msg: `Ending node ${endingNode.id} data not found`
+ }
+ }
+
+ const isEndingNode = endingNodeData?.outputs?.output === 'EndingNode'
+
+ if (!isEndingNode) {
+ if (
+ endingNodeData &&
+ endingNodeData.category !== 'Chains' &&
+ endingNodeData.category !== 'Agents' &&
+ endingNodeData.category !== 'Engine'
+ ) {
+ return {
+ executionError: true,
+ status: 500,
+ msg: `Ending node must be either a Chain or Agent`
+ }
+ }
+
+ if (
+ endingNodeData.outputs &&
+ Object.keys(endingNodeData.outputs).length &&
+ !Object.values(endingNodeData.outputs ?? {}).includes(endingNodeData.name)
+ ) {
+ return {
+ executionError: true,
+ status: 500,
+ msg: `Output of ${endingNodeData.label} (${endingNodeData.id}) must be ${endingNodeData.label}, can't be an Output Prediction`
+ }
+ }
+ }
+
+ isStreamValid = isFlowValidForStream(nodes, endingNodeData)
+ }
+
+ // Once custom function ending node exists, flow is always unavailable to stream
+ isStreamValid = isEndingNodeExists ? false : isStreamValid
+
+ let chatHistory: IMessage[] = incomingInput.history ?? []
+
+ // When {{chat_history}} is used in Prompt Template, fetch the chat conversations from memory node
+ for (const endingNode of endingNodes) {
+ const endingNodeData = endingNode.data
+
+ if (!endingNodeData.inputs?.memory) continue
+
+ const memoryNodeId = endingNodeData.inputs?.memory.split('.')[0].replace('{{', '')
+ const memoryNode = nodes.find((node) => node.data.id === memoryNodeId)
+
+ if (!memoryNode) continue
+
+ if (!chatHistory.length && (incomingInput.chatId || incomingInput.overrideConfig?.sessionId)) {
+ chatHistory = await getSessionChatHistory(
+ memoryNode,
+ appServer.nodesPool.componentNodes,
+ incomingInput,
+ appServer.AppDataSource,
+ databaseEntities,
+ logger
+ )
+ }
+ }
+
+ /*** Get Starting Nodes with Reversed Graph ***/
+ const constructedObj = constructGraphs(nodes, edges, { isReversed: true })
+ const nonDirectedGraph = constructedObj.graph
+ let startingNodeIds: string[] = []
+ let depthQueue: IDepthQueue = {}
+ for (const endingNodeId of endingNodeIds) {
+ const resx = getStartingNodes(nonDirectedGraph, endingNodeId)
+ startingNodeIds.push(...resx.startingNodeIds)
+ depthQueue = Object.assign(depthQueue, resx.depthQueue)
+ }
+ startingNodeIds = [...new Set(startingNodeIds)]
+
+ const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id))
+
+ logger.debug(`[server]: Start building chatflow ${chatflowid}`)
+ /*** BFS to traverse from Starting Nodes to Ending Node ***/
+ const reactFlowNodes = await buildFlow(
+ startingNodeIds,
+ nodes,
+ edges,
+ graph,
+ depthQueue,
+ appServer.nodesPool.componentNodes,
+ incomingInput.question,
+ chatHistory,
+ chatId,
+ sessionId ?? '',
+ chatflowid,
+ appServer.AppDataSource,
+ incomingInput?.overrideConfig,
+ appServer.cachePool,
+ false,
+ undefined,
+ incomingInput.uploads
+ )
+
+ const nodeToExecute =
+ endingNodeIds.length === 1
+ ? reactFlowNodes.find((node: IReactFlowNode) => endingNodeIds[0] === node.id)
+ : reactFlowNodes[reactFlowNodes.length - 1]
+ if (!nodeToExecute) {
+ return {
+ executionError: true,
+ status: 404,
+ msg: `Node not found`
+ }
+ }
+
+ if (incomingInput.overrideConfig) {
+ nodeToExecute.data = replaceInputsWithConfig(nodeToExecute.data, incomingInput.overrideConfig)
+ }
+
+ const reactFlowNodeData: INodeData = resolveVariables(nodeToExecute.data, reactFlowNodes, incomingInput.question, chatHistory)
+ nodeToExecuteData = reactFlowNodeData
+
+ appServer.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes, incomingInput?.overrideConfig)
+ }
+
+ logger.debug(`[server]: Running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`)
+
+ const nodeInstanceFilePath = appServer.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string
+ const nodeModule = await import(nodeInstanceFilePath)
+ const nodeInstance = new nodeModule.nodeClass({ sessionId })
+
+ let result = isStreamValid
+ ? await nodeInstance.run(nodeToExecuteData, incomingInput.question, {
+ chatId,
+ chatflowid,
+ chatHistory: incomingInput.history,
+ logger,
+ appDataSource: appServer.AppDataSource,
+ databaseEntities,
+ analytic: chatflow.analytic,
+ uploads: incomingInput.uploads,
+ socketIO,
+ socketIOClientId: incomingInput.socketIOClientId
+ })
+ : await nodeInstance.run(nodeToExecuteData, incomingInput.question, {
+ chatId,
+ chatflowid,
+ chatHistory: incomingInput.history,
+ logger,
+ appDataSource: appServer.AppDataSource,
+ databaseEntities,
+ analytic: chatflow.analytic,
+ uploads: incomingInput.uploads
+ })
+ result = typeof result === 'string' ? { text: result } : result
+
+ // Retrieve threadId from assistant if exists
+ if (typeof result === 'object' && result.assistant) {
+ sessionId = result.assistant.threadId
+ }
+
+ const userMessage: Omit = {
+ role: 'userMessage',
+ content: incomingInput.question,
+ chatflowid,
+ chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
+ chatId,
+ memoryType,
+ sessionId,
+ createdDate: userMessageDateTime,
+ fileUploads: incomingInput.uploads ? JSON.stringify(fileUploads) : undefined
+ }
+ await utilAddChatMessage(userMessage)
+
+ let resultText = ''
+ if (result.text) resultText = result.text
+ else if (result.json) resultText = '```json\n' + JSON.stringify(result.json, null, 2)
+ else resultText = JSON.stringify(result, null, 2)
+
+ const apiMessage: Omit = {
+ role: 'apiMessage',
+ content: resultText,
+ chatflowid,
+ chatType: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
+ chatId,
+ memoryType,
+ sessionId
+ }
+ if (result?.sourceDocuments) apiMessage.sourceDocuments = JSON.stringify(result.sourceDocuments)
+ if (result?.usedTools) apiMessage.usedTools = JSON.stringify(result.usedTools)
+ if (result?.fileAnnotations) apiMessage.fileAnnotations = JSON.stringify(result.fileAnnotations)
+ const chatMessage = await utilAddChatMessage(apiMessage)
+
+ logger.debug(`[server]: Finished running ${nodeToExecuteData.label} (${nodeToExecuteData.id})`)
+ await appServer.telemetry.sendTelemetry('prediction_sent', {
+ version: await getAppVersion(),
+ chatflowId: chatflowid,
+ chatId,
+ type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
+ flowGraph: getTelemetryFlowObj(nodes, edges)
+ })
+
+ // Prepare response
+ // return the question in the response
+ // this is used when input text is empty but question is in audio format
+ result.question = incomingInput.question
+ result.chatId = chatId
+ result.chatMessageId = chatMessage.id
+ if (sessionId) result.sessionId = sessionId
+ if (memoryType) result.memoryType = memoryType
+
+ return result
+ } catch (e: any) {
+ logger.error('[server]: Error:', e)
+ return {
+ executionError: true,
+ status: 500,
+ msg: e.message
+ }
+ }
+}
diff --git a/packages/server/src/utils/getChatMessage.ts b/packages/server/src/utils/getChatMessage.ts
new file mode 100644
index 000000000..29335cbf4
--- /dev/null
+++ b/packages/server/src/utils/getChatMessage.ts
@@ -0,0 +1,101 @@
+import { MoreThanOrEqual, LessThanOrEqual } from 'typeorm'
+import { chatType } from '../Interface'
+import { ChatMessage } from '../database/entities/ChatMessage'
+import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback'
+import { getRunningExpressApp } from '../utils/getRunningExpressApp'
+/**
+ * Method that get chat messages.
+ * @param {string} chatflowid
+ * @param {chatType} chatType
+ * @param {string} sortOrder
+ * @param {string} chatId
+ * @param {string} memoryType
+ * @param {string} sessionId
+ * @param {string} startDate
+ * @param {string} endDate
+ * @param {boolean} feedback
+ */
+export const utilGetChatMessage = async (
+ chatflowid: string,
+ chatType: chatType | undefined,
+ sortOrder: string = 'ASC',
+ chatId?: string,
+ memoryType?: string,
+ sessionId?: string,
+ startDate?: string,
+ endDate?: string,
+ messageId?: string,
+ feedback?: boolean
+): Promise => {
+ const appServer = getRunningExpressApp()
+ const setDateToStartOrEndOfDay = (dateTimeStr: string, setHours: 'start' | 'end') => {
+ const date = new Date(dateTimeStr)
+ if (isNaN(date.getTime())) {
+ return undefined
+ }
+ setHours === 'start' ? date.setHours(0, 0, 0, 0) : date.setHours(23, 59, 59, 999)
+ return date
+ }
+
+ const aMonthAgo = () => {
+ const date = new Date()
+ date.setMonth(new Date().getMonth() - 1)
+ return date
+ }
+
+ let fromDate
+ if (startDate) fromDate = setDateToStartOrEndOfDay(startDate, 'start')
+
+ let toDate
+ if (endDate) toDate = setDateToStartOrEndOfDay(endDate, 'end')
+
+ if (feedback) {
+ const query = await appServer.AppDataSource.getRepository(ChatMessage).createQueryBuilder('chat_message')
+
+ // do the join with chat message feedback based on messageId for each chat message in the chatflow
+ query
+ .leftJoinAndMapOne('chat_message.feedback', ChatMessageFeedback, 'feedback', 'feedback.messageId = chat_message.id')
+ .where('chat_message.chatflowid = :chatflowid', { chatflowid })
+
+ // based on which parameters are available add `andWhere` clauses to the query
+ if (chatType) {
+ query.andWhere('chat_message.chatType = :chatType', { chatType })
+ }
+ if (chatId) {
+ query.andWhere('chat_message.chatId = :chatId', { chatId })
+ }
+ if (memoryType) {
+ query.andWhere('chat_message.memoryType = :memoryType', { memoryType })
+ }
+ if (sessionId) {
+ query.andWhere('chat_message.sessionId = :sessionId', { sessionId })
+ }
+
+ // set date range
+ query.andWhere('chat_message.createdDate BETWEEN :fromDate AND :toDate', {
+ fromDate: fromDate ?? aMonthAgo(),
+ toDate: toDate ?? new Date()
+ })
+ // sort
+ query.orderBy('chat_message.createdDate', sortOrder === 'DESC' ? 'DESC' : 'ASC')
+
+ const messages = await query.getMany()
+ return messages
+ }
+
+ return await appServer.AppDataSource.getRepository(ChatMessage).find({
+ where: {
+ chatflowid,
+ chatType,
+ chatId,
+ memoryType: memoryType ?? undefined,
+ sessionId: sessionId ?? undefined,
+ ...(fromDate && { createdDate: MoreThanOrEqual(fromDate) }),
+ ...(toDate && { createdDate: LessThanOrEqual(toDate) }),
+ id: messageId ?? undefined
+ },
+ order: {
+ createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC'
+ }
+ })
+}
diff --git a/packages/server/src/utils/getChatMessageFeedback.ts b/packages/server/src/utils/getChatMessageFeedback.ts
new file mode 100644
index 000000000..ebf2b9554
--- /dev/null
+++ b/packages/server/src/utils/getChatMessageFeedback.ts
@@ -0,0 +1,36 @@
+import { Between } from 'typeorm'
+import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback'
+import { getRunningExpressApp } from '../utils/getRunningExpressApp'
+
+/**
+ * Method that get chat messages.
+ * @param {string} chatflowid
+ * @param {string} sortOrder
+ * @param {string} chatId
+ * @param {string} startDate
+ * @param {string} endDate
+ */
+export const utilGetChatMessageFeedback = async (
+ chatflowid: string,
+ chatId?: string,
+ sortOrder: string = 'ASC',
+ startDate?: string,
+ endDate?: string
+): Promise => {
+ const appServer = getRunningExpressApp()
+ let fromDate
+ if (startDate) fromDate = new Date(startDate)
+
+ let toDate
+ if (endDate) toDate = new Date(endDate)
+ return await appServer.AppDataSource.getRepository(ChatMessageFeedback).find({
+ where: {
+ chatflowid,
+ chatId,
+ createdDate: toDate && fromDate ? Between(fromDate, toDate) : undefined
+ },
+ order: {
+ createdDate: sortOrder === 'DESC' ? 'DESC' : 'ASC'
+ }
+ })
+}
diff --git a/packages/server/src/utils/getRunningExpressApp.ts b/packages/server/src/utils/getRunningExpressApp.ts
new file mode 100644
index 000000000..e02f988f1
--- /dev/null
+++ b/packages/server/src/utils/getRunningExpressApp.ts
@@ -0,0 +1,9 @@
+import * as Server from '../index'
+
+export const getRunningExpressApp = function () {
+ const runningExpressInstance = Server.getInstance()
+ if (typeof runningExpressInstance === 'undefined' || typeof runningExpressInstance.nodesPool === 'undefined') {
+ throw new Error(`Error: getRunningExpressApp failed!`)
+ }
+ return runningExpressInstance
+}
diff --git a/packages/server/src/utils/getUploadsConfig.ts b/packages/server/src/utils/getUploadsConfig.ts
new file mode 100644
index 000000000..7be23a6cf
--- /dev/null
+++ b/packages/server/src/utils/getUploadsConfig.ts
@@ -0,0 +1,72 @@
+import { ChatFlow } from '../database/entities/ChatFlow'
+import { getRunningExpressApp } from '../utils/getRunningExpressApp'
+import { IUploadFileSizeAndTypes, IReactFlowNode } from '../Interface'
+import { INodeParams } from 'flowise-components'
+
+/**
+ * Method that checks if uploads are enabled in the chatflow
+ * @param {string} chatflowid
+ */
+export const utilGetUploadsConfig = async (chatflowid: string): Promise => {
+ const appServer = getRunningExpressApp()
+ const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({
+ id: chatflowid
+ })
+ if (!chatflow) return `Chatflow ${chatflowid} not found`
+
+ const uploadAllowedNodes = ['llmChain', 'conversationChain', 'mrklAgentChat', 'conversationalAgent']
+ const uploadProcessingNodes = ['chatOpenAI', 'chatAnthropic', 'awsChatBedrock', 'azureChatOpenAI']
+
+ const flowObj = JSON.parse(chatflow.flowData)
+ const imgUploadSizeAndTypes: IUploadFileSizeAndTypes[] = []
+
+ let isSpeechToTextEnabled = false
+ if (chatflow.speechToText) {
+ const speechToTextProviders = JSON.parse(chatflow.speechToText)
+ for (const provider in speechToTextProviders) {
+ if (provider !== 'none') {
+ const providerObj = speechToTextProviders[provider]
+ if (providerObj.status) {
+ isSpeechToTextEnabled = true
+ break
+ }
+ }
+ }
+ }
+
+ let isImageUploadAllowed = false
+ const nodes: IReactFlowNode[] = flowObj.nodes
+
+ /*
+ * Condition for isImageUploadAllowed
+ * 1.) one of the uploadAllowedNodes exists
+ * 2.) one of the uploadProcessingNodes exists + allowImageUploads is ON
+ */
+ if (!nodes.some((node) => uploadAllowedNodes.includes(node.data.name))) {
+ return {
+ isSpeechToTextEnabled,
+ isImageUploadAllowed: false,
+ imgUploadSizeAndTypes
+ }
+ }
+
+ nodes.forEach((node: IReactFlowNode) => {
+ if (uploadProcessingNodes.indexOf(node.data.name) > -1) {
+ // TODO: for now the maxUploadSize is hardcoded to 5MB, we need to add it to the node properties
+ node.data.inputParams.map((param: INodeParams) => {
+ if (param.name === 'allowImageUploads' && node.data.inputs?.['allowImageUploads']) {
+ imgUploadSizeAndTypes.push({
+ fileTypes: 'image/gif;image/jpeg;image/png;image/webp;'.split(';'),
+ maxUploadSize: 5
+ })
+ isImageUploadAllowed = true
+ }
+ })
+ }
+ })
+ return {
+ isSpeechToTextEnabled,
+ isImageUploadAllowed,
+ imgUploadSizeAndTypes
+ }
+}
diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts
index 9cff366ea..e41e9f64a 100644
--- a/packages/server/src/utils/index.ts
+++ b/packages/server/src/utils/index.ts
@@ -492,7 +492,7 @@ export const getVariableValue = (
isAcceptVariable = false
) => {
const isObject = typeof paramValue === 'object'
- let returnVal = isObject ? JSON.stringify(paramValue) : paramValue
+ let returnVal = (isObject ? JSON.stringify(paramValue) : paramValue) ?? ''
const variableStack = []
const variableDict = {} as IVariableDict
let startIdx = 0
diff --git a/packages/server/src/utils/rateLimit.ts b/packages/server/src/utils/rateLimit.ts
index 68b5b693b..0c08f1d24 100644
--- a/packages/server/src/utils/rateLimit.ts
+++ b/packages/server/src/utils/rateLimit.ts
@@ -23,11 +23,8 @@ async function addRateLimiter(id: string, duration: number, limit: number, messa
export function getRateLimiter(req: Request, res: Response, next: NextFunction) {
const id = req.params.id
-
if (!rateLimiters[id]) return next()
-
const idRateLimiter = rateLimiters[id]
-
return idRateLimiter(req, res, next)
}
diff --git a/packages/server/src/utils/updateChatMessageFeedback.ts b/packages/server/src/utils/updateChatMessageFeedback.ts
new file mode 100644
index 000000000..ef327fa78
--- /dev/null
+++ b/packages/server/src/utils/updateChatMessageFeedback.ts
@@ -0,0 +1,16 @@
+import { IChatMessageFeedback } from '../Interface'
+import { getRunningExpressApp } from '../utils/getRunningExpressApp'
+import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback'
+
+/**
+ * Method that updates chat message feedback.
+ * @param {string} id
+ * @param {Partial} chatMessageFeedback
+ */
+export const utilUpdateChatMessageFeedback = async (id: string, chatMessageFeedback: Partial) => {
+ const appServer = getRunningExpressApp()
+ const newChatMessageFeedback = new ChatMessageFeedback()
+ Object.assign(newChatMessageFeedback, chatMessageFeedback)
+ await appServer.AppDataSource.getRepository(ChatMessageFeedback).update({ id }, chatMessageFeedback)
+ return { status: 'OK' }
+}
diff --git a/packages/server/src/utils/upsertVector.ts b/packages/server/src/utils/upsertVector.ts
new file mode 100644
index 000000000..0fc90eb1c
--- /dev/null
+++ b/packages/server/src/utils/upsertVector.ts
@@ -0,0 +1,139 @@
+import { Request, Response } from 'express'
+import * as fs from 'fs'
+import { ICommonObject } from 'flowise-components'
+import telemetryService from '../services/telemetry'
+import logger from '../utils/logger'
+import {
+ buildFlow,
+ constructGraphs,
+ getAllConnectedNodes,
+ mapMimeTypeToInputField,
+ findMemoryNode,
+ getMemorySessionId,
+ getAppVersion,
+ getTelemetryFlowObj,
+ getStartingNodes
+} from '../utils'
+import { utilValidateKey } from './validateKey'
+import { IncomingInput, INodeDirectedGraph, IReactFlowObject, chatType } from '../Interface'
+import { ChatFlow } from '../database/entities/ChatFlow'
+import { getRunningExpressApp } from '../utils/getRunningExpressApp'
+
+export const upsertVector = async (req: Request, res: Response, isInternal: boolean = false) => {
+ try {
+ const appServer = getRunningExpressApp()
+ const chatflowid = req.params.id
+ let incomingInput: IncomingInput = req.body
+
+ const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({
+ id: chatflowid
+ })
+ if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`)
+
+ if (!isInternal) {
+ const isKeyValidated = await utilValidateKey(req, chatflow)
+ if (!isKeyValidated) return res.status(401).send('Unauthorized')
+ }
+
+ const files = (req.files as any[]) || []
+
+ if (files.length) {
+ const overrideConfig: ICommonObject = { ...req.body }
+ for (const file of files) {
+ const fileData = fs.readFileSync(file.path, { encoding: 'base64' })
+ const dataBase64String = `data:${file.mimetype};base64,${fileData},filename:${file.filename}`
+
+ const fileInputField = mapMimeTypeToInputField(file.mimetype)
+ if (overrideConfig[fileInputField]) {
+ overrideConfig[fileInputField] = JSON.stringify([...JSON.parse(overrideConfig[fileInputField]), dataBase64String])
+ } else {
+ overrideConfig[fileInputField] = JSON.stringify([dataBase64String])
+ }
+ }
+ incomingInput = {
+ question: req.body.question ?? 'hello',
+ overrideConfig,
+ history: [],
+ stopNodeId: req.body.stopNodeId
+ }
+ }
+
+ /*** Get chatflows and prepare data ***/
+ const flowData = chatflow.flowData
+ const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
+ const nodes = parsedFlowData.nodes
+ const edges = parsedFlowData.edges
+
+ let stopNodeId = incomingInput?.stopNodeId ?? ''
+ let chatHistory = incomingInput?.history
+ let chatId = incomingInput.chatId ?? ''
+ let isUpsert = true
+
+ // Get session ID
+ const memoryNode = findMemoryNode(nodes, edges)
+ let sessionId = undefined
+ if (memoryNode) sessionId = getMemorySessionId(memoryNode, incomingInput, chatId, isInternal)
+
+ const vsNodes = nodes.filter(
+ (node) =>
+ node.data.category === 'Vector Stores' && !node.data.label.includes('Upsert') && !node.data.label.includes('Load Existing')
+ )
+ if (vsNodes.length > 1 && !stopNodeId) {
+ return res.status(500).send('There are multiple vector nodes, please provide stopNodeId in body request')
+ } else if (vsNodes.length === 1 && !stopNodeId) {
+ stopNodeId = vsNodes[0].data.id
+ } else if (!vsNodes.length && !stopNodeId) {
+ return res.status(500).send('No vector node found')
+ }
+
+ const { graph } = constructGraphs(nodes, edges, { isReversed: true })
+
+ const nodeIds = getAllConnectedNodes(graph, stopNodeId)
+
+ const filteredGraph: INodeDirectedGraph = {}
+ for (const key of nodeIds) {
+ if (Object.prototype.hasOwnProperty.call(graph, key)) {
+ filteredGraph[key] = graph[key]
+ }
+ }
+
+ const { startingNodeIds, depthQueue } = getStartingNodes(filteredGraph, stopNodeId)
+
+ await buildFlow(
+ startingNodeIds,
+ nodes,
+ edges,
+ filteredGraph,
+ depthQueue,
+ appServer.nodesPool.componentNodes,
+ incomingInput.question,
+ chatHistory,
+ chatId,
+ sessionId ?? '',
+ chatflowid,
+ appServer.AppDataSource,
+ incomingInput?.overrideConfig,
+ appServer.cachePool,
+ isUpsert,
+ stopNodeId
+ )
+
+ const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.data.id))
+
+ await appServer.chatflowPool.add(chatflowid, undefined, startingNodes, incomingInput?.overrideConfig)
+ await telemetryService.createEvent({
+ name: `vector_upserted`,
+ data: {
+ version: await getAppVersion(),
+ chatlowId: chatflowid,
+ type: isInternal ? chatType.INTERNAL : chatType.EXTERNAL,
+ flowGraph: getTelemetryFlowObj(nodes, edges),
+ stopNodeId
+ }
+ })
+ return res.status(201).send('Successfully Upserted')
+ } catch (e: any) {
+ logger.error('[server]: Error:', e)
+ return res.status(500).send(e.message)
+ }
+}
diff --git a/packages/server/src/utils/validateKey.ts b/packages/server/src/utils/validateKey.ts
new file mode 100644
index 000000000..02d36cf28
--- /dev/null
+++ b/packages/server/src/utils/validateKey.ts
@@ -0,0 +1,26 @@
+import { Request } from 'express'
+import { ChatFlow } from '../database/entities/ChatFlow'
+import { getAPIKeys, compareKeys } from './apiKey'
+
+/**
+ * Validate API Key
+ * @param {Request} req
+ * @param {Response} res
+ * @param {ChatFlow} chatflow
+ */
+export const utilValidateKey = async (req: Request, chatflow: ChatFlow) => {
+ const chatFlowApiKeyId = chatflow.apikeyid
+ if (!chatFlowApiKeyId) return true
+
+ const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? ''
+ if (chatFlowApiKeyId && !authorizationHeader) return false
+
+ const suppliedKey = authorizationHeader.split(`Bearer `).pop()
+ if (suppliedKey) {
+ const keys = await getAPIKeys()
+ const apiSecret = keys.find((key) => key.id === chatFlowApiKeyId)?.apiSecret
+ if (!compareKeys(apiSecret, suppliedKey)) return false
+ return true
+ }
+ return false
+}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 5e4c0fcfb..b07446345 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -22,7 +22,7 @@
"@uiw/codemirror-theme-sublime": "^4.21.21",
"@uiw/codemirror-theme-vscode": "^4.21.21",
"@uiw/react-codemirror": "^4.21.21",
- "axios": "^0.27.2",
+ "axios": "1.6.2",
"clsx": "^1.1.1",
"dotenv": "^16.0.0",
"flowise-embed": "latest",
diff --git a/packages/ui/src/store/reducers/dialogReducer.js b/packages/ui/src/store/reducers/dialogReducer.js
index ded576228..decd3922c 100644
--- a/packages/ui/src/store/reducers/dialogReducer.js
+++ b/packages/ui/src/store/reducers/dialogReducer.js
@@ -5,7 +5,8 @@ export const initialState = {
title: '',
description: '',
confirmButtonName: 'OK',
- cancelButtonName: 'Cancel'
+ cancelButtonName: 'Cancel',
+ customBtnId: ''
}
const alertReducer = (state = initialState, action) => {
@@ -16,7 +17,8 @@ const alertReducer = (state = initialState, action) => {
title: action.payload.title,
description: action.payload.description,
confirmButtonName: action.payload.confirmButtonName,
- cancelButtonName: action.payload.cancelButtonName
+ cancelButtonName: action.payload.cancelButtonName,
+ customBtnId: 'btn_confirmDeletingApiKey'
}
case HIDE_CONFIRM:
return initialState
diff --git a/packages/ui/src/ui-component/button/FlowListMenu.jsx b/packages/ui/src/ui-component/button/FlowListMenu.jsx
index bfee4eb16..a70e72767 100644
--- a/packages/ui/src/ui-component/button/FlowListMenu.jsx
+++ b/packages/ui/src/ui-component/button/FlowListMenu.jsx
@@ -153,9 +153,8 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
await updateChatflowApi.request(chatflow.id, updateBody)
await updateFlowsApi.request()
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: errorData,
+ message: error.response.data.message,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -192,9 +191,8 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
await updateChatflowApi.request(chatflow.id, updateBody)
await updateFlowsApi.request()
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: errorData,
+ message: error.response.data.message,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -224,9 +222,8 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
await chatflowsApi.deleteChatflow(chatflow.id)
await updateFlowsApi.request()
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: errorData,
+ message: error.response.data.message,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.jsx b/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.jsx
index 7ce5e15e4..ae9869fa7 100644
--- a/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.jsx
+++ b/packages/ui/src/ui-component/dialog/ManageScrapedLinksDialog.jsx
@@ -82,9 +82,8 @@ const ManageScrapedLinksDialog = ({ show, dialogProps, onCancel, onSave }) => {
})
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: errorData,
+ message: error.response.data.message,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx
index 80d0b45f0..fb672e06e 100644
--- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx
+++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx
@@ -261,9 +261,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
getChatmessageApi.request(chatflowid)
getStatsApi.request(chatflowid) // update stats
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: errorData,
+ message: error.response.data.message,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/ui-component/extended/AllowedDomains.jsx b/packages/ui/src/ui-component/extended/AllowedDomains.jsx
index e0d01dd36..0f37785f9 100644
--- a/packages/ui/src/ui-component/extended/AllowedDomains.jsx
+++ b/packages/ui/src/ui-component/extended/AllowedDomains.jsx
@@ -69,9 +69,8 @@ const AllowedDomains = ({ dialogProps }) => {
dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data })
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to save Allowed Origins: ${errorData}`,
+ message: `Failed to save Allowed Origins: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/ui-component/extended/AnalyseFlow.jsx b/packages/ui/src/ui-component/extended/AnalyseFlow.jsx
index 4d00a12f9..a31bc6bb6 100644
--- a/packages/ui/src/ui-component/extended/AnalyseFlow.jsx
+++ b/packages/ui/src/ui-component/extended/AnalyseFlow.jsx
@@ -144,9 +144,8 @@ const AnalyseFlow = ({ dialogProps }) => {
dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data })
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to save Analytic Configuration: ${errorData}`,
+ message: `Failed to save Analytic Configuration: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/ui-component/extended/ChatFeedback.jsx b/packages/ui/src/ui-component/extended/ChatFeedback.jsx
index 1706d6241..fcebf8f06 100644
--- a/packages/ui/src/ui-component/extended/ChatFeedback.jsx
+++ b/packages/ui/src/ui-component/extended/ChatFeedback.jsx
@@ -59,9 +59,8 @@ const ChatFeedback = ({ dialogProps }) => {
dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data })
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to save Chat Feedback Settings: ${errorData}`,
+ message: `Failed to save Chat Feedback Settings: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/ui-component/extended/RateLimit.jsx b/packages/ui/src/ui-component/extended/RateLimit.jsx
index 5fc88cae2..5ab8532bd 100644
--- a/packages/ui/src/ui-component/extended/RateLimit.jsx
+++ b/packages/ui/src/ui-component/extended/RateLimit.jsx
@@ -73,12 +73,8 @@ const RateLimit = () => {
dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data })
}
} catch (error) {
- console.error(error)
- const errorData = error.response
- ? error.response.data || `${error.response.status}: ${error.response.statusText}`
- : error.message
enqueueSnackbar({
- message: `Failed to save Rate Limit Configuration: ${errorData}`,
+ message: `Failed to save Rate Limit Configuration: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/ui-component/extended/SpeechToText.jsx b/packages/ui/src/ui-component/extended/SpeechToText.jsx
index b56c8f852..ba875544c 100644
--- a/packages/ui/src/ui-component/extended/SpeechToText.jsx
+++ b/packages/ui/src/ui-component/extended/SpeechToText.jsx
@@ -112,9 +112,8 @@ const SpeechToText = ({ dialogProps }) => {
dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data })
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to save Speech To Text Configuration: ${errorData}`,
+ message: `Failed to save Speech To Text Configuration: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/ui-component/extended/StarterPrompts.jsx b/packages/ui/src/ui-component/extended/StarterPrompts.jsx
index a12c51bbb..f19f326aa 100644
--- a/packages/ui/src/ui-component/extended/StarterPrompts.jsx
+++ b/packages/ui/src/ui-component/extended/StarterPrompts.jsx
@@ -80,9 +80,8 @@ const StarterPrompts = ({ dialogProps }) => {
dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data })
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to save Conversation Starter Prompts: ${errorData}`,
+ message: `Failed to save Conversation Starter Prompts: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/ui-component/switch/Switch.jsx b/packages/ui/src/ui-component/switch/Switch.jsx
index 50ace1197..e2d2dd78e 100644
--- a/packages/ui/src/ui-component/switch/Switch.jsx
+++ b/packages/ui/src/ui-component/switch/Switch.jsx
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
import { FormControl, Switch, Typography } from '@mui/material'
export const SwitchInput = ({ label, value, onChange, disabled = false }) => {
- const [myValue, setMyValue] = useState(!!value ?? false)
+ const [myValue, setMyValue] = useState(value !== undefined ? !!value : false)
useEffect(() => {
setMyValue(value)
diff --git a/packages/ui/src/views/apikey/APIKeyDialog.jsx b/packages/ui/src/views/apikey/APIKeyDialog.jsx
index 48262563e..c0326eb9d 100644
--- a/packages/ui/src/views/apikey/APIKeyDialog.jsx
+++ b/packages/ui/src/views/apikey/APIKeyDialog.jsx
@@ -77,9 +77,8 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
onConfirm()
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to add new API key: ${errorData}`,
+ message: `Failed to add new API key: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -114,9 +113,8 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
onConfirm()
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to save API key: ${errorData}`,
+ message: `Failed to save API key: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -211,7 +209,11 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
- (dialogProps.type === 'ADD' ? addNewKey() : saveKey())}>
+ (dialogProps.type === 'ADD' ? addNewKey() : saveKey())}
+ id={dialogProps.customBtnId}
+ >
{dialogProps.confirmButtonName}
diff --git a/packages/ui/src/views/apikey/index.jsx b/packages/ui/src/views/apikey/index.jsx
index 935064c95..9eeb0a8c8 100644
--- a/packages/ui/src/views/apikey/index.jsx
+++ b/packages/ui/src/views/apikey/index.jsx
@@ -234,7 +234,8 @@ const APIKey = () => {
title: 'Add New API Key',
type: 'ADD',
cancelButtonName: 'Cancel',
- confirmButtonName: 'Add'
+ confirmButtonName: 'Add',
+ customBtnId: 'btn_confirmAddingApiKey'
}
setDialogProps(dialogProp)
setShowDialog(true)
@@ -246,6 +247,7 @@ const APIKey = () => {
type: 'EDIT',
cancelButtonName: 'Cancel',
confirmButtonName: 'Save',
+ customBtnId: 'btn_confirmEditingApiKey',
key
}
setDialogProps(dialogProp)
@@ -260,7 +262,8 @@ const APIKey = () => {
? `Delete key [${key.keyName}] ? `
: `Delete key [${key.keyName}] ?\n There are ${key.chatFlows.length} chatflows using this key.`,
confirmButtonName: 'Delete',
- cancelButtonName: 'Cancel'
+ cancelButtonName: 'Cancel',
+ customBtnId: 'btn_initiateDeleteApiKey'
}
const isConfirmed = await confirm(confirmPayload)
@@ -283,9 +286,8 @@ const APIKey = () => {
onConfirm()
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to delete API key: ${errorData}`,
+ message: `Failed to delete API key: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -363,6 +365,7 @@ const APIKey = () => {
sx={{ color: 'white', mr: 1, height: 37 }}
onClick={addNew}
startIcon={}
+ id='btn_createApiKey'
>
Create Key
diff --git a/packages/ui/src/views/assistants/AssistantDialog.jsx b/packages/ui/src/views/assistants/AssistantDialog.jsx
index 88376a82f..da2e54adf 100644
--- a/packages/ui/src/views/assistants/AssistantDialog.jsx
+++ b/packages/ui/src/views/assistants/AssistantDialog.jsx
@@ -235,9 +235,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
}
setLoading(false)
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to add new Assistant: ${errorData}`,
+ message: `Failed to add new Assistant: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -289,9 +288,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
}
setLoading(false)
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to save Assistant: ${errorData}`,
+ message: `Failed to save Assistant: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -329,9 +327,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
}
setLoading(false)
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to sync Assistant: ${errorData}`,
+ message: `Failed to sync Assistant: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -376,9 +373,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
onConfirm()
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to delete Assistant: ${errorData}`,
+ message: `Failed to delete Assistant: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/views/canvas/index.jsx b/packages/ui/src/views/canvas/index.jsx
index badbb162f..0e690218a 100644
--- a/packages/ui/src/views/canvas/index.jsx
+++ b/packages/ui/src/views/canvas/index.jsx
@@ -172,9 +172,8 @@ const Canvas = () => {
localStorage.removeItem(`${chatflow.id}_INTERNAL`)
navigate('/')
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: errorData,
+ message: error.response.data.message,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -359,9 +358,7 @@ const Canvas = () => {
setEdges(initialFlow.edges || [])
dispatch({ type: SET_CHATFLOW, chatflow })
} else if (getSpecificChatflowApi.error) {
- const error = getSpecificChatflowApi.error
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
- errorFailed(`Failed to retrieve chatflow: ${errorData}`)
+ errorFailed(`Failed to retrieve chatflow: ${error.response.data.message}`)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -375,9 +372,7 @@ const Canvas = () => {
saveChatflowSuccess()
window.history.replaceState(null, null, `/canvas/${chatflow.id}`)
} else if (createNewChatflowApi.error) {
- const error = createNewChatflowApi.error
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
- errorFailed(`Failed to save chatflow: ${errorData}`)
+ errorFailed(`Failed to save chatflow: ${error.response.data.message}`)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -389,9 +384,7 @@ const Canvas = () => {
dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data })
saveChatflowSuccess()
} else if (updateChatflowApi.error) {
- const error = updateChatflowApi.error
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
- errorFailed(`Failed to save chatflow: ${errorData}`)
+ errorFailed(`Failed to save chatflow: ${error.response.data.message}`)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/packages/ui/src/views/chatflows/ShareChatbot.jsx b/packages/ui/src/views/chatflows/ShareChatbot.jsx
index 28ad587b6..35003594c 100644
--- a/packages/ui/src/views/chatflows/ShareChatbot.jsx
+++ b/packages/ui/src/views/chatflows/ShareChatbot.jsx
@@ -161,10 +161,8 @@ const ShareChatbot = ({ isSessionMemory }) => {
dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data })
}
} catch (error) {
- console.error(error)
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to save Chatbot Configuration: ${errorData}`,
+ message: `Failed to save Chatbot Configuration: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -198,10 +196,8 @@ const ShareChatbot = ({ isSessionMemory }) => {
dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data })
}
} catch (error) {
- console.error(error)
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to save Chatbot Configuration: ${errorData}`,
+ message: `Failed to save Chatbot Configuration: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/views/chatmessage/ChatMessage.jsx b/packages/ui/src/views/chatmessage/ChatMessage.jsx
index 90dc1a19a..8626ff13c 100644
--- a/packages/ui/src/views/chatmessage/ChatMessage.jsx
+++ b/packages/ui/src/views/chatmessage/ChatMessage.jsx
@@ -456,8 +456,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog, previews, setPreviews
}, 100)
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
- handleError(errorData)
+ handleError(error.response.data.message)
return
}
}
diff --git a/packages/ui/src/views/chatmessage/ChatPopUp.jsx b/packages/ui/src/views/chatmessage/ChatPopUp.jsx
index 45557484c..b592643b0 100644
--- a/packages/ui/src/views/chatmessage/ChatPopUp.jsx
+++ b/packages/ui/src/views/chatmessage/ChatPopUp.jsx
@@ -105,9 +105,8 @@ export const ChatPopUp = ({ chatflowid }) => {
}
})
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: errorData,
+ message: error.response.data.message,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx b/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx
index 64b7590e2..d21ab95ca 100644
--- a/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx
+++ b/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx
@@ -118,9 +118,8 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) =>
onConfirm(createResp.data.id)
}
} catch (error) {
- const errorData = typeof err === 'string' ? err : err.response.data || `${err.response.status}: ${err.response.statusText}`
enqueueSnackbar({
- message: `Failed to add new Credential: ${errorData}`,
+ message: `Failed to add new Credential: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -168,9 +167,8 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) =>
onConfirm(saveResp.data.id)
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to save Credential: ${errorData}`,
+ message: `Failed to save Credential: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/views/credentials/index.jsx b/packages/ui/src/views/credentials/index.jsx
index b22b54604..6e1441015 100644
--- a/packages/ui/src/views/credentials/index.jsx
+++ b/packages/ui/src/views/credentials/index.jsx
@@ -139,9 +139,8 @@ const Credentials = () => {
onConfirm()
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to delete Credential: ${errorData}`,
+ message: `Failed to delete Credential: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/views/tools/ToolDialog.jsx b/packages/ui/src/views/tools/ToolDialog.jsx
index da540ba14..0a314f771 100644
--- a/packages/ui/src/views/tools/ToolDialog.jsx
+++ b/packages/ui/src/views/tools/ToolDialog.jsx
@@ -225,9 +225,8 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
linkElement.click()
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to export Tool: ${errorData}`,
+ message: `Failed to export Tool: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -270,9 +269,8 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
onConfirm(createResp.data.id)
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to add new Tool: ${errorData}`,
+ message: `Failed to add new Tool: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -313,10 +311,8 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
onConfirm(saveResp.data.id)
}
} catch (error) {
- console.error(error)
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to save Tool: ${errorData}`,
+ message: `Failed to save Tool: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -360,9 +356,8 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
onConfirm()
}
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: `Failed to delete Tool: ${errorData}`,
+ message: `Failed to delete Tool: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/views/variables/AddEditVariableDialog.jsx b/packages/ui/src/views/variables/AddEditVariableDialog.jsx
index 295eb3f9b..ba4f45b1a 100644
--- a/packages/ui/src/views/variables/AddEditVariableDialog.jsx
+++ b/packages/ui/src/views/variables/AddEditVariableDialog.jsx
@@ -111,9 +111,8 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
onConfirm(createResp.data.id)
}
} catch (err) {
- const errorData = typeof err === 'string' ? err : err.response?.data || `${err.response?.status}: ${err.response?.statusText}`
enqueueSnackbar({
- message: `Failed to add new Variable: ${errorData}`,
+ message: `Failed to add new Variable: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -154,9 +153,8 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
onConfirm(saveResp.data.id)
}
} catch (error) {
- const errorData = error.response?.data || `${error.response?.status}: ${error.response?.statusText}`
enqueueSnackbar({
- message: `Failed to save Variable: ${errorData}`,
+ message: `Failed to save Variable: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -222,6 +220,7 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
key='variableName'
onChange={(e) => setVariableName(e.target.value)}
value={variableName ?? ''}
+ id='txtInput_variableName'
/>
@@ -237,6 +236,7 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
options={variableTypes}
onSelect={(newValue) => setVariableType(newValue)}
value={variableType ?? 'choose an option'}
+ id='dropdown_variableType'
/>
{variableType === 'static' && (
@@ -255,6 +255,7 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
key='variableValue'
onChange={(e) => setVariableValue(e.target.value)}
value={variableValue ?? ''}
+ id='txtInput_variableValue'
/>
)}
@@ -264,6 +265,7 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
disabled={!variableName || !variableType || (variableType === 'static' && !variableValue)}
variant='contained'
onClick={() => (dialogType === 'ADD' ? addNewVariable() : saveVariable())}
+ id='btn_confirmAddingNewVariable'
>
{dialogProps.confirmButtonName}
diff --git a/packages/ui/src/views/variables/index.jsx b/packages/ui/src/views/variables/index.jsx
index ce599a315..cc6aee78e 100644
--- a/packages/ui/src/views/variables/index.jsx
+++ b/packages/ui/src/views/variables/index.jsx
@@ -81,6 +81,7 @@ const Variables = () => {
type: 'ADD',
cancelButtonName: 'Cancel',
confirmButtonName: 'Add',
+ customBtnId: 'btn_confirmAddingVariable',
data: {}
}
setVariableDialogProps(dialogProp)
@@ -126,9 +127,8 @@ const Variables = () => {
onConfirm()
}
} catch (error) {
- const errorData = error.response?.data || `${error.response?.status}: ${error.response?.statusText}`
enqueueSnackbar({
- message: `Failed to delete Variable: ${errorData}`,
+ message: `Failed to delete Variable: ${error.response.data.message}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
@@ -207,6 +207,7 @@ const Variables = () => {
sx={{ color: 'white', mr: 1, height: 37 }}
onClick={addNew}
startIcon={}
+ id='btn_createVariable'
>
Add Variable
diff --git a/packages/ui/src/views/vectorstore/VectorStoreDialog.jsx b/packages/ui/src/views/vectorstore/VectorStoreDialog.jsx
index 7bc796ebb..6426a08fc 100644
--- a/packages/ui/src/views/vectorstore/VectorStoreDialog.jsx
+++ b/packages/ui/src/views/vectorstore/VectorStoreDialog.jsx
@@ -291,9 +291,8 @@ query(formData).then((response) => {
})
setLoading(false)
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: errorData,
+ message: error.response.data.message,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/packages/ui/src/views/vectorstore/VectorStorePopUp.jsx b/packages/ui/src/views/vectorstore/VectorStorePopUp.jsx
index a77d94866..13cd9043c 100644
--- a/packages/ui/src/views/vectorstore/VectorStorePopUp.jsx
+++ b/packages/ui/src/views/vectorstore/VectorStorePopUp.jsx
@@ -59,9 +59,8 @@ export const VectorStorePopUp = ({ chatflowid }) => {
}
})
} catch (error) {
- const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
- message: errorData,
+ message: error.response.data.message,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 62b589010..2f9d6aa39 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -187,7 +187,7 @@ importers:
version: 4.3.2
axios:
specifier: 1.6.2
- version: 1.6.2
+ version: 1.6.2(debug@4.3.4)
cheerio:
specifier: ^1.0.0-rc.12
version: 1.0.0-rc.12
@@ -411,7 +411,7 @@ importers:
version: 0.4.1
axios:
specifier: 1.6.2
- version: 1.6.2
+ version: 1.6.2(debug@4.3.4)
content-disposition:
specifier: 0.5.4
version: 0.5.4
@@ -439,6 +439,12 @@ importers:
flowise-ui:
specifier: workspace:^
version: link:../ui
+ http-errors:
+ specifier: ^2.0.0
+ version: 2.0.0
+ http-status-codes:
+ specifier: ^2.3.0
+ version: 2.3.0
lodash:
specifier: ^4.17.21
version: 4.17.21
@@ -503,6 +509,9 @@ importers:
concurrently:
specifier: ^7.1.0
version: 7.6.0
+ cypress:
+ specifier: ^13.7.1
+ version: 13.7.1
nodemon:
specifier: ^2.0.22
version: 2.0.22
@@ -518,6 +527,9 @@ importers:
shx:
specifier: ^0.3.3
version: 0.3.4
+ start-server-and-test:
+ specifier: ^2.0.3
+ version: 2.0.3
ts-node:
specifier: ^10.7.0
version: 10.9.2(@types/node@20.11.26)(typescript@4.9.5)
@@ -573,8 +585,8 @@ importers:
specifier: ^4.21.21
version: 4.21.24(@babel/runtime@7.24.0)(@codemirror/autocomplete@6.14.0)(@codemirror/language@6.10.1)(@codemirror/lint@6.5.0)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.25.1)(codemirror@6.0.1)(react-dom@18.2.0)(react@18.2.0)
axios:
- specifier: ^0.27.2
- version: 0.27.2
+ specifier: 1.6.2
+ version: 1.6.2(debug@4.3.4)
clsx:
specifier: ^1.1.1
version: 1.2.1
@@ -3356,6 +3368,12 @@ packages:
w3c-keyname: 2.2.8
dev: false
+ /@colors/colors@1.5.0:
+ resolution: { integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== }
+ engines: { node: '>=0.1.90' }
+ dev: true
+ optional: true
+
/@colors/colors@1.6.0:
resolution: { integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== }
engines: { node: '>=0.1.90' }
@@ -3531,6 +3549,39 @@ packages:
postcss-selector-parser: 6.0.15
dev: true
+ /@cypress/request@3.0.1:
+ resolution: { integrity: sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ== }
+ engines: { node: '>= 6' }
+ dependencies:
+ aws-sign2: 0.7.0
+ aws4: 1.12.0
+ caseless: 0.12.0
+ combined-stream: 1.0.8
+ extend: 3.0.2
+ forever-agent: 0.6.1
+ form-data: 2.3.3
+ http-signature: 1.3.6
+ is-typedarray: 1.0.0
+ isstream: 0.1.2
+ json-stringify-safe: 5.0.1
+ mime-types: 2.1.35
+ performance-now: 2.1.0
+ qs: 6.10.4
+ safe-buffer: 5.2.1
+ tough-cookie: 4.1.3
+ tunnel-agent: 0.6.0
+ uuid: 8.3.2
+ dev: true
+
+ /@cypress/xvfb@1.2.4(supports-color@8.1.1):
+ resolution: { integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== }
+ dependencies:
+ debug: 3.2.7(supports-color@8.1.1)
+ lodash.once: 4.1.1
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/@dabh/diagnostics@2.0.3:
resolution: { integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== }
dependencies:
@@ -3544,7 +3595,7 @@ packages:
engines: { node: '>=14.0.0' }
hasBin: true
dependencies:
- axios: 1.6.2
+ axios: 1.6.2(debug@4.3.4)
bson: 6.4.0
winston: 3.12.0
transitivePeerDependencies:
@@ -4289,6 +4340,16 @@ packages:
yargs: 17.7.2
dev: false
+ /@hapi/hoek@9.3.0:
+ resolution: { integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== }
+ dev: true
+
+ /@hapi/topo@5.1.0:
+ resolution: { integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== }
+ dependencies:
+ '@hapi/hoek': 9.3.0
+ dev: true
+
/@huggingface/inference@2.6.4:
resolution: { integrity: sha512-Xna7arltBSBoKaH3diGi3sYvkExgJMd/pF4T6vl2YbmDccbr1G/X5EPZ2048p+YgrJYG1jTYFCtY6Dr3HvJaow== }
engines: { node: '>=18' }
@@ -6450,6 +6511,20 @@ packages:
resolution: { integrity: sha512-ARhyoYDnY1LES3vYI0fiG6e9esWfTNcXcO6+MPJJXcnyMV3bim4lnFt45VXouV7y82F4x3YH8nOQ6VztuvUiWg== }
dev: false
+ /@sideway/address@4.1.5:
+ resolution: { integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== }
+ dependencies:
+ '@hapi/hoek': 9.3.0
+ dev: true
+
+ /@sideway/formula@3.0.1:
+ resolution: { integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== }
+ dev: true
+
+ /@sideway/pinpoint@2.0.0:
+ resolution: { integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== }
+ dev: true
+
/@sigstore/bundle@1.1.0:
resolution: { integrity: sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog== }
engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 }
@@ -8064,6 +8139,14 @@ packages:
'@types/node': 20.11.26
dev: true
+ /@types/sinonjs__fake-timers@8.1.1:
+ resolution: { integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== }
+ dev: true
+
+ /@types/sizzle@2.3.8:
+ resolution: { integrity: sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg== }
+ dev: true
+
/@types/sockjs@0.3.36:
resolution: { integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== }
dependencies:
@@ -8175,7 +8258,6 @@ packages:
resolution: { integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== }
dependencies:
'@types/node': 20.11.26
- dev: false
optional: true
/@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.57.0)(typescript@4.9.5):
@@ -8908,7 +8990,6 @@ packages:
/ansi-colors@4.1.3:
resolution: { integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== }
engines: { node: '>=6' }
- dev: false
/ansi-cyan@0.1.1:
resolution: { integrity: sha512-eCjan3AVo/SxZ0/MyIYRtkpxIu/H3xZN7URr1vXVrISxeyz8fUFz0FJziamK4sS8I+t35y4rHg1b2PklyBe/7A== }
@@ -9136,6 +9217,10 @@ packages:
/aproba@2.0.0:
resolution: { integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== }
+ /arch@2.2.0:
+ resolution: { integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== }
+ dev: true
+
/archy@1.0.0:
resolution: { integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== }
dev: true
@@ -9415,6 +9500,12 @@ packages:
/asap@2.0.6:
resolution: { integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== }
+ /asn1@0.2.6:
+ resolution: { integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== }
+ dependencies:
+ safer-buffer: 2.1.2
+ dev: true
+
/assemble-core@0.25.0:
resolution: { integrity: sha512-5vS/XZK0ke3gIHoKTyl88brqOR9zw3niz5jJHrEgrDLlZGEri4a1Wr4badallKCx4M4/TWG12GT/O5wABZjaVA== }
engines: { node: '>=4.0' }
@@ -9504,6 +9595,11 @@ packages:
- utf-8-validate
dev: false
+ /assert-plus@1.0.0:
+ resolution: { integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== }
+ engines: { node: '>=0.8' }
+ dev: true
+
/assign-deep@0.4.8:
resolution: { integrity: sha512-uxqXJCnNZDEjPnsaLKVzmh/ST5+Pqoz0wi06HDfHKx1ASNpSbbvz2qW2Gl8ZyHwr5jnm11X2S5eMQaP1lMZmCg== }
engines: { node: '>=0.10.0' }
@@ -9666,9 +9762,12 @@ packages:
xml2js: 0.6.2
dev: true
+ /aws-sign2@0.7.0:
+ resolution: { integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== }
+ dev: true
+
/aws4@1.12.0:
resolution: { integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== }
- dev: false
/axe-core@4.7.0:
resolution: { integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== }
@@ -9680,29 +9779,19 @@ packages:
engines: { node: '>=4' }
dev: false
- /axios@0.27.2:
- resolution: { integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== }
- dependencies:
- follow-redirects: 1.15.5
- form-data: 4.0.0
- transitivePeerDependencies:
- - debug
- dev: false
-
- /axios@1.6.2:
+ /axios@1.6.2(debug@4.3.4):
resolution: { integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A== }
dependencies:
- follow-redirects: 1.15.5
+ follow-redirects: 1.15.5(debug@4.3.4)
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
- dev: false
/axios@1.6.7:
resolution: { integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== }
dependencies:
- follow-redirects: 1.15.5
+ follow-redirects: 1.15.5(debug@4.3.4)
form-data: 4.0.0
proxy-from-env: 1.1.0
transitivePeerDependencies:
@@ -10519,6 +10608,12 @@ packages:
resolution: { integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== }
dev: true
+ /bcrypt-pbkdf@1.0.2:
+ resolution: { integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== }
+ dependencies:
+ tweetnacl: 0.14.5
+ dev: true
+
/before-after-hook@2.2.3:
resolution: { integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== }
dev: true
@@ -10589,6 +10684,10 @@ packages:
inherits: 2.0.4
readable-stream: 3.6.2
+ /blob-util@2.0.2:
+ resolution: { integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== }
+ dev: true
+
/bluebird@3.4.7:
resolution: { integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA== }
dev: false
@@ -10722,7 +10821,6 @@ packages:
/buffer-crc32@0.2.13:
resolution: { integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== }
- dev: false
/buffer-equal-constant-time@1.0.1:
resolution: { integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== }
@@ -10916,6 +11014,11 @@ packages:
responselike: 2.0.1
dev: true
+ /cachedir@2.4.0:
+ resolution: { integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ== }
+ engines: { node: '>=6' }
+ dev: true
+
/call-bind@1.0.7:
resolution: { integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== }
engines: { node: '>= 0.4' }
@@ -11008,6 +11111,10 @@ packages:
engines: { node: '>=4' }
dev: true
+ /caseless@0.12.0:
+ resolution: { integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== }
+ dev: true
+
/catharsis@0.9.0:
resolution: { integrity: sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A== }
engines: { node: '>= 10' }
@@ -11087,6 +11194,11 @@ packages:
resolution: { integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== }
dev: false
+ /check-more-types@2.24.0:
+ resolution: { integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== }
+ engines: { node: '>= 0.8.0' }
+ dev: true
+
/check-types@11.2.3:
resolution: { integrity: sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg== }
dev: true
@@ -11313,6 +11425,15 @@ packages:
engines: { node: '>=6' }
dev: true
+ /cli-table3@0.6.4:
+ resolution: { integrity: sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw== }
+ engines: { node: 10.* || >= 12.* }
+ dependencies:
+ string-width: 4.2.3
+ optionalDependencies:
+ '@colors/colors': 1.5.0
+ dev: true
+
/cli-table@0.3.11:
resolution: { integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ== }
engines: { node: '>= 0.2.0' }
@@ -11320,6 +11441,14 @@ packages:
colors: 1.0.3
dev: true
+ /cli-truncate@2.1.0:
+ resolution: { integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== }
+ engines: { node: '>=8' }
+ dependencies:
+ slice-ansi: 3.0.0
+ string-width: 4.2.3
+ dev: true
+
/cli-truncate@3.1.0:
resolution: { integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA== }
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
@@ -11612,6 +11741,11 @@ packages:
engines: { node: '>= 6' }
dev: true
+ /commander@6.2.1:
+ resolution: { integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== }
+ engines: { node: '>= 6' }
+ dev: true
+
/commander@7.1.0:
resolution: { integrity: sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== }
engines: { node: '>= 10' }
@@ -11829,6 +11963,10 @@ packages:
resolution: { integrity: sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw== }
dev: true
+ /core-util-is@1.0.2:
+ resolution: { integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== }
+ dev: true
+
/core-util-is@1.0.3:
resolution: { integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== }
@@ -12208,6 +12346,55 @@ packages:
find-pkg: 0.1.2
dev: false
+ /cypress@13.7.1:
+ resolution: { integrity: sha512-4u/rpFNxOFCoFX/Z5h+uwlkBO4mWzAjveURi3vqdSu56HPvVdyGTxGw4XKGWt399Y1JwIn9E1L9uMXQpc0o55w== }
+ engines: { node: ^16.0.0 || ^18.0.0 || >=20.0.0 }
+ hasBin: true
+ dependencies:
+ '@cypress/request': 3.0.1
+ '@cypress/xvfb': 1.2.4(supports-color@8.1.1)
+ '@types/sinonjs__fake-timers': 8.1.1
+ '@types/sizzle': 2.3.8
+ arch: 2.2.0
+ blob-util: 2.0.2
+ bluebird: 3.7.2
+ buffer: 5.7.1
+ cachedir: 2.4.0
+ chalk: 4.1.2
+ check-more-types: 2.24.0
+ cli-cursor: 3.1.0
+ cli-table3: 0.6.4
+ commander: 6.2.1
+ common-tags: 1.8.2
+ dayjs: 1.11.10
+ debug: 4.3.4(supports-color@8.1.1)
+ enquirer: 2.4.1
+ eventemitter2: 6.4.7
+ execa: 4.1.0
+ executable: 4.1.1
+ extract-zip: 2.0.1(supports-color@8.1.1)
+ figures: 3.2.0
+ fs-extra: 9.1.0
+ getos: 3.2.1
+ is-ci: 3.0.1
+ is-installed-globally: 0.4.0
+ lazy-ass: 1.6.0
+ listr2: 3.14.0(enquirer@2.4.1)
+ lodash: 4.17.21
+ log-symbols: 4.1.0
+ minimist: 1.2.8
+ ospath: 1.2.2
+ pretty-bytes: 5.6.0
+ process: 0.11.10
+ proxy-from-env: 1.0.0
+ request-progress: 3.0.0
+ semver: 7.6.0
+ supports-color: 8.1.1
+ tmp: 0.2.3
+ untildify: 4.0.0
+ yauzl: 2.10.0
+ dev: true
+
/d3-color@3.1.0:
resolution: { integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== }
engines: { node: '>=12' }
@@ -12299,6 +12486,13 @@ packages:
engines: { node: '>=8' }
dev: true
+ /dashdash@1.14.1:
+ resolution: { integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== }
+ engines: { node: '>=0.10' }
+ dependencies:
+ assert-plus: 1.0.0
+ dev: true
+
/data-store@0.16.1:
resolution: { integrity: sha512-tGbl4oVi9UPysie6y6+fuCjUNhaR3KxnuIRV0OMUCwq/wvikmWHXQYALbW/IVQvmxBNbrxUwjG5BWsrjx5v55w== }
engines: { node: '>=0.10.0' }
@@ -12364,7 +12558,6 @@ packages:
/dayjs@1.11.10:
resolution: { integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== }
- dev: false
/debug@2.6.9:
resolution: { integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== }
@@ -12387,6 +12580,18 @@ packages:
ms: 2.1.3
supports-color: 5.5.0
+ /debug@3.2.7(supports-color@8.1.1):
+ resolution: { integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== }
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.1.3
+ supports-color: 8.1.1
+ dev: true
+
/debug@4.3.4(supports-color@5.5.0):
resolution: { integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== }
engines: { node: '>=6.0' }
@@ -12942,6 +13147,13 @@ packages:
/eastasianwidth@0.2.0:
resolution: { integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== }
+ /ecc-jsbn@0.1.2:
+ resolution: { integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== }
+ dependencies:
+ jsbn: 0.1.1
+ safer-buffer: 2.1.2
+ dev: true
+
/ecdsa-sig-formatter@1.0.11:
resolution: { integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== }
dependencies:
@@ -13118,6 +13330,14 @@ packages:
tapable: 2.2.1
dev: true
+ /enquirer@2.4.1:
+ resolution: { integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== }
+ engines: { node: '>=8.6' }
+ dependencies:
+ ansi-colors: 4.1.3
+ strip-ansi: 6.0.1
+ dev: true
+
/entities@2.1.0:
resolution: { integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== }
dev: false
@@ -13884,6 +14104,10 @@ packages:
resolution: { integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== }
engines: { node: '>=6' }
+ /eventemitter2@6.4.7:
+ resolution: { integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg== }
+ dev: true
+
/eventemitter3@4.0.7:
resolution: { integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== }
@@ -13955,6 +14179,13 @@ packages:
strip-final-newline: 3.0.0
dev: true
+ /executable@4.1.1:
+ resolution: { integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== }
+ engines: { node: '>=4' }
+ dependencies:
+ pify: 2.3.0
+ dev: true
+
/exit-hook@1.1.1:
resolution: { integrity: sha512-MsG3prOVw1WtLXAZbM3KiYtooKR1LvxHh3VHsVtIy0uiUu8usxgB/94DP2HxtD/661lLdB6yzQ09lGJSQr6nkg== }
engines: { node: '>=0.10.0' }
@@ -14227,6 +14458,25 @@ packages:
- supports-color
dev: false
+ /extract-zip@2.0.1(supports-color@8.1.1):
+ resolution: { integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== }
+ engines: { node: '>= 10.17.0' }
+ hasBin: true
+ dependencies:
+ debug: 4.3.4(supports-color@8.1.1)
+ get-stream: 5.2.0
+ yauzl: 2.10.0
+ optionalDependencies:
+ '@types/yauzl': 2.10.3
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /extsprintf@1.3.0:
+ resolution: { integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== }
+ engines: { '0': node >=0.6.0 }
+ dev: true
+
/faiss-node@0.2.3:
resolution: { integrity: sha512-HfGhKFjyXPIyAlatcBNjv66q5ZQ43xfIpv8Uc17mIbEye7gbrmzVqAy+OxdlRy0usuLni+Dk1vSXf/z2yyr1Dg== }
engines: { node: '>= 14.0.0' }
@@ -14373,7 +14623,6 @@ packages:
resolution: { integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== }
dependencies:
pend: 1.2.0
- dev: false
/fecha@4.2.3:
resolution: { integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== }
@@ -14749,7 +14998,7 @@ packages:
resolution: { integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== }
dev: false
- /follow-redirects@1.15.5:
+ /follow-redirects@1.15.5(debug@4.3.4):
resolution: { integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== }
engines: { node: '>=4.0' }
peerDependencies:
@@ -14757,6 +15006,8 @@ packages:
peerDependenciesMeta:
debug:
optional: true
+ dependencies:
+ debug: 4.3.4(supports-color@5.5.0)
/for-each@0.3.3:
resolution: { integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== }
@@ -14794,6 +15045,10 @@ packages:
cross-spawn: 7.0.3
signal-exit: 4.1.0
+ /forever-agent@0.6.1:
+ resolution: { integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== }
+ dev: true
+
/fork-ts-checker-webpack-plugin@6.5.3(eslint@8.57.0)(typescript@4.9.5)(webpack@5.90.3):
resolution: { integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ== }
engines: { node: '>=10', yarn: '>=1.0.0' }
@@ -14830,6 +15085,15 @@ packages:
resolution: { integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A== }
dev: false
+ /form-data@2.3.3:
+ resolution: { integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== }
+ engines: { node: '>= 0.12' }
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ mime-types: 2.1.35
+ dev: true
+
/form-data@3.0.1:
resolution: { integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== }
engines: { node: '>= 6' }
@@ -14845,7 +15109,6 @@ packages:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
- dev: false
/format@0.2.2:
resolution: { integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== }
@@ -15236,6 +15499,18 @@ packages:
match-file: 0.2.2
dev: false
+ /getos@3.2.1:
+ resolution: { integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== }
+ dependencies:
+ async: 3.2.5
+ dev: true
+
+ /getpass@0.1.7:
+ resolution: { integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== }
+ dependencies:
+ assert-plus: 1.0.0
+ dev: true
+
/git-config-path@1.0.1:
resolution: { integrity: sha512-KcJ2dlrrP5DbBnYIZ2nlikALfRhKzNSX0stvv3ImJ+fvC4hXKoV+U+74SV0upg+jlQZbrtQzc0bu6/Zh+7aQbg== }
engines: { node: '>=0.10.0' }
@@ -15395,6 +15670,13 @@ packages:
minimatch: 5.1.6
once: 1.4.0
+ /global-dirs@3.0.1:
+ resolution: { integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== }
+ engines: { node: '>=10' }
+ dependencies:
+ ini: 2.0.0
+ dev: true
+
/global-modules@0.2.3:
resolution: { integrity: sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA== }
engines: { node: '>=0.10.0' }
@@ -16306,12 +16588,25 @@ packages:
engines: { node: '>=8.0.0' }
dependencies:
eventemitter3: 4.0.7
- follow-redirects: 1.15.5
+ follow-redirects: 1.15.5(debug@4.3.4)
requires-port: 1.0.0
transitivePeerDependencies:
- debug
dev: true
+ /http-signature@1.3.6:
+ resolution: { integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw== }
+ engines: { node: '>=0.10' }
+ dependencies:
+ assert-plus: 1.0.0
+ jsprim: 2.0.2
+ sshpk: 1.18.0
+ dev: true
+
+ /http-status-codes@2.3.0:
+ resolution: { integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA== }
+ dev: false
+
/http2-wrapper@1.0.3:
resolution: { integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== }
engines: { node: '>=10.19.0' }
@@ -16493,6 +16788,11 @@ packages:
/ini@1.3.8:
resolution: { integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== }
+ /ini@2.0.0:
+ resolution: { integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== }
+ engines: { node: '>=10' }
+ dev: true
+
/inline-style-parser@0.1.1:
resolution: { integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== }
dev: false
@@ -16720,6 +17020,13 @@ packages:
engines: { node: '>= 0.4' }
dev: true
+ /is-ci@3.0.1:
+ resolution: { integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== }
+ hasBin: true
+ dependencies:
+ ci-info: 3.9.0
+ dev: true
+
/is-core-module@2.13.1:
resolution: { integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== }
dependencies:
@@ -16854,6 +17161,14 @@ packages:
/is-hexadecimal@1.0.4:
resolution: { integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== }
+ /is-installed-globally@0.4.0:
+ resolution: { integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== }
+ engines: { node: '>=10' }
+ dependencies:
+ global-dirs: 3.0.1
+ is-path-inside: 3.0.3
+ dev: true
+
/is-interactive@1.0.0:
resolution: { integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== }
engines: { node: '>=8' }
@@ -17251,6 +17566,10 @@ packages:
- encoding
dev: false
+ /isstream@0.1.2:
+ resolution: { integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== }
+ dev: true
+
/istanbul-lib-coverage@3.2.2:
resolution: { integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== }
engines: { node: '>=8' }
@@ -17933,6 +18252,16 @@ packages:
engines: { node: '>= 0.6.0' }
dev: true
+ /joi@17.12.2:
+ resolution: { integrity: sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw== }
+ dependencies:
+ '@hapi/hoek': 9.3.0
+ '@hapi/topo': 5.1.0
+ '@sideway/address': 4.1.5
+ '@sideway/formula': 3.0.1
+ '@sideway/pinpoint': 2.0.0
+ dev: true
+
/js-base64@3.7.2:
resolution: { integrity: sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ== }
dev: false
@@ -17973,6 +18302,10 @@ packages:
xmlcreate: 2.0.4
dev: false
+ /jsbn@0.1.1:
+ resolution: { integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== }
+ dev: true
+
/jsbn@1.1.0:
resolution: { integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== }
@@ -18173,6 +18506,10 @@ packages:
resolution: { integrity: sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw== }
dev: true
+ /json-stringify-safe@5.0.1:
+ resolution: { integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== }
+ dev: true
+
/json5@0.5.1:
resolution: { integrity: sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw== }
hasBin: true
@@ -18226,6 +18563,16 @@ packages:
resolution: { integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== }
engines: { node: '>=0.10.0' }
+ /jsprim@2.0.2:
+ resolution: { integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ== }
+ engines: { '0': node >=0.6.0 }
+ dependencies:
+ assert-plus: 1.0.0
+ extsprintf: 1.3.0
+ json-schema: 0.4.0
+ verror: 1.10.0
+ dev: true
+
/jsx-ast-utils@3.3.5:
resolution: { integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== }
engines: { node: '>=4.0' }
@@ -18527,7 +18874,7 @@ packages:
'@supabase/supabase-js': 2.39.8
apify-client: 2.9.3
assemblyai: 4.3.2
- axios: 1.6.2
+ axios: 1.6.2(debug@4.3.4)
binary-extensions: 2.2.0
cheerio: 1.0.0-rc.12
chromadb: 1.8.1(@google/generative-ai@0.1.3)(cohere-ai@6.2.2)(openai@4.28.4)
@@ -18714,6 +19061,11 @@ packages:
lazy-cache: 1.0.4
dev: false
+ /lazy-ass@1.6.0:
+ resolution: { integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw== }
+ engines: { node: '> 0.8' }
+ dev: true
+
/lazy-cache@0.2.7:
resolution: { integrity: sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ== }
engines: { node: '>=0.10.0' }
@@ -18840,6 +19192,26 @@ packages:
- supports-color
dev: true
+ /listr2@3.14.0(enquirer@2.4.1):
+ resolution: { integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== }
+ engines: { node: '>=10.0.0' }
+ peerDependencies:
+ enquirer: '>= 2.3.0 < 3'
+ peerDependenciesMeta:
+ enquirer:
+ optional: true
+ dependencies:
+ cli-truncate: 2.1.0
+ colorette: 2.0.20
+ enquirer: 2.4.1
+ log-update: 4.0.0
+ p-map: 4.0.0
+ rfdc: 1.3.1
+ rxjs: 7.8.1
+ through: 2.3.8
+ wrap-ansi: 7.0.0
+ dev: true
+
/listr2@6.6.1:
resolution: { integrity: sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg== }
engines: { node: '>=16.0.0' }
@@ -19186,6 +19558,10 @@ packages:
/lodash.merge@4.6.2:
resolution: { integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== }
+ /lodash.once@4.1.1:
+ resolution: { integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== }
+ dev: true
+
/lodash.pairs@3.0.1:
resolution: { integrity: sha512-lgXvpU43ZNQrZ/pK2cR97YzKeAno3e3HhcyvLKsofljeHKrQcZhT1vW7fg4X61c92tM+mjD/DypoLZYuAKNIkQ== }
dependencies:
@@ -19246,6 +19622,16 @@ packages:
is-unicode-supported: 0.1.0
dev: true
+ /log-update@4.0.0:
+ resolution: { integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== }
+ engines: { node: '>=10' }
+ dependencies:
+ ansi-escapes: 4.3.2
+ cli-cursor: 3.1.0
+ slice-ansi: 4.0.0
+ wrap-ansi: 6.2.0
+ dev: true
+
/log-update@5.0.1:
resolution: { integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw== }
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
@@ -21707,6 +22093,10 @@ packages:
engines: { node: '>=0.10.0' }
dev: true
+ /ospath@1.2.2:
+ resolution: { integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA== }
+ dev: true
+
/ow@0.28.2:
resolution: { integrity: sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q== }
engines: { node: '>=12' }
@@ -22227,7 +22617,6 @@ packages:
/pend@1.2.0:
resolution: { integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== }
- dev: false
/perfect-scrollbar@1.5.5:
resolution: { integrity: sha512-dzalfutyP3e/FOpdlhVryN4AJ5XDVauVWxybSkLZmakFE2sS3y3pc4JnSprw8tGmHvkaG5Edr5T7LBTZ+WWU2g== }
@@ -23260,7 +23649,7 @@ packages:
resolution: { integrity: sha512-JB+ei0LkwE+rKHyW5z79Nd1jUaGxU6TvkfjFqY9vQaHxU5aU8dRl0UUaEmZdZbHwjp3WmXCBQQRNyimwbNQfCw== }
engines: { node: '>=15.0.0' }
dependencies:
- axios: 1.6.2
+ axios: 1.6.2(debug@4.3.4)
rusha: 0.8.14
transitivePeerDependencies:
- debug
@@ -23637,9 +24026,12 @@ packages:
- supports-color
dev: false
+ /proxy-from-env@1.0.0:
+ resolution: { integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A== }
+ dev: true
+
/proxy-from-env@1.1.0:
resolution: { integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== }
- dev: false
/ps-tree@1.2.0:
resolution: { integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA== }
@@ -23747,6 +24139,13 @@ packages:
engines: { node: '>=0.6.0', teleport: '>=0.2.0' }
dev: true
+ /qs@6.10.4:
+ resolution: { integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g== }
+ engines: { node: '>=0.6' }
+ dependencies:
+ side-channel: 1.0.6
+ dev: true
+
/qs@6.11.0:
resolution: { integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== }
engines: { node: '>=0.6' }
@@ -24933,6 +25332,12 @@ packages:
project-name: 0.2.6
dev: false
+ /request-progress@3.0.0:
+ resolution: { integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg== }
+ dependencies:
+ throttleit: 1.0.1
+ dev: true
+
/require-directory@2.1.1:
resolution: { integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== }
engines: { node: '>=0.10.0' }
@@ -25813,6 +26218,15 @@ packages:
resolution: { integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== }
engines: { node: '>=12' }
+ /slice-ansi@3.0.0:
+ resolution: { integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== }
+ engines: { node: '>=8' }
+ dependencies:
+ ansi-styles: 4.3.0
+ astral-regex: 2.0.0
+ is-fullwidth-code-point: 3.0.0
+ dev: true
+
/slice-ansi@4.0.0:
resolution: { integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== }
engines: { node: '>=10' }
@@ -26221,6 +26635,22 @@ packages:
hasBin: true
dev: false
+ /sshpk@1.18.0:
+ resolution: { integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== }
+ engines: { node: '>=0.10.0' }
+ hasBin: true
+ dependencies:
+ asn1: 0.2.6
+ assert-plus: 1.0.0
+ bcrypt-pbkdf: 1.0.2
+ dashdash: 1.14.1
+ ecc-jsbn: 0.1.2
+ getpass: 0.1.7
+ jsbn: 0.1.1
+ safer-buffer: 2.1.2
+ tweetnacl: 0.14.5
+ dev: true
+
/ssri@10.0.5:
resolution: { integrity: sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A== }
engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 }
@@ -26264,6 +26694,23 @@ packages:
resolution: { integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== }
dev: false
+ /start-server-and-test@2.0.3:
+ resolution: { integrity: sha512-QsVObjfjFZKJE6CS6bSKNwWZCKBG6975/jKRPPGFfFh+yOQglSeGXiNWjzgQNXdphcBI9nXbyso9tPfX4YAUhg== }
+ engines: { node: '>=16' }
+ hasBin: true
+ dependencies:
+ arg: 5.0.2
+ bluebird: 3.7.2
+ check-more-types: 2.24.0
+ debug: 4.3.4(supports-color@5.5.0)
+ execa: 5.1.1
+ lazy-ass: 1.6.0
+ ps-tree: 1.2.0
+ wait-on: 7.2.0(debug@4.3.4)
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
/static-eval@2.0.2:
resolution: { integrity: sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg== }
dependencies:
@@ -27022,6 +27469,10 @@ packages:
resolution: { integrity: sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ== }
dev: true
+ /throttleit@1.0.1:
+ resolution: { integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ== }
+ dev: true
+
/through2-filter@2.0.0:
resolution: { integrity: sha512-miwWajb1B80NvIVKXFPN/o7+vJc4jYUvnZCwvhicRAoTxdD9wbcjri70j+BenCrN/JXEPKDjhpw4iY7yiNsCGg== }
dependencies:
@@ -27101,7 +27552,6 @@ packages:
/tmp@0.2.3:
resolution: { integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== }
engines: { node: '>=14.14' }
- dev: false
/tmpl@1.0.5:
resolution: { integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== }
@@ -27446,6 +27896,10 @@ packages:
turbo-windows-arm64: 1.10.16
dev: true
+ /tweetnacl@0.14.5:
+ resolution: { integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== }
+ dev: true
+
/type-check@0.3.2:
resolution: { integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== }
engines: { node: '>= 0.8.0' }
@@ -28326,6 +28780,15 @@ packages:
resolution: { integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== }
engines: { node: '>= 0.8' }
+ /verror@1.10.0:
+ resolution: { integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== }
+ engines: { '0': node >=0.6.0 }
+ dependencies:
+ assert-plus: 1.0.0
+ core-util-is: 1.0.2
+ extsprintf: 1.3.0
+ dev: true
+
/vfile-location@5.0.2:
resolution: { integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg== }
dependencies:
@@ -28638,6 +29101,20 @@ packages:
xml-name-validator: 4.0.0
dev: false
+ /wait-on@7.2.0(debug@4.3.4):
+ resolution: { integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ== }
+ engines: { node: '>=12.0.0' }
+ hasBin: true
+ dependencies:
+ axios: 1.6.2(debug@4.3.4)
+ joi: 17.12.2
+ lodash: 4.17.21
+ minimist: 1.2.8
+ rxjs: 7.8.1
+ transitivePeerDependencies:
+ - debug
+ dev: true
+
/walk-up-path@1.0.0:
resolution: { integrity: sha512-hwj/qMDUEjCU5h0xr90KGCf0tg0/LgJbmOWgrWKYlcJZM7XvquvUJZ0G/HMGr7F7OQMOUuPHWP9JpriinkAlkg== }
dev: true
@@ -29693,7 +30170,6 @@ packages:
dependencies:
buffer-crc32: 0.2.13
fd-slicer: 1.1.0
- dev: false
/yeoman-environment@3.19.3:
resolution: { integrity: sha512-/+ODrTUHtlDPRH9qIC0JREH8+7nsRcjDl3Bxn2Xo/rvAaVvixH5275jHwg0C85g4QsF4P6M2ojfScPPAl+pLAg== }