Repository: FlowTestAI/FlowTest Branch: main Commit: 3b9ca3a49b89 Files: 217 Total size: 635.3 KB Directory structure: gitextract_hj9fjt_a/ ├── .changeset/ │ ├── README.md │ ├── config.json │ ├── early-colts-approve.md │ ├── honest-bags-fail.md │ └── proud-ants-flash.md ├── .eslintignore ├── .eslintrc.js ├── .github/ │ └── CODEOWNERS ├── .gitignore ├── .npmrc ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── jsconfig.json ├── package.json ├── packages/ │ ├── flowtest-cli/ │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── bin/ │ │ │ ├── axiosClient.js │ │ │ └── index.js │ │ ├── graph/ │ │ │ ├── Graph.js │ │ │ ├── GraphLogger.js │ │ │ ├── compute/ │ │ │ │ ├── assertnode.js │ │ │ │ ├── authnode.js │ │ │ │ ├── node.js │ │ │ │ ├── requestnode.js │ │ │ │ ├── setvarnode.js │ │ │ │ ├── utils.js │ │ │ │ └── utils.test.js │ │ │ └── constants/ │ │ │ ├── assertOperators.js │ │ │ └── evaluateOperators.js │ │ ├── package.json │ │ └── utils/ │ │ ├── flowparser/ │ │ │ ├── AssertNode.js │ │ │ ├── AuthNode.js │ │ │ ├── DelayNode.js │ │ │ ├── NestedFlowNode.js │ │ │ ├── Node.js │ │ │ ├── OutputNode.js │ │ │ ├── RequestNode.js │ │ │ ├── SetVarNode.js │ │ │ ├── StartNode.js │ │ │ └── parser.js │ │ └── readfile.js │ └── flowtest-electron/ │ ├── .npmrc │ ├── CHANGELOG.md │ ├── assets/ │ │ └── MyIcon.icns │ ├── electron-main.js │ ├── electron-menu.js │ ├── notarize.js │ ├── package.json │ ├── preload.js │ ├── src/ │ │ ├── ai/ │ │ │ ├── flowtestai.js │ │ │ └── models/ │ │ │ ├── bedrock_claude.js │ │ │ ├── gemini.js │ │ │ └── openai.js │ │ ├── app/ │ │ │ └── watcher.js │ │ ├── ipc/ │ │ │ ├── axiosClient.js │ │ │ ├── collection.js │ │ │ └── settings.js │ │ ├── store/ │ │ │ ├── collection.js │ │ │ └── settings.js │ │ └── utils/ │ │ ├── collection.js │ │ ├── collection.test.js │ │ ├── filemanager/ │ │ │ ├── createdirectory.js │ │ │ ├── createfile.js │ │ │ ├── deletedirectory.js │ │ │ ├── deletefile.js │ │ │ ├── filesystem.js │ │ │ ├── readfile.js │ │ │ └── updatefile.js │ │ ├── flowparser/ │ │ │ ├── AssertNode.js │ │ │ ├── AuthNode.js │ │ │ ├── DelayNode.js │ │ │ ├── NestedFlowNode.js │ │ │ ├── Node.js │ │ │ ├── OutputNode.js │ │ │ ├── RequestNode.js │ │ │ ├── SetVarNode.js │ │ │ ├── StartNode.js │ │ │ └── parser.js │ │ ├── generate-request-body.js │ │ └── generate-request-parameters.js │ └── tests/ │ ├── store/ │ │ ├── collection-store.test.js │ │ └── settings-store.test.js │ ├── test.yaml │ ├── utils/ │ │ ├── collection-parser.test.js │ │ ├── filemanager.test.js │ │ ├── flowtest-ai.test.js │ │ └── flowtest-parser.test.js │ └── watcher.test.js ├── pnpm-workspace.yaml ├── postcss.config.js ├── public/ │ ├── index.html │ ├── manifest.json │ └── robots.txt ├── src/ │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── components/ │ │ ├── atoms/ │ │ │ ├── EditableTextItem.js │ │ │ ├── Editor.js │ │ │ ├── Logo.js │ │ │ ├── SelectAuthKeys.js │ │ │ ├── SelectEnvironment.js │ │ │ ├── Tabs.js │ │ │ ├── ThemeController.js │ │ │ ├── common/ │ │ │ │ ├── Button.js │ │ │ │ ├── HomeLoadingScreen.js │ │ │ │ ├── HorizontalDivider.js │ │ │ │ ├── LoadingSpinner.js │ │ │ │ ├── NumberInput.js │ │ │ │ ├── TextEditor.js │ │ │ │ ├── TextInput.js │ │ │ │ ├── TextInputWithLabel.js │ │ │ │ └── TimeoutSelector.js │ │ │ ├── flow/ │ │ │ │ ├── FlowNode.js │ │ │ │ ├── NodeHorizontalDivider.js │ │ │ │ └── Textarea.js │ │ │ ├── sidebar/ │ │ │ │ ├── collections/ │ │ │ │ │ └── OptionsMenu.js │ │ │ │ └── environments/ │ │ │ │ └── EnvOptionsMenu.js │ │ │ └── util.js │ │ ├── layouts/ │ │ │ ├── SplitPane.js │ │ │ └── WithoutSplitPane.js │ │ ├── molecules/ │ │ │ ├── environment/ │ │ │ │ └── index.js │ │ │ ├── flow/ │ │ │ │ ├── AddNodes.js │ │ │ │ ├── constants/ │ │ │ │ │ ├── assertOperators.js │ │ │ │ │ ├── evaluateOperators.js │ │ │ │ │ └── requestNodes.js │ │ │ │ ├── edges/ │ │ │ │ │ └── ButtonEdge.js │ │ │ │ ├── flowtestai.js │ │ │ │ ├── graph/ │ │ │ │ │ ├── Graph.js │ │ │ │ │ ├── GraphLogger.js │ │ │ │ │ ├── GraphRun.js │ │ │ │ │ └── compute/ │ │ │ │ │ ├── assertnode.js │ │ │ │ │ ├── authnode.js │ │ │ │ │ ├── nestedflownode.js │ │ │ │ │ ├── node.js │ │ │ │ │ ├── requestnode.js │ │ │ │ │ ├── setvarnode.js │ │ │ │ │ ├── utils.js │ │ │ │ │ └── utils.test.js │ │ │ │ ├── index.js │ │ │ │ ├── nodes/ │ │ │ │ │ ├── AssertNode.js │ │ │ │ │ ├── AuthNode.js │ │ │ │ │ ├── DelayNode.js │ │ │ │ │ ├── FormDataSelector.js │ │ │ │ │ ├── NestedFlowNode.js │ │ │ │ │ ├── OutputNode.js │ │ │ │ │ ├── RequestBody.js │ │ │ │ │ ├── RequestNode.js │ │ │ │ │ └── SetVarNode.js │ │ │ │ └── utils.js │ │ │ ├── footers/ │ │ │ │ └── MainFooter.js │ │ │ ├── headers/ │ │ │ │ ├── MainHeader.js │ │ │ │ ├── SideBarHeader.js │ │ │ │ ├── SideBarSubHeader.js │ │ │ │ ├── TabPanelHeader.js │ │ │ │ └── WorkspaceHeader.js │ │ │ ├── modals/ │ │ │ │ ├── AddEnvVariableModal.js │ │ │ │ ├── ConfirmActionModal.js │ │ │ │ ├── EditEnvVariableModal.js │ │ │ │ ├── GenAIUsageDisclaimer.js │ │ │ │ ├── GenerateFlowTestModal.js │ │ │ │ ├── ImportCollectionModal.js │ │ │ │ ├── OpenCollectionModal.js │ │ │ │ ├── OutputNodeExpandedModal.js │ │ │ │ ├── SaveFlowModal.js │ │ │ │ ├── SettingsModal.js │ │ │ │ ├── create/ │ │ │ │ │ └── NewCollectionModal.js │ │ │ │ ├── flow/ │ │ │ │ │ ├── AddVariableModal.js │ │ │ │ │ └── NewFlowTestModal.js │ │ │ │ └── sidebar/ │ │ │ │ ├── NewEnvironmentFileModal.js │ │ │ │ └── NewLabelModal.js │ │ │ ├── sideSheets/ │ │ │ │ └── FlowLogs.js │ │ │ ├── sidebar/ │ │ │ │ ├── Empty.js │ │ │ │ └── content/ │ │ │ │ ├── Collection.js │ │ │ │ ├── Collections.js │ │ │ │ ├── Environment.js │ │ │ │ ├── Environments.js │ │ │ │ └── index.js │ │ │ └── workspace/ │ │ │ ├── EmptyWorkSpaceContent.js │ │ │ └── WorkspaceContent.js │ │ ├── organisms/ │ │ │ ├── AppNavBar.js │ │ │ ├── SideBar.js │ │ │ └── workspace/ │ │ │ └── Workspace.js │ │ └── pages/ │ │ └── Home.js │ ├── constants/ │ │ ├── AppNavBar.js │ │ ├── Common.js │ │ ├── ImportCollectionTypes.js │ │ ├── ModalNames.js │ │ ├── WorkspaceDirectory.js │ │ └── sidebar/ │ │ └── Environnments.js │ ├── index.css │ ├── index.js │ ├── ipc/ │ │ ├── collection.js │ │ └── settings.js │ ├── reportWebVitals.js │ ├── routes/ │ │ ├── Main.js │ │ └── index.js │ ├── service/ │ │ ├── collection.js │ │ └── settings.js │ ├── setupTests.js │ ├── stores/ │ │ ├── AppNavBarStore.js │ │ ├── CanvasStore.js │ │ ├── CollectionStore.js │ │ ├── CommonStore.js │ │ ├── EnvStore.js │ │ ├── EventListenerStore.js │ │ ├── SettingsStore.js │ │ ├── TabStore.js │ │ ├── collectionstore.test.js │ │ ├── eventstore.test.js │ │ ├── tabstore.test.js │ │ └── utils.js │ └── utils/ │ ├── common.js │ ├── useRenderCount.js │ └── useTelemetry.js └── tailwind.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/README.md ================================================ # Changesets Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", "changelog": ["@changesets/cli/changelog", { "repo": "FlowTestAI/FlowTest" }], "commit": false, "fixed": [], "linked": [], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": [], "changedFilePatterns": ["src/**/"] } ================================================ FILE: .changeset/early-colts-approve.md ================================================ --- 'flowtestai-app': minor --- Add support for google geminin function calling in generating flow ================================================ FILE: .changeset/honest-bags-fail.md ================================================ --- 'flowtestai-app': minor --- add support for bearer token auth type ================================================ FILE: .changeset/proud-ants-flash.md ================================================ --- 'flowtestai-app': minor --- allow request headers to be input by users ================================================ FILE: .eslintignore ================================================ # dependencies **/node_modules # production /build # intermal dependencies /server ================================================ FILE: .eslintrc.js ================================================ module.exports = { env: { browser: true, es2021: true, jest: true, }, extends: ['eslint:recommended', 'plugin:react/recommended', 'prettier'], overrides: [ { env: { node: true, }, files: ['.eslintrc.{js,cjs}'], parserOptions: { sourceType: 'script', }, }, ], parserOptions: { ecmaVersion: 'latest', sourceType: 'module', }, plugins: ['react', 'react-hooks', 'prettier'], rules: { 'react/prop-types': 'off', // keeping off for first phase 'no-unused-vars': 'off', // Getting a lot of Error for: '_' is assigned a value but never used no-unused-vars. For now disabling this because need to understand more about the use '_'. }, settings: { 'import/resolver': { node: { moduleDirectory: ['node_modules', './src/*'], extensions: ['.js', '.jsx', '.ts', '.tsx'], }, }, }, }; ================================================ FILE: .github/CODEOWNERS ================================================ # Lines starting with '#' are comments. # Each line is a file pattern followed by one or more owners. # More details are here: https://help.github.com/articles/about-codeowners/ # The '*' pattern is global owners. # Order is important. The last matching pattern has the most precedence. # The folders are ordered as follows: # In each subsection folders are ordered first by depth, then alphabetically. # This should make it easy to add new rules without breaking existing ones. # Global rule: * @jsajal ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies **/node_modules /.pnp .pnp.js # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ## build **/dist **/build # db .flowtest # temp upload directory uploads/ # env .env .env.example # Ide .vscode/ ================================================ FILE: .npmrc ================================================ node-linker=hoisted #pnpm requirement when working with electron ignore-workspace-root-check=true ================================================ FILE: .prettierrc ================================================ { "semi": true, "tabWidth": 2, "printWidth": 120, "singleQuote": true, "trailingComma": "all", "jsxSingleQuote": true, "bracketSpacing": true, "plugins": ["prettier-plugin-tailwindcss"] } ================================================ FILE: CONTRIBUTING.md ================================================ # Contribution Guidelines When contributing to `FlowTestAI`, whether on GitHub or in other community spaces: - Be respectful, civil, and open-minded. - Before opening a new pull request, try searching through the [issue tracker](https://github.com/FlowTestAI/FlowTest/issues) for known issues or fixes. - If you want to make code changes based on your personal opinion(s), make sure you open an issue first describing the changes you want to make, and open a pull request only when your suggestions get approved by maintainers. ## How to Contribute ### Prerequisites In order to not waste your time implementing a change that has already been declined, or is generally not needed, start by [opening an issue](https://github.com/FlowTestAI/FlowTest/issues/new/choose) describing the problem you would like to solve. ### Setup your environment locally _Some commands will assume you have the GitHub CLI installed, if you haven't, consider [installing it](https://github.com/cli/cli#installation), but you can always use the Web UI if you prefer that instead._ In order to contribute to this project, you will need to fork the repository: ```bash gh repo fork FlowTestAI/FlowTest ``` then, clone it to your local machine: ```bash gh repo clone /FlowTest ``` This project uses [pnpm](https://pnpm.io) as its package manager. Install it if you haven't already: ```bash npm install -g pnpm@9.0.6 ``` Then, install the project's dependencies: ```bash pnpm install ``` ### Implement your changes This project is a monorepo. FlowTestAI is offered as a local electron desktop app. In lieu of that it has two major components, the main logical part of the application resides in `packages/flowtest-electron` and the renderer (UI) part of the application resides in `src`. We are also actively developing the CLI and it resides in `packages/flowtest-cli` directory. Here are some useful scripts for when you are developing: | Command | Description | | --------------- | -------------------------------------------------- | | `pnpm start` | Builds and starts the FlowTest App on your desktop | | `pnpm build` | Builds the application for development use | | `pnpm format` | Formats the code | | `pnpm lint` | Lints the code | | `pnpm lint:fix` | Lints the code and fixes any errors | | `pnpm clean` | Deletes all node_modules in the project | ### When you're done Please make a manual, functional test of your changes. Check for formatting and linting errors: ```bash pnpm lint && pnpmt format ``` When making commits, make sure to follow the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) guidelines, i.e. prepending the message with `feat:`, `fix:`, `chore:`, `docs:`, etc... You can use `git status` to double check which files have not yet been staged for commit: ```bash git add && git commit -m "feat/fix/chore/docs: commit message" ``` If your change should appear in the changelog, i.e. it changes some behavior of either the CLI or the outputted elctron application, it must be captured by `changeset`. If this does not apply to your contribution skip to the pr creation step. Run the changeset: ```bash pnpm changeset ``` and filling out the form with the appropriate information. Then, add the generated changeset to git: ```bash git add .changeset/*.md && git commit -m "chore: add changeset" ``` When all that's done, it's time to file a pull request to upstream: ```bash gh pr create --web ``` and fill out the title and body appropriately. Again, make sure to follow the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) guidelines for your title. ## Credits This documented was inspired by the contributing guidelines for [t3-oss/create-t3-app](https://github.com/t3-oss/create-t3-app/blob/main/CONTRIBUTING.md). ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Sajal Jain Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # FlowTestAI: Streamlining End-to-End API Testing [![Release Notes](https://img.shields.io/github/release/FlowTestAI/FlowTest)](https://github.com/FlowTestAI/FlowTest/releases) [![Linkedin](https://img.shields.io/badge/LinkedIn-blue?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/company/flowtestai) [![Twitter Follow](https://img.shields.io/twitter/follow/FlowTestAI?style=social)](https://twitter.com/FlowTestAI) [![Chat on Discord](https://img.shields.io/badge/chat-Discord-7289DA?logo=discord)](https://discord.gg/Pf9tdSjPeF) 💡 We are proud to announce that we were recently featured in a [LangChain](https://blog.langchain.dev/empowering-development-with-flowtestai/) blog post. FlowTestAI is a powerful, code-agnostic tool designed to simplify the creation and execution of end-to-end API tests. With its intuitive interface and robust features, FlowTestAI empowers developers and QA teams to streamline their API testing process, improve collaboration, and gain valuable insights into their API performance. Screenshot 2024-04-18 at 5 41 43 PM ## 🚀 Key Features - **Low Code/No Code Solution**: Create complex end-to-end API tests without writing code. - **Natural Language Processing**: Describe your test scenarios in plain English. - **Support Leading LLMs**: Choose from a wide range of leading LLMs: OpenAI, AWS Bedrock, Google Gemini etc. - **Drag-and-Drop Interface**: Visually design your API tests with ease. - **OpenAPI Spec Integration**: Automatically parse and pre-fill request nodes from your OpenAPI specifications. - **Cross-Platform Compatibility**: Available as an Desktop application for Mac, Windows, and Linux. - **Local File System Integration**: Direct interaction with local file system for enhanced privacy and control. - **Version Control Ready**: Easily collaborate using Git or any other VCS. - **CI/CD Ready**: Run tests in CI pipelines with our CLI tool. - **Advanced Analytics**: Gain insights into API performance and test results. ## 🛠️ Getting Started ### Desktop App Installation 1. Download FlowTestAI for your OS from our [releases page](https://github.com/FlowTestAI/FlowTest/releases). 2. Install and launch FlowTestAI like any other desktop application. 3. Start creating end-to-end API tests using natural language or drag-and-drop. 4. Save your work locally and use Git for version control, just like with traditional IDEs. ### CLI Installation (for CI/CD) ```bash npm install -g flowtestai ``` https://www.npmjs.com/package/flowtestai The CLI allows you to run flows created using FlowTestAI from command line interface making it easier to automate and run them in a CI/CD (continuous integration/development) fashion. [README](https://github.com/FlowTestAI/FlowTest/blob/main/packages/flowtest-cli/README.md) ### Analytics Setup (Optional) 1. Visit https://www.useflowtest.ai/ 2. Go to Products -> Analytics -> Get Access Key Pairs 3. For CLI: Export key pairs as environment variables 4. For IDE: Open Settings and paste the access key pairs 5. Now start publishing scans for each test run. ## 📚 Documentation https://flowtestai.gitbook.io/flowtestai ## Setup ## 💻 Production FlowTestAI is an electron app that runs entirely in your local environment interacting with your local file system just like other IDE(s) out there like VSCode, Intellij etc. The platform-specific binaries are available for download from our GitHub releases. We currently offer [binaries for macOS](https://github.com/FlowTestAI/FlowTest/releases), with versions for Windows and Linux under development 🚧. If you require a binary for a specific platform, please let us know in the Discussions section. We will prioritize your request accordingly. ## 🔧 Development ### Prerequisite This package uses version >= 18 of Node.js. There are different ways that you can install Node.js, following are steps for [Node Verson Manager or NVM](https://github.com/nvm-sh/nvm). If you need steps for other methods than NVM then please check [Official Node.js documentation](https://nodejs.org/en/download/package-manager). Here is a sample walkthrough installing version 18. 1. Installs nvm (Node Version Manager) ```bash curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash ``` 2. Download and install Node.js ```bash nvm install 18 ``` 3. Verifies the right Node.js version is in the environment ```bash node -v # should print `v18.20.2` ``` 4. Verifies the right NPM version is in the environment ```bash npm -v # should print `10.5.0` ``` ### Main setup 1. Clone the repository ```bash git clone https://github.com/FlowTestAI/FlowTest.git ``` 2. Go into repository folder ```bash cd FlowTest ``` 3. This project uses pnpm. Use [corepack](https://github.com/nodejs/corepack) to enable the required pnpm version: ```bash corepack enable pnpm ``` or install with npm ```bash npm install -g pnpm@9.0.6 ``` 4. Install all project dependencies: ```bash pnpm install ``` 5. Build and start the app: ```bash pnpm start ``` The app should start as a normal desktop app NOTE: if you use npm and corepack to install pnpm you will have two instances of pnpm. Make sure the version you're using is the correct version for the repo. Check the [pnpm docs](https://pnpm.io/installation) and [corepack](https://github.com/nodejs/corepack) for troubleshooting. Pnpm installed with npm will overrun corepacks pnpm instance. ## 🤝 Contribution _"Little drops of water make a mighty ocean"_ No contribution is small even if it means fixing a spelling mistake. Follow our contributing guide below. https://github.com/FlowTestAI/FlowTest/blob/main/CONTRIBUTING.md Fun fact: our contributing guide itself was an external contribution 🍺 ## 🌟 Support - ❓ QNA: feel free to ask questions, request new features or start a constructive discussion here [discussion](https://github.com/FlowTestAI/FlowTest/discussions) - 🐛 Issues: Feel free to raise issues here [issues](https://github.com/FlowTestAI/FlowTest/issues) (contributing guidelines coming soon..) - 🔄 Integration: If you want to explore how you can use this tool in your day to day activities or integrate with your existing stack or in general want to chat, you can reach out to us at any of our [social media handles](https://flowtestai.gitbook.io/flowtestai) or email me at jsajal1993@gmail.com. - 🔐 Our tool integrates with various leading Large Lanugage Models (LLMs) if you wish to use the natural language to flow translation feature. You can request their api keys: - [OpenAI](https://platform.openai.com/) - [AWS Bedrock](https://console.aws.amazon.com/bedrock/) - [Google GEMINI](https://ai.google.dev/gemini-api/docs/api-key) - [Local AI] (Coming Soon...) ## 📜 License Source code in this repository is made available under the [MIT License](LICENSE). ## Connect with Us - Website: [useflowtest.ai](https://www.useflowtest.ai/) - Email: jsajal1993@gmail.com ================================================ FILE: jsconfig.json ================================================ { "compilerOptions": { "baseUrl": "src", "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ] }, "include": [ "src" ] } ================================================ FILE: package.json ================================================ { "name": "flowtest", "version": "1.0.0", "private": true, "homepage": ".", "packageManager": "pnpm@9.0.6", "engines": { "node": ">=18.17.0" }, "scripts": { "start": "pnpm run build && cd packages/flowtest-electron && pnpm start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", "lint": "eslint . --ext .js,.json,.cjs", "lint:fix": "eslint --fix . --ext .js,.json,.cjs", "format": "prettier --write '**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc", "clean": "rm -rf node_modules/ && rm -rf packages/flowtest-electron/node_modules/" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "dependencies": { "@codemirror/commands": "^6.5.0", "@codemirror/lang-json": "^6.0.1", "@codemirror/language": "^6.10.1", "@codemirror/state": "^6.4.1", "@codemirror/view": "^6.26.3", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@headlessui/react": "^1.7.18", "@heroicons/react": "^2.1.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@tippyjs/react": "^4.2.6", "allotment": "^1.20.0", "autoprefixer": "^10.4.18", "axios": "^1.5.1", "codemirror": "^6.0.1", "date-fns": "^3.6.0", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.29.1", "form-data": "^4.0.0", "immer": "^10.0.4", "lodash": "^4.17.21", "mousetrap": "^1.6.5", "notistack": "^3.0.1", "postcss": "^8.4.35", "posthog-node": "^4.0.1", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-custom-scrollbars": "^4.2.1", "react-dom": "^18.2.0", "react-edit-text": "^5.1.1", "react-icons": "^5.0.1", "react-json-view-lite": "^1.4.0", "react-perfect-scrollbar": "^1.5.8", "react-router": "^6.15.0", "react-router-dom": "^6.22.2", "react-scripts": "5.0.1", "react-sliding-pane": "^7.3.0", "react-toastify": "^10.0.5", "react-tooltip": "^5.26.2", "reactflow": "^11.8.3", "socket.io-client": "^4.7.4", "tailwindcss": "^3.4.1", "typescript": "4", "web-vitals": "^2.1.4", "zustand": "^4.5.2" }, "devDependencies": { "@changesets/cli": "^2.27.1", "@tailwindcss/typography": "^0.5.10", "@types/node": "20.11.5", "daisyui": "^4.7.2", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import-alias": "^1.2.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "prettier": "^3.2.5", "prettier-plugin-tailwindcss": "^0.5.11" } } ================================================ FILE: packages/flowtest-cli/LICENSE.md ================================================ MIT License Copyright (c) 2023 Sajal Jain Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/flowtest-cli/README.md ================================================ # flowtestai-cli With FlowTestAI CLI, you can now run your end to end flows, constructed using FlowTestAI, directly from command line. This makes it easier to run your tests in different environments, automate your testing process, and integrate your tests with your continuous integration and deployment workflows. ## Installation To install the FlowTestAI CLI, use the node package manager of your choice, such as NPM: ```bash npm install -g flowtestai ``` ## Getting started Navigate to the root directory of your collection, and then run: ```bash flow run help ``` This command will give you various options you can use to run a flow. You can also run a single flow by specifying its filename with the `--file` or `-f` option: ```bash flow run -f test.flow ``` Or run a requests inside a subfolder: ```bash flow run -f folder/subfolder/test.flow ``` If you need to use an environment, you can specify it with the `--env` or `-e` option: ```bash flow run -f test.flow -e environments/test.env ``` If you need to publish the results of your flow runs for further analysis, you can specify the `-s` option. Request your access key pairs from https://www.useflowtest.ai/ and then run export $FLOWTEST_ACCESS_ID and $FLOWTEST_ACCESS_KEY before publishing: ```bash flow run -f test.flow -e environments/test.env -s ``` ## Demo ![demo1](assets/demo1.png) ![demo2](assets/demo2.png) ## Support If you encounter any issues or have any feedback or suggestions, please raise them on our [GitHub repository](https://github.com/FlowTestAI/FlowTest) Thank you for using FlowTestAI CLI! ## Changelog See [https://github.com/FlowTestAI/FlowTest/releases](https://github.com/FlowTestAI/FlowTest/releases) ## License [MIT](LICENSE.md) ================================================ FILE: packages/flowtest-cli/bin/axiosClient.js ================================================ // lib/axiosClient.ts const axios = require('axios'); const axiosRetry = require('axios-retry').default; const baseUrl = 'https://www.useflowtest.ai'; const axiosClient = axios.create({ baseURL: `${baseUrl}/api`, headers: { 'Content-Type': 'application/json', }, }); axiosRetry(axiosClient, { retries: 3, // Number of retries retryDelay: (retryCount) => { return retryCount * 1000; // Time interval between retries (1000 ms = 1 second) }, retryCondition: (error) => { // Retry on network errors or rate limit errors or 5xx server errors return error.response?.status === 500 || error.response?.status === 429 || error.code === 'ECONNABORTED'; }, }); module.exports = { baseUrl, axiosClient, }; ================================================ FILE: packages/flowtest-cli/bin/index.js ================================================ #!/usr/bin/env node const yargs = require('yargs/yargs'); const { hideBin } = require('yargs/helpers'); const chalk = require('chalk'); const readFile = require('../utils/readfile'); const { serialize } = require('../utils/flowparser/parser'); const { Graph } = require('../graph/Graph'); const { cloneDeep } = require('lodash'); const dotenv = require('dotenv'); const { GraphLogger, LogLevel } = require('../graph/GraphLogger'); const { baseUrl, axiosClient } = require('./axiosClient'); require('dotenv').config(); const getEnvVariables = (pathname) => { const content = readFile(pathname); const buf = Buffer.from(content); const parsed = dotenv.parse(buf); return parsed; }; function bytesToBase64(bytes) { const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join(''); return btoa(binString); } // Define the CLI application using yargs const argv = yargs(hideBin(process.argv)) .usage('Usage: $0 [options]') .command( 'run', 'Run a flow', (yargs) => { return yargs .option('file', { alias: 'f', describe: 'path of the flow to run', demandOption: true, type: 'string', }) .option('env', { alias: 'e', describe: 'path of the environment file', demandOption: false, type: 'string', }) .option('timeout', { alias: 't', describe: 'timeout for flow run in ms', demandOption: false, type: 'number', }) .option('scan', { alias: 's', describe: 'generate and upload flow scan', }); }, async (argv) => { if (argv.file.toLowerCase().endsWith(`.flow`)) { let content = undefined; try { content = readFile(argv.file); } catch (error) { console.error(chalk.red(`${error}`)); process.exit(1); } try { const flowData = serialize(JSON.parse(content)); // output json output to a file const logger = new GraphLogger(); const startTime = Date.now(); const g = new Graph( cloneDeep(flowData.nodes), cloneDeep(flowData.edges), startTime, argv.timeout ? argv.timeout : 60000, argv.env ? getEnvVariables(argv.env) : {}, logger, ); console.log(chalk.blue('Running Flow \n')); console.log( chalk.yellow( 'Right now CLI commands must be run from root directory of collection. We will gradually add support to run commands from anywhere inside the collection. \n', ), ); const result = await g.run(); console.log('\n'); if (result.status === 'Success') { console.log(chalk.bold('Flow Run: ') + chalk.green(` ✓ `) + chalk.dim(result.status)); } else { console.log(chalk.bold('Flow Run: ') + chalk.red(` ✕ `) + chalk.dim(result.status)); } const time = Date.now() - startTime; logger.add(LogLevel.INFO, `Total time: ${time} ms`); console.log(chalk.bold('Total Time: ') + chalk.dim(`${time} ms`)); //console.log(logger.get()); if (argv.scan) { const data = { scan_metadata: { version: 1, name: argv.file.toString(), status: result.status, time, }, scan: bytesToBase64(new TextEncoder().encode(JSON.stringify(logger.get()))), }; const accessId = process.env.FLOWTEST_ACCESS_ID; const accessKey = process.env.FLOWTEST_ACCESS_KEY; if (!accessId || accessId.trim() === '' || !accessKey || accessKey.trim() === '') { console.log(chalk.red(` ✕ `) + chalk.dim('Unable to upload flow scan')); console.log( chalk.yellow(`Failed to detect access key pairs. Make sure to set environment variables properly.`), ); console.log(chalk.yellow(` export FLOWTEST_ACCESS_ID="<>"`)); console.log(chalk.yellow(` export FLOWTEST_ACCESS_KEY="<>"`)); } else { try { const response = await axiosClient.post('/upload', data, { headers: { 'Content-Type': 'application/json', 'x-access-id': accessId, 'x-access-key': accessKey, }, }); console.log(chalk.bold('Flow Scan: ') + chalk.dim(`${baseUrl}/scan/${response.data.data[0].id}`)); } catch (error) { if (error?.response) { if (error.response?.status >= 400 && error.response?.status < 500) { console.log(chalk.red(` ${JSON.stringify(error.response?.data)}`)); } if (error.response?.status === 500) { console.log(chalk.red(' Internal Server Error')); } } console.log(chalk.red(` ✕ `) + chalk.dim('Unable to upload flow scan')); } } } else { console.log('\n'); console.log( chalk.yellow( 'Enable flow scans today to get more value our of your APIs. Get your access key pairs at https://www.useflowtest.ai/ \n', ), ); } process.exit(1); //console.log(chalk.green(JSON.stringify(result))); } catch (error) { console.error(chalk.red(`Internal error running flow`)); process.exit(1); } } else { console.error(chalk.red('Input file is not a flow file')); process.exit(1); } }, ) .help() .alias('help', 'h') .parse(); ================================================ FILE: packages/flowtest-cli/graph/Graph.js ================================================ // assumption is that apis are giving json as output const { cloneDeep } = require('lodash'); const authNode = require('./compute/authnode'); const assertNode = require('./compute/assertnode'); const requestNode = require('./compute/requestNode'); const setVarNode = require('./compute/setvarnode'); const chalk = require('chalk'); const path = require('path'); const Node = require('./compute/node'); const { LogLevel } = require('./GraphLogger'); const readFile = require('../utils/readfile'); const { serialize } = require('../utils/flowparser/parser'); class nestedFlowNode extends Node { constructor(nodes, edges, startTime, timeout, initialEnvVars, logger) { super('flowNode'); try { this.internalGraph = new Graph(nodes, edges, startTime, timeout, initialEnvVars, logger); } catch (error) { console.log(error); } } async evaluate() { return this.internalGraph.run(); } } class Graph { constructor(nodes, edges, startTime, timeout, initialEnvVars, logger) { this.nodes = nodes; this.edges = edges; this.timeout = timeout; this.startTime = startTime; this.graphRunNodeOutput = {}; this.auth = undefined; this.envVariables = initialEnvVars; this.logger = logger; } #checkTimeout() { return Date.now() - this.startTime > this.timeout; } #computeConnectingEdge(node, result) { let connectingEdge = undefined; if (node.type === 'assertNode') { if (result.output === true) { connectingEdge = this.edges.find((edge) => edge.sourceHandle == 'true' && edge.source === node.id); } else { connectingEdge = this.edges.find((edge) => edge.sourceHandle == 'false' && edge.source === node.id); } } else { if (result.status === 'Success') { connectingEdge = this.edges.find((edge) => edge.source === node.id); } } return connectingEdge; } #computeDataFromPreviousNodes(node) { var prevNodesData = {}; // a request node is allowed multiple incoming edges this.edges.forEach((edge) => { if (edge.target === node.id) { if (this.graphRunNodeOutput[edge.source]) { prevNodesData = { ...prevNodesData, ...this.graphRunNodeOutput[edge.source], }; } } }); return prevNodesData; } async #computeNode(node) { let result = undefined; const prevNodeOutputData = this.#computeDataFromPreviousNodes(node); try { if (node.type === 'outputNode') { console.log('Output Node'); console.log(chalk.green(` ✓ `) + chalk.dim(`${JSON.stringify(prevNodeOutputData)}`)); this.logger.add(LogLevel.INFO, '', { type: 'outputNode', data: { output: prevNodeOutputData } }); result = { status: 'Success', data: prevNodeOutputData, }; } if (node.type === 'assertNode') { const eNode = new assertNode( node.data.operator, node.data.variables, prevNodeOutputData, this.envVariables, this.logger, ); if (eNode.evaluate()) { console.log(chalk.green(` ✓ `) + chalk.dim('True')); result = { status: 'Success', data: prevNodeOutputData, output: true, }; } else { console.log(chalk.red(` ✕ `) + chalk.dim('False')); result = { status: 'Success', data: prevNodeOutputData, output: false, }; } } if (node.type === 'delayNode') { const delay = node.data.delay; const wait = (ms) => { return new Promise((resolve) => setTimeout(resolve, Math.min(ms, this.timeout))); }; console.log('Delay Node: ' + chalk.green(`....waiting for: ${delay} ms`)); await wait(delay); this.logger.add(LogLevel.INFO, '', { type: 'delayNode', data: { delay } }); result = { status: 'Success', data: prevNodeOutputData, }; } if (node.type === 'authNode') { console.log('Authentication Node'); const aNode = new authNode(node.data, this.envVariables, this.logger); this.auth = node.data.type ? aNode.evaluate() : undefined; result = { status: 'Success', data: prevNodeOutputData, }; } if (node.type === 'requestNode') { console.log('Request Node'); const rNode = new requestNode(node.data, prevNodeOutputData, this.envVariables, this.auth, this.logger); result = await rNode.evaluate(); // add post response variables if any if (result.postRespVars) { this.envVariables = { ...this.envVariables, ...result.postRespVars, }; } } if (node.type === 'flowNode') { console.log('Flow Node (Nested graph)'); const content = readFile(path.join(process.cwd(), node.data.relativePath)); const flowData = serialize(JSON.parse(content)); if (flowData) { const cNode = new nestedFlowNode( cloneDeep(flowData.nodes), cloneDeep(flowData.edges), this.startTime, this.timeout, this.envVariables, this.logger, ); result = await cNode.evaluate(); this.envVariables = result.envVars; } else { result = { status: 'Success', data: prevNodeOutputData, }; } } if (node.type === 'setVarNode') { console.log('Set Variable Node'); const sNode = new setVarNode(node.data, prevNodeOutputData, this.envVariables); const newVariable = sNode.evaluate(); if (newVariable != undefined) { console.log(chalk.green(` ✓ `) + chalk.dim(`Set variable: ${JSON.stringify(newVariable)}`)); this.logger.add(LogLevel.INFO, '', { type: 'setVarNode', data: { name: Object.keys(newVariable)[0], value: newVariable[Object.keys(newVariable)[0]], }, }); this.envVariables = { ...this.envVariables, ...newVariable, }; } result = { status: 'Success', data: prevNodeOutputData, }; } if (this.#checkTimeout()) { throw Error(`Timeout of ${this.timeout} ms exceeded, stopping graph run`); } } catch (err) { console.log(chalk.red(`Flow failed at: ${JSON.stringify(node.data)} due to ${err}`)); this.logger.add(LogLevel.ERROR, `Flow failed due to ${err}`, { type: 'errorNode', data: node.data, }); return { status: 'Failed', }; } if (result === undefined) { console.log(chalk.red(`Flow failed due to failure to evaluate result at node: ${node.data}`)); this.logger.add(LogLevel.ERROR, 'Flow failed due to failure to evaluate result', { type: 'errorNode', data: node.data, }); return { status: 'Failed', }; } else { const connectingEdge = this.#computeConnectingEdge(node, result); if (connectingEdge != undefined) { const nextNode = this.nodes.find( (node) => ['requestNode', 'outputNode', 'assertNode', 'delayNode', 'authNode', 'flowNode', 'setVarNode'].includes( node.type, ) && node.id === connectingEdge.target, ); this.graphRunNodeOutput[node.id] = result.data ? result.data : {}; return this.#computeNode(nextNode); } else { return result; } } } async run() { this.graphRunNodeOutput = {}; console.log(chalk.green('Start Flowtest')); this.logger.add(LogLevel.INFO, 'Start Flowtest'); const startNode = this.nodes.find((node) => node.type === 'startNode'); if (startNode == undefined) { console.log(chalk.red(`✕ `) + chalk.red('No start node found')); console.log(chalk.red('End Flowtest')); this.logger.add(LogLevel.INFO, 'No start node found'); this.logger.add(LogLevel.INFO, 'End Flowtest'); return { status: 'Success', envVars: this.envVariables, }; } const connectingEdge = this.edges.find((edge) => edge.source === startNode.id); // only start computing graph if initial node has the connecting edge if (connectingEdge != undefined) { const firstNode = this.nodes.find((node) => node.id === connectingEdge.target); const result = await this.#computeNode(firstNode); if (result.status == 'Failed') { console.log(chalk.red('End Flowtest')); } else { console.log(chalk.green('End Flowtest')); } this.logger.add(LogLevel.INFO, 'End Flowtest'); return { status: result.status, envVars: this.envVariables, }; } else { console.log(chalk.green('End Flowtest')); this.logger.add(LogLevel.INFO, 'End Flowtest'); return { status: 'Success', envVars: this.envVariables, }; } } } module.exports = { Graph }; ================================================ FILE: packages/flowtest-cli/graph/GraphLogger.js ================================================ const LogLevel = Object.freeze({ INFO: 'info', WARN: 'warn', ERROR: 'error', }); class GraphLogger { constructor() { this.logs = []; } add(logLevel, message, node) { this.logs.push({ level: logLevel, timestamp: new Date().toISOString(), message, node, }); } get() { return this.logs; } } module.exports = { GraphLogger, LogLevel, }; ================================================ FILE: packages/flowtest-cli/graph/compute/assertnode.js ================================================ const AssertOperators = require('../constants/assertOperators'); const { computeNodeVariable } = require('./utils'); const Node = require('./node'); const chalk = require('chalk'); const { LogLevel } = require('../GraphLogger'); class assertNode extends Node { constructor(operator, variables, prevNodeOutputData, envVariables, logger) { super('assertNode'); this.operator = operator; this.variables = variables; this.prevNodeOutputData = prevNodeOutputData; this.logger = logger; this.envVariables = envVariables; } getVariableValue(variable) { if (variable.type.toLowerCase() === 'variable') { if (Object.prototype.hasOwnProperty.call(this.envVariables, variable.value)) { return this.envVariables[variable.value]; } else { throw Error(`Cannot find value of variable ${variable.value}`); } } else { return computeNodeVariable(variable, this.prevNodeOutputData); } } evaluate() { //console.log('Evaluating an assert node'); const var1 = this.getVariableValue(this.variables.var1); const var2 = this.getVariableValue(this.variables.var2); const operator = this.operator; if (operator == undefined) { throw Error('Operator undefined'); } // this.logs.push( // `Assert var1: ${JSON.stringify(var1)} of type: ${typeof var1}, var2: ${JSON.stringify(var2)} of type: ${typeof var2} with operator: ${operator}`, // ); console.log( 'Assert Node: ' + chalk.green( `Assert var1: ${JSON.stringify(var1)} of type: ${typeof var1}, var2: ${JSON.stringify(var2)} of type: ${typeof var2} with operator: ${operator}`, ), ); let result; switch (operator) { case AssertOperators.isEqualTo: result = var1 === var2; break; case AssertOperators.isNotEqualTo: result = var1 != var2; break; case AssertOperators.isGreaterThan: result = var1 > var2; break; case AssertOperators.isLessThan: result = var1 < var2; break; default: throw Error('Unsupported operator'); } this.logger.add(LogLevel.INFO, '', { type: 'assertNode', data: { var1, var2, operator, result } }); return result; } } module.exports = assertNode; ================================================ FILE: packages/flowtest-cli/graph/compute/authnode.js ================================================ const { computeVariables } = require('./utils'); const Node = require('./node'); const chalk = require('chalk'); const { LogLevel } = require('../GraphLogger'); class authNode extends Node { constructor(nodeData, envVariables, logger) { super('authNode'); (this.nodeData = nodeData), (this.envVariables = envVariables); this.logger = logger; } evaluate() { //console.log('Evaluating an auth node'); if (this.nodeData.type === 'basic-auth') { console.log(chalk.green(` ✓ `) + chalk.dim('.....setting basic authentication')); this.logger.add(LogLevel.INFO, '', { type: 'authNode', data: { authType: 'Basic Authentication' } }); const username = computeVariables(this.nodeData.username, this.envVariables); const password = computeVariables(this.nodeData.password, this.envVariables); return { type: 'basic-auth', username, password, }; } else if (this.nodeData.type === 'bearer-token') { this.logger.add(LogLevel.INFO, '', { type: 'authNode', data: { authType: 'Bearer Token' } }); const token = computeVariables(this.nodeData.token, this.envVariables); return { type: 'bearer-token', token, }; } else if (this.nodeData.type === 'no-auth') { console.log(chalk.green(` ✓ `) + chalk.dim('.....using no authentication')); this.logger.add(LogLevel.INFO, '', { type: 'authNode', data: { authType: 'No Authentication' } }); return { type: 'no-auth', }; } else { throw Error(`auth type: ${this.nodeData.type} is not valid`); } } } module.exports = authNode; ================================================ FILE: packages/flowtest-cli/graph/compute/node.js ================================================ class Node { constructor(type) { this.type = type; } evaluate() { throw new Error('Evaluate method must be implemented by subclasses'); } } module.exports = Node; ================================================ FILE: packages/flowtest-cli/graph/compute/requestnode.js ================================================ const { computeNodeVariables, computeVariables } = require('./utils'); const Node = require('./node'); const axios = require('axios'); const chalk = require('chalk'); const { LogLevel } = require('../GraphLogger'); const FormData = require('form-data'); const { extend, cloneDeep } = require('lodash'); const fs = require('fs'); const path = require('path'); const newAbortSignal = () => { const abortController = new AbortController(); setTimeout(() => abortController.abort(), 60000 || 0); return abortController.signal; }; /** web platform: blob. */ const convertBase64ToBlob = async (base64) => { const response = await fetch(base64); const blob = await response.blob(); return blob; }; class requestNode extends Node { constructor(nodeData, prevNodeOutputData, envVariables, auth, logger) { super('requestNode'); this.nodeData = nodeData; this.prevNodeOutputData = prevNodeOutputData; this.envVariables = envVariables; this.auth = auth; this.logger = logger; } async evaluate() { // step1 evaluate pre request variables of this node const evalVariables = computeNodeVariables(this.nodeData.preReqVars, this.prevNodeOutputData); const variablesDict = { ...this.envVariables, ...evalVariables, }; // step2 replace variables in url with value const finalUrl = computeVariables(this.nodeData.url, variablesDict); // step 3 const rawRequest = this.formulateRequest(finalUrl, variablesDict); console.log(chalk.green(` ✓ `) + chalk.dim(`type = ${this.nodeData.requestType.toUpperCase()}`)); console.log(chalk.green(` ✓ `) + chalk.dim(`url = ${finalUrl}`)); const { request, response } = await this.runHttpRequest(rawRequest); if (response.error) { console.log(chalk.red(` ✕ `) + chalk.dim(`Request failed: ${JSON.stringify(response.error)}`)); this.logger.add(LogLevel.ERROR, 'HTTP request failed', { type: 'requestNode', data: { request, response: response.error, preReqVars: evalVariables, }, }); return { status: 'Failed', }; } else { console.log(chalk.green(` ✓ `) + chalk.dim(`Request successful: ${JSON.stringify(response)}`)); if (this.nodeData.postRespVars) { const evalPostRespVars = computeNodeVariables(this.nodeData.postRespVars, response.data); this.logger.add(LogLevel.INFO, 'HTTP request success', { type: 'requestNode', data: { request, response, preReqVars: evalVariables, postRespVars: evalPostRespVars, }, }); return { status: 'Success', data: response.data, postRespVars: evalPostRespVars, }; } this.logger.add(LogLevel.INFO, 'HTTP request success', { type: 'requestNode', data: { request, response, preReqVars: evalVariables, }, }); return { status: 'Success', data: response.data, }; } } formulateRequest(finalUrl, variablesDict) { let restMethod = this.nodeData.requestType.toLowerCase(); let headers = {}; let requestData = undefined; if (this.nodeData.requestBody) { if (this.nodeData.requestBody.type === 'raw-json') { headers['content-type'] = 'application/json'; requestData = this.nodeData.requestBody.body ? JSON.parse(computeVariables(this.nodeData.requestBody.body, variablesDict)) : JSON.parse('{}'); } else if (this.nodeData.requestBody.type === 'form-data') { headers['content-type'] = 'multipart/form-data'; const params = cloneDeep(this.nodeData.requestBody.body); requestData = params; } } if (this.nodeData.headers && this.nodeData.headers.length > 0) { this.nodeData.headers.map((pair, index) => { headers[computeVariables(pair.name, variablesDict)] = computeVariables(pair.value, variablesDict); }); } if (this.auth && this.auth?.type === 'bearer-token') { headers['Authorization'] = `Bearer ${this.auth.token}`; } const options = { method: restMethod, url: finalUrl, headers, data: requestData, }; if (this.auth && this.auth?.type === 'basic-auth') { options.auth = {}; options.auth.username = this.auth.username; options.auth.password = this.auth.password; } return options; } async runHttpRequest(request) { let requestSent; try { if (request.headers['content-type'] === 'multipart/form-data') { const formData = new FormData(); const params = request.data; await params.map(async (param, index) => { if (param.type === 'text') { formData.append(param.key, param.value); } if (param.type === 'file') { let trimmedFilePath = param.value.trim(); formData.append(param.key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); } }); request.data = formData; extend(request.headers, formData.getHeaders()); } requestSent = { url: request.url, method: request.method, headers: request.headers, // form data obj gets serialized here so that it can be sent over wire // otherwise ipc communication errors out data: request.data ? JSON.parse(JSON.stringify(request.data)) : request.data, }; const result = await axios({ ...request, signal: newAbortSignal(), }); return { request: requestSent, response: { status: result.status, statusText: result.statusText, data: result.data, headers: result.headers, }, }; } catch (error) { if (error?.response) { return { request: requestSent, response: { error: { status: error.response.status, statusText: error.response.statusText, data: error.response.data, headers: error.response.headers, }, }, }; } else { return { request: requestSent, response: { error: { status: '', statusText: '', data: `An error occurred while running the request : ${error?.message}`, }, }, }; } } } } module.exports = requestNode; ================================================ FILE: packages/flowtest-cli/graph/compute/setvarnode.js ================================================ const { computeNodeVariable } = require('./utils'); const Node = require('./node'); const EvaluateOperators = require('../constants/evaluateOperators'); class setVarNode extends Node { constructor(nodeData, prevNodeOutputData, envVariables) { super('setVarNode'); this.nodeData = nodeData; this.prevNodeOutputData = prevNodeOutputData; this.envVariables = envVariables; } getVariableValue(variable) { if (variable.type.toLowerCase() === 'variable') { if (Object.prototype.hasOwnProperty.call(this.envVariables, variable.value)) { return this.envVariables[variable.value]; } else { throw Error(`Cannot find value of variable ${variable.value}`); } } else { return computeNodeVariable(variable, this.prevNodeOutputData); } } evaluate() { //console.log('Evaluating set variable node'); if (this.nodeData.variable) { if (this.nodeData.variable.name && this.nodeData.variable.name.trim() != '') { const vName = this.nodeData.variable.name; const vType = this.nodeData.variable.type.trim(); if (['String', 'Number', 'Boolean', 'Now', 'Select'].includes(vType)) { const value = computeNodeVariable(this.nodeData.variable, this.prevNodeOutputData); return { [vName]: value, }; } else if (vType === 'Expression') { const variables = this.nodeData.variable.value.variables; if (variables && variables.var1 && variables.var2) { const var1 = this.getVariableValue(this.nodeData.variable.value.variables.var1); const var2 = this.getVariableValue(this.nodeData.variable.value.variables.var2); const operator = this.nodeData.variable.value.operator; if (operator == undefined) { throw 'Operator undefined'; } if (operator == EvaluateOperators.Add) { if (typeof var1 !== 'number' || typeof var2 !== 'number') { throw Error(`Cannot perform ${typeof var1} + ${typeof var2}`); } return { [vName]: var1 + var2, }; } else if (operator == EvaluateOperators.Subtract) { if (typeof var1 !== 'number' || typeof var2 !== 'number') { throw Error(`Cannot perform ${typeof var1} + ${typeof var2}`); } return { [vName]: var1 - var2, }; } } } } } } } module.exports = setVarNode; ================================================ FILE: packages/flowtest-cli/graph/compute/utils.js ================================================ const computeNodeVariable = (variable, prevNodeOutputData) => { if (variable.type.toLowerCase() === 'string') { return variable.value.toString(); } if (variable.type.toLowerCase() === 'number') { return Number(variable.value); } if (variable.type.toLowerCase() === 'boolean') { return Boolean(variable.value); } if (variable.type.toLowerCase() === 'now') { return Date.now(); } if (variable.type.toLowerCase() === 'select') { try { if (prevNodeOutputData == undefined || Object.keys(prevNodeOutputData).length === 0) { console.debug( `Cannot evaluate variable ${variable} as previous node output data ${JSON.stringify(prevNodeOutputData)} is empty`, ); throw Error(`Cannot evaluate variable ${variable.value}`); } const jsonTree = variable.value.split('.'); const getVal = (parent, pos) => { if (pos == jsonTree.length) { return parent; } const key = jsonTree[pos]; if (key == '') { return parent; } return getVal(parent[key], pos + 1); }; const result = getVal(prevNodeOutputData, 0); if (result == undefined) { console.debug( `Cannot evaluate variable ${JSON.stringify(variable)} as previous node output data ${JSON.stringify(prevNodeOutputData)} did not contain the variable`, ); throw Error(`Cannot evaluate variable ${variable.value}`); } return result; } catch (error) { throw Error(`Cannot evaluate variable ${variable.value}`); } } }; const computeNodeVariables = (variables, prevNodeOutputData) => { const evalVariables = {}; if (variables) { Object.entries(variables).map(([vname, variable]) => { evalVariables[vname] = computeNodeVariable(variable, prevNodeOutputData); }); } return evalVariables; }; const computeVariables = (str, variablesDict) => { const regex = /\{\{(.+)\}\}/; const foundRegex = regex.exec(str); if (foundRegex) { const match = str.match(/{{([^}]+)}}/); if (variablesDict) { if (Object.prototype.hasOwnProperty.call(variablesDict, match[1])) { const varValue = variablesDict[`${match[1]}`]; return computeVariables(str.replaceAll(match[0], varValue), variablesDict); } else { throw Error(`Cannot find value of variable ${match[1]}`); } } else { throw Error(`Cannot compute variable ${match[1]} as dict is empty`); } } else { return str; } }; module.exports = { computeNodeVariable, computeNodeVariables, computeVariables, }; ================================================ FILE: packages/flowtest-cli/graph/compute/utils.test.js ================================================ const { computeVariables } = require('./utils'); describe('Utils', () => { it('should compute variables correctly', () => { let str = 'hello {{var1}}! hello {{var1}}! bye'; let dict = { var1: 'world', }; let result = computeVariables(str, dict); expect(result).toEqual('hello world! hello world! bye'); str = 'hello {{var1}}! hello {{var2}}! bye'; expect(() => { computeVariables(str, dict); }).toThrow(Error); str = 'hello {{var1}}! hello {{var2}}! bye'; dict = null; expect(() => { computeVariables(str, dict); }).toThrow(Error); dict = { var1: 'world', var2: 'person', }; result = computeVariables(str, dict); expect(result).toEqual('hello world! hello person! bye'); str = 'hello world!'; result = computeVariables(str, dict); expect(result).toEqual(str); }); }); ================================================ FILE: packages/flowtest-cli/graph/constants/assertOperators.js ================================================ const AssertOperators = { isLessThan: 'isLessThan', isGreaterThan: 'isGreaterThan', isEqualTo: 'isEqualTo', isNotEqualTo: 'isNotEqualTo', }; module.exports = AssertOperators; ================================================ FILE: packages/flowtest-cli/graph/constants/evaluateOperators.js ================================================ const EvaluateOperators = { Add: 'Add two numbers', Subtract: 'Subtract two numbers', }; module.exports = EvaluateOperators; ================================================ FILE: packages/flowtest-cli/package.json ================================================ { "name": "flowtestai", "version": "1.0.2", "description": "CLI to run flow from command line", "main": "bin/index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "bin": { "flow": "bin/index.js" }, "bugs": { "url": "https://github.com/FlowTestAI/FlowTest/issues" }, "repository": { "type": "git", "url": "git+https://github.com/FlowTestAI/FlowTest.git" }, "author": "Sajal Jain ", "license": "MIT", "dependencies": { "axios": "^1.7.2", "axios-retry": "^4.4.0", "boxen": "^7.1.1", "chalk": "^3.0.0", "dotenv": "^16.4.5", "form-data": "^4.0.0", "fs": "^0.0.1-security", "lodash": "^4.17.21", "omelette": "^0.4.17", "path": "^0.12.7", "yargs": "^17.7.2" }, "engines": { "node": ">=18.17.0" } } ================================================ FILE: packages/flowtest-cli/utils/flowparser/AssertNode.js ================================================ const { Node } = require('./Node'); class AssertNode extends Node { constructor() { super('assertNode'); } serialize(id, data, metadata) { return { id, type: this.type, data, ...metadata, }; } deserialize(node) { const id = node.id; const data = node.data; delete node.id; delete node.data; const metadata = node; return { id, data, metadata, }; } } module.exports = { AssertNode, }; ================================================ FILE: packages/flowtest-cli/utils/flowparser/AuthNode.js ================================================ const { Node } = require('./Node'); class AuthNode extends Node { constructor() { super('authNode'); } serialize(id, data, metadata) { return { id, type: this.type, data, ...metadata, }; } deserialize(node) { const id = node.id; const data = node.data; delete node.id; delete node.data; const metadata = node; return { id, data, metadata, }; } } module.exports = { AuthNode, }; ================================================ FILE: packages/flowtest-cli/utils/flowparser/DelayNode.js ================================================ const { Node } = require('./Node'); class DelayNode extends Node { constructor() { super('delayNode'); } serialize(id, data, metadata) { return { id, type: this.type, data, ...metadata, }; } deserialize(node) { const id = node.id; const data = node.data; delete node.id; delete node.data; const metadata = node; return { id, data, metadata, }; } } module.exports = { DelayNode, }; ================================================ FILE: packages/flowtest-cli/utils/flowparser/NestedFlowNode.js ================================================ const { Node } = require('./Node'); class NestedFlowNode extends Node { constructor() { super('flowNode'); } serialize(id, data, metadata) { return { id, type: this.type, data, ...metadata, }; } deserialize(node) { const id = node.id; const data = node.data; delete node.id; delete node.data; const metadata = node; return { id, data, metadata, }; } } module.exports = { NestedFlowNode, }; ================================================ FILE: packages/flowtest-cli/utils/flowparser/Node.js ================================================ class Node { constructor(type) { this.type = type; } serialize(id, data, metadata) { throw new Error('Serialize method must be implemented by subclasses'); } deserialize(node) { throw new Error('Deserialize method must be implemented by subclasses'); } } module.exports = { Node, }; ================================================ FILE: packages/flowtest-cli/utils/flowparser/OutputNode.js ================================================ const { Node } = require('./Node'); class OutputNode extends Node { constructor() { super('outputNode'); } serialize(id, data, metadata) { return { id, type: this.type, data, ...metadata, }; } deserialize(node) { const id = node.id; const { ['output']: _, ...data } = node.data; delete node.id; delete node.data; const metadata = node; return { id, data, metadata, }; } } module.exports = { OutputNode, }; ================================================ FILE: packages/flowtest-cli/utils/flowparser/RequestNode.js ================================================ const { Node } = require('./Node'); class RequestNode extends Node { constructor() { super('requestNode'); } serialize(id, data, metadata) { return { id, type: this.type, data, ...metadata, }; } deserialize(node) { const id = node.id; const data = node.data; delete node.id; delete node.data; const metadata = node; return { id, data, metadata, }; } } module.exports = { RequestNode, }; ================================================ FILE: packages/flowtest-cli/utils/flowparser/SetVarNode.js ================================================ const { Node } = require('./Node'); class SetVarNode extends Node { constructor() { super('setVarNode'); } serialize(id, data, metadata) { return { id, type: this.type, data, ...metadata, }; } deserialize(node) { const id = node.id; const data = node.data; delete node.id; delete node.data; const metadata = node; return { id, data, metadata, }; } } module.exports = { SetVarNode, }; ================================================ FILE: packages/flowtest-cli/utils/flowparser/StartNode.js ================================================ const { Node } = require('./Node'); class StartNode extends Node { constructor() { super('startNode'); } serialize(id, data, metadata) { return { id, ...metadata, }; } deserialize(node) { const id = node.id; delete node.id; const metadata = node; return { id, metadata, }; } } module.exports = { StartNode, }; ================================================ FILE: packages/flowtest-cli/utils/flowparser/parser.js ================================================ const { cloneDeep } = require('lodash'); const { AuthNode } = require('./AuthNode'); const { NestedFlowNode } = require('./NestedFlowNode'); const { DelayNode } = require('./DelayNode'); const { AssertNode } = require('./AssertNode'); const { OutputNode } = require('./OutputNode'); const { RequestNode } = require('./RequestNode'); const { StartNode } = require('./StartNode'); const { SetVarNode } = require('./SetVarNode'); const VERSION = 1; const deserialize = (flowData) => { // we don't want to modify original object const flowDataCopy = cloneDeep(flowData); const textData = {}; textData.version = VERSION; textData.graph = {}; if (flowData) { if (flowData.nodes) { const nodes = flowDataCopy.nodes; textData.graph.data = {}; textData.graph.data.nodes = {}; textData.graph.metadata = {}; textData.graph.metadata.nodes = {}; nodes.forEach((node) => { if (node.type === 'startNode') { const sNode = new StartNode(); const result = sNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'startNode', }; textData.graph.metadata.nodes[result.id] = { type: 'startNode', ...result.metadata, }; } if (node.type === 'authNode') { const aNode = new AuthNode(); const result = aNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'authNode', auth: result.data, }; textData.graph.metadata.nodes[result.id] = { type: 'authNode', ...result.metadata, }; } if (node.type === 'requestNode') { const rNode = new RequestNode(); const result = rNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'requestNode', ...result.data, }; textData.graph.metadata.nodes[result.id] = { type: 'requestNode', ...result.metadata, }; } if (node.type === 'outputNode') { const oNode = new OutputNode(); const result = oNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'outputNode', ...result.data, }; textData.graph.metadata.nodes[result.id] = { type: 'outputNode', ...result.metadata, }; } if (node.type === 'delayNode') { const dNode = new DelayNode(); const result = dNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'delayNode', ...result.data, }; textData.graph.metadata.nodes[result.id] = { type: 'delayNode', ...result.metadata, }; } if (node.type === 'assertNode') { const eNode = new AssertNode(); const result = eNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'assertNode', ...result.data, }; textData.graph.metadata.nodes[result.id] = { type: 'assertNode', ...result.metadata, }; } if (node.type === 'flowNode') { const fNode = new NestedFlowNode(); const result = fNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'flowNode', ...result.data, }; textData.graph.metadata.nodes[result.id] = { type: 'flowNode', ...result.metadata, }; } if (node.type === 'setVarNode') { const sNode = new SetVarNode(); const result = sNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'setVarNode', ...result.data, }; textData.graph.metadata.nodes[result.id] = { type: 'setVarNode', ...result.metadata, }; } }); } if (flowData.edges) { const edges = flowDataCopy.edges; textData.graph.data.edges = []; textData.graph.metadata.edges = {}; edges.forEach((edge) => { textData.graph.data.edges.push(`${edge.source} -> ${edge.target}`); const { ['id']: _, ..._edge } = edge; textData.graph.metadata.edges[edge.id] = _edge; }); } if (flowData.viewport) { textData.graph.metadata.viewport = flowDataCopy.viewport; } } return textData; }; const serialize = (textData) => { const flowData = {}; flowData.nodes = []; flowData.edges = []; flowData.viewport = { x: 0, y: 0, zoom: 1 }; // we don't want to modify original object const textDataCopy = cloneDeep(textData); const version = textDataCopy.version; if (version === 1) { if (textDataCopy.graph.data) { Object.entries(textDataCopy.graph.data.nodes).map(([key, value], index) => { const id = key; if (value.type === 'startNode') { const metadata = textDataCopy.graph.metadata.nodes[id]; const sNode = new StartNode(); const result = sNode.serialize(id, undefined, metadata); flowData.nodes.push(result); } if (value.type === 'authNode') { const data = value.auth; const metadata = textDataCopy.graph.metadata.nodes[id]; const aNode = new AuthNode(); const result = aNode.serialize(id, data, metadata); flowData.nodes.push(result); } if (value.type === 'requestNode') { const data = value; const metadata = textDataCopy.graph.metadata.nodes[id]; const rNode = new RequestNode(); const result = rNode.serialize(id, data, metadata); flowData.nodes.push(result); } if (value.type === 'outputNode') { const data = value; const metadata = textDataCopy.graph.metadata.nodes[id]; const oNode = new OutputNode(); const result = oNode.serialize(id, data, metadata); flowData.nodes.push(result); } if (value.type === 'delayNode') { const data = value; const metadata = textDataCopy.graph.metadata.nodes[id]; const dNode = new DelayNode(); const result = dNode.serialize(id, data, metadata); flowData.nodes.push(result); } if (value.type === 'assertNode') { const data = value; const metadata = textDataCopy.graph.metadata.nodes[id]; const dNode = new AssertNode(); const result = dNode.serialize(id, data, metadata); flowData.nodes.push(result); } if (value.type === 'flowNode') { const data = value; const metadata = textDataCopy.graph.metadata.nodes[id]; const fNode = new NestedFlowNode(); const result = fNode.serialize(id, data, metadata); flowData.nodes.push(result); } if (value.type === 'setVarNode') { const data = value; const metadata = textDataCopy.graph.metadata.nodes[id]; const cNode = new SetVarNode(); const result = cNode.serialize(id, data, metadata); flowData.nodes.push(result); } }); Object.entries(textDataCopy.graph.metadata.edges).map(([key, value], index) => { flowData.edges.push({ id: key, ...value, }); }); if (textDataCopy.graph.metadata.viewport) { flowData.viewport = textDataCopy.graph.metadata.viewport; } } } else { throw new Error('Version not recognized'); } return flowData; }; module.exports = { deserialize, serialize, }; ================================================ FILE: packages/flowtest-cli/utils/readfile.js ================================================ const fs = require('fs'); const pathExists = (path) => { try { fs.accessSync(path); return true; } catch (error) { return false; } }; const readFile = (path) => { if (!path) { throw new Error('File path is required'); } // check if file exists if (!pathExists(path)) { throw new Error('File does not exist'); } // now delete the file return fs.readFileSync(path, 'utf8'); }; module.exports = readFile; ================================================ FILE: packages/flowtest-electron/.npmrc ================================================ node-linker=hoisted #pnpm requirement when working with electron ================================================ FILE: packages/flowtest-electron/CHANGELOG.md ================================================ # flowtestai ## 1.2.0 ### Minor Changes - aadbd1b: Collapse sidebar to give more real estate to canvas - 780f3c2: generate sample request body and parameter values - e968f1e: beautify logs sidesheet and ability to upload flow scans - 92a0bab: Add support for anthropic claude hosted on bedrock - 97c6981: display pretty structured logs in graph run - 6def317: check version mismatch on startup and notify user of latest version availability - 5a92053: Ability to manage multiple flow tabs simultaneously - 563e011: Allow multiple kv params in multipart form data request type - a07dbea: allow configurable user settings - d20fb8b: make app platform agnostic to allow windows platform support - 912483f: use rich editor for auto complete variables and redefine UI of request nodes ## 1.1.0 ### Minor Changes - a5d61a8: Introduce a UI theme, json editor powered by codemirror and few shortcut keys to improve workflow - 7f45adc: A more intuitive UX to onboard first time user - 95310af: Updated contributing docs and linting - 19c52aa: Ability to clone a fow and expand output node for bigger view - 5ac8237: Maintain state of logs and viewports of each canvas separately. ================================================ FILE: packages/flowtest-electron/electron-main.js ================================================ // Modules to control application life and create native browser window const { app, BrowserWindow, Menu, shell } = require('electron'); const path = require('path'); const url = require('url'); const template = require('./electron-menu'); const Watcher = require('./src/app/watcher'); const registerRendererEventHandlers = require('./src/ipc/collection'); const registerSettingsEventHandlers = require('./src/ipc/settings'); const packageJson = require('./package.json'); // app's package.json const https = require('https'); let mainWindow; let watcher; if (process.env.NODE_ENV === 'production') { const noop = () => {}; console.log = noop; console.info = noop; console.error = noop; console.warn = noop; console.debug = noop; console.trace = noop; } const version = { current: packageJson.version, latest: packageJson.version, }; function checkForUpdates() { const url = `https://raw.githubusercontent.com/FlowTestAI/FlowTest/main/packages/flowtest-electron/package.json`; https .get(url, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { const remotePackageJson = JSON.parse(data); const latestVersion = remotePackageJson.version; if (latestVersion !== version.current) { version.latest = latestVersion; //shell.openExternal(`https://github.com/${username}/${repo}/releases`); } } catch (error) { console.error('Error parsing JSON:', error); } }); }) .on('error', (err) => { console.error('Error fetching package.json:', err); }); } app.on('ready', async () => { const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); // Create the browser window. mainWindow = new BrowserWindow({ width: 1280, height: 768, icon: path.join(__dirname, 'assets/MyIcon.png'), webPreferences: { nodeIntegration: true, contextIsolation: true, preload: path.join(__dirname, 'preload.js'), webviewTag: true, }, title: 'FlowTestAI', }); mainWindow.maximize(); // and load the index.html of the app. const startUrl = url.format({ pathname: path.join(__dirname, '../../build/index.html'), protocol: 'file:', slashes: true, }); mainWindow.loadURL(startUrl); // Open the DevTools. // mainWindow.webContents.openDevTools() // This is required to open a link in the external browser mainWindow.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; }); watcher = new Watcher(); checkForUpdates(); mainWindow.webContents.on('did-finish-load', () => { // Send a message to the renderer process mainWindow.webContents.send('main:app-version', version); }); registerRendererEventHandlers(mainWindow, watcher); registerSettingsEventHandlers(mainWindow); }); // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', function () { //if (process.platform !== 'darwin') app.quit(); }); ================================================ FILE: packages/flowtest-electron/electron-menu.js ================================================ const { shell } = require('electron'); const template = [ { label: 'FlowTestAI', submenu: [ { type: 'separator' }, { role: 'quit', label: 'Exit FlowTestAI', }, ], }, { label: 'Edit', submenu: [ { role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, { role: 'selectAll' }, { type: 'separator' }, { role: 'hide' }, { role: 'hideOthers' }, ], }, { label: 'View', submenu: [ { role: 'toggledevtools' }, { type: 'separator' }, { role: 'resetzoom' }, { role: 'zoomin' }, { role: 'zoomout' }, { type: 'separator' }, { role: 'togglefullscreen' }, ], }, { role: 'window', submenu: [{ role: 'minimize' }, { role: 'close', accelerator: 'CommandOrControl+Shift+Q' }], }, { role: 'help', label: 'Help', submenu: [ { label: 'About', click: async () => { await shell.openExternal('https://github.com/FlowTestAI/FlowTest'); }, }, ], }, ]; module.exports = template; ================================================ FILE: packages/flowtest-electron/notarize.js ================================================ require('dotenv').config(); const { notarize } = require('@electron/notarize'); exports.default = async function notarizing(context) { const { electronPlatformName, appOutDir } = context; if (electronPlatformName !== 'darwin') { return; } const appName = context.packager.appInfo.productFilename; return await notarize({ appBundleId: 'com.flowtestai.app', appPath: `${appOutDir}/${appName}.app`, appleId: process.env.APPLE_ID, appleIdPassword: process.env.APPLE_ID_PASSWORD, teamId: process.env.TEAM_ID, }); }; ================================================ FILE: packages/flowtest-electron/package.json ================================================ { "name": "flowtestai-app", "productName": "FlowTestAI", "version": "1.2.0", "homepage": "https://github.com/FlowTestAI/FlowTest/tree/main", "description": "GenAI powered OpenSource IDE for API first workflows", "main": "electron-main.js", "bugs": { "url": "https://github.com/FlowTestAI/FlowTest/issues" }, "engines": { "node": ">=18.17.0" }, "scripts": { "start": "electron .", "test": "jest", "pack": "NODE_ENV=production electron-builder --dir", "dist:mac": "NODE_ENV=production electron-builder --mac", "dist:win": "SET NODE_ENV=production & electron-builder --win" }, "author": "Sajal Jain ", "license": "MIT", "devDependencies": { "@electron/notarize": "^2.3.0", "electron": "^29.0.0", "electron-builder": "^24.13.3", "jest": "^29.7.0" }, "dependencies": { "@apidevtools/swagger-parser": "^10.1.0", "@aws-crypto/sha256-js": "^5.2.0", "@aws-sdk/client-bedrock": "^3.583.0", "@aws-sdk/client-bedrock-runtime": "^3.583.0", "@aws-sdk/credential-provider-node": "^3.583.0", "@aws-sdk/types": "^3.577.0", "@google/generative-ai": "^0.16.0", "@langchain/community": "^0.2.19", "@langchain/google-genai": "^0.0.25", "@smithy/eventstream-codec": "^3.0.0", "@smithy/protocol-http": "^4.0.0", "@smithy/signature-v4": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "axios": "^1.6.7", "axios-retry": "^4.4.0", "chokidar": "^3.6.0", "dotenv": "^16.4.5", "electron-store": "^8.1.0", "flatted": "^3.3.1", "form-data": "^4.0.0", "fs": "^0.0.1-security", "json-refs": "^3.0.15", "langchain": "^0.1.28", "lodash": "^4.17.21", "openai": "^4.29.1", "path": "^0.12.7", "uuid": "^9.0.1" }, "build": { "appId": "com.flowtestai.app", "productName": "FlowTestAI", "directories": { "buildResources": "resources", "output": "dist" }, "files": [ "**/*" ], "afterSign": "notarize.js", "win": { "target": "nsis" }, "mac": { "target": { "target": "default", "arch": [ "x64", "arm64" ] }, "category": "public.app-category.developer-tools", "identity": "Sajal Jain (Z25C545DT5)", "hardenedRuntime": true, "gatekeeperAssess": false, "icon": "assets/MyIcon.icns" }, "linux": { "target": [ "AppImage", "deb" ] } } } ================================================ FILE: packages/flowtest-electron/preload.js ================================================ const { ipcRenderer, contextBridge } = require('electron'); const path = require('path'); const { isMacOS } = require('./src/utils/filemanager/filesystem'); contextBridge.exposeInMainWorld('ipcRenderer', { invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), on: (channel, handler) => ipcRenderer.on(channel, (event, ...args) => handler(...args)), join: (...args) => path.join(...args), relative: (...args) => path.relative(...args), dirname: (...args) => path.dirname(...args), isMacOs: isMacOS, }); ================================================ FILE: packages/flowtest-electron/src/ai/flowtestai.js ================================================ const BedrockClaudeGenerate = require('./models/bedrock_claude'); const GeminiGenerate = require('./models/gemini'); const OpenAIGenerate = require('./models/openai'); class FlowtestAI { async generate(collection, user_instruction, model) { if (model.name === 'OPENAI') { const available_functions = await this.get_available_functions(collection); const openai = new OpenAIGenerate(); const functions = await openai.filter_functions(available_functions, user_instruction, model.apiKey); return await openai.process_user_instruction(functions, user_instruction, model.apiKey); } else if (model.name === 'BEDROCK_CLAUDE') { const available_functions = await this.get_available_functions(collection); const bedrock_claude = new BedrockClaudeGenerate(model.apiKey); const functions = await bedrock_claude.filter_functions(available_functions, user_instruction); return await bedrock_claude.process_user_instruction(functions, user_instruction); } else if (model.name === 'GEMINI') { const available_functions = await this.get_available_functions(collection); const gemini = new GeminiGenerate(model.apiKey); const functions = await gemini.filter_functions(available_functions, user_instruction); return await gemini.process_user_instruction(functions, user_instruction); } else { throw Error(`Model ${model.name} not supported`); } } async get_available_functions(collection) { let functions = []; Object.entries(collection['paths']).map(([path, methods], index) => { Object.entries(methods).map(([method, spec], index1) => { const function_name = spec['operationId']; const desc = spec['description'] || spec['summary'] || ''; let schema = { type: 'object', properties: {} }; let req_body = undefined; if (spec['requestBody']) { if (spec['requestBody']['content']) { if (spec['requestBody']['content']['application/json']) { if (spec['requestBody']['content']['application/json']['schema']) { req_body = spec['requestBody']['content']['application/json']['schema']; } } } } if (req_body != undefined) { schema['properties']['requestBody'] = req_body; } const params = spec['parameters'] ? spec['parameters'] : []; const param_properties = {}; if (params.length > 0) { for (const param of params) { if (param['schema']) { param_properties[param['name']] = param['schema']; } } schema['properties']['parameters'] = { type: 'object', properties: param_properties, }; } const f = { type: 'function', function: { name: function_name, description: desc, parameters: schema }, }; if (this.isCyclic(f)) { functions.push({ type: 'function', function: { name: function_name, description: desc, parameters: {} }, }); } else { functions.push(f); } }); }); return functions; } isCyclic(obj) { var keys = []; var stack = []; var stackSet = new Set(); var detected = false; function detect(obj, key) { if (obj && typeof obj != 'object') { return false; } if (stackSet.has(obj)) { // it's cyclic! Print the object and its locations. var oldindex = stack.indexOf(obj); var l1 = keys.join('.') + '.' + key; var l2 = keys.slice(0, oldindex + 1).join('.'); //console.log('CIRCULAR: ' + l1 + ' = ' + l2 + ' = ' + obj); //console.log(obj); detected = true; return; } keys.push(key); stack.push(obj); stackSet.add(obj); for (var k in obj) { //dive on the object's children if (Object.prototype.hasOwnProperty.call(obj, k)) { detect(obj[k], k); } } keys.pop(); stack.pop(); stackSet.delete(obj); return; } detect(obj, 'obj', keys, stack, stackSet, detected); return detected; } } module.exports = FlowtestAI; ================================================ FILE: packages/flowtest-electron/src/ai/models/bedrock_claude.js ================================================ const { BedrockChat } = require('@langchain/community/chat_models/bedrock'); const { HumanMessage, SystemMessage, BaseMessage } = require('@langchain/core/messages'); const { BedrockEmbeddings } = require('@langchain/community/embeddings/bedrock'); const { MemoryVectorStore } = require('langchain/vectorstores/memory'); class BedrockClaudeGenerate { constructor(creds) { this.model = new BedrockChat({ model: 'anthropic.claude-3-sonnet-20240229-v1:0', region: 'us-west-2', // endpointUrl: "custom.amazonaws.com", credentials: creds, modelKwargs: { anthropic_version: 'bedrock-2023-05-31', }, }); this.embeddings = new BedrockEmbeddings({ region: 'us-west-2', credentials: creds, model: 'amazon.titan-embed-text-v2:0', // Default value }); } async filter_functions(functions, instruction) { const documents = functions.map((f) => { const { parameters, ...fDescription } = f.function; return JSON.stringify(fDescription); }); const vectorStore = await MemoryVectorStore.fromTexts(documents, [], this.embeddings); // 128 (max no of functions accepted by openAI function calling) const retrievedDocuments = await vectorStore.similaritySearch(instruction, 10); var selectedFunctions = []; retrievedDocuments.forEach((document) => { const pDocument = JSON.parse(document.pageContent); const findF = functions.find( (f) => f.function.name === pDocument.name && f.function.description === pDocument.description, ); if (findF) { selectedFunctions = selectedFunctions.concat(findF); } }); return selectedFunctions; } async process_user_instruction(functions, instruction) { //console.log(functions.map((f) => f.function.name)); // Define the function call format const fn = `{"name": "function_name"}`; // Prepare the function string for the system prompt const fnStr = functions.map((f) => JSON.stringify(f)).join('\n'); // Define the system prompt const systemPrompt = ` You are a helpful assistant with access to the following functions: ${fnStr} To use these functions respond with, only output function names, ignore arguments needed by those functions: ${fn} ${fn} ... Edge cases you must handle: - If there are multiple functions that can fullfill user request, list them all. - If there are no functions that match the user request, you will respond politely that you cannot help. - If the user has not provided all information to execute the function call, choose the best possible set of values. Only, respond with the information requested and nothing else. - If asked something that cannot be determined with the user's request details, respond that it is not possible to fulfill the request and explain why. `; // Prepare the messages for the language model const messages = [new SystemMessage({ content: systemPrompt }), new HumanMessage({ content: instruction })]; // Invoke the language model and get the completion const completion = await this.model.invoke(messages); const content = completion.content.trim(); // Extract function calls from the completion const extractedFunctions = this.extractFunctionCalls(content); console.log(extractedFunctions); return extractedFunctions; } extractFunctionCalls(completion) { let content = typeof completion === 'string' ? completion : completion.content; // Multiple functions lookup const mfnPattern = /(.*?)<\/multiplefunctions>/s; const mfnMatch = content.match(mfnPattern); // Single function lookup const singlePattern = /(.*?)<\/functioncall>/s; const singleMatch = content.match(singlePattern); let functions = []; if (!mfnMatch && !singleMatch) { // No function calls found return null; } else if (mfnMatch) { // Multiple function calls found const multiplefn = mfnMatch[1]; const fnMatches = [...multiplefn.matchAll(/(.*?)<\/functioncall>/gs)]; for (let fnMatch of fnMatches) { const fnText = fnMatch[1].replace(/\\/g, ''); try { functions.push(JSON.parse(fnText)); } catch { // Ignore invalid JSON } } } else { // Single function call found const fnText = singleMatch[1].replace(/\\/g, ''); try { functions.push(JSON.parse(fnText)); } catch { // Ignore invalid JSON } } return functions; } } module.exports = BedrockClaudeGenerate; ================================================ FILE: packages/flowtest-electron/src/ai/models/gemini.js ================================================ const { GoogleGenerativeAI } = require('@google/generative-ai'); const { GoogleGenerativeAIEmbeddings } = require('@langchain/google-genai'); const { TaskType } = require('@google/generative-ai'); const { MemoryVectorStore } = require('langchain/vectorstores/memory'); class GeminiGenerate { constructor(apiKey) { this.genAI = new GoogleGenerativeAI(apiKey); this.embeddings = new GoogleGenerativeAIEmbeddings({ apiKey, model: 'text-embedding-004', // 768 dimensions taskType: TaskType.RETRIEVAL_DOCUMENT, title: 'Document title', }); } async filter_functions(functions, instruction) { const documents = functions.map((f) => { const { parameters, ...fDescription } = f.function; return JSON.stringify(fDescription); }); const vectorStore = await MemoryVectorStore.fromTexts(documents, [], this.embeddings); // 128 (max no of functions accepted by openAI function calling) const retrievedDocuments = await vectorStore.similaritySearch(instruction, 10); var selectedFunctions = []; retrievedDocuments.forEach((document) => { const pDocument = JSON.parse(document.pageContent); const findF = functions.find( (f) => f.function.name === pDocument.name && f.function.description === pDocument.description, ); if (findF) { selectedFunctions = selectedFunctions.concat(findF); } }); return selectedFunctions; } async process_user_instruction(functions, instruction) { //console.log(functions.map((f) => f.function.name)); // Define the function call format const fn = `{"name": "function_name"}`; // Prepare the function string for the system prompt const fnStr = functions.map((f) => JSON.stringify(f)).join('\n'); // Define the system prompt const systemPrompt = ` You are a helpful assistant with access to the following functions: ${fnStr} To use these functions respond with, only output function names, ignore arguments needed by those functions: ${fn} ${fn} ... Edge cases you must handle: - If there are multiple functions that can fullfill user request, list them all. - If there are no functions that match the user request, you will respond politely that you cannot help. - If the user has not provided all information to execute the function call, choose the best possible set of values. Only, respond with the information requested and nothing else. - If asked something that cannot be determined with the user's request details, respond that it is not possible to fulfill the request and explain why. `; const model = this.genAI.getGenerativeModel({ model: 'gemini-1.5-pro-latest', systemInstruction: { role: 'system', parts: [{ text: systemPrompt }], }, }); // Prepare the messages for the language model const request = { contents: [{ role: 'user', parts: [{ text: instruction }] }], }; // Invoke the language model and get the completion const completion = await model.generateContent(request); const content = completion.response.candidates[0].content.parts[0].text.trim(); // Extract function calls from the completion const extractedFunctions = this.extractFunctionCalls(content); return extractedFunctions; } extractFunctionCalls(completion) { let content = typeof completion === 'string' ? completion : completion.content; // Multiple functions lookup const mfnPattern = /(.*?)<\/multiplefunctions>/s; const mfnMatch = content.match(mfnPattern); // Single function lookup const singlePattern = /(.*?)<\/functioncall>/s; const singleMatch = content.match(singlePattern); let functions = []; if (!mfnMatch && !singleMatch) { // No function calls found return null; } else if (mfnMatch) { // Multiple function calls found const multiplefn = mfnMatch[1]; const fnMatches = [...multiplefn.matchAll(/(.*?)<\/functioncall>/gs)]; for (let fnMatch of fnMatches) { const fnText = fnMatch[1].replace(/\\/g, ''); try { functions.push(JSON.parse(fnText)); } catch { // Ignore invalid JSON } } } else { // Single function call found const fnText = singleMatch[1].replace(/\\/g, ''); try { functions.push(JSON.parse(fnText)); } catch { // Ignore invalid JSON } } return functions; } } module.exports = GeminiGenerate; ================================================ FILE: packages/flowtest-electron/src/ai/models/openai.js ================================================ const OpenAI = require('openai'); const { MemoryVectorStore } = require('langchain/vectorstores/memory'); const { OpenAIEmbeddings } = require('@langchain/openai'); const SYSTEM_MESSAGE = `You are a helpful assistant. \ Respond to the following prompt by using function_call and then summarize actions. \ If a user request is ambiguous, choose the best response possible.`; // Maximum number of function calls allowed to prevent infinite or lengthy loops const MAX_CALLS = 10; class OpenAIGenerate { async filter_functions(functions, instruction, apiKey) { const documents = functions.map((f) => { const { parameters, ...fDescription } = f.function; return JSON.stringify(fDescription); }); const vectorStore = await MemoryVectorStore.fromTexts( documents, [], new OpenAIEmbeddings({ openAIApiKey: apiKey, }), ); // 128 (max no of functions accepted by openAI function calling) const retrievedDocuments = await vectorStore.similaritySearch(instruction, 10); var selectedFunctions = []; retrievedDocuments.forEach((document) => { const pDocument = JSON.parse(document.pageContent); const findF = functions.find( (f) => f.function.name === pDocument.name && f.function.description === pDocument.description, ); if (findF) { selectedFunctions = selectedFunctions.concat(findF); } }); return selectedFunctions; } async get_openai_response(functions, messages, apiKey) { const openai = new OpenAI({ apiKey, }); return await openai.chat.completions.create({ model: 'gpt-4', //gpt-3.5-turbo-16k-0613 tools: functions, tool_choice: 'auto', // "auto" means the model can pick between generating a message or calling a function. temperature: 0, messages: messages, }); } async process_user_instruction(functions, instruction, apiKey) { //console.log(functions.map((f) => f.function.name)); let result = []; let num_calls = 0; const messages = [ { content: SYSTEM_MESSAGE, role: 'system' }, { content: instruction, role: 'user' }, ]; while (num_calls < MAX_CALLS) { const response = await this.get_openai_response(functions, messages, apiKey); const message = response['choices'][0]['message']; if (message.tool_calls) { messages.push(message); message.tool_calls.map((tool_call) => { console.log('Function call #: ', num_calls + 1); console.log(JSON.stringify(tool_call)); // We'll simply add a message to simulate successful function call. messages.push({ role: 'tool', content: 'success', tool_call_id: tool_call.id, }); result.push(tool_call.function); num_calls += 1; }); } else { console.log('Message: '); console.log(message['content']); break; } } if (num_calls >= MAX_CALLS) { console.log('Reached max chained function calls: ', MAX_CALLS); } return result; } } module.exports = OpenAIGenerate; ================================================ FILE: packages/flowtest-electron/src/app/watcher.js ================================================ const chokidar = require('chokidar'); const path = require('path'); const dotenv = require('dotenv'); const { PATH_SEPARATOR, getSubdirectoriesFromRoot } = require('../utils/filemanager/filesystem'); const readFile = require('../utils/filemanager/readfile'); const { serialize } = require('../utils/flowparser/parser'); class Watcher { constructor() { this.watchers = {}; } isFlowTestFile(pathname) { if (!pathname || typeof pathname !== 'string') return false; return ['flow'].some((ext) => pathname.toLowerCase().endsWith(`.${ext}`)); } isEnvFile(pathname, collectionPath) { if (!pathname || typeof pathname !== 'string') return false; const dirname = path.dirname(pathname); const envDirectory = path.join(collectionPath, 'environments'); return dirname === envDirectory && ['env'].some((ext) => pathname.toLowerCase().endsWith(`.${ext}`)); } isDotEnvFile(pathname, collectionPath) { const dirname = path.dirname(pathname); const basename = path.basename(pathname); return dirname === collectionPath && basename === '.env'; } add(mainWindow, pathname, collectionId, watchPath) { console.log(`[Watcher] File ${pathname} added`); if (this.isFlowTestFile(pathname)) { const content = readFile(pathname); const flowData = serialize(JSON.parse(content)); const dirname = path.dirname(pathname); const subDirectories = getSubdirectoriesFromRoot(watchPath, dirname); const file = { name: path.basename(pathname), pathname: pathname, subDirectories, sep: PATH_SEPARATOR, flowData, }; mainWindow.webContents.send('main:create-flowtest', file, collectionId); } else if (this.isEnvFile(pathname, watchPath)) { try { const variables = this.getEnvVariables(pathname); const file = { name: path.basename(pathname), pathname: pathname, variables, }; mainWindow.webContents.send('main:addOrUpdate-environment', file, collectionId); } catch (error) { console.error(`Failed to add ${pathname} due to: ${error}`); } } else if (this.isDotEnvFile(pathname, watchPath)) { try { const variables = this.getEnvVariables(pathname); mainWindow.webContents.send('main:addOrUpdate-dotEnvironment', variables, collectionId); } catch (error) { console.error(`Failed to add .env variables due to: ${error}`); } } } addDirectory(mainWindow, pathname, collectionId, watchPath) { const envDirectory = path.join(watchPath, 'environments'); if (pathname === envDirectory) { return; } if (pathname === watchPath) { // we have already added collection object to store return; } console.log(`[Watcher] Directory ${pathname} added`); const directory = { name: path.basename(pathname), pathname: pathname, }; const subDirsFromRoot = getSubdirectoriesFromRoot(watchPath, directory.pathname); mainWindow.webContents.send('main:add-directory', directory, collectionId, subDirsFromRoot, PATH_SEPARATOR); } change(mainWindow, pathname, collectionId, watchPath) { console.log(`[Watcher] file ${pathname} changed`); if (this.isFlowTestFile(pathname)) { const content = readFile(pathname); const flowData = serialize(JSON.parse(content)); const file = { name: path.basename(pathname), pathname, flowData, }; mainWindow.webContents.send('main:update-flowtest', file, collectionId); } else if (this.isEnvFile(pathname, watchPath)) { try { const variables = this.getEnvVariables(pathname); const file = { name: path.basename(pathname), pathname: pathname, variables, }; mainWindow.webContents.send('main:addOrUpdate-environment', file, collectionId); } catch (error) { console.error(`Failed to save ${pathname} due to: ${error}`); } } else if (this.isDotEnvFile(pathname, watchPath)) { try { const variables = this.getEnvVariables(pathname); mainWindow.webContents.send('main:addOrUpdate-dotEnvironment', variables, collectionId); } catch (error) { console.error(`Failed to add .env variables due to: ${error}`); } } } unlink(mainWindow, pathname, collectionId, watchPath) { console.log(`[Watcher] File ${pathname} removed`); if (this.isFlowTestFile(pathname)) { const file = { name: path.basename(pathname), pathname: pathname, }; mainWindow.webContents.send('main:delete-flowtest', file, collectionId); } else if (this.isEnvFile(pathname, watchPath)) { try { const file = { name: path.basename(pathname), pathname: pathname, }; mainWindow.webContents.send('main:delete-environment', file, collectionId); } catch (error) { console.error(`Failed to save ${pathname} due to: ${error}`); } } } unlinkDir(mainWindow, pathname, collectionId, watchPath) { const envDirectory = path.join(watchPath, 'environments'); if (pathname === envDirectory) { return; } console.log(`[Watcher] Directory ${pathname} removed`); const directory = { name: path.basename(pathname), pathname: pathname, }; mainWindow.webContents.send('main:delete-directory', directory, collectionId); } getEnvVariables(pathname) { const content = readFile(pathname); const buf = Buffer.from(content); const parsed = dotenv.parse(buf); return parsed; } addWatcher(mainWindow, watchPath, collectionId) { if (!this.hasWatcher(watchPath)) { console.log(`[Watcher] watcher added for path: ${watchPath} `); if (this.watchers[watchPath]) { this.watchers[watchPath].close(); } setTimeout(() => { const watcher = chokidar.watch(watchPath, { ignoreInitial: false, usePolling: watchPath.startsWith('\\\\') ? true : false, ignored: (path) => ['node_modules', '.git'].some((s) => path.includes(s)), persistent: true, ignorePermissionErrors: true, awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 10, }, depth: 20, }); watcher .on('add', (pathname) => this.add(mainWindow, pathname, collectionId, watchPath)) .on('addDir', (pathname) => this.addDirectory(mainWindow, pathname, collectionId, watchPath)) .on('change', (pathname) => this.change(mainWindow, pathname, collectionId, watchPath)) .on('unlink', (pathname) => this.unlink(mainWindow, pathname, collectionId, watchPath)) .on('unlinkDir', (pathname) => this.unlinkDir(mainWindow, pathname, collectionId, watchPath)); this.watchers[watchPath] = watcher; }, 100); } } hasWatcher(watchPath) { return this.watchers[watchPath] != undefined ? true : false; } removeWatcher(watchPath) { if (this.watchers[watchPath]) { this.watchers[watchPath].close(); this.watchers[watchPath] = null; } } } module.exports = Watcher; ================================================ FILE: packages/flowtest-electron/src/ipc/axiosClient.js ================================================ // lib/axiosClient.ts const axios = require('axios'); const axiosRetry = require('axios-retry').default; const axiosClient = (baseUrl, accessId, accessKey) => { const client = axios.create({ baseURL: `${baseUrl}/api`, headers: { 'Content-Type': 'application/json', 'x-access-id': accessId, 'x-access-key': accessKey, }, }); axiosRetry(client, { retries: 3, // Number of retries retryDelay: (retryCount) => { return retryCount * 1000; // Time interval between retries (1000 ms = 1 second) }, retryCondition: (error) => { // Retry on network errors or rate limit errors or 5xx server errors return error.response?.status === 500 || error.response?.status === 429 || error.code === 'ECONNABORTED'; }, }); return client; }; module.exports = { axiosClient, }; ================================================ FILE: packages/flowtest-electron/src/ipc/collection.js ================================================ const fs = require('fs'); const path = require('path'); const axios = require('axios'); const { ipcMain, shell, dialog, app } = require('electron'); const SwaggerParser = require('@apidevtools/swagger-parser'); const JsonRefs = require('json-refs'); const createDirectory = require('../utils/filemanager/createdirectory'); const deleteDirectory = require('../utils/filemanager/deletedirectory'); const uuidv4 = require('uuid').v4; const Collections = require('../store/collection'); const { parseOpenAPISpec } = require('../utils/collection'); const { isDirectory, pathExists } = require('../utils/filemanager/filesystem'); const createFile = require('../utils/filemanager/createfile'); const updateFile = require('../utils/filemanager/updatefile'); const deleteFile = require('../utils/filemanager/deletefile'); const readFile = require('../utils/filemanager/readfile'); const FlowtestAI = require('../ai/flowtestai'); const { stringify, parse } = require('flatted'); const { deserialize, serialize } = require('../utils/flowparser/parser'); const { axiosClient } = require('./axiosClient'); const FormData = require('form-data'); const { extend, cloneDeep } = require('lodash'); const collectionStore = new Collections(); const flowTestAI = new FlowtestAI(); const timeout = 60000; const newAbortSignal = () => { const abortController = new AbortController(); setTimeout(() => abortController.abort(), timeout || 0); return abortController.signal; }; /** web platform: blob. */ const convertBase64ToBlob = async (base64) => { const response = await fetch(base64); const blob = await response.blob(); return blob; }; const registerRendererEventHandlers = (mainWindow, watcher) => { ipcMain.handle('renderer:open-directory-selection-dialog', async (event, arg) => { try { const result = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory'], }); return result.filePaths[0]; } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:browser-window-ready', async (event) => { const savedCollections = collectionStore.getAll(); for (let i = 0; i < savedCollections.length; i++) { if (isDirectory(savedCollections[i].pathname)) { mainWindow.webContents.send( 'main:collection-created', savedCollections[i].id, path.basename(savedCollections[i].pathname), savedCollections[i].pathname, savedCollections[i].nodes, ); watcher.addWatcher(mainWindow, savedCollections[i].pathname, savedCollections[i].id); } else { collectionStore.remove(savedCollections[i]); } } }); ipcMain.handle('renderer:create-collection', async (event, openAPISpecFilePath, collectionFolderPath) => { try { const spec = fs.readFileSync(openAPISpecFilePath, 'utf8'); // async/await syntax let api = await SwaggerParser.validate(openAPISpecFilePath); // console.log("API name: %s, Version: %s", api.info.title, api.info.version); // resolve references in openapi spec const resolvedSpec = await JsonRefs.resolveRefs(api); const parsedNodes = parseOpenAPISpec(resolvedSpec.resolved); const id = uuidv4(); const collectionName = api.info.title; const pathname = path.join(collectionFolderPath, collectionName); const newCollection = { id: id, name: collectionName, pathname: pathname, openapi_spec: stringify(resolvedSpec.resolved), nodes: parsedNodes, }; const result = createDirectory(newCollection.name, collectionFolderPath); console.log(`Created directory: ${result}`); createDirectory('environments', pathname); mainWindow.webContents.send('main:collection-created', id, path.basename(pathname), pathname, parsedNodes); watcher.addWatcher(mainWindow, pathname, id); collectionStore.add(newCollection); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:open-collection', async (event, openAPISpecFilePath, collectionFolderPath) => { try { if (isDirectory(collectionFolderPath)) { // async/await syntax const api = await SwaggerParser.validate(openAPISpecFilePath); // console.log("API name: %s, Version: %s", api.info.title, api.info.version); // resolve references in openapi spec const resolvedSpec = await JsonRefs.resolveRefs(api); const parsedNodes = parseOpenAPISpec(resolvedSpec.resolved); const id = uuidv4(); const collectionName = api.info.title; const newCollection = { id: id, name: collectionName, pathname: collectionFolderPath, openapi_spec: stringify(resolvedSpec.resolved), nodes: parsedNodes, }; mainWindow.webContents.send('main:collection-created', id, collectionName, collectionFolderPath, parsedNodes); watcher.addWatcher(mainWindow, collectionFolderPath, id); collectionStore.add(newCollection); } else { return Promise.reject(new Error(`Directory: ${collectionFolderPath} does not exist`)); } } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:delete-collection', async (event, collection) => { try { // deleteDirectory(collection.pathname); // console.log(`Deleted directory: ${collection.pathname}`); mainWindow.webContents.send('main:collection-deleted', collection.id); watcher.removeWatcher(collection.pathname); collectionStore.remove(collection); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:create-folder', async (event, name, path) => { try { const result = createDirectory(name, path); console.log(`Created directory: ${result}`); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:delete-folder', async (event, path) => { try { const result = deleteDirectory(path); console.log(`Deleted directory: ${path}`); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:create-environment', async (event, collectionPath, name) => { try { const envDir = path.join(collectionPath, 'environments'); if (!isDirectory(envDir)) { createDirectory('environments', collectionPath); } const result = createFile(`${name}.env`, envDir, ''); console.log(`Created file: ${name}.env`); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:update-environment', async (event, collectionPath, name, variables) => { try { const env = Object.entries(variables) .map(([key, value]) => `${key}: "${value}"`) .join('\n'); const envDir = path.join(collectionPath, 'environments'); updateFile(path.join(envDir, name), env); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:delete-environment', async (event, collectionPath, name) => { try { const envDir = path.join(collectionPath, 'environments'); deleteFile(path.join(envDir, name)); console.log(`Delete file: ${name}`); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:create-dotenv', async (event, collectionPath, content) => { try { createFile('.env', collectionPath, content || ''); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:addOrUpdate-dotEnvironment', async (event, collectionPath, variables) => { try { const pathname = path.join(collectionPath, '.env'); // variables should be of format `k1=v1\nk2=v2`; updateFile(pathname, variables); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:create-flowtest', async (event, name, path, flowData) => { try { if (isDirectory(path)) { const textData = deserialize(flowData); createFile(`${name}.flow`, path, JSON.stringify(textData, null, 4)); console.log(`Created file: ${name}.flow`); } } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:clone-flowtest', async (event, name, flowtestPath) => { try { const content = readFile(flowtestPath); createFile(`${name}.flow`, path.dirname(flowtestPath), content); console.log(`Cloned file: ${name}.flow`); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:read-flowtest', async (event, pathname, collectionId) => { try { const content = readFile(pathname); const flowData = serialize(JSON.parse(content)); mainWindow.webContents.send('main:read-flowtest', pathname, collectionId, flowData); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:read-flowtest-sync', (event, pathname) => { const content = readFile(pathname); const flowData = serialize(JSON.parse(content)); return flowData; }); ipcMain.handle('renderer:update-flowtest', async (event, pathname, flowData) => { try { const textData = deserialize(flowData); updateFile(pathname, JSON.stringify(textData, null, 4)); console.log(`Updated file: ${pathname}`); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:delete-flowtest', async (event, pathname) => { try { deleteFile(pathname); console.log(`Delete file: ${pathname}`); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:run-http-request', async (event, request, collectionPath) => { let requestSent; try { if (request.headers['content-type'] === 'multipart/form-data') { const formData = new FormData(); const params = request.data; await params.map(async (param, index) => { if (param.type === 'text') { formData.append(param.key, param.value); } if (param.type === 'file') { let trimmedFilePath = param.value.trim(); if (!path.isAbsolute(trimmedFilePath)) { trimmedFilePath = path.join(collectionPath, trimmedFilePath); } formData.append(param.key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); } }); request.data = formData; extend(request.headers, formData.getHeaders()); } requestSent = { url: request.url, method: request.method, headers: request.headers, // form data obj gets serialized here so that it can be sent over wire // otherwise ipc communication errors out data: request.data ? JSON.parse(JSON.stringify(request.data)) : request.data, }; const result = await axios({ ...request, signal: newAbortSignal(), }); return { request: requestSent, response: { status: result.status, statusText: result.statusText, data: result.data, headers: result.headers, }, }; } catch (error) { if (error?.response) { return { request: requestSent, response: { error: { status: error.response.status, statusText: error.response.statusText, data: error.response.data, headers: error.response.headers, }, }, }; } else { return { request: requestSent, response: { error: { status: '', statusText: '', data: `An error occurred while running the request : ${error?.message}`, }, }, }; } } }); ipcMain.handle('renderer:generate-nodes-ai', async (event, instruction, collectionId, model) => { try { const collection = collectionStore.getAll().find((c) => c.id === collectionId); if (collection) { return await flowTestAI.generate(parse(collection.openapi_spec), instruction, model); } else { return Promise.reject(new Error('Collection not found')); } } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:upload-logs', async (event, name, config, status, time, logs) => { function bytesToBase64(bytes) { const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join(''); return btoa(binString); } try { const data = { scan_metadata: { version: 1, name, status, time, }, scan: bytesToBase64(new TextEncoder().encode(JSON.stringify(logs))), }; try { const response = await axiosClient(config.hostUrl, config.accessId, config.accessKey).post('/upload', data); return { upload: 'success', url: `${config.hostUrl}/scan/${response.data.data[0].id}`, }; } catch (error) { if (error?.response) { if (error.response?.status >= 400 && error.response?.status < 500) { return { upload: 'fail', message: 'Unable to upload flow scan', reason: `${JSON.stringify(error.response?.data)}`, }; } if (error.response?.status === 500) { return { upload: 'fail', message: 'Unable to upload flow scan', reason: 'Internal Server Error', }; } } return { upload: 'fail', message: 'Unable to upload flow scan', }; } } catch (error) { return Promise.reject(error); } }); }; module.exports = registerRendererEventHandlers; ================================================ FILE: packages/flowtest-electron/src/ipc/settings.js ================================================ const { ipcMain, shell, dialog, app } = require('electron'); const Settings = require('../store/settings'); const settingsStore = new Settings(); const registerSettingsEventHandlers = (mainWindow) => { ipcMain.handle('renderer:settings-window-ready', async (event) => { const savedSettings = settingsStore.getAll(); mainWindow.webContents.send('main:saved-settings', savedSettings); }); ipcMain.handle('renderer:add-logsyncconfig', async (event, config) => { try { settingsStore.addLogSyncConfig(config.enabled, config.hostUrl, config.accessId, config.accessKey); const savedSettings = settingsStore.getAll(); mainWindow.webContents.send('main:saved-settings', savedSettings); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:add-genAIUsageDisclaimer', async (event, accepted) => { try { settingsStore.addGenAIUsageDisclaimer(accepted); const savedSettings = settingsStore.getAll(); mainWindow.webContents.send('main:saved-settings', savedSettings); } catch (error) { return Promise.reject(error); } }); }; module.exports = registerSettingsEventHandlers; ================================================ FILE: packages/flowtest-electron/src/store/collection.js ================================================ const Store = require('electron-store'); const { isDirectory } = require('../utils/filemanager/filesystem'); class Collections { constructor() { this.store = new Store(); } add(collection) { const collections = this.store.get('collections') || []; if (isDirectory(collection.pathname)) { if (!collections.find((c) => c.pathname === collection.pathname)) { collections.push(collection); this.store.set('collections', collections); } } } remove(collection) { const collections = this.store.get('collections') || []; if (collections.find((c) => c.id === collection.id)) { this.store.set( 'collections', collections.filter((c) => c.pathname !== collection.pathname), ); } } getAll() { return this.store.get('collections') || []; } removeAll() { return this.store.set('collections', []); } } module.exports = Collections; ================================================ FILE: packages/flowtest-electron/src/store/settings.js ================================================ const Store = require('electron-store'); class Settings { constructor() { this.store = new Store(); } addLogSyncConfig(enabled, hostUrl, accessId, accessKey) { this.store.set('logSyncConfig', { enabled, hostUrl, accessId, accessKey }); } addGenAIUsageDisclaimer(accepted) { this.store.set('genAIUsageDisclaimer', accepted); } getAll() { return { logSyncConfig: this.store.get('logSyncConfig') || {}, genAIUsageDisclaimer: this.store.get('genAIUsageDisclaimer') || false, }; } clearAll() { this.store.set('logSyncConfig', {}); this.store.set('genAIUsageDisclaimer', false); } } module.exports = Settings; ================================================ FILE: packages/flowtest-electron/src/utils/collection.js ================================================ const { generateRequestBodyExample } = require('./generate-request-body'); const { generateQueryParamsExample, generateParameterExample, generatePathParamsExample, } = require('./generate-request-parameters'); const computeUrl = (baseUrl, path) => { if (baseUrl.charAt(baseUrl.length - 1) === '/' && path.charAt(0) === '/') { return baseUrl + path.substring(1, path.length); } else if (baseUrl.charAt(baseUrl.length - 1) !== '/' && path.charAt(0) !== '/') { return baseUrl + '/' + path; } else { return baseUrl + path; } }; const replaceSingleToDoubleCurlyBraces = (str) => { // Replace opening curly braces str = str.replace(/{/g, '{{'); // Replace closing curly braces str = str.replace(/}/g, '}}'); return str; }; const parseOpenAPISpec = (collection) => { let parsedNodes = []; try { // servers is array,, figure case where there can be multiple servers const baseUrl = collection['servers'].length > 1 ? '{baseUrl}' : collection['servers'][0]['url']; Object.entries(collection['paths']).map(([path, operations], _) => { const commonParameters = Object.prototype.hasOwnProperty.call(operations, 'parameters') ? operations['parameters'] : []; const { parameters, ...operationsFiltered } = operations; Object.entries(operationsFiltered).map(([requestType, request], _) => { const summary = request['summary']; const operationId = request['operationId']; const tags = request['tags']; var url = replaceSingleToDoubleCurlyBraces(computeUrl(baseUrl, path)); var variables = {}; var requestBody = {}; const pathParameters = []; const queryParameters = []; const requestParameters = commonParameters.map((obj) => { if (request['parameters']) { // Find the object in the second array that has the same id as the current object const objFromArr2 = request['parameters'].find((o) => o.name === obj.name && o.in === obj.in); // If found, merge the two objects, otherwise return the original object return objFromArr2 ? { ...obj, ...objFromArr2 } : obj; } else { return obj; } }); if (request['parameters']) { // Add any objects from the second array that do not exist in the first array request['parameters'].forEach((obj) => { if (!commonParameters.some((o) => o.name === obj.name && o.in === obj.in)) { requestParameters.push(obj); } }); } if (requestParameters.length > 0) { let firstQueryParam = true; requestParameters.map((value, _) => { if (value['in'] === 'query') { if (firstQueryParam) { url = url.concat(`?${value['name']}={{${value['name']}}}`); firstQueryParam = false; } else { url = url.concat(`&${value['name']}={{${value['name']}}}`); } queryParameters.push(value); } if (value['in'] === 'path') { pathParameters.push(value); } }); } if (queryParameters.length > 0) { const res = generateQueryParamsExample(queryParameters); Array.from(res.entries()).map(([key, value], index) => { variables[key] = { type: typeof value, value, }; }); } if (pathParameters.length > 0) { const res = generatePathParamsExample(pathParameters); Array.from(res.entries()).map(([key, value], index) => { variables[key] = { type: typeof value, value, }; }); } if (request['requestBody']) { if (request['requestBody']['content']['application/json']) { requestBody = { type: 'raw-json', body: JSON.stringify( generateRequestBodyExample(request['requestBody']['content']['application/json']['schema']), ), }; } if (request['requestBody']['content']['multipart/form-data']) { requestBody = { type: 'form-data', body: [], }; } } const finalNode = { url: url, description: summary, operationId: operationId, requestType: requestType.toUpperCase(), tags: tags, requestBody, preReqVars: variables, }; parsedNodes.push(finalNode); }); }); } catch (err) { console.error(err); } return parsedNodes; }; module.exports = { parseOpenAPISpec, }; ================================================ FILE: packages/flowtest-electron/src/utils/collection.test.js ================================================ const { generateRequestBodyExample } = require('./generate-request-body.js'); const { generatePathParamsExample, generateQueryParamsExample } = require('./generate-request-parameters.js'); describe('collection parser', () => { it('should generate request body example', () => { console.log(JSON.stringify(generateRequestBodyExample(userSchema), null, 2)); console.log(JSON.stringify(generateRequestBodyExample(productSchema), null, 2)); console.log(JSON.stringify(generateRequestBodyExample(complexSchema), null, 2)); }); it('should generate request parameters example', () => { console.log('Path Parameters Example:', generatePathParamsExample(pathParameters).toString()); console.log('Query Parameters Example:', generateQueryParamsExample(queryParameters).toString()); }); }); const userSchema = { type: 'object', properties: { id: { type: 'integer', format: 'int64', example: 1, minimum: 1, }, name: { type: 'string', example: 'John Doe', minLength: 3, maxLength: 20, }, email: { type: 'string', format: 'email', example: 'john.doe@example.com', }, birthdate: { type: 'string', format: 'date', example: '1990-01-01', }, website: { type: 'string', format: 'uri', example: 'https://johndoe.com', }, role: { type: 'string', enum: ['admin', 'user', 'guest'], example: 'user', }, username: { type: 'string', pattern: '^[a-zA-Z0-9]{3,}$', example: 'user123', }, interests: { type: 'array', items: { type: 'string', }, example: ['coding', 'reading'], }, }, }; const productSchema = { type: 'object', properties: { id: { type: 'integer', format: 'int32', example: 101, minimum: 1, maximum: 1000, }, name: { type: 'string', example: 'Sample Product', minLength: 3, }, price: { type: 'number', format: 'double', example: 19.99, minimum: 0, maximum: 1000, }, tags: { type: 'array', items: { type: 'string', example: 'tag1', }, }, status: { type: 'string', enum: ['available', 'out of stock', 'discontinued'], example: 'available', }, releaseDate: { type: 'string', format: 'date-time', example: '2023-01-01T00:00:00Z', }, }, }; const complexSchema = { type: 'object', properties: { name: { type: 'string', example: 'Complex Example', }, detail: { oneOf: [ { type: 'string', example: 'OneOf String' }, { type: 'integer', example: 42 }, ], }, options: { anyOf: [ { type: 'boolean', example: true }, { type: 'string', example: 'AnyOf String' }, ], }, allDetails: { allOf: [ { type: 'object', properties: { part1: { type: 'string', example: 'Part 1', }, }, }, { type: 'object', properties: { part2: { type: 'number', example: 123.45, }, }, }, ], }, }, }; // Example usage: const pathParameters = [ { name: 'userId', in: 'path', required: true, schema: { type: 'integer', format: 'int64', example: 123, minimum: 1, }, }, { name: 'username', in: 'path', required: true, schema: { type: 'string', minLength: 3, maxLength: 20, example: 'john_doe', }, }, ]; const queryParameters = [ { name: 'page', in: 'query', schema: { type: 'integer', example: 1, minimum: 1, }, }, { name: 'limit', in: 'query', schema: { type: 'integer', example: 10, minimum: 1, maximum: 100, }, }, { name: 'sort', in: 'query', schema: { type: 'string', enum: ['asc', 'desc'], example: 'asc', }, }, { name: 'filter', in: 'query', schema: { type: 'array', items: { type: 'string', example: 'status:active', }, }, }, { name: 'search', in: 'query', schema: { type: 'string', example: 'example', }, }, ]; ================================================ FILE: packages/flowtest-electron/src/utils/filemanager/createdirectory.js ================================================ const fs = require('fs'); const { isDirectory, pathExists } = require('./filesystem'); const path = require('path'); const createDirectory = (name, basePath) => { // now validate the name and path if (!name) { throw new Error('Directory name is required'); } if (!basePath) { throw new Error('Directory path is required'); } // check if the directory exists if (!isDirectory(basePath)) { throw new Error('Path is not a directory'); } // check if the directory already exists const directoryPath = path.join(basePath, name); if (isDirectory(directoryPath)) { throw new Error('The directory already exists'); } // now create the directory return fs.mkdirSync(directoryPath, { mode: 0o777, recursive: true, }); }; module.exports = createDirectory; ================================================ FILE: packages/flowtest-electron/src/utils/filemanager/createfile.js ================================================ const fs = require('fs'); const dpath = require('path'); const { isDirectory, pathExists } = require('./filesystem'); const createFile = (name, path, content) => { // now validate the name and path if (!name) { throw new Error('File name is required'); } if (!path) { throw new Error('Directory path is required'); } // check if the directory exists if (!isDirectory(path)) { throw new Error('Path is not a directory'); } // check if the file already exists const filePath = dpath.join(path, name); if (pathExists(filePath)) { throw new Error(`File already exists ${filePath}`); } // now create the file return fs.writeFileSync(filePath, String(content), 'utf8'); }; module.exports = createFile; ================================================ FILE: packages/flowtest-electron/src/utils/filemanager/deletedirectory.js ================================================ const fs = require('fs'); const { isDirectory } = require('./filesystem'); const deleteDirectory = (path) => { if (!path) { throw new Error('Directory path is required'); } // check if the directory exists if (!isDirectory(path)) { throw new Error('Path is not a directory'); } // now delete the directory return fs.rmSync(path, { recursive: true, force: true }); }; module.exports = deleteDirectory; ================================================ FILE: packages/flowtest-electron/src/utils/filemanager/deletefile.js ================================================ const fs = require('fs'); const { pathExists } = require('./filesystem'); const deleteFile = (path) => { if (!path) { throw new Error('File path is required'); } // check if file exists if (!pathExists(path)) { throw new Error('File does not exist'); } // now delete the file return fs.rmSync(path, { recursive: true, force: true }); }; module.exports = deleteFile; ================================================ FILE: packages/flowtest-electron/src/utils/filemanager/filesystem.js ================================================ const fs = require('fs'); const path = require('path'); /** * Determine if the given path is directory */ const isDirectory = (path) => { try { return fs.existsSync(path) && fs.lstatSync(path).isDirectory(); } catch (e) { // lstatSync throws an error if path doesn't exist return false; } }; /** * Determine if the given path exists */ const pathExists = (path) => { try { fs.accessSync(path); return true; } catch (error) { return false; } }; const getSubdirectoriesFromRoot = (rootPath, pathname) => { // convert to unix style path pathname = slash(pathname); rootPath = slash(rootPath); const relativePath = path.relative(rootPath, pathname); return relativePath ? relativePath.split(path.sep) : []; }; const getDirectoryName = (pathname) => { // convert to unix style path pathname = slash(pathname); return path.dirname(pathname); }; const isWindowsOS = () => { return process.platform === 'win32'; }; const isMacOS = () => { return process.platform === 'darwin'; }; const PATH_SEPARATOR = isWindowsOS() ? '\\' : '/'; const slash = (path) => { const isExtendedLengthPath = /^\\\\\?\\/.test(path); if (isExtendedLengthPath) { return path; } return path.replace(/\\/g, '/'); }; module.exports = { isDirectory, pathExists, getSubdirectoriesFromRoot, getDirectoryName, isMacOS, PATH_SEPARATOR, }; ================================================ FILE: packages/flowtest-electron/src/utils/filemanager/readfile.js ================================================ const fs = require('fs'); const { pathExists } = require('./filesystem'); const readFile = (path) => { if (!path) { throw new Error('File path is required'); } // check if file exists if (!pathExists(path)) { throw new Error('File does not exist'); } // now delete the file return fs.readFileSync(path, 'utf8'); }; module.exports = readFile; ================================================ FILE: packages/flowtest-electron/src/utils/filemanager/updatefile.js ================================================ const fs = require('fs'); const { pathExists } = require('./filesystem'); const upadateFile = (path, content) => { if (!path) { throw new Error('File path is required'); } // check if file exists if (!pathExists(path)) { throw new Error('File does not exist'); } // now update the file return fs.writeFileSync(path, String(content), 'utf8'); }; module.exports = upadateFile; ================================================ FILE: packages/flowtest-electron/src/utils/flowparser/AssertNode.js ================================================ const { Node } = require('./Node'); class AssertNode extends Node { constructor() { super('assertNode'); } serialize(id, data, metadata) { return { id, type: this.type, data, ...metadata, }; } deserialize(node) { const id = node.id; const data = node.data; delete node.id; delete node.data; const metadata = node; return { id, data, metadata, }; } } module.exports = { AssertNode, }; ================================================ FILE: packages/flowtest-electron/src/utils/flowparser/AuthNode.js ================================================ const { Node } = require('./Node'); class AuthNode extends Node { constructor() { super('authNode'); } serialize(id, data, metadata) { return { id, type: this.type, data, ...metadata, }; } deserialize(node) { const id = node.id; const data = node.data; delete node.id; delete node.data; const metadata = node; return { id, data, metadata, }; } } module.exports = { AuthNode, }; ================================================ FILE: packages/flowtest-electron/src/utils/flowparser/DelayNode.js ================================================ const { Node } = require('./Node'); class DelayNode extends Node { constructor() { super('delayNode'); } serialize(id, data, metadata) { return { id, type: this.type, data, ...metadata, }; } deserialize(node) { const id = node.id; const data = node.data; delete node.id; delete node.data; const metadata = node; return { id, data, metadata, }; } } module.exports = { DelayNode, }; ================================================ FILE: packages/flowtest-electron/src/utils/flowparser/NestedFlowNode.js ================================================ const { Node } = require('./Node'); class NestedFlowNode extends Node { constructor() { super('flowNode'); } serialize(id, data, metadata) { return { id, type: this.type, data, ...metadata, }; } deserialize(node) { const id = node.id; const data = node.data; delete node.id; delete node.data; const metadata = node; return { id, data, metadata, }; } } module.exports = { NestedFlowNode, }; ================================================ FILE: packages/flowtest-electron/src/utils/flowparser/Node.js ================================================ class Node { constructor(type) { this.type = type; } serialize(id, data, metadata) { throw new Error('Serialize method must be implemented by subclasses'); } deserialize(node) { throw new Error('Deserialize method must be implemented by subclasses'); } } module.exports = { Node, }; ================================================ FILE: packages/flowtest-electron/src/utils/flowparser/OutputNode.js ================================================ const { Node } = require('./Node'); class OutputNode extends Node { constructor() { super('outputNode'); } serialize(id, data, metadata) { return { id, type: this.type, data, ...metadata, }; } deserialize(node) { const id = node.id; const { ['output']: _, ...data } = node.data; delete node.id; delete node.data; const metadata = node; return { id, data, metadata, }; } } module.exports = { OutputNode, }; ================================================ FILE: packages/flowtest-electron/src/utils/flowparser/RequestNode.js ================================================ const { Node } = require('./Node'); class RequestNode extends Node { constructor() { super('requestNode'); } serialize(id, data, metadata) { return { id, type: this.type, data, ...metadata, }; } deserialize(node) { const id = node.id; const data = node.data; delete node.id; delete node.data; const metadata = node; return { id, data, metadata, }; } } module.exports = { RequestNode, }; ================================================ FILE: packages/flowtest-electron/src/utils/flowparser/SetVarNode.js ================================================ const { Node } = require('./Node'); class SetVarNode extends Node { constructor() { super('setVarNode'); } serialize(id, data, metadata) { return { id, type: this.type, data, ...metadata, }; } deserialize(node) { const id = node.id; const data = node.data; delete node.id; delete node.data; const metadata = node; return { id, data, metadata, }; } } module.exports = { SetVarNode, }; ================================================ FILE: packages/flowtest-electron/src/utils/flowparser/StartNode.js ================================================ const { Node } = require('./Node'); class StartNode extends Node { constructor() { super('startNode'); } serialize(id, data, metadata) { return { id, ...metadata, }; } deserialize(node) { const id = node.id; delete node.id; const metadata = node; return { id, metadata, }; } } module.exports = { StartNode, }; ================================================ FILE: packages/flowtest-electron/src/utils/flowparser/parser.js ================================================ const { cloneDeep } = require('lodash'); const { AuthNode } = require('./AuthNode'); const { NestedFlowNode } = require('./NestedFlowNode'); const { DelayNode } = require('./DelayNode'); const { AssertNode } = require('./AssertNode'); const { OutputNode } = require('./OutputNode'); const { RequestNode } = require('./RequestNode'); const { StartNode } = require('./StartNode'); const { SetVarNode } = require('./SetVarNode'); const VERSION = 1; const deserialize = (flowData) => { // we don't want to modify original object const flowDataCopy = cloneDeep(flowData); const textData = {}; textData.version = VERSION; textData.graph = {}; if (flowData) { if (flowData.nodes) { const nodes = flowDataCopy.nodes; textData.graph.data = {}; textData.graph.data.nodes = {}; textData.graph.metadata = {}; textData.graph.metadata.nodes = {}; nodes.forEach((node) => { if (node.type === 'startNode') { const sNode = new StartNode(); const result = sNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'startNode', }; textData.graph.metadata.nodes[result.id] = { type: 'startNode', ...result.metadata, }; } if (node.type === 'authNode') { const aNode = new AuthNode(); const result = aNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'authNode', auth: result.data, }; textData.graph.metadata.nodes[result.id] = { type: 'authNode', ...result.metadata, }; } if (node.type === 'requestNode') { const rNode = new RequestNode(); const result = rNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'requestNode', ...result.data, }; textData.graph.metadata.nodes[result.id] = { type: 'requestNode', ...result.metadata, }; } if (node.type === 'outputNode') { const oNode = new OutputNode(); const result = oNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'outputNode', ...result.data, }; textData.graph.metadata.nodes[result.id] = { type: 'outputNode', ...result.metadata, }; } if (node.type === 'delayNode') { const dNode = new DelayNode(); const result = dNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'delayNode', ...result.data, }; textData.graph.metadata.nodes[result.id] = { type: 'delayNode', ...result.metadata, }; } if (node.type === 'assertNode') { const eNode = new AssertNode(); const result = eNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'assertNode', ...result.data, }; textData.graph.metadata.nodes[result.id] = { type: 'assertNode', ...result.metadata, }; } if (node.type === 'flowNode') { const fNode = new NestedFlowNode(); const result = fNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'flowNode', ...result.data, }; textData.graph.metadata.nodes[result.id] = { type: 'flowNode', ...result.metadata, }; } if (node.type === 'setVarNode') { const sNode = new SetVarNode(); const result = sNode.deserialize(node); textData.graph.data.nodes[result.id] = { type: 'setVarNode', ...result.data, }; textData.graph.metadata.nodes[result.id] = { type: 'setVarNode', ...result.metadata, }; } }); } if (flowData.edges) { const edges = flowDataCopy.edges; textData.graph.data.edges = []; textData.graph.metadata.edges = {}; edges.forEach((edge) => { textData.graph.data.edges.push(`${edge.source} -> ${edge.target}`); const { ['id']: _, ..._edge } = edge; textData.graph.metadata.edges[edge.id] = _edge; }); } if (flowData.viewport) { textData.graph.metadata.viewport = flowDataCopy.viewport; } } return textData; }; const serialize = (textData) => { const flowData = {}; flowData.nodes = []; flowData.edges = []; flowData.viewport = { x: 0, y: 0, zoom: 1 }; // we don't want to modify original object const textDataCopy = cloneDeep(textData); const version = textDataCopy.version; if (version === 1) { if (textDataCopy.graph.data) { Object.entries(textDataCopy.graph.data.nodes).map(([key, value], index) => { const id = key; if (value.type === 'startNode') { const metadata = textDataCopy.graph.metadata.nodes[id]; const sNode = new StartNode(); const result = sNode.serialize(id, undefined, metadata); flowData.nodes.push(result); } if (value.type === 'authNode') { const data = value.auth; const metadata = textDataCopy.graph.metadata.nodes[id]; const aNode = new AuthNode(); const result = aNode.serialize(id, data, metadata); flowData.nodes.push(result); } if (value.type === 'requestNode') { const data = value; const metadata = textDataCopy.graph.metadata.nodes[id]; const rNode = new RequestNode(); const result = rNode.serialize(id, data, metadata); flowData.nodes.push(result); } if (value.type === 'outputNode') { const data = value; const metadata = textDataCopy.graph.metadata.nodes[id]; const oNode = new OutputNode(); const result = oNode.serialize(id, data, metadata); flowData.nodes.push(result); } if (value.type === 'delayNode') { const data = value; const metadata = textDataCopy.graph.metadata.nodes[id]; const dNode = new DelayNode(); const result = dNode.serialize(id, data, metadata); flowData.nodes.push(result); } if (value.type === 'assertNode') { const data = value; const metadata = textDataCopy.graph.metadata.nodes[id]; const dNode = new AssertNode(); const result = dNode.serialize(id, data, metadata); flowData.nodes.push(result); } if (value.type === 'flowNode') { const data = value; const metadata = textDataCopy.graph.metadata.nodes[id]; const fNode = new NestedFlowNode(); const result = fNode.serialize(id, data, metadata); flowData.nodes.push(result); } if (value.type === 'setVarNode') { const data = value; const metadata = textDataCopy.graph.metadata.nodes[id]; const cNode = new SetVarNode(); const result = cNode.serialize(id, data, metadata); flowData.nodes.push(result); } }); Object.entries(textDataCopy.graph.metadata.edges).map(([key, value], index) => { flowData.edges.push({ id: key, ...value, }); }); if (textDataCopy.graph.metadata.viewport) { flowData.viewport = textDataCopy.graph.metadata.viewport; } } } else { throw new Error('Version not recognized'); } return flowData; }; module.exports = { deserialize, serialize, }; ================================================ FILE: packages/flowtest-electron/src/utils/generate-request-body.js ================================================ const generateRequestBodyExample = (schema, level = 0, context = { processedSchemas: new Set() }) => { if (!schema) return {}; if (schema.example !== undefined) { return schema.example; } if (schema.enum) { return schema.example || schema.enum[0]; } if (schema.oneOf) { return generateRequestBodyExample(schema.oneOf[0], level + 1, context); } if (schema.anyOf) { return generateRequestBodyExample(schema.anyOf[0], level + 1, context); } if (schema.allOf) { return generateAllOfExample(schema.allOf, level + 1, context); } switch (schema.type) { case 'object': return generateObjectExample(schema, level + 1, context); case 'array': return generateArrayExample(schema, level + 1, context); case 'string': return generateStringExample(schema); case 'integer': return generateIntegerExample(schema); case 'number': return generateNumberExample(schema); case 'boolean': return schema.example || true; default: return schema.example || null; } }; const generateAllOfExample = (schemas, level, context) => { const example = {}; schemas.forEach((subSchema) => { const subExample = generateRequestBodyExample(subSchema, level, context); Object.assign(example, subExample); }); return example; }; const generateObjectExample = (schema, level, context) => { if (schema.example !== undefined) { return schema.example; } if (context.processedSchemas.has(schema) && level > 1) { return {}; } context.processedSchemas.add(schema); const example = {}; const properties = schema.properties || {}; for (const [key, propertySchema] of Object.entries(properties)) { example[key] = generateRequestBodyExample(propertySchema, level, context); } if (schema.additionalProperties) { example.additionalProperty1 = generateRequestBodyExample(schema.additionalProperties, level, context); example.additionalProperty2 = generateRequestBodyExample(schema.additionalProperties, level, context); } context.processedSchemas.delete(schema); return example; }; const generateArrayExample = (schema, level, context) => { if (schema.example !== undefined) { return schema.example; } const itemsSchema = schema.items || {}; return [generateRequestBodyExample(itemsSchema, level, context)]; }; const generateStringExample = (schema) => { let example = String(schema.example || 'string'); if (schema.minLength || schema.maxLength) { example = generateStringWithLengthConstraints(example, schema.minLength, schema.maxLength); } switch (schema.format) { case 'date-time': return schema.example || new Date().toISOString(); case 'date': return schema.example || new Date().toISOString().split('T')[0]; case 'email': return schema.example || 'example@example.com'; case 'uuid': return schema.example || '123e4567-e89b-12d3-a456-426614174000'; case 'uri': return schema.example || 'https://example.com'; case 'hostname': return schema.example || 'example.com'; case 'ipv4': return schema.example || '192.168.0.1'; case 'ipv6': return schema.example || '2001:0db8:85a3:0000:0000:8a2e:0370:7334'; case 'byte': return schema.example || btoa('example'); case 'binary': return schema.example || 'binary data'; case 'password': return schema.example || 'password'; default: return example; } }; const generateStringWithLengthConstraints = (str, minLength, maxLength) => { if (minLength) { while (str.length < minLength) { str += 'a'; } } if (maxLength) { str = str.substring(0, maxLength); } return str; }; const generateIntegerExample = (schema) => { const min = schema.minimum || 0; const max = schema.maximum || min + 100; return schema.example || Math.floor(Math.random() * (max - min + 1)) + min; }; const generateNumberExample = (schema) => { const min = schema.minimum || 0.0; const max = schema.maximum || min + 100.0; return schema.example || Math.random() * (max - min) + min; }; module.exports = { generateRequestBodyExample, }; ================================================ FILE: packages/flowtest-electron/src/utils/generate-request-parameters.js ================================================ function generateParameterExample(parameter) { if (!parameter.schema) return {}; const schema = parameter.schema; if (schema.enum) { return schema.example || schema.enum[0]; } switch (schema.type) { case 'string': return generateStringExample(schema); case 'integer': return generateIntegerExample(schema); case 'number': return generateNumberExample(schema); case 'boolean': return schema.example || true; case 'array': return generateArrayExample(schema); default: return schema.example || null; } } function generateStringExample(schema) { let example = schema.example || 'string'; if (schema.minLength || schema.maxLength) { example = generateStringWithLengthConstraints(example, schema.minLength, schema.maxLength); } switch (schema.format) { case 'date-time': return schema.example || new Date().toISOString(); case 'date': return schema.example || new Date().toISOString().split('T')[0]; case 'email': return schema.example || 'example@example.com'; case 'uuid': return schema.example || '123e4567-e89b-12d3-a456-426614174000'; case 'uri': return schema.example || 'https://example.com'; case 'hostname': return schema.example || 'example.com'; case 'ipv4': return schema.example || '192.168.0.1'; case 'ipv6': return schema.example || '2001:0db8:85a3:0000:0000:8a2e:0370:7334'; case 'byte': return schema.example || btoa('example'); case 'binary': return schema.example || 'binary data'; case 'password': return schema.example || 'password'; default: return example; } } function generateStringWithLengthConstraints(str, minLength, maxLength) { if (minLength) { while (str.length < minLength) { str += 'a'; } } if (maxLength) { str = str.substring(0, maxLength); } return str; } function generateIntegerExample(schema) { const min = schema.minimum || 0; const max = schema.maximum || min + 100; return schema.example || Math.floor(Math.random() * (max - min + 1)) + min; } function generateNumberExample(schema) { const min = schema.minimum || 0.0; const max = schema.maximum || min + 100.0; return schema.example || Math.random() * (max - min) + min; } function generateArrayExample(schema) { const itemsSchema = schema.items || {}; return [generateParameterExample({ schema: itemsSchema })]; } const generatePathParamsExample = (parameters) => { const examples = {}; parameters.forEach((param) => { examples[param.name] = generateParameterExample(param); }); return new URLSearchParams(examples); }; const generateQueryParamsExample = (parameters) => { const examples = {}; parameters.forEach((param) => { examples[param.name] = generateParameterExample(param); }); return new URLSearchParams(examples); }; module.exports = { generateParameterExample, generatePathParamsExample, generateQueryParamsExample, }; ================================================ FILE: packages/flowtest-electron/tests/store/collection-store.test.js ================================================ const path = require('path'); const Collections = require('../../src/store/collection'); const createDirectory = require('../../src/utils/filemanager/createdirectory'); const deleteDirectory = require('../../src/utils/filemanager/deletedirectory'); describe('collection-store', () => { it('should create and remove collection', async () => { const store = new Collections(); const newCollection = { id: '1234', name: 'test', pathname: `${__dirname}/test`, openapi_spec: '', nodes: '{}', }; const newestCollection = { id: '12345', name: 'test1', pathname: `${__dirname}/test1`, openapi_spec: '', nodes: '{}', }; store.removeAll(); expect(store.getAll()).toEqual([]); // adding a collection whose directory doesn't exist store.add(newCollection); expect(store.getAll()).toEqual([]); createDirectory('test', __dirname); store.add(newCollection); expect(store.getAll()).toEqual([newCollection]); createDirectory('test1', __dirname); store.add(newestCollection); expect(store.getAll()).toEqual([newCollection, newestCollection]); store.remove(newCollection); expect(store.getAll()).toEqual([newestCollection]); store.remove(newestCollection); expect(store.getAll()).toEqual([]); deleteDirectory(`${__dirname}/test`); deleteDirectory(`${__dirname}/test1`); }); it('collection set should be unique by pathname', async () => { const store = new Collections(); const newCollection = { id: '1234', name: 'test', pathname: `${__dirname}/test`, openapi_spec: '', nodes: '{}', }; const newestCollection = { id: '12345', name: 'test', pathname: `${__dirname}/test`, openapi_spec: '', nodes: '{}', }; store.removeAll(); expect(store.getAll()).toEqual([]); createDirectory('test', __dirname); store.add(newCollection); expect(store.getAll()).toEqual([newCollection]); // collection in the store should be unique by path store.add(newestCollection); expect(store.getAll()).toEqual([newCollection]); store.remove(newCollection); expect(store.getAll()).toEqual([]); deleteDirectory(`${__dirname}/test`); }); }); ================================================ FILE: packages/flowtest-electron/tests/store/settings-store.test.js ================================================ const Settings = require('../../src/store/settings'); describe('settings-store', () => { it('should create and get settings', async () => { const store = new Settings(); store.clearAll(); let settings = store.getAll(); expect(settings.logSyncConfig).toEqual({}); expect(settings.genAIUsageDisclaimer).toEqual(false); // adding a collection whose directory doesn't exist store.addLogSyncConfig(true, 'http://localhost:3000', 'access_id', 'access_key'); store.addGenAIUsageDisclaimer(true); settings = store.getAll(); const config = settings.logSyncConfig; expect(config.enabled).toEqual(true); expect(config.hostUrl).toEqual('http://localhost:3000'); expect(config.accessId).toEqual('access_id'); expect(config.accessKey).toEqual('access_key'); expect(settings.genAIUsageDisclaimer).toEqual(true); }); }); ================================================ FILE: packages/flowtest-electron/tests/test.yaml ================================================ openapi: 3.0.3 info: title: Swagger Petstore - OpenAPI 3.0 description: |- This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! You can now help us improve the API whether it's by making changes to the definition itself or to the code. That way, with time, we can improve the API in general, and expose some of the new features in OAS3. _If you're looking for the Swagger 2.0/OAS 2.0 version of Petstore, then click [here](https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml). Alternatively, you can load via the `Edit > Load Petstore OAS 2.0` menu option!_ Some useful links: - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) termsOfService: http://swagger.io/terms/ contact: email: apiteam@swagger.io license: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html version: 1.0.11 externalDocs: description: Find out more about Swagger url: http://swagger.io servers: - url: https://petstore3.swagger.io/api/v3 tags: - name: pet description: Everything about your Pets externalDocs: description: Find out more url: http://swagger.io - name: store description: Access to Petstore orders externalDocs: description: Find out more about our store url: http://swagger.io - name: user description: Operations about user paths: /pet: put: tags: - pet summary: Update an existing pet description: Update an existing pet by Id operationId: updatePet requestBody: description: Update an existent pet in the store content: application/json: schema: $ref: '#/components/schemas/Pet' application/xml: schema: $ref: '#/components/schemas/Pet' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Pet' required: true responses: '200': description: Successful operation content: application/json: schema: $ref: '#/components/schemas/Pet' application/xml: schema: $ref: '#/components/schemas/Pet' '400': description: Invalid ID supplied '404': description: Pet not found '405': description: Validation exception security: - petstore_auth: - write:pets - read:pets post: tags: - pet summary: Add a new pet to the store description: Add a new pet to the store operationId: addPet requestBody: description: Create a new pet in the store content: application/json: schema: $ref: '#/components/schemas/Pet' application/xml: schema: $ref: '#/components/schemas/Pet' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Pet' required: true responses: '200': description: Successful operation content: application/json: schema: $ref: '#/components/schemas/Pet' application/xml: schema: $ref: '#/components/schemas/Pet' '405': description: Invalid input security: - petstore_auth: - write:pets - read:pets /pet/findByStatus: get: tags: - pet summary: Finds Pets by status description: Multiple status values can be provided with comma separated strings operationId: findPetsByStatus parameters: - name: status in: query description: Status values that need to be considered for filter required: false explode: true schema: type: string default: available enum: - available - pending - sold responses: '200': description: successful operation content: application/json: schema: type: array items: $ref: '#/components/schemas/Pet' application/xml: schema: type: array items: $ref: '#/components/schemas/Pet' '400': description: Invalid status value security: - petstore_auth: - write:pets - read:pets /pet/findByTags: get: tags: - pet summary: Finds Pets by tags description: Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. operationId: findPetsByTags parameters: - name: tags in: query description: Tags to filter by required: false explode: true schema: type: array items: type: string responses: '200': description: successful operation content: application/json: schema: type: array items: $ref: '#/components/schemas/Pet' application/xml: schema: type: array items: $ref: '#/components/schemas/Pet' '400': description: Invalid tag value security: - petstore_auth: - write:pets - read:pets /pet/{petId}: get: tags: - pet summary: Find pet by ID description: Returns a single pet operationId: getPetById parameters: - name: petId in: path description: ID of pet to return required: true schema: type: integer format: int64 responses: '200': description: successful operation content: application/json: schema: $ref: '#/components/schemas/Pet' application/xml: schema: $ref: '#/components/schemas/Pet' '400': description: Invalid ID supplied '404': description: Pet not found security: - api_key: [] - petstore_auth: - write:pets - read:pets post: tags: - pet summary: Updates a pet in the store with form data description: '' operationId: updatePetWithForm parameters: - name: petId in: path description: ID of pet that needs to be updated required: true schema: type: integer format: int64 - name: name in: query description: Name of pet that needs to be updated schema: type: string - name: status in: query description: Status of pet that needs to be updated schema: type: string responses: '405': description: Invalid input security: - petstore_auth: - write:pets - read:pets delete: tags: - pet summary: Deletes a pet description: delete a pet operationId: deletePet parameters: - name: api_key in: header description: '' required: false schema: type: string - name: petId in: path description: Pet id to delete required: true schema: type: integer format: int64 responses: '400': description: Invalid pet value security: - petstore_auth: - write:pets - read:pets /pet/{petId}/uploadImage: post: tags: - pet summary: uploads an image description: '' operationId: uploadFile parameters: - name: petId in: path description: ID of pet to update required: true schema: type: integer format: int64 - name: additionalMetadata in: query description: Additional Metadata required: false schema: type: string requestBody: content: application/octet-stream: schema: type: string format: binary responses: '200': description: successful operation content: application/json: schema: $ref: '#/components/schemas/ApiResponse' security: - petstore_auth: - write:pets - read:pets /store/inventory: get: tags: - store summary: Returns pet inventories by status description: Returns a map of status codes to quantities operationId: getInventory responses: '200': description: successful operation content: application/json: schema: type: object additionalProperties: type: integer format: int32 security: - api_key: [] /store/order: post: tags: - store summary: Place an order for a pet description: Place a new order in the store operationId: placeOrder requestBody: content: application/json: schema: $ref: '#/components/schemas/Order' application/xml: schema: $ref: '#/components/schemas/Order' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/Order' responses: '200': description: successful operation content: application/json: schema: $ref: '#/components/schemas/Order' '405': description: Invalid input /store/order/{orderId}: get: tags: - store summary: Find purchase order by ID description: For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. operationId: getOrderById parameters: - name: orderId in: path description: ID of order that needs to be fetched required: true schema: type: integer format: int64 responses: '200': description: successful operation content: application/json: schema: $ref: '#/components/schemas/Order' application/xml: schema: $ref: '#/components/schemas/Order' '400': description: Invalid ID supplied '404': description: Order not found delete: tags: - store summary: Delete purchase order by ID description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors operationId: deleteOrder parameters: - name: orderId in: path description: ID of the order that needs to be deleted required: true schema: type: integer format: int64 responses: '400': description: Invalid ID supplied '404': description: Order not found /user: post: tags: - user summary: Create user description: This can only be done by the logged in user. operationId: createUser requestBody: description: Created user object content: application/json: schema: $ref: '#/components/schemas/User' application/xml: schema: $ref: '#/components/schemas/User' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/User' responses: default: description: successful operation content: application/json: schema: $ref: '#/components/schemas/User' application/xml: schema: $ref: '#/components/schemas/User' /user/createWithList: post: tags: - user summary: Creates list of users with given input array description: Creates list of users with given input array operationId: createUsersWithListInput requestBody: content: application/json: schema: type: array items: $ref: '#/components/schemas/User' responses: '200': description: Successful operation content: application/json: schema: $ref: '#/components/schemas/User' application/xml: schema: $ref: '#/components/schemas/User' default: description: successful operation /user/login: get: tags: - user summary: Logs user into the system description: '' operationId: loginUser parameters: - name: username in: query description: The user name for login required: false schema: type: string - name: password in: query description: The password for login in clear text required: false schema: type: string responses: '200': description: successful operation headers: X-Rate-Limit: description: calls per hour allowed by the user schema: type: integer format: int32 X-Expires-After: description: date in UTC when token expires schema: type: string format: date-time content: application/xml: schema: type: string application/json: schema: type: string '400': description: Invalid username/password supplied /user/logout: get: tags: - user summary: Logs out current logged in user session description: '' operationId: logoutUser parameters: [] responses: default: description: successful operation /user/{username}: get: tags: - user summary: Get user by user name description: '' operationId: getUserByName parameters: - name: username in: path description: 'The name that needs to be fetched. Use user1 for testing. ' required: true schema: type: string responses: '200': description: successful operation content: application/json: schema: $ref: '#/components/schemas/User' application/xml: schema: $ref: '#/components/schemas/User' '400': description: Invalid username supplied '404': description: User not found put: tags: - user summary: Update user description: This can only be done by the logged in user. operationId: updateUser parameters: - name: username in: path description: name that need to be deleted required: true schema: type: string requestBody: description: Update an existent user in the store content: application/json: schema: $ref: '#/components/schemas/User' application/xml: schema: $ref: '#/components/schemas/User' application/x-www-form-urlencoded: schema: $ref: '#/components/schemas/User' responses: default: description: successful operation delete: tags: - user summary: Delete user description: This can only be done by the logged in user. operationId: deleteUser parameters: - name: username in: path description: The name that needs to be deleted required: true schema: type: string responses: '400': description: Invalid username supplied '404': description: User not found components: schemas: Order: type: object properties: id: type: integer format: int64 example: 10 petId: type: integer format: int64 example: 198772 quantity: type: integer format: int32 example: 7 shipDate: type: string format: date-time status: type: string description: Order Status example: approved enum: - placed - approved - delivered complete: type: boolean xml: name: order Customer: type: object properties: id: type: integer format: int64 example: 100000 username: type: string example: fehguy address: type: array xml: name: addresses wrapped: true items: $ref: '#/components/schemas/Address' xml: name: customer Address: type: object properties: street: type: string example: 437 Lytton city: type: string example: Palo Alto state: type: string example: CA zip: type: string example: '94301' xml: name: address Category: type: object properties: id: type: integer format: int64 example: 1 name: type: string example: Dogs xml: name: category User: type: object properties: id: type: integer format: int64 example: 10 username: type: string example: theUser firstName: type: string example: John lastName: type: string example: James email: type: string example: john@email.com password: type: string example: '12345' phone: type: string example: '12345' userStatus: type: integer description: User Status format: int32 example: 1 xml: name: user Tag: type: object properties: id: type: integer format: int64 name: type: string xml: name: tag Pet: required: - name - photoUrls type: object properties: id: type: integer format: int64 example: 10 name: type: string example: doggie category: $ref: '#/components/schemas/Category' photoUrls: type: array xml: wrapped: true items: type: string xml: name: photoUrl tags: type: array xml: wrapped: true items: $ref: '#/components/schemas/Tag' status: type: string description: pet status in the store enum: - available - pending - sold xml: name: pet ApiResponse: type: object properties: code: type: integer format: int32 type: type: string message: type: string xml: name: '##default' requestBodies: Pet: description: Pet object that needs to be added to the store content: application/json: schema: $ref: '#/components/schemas/Pet' application/xml: schema: $ref: '#/components/schemas/Pet' UserArray: description: List of user object content: application/json: schema: type: array items: $ref: '#/components/schemas/User' securitySchemes: petstore_auth: type: oauth2 flows: implicit: authorizationUrl: https://petstore3.swagger.io/oauth/authorize scopes: write:pets: modify pets in your account read:pets: read your pets api_key: type: apiKey name: api_key in: header ================================================ FILE: packages/flowtest-electron/tests/utils/collection-parser.test.js ================================================ const SwaggerParser = require('@apidevtools/swagger-parser'); const JsonRefs = require('json-refs'); const parseOpenAPISpec = require('../../src/utils/collection'); describe('parse', () => { it('should add do basic parsing', async () => { let api = await SwaggerParser.validate('tests/test.yaml'); console.log('API name: %s, Version: %s', api.info.title, api.info.version); const resolvedSpec = await JsonRefs.resolveRefs(api); const nodes = parseOpenAPISpec(resolvedSpec.resolved); expect(nodes).toEqual(expected); }); }); const expected = [ { url: 'https://petstore3.swagger.io/api/v3/pet', description: 'Update an existing pet', operationId: 'updatePet', requestType: 'PUT', }, { url: 'https://petstore3.swagger.io/api/v3/pet', description: 'Add a new pet to the store', operationId: 'addPet', requestType: 'POST', }, { url: 'https://petstore3.swagger.io/api/v3/pet/findByStatus?status={{status}}', description: 'Finds Pets by status', operationId: 'findPetsByStatus', requestType: 'GET', }, { url: 'https://petstore3.swagger.io/api/v3/pet/findByTags?tags={{tags}}', description: 'Finds Pets by tags', operationId: 'findPetsByTags', requestType: 'GET', }, { url: 'https://petstore3.swagger.io/api/v3/pet/{{petId}}', description: 'Find pet by ID', operationId: 'getPetById', requestType: 'GET', }, { url: 'https://petstore3.swagger.io/api/v3/pet/{{petId}}', description: 'Updates a pet in the store with form data', operationId: 'updatePetWithForm', requestType: 'POST', }, { url: 'https://petstore3.swagger.io/api/v3/pet/{{petId}}', description: 'Deletes a pet', operationId: 'deletePet', requestType: 'DELETE', }, { url: 'https://petstore3.swagger.io/api/v3/pet/{{petId}}/uploadImage', description: 'uploads an image', operationId: 'uploadFile', requestType: 'POST', }, { url: 'https://petstore3.swagger.io/api/v3/store/inventory', description: 'Returns pet inventories by status', operationId: 'getInventory', requestType: 'GET', }, { url: 'https://petstore3.swagger.io/api/v3/store/order', description: 'Place an order for a pet', operationId: 'placeOrder', requestType: 'POST', }, { url: 'https://petstore3.swagger.io/api/v3/store/order/{{orderId}}', description: 'Find purchase order by ID', operationId: 'getOrderById', requestType: 'GET', }, { url: 'https://petstore3.swagger.io/api/v3/store/order/{{orderId}}', description: 'Delete purchase order by ID', operationId: 'deleteOrder', requestType: 'DELETE', }, { url: 'https://petstore3.swagger.io/api/v3/user', description: 'Create user', operationId: 'createUser', requestType: 'POST', }, { url: 'https://petstore3.swagger.io/api/v3/user/createWithList', description: 'Creates list of users with given input array', operationId: 'createUsersWithListInput', requestType: 'POST', }, { url: 'https://petstore3.swagger.io/api/v3/user/login?username={{username}}&password={{password}}', description: 'Logs user into the system', operationId: 'loginUser', requestType: 'GET', }, { url: 'https://petstore3.swagger.io/api/v3/user/logout', description: 'Logs out current logged in user session', operationId: 'logoutUser', requestType: 'GET', }, { url: 'https://petstore3.swagger.io/api/v3/user/{{username}}', description: 'Get user by user name', operationId: 'getUserByName', requestType: 'GET', }, { url: 'https://petstore3.swagger.io/api/v3/user/{{username}}', description: 'Update user', operationId: 'updateUser', requestType: 'PUT', }, { url: 'https://petstore3.swagger.io/api/v3/user/{{username}}', description: 'Delete user', operationId: 'deleteUser', requestType: 'DELETE', }, ]; ================================================ FILE: packages/flowtest-electron/tests/utils/filemanager.test.js ================================================ const createDirectory = require('../../src/utils/filemanager/createdirectory'); const deleteDirectory = require('../../src/utils/filemanager/deletedirectory'); const path = require('path'); const createFile = require('../../src/utils/filemanager/createfile'); const readFile = require('../../src/utils/filemanager/readfile'); const deleteFile = require('../../src/utils/filemanager/deletefile'); const { pathExists } = require('../../src/utils/filemanager/filesystem'); const DIRECTORY_NAME = 'testDir'; describe('file-manager', () => { it('should create and delete directory', async () => { let result = createDirectory(DIRECTORY_NAME, __dirname); expect(result).toEqual(path.join(__dirname, DIRECTORY_NAME)); // directory already exists expect(() => { createDirectory(DIRECTORY_NAME, __dirname); }).toThrow(Error); deleteDirectory(path.join(__dirname, DIRECTORY_NAME)); expect(pathExists(path.join(__dirname, DIRECTORY_NAME))).toEqual(false); // directory doesn't exist expect(() => { deleteDirectory(path.join(__dirname, DIRECTORY_NAME)); }).toThrow(Error); }); it('should create and delete files', async () => { let result = createDirectory(DIRECTORY_NAME, __dirname); expect(result).toEqual(path.join(__dirname, DIRECTORY_NAME)); createFile('test.flow', path.join(__dirname, DIRECTORY_NAME), '{"k1":"v1"}'); expect(pathExists(path.join(__dirname, DIRECTORY_NAME, 'test.flow'))).toEqual(true); // read file const rContent = readFile(path.join(__dirname, DIRECTORY_NAME, 'test.flow')); expect(rContent).toEqual('{"k1":"v1"}'); // file already exists expect(() => { createFile('test.flow', path.join(__dirname, DIRECTORY_NAME), '{"k1":"v1"}'); }).toThrow(Error); createFile('test1.flow', path.join(__dirname, DIRECTORY_NAME), '{"k1":"v1"}'); expect(pathExists(path.join(__dirname, DIRECTORY_NAME, 'test1.flow'))).toEqual(true); // delete file deleteFile(path.join(__dirname, DIRECTORY_NAME, 'test1.flow')); expect(pathExists(path.join(__dirname, DIRECTORY_NAME, 'test1.flow'))).toEqual(false); // delete file: file no longer exists expect(() => { deleteFile(path.join(__dirname, DIRECTORY_NAME, 'test1.flow')); }).toThrow(Error); // delete directory deleteDirectory(path.join(__dirname, DIRECTORY_NAME)); expect(pathExists(path.join(__dirname, DIRECTORY_NAME))).toEqual(false); }); }); ================================================ FILE: packages/flowtest-electron/tests/utils/flowtest-ai.test.js ================================================ const fs = require('fs'); const SwaggerParser = require('@apidevtools/swagger-parser'); const JsonRefs = require('json-refs'); const FlowtestAI = require('../../src/ai/flowtestai'); describe('generate', () => { it('should generate functions using openai', async () => { const f = new FlowtestAI(); const USER_INSTRUCTION = 'Add a new pet to the store. \ Then get the created pet. \ Then get pet with status as available.'; //const testYaml = fs.readFileSync('tests/test.yaml', { encoding: 'utf8', flag: 'r' }); let api = await SwaggerParser.validate('tests/test.yaml'); console.log('API name: %s, Version: %s', api.info.title, api.info.version); const resolvedSpec = (await JsonRefs.resolveRefs(api)).resolved; let result = await f.generate(resolvedSpec, USER_INSTRUCTION, { name: 'OPENAI', apiKey: '', }); const nodeNames = result.map((node) => node.name); expect(nodeNames).toEqual(['addPet', 'getPetById', 'findPetsByStatus']); }, 60000); it('should generate functions using bedrock', async () => { const f = new FlowtestAI(); const USER_INSTRUCTION = 'Add a new pet to the store. \ Then get the created pet. \ Then get pet with status as available.'; //const testYaml = fs.readFileSync('tests/test.yaml', { encoding: 'utf8', flag: 'r' }); let api = await SwaggerParser.validate('tests/test.yaml'); console.log('API name: %s, Version: %s', api.info.title, api.info.version); const resolvedSpec = (await JsonRefs.resolveRefs(api)).resolved; let result = await f.generate(resolvedSpec, USER_INSTRUCTION, { name: 'BEDROCK_CLAUDE', apiKey: { accessKeyId: '', secretAccessKey: '', }, }); const nodeNames = result.map((node) => node.name); expect(nodeNames).toEqual(['addPet', 'getPetById', 'findPetsByStatus']); }, 60000); it('should generate functions using gemini', async () => { const f = new FlowtestAI(); const USER_INSTRUCTION = 'Add a new pet to the store. \ Then get the created pet. \ Then get pet with status as available.'; //const testYaml = fs.readFileSync('tests/test.yaml', { encoding: 'utf8', flag: 'r' }); let api = await SwaggerParser.validate('tests/test.yaml'); console.log('API name: %s, Version: %s', api.info.title, api.info.version); const resolvedSpec = (await JsonRefs.resolveRefs(api)).resolved; let result = await f.generate(resolvedSpec, USER_INSTRUCTION, { name: 'GEMINI', apiKey: '', }); const nodeNames = result.map((node) => node.name); expect(nodeNames).toEqual(['addPet', 'getPetById', 'findPetsByStatus']); }, 60000); }); ================================================ FILE: packages/flowtest-electron/tests/utils/flowtest-parser.test.js ================================================ const { deserialize, serialize } = require('../../src/utils/flowparser/parser'); describe('FlowTest parser', () => { it('should parse correctly', async () => { const flowData = { nodes: [ { id: '0', type: 'startNode', position: { x: 150, y: 150, }, deletable: false, width: 90, height: 60, }, { id: '1', data: { type: 'basic-auth', username: '{{accessId}}', password: '{{accessKey}}', }, type: 'authNode', position: { x: 402, y: 138, }, deletable: false, width: 214, height: 199, selected: false, dragging: false, positionAbsolute: { x: 402, y: 138, }, }, { id: '2', data: { url: 'https://petstore3.swagger.io/api/v3/pet', description: 'Add a new pet to the store', operationId: 'addPet', requestType: 'POST', tags: ['pet'], type: 'requestNode', requestBody: { type: 'raw-json', body: '{"id":1,"name":"Max","category":{"id":1,"name":"Dog"},"photoUrls":["https://example.com/max.jpg"],"tags":[{"id":1,"name":"friendly"}],"status":"available"}', }, postRespVars: { petId: { type: 'Select', value: 'id', }, }, }, type: 'requestNode', position: { x: 747.1300785316197, y: 71.34121814738344, }, width: 402, height: 528, selected: false, positionAbsolute: { x: 747.1300785316197, y: 71.34121814738344, }, dragging: false, }, { id: '3', data: { url: 'https://petstore3.swagger.io/api/v3/pet/findByStatus?status={{status}}', description: 'Finds Pets by status', operationId: 'findPetsByStatus', requestType: 'GET', tags: ['pet'], type: 'requestNode', preReqVars: { status: { type: 'String', value: 'available', }, }, }, type: 'requestNode', position: { x: 1217.0371813974814, y: 75.76040720254832, }, width: 406, height: 392, selected: false, positionAbsolute: { x: 1217.0371813974814, y: 75.76040720254832, }, dragging: false, }, { id: '8', type: 'delayNode', position: { x: 1671.5283794101285, y: 181.3397155683441, }, data: { description: 'Add a certain delay before next computation.', type: 'delayNode', delay: '6', }, width: 214, height: 110, selected: false, positionAbsolute: { x: 1671.5283794101285, y: 181.3397155683441, }, dragging: false, }, { id: '9', type: 'assertNode', position: { x: 1958.6425053074213, y: 59.84168648963893, }, data: { description: 'Assert on conditional expressions.', type: 'assertNode', variables: { var1: { type: 'String', value: '1', }, var2: { type: 'String', value: '1', }, }, operator: 'isEqualTo', }, width: 296, height: 328, selected: false, positionAbsolute: { x: 1958.6425053074213, y: 59.84168648963893, }, dragging: false, }, { id: '10', type: 'flowNode', position: { x: 2427.6600136878287, y: 94.18397266812065, }, data: { description: 'Helps to create nested flows', type: 'flowNode', relativePath: 'sample.flow', }, width: 164, height: 108, selected: true, positionAbsolute: { x: 2427.6600136878287, y: 94.18397266812065, }, dragging: false, }, { id: '11', type: 'outputNode', position: { x: 2366.8251067430897, y: 268.8390280901134, }, data: { description: 'Displays any data received.', type: 'outputNode', }, width: 276, height: 386, selected: false, positionAbsolute: { x: 2366.8251067430897, y: 268.8390280901134, }, dragging: false, }, ], edges: [ { id: 'reactflow__edge-0-1', source: '0', sourceHandle: null, target: '1', targetHandle: null, type: 'buttonedge', }, { source: '1', sourceHandle: null, target: '2', targetHandle: null, type: 'buttonedge', id: 'reactflow__edge-1-2', }, { source: '2', sourceHandle: null, target: '3', targetHandle: null, type: 'buttonedge', id: 'reactflow__edge-2-3', }, { source: '3', sourceHandle: null, target: '8', targetHandle: null, type: 'buttonedge', id: 'reactflow__edge-3-8', }, { source: '8', sourceHandle: null, target: '9', targetHandle: null, type: 'buttonedge', id: 'reactflow__edge-8-9', }, { source: '9', sourceHandle: 'false', target: '11', targetHandle: null, type: 'buttonedge', id: 'reactflow__edge-9false-11', }, { source: '9', sourceHandle: 'true', target: '10', targetHandle: null, type: 'buttonedge', id: 'reactflow__edge-9true-10', }, ], viewport: { x: 0.1, y: 0.2, zoom: 1.9876 }, }; const textData = deserialize(flowData); const _flowData = serialize(textData); //console.log(JSON.stringify(textData)); //console.log(JSON.stringify(_flowData)); expect(_flowData.nodes).toEqual(flowData.nodes); expect(_flowData.edges).toEqual(flowData.edges); expect(_flowData.viewport).toEqual(flowData.viewport); }); }); ================================================ FILE: packages/flowtest-electron/tests/watcher.test.js ================================================ const Watcher = require('../src/app/watcher'); const path = require('path'); const createDirectory = require('../src/utils/filemanager/createdirectory'); const deleteDirectory = require('../src/utils/filemanager/deletedirectory'); const createFile = require('../src/utils/filemanager/createfile'); const updateFile = require('../src/utils/filemanager/updatefile'); describe('watcher', () => { const watcher = new Watcher(); const DIRECTORY_NAME = 'collection'; const collectionPath = path.join(__dirname, DIRECTORY_NAME); const collectionId = '1234'; const mainWindow = { webContents: { send: jest.fn((channel, ...args) => {}), }, }; beforeEach(() => { mainWindow.webContents.send.mockClear(); }); it('should watch add and delete directory', async () => { watcher.addDirectory(mainWindow, path.join(collectionPath, 'folder1'), collectionId, collectionPath); expect(mainWindow.webContents.send).toHaveBeenCalledTimes(1); expect(mainWindow.webContents.send).toHaveBeenCalledWith( 'main:add-directory', { name: 'folder1', pathname: '/Users/sjain/projects/FlowTest/packages/flowtest-electron/tests/collection/folder1', }, '1234', ['folder1'], '/', ); watcher.addDirectory(mainWindow, path.join(collectionPath, 'environments'), collectionId, collectionPath); expect(mainWindow.webContents.send).toHaveBeenCalledTimes(1); watcher.addDirectory( mainWindow, path.join(collectionPath, 'folder1', 'folder2', 'folder3'), collectionId, collectionPath, ); expect(mainWindow.webContents.send).toHaveBeenCalledWith( 'main:add-directory', { name: 'folder3', pathname: '/Users/sjain/projects/FlowTest/packages/flowtest-electron/tests/collection/folder1/folder2/folder3', }, '1234', ['folder1', 'folder2', 'folder3'], '/', ); watcher.unlinkDir(mainWindow, path.join(collectionPath, 'folder1'), collectionId, collectionPath); expect(mainWindow.webContents.send).toHaveBeenCalledWith( 'main:delete-directory', { name: 'folder1', pathname: '/Users/sjain/projects/FlowTest/packages/flowtest-electron/tests/collection/folder1', }, '1234', ); }); it('should not do anything for environments folder', async () => { watcher.addDirectory(mainWindow, path.join(collectionPath, 'environments'), collectionId, collectionPath); expect(mainWindow.webContents.send).toHaveBeenCalledTimes(0); watcher.unlinkDir(mainWindow, path.join(collectionPath, 'environments'), collectionId, collectionPath); expect(mainWindow.webContents.send).toHaveBeenCalledTimes(0); }); it('should watch add, change and delete flowtest files', async () => { watcher.add(mainWindow, path.join(collectionPath, 'test.flow'), collectionId, collectionPath); expect(mainWindow.webContents.send).toHaveBeenCalledTimes(1); expect(mainWindow.webContents.send).toHaveBeenCalledWith( 'main:create-flowtest', { name: 'test.flow', pathname: '/Users/sjain/projects/FlowTest/packages/flowtest-electron/tests/collection/test.flow', sep: '/', subDirectories: [], }, '1234', ); watcher.add(mainWindow, path.join(collectionPath, 'folder1', 'folder2', 'test.flow'), collectionId, collectionPath); expect(mainWindow.webContents.send).toHaveBeenCalledWith( 'main:create-flowtest', { name: 'test.flow', pathname: '/Users/sjain/projects/FlowTest/packages/flowtest-electron/tests/collection/folder1/folder2/test.flow', sep: '/', subDirectories: ['folder1', 'folder2'], }, '1234', ); watcher.change( mainWindow, path.join(collectionPath, 'folder1', 'folder2', 'test.flow'), collectionId, collectionPath, ); expect(mainWindow.webContents.send).toHaveBeenCalledWith( 'main:update-flowtest', { name: 'test.flow', pathname: '/Users/sjain/projects/FlowTest/packages/flowtest-electron/tests/collection/folder1/folder2/test.flow', }, '1234', ); watcher.unlink( mainWindow, path.join(collectionPath, 'folder1', 'folder2', 'test.flow'), collectionId, collectionPath, ); expect(mainWindow.webContents.send).toHaveBeenCalledWith( 'main:delete-flowtest', { name: 'test.flow', pathname: '/Users/sjain/projects/FlowTest/packages/flowtest-electron/tests/collection/folder1/folder2/test.flow', }, '1234', ); }); it('should watch add, change and delete env files', async () => { createDirectory(DIRECTORY_NAME, __dirname); createDirectory('environments', collectionPath); createFile('test.env', path.join(collectionPath, 'environments'), 'k1=v1\nk2=v2\nk3=v3'); watcher.add(mainWindow, path.join(collectionPath, 'environments', 'test.env'), collectionId, collectionPath); expect(mainWindow.webContents.send).toHaveBeenCalledTimes(1); expect(mainWindow.webContents.send).toHaveBeenCalledWith( 'main:addOrUpdate-environment', { name: 'test.env', pathname: '/Users/sjain/projects/FlowTest/packages/flowtest-electron/tests/collection/environments/test.env', variables: { k1: 'v1', k2: 'v2', k3: 'v3', }, }, '1234', ); //environments folder is the source of truth for env files mainWindow.webContents.send.mockClear(); watcher.add(mainWindow, path.join(collectionPath, 'test.env'), collectionId, collectionPath); expect(mainWindow.webContents.send).toHaveBeenCalledTimes(0); updateFile(path.join(collectionPath, 'environments', 'test.env'), 'k2=v2\nk4=v4\nk6=v6'); watcher.change(mainWindow, path.join(collectionPath, 'environments', 'test.env'), collectionId, collectionPath); expect(mainWindow.webContents.send).toHaveBeenCalledTimes(1); expect(mainWindow.webContents.send).toHaveBeenCalledWith( 'main:addOrUpdate-environment', { name: 'test.env', pathname: '/Users/sjain/projects/FlowTest/packages/flowtest-electron/tests/collection/environments/test.env', variables: { k2: 'v2', k4: 'v4', k6: 'v6', }, }, '1234', ); deleteDirectory(collectionPath); }); it('should watch add and change dotenv file', async () => { createDirectory(DIRECTORY_NAME, __dirname); createFile('.env', collectionPath, 'k1=v1\nk2=v2\nk3=v3'); watcher.add(mainWindow, path.join(collectionPath, '.env'), collectionId, collectionPath); expect(mainWindow.webContents.send).toHaveBeenCalledTimes(1); expect(mainWindow.webContents.send).toHaveBeenCalledWith( 'main:addOrUpdate-dotEnvironment', { k1: 'v1', k2: 'v2', k3: 'v3', }, '1234', ); //.env is only allowed in collection root path mainWindow.webContents.send.mockClear(); watcher.add(mainWindow, path.join(collectionPath, 'folder', '.env'), collectionId, collectionPath); expect(mainWindow.webContents.send).toHaveBeenCalledTimes(0); updateFile(path.join(collectionPath, '.env'), 'k2=v2\nk4=v4\nk6=v6'); watcher.change(mainWindow, path.join(collectionPath, '.env'), collectionId, collectionPath); expect(mainWindow.webContents.send).toHaveBeenCalledTimes(1); expect(mainWindow.webContents.send).toHaveBeenCalledWith( 'main:addOrUpdate-dotEnvironment', { k2: 'v2', k4: 'v4', k6: 'v6', }, '1234', ); deleteDirectory(collectionPath); }); }); ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - packages/flowtest-cli - packages/flowtest-electron ================================================ FILE: postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ================================================ FILE: public/index.html ================================================ FlowTest
================================================ FILE: public/manifest.json ================================================ { "short_name": "FlowTest", "name": "Drag & Drop UI for openAI function calling", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "logo192.png", "type": "image/png", "sizes": "192x192" }, { "src": "logo512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: public/robots.txt ================================================ # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: ================================================ FILE: src/App.css ================================================ .App { text-align: center; } .App-logo { height: 40vmin; pointer-events: none; } @media (prefers-reduced-motion: no-preference) { .App-logo { animation: App-logo-spin infinite 20s linear; } } .App-header { background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; } .App-link { color: #61dafb; } @keyframes App-logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } ================================================ FILE: src/App.js ================================================ import React from 'react'; import './App.css'; import Routes from './routes'; import { HashRouter } from 'react-router-dom'; function App() { return ( ); } export default App; ================================================ FILE: src/App.test.js ================================================ import { render, screen } from '@testing-library/react'; import App from './App'; test('renders learn react link', () => { render(); const linkElement = screen.getByText(/learn react/i); expect(linkElement).toBeInTheDocument(); }); ================================================ FILE: src/components/atoms/EditableTextItem.js ================================================ import React from 'react'; import { PropTypes } from 'prop-types'; import { EditText } from 'react-edit-text'; import 'react-edit-text/dist/index.css'; const EditableTextItem = ({ initialText }) => { // ToDo: Fix: as of now it is taking empty value which should not be the case return ; }; // https://legacy.reactjs.org/docs/typechecking-with-proptypes.html EditableTextItem.propTypes = { initialText: PropTypes.string.isRequired, }; export default EditableTextItem; ================================================ FILE: src/components/atoms/Editor.js ================================================ import React, { useRef, useEffect, useState } from 'react'; import { basicSetup } from 'codemirror'; import { EditorState, Compartment } from '@codemirror/state'; import { EditorView, keymap, lineNumbers, tooltips } from '@codemirror/view'; import { indentWithTab, history } from '@codemirror/commands'; import { json } from '@codemirror/lang-json'; import { defaultKeymap } from '@codemirror/commands'; import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language'; import { isEqual } from 'lodash'; import { autocompletion, CompletionContext } from '@codemirror/autocomplete'; // Function to dynamically generate autocomplete options const createAutocompleteSource = (options) => { return (context) => { let word = context.matchBefore(/\{\{\w*$/); if (!word) return null; return { from: word.from, options: options.map((option) => ({ label: `{{${option}}}`, type: 'keyword' })), }; }; }; // Create a Compartment for autocomplete const autocompleteCompartment = new Compartment(); // Function to update autocomplete options const updateAutocompleteOptions = (view, newOptions) => { view.dispatch({ effects: autocompleteCompartment.reconfigure(autocompletion({ override: [createAutocompleteSource(newOptions)] })), }); }; // Custom styles to hide scrollbar const hideScrollbar = EditorView.theme({ '.cm-scroller': { overflowX: 'auto', overflowY: 'hidden', whiteSpace: 'nowrap', scrollbarWidth: 'none' /* For Firefox */, }, '&': { cursor: 'text', }, }); export const Editor = ({ ...props }) => { const editor = useRef(); const [view, setView] = useState(null); const [dynamicOptions, setDynamicOptions] = useState(props.completionOptions || []); if (view) { if (!isEqual(dynamicOptions, props.completionOptions)) { updateAutocompleteOptions(view, props.completionOptions); setDynamicOptions(props.completionOptions); } if (props.value != view.state.doc.toString()) { view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: props.value } }); } } const onUpdate = EditorView.updateListener.of((v) => { if (props.onChange) { props.onChange(v.state.doc.toString()); } }); useEffect(() => { const state = EditorState.create({ doc: props.value || '', extensions: [ lineNumbers(), json(), basicSetup, keymap.of([defaultKeymap, indentWithTab]), onUpdate, EditorState.readOnly.of(props.readOnly || false), history(), hideScrollbar, autocompleteCompartment.of(autocompletion({ override: [createAutocompleteSource(dynamicOptions)] })), tooltips({ parent: document.body, }), ], }); const view = new EditorView({ state, parent: editor.current }); setView(view); return () => { view.destroy(); setView(null); }; }, []); return
; }; ================================================ FILE: src/components/atoms/Logo.js ================================================ import React from 'react'; import FlowTestAI from 'assets/icons/FlowTestAI.png'; const AppLogo = ({ styleClasses }) => { return (
FlowTestAI app logo
); }; export default AppLogo; ================================================ FILE: src/components/atoms/SelectAuthKeys.js ================================================ // ToDo: Remove as we ar not using it // import React, { Fragment, useState } from 'react'; // import { Listbox, Transition } from '@headlessui/react'; // import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'; // const authKeys = [{ name: 'Select Auth Key' }, { name: 'No Authorization' }]; // const SelectAuthKeys = () => { // const [selected, setSelected] = useState(authKeys[0]); // return ( // //
// // {selected.name} // // // // // // {authKeys.map((authKey, authKeyIdx) => ( // // `relative cursor-default select-none py-2 pl-10 pr-4 ${ // active ? 'bg-amber-100 text-amber-900' : 'text-gray-900' // }` // } // value={authKey} // > // {({ selected }) => ( // <> // {authKey.name} // {selected ? ( // // // ) : null} // // )} // // ))} // // //
//
// ); // }; // export default SelectAuthKeys; ================================================ FILE: src/components/atoms/SelectEnvironment.js ================================================ import React, { Fragment, useState } from 'react'; import { PropTypes } from 'prop-types'; import { Listbox, Transition } from '@headlessui/react'; import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'; import { Square3Stack3DIcon } from '@heroicons/react/24/outline'; import { useTabStore } from 'stores/TabStore'; const SelectEnvironment = ({ environments }) => { const setEnv = useTabStore((state) => state.setSelectedEnv); const [selected, setSelected] = useState(null); if (selected) { setEnv(selected); } return (
{selected ? selected : 'Select environment'}
{environments.length ? ( {environments.map((environment, environmentIndex) => ( `relative cursor-default select-none py-2 pl-10 pr-4 hover:font-semibold ${ active ? 'bg-background-light text-slate-900' : '' }` } value={environment.name} > {({ selected }) => ( <> {environment.name} {selected ? ( ) : null} )} ))} ) : ( '' )}
); }; SelectEnvironment.propTypes = { environments: PropTypes.Array.isRequired, }; export default SelectEnvironment; ================================================ FILE: src/components/atoms/Tabs.js ================================================ import React, { useState } from 'react'; import { XMarkIcon } from '@heroicons/react/24/outline'; import { useTabStore } from 'stores/TabStore'; import ConfirmActionModal from 'components/molecules/modals/ConfirmActionModal'; import { isEqual } from 'lodash'; import { OBJ_TYPES } from 'constants/Common'; import { isSaveNeeded } from './util'; import { saveHandle } from 'components/molecules/modals/SaveFlowModal'; const tabUnsavedChanges = (tab) => { if (tab.type === OBJ_TYPES.flowtest && tab.flowDataDraft) { return isSaveNeeded(tab.flowData, tab.flowDataDraft); } else if (tab.type === OBJ_TYPES.environment && tab.variablesDraft && !isEqual(tab.variables, tab.variablesDraft)) { return true; } else { return false; } }; const Tabs = () => { const tabs = useTabStore((state) => state.tabs); const setFocusTab = useTabStore((state) => state.setFocusTab); const focusTabId = useTabStore((state) => state.focusTabId); const focusTab = tabs.find((t) => t.id === focusTabId); const [confirmActionModalOpen, setConfirmActionModalOpen] = useState(false); const closeTab = useTabStore((state) => state.closeTab); const [closingTab, setClosingTab] = useState(''); const [closingCollectionId, setClosingCollectionId] = useState(''); // ToDo: change color according to theme const activeTabStyles = 'bg-cyan-900 text-white'; const tabCommonStyles = 'tab flex items-center gap-x-2 border-r border-neutral-300 pr-0 tracking-[0.15em] transition duration-300 ease-in text-sm flex-nowrap'; const messageForConfirmActionModal = `You have unsaved changes in the ${focusTab?.type}, are you sure you want to close it?`; const handleCloseTab = (event, tab) => { event.stopPropagation(); event.preventDefault(); setClosingTab(tab); setClosingCollectionId(tab.collectionId); if (tabUnsavedChanges(tab)) { console.debug(`Confirm close for tabId: ${tab.id} : collectionId: ${tab.collectionId}`); setConfirmActionModalOpen(true); return; } closeTab(tab.id, tab.collectionId); }; return (
{tabs // tabs belonging to one collection will be shown at a time //.reverse() .filter((t) => t.collectionId === focusTab.collectionId) .map((tab, index) => { return (
{ setFocusTab(tab.id); console.debug(`Selected tab: ${tab.id}`); }} data-id={tab.id} data-collection-id={tab.collectionId} > {tabUnsavedChanges(tab) ? '*' : ''} {tab.name}
handleCloseTab(e, tab)} >
); })} { closeTab(closingTab.id, closingCollectionId); setConfirmActionModalOpen(false); }} open={confirmActionModalOpen} message={messageForConfirmActionModal} actionFn={() => { saveHandle(closingTab); closeTab(closingTab.id, closingCollectionId); setConfirmActionModalOpen(false); }} closeModal={() => setConfirmActionModalOpen(false)} leftButtonMessage={'Close Withuout Saving'} rightButtonMessage={'Save And Close'} />
); }; export default Tabs; ================================================ FILE: src/components/atoms/ThemeController.js ================================================ import React from 'react'; import Tippy from '@tippyjs/react'; import 'tippy.js/dist/tippy.css'; const ThemeController = () => { const [dark, setDark] = React.useState(false); const darkModeHandler = () => { setDark(!dark); document.body.classList.toggle('dark'); }; return ( // // //
); }; export default ThemeController; ================================================ FILE: src/components/atoms/common/Button.js ================================================ import React from 'react'; import { PropTypes } from 'prop-types'; // ToDo: can be more generalized const Button = ({ children, classes, btnType, intentType, isDisabled, onClickHandle, fullWidth, onlyIcon, padding, }) => { const btnColorStyles = { primary: { default: 'border-cyan-900 text-white bg-cyan-900 hover:border-cyan-950 hover:bg-cyan-950', // with bg color intent: { info: 'border-sky-600 bg-sky-600 text-white hover:bg-sky-700', success: 'border-green-600 bg-green-600 text-white hover:bg-green-700', warning: 'border-amber-600 bg-amber-600 text-white hover:bg-amber-700', error: 'border-red-600 bg-red-600 text-white hover:bg-red-700', }, }, secondary: { default: 'border-cyan-900 text-cyan-900 bg-background-light hover:bg-background border', // outline intent: { info: 'border border-sky-600 bg-sky-50 text-sky-600 hover:bg-sky-100', success: 'border border-green-600 bg-green-50 text-green-600 hover:bg-green-100', warning: 'border border-amber-600 bg-amber-50 text-amber-600 hover:bg-amber-100', error: 'border border-red-600 bg-red-50 text-red-600 hover:bg-red-100', }, }, tertiary: { default: 'text-cyan-900 hover:bg-background-light', // without border but with primary color scheme intent: { info: 'text-sky-600 hover:bg-sky-100', success: 'text-green-600 hover:bg-green-100', warning: 'text-amber-600 hover:bg-amber-100', error: 'text-red-600 hover:bg-red-100', }, }, minimal: { default: 'text-cyan-950 hover:bg-background-light', // without border and a neutral button intent: { info: 'text-sky-600 hover:text-sky-900 hover:font-semibold', success: 'text-green-600 hover:text-green-900 hover:font-semibold', warning: 'text-amber-600 hover:text-amber-900 hover:font-semibold', error: 'text-red-600 hover:text-red-900 hover:font-semibold', }, }, disabled: { default: 'text-gray-300 border border-gray-300 cursor-not-allowed', }, }; const getButtonStyles = () => { const buttonColorStyles = btnColorStyles[btnType]; let styles = ''; if (onlyIcon) { if (padding && padding !== '') { styles = `${commonStyles} ${padding}`; } else { styles = `${commonStyles} p-0.5`; } } else { styles = `${commonStyles} px-4 py-2`; } if (intentType) { return `${buttonColorStyles.intent[intentType]} ${styles}`; } return `${buttonColorStyles.default} ${styles}`; }; const commonStyles = `inline-flex items-center justify-center gap-2 whitespace-nowrap outline-none rounded transition ${fullWidth ? 'w-full' : ''} ${classes ? classes : ''}`; return ( ); }; Button.propTypes = { children: PropTypes.node.isRequired, btnType: PropTypes.string.isRequired, isDisabled: PropTypes.boolean.isRequired, onClickHandle: PropTypes.func, fullWidth: PropTypes.boolean, }; export default Button; ================================================ FILE: src/components/atoms/common/HomeLoadingScreen.js ================================================ import React from 'react'; // import PropTypes from 'prop-types'; import FlowGPU from 'assets/icons/Flow-GPU-text-no-background-white.png'; const HomeLoadingScreen = () => { return (
FlowTestAI app logo
); }; // HomeLoadingScreen.propTypes = {}; export default HomeLoadingScreen; ================================================ FILE: src/components/atoms/common/HorizontalDivider.js ================================================ import React from 'react'; const HorizontalDivider = ({ themeColor, themeStyles }) => { return (
); }; export default HorizontalDivider; ================================================ FILE: src/components/atoms/common/LoadingSpinner.js ================================================ import React, { useState } from 'react'; const LoadingSpinner = ({ spinnerColor }) => { return (
); }; export default LoadingSpinner; ================================================ FILE: src/components/atoms/common/NumberInput.js ================================================ import React from 'react'; const NumberInput = ({ placeHolder, onChangeHandler, name, value }) => { return ( ); }; export default NumberInput; ================================================ FILE: src/components/atoms/common/TextEditor.js ================================================ import React, { useRef, useEffect, useState } from 'react'; import { basicSetup } from 'codemirror'; import { EditorState, Compartment } from '@codemirror/state'; import { EditorView, keymap, lineNumbers, placeholder, Decoration, ViewPlugin, MatchDecorator } from '@codemirror/view'; import { indentWithTab, history } from '@codemirror/commands'; //import { json } from '@codemirror/lang-json'; import { defaultKeymap } from '@codemirror/commands'; import { syntaxHighlighting, defaultHighlightStyle, HighlightStyle, StreamLanguage } from '@codemirror/language'; import { autocompletion, CompletionContext } from '@codemirror/autocomplete'; import { tags, styleTags } from '@lezer/highlight'; import { isEqual } from 'lodash'; // Function to dynamically generate autocomplete options const createAutocompleteSource = (options) => { return (context) => { let word = context.matchBefore(/\{\{\w*$/); if (!word) return null; return { from: word.from, options: options.map((option) => ({ label: `{{${option}}}`, type: 'keyword' })), }; }; }; // Create a Compartment for autocomplete const autocompleteCompartment = new Compartment(); // Function to update autocomplete options const updateAutocompleteOptions = (view, newOptions) => { view.dispatch({ effects: autocompleteCompartment.reconfigure(autocompletion({ override: [createAutocompleteSource(newOptions)] })), }); }; // Custom styles to hide scrollbar const hideScrollbar = EditorView.theme({ '.cm-scroller': { overflowX: 'auto', overflowY: 'hidden', whiteSpace: 'nowrap', scrollbarWidth: 'none' /* For Firefox */, }, '.cm-content': { padding: '0', // Adjust padding to fit your needs overflow: 'auto', lineHeight: '1.75rem', fontSize: '1.125rem', }, '.cm-scroller::-webkit-scrollbar': { display: 'none' /* For Chrome, Safari, and Opera */, }, '.cm-line': { padding: '0', // Adjust padding to fit your needs }, '&': { height: 'auto', // Adjust height to auto for single-line input cursor: 'text', }, '.cm-placeholder': { color: '#aaa', // Placeholder text color }, '&.cm-focused': { outline: 'none' }, }); // Rebind the Enter key to do nothing const rebindEnterKey = keymap.of([ { key: 'Enter', run: () => true, // Return true to prevent the default action }, ]); // Create a MatchDecorator to highlight {{text}} strings const decorator = new MatchDecorator({ // Regular expression to match {{text}} strings regexp: /{{[^}]*}}/g, decoration: Decoration.mark({ class: 'highlight' }), }); // View plugin to apply the MatchDecorator const highlightPlugin = ViewPlugin.fromClass( class { constructor(view) { this.decorations = decorator.createDeco(view); } update(update) { this.decorations = decorator.updateDeco(update, this.decorations); } }, { decorations: (v) => v.decorations, }, ); // Custom styles for highlighting const highlightStyle = EditorView.baseTheme({ '.highlight': { color: 'brown', }, }); export const TextEditor = ({ placeHolder, onChangeHandler, value, disableState, completionOptions, styles }) => { const editorRef = useRef(null); const viewRef = useRef(null); const [dynamicOptions, setDynamicOptions] = useState(completionOptions); useEffect(() => { if (!editorRef.current) return; const state = EditorState.create({ doc: value, extensions: [ //EditorView.lineWrapping, placeholder(placeHolder), //myAutocomplete, //basicSetup, keymap.of([defaultKeymap, indentWithTab]), EditorView.updateListener.of((v) => { if (onChangeHandler && v.docChanged) { onChangeHandler(v.state.doc.toString()); } }), //EditorState.readOnly.of(props.readOnly || false), history(), hideScrollbar, rebindEnterKey, highlightPlugin, highlightStyle, autocompleteCompartment.of(autocompletion({ override: [createAutocompleteSource(dynamicOptions)] })), ], }); const view = new EditorView({ state, parent: editorRef.current }); viewRef.current = view; return () => { view.destroy(); }; }, []); useEffect(() => { const view = viewRef.current; if (!view) return; // Update completion options if they've changed if (!isEqual(dynamicOptions, completionOptions)) { updateAutocompleteOptions(view, completionOptions); setDynamicOptions(completionOptions); } // Update content if it's different from current editor content const currentContent = view.state.doc.toString(); if (value !== currentContent) { view.dispatch({ changes: { from: 0, to: currentContent.length, insert: value }, }); } }, [value, completionOptions]); //const mainStyles = // 'nodrag nowheel block border border-slate-700 bg-background-light p-2.5 text-base outline-none'; const intentStyles = disableState ? 'cursor-not-allowed text-slate-400' : 'text-slate-900'; return
; }; ================================================ FILE: src/components/atoms/common/TextInput.js ================================================ import React from 'react'; const TextInput = ({ id, placeHolder, onChangeHandler, name, value, disableState }) => { const mainStyles = 'nodrag nowheel block w-full rounded border border-slate-700 bg-background-light p-2.5 text-sm outline-none'; const intentStyles = disableState ? 'cursor-not-allowed text-slate-400' : 'text-slate-900'; return ( ); }; export default TextInput; ================================================ FILE: src/components/atoms/common/TextInputWithLabel.js ================================================ import React from 'react'; const TextInputWithLabel = ({ children, placeHolder, onChangeHandler, name, value, label }) => { return (
{label}
); }; export default TextInputWithLabel; ================================================ FILE: src/components/atoms/common/TimeoutSelector.js ================================================ import React, { useState, Fragment } from 'react'; import { ClockIcon } from '@heroicons/react/24/outline'; import { Listbox, Transition } from '@headlessui/react'; import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'; const TimeoutSelector = ({ optionsData, onSelectHandler = () => null }) => { const [selected, setSelected] = useState(null); const getSelectedLabel = (selectedValue) => { let labelToShow = ''; optionsData.forEach((element) => { if (element.value === selectedValue) { labelToShow = element.label; } }); return labelToShow; }; return ( <> { setSelected(getSelectedLabel(selectedValue)); onSelectHandler(selectedValue); }} >
{selected ? selected : 'Select Timeout'}
{optionsData.length ? ( {optionsData.map((optionData, optionDataIndex) => ( `relative cursor-default select-none py-2 pl-10 pr-4 hover:font-semibold ${ active ? 'bg-background-light text-slate-900' : '' }` } value={optionData.value} > {({ selected }) => ( <> {optionData.label} {selected ? ( ) : null} )} ))} ) : ( '' )}
); }; export default TimeoutSelector; ================================================ FILE: src/components/atoms/flow/FlowNode.js ================================================ import React from 'react'; import { PropTypes } from 'prop-types'; import { Handle, Position } from 'reactflow'; const FlowNode = ({ children, title, handleLeft, handleLeftData, handleRight, handleRightData }) => { return ( <> {handleLeft ? : ''}
{children ? ( <>

{title}

{children}
) : ( <>

{title}

{children} )}
{handleRight ? ( ) : ( '' )} ); }; FlowNode.propTypes = { children: PropTypes.node.isRequired, title: PropTypes.string.isRequired, handleLeft: PropTypes.node.isRequired, handleLeftData: PropTypes.object.isRequired, handleRight: PropTypes.node.isRequired, handleRightData: PropTypes.object.isRequired, }; export default FlowNode; ================================================ FILE: src/components/atoms/flow/NodeHorizontalDivider.js ================================================ import React from 'react'; const NodeHorizontalDivider = () => { return
; }; export default NodeHorizontalDivider; ================================================ FILE: src/components/atoms/flow/Textarea.js ================================================ import React from 'react'; const Textarea = ({ id, placeHolder, onChangeHandler, name, value, rows }) => { return (