Showing preview only (695K chars total). Download the full file or copy to clipboard to get everything.
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 <your-github-name>/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 <file> && 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
[](https://github.com/FlowTestAI/FlowTest/releases)
[](https://www.linkedin.com/company/flowtestai)
[](https://twitter.com/FlowTestAI)
[](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.
<img width="1728" alt="Screenshot 2024-04-18 at 5 41 43 PM" src="https://github.com/FlowTestAI/FlowTest/assets/5829490/c04f6e3e-fe69-4d25-a008-ba558c8fe149">
## 🚀 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


## 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 <command> [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="<<FLOWTEST_ACCESS_ID>>"`));
console.log(chalk.yellow(` export FLOWTEST_ACCESS_KEY="<<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 <jsajal1993@gmail.com>",
"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 <jsajal1993@gmail.com>",
"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:
<multiplefunctions>
<functioncall> ${fn} </functioncall>
<functioncall> ${fn} </functioncall>
...
</multiplefunctions>
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>(.*?)<\/multiplefunctions>/s;
const mfnMatch = content.match(mfnPattern);
// Single function lookup
const singlePattern = /<functioncall>(.*?)<\/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>(.*?)<\/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:
<multiplefunctions>
<functioncall> ${fn} </functioncall>
<functioncall> ${fn} </functioncall>
...
</multiplefunctions>
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>(.*?)<\/multiplefunctions>/s;
const mfnMatch = content.match(mfnPattern);
// Single function lookup
const singlePattern = /<functioncall>(.*?)<\/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>(.*?)<\/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}}',
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
SYMBOL INDEX (211 symbols across 55 files)
FILE: packages/flowtest-cli/bin/index.js
function bytesToBase64 (line 22) | function bytesToBase64(bytes) {
FILE: packages/flowtest-cli/graph/Graph.js
class nestedFlowNode (line 15) | class nestedFlowNode extends Node {
method constructor (line 16) | constructor(nodes, edges, startTime, timeout, initialEnvVars, logger) {
method evaluate (line 25) | async evaluate() {
class Graph (line 30) | class Graph {
method constructor (line 31) | constructor(nodes, edges, startTime, timeout, initialEnvVars, logger) {
method #checkTimeout (line 42) | #checkTimeout() {
method #computeConnectingEdge (line 46) | #computeConnectingEdge(node, result) {
method #computeDataFromPreviousNodes (line 64) | #computeDataFromPreviousNodes(node) {
method #computeNode (line 80) | async #computeNode(node) {
method run (line 245) | async run() {
FILE: packages/flowtest-cli/graph/GraphLogger.js
class GraphLogger (line 7) | class GraphLogger {
method constructor (line 8) | constructor() {
method add (line 12) | add(logLevel, message, node) {
method get (line 21) | get() {
FILE: packages/flowtest-cli/graph/compute/assertnode.js
class assertNode (line 7) | class assertNode extends Node {
method constructor (line 8) | constructor(operator, variables, prevNodeOutputData, envVariables, log...
method getVariableValue (line 17) | getVariableValue(variable) {
method evaluate (line 29) | evaluate() {
FILE: packages/flowtest-cli/graph/compute/authnode.js
class authNode (line 6) | class authNode extends Node {
method constructor (line 7) | constructor(nodeData, envVariables, logger) {
method evaluate (line 13) | evaluate() {
FILE: packages/flowtest-cli/graph/compute/node.js
class Node (line 1) | class Node {
method constructor (line 2) | constructor(type) {
method evaluate (line 6) | evaluate() {
FILE: packages/flowtest-cli/graph/compute/requestnode.js
class requestNode (line 25) | class requestNode extends Node {
method constructor (line 26) | constructor(nodeData, prevNodeOutputData, envVariables, auth, logger) {
method evaluate (line 35) | async evaluate() {
method formulateRequest (line 102) | formulateRequest(finalUrl, variablesDict) {
method runHttpRequest (line 146) | async runHttpRequest(request) {
FILE: packages/flowtest-cli/graph/compute/setvarnode.js
class setVarNode (line 5) | class setVarNode extends Node {
method constructor (line 6) | constructor(nodeData, prevNodeOutputData, envVariables) {
method getVariableValue (line 13) | getVariableValue(variable) {
method evaluate (line 25) | evaluate() {
FILE: packages/flowtest-cli/utils/flowparser/AssertNode.js
class AssertNode (line 3) | class AssertNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 17) | deserialize(node) {
FILE: packages/flowtest-cli/utils/flowparser/AuthNode.js
class AuthNode (line 3) | class AuthNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 17) | deserialize(node) {
FILE: packages/flowtest-cli/utils/flowparser/DelayNode.js
class DelayNode (line 3) | class DelayNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 17) | deserialize(node) {
FILE: packages/flowtest-cli/utils/flowparser/NestedFlowNode.js
class NestedFlowNode (line 3) | class NestedFlowNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 17) | deserialize(node) {
FILE: packages/flowtest-cli/utils/flowparser/Node.js
class Node (line 1) | class Node {
method constructor (line 2) | constructor(type) {
method serialize (line 6) | serialize(id, data, metadata) {
method deserialize (line 10) | deserialize(node) {
FILE: packages/flowtest-cli/utils/flowparser/OutputNode.js
class OutputNode (line 3) | class OutputNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 17) | deserialize(node) {
FILE: packages/flowtest-cli/utils/flowparser/RequestNode.js
class RequestNode (line 3) | class RequestNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 17) | deserialize(node) {
FILE: packages/flowtest-cli/utils/flowparser/SetVarNode.js
class SetVarNode (line 3) | class SetVarNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 17) | deserialize(node) {
FILE: packages/flowtest-cli/utils/flowparser/StartNode.js
class StartNode (line 3) | class StartNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 15) | deserialize(node) {
FILE: packages/flowtest-cli/utils/flowparser/parser.js
constant VERSION (line 11) | const VERSION = 1;
FILE: packages/flowtest-electron/electron-main.js
function checkForUpdates (line 30) | function checkForUpdates() {
FILE: packages/flowtest-electron/src/ai/flowtestai.js
class FlowtestAI (line 5) | class FlowtestAI {
method generate (line 6) | async generate(collection, user_instruction, model) {
method get_available_functions (line 27) | async get_available_functions(collection) {
method isCyclic (line 85) | isCyclic(obj) {
FILE: packages/flowtest-electron/src/ai/models/bedrock_claude.js
class BedrockClaudeGenerate (line 6) | class BedrockClaudeGenerate {
method constructor (line 7) | constructor(creds) {
method filter_functions (line 25) | async filter_functions(functions, instruction) {
method process_user_instruction (line 47) | async process_user_instruction(functions, instruction) {
method extractFunctionCalls (line 91) | extractFunctionCalls(completion) {
FILE: packages/flowtest-electron/src/ai/models/gemini.js
class GeminiGenerate (line 6) | class GeminiGenerate {
method constructor (line 7) | constructor(apiKey) {
method filter_functions (line 18) | async filter_functions(functions, instruction) {
method process_user_instruction (line 42) | async process_user_instruction(functions, instruction) {
method extractFunctionCalls (line 96) | extractFunctionCalls(completion) {
FILE: packages/flowtest-electron/src/ai/models/openai.js
constant SYSTEM_MESSAGE (line 5) | const SYSTEM_MESSAGE = `You are a helpful assistant. \
constant MAX_CALLS (line 10) | const MAX_CALLS = 10;
class OpenAIGenerate (line 12) | class OpenAIGenerate {
method filter_functions (line 13) | async filter_functions(functions, instruction, apiKey) {
method get_openai_response (line 43) | async get_openai_response(functions, messages, apiKey) {
method process_user_instruction (line 57) | async process_user_instruction(functions, instruction, apiKey) {
FILE: packages/flowtest-electron/src/app/watcher.js
class Watcher (line 8) | class Watcher {
method constructor (line 9) | constructor() {
method isFlowTestFile (line 13) | isFlowTestFile(pathname) {
method isEnvFile (line 18) | isEnvFile(pathname, collectionPath) {
method isDotEnvFile (line 26) | isDotEnvFile(pathname, collectionPath) {
method add (line 33) | add(mainWindow, pathname, collectionId, watchPath) {
method addDirectory (line 70) | addDirectory(mainWindow, pathname, collectionId, watchPath) {
method change (line 92) | change(mainWindow, pathname, collectionId, watchPath) {
method unlink (line 125) | unlink(mainWindow, pathname, collectionId, watchPath) {
method unlinkDir (line 146) | unlinkDir(mainWindow, pathname, collectionId, watchPath) {
method getEnvVariables (line 161) | getEnvVariables(pathname) {
method addWatcher (line 168) | addWatcher(mainWindow, watchPath, collectionId) {
method hasWatcher (line 201) | hasWatcher(watchPath) {
method removeWatcher (line 205) | removeWatcher(watchPath) {
FILE: packages/flowtest-electron/src/ipc/collection.js
function bytesToBase64 (line 379) | function bytesToBase64(bytes) {
FILE: packages/flowtest-electron/src/store/collection.js
class Collections (line 4) | class Collections {
method constructor (line 5) | constructor() {
method add (line 9) | add(collection) {
method remove (line 20) | remove(collection) {
method getAll (line 31) | getAll() {
method removeAll (line 35) | removeAll() {
FILE: packages/flowtest-electron/src/store/settings.js
class Settings (line 3) | class Settings {
method constructor (line 4) | constructor() {
method addLogSyncConfig (line 8) | addLogSyncConfig(enabled, hostUrl, accessId, accessKey) {
method addGenAIUsageDisclaimer (line 12) | addGenAIUsageDisclaimer(accepted) {
method getAll (line 16) | getAll() {
method clearAll (line 23) | clearAll() {
FILE: packages/flowtest-electron/src/utils/filemanager/filesystem.js
constant PATH_SEPARATOR (line 52) | const PATH_SEPARATOR = isWindowsOS() ? '\\' : '/';
FILE: packages/flowtest-electron/src/utils/flowparser/AssertNode.js
class AssertNode (line 3) | class AssertNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 17) | deserialize(node) {
FILE: packages/flowtest-electron/src/utils/flowparser/AuthNode.js
class AuthNode (line 3) | class AuthNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 17) | deserialize(node) {
FILE: packages/flowtest-electron/src/utils/flowparser/DelayNode.js
class DelayNode (line 3) | class DelayNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 17) | deserialize(node) {
FILE: packages/flowtest-electron/src/utils/flowparser/NestedFlowNode.js
class NestedFlowNode (line 3) | class NestedFlowNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 17) | deserialize(node) {
FILE: packages/flowtest-electron/src/utils/flowparser/Node.js
class Node (line 1) | class Node {
method constructor (line 2) | constructor(type) {
method serialize (line 6) | serialize(id, data, metadata) {
method deserialize (line 10) | deserialize(node) {
FILE: packages/flowtest-electron/src/utils/flowparser/OutputNode.js
class OutputNode (line 3) | class OutputNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 17) | deserialize(node) {
FILE: packages/flowtest-electron/src/utils/flowparser/RequestNode.js
class RequestNode (line 3) | class RequestNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 17) | deserialize(node) {
FILE: packages/flowtest-electron/src/utils/flowparser/SetVarNode.js
class SetVarNode (line 3) | class SetVarNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 17) | deserialize(node) {
FILE: packages/flowtest-electron/src/utils/flowparser/StartNode.js
class StartNode (line 3) | class StartNode extends Node {
method constructor (line 4) | constructor() {
method serialize (line 8) | serialize(id, data, metadata) {
method deserialize (line 15) | deserialize(node) {
FILE: packages/flowtest-electron/src/utils/flowparser/parser.js
constant VERSION (line 11) | const VERSION = 1;
FILE: packages/flowtest-electron/src/utils/generate-request-parameters.js
function generateParameterExample (line 1) | function generateParameterExample(parameter) {
function generateStringExample (line 26) | function generateStringExample(schema) {
function generateStringWithLengthConstraints (line 61) | function generateStringWithLengthConstraints(str, minLength, maxLength) {
function generateIntegerExample (line 73) | function generateIntegerExample(schema) {
function generateNumberExample (line 79) | function generateNumberExample(schema) {
function generateArrayExample (line 85) | function generateArrayExample(schema) {
FILE: packages/flowtest-electron/tests/utils/filemanager.test.js
constant DIRECTORY_NAME (line 9) | const DIRECTORY_NAME = 'testDir';
FILE: src/App.js
function App (line 6) | function App() {
FILE: src/components/atoms/common/TextEditor.js
method constructor (line 85) | constructor(view) {
method update (line 88) | update(update) {
FILE: src/components/molecules/flow/edges/ButtonEdge.js
function CustomEdge (line 9) | function CustomEdge({
FILE: src/components/molecules/flow/graph/Graph.js
class Graph (line 13) | class Graph {
method constructor (line 14) | constructor(nodes, edges, startTime, initialEnvVars, logger, collectio...
method #checkTimeout (line 28) | #checkTimeout() {
method #computeConnectingEdge (line 32) | #computeConnectingEdge(node, result) {
method #computeDataFromPreviousNodes (line 50) | #computeDataFromPreviousNodes(node) {
method #computeNode (line 66) | async #computeNode(node) {
method run (line 239) | async run() {
FILE: src/components/molecules/flow/graph/GraphLogger.js
class GraphLogger (line 7) | class GraphLogger {
method constructor (line 8) | constructor() {
method add (line 12) | add(logLevel, message, node) {
method get (line 21) | get() {
FILE: src/components/molecules/flow/graph/compute/assertnode.js
class assertNode (line 6) | class assertNode extends Node {
method constructor (line 7) | constructor(operator, variables, prevNodeOutputData, envVariables, log...
method getVariableValue (line 16) | getVariableValue(variable) {
method evaluate (line 28) | evaluate() {
FILE: src/components/molecules/flow/graph/compute/authnode.js
class authNode (line 5) | class authNode extends Node {
method constructor (line 6) | constructor(nodeData, envVariables, logger) {
method evaluate (line 12) | evaluate() {
FILE: src/components/molecules/flow/graph/compute/nestedflownode.js
class nestedFlowNode (line 4) | class nestedFlowNode extends Node {
method constructor (line 5) | constructor(nodes, edges, startTime, initialEnvVars, logger, collectio...
method evaluate (line 10) | async evaluate() {
FILE: src/components/molecules/flow/graph/compute/node.js
class Node (line 1) | class Node {
method constructor (line 2) | constructor(type) {
method evaluate (line 6) | evaluate() {
FILE: src/components/molecules/flow/graph/compute/requestnode.js
class requestNode (line 6) | class requestNode extends Node {
method constructor (line 7) | constructor(nodeData, prevNodeOutputData, envVariables, auth, logger, ...
method evaluate (line 17) | async evaluate() {
method formulateRequest (line 82) | async formulateRequest(finalUrl, variablesDict) {
method runHttpRequest (line 126) | runHttpRequest(rawRequest) {
FILE: src/components/molecules/flow/graph/compute/setvarnode.js
class setVarNode (line 5) | class setVarNode extends Node {
method constructor (line 6) | constructor(nodeData, prevNodeOutputData, envVariables) {
method getVariableValue (line 13) | getVariableValue(variable) {
method evaluate (line 25) | evaluate() {
FILE: src/components/molecules/modals/GenerateFlowTestModal.js
function gen (line 599) | function gen() {
FILE: src/constants/Common.js
constant FLOW_FILE_SUFFIX_REGEX (line 1) | const FLOW_FILE_SUFFIX_REGEX = /^.+\.flow$/gm;
constant CHOOSE_OPERATOR_DEFAULT_VALUE_OBJ (line 3) | const CHOOSE_OPERATOR_DEFAULT_VALUE_OBJ = {
constant INPUT_DEFAULT_TYPE (line 8) | const INPUT_DEFAULT_TYPE = 'Text';
constant BUTTON_TYPES (line 10) | const BUTTON_TYPES = {
constant BUTTON_INTENT_TYPES (line 18) | const BUTTON_INTENT_TYPES = {
constant OBJ_TYPES (line 25) | const OBJ_TYPES = {
constant GENAI_MODELS (line 32) | const GENAI_MODELS = {
FILE: src/routes/index.js
function Routes (line 4) | function Routes() {
FILE: src/stores/CollectionStore.js
method deleteCollection (line 35) | deleteCollection(collectionId) {
Condensed preview — 217 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (686K chars).
[
{
"path": ".changeset/README.md",
"chars": 510,
"preview": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that wo"
},
{
"path": ".changeset/config.json",
"chars": 346,
"preview": "{\n \"$schema\": \"https://unpkg.com/@changesets/config@3.0.0/schema.json\",\n \"changelog\": [\"@changesets/cli/changelog\", { "
},
{
"path": ".changeset/early-colts-approve.md",
"chars": 100,
"preview": "---\n'flowtestai-app': minor\n---\n\nAdd support for google geminin function calling in generating flow\n"
},
{
"path": ".changeset/honest-bags-fail.md",
"chars": 72,
"preview": "---\n'flowtestai-app': minor\n---\n\nadd support for bearer token auth type\n"
},
{
"path": ".changeset/proud-ants-flash.md",
"chars": 76,
"preview": "---\n'flowtestai-app': minor\n---\n\nallow request headers to be input by users\n"
},
{
"path": ".eslintignore",
"chars": 85,
"preview": "# dependencies\n**/node_modules\n\n# production\n/build\n\n# intermal dependencies\n/server\n"
},
{
"path": ".eslintrc.js",
"chars": 913,
"preview": "module.exports = {\n env: {\n browser: true,\n es2021: true,\n jest: true,\n },\n extends: ['eslint:recommended', "
},
{
"path": ".github/CODEOWNERS",
"chars": 518,
"preview": "# Lines starting with '#' are comments.\n# Each line is a file pattern followed by one or more owners.\n\n# More details ar"
},
{
"path": ".gitignore",
"chars": 429,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n**/node_modules\n/."
},
{
"path": ".npmrc",
"chars": 97,
"preview": "node-linker=hoisted #pnpm requirement when working with electron\nignore-workspace-root-check=true"
},
{
"path": ".prettierrc",
"chars": 204,
"preview": "{\n \"semi\": true,\n \"tabWidth\": 2,\n \"printWidth\": 120,\n \"singleQuote\": true,\n \"trailingComma\": \"all\",\n \"jsxSingleQuo"
},
{
"path": "CONTRIBUTING.md",
"chars": 3985,
"preview": "# Contribution Guidelines\n\nWhen contributing to `FlowTestAI`, whether on GitHub or in other community spaces:\n\n- Be resp"
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "MIT License\n\nCopyright (c) 2023 Sajal Jain\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 7160,
"preview": "# FlowTestAI: Streamlining End-to-End API Testing\n\n[ 2023 Sajal Jain\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "packages/flowtest-cli/README.md",
"chars": 1746,
"preview": "# flowtestai-cli\n\nWith FlowTestAI CLI, you can now run your end to end flows, constructed using FlowTestAI, directly fro"
},
{
"path": "packages/flowtest-cli/bin/axiosClient.js",
"chars": 734,
"preview": "// lib/axiosClient.ts\nconst axios = require('axios');\nconst axiosRetry = require('axios-retry').default;\n\nconst baseUrl "
},
{
"path": "packages/flowtest-cli/bin/index.js",
"chars": 5970,
"preview": "#!/usr/bin/env node\n\nconst yargs = require('yargs/yargs');\nconst { hideBin } = require('yargs/helpers');\nconst chalk = r"
},
{
"path": "packages/flowtest-cli/graph/Graph.js",
"chars": 9184,
"preview": "// assumption is that apis are giving json as output\n\nconst { cloneDeep } = require('lodash');\nconst authNode = require("
},
{
"path": "packages/flowtest-cli/graph/GraphLogger.js",
"chars": 399,
"preview": "const LogLevel = Object.freeze({\n INFO: 'info',\n WARN: 'warn',\n ERROR: 'error',\n});\n\nclass GraphLogger {\n constructo"
},
{
"path": "packages/flowtest-cli/graph/compute/assertnode.js",
"chars": 2290,
"preview": "const AssertOperators = require('../constants/assertOperators');\nconst { computeNodeVariable } = require('./utils');\ncon"
},
{
"path": "packages/flowtest-cli/graph/compute/authnode.js",
"chars": 1639,
"preview": "const { computeVariables } = require('./utils');\nconst Node = require('./node');\nconst chalk = require('chalk');\nconst {"
},
{
"path": "packages/flowtest-cli/graph/compute/node.js",
"chars": 181,
"preview": "class Node {\n constructor(type) {\n this.type = type;\n }\n\n evaluate() {\n throw new Error('Evaluate method must b"
},
{
"path": "packages/flowtest-cli/graph/compute/requestnode.js",
"chars": 6582,
"preview": "const { computeNodeVariables, computeVariables } = require('./utils');\nconst Node = require('./node');\nconst axios = req"
},
{
"path": "packages/flowtest-cli/graph/compute/setvarnode.js",
"chars": 2556,
"preview": "const { computeNodeVariable } = require('./utils');\nconst Node = require('./node');\nconst EvaluateOperators = require('."
},
{
"path": "packages/flowtest-cli/graph/compute/utils.js",
"chars": 2616,
"preview": "const computeNodeVariable = (variable, prevNodeOutputData) => {\n if (variable.type.toLowerCase() === 'string') {\n re"
},
{
"path": "packages/flowtest-cli/graph/compute/utils.test.js",
"chars": 884,
"preview": "const { computeVariables } = require('./utils');\n\ndescribe('Utils', () => {\n it('should compute variables correctly', ("
},
{
"path": "packages/flowtest-cli/graph/constants/assertOperators.js",
"chars": 184,
"preview": "const AssertOperators = {\n isLessThan: 'isLessThan',\n isGreaterThan: 'isGreaterThan',\n isEqualTo: 'isEqualTo',\n isNo"
},
{
"path": "packages/flowtest-cli/graph/constants/evaluateOperators.js",
"chars": 130,
"preview": "const EvaluateOperators = {\n Add: 'Add two numbers',\n Subtract: 'Subtract two numbers',\n};\n\nmodule.exports = EvaluateO"
},
{
"path": "packages/flowtest-cli/package.json",
"chars": 844,
"preview": "{\n \"name\": \"flowtestai\",\n \"version\": \"1.0.2\",\n \"description\": \"CLI to run flow from command line\",\n \"main\": \"bin/ind"
},
{
"path": "packages/flowtest-cli/utils/flowparser/AssertNode.js",
"chars": 485,
"preview": "const { Node } = require('./Node');\n\nclass AssertNode extends Node {\n constructor() {\n super('assertNode');\n }\n\n s"
},
{
"path": "packages/flowtest-cli/utils/flowparser/AuthNode.js",
"chars": 479,
"preview": "const { Node } = require('./Node');\n\nclass AuthNode extends Node {\n constructor() {\n super('authNode');\n }\n\n seria"
},
{
"path": "packages/flowtest-cli/utils/flowparser/DelayNode.js",
"chars": 482,
"preview": "const { Node } = require('./Node');\n\nclass DelayNode extends Node {\n constructor() {\n super('delayNode');\n }\n\n ser"
},
{
"path": "packages/flowtest-cli/utils/flowparser/NestedFlowNode.js",
"chars": 491,
"preview": "const { Node } = require('./Node');\n\nclass NestedFlowNode extends Node {\n constructor() {\n super('flowNode');\n }\n\n "
},
{
"path": "packages/flowtest-cli/utils/flowparser/Node.js",
"chars": 312,
"preview": "class Node {\n constructor(type) {\n this.type = type;\n }\n\n serialize(id, data, metadata) {\n throw new Error('Ser"
},
{
"path": "packages/flowtest-cli/utils/flowparser/OutputNode.js",
"chars": 507,
"preview": "const { Node } = require('./Node');\n\nclass OutputNode extends Node {\n constructor() {\n super('outputNode');\n }\n\n s"
},
{
"path": "packages/flowtest-cli/utils/flowparser/RequestNode.js",
"chars": 488,
"preview": "const { Node } = require('./Node');\n\nclass RequestNode extends Node {\n constructor() {\n super('requestNode');\n }\n\n "
},
{
"path": "packages/flowtest-cli/utils/flowparser/SetVarNode.js",
"chars": 485,
"preview": "const { Node } = require('./Node');\n\nclass SetVarNode extends Node {\n constructor() {\n super('setVarNode');\n }\n\n s"
},
{
"path": "packages/flowtest-cli/utils/flowparser/StartNode.js",
"chars": 385,
"preview": "const { Node } = require('./Node');\n\nclass StartNode extends Node {\n constructor() {\n super('startNode');\n }\n\n ser"
},
{
"path": "packages/flowtest-cli/utils/flowparser/parser.js",
"chars": 7834,
"preview": "const { cloneDeep } = require('lodash');\nconst { AuthNode } = require('./AuthNode');\nconst { NestedFlowNode } = require("
},
{
"path": "packages/flowtest-cli/utils/readfile.js",
"chars": 447,
"preview": "const fs = require('fs');\n\nconst pathExists = (path) => {\n try {\n fs.accessSync(path);\n return true;\n } catch (e"
},
{
"path": "packages/flowtest-electron/.npmrc",
"chars": 64,
"preview": "node-linker=hoisted #pnpm requirement when working with electron"
},
{
"path": "packages/flowtest-electron/CHANGELOG.md",
"chars": 1177,
"preview": "# flowtestai\n\n## 1.2.0\n\n### Minor Changes\n\n- aadbd1b: Collapse sidebar to give more real estate to canvas\n- 780f3c2: gen"
},
{
"path": "packages/flowtest-electron/electron-main.js",
"chars": 3237,
"preview": "// Modules to control application life and create native browser window\nconst { app, BrowserWindow, Menu, shell } = requ"
},
{
"path": "packages/flowtest-electron/electron-menu.js",
"chars": 1172,
"preview": "const { shell } = require('electron');\n\nconst template = [\n {\n label: 'FlowTestAI',\n submenu: [\n { type: 'se"
},
{
"path": "packages/flowtest-electron/notarize.js",
"chars": 550,
"preview": "require('dotenv').config();\nconst { notarize } = require('@electron/notarize');\n\nexports.default = async function notari"
},
{
"path": "packages/flowtest-electron/package.json",
"chars": 2484,
"preview": "{\n \"name\": \"flowtestai-app\",\n \"productName\": \"FlowTestAI\",\n \"version\": \"1.2.0\",\n \"homepage\": \"https://github.com/Flo"
},
{
"path": "packages/flowtest-electron/preload.js",
"chars": 529,
"preview": "const { ipcRenderer, contextBridge } = require('electron');\nconst path = require('path');\nconst { isMacOS } = require('."
},
{
"path": "packages/flowtest-electron/src/ai/flowtestai.js",
"chars": 4263,
"preview": "const BedrockClaudeGenerate = require('./models/bedrock_claude');\nconst GeminiGenerate = require('./models/gemini');\ncon"
},
{
"path": "packages/flowtest-electron/src/ai/models/bedrock_claude.js",
"chars": 4829,
"preview": "const { BedrockChat } = require('@langchain/community/chat_models/bedrock');\nconst { HumanMessage, SystemMessage, BaseMe"
},
{
"path": "packages/flowtest-electron/src/ai/models/gemini.js",
"chars": 4763,
"preview": "const { GoogleGenerativeAI } = require('@google/generative-ai');\nconst { GoogleGenerativeAIEmbeddings } = require('@lang"
},
{
"path": "packages/flowtest-electron/src/ai/models/openai.js",
"chars": 3163,
"preview": "const OpenAI = require('openai');\nconst { MemoryVectorStore } = require('langchain/vectorstores/memory');\nconst { OpenAI"
},
{
"path": "packages/flowtest-electron/src/app/watcher.js",
"chars": 7235,
"preview": "const chokidar = require('chokidar');\nconst path = require('path');\nconst dotenv = require('dotenv');\nconst { PATH_SEPAR"
},
{
"path": "packages/flowtest-electron/src/ipc/axiosClient.js",
"chars": 839,
"preview": "// lib/axiosClient.ts\nconst axios = require('axios');\nconst axiosRetry = require('axios-retry').default;\n\nconst axiosCli"
},
{
"path": "packages/flowtest-electron/src/ipc/collection.js",
"chars": 14009,
"preview": "const fs = require('fs');\nconst path = require('path');\nconst axios = require('axios');\nconst { ipcMain, shell, dialog, "
},
{
"path": "packages/flowtest-electron/src/ipc/settings.js",
"chars": 1185,
"preview": "const { ipcMain, shell, dialog, app } = require('electron');\nconst Settings = require('../store/settings');\n\nconst setti"
},
{
"path": "packages/flowtest-electron/src/store/collection.js",
"chars": 937,
"preview": "const Store = require('electron-store');\nconst { isDirectory } = require('../utils/filemanager/filesystem');\n\nclass Coll"
},
{
"path": "packages/flowtest-electron/src/store/settings.js",
"chars": 670,
"preview": "const Store = require('electron-store');\n\nclass Settings {\n constructor() {\n this.store = new Store();\n }\n\n addLog"
},
{
"path": "packages/flowtest-electron/src/utils/collection.js",
"chars": 4786,
"preview": "const { generateRequestBodyExample } = require('./generate-request-body');\nconst {\n generateQueryParamsExample,\n gener"
},
{
"path": "packages/flowtest-electron/src/utils/collection.test.js",
"chars": 4452,
"preview": "const { generateRequestBodyExample } = require('./generate-request-body.js');\nconst { generatePathParamsExample, generat"
},
{
"path": "packages/flowtest-electron/src/utils/filemanager/createdirectory.js",
"chars": 806,
"preview": "const fs = require('fs');\nconst { isDirectory, pathExists } = require('./filesystem');\nconst path = require('path');\n\nco"
},
{
"path": "packages/flowtest-electron/src/utils/filemanager/createfile.js",
"chars": 747,
"preview": "const fs = require('fs');\nconst dpath = require('path');\nconst { isDirectory, pathExists } = require('./filesystem');\n\nc"
},
{
"path": "packages/flowtest-electron/src/utils/filemanager/deletedirectory.js",
"chars": 427,
"preview": "const fs = require('fs');\nconst { isDirectory } = require('./filesystem');\n\nconst deleteDirectory = (path) => {\n if (!p"
},
{
"path": "packages/flowtest-electron/src/utils/filemanager/deletefile.js",
"chars": 392,
"preview": "const fs = require('fs');\nconst { pathExists } = require('./filesystem');\n\nconst deleteFile = (path) => {\n if (!path) {"
},
{
"path": "packages/flowtest-electron/src/utils/filemanager/filesystem.js",
"chars": 1395,
"preview": "const fs = require('fs');\nconst path = require('path');\n\n/**\n * Determine if the given path is directory\n */\nconst isDir"
},
{
"path": "packages/flowtest-electron/src/utils/filemanager/readfile.js",
"chars": 368,
"preview": "const fs = require('fs');\nconst { pathExists } = require('./filesystem');\n\nconst readFile = (path) => {\n if (!path) {\n "
},
{
"path": "packages/flowtest-electron/src/utils/filemanager/updatefile.js",
"chars": 401,
"preview": "const fs = require('fs');\nconst { pathExists } = require('./filesystem');\n\nconst upadateFile = (path, content) => {\n if"
},
{
"path": "packages/flowtest-electron/src/utils/flowparser/AssertNode.js",
"chars": 485,
"preview": "const { Node } = require('./Node');\n\nclass AssertNode extends Node {\n constructor() {\n super('assertNode');\n }\n\n s"
},
{
"path": "packages/flowtest-electron/src/utils/flowparser/AuthNode.js",
"chars": 479,
"preview": "const { Node } = require('./Node');\n\nclass AuthNode extends Node {\n constructor() {\n super('authNode');\n }\n\n seria"
},
{
"path": "packages/flowtest-electron/src/utils/flowparser/DelayNode.js",
"chars": 482,
"preview": "const { Node } = require('./Node');\n\nclass DelayNode extends Node {\n constructor() {\n super('delayNode');\n }\n\n ser"
},
{
"path": "packages/flowtest-electron/src/utils/flowparser/NestedFlowNode.js",
"chars": 491,
"preview": "const { Node } = require('./Node');\n\nclass NestedFlowNode extends Node {\n constructor() {\n super('flowNode');\n }\n\n "
},
{
"path": "packages/flowtest-electron/src/utils/flowparser/Node.js",
"chars": 312,
"preview": "class Node {\n constructor(type) {\n this.type = type;\n }\n\n serialize(id, data, metadata) {\n throw new Error('Ser"
},
{
"path": "packages/flowtest-electron/src/utils/flowparser/OutputNode.js",
"chars": 507,
"preview": "const { Node } = require('./Node');\n\nclass OutputNode extends Node {\n constructor() {\n super('outputNode');\n }\n\n s"
},
{
"path": "packages/flowtest-electron/src/utils/flowparser/RequestNode.js",
"chars": 488,
"preview": "const { Node } = require('./Node');\n\nclass RequestNode extends Node {\n constructor() {\n super('requestNode');\n }\n\n "
},
{
"path": "packages/flowtest-electron/src/utils/flowparser/SetVarNode.js",
"chars": 485,
"preview": "const { Node } = require('./Node');\n\nclass SetVarNode extends Node {\n constructor() {\n super('setVarNode');\n }\n\n s"
},
{
"path": "packages/flowtest-electron/src/utils/flowparser/StartNode.js",
"chars": 385,
"preview": "const { Node } = require('./Node');\n\nclass StartNode extends Node {\n constructor() {\n super('startNode');\n }\n\n ser"
},
{
"path": "packages/flowtest-electron/src/utils/flowparser/parser.js",
"chars": 7834,
"preview": "const { cloneDeep } = require('lodash');\nconst { AuthNode } = require('./AuthNode');\nconst { NestedFlowNode } = require("
},
{
"path": "packages/flowtest-electron/src/utils/generate-request-body.js",
"chars": 4178,
"preview": "const generateRequestBodyExample = (schema, level = 0, context = { processedSchemas: new Set() }) => {\n if (!schema) re"
},
{
"path": "packages/flowtest-electron/src/utils/generate-request-parameters.js",
"chars": 3017,
"preview": "function generateParameterExample(parameter) {\n if (!parameter.schema) return {};\n\n const schema = parameter.schema;\n\n"
},
{
"path": "packages/flowtest-electron/tests/store/collection-store.test.js",
"chars": 2290,
"preview": "const path = require('path');\n\nconst Collections = require('../../src/store/collection');\nconst createDirectory = requir"
},
{
"path": "packages/flowtest-electron/tests/store/settings-store.test.js",
"chars": 874,
"preview": "const Settings = require('../../src/store/settings');\n\ndescribe('settings-store', () => {\n it('should create and get se"
},
{
"path": "packages/flowtest-electron/tests/test.yaml",
"chars": 21604,
"preview": "openapi: 3.0.3\ninfo:\n title: Swagger Petstore - OpenAPI 3.0\n description: |-\n This is a sample Pet Store Server bas"
},
{
"path": "packages/flowtest-electron/tests/utils/collection-parser.test.js",
"chars": 3949,
"preview": "const SwaggerParser = require('@apidevtools/swagger-parser');\nconst JsonRefs = require('json-refs');\nconst parseOpenAPIS"
},
{
"path": "packages/flowtest-electron/tests/utils/filemanager.test.js",
"chars": 2457,
"preview": "const createDirectory = require('../../src/utils/filemanager/createdirectory');\nconst deleteDirectory = require('../../s"
},
{
"path": "packages/flowtest-electron/tests/utils/flowtest-ai.test.js",
"chars": 2742,
"preview": "const fs = require('fs');\nconst SwaggerParser = require('@apidevtools/swagger-parser');\nconst JsonRefs = require('json-r"
},
{
"path": "packages/flowtest-electron/tests/utils/flowtest-parser.test.js",
"chars": 7003,
"preview": "const { deserialize, serialize } = require('../../src/utils/flowparser/parser');\n\ndescribe('FlowTest parser', () => {\n "
},
{
"path": "packages/flowtest-electron/tests/watcher.test.js",
"chars": 7700,
"preview": "const Watcher = require('../src/app/watcher');\nconst path = require('path');\nconst createDirectory = require('../src/uti"
},
{
"path": "pnpm-workspace.yaml",
"chars": 67,
"preview": "packages:\n - packages/flowtest-cli\n - packages/flowtest-electron\n"
},
{
"path": "postcss.config.js",
"chars": 83,
"preview": "module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n};\n"
},
{
"path": "public/index.html",
"chars": 2044,
"preview": "<!-- prettier-ignore -->\n<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <link rel=\"icon\" hre"
},
{
"path": "public/manifest.json",
"chars": 510,
"preview": "{\n \"short_name\": \"FlowTest\",\n \"name\": \"Drag & Drop UI for openAI function calling\",\n \"icons\": [\n {\n \"src\": \"f"
},
{
"path": "public/robots.txt",
"chars": 67,
"preview": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
},
{
"path": "src/App.css",
"chars": 564,
"preview": ".App {\n text-align: center;\n}\n\n.App-logo {\n height: 40vmin;\n pointer-events: none;\n}\n\n@media (prefers-reduced-motion:"
},
{
"path": "src/App.js",
"chars": 234,
"preview": "import React from 'react';\nimport './App.css';\nimport Routes from './routes';\nimport { HashRouter } from 'react-router-d"
},
{
"path": "src/App.test.js",
"chars": 246,
"preview": "import { render, screen } from '@testing-library/react';\nimport App from './App';\n\ntest('renders learn react link', () ="
},
{
"path": "src/components/atoms/EditableTextItem.js",
"chars": 577,
"preview": "import React from 'react';\nimport { PropTypes } from 'prop-types';\nimport { EditText } from 'react-edit-text';\nimport 'r"
},
{
"path": "src/components/atoms/Editor.js",
"chars": 3021,
"preview": "import React, { useRef, useEffect, useState } from 'react';\n\nimport { basicSetup } from 'codemirror';\nimport { EditorSta"
},
{
"path": "src/components/atoms/Logo.js",
"chars": 269,
"preview": "import React from 'react';\nimport FlowTestAI from 'assets/icons/FlowTestAI.png';\n\nconst AppLogo = ({ styleClasses }) => "
},
{
"path": "src/components/atoms/SelectAuthKeys.js",
"chars": 2429,
"preview": "// ToDo: Remove as we ar not using it\n// import React, { Fragment, useState } from 'react';\n// import { Listbox, Transit"
},
{
"path": "src/components/atoms/SelectEnvironment.js",
"chars": 2660,
"preview": "import React, { Fragment, useState } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Listbox, Transition "
},
{
"path": "src/components/atoms/Tabs.js",
"chars": 3966,
"preview": "import React, { useState } from 'react';\nimport { XMarkIcon } from '@heroicons/react/24/outline';\nimport { useTabStore }"
},
{
"path": "src/components/atoms/ThemeController.js",
"chars": 2693,
"preview": "import React from 'react';\nimport Tippy from '@tippyjs/react';\nimport 'tippy.js/dist/tippy.css';\n\nconst ThemeController "
},
{
"path": "src/components/atoms/common/Button.js",
"chars": 3171,
"preview": "import React from 'react';\nimport { PropTypes } from 'prop-types';\n\n// ToDo: can be more generalized\nconst Button = ({\n "
},
{
"path": "src/components/atoms/common/HomeLoadingScreen.js",
"chars": 577,
"preview": "import React from 'react';\n// import PropTypes from 'prop-types';\nimport FlowGPU from 'assets/icons/Flow-GPU-text-no-bac"
},
{
"path": "src/components/atoms/common/HorizontalDivider.js",
"chars": 278,
"preview": "import React from 'react';\n\nconst HorizontalDivider = ({ themeColor, themeStyles }) => {\n return (\n <div\n class"
},
{
"path": "src/components/atoms/common/LoadingSpinner.js",
"chars": 339,
"preview": "import React, { useState } from 'react';\n\nconst LoadingSpinner = ({ spinnerColor }) => {\n return (\n <div className='"
},
{
"path": "src/components/atoms/common/NumberInput.js",
"chars": 430,
"preview": "import React from 'react';\n\nconst NumberInput = ({ placeHolder, onChangeHandler, name, value }) => {\n return (\n <inp"
},
{
"path": "src/components/atoms/common/TextEditor.js",
"chars": 5127,
"preview": "import React, { useRef, useEffect, useState } from 'react';\n\nimport { basicSetup } from 'codemirror';\nimport { EditorSta"
},
{
"path": "src/components/atoms/common/TextInput.js",
"chars": 625,
"preview": "import React from 'react';\n\nconst TextInput = ({ id, placeHolder, onChangeHandler, name, value, disableState }) => {\n c"
},
{
"path": "src/components/atoms/common/TextInputWithLabel.js",
"chars": 668,
"preview": "import React from 'react';\n\nconst TextInputWithLabel = ({ children, placeHolder, onChangeHandler, name, value, label }) "
},
{
"path": "src/components/atoms/common/TimeoutSelector.js",
"chars": 2918,
"preview": "import React, { useState, Fragment } from 'react';\nimport { ClockIcon } from '@heroicons/react/24/outline';\nimport { Lis"
},
{
"path": "src/components/atoms/flow/FlowNode.js",
"chars": 1469,
"preview": "import React from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Handle, Position } from 'reactflow';\n\nconst "
},
{
"path": "src/components/atoms/flow/NodeHorizontalDivider.js",
"chars": 176,
"preview": "import React from 'react';\n\nconst NodeHorizontalDivider = () => {\n return <div className='bg-background-dark h-[2px] w-"
},
{
"path": "src/components/atoms/flow/Textarea.js",
"chars": 461,
"preview": "import React from 'react';\n\nconst Textarea = ({ id, placeHolder, onChangeHandler, name, value, rows }) => {\n return (\n "
},
{
"path": "src/components/atoms/sidebar/collections/OptionsMenu.js",
"chars": 6121,
"preview": "import React, { Fragment } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Menu, Transition } from '@head"
},
{
"path": "src/components/atoms/sidebar/environments/EnvOptionsMenu.js",
"chars": 2772,
"preview": "import React, { Fragment } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Menu, Transition } from '@head"
},
{
"path": "src/components/atoms/util.js",
"chars": 3213,
"preview": "import _, { isEqual, map } from 'lodash';\n\nexport const compare = (a, b) => {\n const result = {\n different: [],\n "
},
{
"path": "src/components/layouts/SplitPane.js",
"chars": 1487,
"preview": "import React from 'react';\nimport { Allotment } from 'allotment';\nimport 'allotment/dist/style.css';\nimport Workspace fr"
},
{
"path": "src/components/layouts/WithoutSplitPane.js",
"chars": 721,
"preview": "import React from 'react';\nimport { Allotment } from 'allotment';\nimport 'allotment/dist/style.css';\nimport Workspace fr"
},
{
"path": "src/components/molecules/environment/index.js",
"chars": 5023,
"preview": "import React, { useState } from 'react';\nimport useEnvStore from 'stores/EnvStore';\nimport 'react-edit-text/dist/index.c"
},
{
"path": "src/components/molecules/flow/AddNodes.js",
"chars": 17852,
"preview": "import React, { useState } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Popover, Transition } from '@h"
},
{
"path": "src/components/molecules/flow/constants/assertOperators.js",
"chars": 182,
"preview": "const AssertOperators = {\n isLessThan: 'isLessThan',\n isGreaterThan: 'isGreaterThan',\n isEqualTo: 'isEqualTo',\n isNo"
},
{
"path": "src/components/molecules/flow/constants/evaluateOperators.js",
"chars": 128,
"preview": "const EvaluateOperators = {\n Add: 'Add two numbers',\n Subtract: 'Subtract two numbers',\n};\n\nexport default EvaluateOpe"
},
{
"path": "src/components/molecules/flow/constants/requestNodes.js",
"chars": 784,
"preview": "const requestNodes = [\n {\n requestType: 'GET',\n description: 'GET is used to request data from a specified resour"
},
{
"path": "src/components/molecules/flow/edges/ButtonEdge.js",
"chars": 1882,
"preview": "import React from 'react';\nimport { BaseEdge, EdgeLabelRenderer, getSmoothStepPath } from 'reactflow';\nimport useCanvasS"
},
{
"path": "src/components/molecules/flow/flowtestai.js",
"chars": 5340,
"preview": "import { GENAI_MODELS } from 'constants/Common';\nimport { addOrUpdateDotEnvironmentFile } from 'service/collection';\nimp"
},
{
"path": "src/components/molecules/flow/graph/Graph.js",
"chars": 8557,
"preview": "// assumption is that apis are giving json as output\n\nimport { cloneDeep } from 'lodash';\nimport { readFlowTestSync } fr"
},
{
"path": "src/components/molecules/flow/graph/GraphLogger.js",
"chars": 362,
"preview": "export const LogLevel = Object.freeze({\n INFO: 0,\n WARN: 1,\n ERROR: 2,\n});\n\nclass GraphLogger {\n constructor() {\n "
},
{
"path": "src/components/molecules/flow/graph/GraphRun.js",
"chars": 1633,
"preview": "import GraphLogger, { LogLevel } from './GraphLogger';\nimport Graph from './Graph';\nimport { useTabStore } from 'stores/"
},
{
"path": "src/components/molecules/flow/graph/compute/assertnode.js",
"chars": 1814,
"preview": "import AssertOperators from '../../constants/assertOperators';\nimport { computeNodeVariable } from './utils';\nimport Nod"
},
{
"path": "src/components/molecules/flow/graph/compute/authnode.js",
"chars": 1413,
"preview": "import { computeVariables } from './utils';\nimport Node from './node';\nimport { LogLevel } from '../GraphLogger';\n\nclass"
},
{
"path": "src/components/molecules/flow/graph/compute/nestedflownode.js",
"chars": 482,
"preview": "import Graph1 from '../Graph';\nimport Node from './node';\n\nclass nestedFlowNode extends Node {\n constructor(nodes, edge"
},
{
"path": "src/components/molecules/flow/graph/compute/node.js",
"chars": 179,
"preview": "class Node {\n constructor(type) {\n this.type = type;\n }\n\n evaluate() {\n throw new Error('Evaluate method must b"
},
{
"path": "src/components/molecules/flow/graph/compute/requestnode.js",
"chars": 4099,
"preview": "import { cloneDeep } from 'lodash';\nimport { computeNodeVariables, computeVariables } from '../compute/utils';\nimport { "
},
{
"path": "src/components/molecules/flow/graph/compute/setvarnode.js",
"chars": 2540,
"preview": "import { computeNodeVariable } from './utils';\nimport Node from './node';\nimport EvaluateOperators from '../../constants"
},
{
"path": "src/components/molecules/flow/graph/compute/utils.js",
"chars": 2547,
"preview": "export const computeNodeVariable = (variable, prevNodeOutputData) => {\n if (variable.type.toLowerCase() === 'string') {"
},
{
"path": "src/components/molecules/flow/graph/compute/utils.test.js",
"chars": 884,
"preview": "const { computeVariables } = require('./utils');\n\ndescribe('Utils', () => {\n it('should compute variables correctly', ("
},
{
"path": "src/components/molecules/flow/index.js",
"chars": 7886,
"preview": "import React, { useCallback, useMemo, useState } from 'react';\nimport { PropTypes } from 'prop-types';\nimport ReactFlow,"
},
{
"path": "src/components/molecules/flow/nodes/AssertNode.js",
"chars": 6708,
"preview": "import React, { useState } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Handle, Position } from 'react"
},
{
"path": "src/components/molecules/flow/nodes/AuthNode.js",
"chars": 7808,
"preview": "import React, { useState, Fragment } from 'react';\nimport { PropTypes } from 'prop-types';\nimport FlowNode from 'compone"
},
{
"path": "src/components/molecules/flow/nodes/DelayNode.js",
"chars": 1012,
"preview": "import * as React from 'react';\nimport { PropTypes } from 'prop-types';\nimport FlowNode from 'components/atoms/flow/Flow"
},
{
"path": "src/components/molecules/flow/nodes/FormDataSelector.js",
"chars": 2873,
"preview": "import React, { useState, Fragment } from 'react';\nimport { ClockIcon } from '@heroicons/react/24/outline';\nimport { Lis"
},
{
"path": "src/components/molecules/flow/nodes/NestedFlowNode.js",
"chars": 3183,
"preview": "import React, { useState } from 'react';\nimport { PropTypes } from 'prop-types';\nimport FlowNode from 'components/atoms/"
},
{
"path": "src/components/molecules/flow/nodes/OutputNode.js",
"chars": 1760,
"preview": "import * as React from 'react';\nimport { PropTypes } from 'prop-types';\nimport FlowNode from 'components/atoms/flow/Flow"
},
{
"path": "src/components/molecules/flow/nodes/RequestBody.js",
"chars": 14027,
"preview": "import React, { Fragment, useState, useRef } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Listbox, Tra"
},
{
"path": "src/components/molecules/flow/nodes/RequestNode.js",
"chars": 18240,
"preview": "import React, { useEffect, useState, Fragment } from 'react';\nimport { PropTypes } from 'prop-types';\nimport RequestBody"
},
{
"path": "src/components/molecules/flow/nodes/SetVarNode.js",
"chars": 9683,
"preview": "import React from 'react';\nimport { PropTypes } from 'prop-types';\nimport FlowNode from 'components/atoms/flow/FlowNode'"
},
{
"path": "src/components/molecules/flow/utils.js",
"chars": 3676,
"preview": "import { isEqual, reduce, map } from 'lodash';\nimport requestNodes from './constants/requestNodes';\n\nexport const orderN"
},
{
"path": "src/components/molecules/footers/MainFooter.js",
"chars": 3157,
"preview": "import React, { useState } from 'react';\nimport {\n ArrowLeftEndOnRectangleIcon,\n ArrowRightStartOnRectangleIcon,\n Cog"
},
{
"path": "src/components/molecules/headers/MainHeader.js",
"chars": 1534,
"preview": "import React from 'react';\nimport ThemeController from 'components/atoms/ThemeController';\nimport AppLogo from 'componen"
},
{
"path": "src/components/molecules/headers/SideBarHeader.js",
"chars": 1188,
"preview": "import React from 'react';\nimport { UserIcon } from '@heroicons/react/20/solid';\nimport { ChevronRightIcon } from '@hero"
},
{
"path": "src/components/molecules/headers/SideBarSubHeader.js",
"chars": 1751,
"preview": "import React, { useState } from 'react';\nimport { FolderArrowDownIcon } from '@heroicons/react/24/outline';\nimport { Plu"
},
{
"path": "src/components/molecules/headers/TabPanelHeader.js",
"chars": 6206,
"preview": "import React, { useState } from 'react';\nimport { SparklesIcon, DocumentTextIcon } from '@heroicons/react/24/outline';\ni"
},
{
"path": "src/components/molecules/headers/WorkspaceHeader.js",
"chars": 1952,
"preview": "import React, { useState } from 'react';\nimport { PlusIcon } from '@heroicons/react/20/solid';\nimport Tabs from '../../a"
},
{
"path": "src/components/molecules/modals/AddEnvVariableModal.js",
"chars": 5210,
"preview": "import React, { Fragment, useState } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Dialog, Transition }"
},
{
"path": "src/components/molecules/modals/ConfirmActionModal.js",
"chars": 3014,
"preview": "import React, { Fragment } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Dialog, Transition } from '@he"
},
{
"path": "src/components/molecules/modals/EditEnvVariableModal.js",
"chars": 3555,
"preview": "import React, { Fragment, useState, useEffect } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Dialog, T"
},
{
"path": "src/components/molecules/modals/GenAIUsageDisclaimer.js",
"chars": 5612,
"preview": "import React, { Fragment, useState, useEffect } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Dialog, T"
},
{
"path": "src/components/molecules/modals/GenerateFlowTestModal.js",
"chars": 32732,
"preview": "import React, { Fragment, useState, useEffect } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Dialog, T"
},
{
"path": "src/components/molecules/modals/ImportCollectionModal.js",
"chars": 7002,
"preview": "import React, { Fragment, useRef, useState } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Dialog, Tran"
},
{
"path": "src/components/molecules/modals/OpenCollectionModal.js",
"chars": 6992,
"preview": "import React, { Fragment, useRef, useState } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Dialog, Tran"
},
{
"path": "src/components/molecules/modals/OutputNodeExpandedModal.js",
"chars": 2805,
"preview": "import React, { Fragment, useState } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Dialog, Transition, "
},
{
"path": "src/components/molecules/modals/SaveFlowModal.js",
"chars": 2242,
"preview": "import React from 'react';\nimport { PropTypes } from 'prop-types';\nimport { InboxArrowDownIcon } from '@heroicons/react/"
},
{
"path": "src/components/molecules/modals/SettingsModal.js",
"chars": 10364,
"preview": "import React, { Fragment, useState, useEffect } from 'react';\nimport { Dialog, Transition, Tab } from '@headlessui/react"
},
{
"path": "src/components/molecules/modals/create/NewCollectionModal.js",
"chars": 2972,
"preview": "import React, { Fragment } from 'react';\nimport { Dialog, Transition } from '@headlessui/react';\nimport Button from 'com"
},
{
"path": "src/components/molecules/modals/flow/AddVariableModal.js",
"chars": 4694,
"preview": "import React, { useState, Fragment } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Dialog, Transition }"
},
{
"path": "src/components/molecules/modals/flow/NewFlowTestModal.js",
"chars": 15339,
"preview": "import React, { Fragment, useState } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Dialog, Transition, "
},
{
"path": "src/components/molecules/modals/sidebar/NewEnvironmentFileModal.js",
"chars": 9544,
"preview": "import React, { Fragment, useState } from 'react';\nimport { Dialog, Transition, Listbox } from '@headlessui/react';\nimpo"
},
{
"path": "src/components/molecules/modals/sidebar/NewLabelModal.js",
"chars": 8295,
"preview": "import React, { Fragment, useState } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { Dialog, Transition }"
},
{
"path": "src/components/molecules/sideSheets/FlowLogs.js",
"chars": 8801,
"preview": "import React, { useState } from 'react';\nimport {\n ShieldCheckIcon,\n BarsArrowUpIcon,\n ExclamationTriangleIcon,\n XCi"
},
{
"path": "src/components/molecules/sidebar/Empty.js",
"chars": 1754,
"preview": "import React, { useState } from 'react';\nimport { FolderArrowDownIcon } from '@heroicons/react/24/outline';\nimport { Plu"
},
{
"path": "src/components/molecules/sidebar/content/Collection.js",
"chars": 4908,
"preview": "import React, { useState } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { ArchiveBoxIcon, FolderIcon, Do"
},
{
"path": "src/components/molecules/sidebar/content/Collections.js",
"chars": 4883,
"preview": "import React, { useState } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { OBJ_TYPES } from 'constants/Co"
},
{
"path": "src/components/molecules/sidebar/content/Environment.js",
"chars": 4860,
"preview": "import React, { useState } from 'react';\nimport PropTypes from 'prop-types';\nimport { ArchiveBoxIcon, DocumentIcon, Tras"
},
{
"path": "src/components/molecules/sidebar/content/Environments.js",
"chars": 2148,
"preview": "import React, { useState } from 'react';\nimport PropTypes from 'prop-types';\nimport Environment from './Environment';\nim"
},
{
"path": "src/components/molecules/sidebar/content/index.js",
"chars": 947,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport useNavigationStore from 'stores/AppNavBarStore';\ni"
},
{
"path": "src/components/molecules/workspace/EmptyWorkSpaceContent.js",
"chars": 6515,
"preview": "import React, { useState } from 'react';\nimport Tippy from '@tippyjs/react';\nimport { RectangleStackIcon, Square3Stack3D"
},
{
"path": "src/components/molecules/workspace/WorkspaceContent.js",
"chars": 1751,
"preview": "import React from 'react';\nimport Flow, { init } from 'components/molecules/flow';\nimport { useTabStore } from 'stores/T"
},
{
"path": "src/components/organisms/AppNavBar.js",
"chars": 5376,
"preview": "import React from 'react';\nimport { Square3Stack3DIcon, RectangleStackIcon, ClockIcon } from '@heroicons/react/20/solid'"
},
{
"path": "src/components/organisms/SideBar.js",
"chars": 774,
"preview": "import React from 'react';\nimport useCollectionStore from 'stores/CollectionStore';\nimport SideBarHeader from 'component"
},
{
"path": "src/components/organisms/workspace/Workspace.js",
"chars": 466,
"preview": "import React from 'react';\nimport WorkspaceHeader from 'components/molecules/headers/WorkspaceHeader';\nimport WorkspaceC"
},
{
"path": "src/components/pages/Home.js",
"chars": 1707,
"preview": "import React from 'react';\nimport SplitPane from '../layouts/SplitPane';\nimport MainHeader from '../molecules/headers/Ma"
},
{
"path": "src/constants/AppNavBar.js",
"chars": 766,
"preview": "export const AppNavBarItems = {\n // default value\n collections: {\n displayValue: 'Collections',\n value: 'COLLECT"
},
{
"path": "src/constants/Common.js",
"chars": 780,
"preview": "export const FLOW_FILE_SUFFIX_REGEX = /^.+\\.flow$/gm; // regex to check the file extension of flow files\n\nexport const C"
},
{
"path": "src/constants/ImportCollectionTypes.js",
"chars": 136,
"preview": "const ImportCollectionTypes = {\n YAML: 'yaml',\n OPEN_API: 'open_api',\n POSTMAN: 'postman',\n};\n\nexport default ImportC"
},
{
"path": "src/constants/ModalNames.js",
"chars": 147,
"preview": "export const ModalNames = {\n IMPORT_COLLECTION_MODAL: 'import-collection-modal',\n CREATE_NEW_COLLECTION_MODAL: 'create"
},
{
"path": "src/constants/WorkspaceDirectory.js",
"chars": 689,
"preview": "export const DirectoryOptionsActions = {\n addNewFolder: {\n displayValue: 'New Folder',\n value: 'new-folder',\n "
},
{
"path": "src/constants/sidebar/Environnments.js",
"chars": 171,
"preview": "export const OptionsMenuActions = {\n addNewEnvironment: {\n displayValue: 'New Environment',\n value: 'new-environm"
},
{
"path": "src/index.css",
"chars": 3680,
"preview": "@import 'tailwindcss/base';\n@import 'tailwindcss/components';\n@import 'tailwindcss/utilities';\n@import url('https://font"
},
{
"path": "src/index.js",
"chars": 536,
"preview": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport './index.css';\nimport App from './App';\nimpor"
},
{
"path": "src/ipc/collection.js",
"chars": 2696,
"preview": "import useCollectionStore from 'stores/CollectionStore';\nimport { useEffect } from 'react';\n\nconst registerMainEventHand"
},
{
"path": "src/ipc/settings.js",
"chars": 1086,
"preview": "import { useEffect } from 'react';\nimport useSettingsStore from 'stores/SettingsStore';\n\nconst registerSettingsEventHand"
},
{
"path": "src/reportWebVitals.js",
"chars": 364,
"preview": "const reportWebVitals = (onPerfEntry) => {\n if (onPerfEntry && onPerfEntry instanceof Function) {\n import('web-vital"
},
{
"path": "src/routes/Main.js",
"chars": 262,
"preview": "import React from 'react';\nimport { Outlet } from 'react-router';\nimport Home from '../components/pages/Home';\n\nconst Ma"
},
{
"path": "src/routes/index.js",
"chars": 140,
"preview": "import { useRoutes } from 'react-router';\nimport Main from 'routes/Main';\n\nexport default function Routes() {\n return u"
},
{
"path": "src/service/collection.js",
"chars": 15133,
"preview": "import useCollectionStore from '../stores/CollectionStore';\nimport { v4 as uuidv4 } from 'uuid';\nimport { findItemInColl"
},
{
"path": "src/service/settings.js",
"chars": 768,
"preview": "export const addLogSyncConfig = (enabled, hostUrl, accessId, accessKey) => {\n try {\n const { ipcRenderer } = window;"
},
{
"path": "src/setupTests.js",
"chars": 241,
"preview": "// jest-dom adds custom jest matchers for asserting on DOM nodes.\n// allows you to do things like:\n// expect(element).to"
}
]
// ... and 17 more files (download for full content)
About this extraction
This page contains the full source code of the FlowTestAI/FlowTest GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 217 files (635.3 KB), approximately 150.5k tokens, and a symbol index with 211 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.