├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_TEMPLATE.md │ ├── FEEDBACK_TEMPLATE.md │ └── QUESTION_TEMPLATE.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .prettierrc.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin └── superface ├── docs └── LogoGreen.png ├── fixtures ├── compiled │ ├── strict.suma.ast.json │ ├── strict.supr.ast.json │ └── with-examples.supr.ast.json ├── install │ └── playground │ │ ├── .gitignore │ │ ├── README.md │ │ ├── character-information.supr │ │ ├── spaceship-information.supr │ │ └── superface │ │ ├── .gitignore │ │ └── super.json ├── invalid-map.twilio.suma ├── invalid.suma ├── io │ └── initial │ │ └── test.txt ├── lint │ ├── bar.provider.suma │ ├── bar.supr │ ├── example │ │ └── profile-with-examples.supr.ast.json │ ├── foo.provider.suma │ ├── foo.supr │ ├── my-scope │ │ ├── map2.suma │ │ ├── my-profile.supr │ │ ├── my-profile.twilio.suma │ │ ├── my-profile.tyntec.suma │ │ └── my-profile2.supr │ └── twilio.provider.json ├── profiles │ ├── communication │ │ ├── send-email.json │ │ ├── send-email.supr │ │ ├── send-email.supr.ast.json │ │ ├── send-email@1.0.1.json │ │ ├── send-email@1.0.1.supr │ │ └── send-email@1.0.1.supr.ast.json │ └── starwars │ │ ├── character-information.json │ │ ├── character-information.supr │ │ ├── character-information.supr.ast.json │ │ ├── character-information@1.0.2.json │ │ ├── character-information@1.0.2.supr │ │ ├── character-information@1.0.2.supr.ast.json │ │ ├── maps │ │ ├── swapi.character-information.json │ │ ├── swapi.character-information.suma │ │ ├── swapi.character-information.suma.ast.json │ │ ├── swapi.character-information@1.0.2.json │ │ ├── swapi.character-information@1.0.2.suma │ │ ├── swapi.character-information@1.0.2.suma.ast.json │ │ ├── unverified-swapi.character-information.json │ │ ├── unverified-swapi.character-information.suma │ │ └── unverified-swapi.character-information.suma.ast.json │ │ ├── spaceship-information.json │ │ ├── spaceship-information.supr │ │ └── spaceship-information.supr.ast.json ├── providers │ ├── azure-cognitive-services.json │ ├── empty.json │ ├── mailchimp.json │ ├── mailgun.json │ ├── mock.json │ ├── provider-without-security.json │ ├── sendgrid.json │ ├── swapi.json │ └── unverified-swapi.json ├── strict.suma ├── strict.supr ├── valid-map.provider.suma ├── valid.suma └── with-examples.supr ├── jest.config.integration.js ├── jest.config.js ├── package.json ├── src ├── commands │ ├── execute.test.ts │ ├── execute.ts │ ├── login.integration.test.ts │ ├── login.test.ts │ ├── login.ts │ ├── logout.integration.test.ts │ ├── logout.test.ts │ ├── logout.ts │ ├── map.test.ts │ ├── map.ts │ ├── new.test.ts │ ├── new.ts │ ├── prepare.test.ts │ ├── prepare.ts │ ├── whoami.integration.test.ts │ ├── whoami.test.ts │ └── whoami.ts ├── common │ ├── chalk-template.ts │ ├── command.abstract.ts │ ├── common.test.ts │ ├── document.interfaces.ts │ ├── document.ts │ ├── error.test.ts │ ├── error.ts │ ├── file-structure.test.ts │ ├── file-structure.ts │ ├── format.test.ts │ ├── format.ts │ ├── http.test.ts │ ├── http.ts │ ├── index.ts │ ├── io.test.ts │ ├── io.ts │ ├── log.ts │ ├── messages.ts │ ├── netrc.test.ts │ ├── netrc.ts │ ├── output-stream.test.ts │ ├── output-stream.ts │ ├── package-manager.test.ts │ ├── package-manager.ts │ ├── polling.test.ts │ ├── polling.ts │ ├── profile.ts │ ├── provider.test.ts │ ├── provider.ts │ └── ux.ts ├── index.ts ├── logic │ ├── application-code │ │ ├── application-code.test.ts │ │ ├── application-code.ts │ │ ├── dotenv │ │ │ ├── dotenv.test.ts │ │ │ ├── dotenv.ts │ │ │ ├── index.ts │ │ │ ├── onesdk-log.ts │ │ │ └── onesdk-token.ts │ │ ├── index.ts │ │ ├── input │ │ │ ├── example │ │ │ │ ├── build.ts │ │ │ │ ├── structure-tree │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── parse.test.ts │ │ │ │ │ └── parse.ts │ │ │ │ └── usecase-example.ts │ │ │ ├── prepare-usecase-input.test.ts │ │ │ ├── prepare-usecase-input.ts │ │ │ └── templates │ │ │ │ ├── array.ts │ │ │ │ ├── boolean.ts │ │ │ │ ├── index.ts │ │ │ │ ├── input.ts │ │ │ │ ├── number.ts │ │ │ │ ├── object.ts │ │ │ │ ├── string.ts │ │ │ │ └── template-renderer │ │ │ │ ├── helpers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── renderer.ts │ │ │ │ └── template.interface.ts │ │ ├── js │ │ │ ├── application-code.test.ts │ │ │ ├── application-code.ts │ │ │ ├── index.ts │ │ │ ├── parameters.test.ts │ │ │ ├── parameters.ts │ │ │ ├── security.test.ts │ │ │ └── security.ts │ │ └── python │ │ │ ├── application-code.test.ts │ │ │ ├── application-code.ts │ │ │ ├── index.ts │ │ │ ├── parameters.test.ts │ │ │ ├── parameters.ts │ │ │ ├── security.test.ts │ │ │ └── security.ts │ ├── execution │ │ ├── execute.ts │ │ ├── index.ts │ │ ├── runner.test.ts │ │ └── runner.ts │ ├── index.ts │ ├── login.test.ts │ ├── login.ts │ ├── map.test.ts │ ├── map.ts │ ├── new.test.ts │ ├── new.ts │ ├── prepare.test.ts │ ├── prepare.ts │ └── project │ │ ├── index.ts │ │ ├── js │ │ ├── index.ts │ │ ├── js.test.ts │ │ └── js.ts │ │ ├── prepare-project.ts │ │ └── python │ │ ├── index.ts │ │ ├── python.test.ts │ │ └── python.ts └── test │ ├── compile-fixtures.ts │ ├── map-document-node.ts │ ├── mock-std.ts │ ├── profile-document-node.ts │ ├── provider-json.ts │ └── utils.ts ├── tsconfig.json ├── tsconfig.release.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | tsconfigRootDir: __dirname, 6 | project: ['./tsconfig.json'], 7 | }, 8 | plugins: [ 9 | '@typescript-eslint', 10 | 'jest', 11 | 'simple-import-sort', 12 | 'jest-formatting', 13 | ], 14 | extends: [ 15 | 'eslint:recommended', 16 | 'plugin:@typescript-eslint/eslint-recommended', 17 | 'plugin:@typescript-eslint/recommended', 18 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 19 | 'plugin:import/errors', 20 | 'plugin:import/warnings', 21 | 'plugin:jest/recommended', 22 | 'plugin:jest-formatting/recommended', 23 | 'prettier', 24 | ], 25 | rules: { 26 | 'newline-before-return': 'error', 27 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 28 | 'simple-import-sort/imports': 'error', 29 | 'sort-imports': 'off', 30 | 'import/first': 'error', 31 | 'import/newline-after-import': 'error', 32 | 'import/no-duplicates': 'error', 33 | 'no-multiple-empty-lines': 'error', 34 | 'lines-between-class-members': 'off', 35 | '@typescript-eslint/lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true, exceptAfterOverload: true }], 36 | '@typescript-eslint/require-await': 'off', 37 | 'spaced-comment': ['error', 'always'], 38 | 'quotes': 'off', 39 | '@typescript-eslint/quotes': ['warn', 'single', { avoidEscape: true, allowTemplateLiterals: true }], 40 | '@typescript-eslint/no-inferrable-types': ['warn'], 41 | 'no-implicit-coercion': 'error', 42 | '@typescript-eslint/strict-boolean-expressions': 'error', 43 | '@typescript-eslint/explicit-member-accessibility': [ 44 | 'error', 45 | { accessibility: 'explicit', overrides: { constructors: 'no-public' } }, 46 | ], 47 | '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], 48 | }, 49 | settings: { 50 | 'import/parsers': { 51 | '@typescript-eslint/parser': ['.ts'], 52 | }, 53 | 'import/resolver': { 54 | typescript: { 55 | alwaysTryTypes: true, 56 | }, 57 | }, 58 | }, 59 | overrides: [{ 60 | files: '*.test.ts', 61 | rules: { 62 | '@typescript-eslint/no-explicit-any': 'off', 63 | '@typescript-eslint/no-unsafe-return': 'off', 64 | '@typescript-eslint/no-unsafe-assignment': 'off', 65 | '@typescript-eslint/no-non-null-assertion': 'off', 66 | '@typescript-eslint/unbound-method': 'off', 67 | } 68 | }], 69 | }; 70 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Use this template for reporting a bug. 4 | title: "[BUG]" 5 | labels: "Type: Bug" 6 | --- 7 | 8 | Provide a general summary of the issue in the Title above 9 | 10 | ## Expected Behavior 11 | Tell us what should happen 12 | 13 | ## Current Behavior 14 | Tell us what happens instead of the expected behavior 15 | 16 | ## Possible Solution 17 | Not obligatory, but suggest a fix/reason for the bug 18 | 19 | ## Steps to Reproduce 20 | Provide a link to a live example, or an unambiguous set of steps to reproduce this bug. Include code to reproduce, if relevant 21 | 1. 22 | 2. 23 | 3. 24 | 4. 25 | 26 | ## Your Environment 27 | Include as many relevant details about the environment you experienced the bug in. Preferably include result of `superface --version` 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEEDBACK_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Superface CLI Feedback 3 | about: Give us your feedback and suggestions on the new Superface CLI 4 | title: "[Feedback]" 5 | labels: 'cli feedback' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ### API Provider/Prompt used 13 | 14 | 15 | - Documentation URL: 16 | - API provider: 17 | - Prompt: 18 | 19 | ### Feedback 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/QUESTION_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Use this template for asking a Question. 4 | title: "[QUESTION]" 5 | labels: "Type: Question" 6 | --- 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | allow: 8 | - dependency-name: "@superfaceai/*" 9 | dependency-type: "direct" 10 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | 10 | ## Types of changes 11 | 12 | - [ ] Bug fix (non-breaking change which fixes an issue) 13 | - [ ] New feature (non-breaking change which adds functionality) 14 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 15 | 16 | ## Checklist: 17 | 18 | 19 | 20 | - [ ] I have updated the documentation accordingly. For updating Oclif commands documentation use [oclif-dev](https://github.com/oclif/dev-cli#oclif-dev-readme). 21 | - [ ] I have read the **CONTRIBUTING** document. 22 | - [ ] I haven't repeated the code. (DRY) 23 | - [ ] I have added tests to cover my changes. 24 | - [ ] All new and existing tests passed. 25 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | # Setup environment and checkout the project master 10 | - name: Setup Node.js environment 11 | uses: actions/setup-node@v2 12 | with: 13 | registry-url: https://registry.npmjs.org/ 14 | scope: "@superfaceai" 15 | node-version: "14" 16 | 17 | - name: Checkout 18 | uses: actions/checkout@v2.3.4 19 | 20 | # Setup yarn cache 21 | - name: Get yarn cache directory path 22 | id: yarn-cache-dir-path 23 | run: echo "::set-output name=dir::$(yarn cache dir)" 24 | - uses: actions/cache@v2.1.3 25 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 26 | with: 27 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 28 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 29 | restore-keys: | 30 | ${{ runner.os }}-yarn- 31 | 32 | # Install and run tests 33 | - name: Install dependencies 34 | run: yarn install 35 | - name: Build 36 | run: yarn build 37 | - name: Test 38 | run: yarn test --bail 39 | 40 | lint: 41 | runs-on: ubuntu-latest 42 | steps: 43 | # Setup environment and checkout the project master 44 | - name: Setup Node.js environment 45 | uses: actions/setup-node@v2 46 | with: 47 | registry-url: https://registry.npmjs.org/ 48 | scope: "@superfaceai" 49 | node-version: "14" 50 | 51 | - name: Checkout 52 | uses: actions/checkout@v2.3.4 53 | 54 | # Setup yarn cache 55 | - name: Get yarn cache directory path 56 | id: yarn-cache-dir-path 57 | run: echo "::set-output name=dir::$(yarn cache dir)" 58 | - uses: actions/cache@v2.1.3 59 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 60 | with: 61 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 62 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 63 | restore-keys: | 64 | ${{ runner.os }}-yarn- 65 | 66 | # Install and run lint 67 | - name: Install dependencies 68 | run: yarn install 69 | - name: Lint 70 | run: yarn lint 71 | 72 | license-check: 73 | runs-on: ubuntu-latest 74 | steps: 75 | # Setup environment and checkout the project master 76 | - name: Setup Node.js environment 77 | uses: actions/setup-node@v2 78 | with: 79 | registry-url: https://registry.npmjs.org/ 80 | scope: "@superfaceai" 81 | node-version: "14" 82 | 83 | - name: Checkout 84 | uses: actions/checkout@v2.3.4 85 | 86 | # Setup yarn cache 87 | - name: Get yarn cache directory path 88 | id: yarn-cache-dir-path 89 | run: echo "::set-output name=dir::$(yarn cache dir)" 90 | - uses: actions/cache@v2.1.3 91 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 92 | with: 93 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 94 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 95 | restore-keys: | 96 | ${{ runner.os }}-yarn- 97 | 98 | # Install and run license checker 99 | - name: Install dependencies 100 | run: yarn install 101 | - name: Install License checker 102 | run: | 103 | yarn global add license-checker 104 | echo "$(yarn global bin)" >> $GITHUB_PATH 105 | - name: Check licenses 106 | run: "license-checker --onlyAllow '0BDS;MIT;Apache-2.0;ISC;BSD-3-Clause;BSD-2-Clause;CC-BY-4.0;CC-BY-3.0;BSD;CC0-1.0;Unlicense' --summary" 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # yalc data 107 | .yalc/ 108 | yalc.lock 109 | 110 | testground 111 | /test/ 112 | 113 | # Superface Comlink artifacts 114 | superface/ -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | quoteProps: "as-needed", 8 | trailingComma: "es5", 9 | bracketSpacing: true, 10 | arrowParens: "avoid", 11 | endOfLine: "lf", 12 | } 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Superface 2 | 3 | We welcome contributions to our [open source project on GitHub](https://github.com/superfaceai/cli). 4 | 5 | **Please open an issue first if you want to make larger changes** 6 | 7 | ## Introduction 8 | 9 | We are glad that you are interested in Superface in the way of contributing. We value the pro-community developers as you are. 10 | 11 | ## Help the community 12 | 13 | 1. Report an Error or a Bug 14 | 2. Contribute to the Documentation 15 | 3. Provide Support on Issues 16 | 17 | ## Need help? 18 | 19 | If you have any question about this project (for example, how to use it) or if you just need some clarification about anything, please open an Issue at [Issues](https://github.com/superfaceai/cli/issues). 20 | 21 | ## Contributing 22 | 23 | Follow these steps: 24 | 25 | 1. **Fork & Clone** the repository 26 | 2. **Setup** the Superface CLI 27 | - Install packages with `yarn install` or `npm install` 28 | - Build with `yarn build` or `npm run build` 29 | - Run tests with `yarn test` or `npm test` 30 | - Lint code with `yarn lint:fix` or `npm run lint:fix` 31 | 3. **Update** [CHANGELOG](CHANGELOG.md). See https://keepachangelog.com/en/1.0.0/ 32 | 4. **Commit** changes to your own branch by convention. See https://www.conventionalcommits.org/en/v1.0.0/ 33 | 5. **Push** your work back up to your fork 34 | 6. Submit a **Pull Request** so that we can review your changes 35 | 36 | **NOTE: Be sure to merge the latest from "upstream" before making a pull request.** 37 | 38 | **NOTE: Please open an issue first if you want to make larger changes** 39 | 40 | ### Contribute by reporting bugs 41 | 42 | If you are experiencing bug or undocumented behavior please open an Issue with bug template at [Issues](https://github.com/superfaceai/cli/issues). 43 | 44 | ### Contribute to documentation 45 | 46 | Help us improve Superface documentation, you can report typos, improve examples. 47 | 48 | **NOTE: If editing the README, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification.** 49 | 50 | ## Copyright and Licensing 51 | 52 | The Superface CLI open source project is licensed under the [MIT License](LICENSE). 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Superface s.r.o. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/superface: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('@oclif/command') 4 | .run() 5 | .then(require('@oclif/command/flush')) 6 | .catch(require('@oclif/errors/handle')); 7 | -------------------------------------------------------------------------------- /docs/LogoGreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfaceai/cli/34d8fc7a26bc2e2c0a41746cc90f89715280274e/docs/LogoGreen.png -------------------------------------------------------------------------------- /fixtures/install/playground/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /fixtures/install/playground/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfaceai/cli/34d8fc7a26bc2e2c0a41746cc90f89715280274e/fixtures/install/playground/README.md -------------------------------------------------------------------------------- /fixtures/install/playground/character-information.supr: -------------------------------------------------------------------------------- 1 | name = "starwars/character-information" 2 | version = "1.0.0" 3 | 4 | "Starwars" 5 | usecase RetrieveCharacterInformation safe { 6 | input { 7 | characterName 8 | } 9 | 10 | result { 11 | height 12 | weight 13 | yearOfBirth 14 | } 15 | 16 | error { 17 | message 18 | } 19 | } -------------------------------------------------------------------------------- /fixtures/install/playground/spaceship-information.supr: -------------------------------------------------------------------------------- 1 | name = "starwars/spaceship-information" 2 | version = "1.0.0" 3 | 4 | "Starwars Spaceship Information" 5 | usecase RetrieveSpaceshipInformation safe { 6 | input { 7 | spaceshipName 8 | } 9 | 10 | result { 11 | name string! 12 | model string! 13 | pilots [string] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /fixtures/install/playground/superface/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /fixtures/install/playground/superface/super.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "starwars/character-information": { 4 | "file": "../character-information.supr", 5 | "providers": {} 6 | } 7 | }, 8 | "providers": {} 9 | } -------------------------------------------------------------------------------- /fixtures/invalid-map.twilio.suma: -------------------------------------------------------------------------------- 1 | profile = "example/profile@1" 2 | provider = "twilio" 3 | 4 | map Foo { 5 | map result true 6 | map result {} 7 | map result { 8 | f1: 'string' 9 | } 10 | 11 | map error {} 12 | } -------------------------------------------------------------------------------- /fixtures/invalid.suma: -------------------------------------------------------------------------------- 1 | profile = "example/profile@1" 2 | 3 | map Foo { 4 | 5 | } -------------------------------------------------------------------------------- /fixtures/io/initial/test.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfaceai/cli/34d8fc7a26bc2e2c0a41746cc90f89715280274e/fixtures/io/initial/test.txt -------------------------------------------------------------------------------- /fixtures/lint/bar.provider.suma: -------------------------------------------------------------------------------- 1 | profile = "bar@1.0" 2 | provider = "provider" 3 | 4 | """ 5 | Bar map 6 | """ 7 | map Bar { 8 | map result true 9 | } 10 | -------------------------------------------------------------------------------- /fixtures/lint/bar.supr: -------------------------------------------------------------------------------- 1 | name = "bar" 2 | version = "1.0.0" 3 | 4 | """ 5 | Bar usecase 6 | """ 7 | usecase Bar { 8 | result number 9 | } 10 | -------------------------------------------------------------------------------- /fixtures/lint/foo.provider.suma: -------------------------------------------------------------------------------- 1 | profile = "unknown@1.0" 2 | provider = "provider" 3 | 4 | """ 5 | Foo map 6 | """ 7 | map Foo { 8 | map result true 9 | } 10 | -------------------------------------------------------------------------------- /fixtures/lint/foo.supr: -------------------------------------------------------------------------------- 1 | name = "foo" 2 | version = "1.0.0" 3 | 4 | """ 5 | Foo usecase 6 | """ 7 | usecase Foo { 8 | result boolean 9 | } 10 | -------------------------------------------------------------------------------- /fixtures/lint/my-scope/map2.suma: -------------------------------------------------------------------------------- 1 | profile = "some-profile@2.0.0" 2 | provider = "twilio" 3 | 4 | """ 5 | MyProfile map 6 | """ 7 | map MyProfile { 8 | map result 222 9 | map error false 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/lint/my-scope/my-profile.supr: -------------------------------------------------------------------------------- 1 | name = "my-scope/my-profile" 2 | version = "1.0.0" 3 | 4 | """ 5 | MyProfile usecase 6 | """ 7 | usecase MyProfile { 8 | result string 9 | error boolean 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/lint/my-scope/my-profile.twilio.suma: -------------------------------------------------------------------------------- 1 | profile = "my-scope/my-profile@1.0" 2 | provider = "twilio" 3 | 4 | """ 5 | MyProfile map 6 | """ 7 | map MyProfile { 8 | map result 'string' 9 | map error false 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/lint/my-scope/my-profile.tyntec.suma: -------------------------------------------------------------------------------- 1 | profile = "my-scope/my-profile@1.0" 2 | provider = "twilio" 3 | 4 | """ 5 | MyProfile map 6 | """ 7 | map MyProfile { 8 | map result 'string' 9 | map result 222 10 | map result false 11 | map result {} 12 | } 13 | -------------------------------------------------------------------------------- /fixtures/lint/my-scope/my-profile2.supr: -------------------------------------------------------------------------------- 1 | name = "my-scope/my-profile" 2 | version = "1.0.0" 3 | 4 | """ 5 | MyProfile usecase 6 | """ 7 | usecase MyProfile {} 8 | -------------------------------------------------------------------------------- /fixtures/lint/twilio.provider.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twilio", 3 | "deployments": [ 4 | { 5 | "id": "default", 6 | "baseUrl": "api.twilio.localhost" 7 | } 8 | ], 9 | "security": [ 10 | { 11 | "auth": { 12 | "BasicAuth": { 13 | "type": "http", 14 | "scheme": "basic" 15 | } 16 | }, 17 | "hosts": [ 18 | "default" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /fixtures/profiles/communication/send-email.json: -------------------------------------------------------------------------------- 1 | { 2 | "profile_id": "communication/send-email@1.0.1", 3 | "profile_name": "communication/send-email", 4 | "profile_version": "1.0.1", 5 | "url": "https://superface.dev/communication/send-email@1.0.1", 6 | "owner": "hello", 7 | "owner_url": "", 8 | "published_at": "2021-05-17T08:55:54.905Z", 9 | "published_by": "hello " 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/profiles/communication/send-email.supr: -------------------------------------------------------------------------------- 1 | """ 2 | Send Email 3 | Send one transactional email 4 | """ 5 | 6 | name = "communication/send-email" 7 | version = "1.0.1" 8 | 9 | """ 10 | Send transactional email to one recipient 11 | Email can contain text and/or html representation 12 | """ 13 | usecase SendEmail unsafe { 14 | input { 15 | from! 16 | to! 17 | subject! 18 | text 19 | html 20 | } 21 | 22 | result { 23 | messageId 24 | } 25 | 26 | error { 27 | title 28 | detail 29 | } 30 | } -------------------------------------------------------------------------------- /fixtures/profiles/communication/send-email@1.0.1.json: -------------------------------------------------------------------------------- 1 | { 2 | "profile_id": "communication/send-email@1.0.1", 3 | "profile_name": "communication/send-email", 4 | "profile_version": "1.0.1", 5 | "url": "https://superface.dev/communication/send-email@1.0.1", 6 | "owner": "hello", 7 | "owner_url": "", 8 | "published_at": "2021-05-17T08:55:54.905Z", 9 | "published_by": "hello " 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/profiles/communication/send-email@1.0.1.supr: -------------------------------------------------------------------------------- 1 | """ 2 | Send Email 3 | Send one transactional email 4 | """ 5 | 6 | name = "communication/send-email" 7 | version = "1.0.1" 8 | 9 | """ 10 | Send transactional email to one recipient 11 | Email can contain text and/or html representation 12 | """ 13 | usecase SendEmail unsafe { 14 | input { 15 | from! 16 | to! 17 | subject! 18 | text 19 | html 20 | } 21 | 22 | result { 23 | messageId 24 | } 25 | 26 | error { 27 | title 28 | detail 29 | } 30 | } -------------------------------------------------------------------------------- /fixtures/profiles/starwars/character-information.json: -------------------------------------------------------------------------------- 1 | {"profile_id":"starwars/character-information@1.0.1","profile_name":"starwars/character-information","profile_version":"1.0.1","url":"https://superface.dev/starwars/character-information@1.0.1","owner":"hello","owner_url":"","published_at":"2021-05-17T08:55:54.905Z","published_by":"hello "} -------------------------------------------------------------------------------- /fixtures/profiles/starwars/character-information.supr: -------------------------------------------------------------------------------- 1 | name = "starwars/character-information" 2 | version = "1.0.1" 3 | 4 | """ 5 | Retrieve information about Star Wars characters from the Star Wars API. 6 | """ 7 | usecase RetrieveCharacterInformation safe { 8 | input { 9 | characterName 10 | } 11 | 12 | result { 13 | height 14 | weight 15 | yearOfBirth 16 | } 17 | 18 | error { 19 | message 20 | } 21 | } 22 | 23 | " 24 | Character name 25 | The character name to use when looking up character information 26 | " 27 | field characterName string 28 | 29 | " 30 | Height 31 | The height of the character 32 | " 33 | field height string 34 | 35 | " 36 | Weight 37 | The weight of the character 38 | " 39 | field weight string 40 | 41 | " 42 | Year of birth 43 | The year of birth of the character 44 | " 45 | field yearOfBirth string 46 | 47 | " 48 | Message 49 | The message for when an error occurs looking up character information 50 | " 51 | field message string 52 | -------------------------------------------------------------------------------- /fixtures/profiles/starwars/character-information@1.0.2.json: -------------------------------------------------------------------------------- 1 | { 2 | "profile_id": "starwars/character-information@1.0.2", 3 | "profile_name": "starwars/character-information", 4 | "profile_version": "1.0.2", 5 | "url": "https://superface.dev/starwars/character-information@1.0.1", 6 | "owner": "hello", 7 | "owner_url": "", 8 | "published_at": "2021-05-17T08:55:54.905Z", 9 | "published_by": "hello " 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/profiles/starwars/character-information@1.0.2.supr: -------------------------------------------------------------------------------- 1 | name = "starwars/character-information" 2 | version = "1.0.2" 3 | 4 | """ 5 | Retrieve information about Star Wars characters from the Star Wars API. 6 | """ 7 | usecase RetrieveCharacterInformation safe { 8 | input { 9 | characterName 10 | } 11 | 12 | result { 13 | height 14 | weight 15 | yearOfBirth 16 | } 17 | 18 | error { 19 | message 20 | } 21 | } 22 | 23 | " 24 | Character name 25 | The character name to use when looking up character information 26 | " 27 | field characterName string 28 | 29 | " 30 | Height 31 | The height of the character 32 | " 33 | field height string 34 | 35 | " 36 | Weight 37 | The weight of the character 38 | " 39 | field weight string 40 | 41 | " 42 | Year of birth 43 | The year of birth of the character 44 | " 45 | field yearOfBirth string 46 | 47 | " 48 | Message 49 | The message for when an error occurs looking up character information 50 | " 51 | field message string 52 | -------------------------------------------------------------------------------- /fixtures/profiles/starwars/maps/swapi.character-information.json: -------------------------------------------------------------------------------- 1 | { 2 | "map_id": "starwars/character-information.swapi@1.0-rev2", 3 | "map_provider": "swapi", 4 | "map_provider_url": "https://superface.ai/providers/swapi", 5 | "map_revision": "rev2", 6 | "map_variant": null, 7 | "url": "https://superface.ai/starwars/character-information.swapi@1.0-rev2", 8 | "owner": "superface", 9 | "owner_url": "", 10 | "profile_name": "starwars/character-information", 11 | "profile_url": "https://superface.ai/starwars/character-information", 12 | "profile_version": "1.0", 13 | "published_at": "2021-06-23T08:56:55.479Z", 14 | "published_by": "Superface " 15 | } 16 | -------------------------------------------------------------------------------- /fixtures/profiles/starwars/maps/swapi.character-information.suma: -------------------------------------------------------------------------------- 1 | profile = "starwars/character-information@1.0" 2 | provider = "swapi" 3 | 4 | map RetrieveCharacterInformation { 5 | http GET "/people/" { 6 | request { 7 | query { 8 | search = input.characterName 9 | } 10 | } 11 | 12 | response 200 "application/json" { 13 | return map error if (body.count === 0) { 14 | message = "No character found" 15 | } 16 | 17 | entries = body.results.filter(result => result.name.toLowerCase() === input.characterName.toLowerCase()) 18 | 19 | return map error if (entries.length === 0) { 20 | message = "Specified character name is incorrect, did you mean to enter one of following?" 21 | } 22 | 23 | character = entries[0] 24 | 25 | map result { 26 | height = character.height 27 | weight = character.mass 28 | yearOfBirth = character.birth_year 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /fixtures/profiles/starwars/maps/swapi.character-information@1.0.2.json: -------------------------------------------------------------------------------- 1 | { 2 | "map_id": "starwars/character-information.swapi@1.0-rev2", 3 | "map_provider": "swapi", 4 | "map_provider_url": "https://superface.ai/providers/swapi", 5 | "map_revision": "rev2", 6 | "map_variant": null, 7 | "url": "https://superface.ai/starwars/character-information.swapi@1.0-rev2", 8 | "owner": "superface", 9 | "owner_url": "", 10 | "profile_name": "starwars/character-information", 11 | "profile_url": "https://superface.ai/starwars/character-information", 12 | "profile_version": "1.0", 13 | "published_at": "2021-06-23T08:56:55.479Z", 14 | "published_by": "Superface " 15 | } 16 | -------------------------------------------------------------------------------- /fixtures/profiles/starwars/maps/swapi.character-information@1.0.2.suma: -------------------------------------------------------------------------------- 1 | profile = "starwars/character-information@1.0" 2 | provider = "swapi" 3 | 4 | map RetrieveCharacterInformation { 5 | http GET "/people/" { 6 | request { 7 | query { 8 | search = input.characterName 9 | } 10 | } 11 | 12 | response 200 "application/json" { 13 | return map error if (body.count === 0) { 14 | message = "No character found" 15 | } 16 | 17 | entries = body.results.filter(result => result.name.toLowerCase() === input.characterName.toLowerCase()) 18 | 19 | return map error if (entries.length === 0) { 20 | message = "Specified character name is incorrect, did you mean to enter one of following?" 21 | characters = body.results.map(result => result.name) 22 | } 23 | 24 | character = entries[0] 25 | 26 | map result { 27 | height = character.height 28 | weight = character.mass 29 | yearOfBirth = character.birth_year 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /fixtures/profiles/starwars/maps/unverified-swapi.character-information.json: -------------------------------------------------------------------------------- 1 | { 2 | "map_id": "starwars/character-information.swapi@1.0-rev2", 3 | "map_provider": "unverified-swapi", 4 | "map_provider_url": "https://superface.ai/providers/unverified-swapi", 5 | "map_revision": "rev2", 6 | "map_variant": null, 7 | "url": "https://superface.ai/starwars/character-information.swapi@1.0-rev2", 8 | "owner": "superface", 9 | "owner_url": "", 10 | "profile_name": "starwars/character-information", 11 | "profile_url": "https://superface.ai/starwars/character-information", 12 | "profile_version": "1.0", 13 | "published_at": "2021-06-23T08:56:55.479Z", 14 | "published_by": "Superface " 15 | } 16 | -------------------------------------------------------------------------------- /fixtures/profiles/starwars/maps/unverified-swapi.character-information.suma: -------------------------------------------------------------------------------- 1 | profile = "starwars/character-information@1.0" 2 | provider = "unverified-swapi" 3 | 4 | map RetrieveCharacterInformation { 5 | http GET "/people/" { 6 | request { 7 | query { 8 | search = input.characterName 9 | } 10 | } 11 | 12 | response 200 "application/json" { 13 | return map error if (body.count === 0) { 14 | message = "No character found" 15 | } 16 | 17 | entries = body.results.filter(result => result.name.toLowerCase() === input.characterName.toLowerCase()) 18 | 19 | return map error if (entries.length === 0) { 20 | message = "Specified character name is incorrect, did you mean to enter one of following?" 21 | } 22 | 23 | character = entries[0] 24 | 25 | map result { 26 | height = character.height 27 | weight = character.mass 28 | yearOfBirth = character.birth_year 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /fixtures/profiles/starwars/spaceship-information.json: -------------------------------------------------------------------------------- 1 | { 2 | "profile_id": "starwars/spaceship-information@1.0.0", 3 | "profile_name": "starwars/spaceship-information", 4 | "profile_version": "1.0.0", 5 | "url": "https://superface.dev/starwars/spaceship-information@1.0.0", 6 | "owner": "hello", 7 | "owner_url": "", 8 | "published_at": "2021-05-17T08:55:54.905Z", 9 | "published_by": "hello " 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/profiles/starwars/spaceship-information.supr: -------------------------------------------------------------------------------- 1 | name = "starwars/spaceship-information" 2 | version = "1.0.0" 3 | 4 | "Starwars Spaceship Information" 5 | usecase RetrieveSpaceshipInformation safe { 6 | input { 7 | spaceshipName 8 | } 9 | 10 | result { 11 | name string! 12 | model string! 13 | pilots [string] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /fixtures/providers/azure-cognitive-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azure-cognitive-services", 3 | "services": [ 4 | { 5 | "id": "default", 6 | "baseUrl": "https://{instance}.cognitiveservices.azure.com/{version}" 7 | } 8 | ], 9 | "securitySchemes": [ 10 | { 11 | "id": "azure-subscription-key", 12 | "type": "apiKey", 13 | "in": "header", 14 | "name": "Ocp-Apim-Subscription-Key" 15 | } 16 | ], 17 | "defaultService": "default", 18 | "parameters": [ 19 | { 20 | "name": "instance", 21 | "description": "Instance of your azure cognitive service" 22 | }, 23 | { 24 | "name": "version", 25 | "default": "v1" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /fixtures/providers/empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "empty", 3 | "services": [ 4 | { 5 | "baseUrl": "https://empty.dev/api", 6 | "id": "empty" 7 | } 8 | ], 9 | "securitySchemes": [], 10 | "defaultService": "empty" 11 | } -------------------------------------------------------------------------------- /fixtures/providers/mailchimp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mailchimp", 3 | "services": [ 4 | { 5 | "id": "mandrill", 6 | "baseUrl": "https://mandrillapp.com/api/1.0" 7 | } 8 | ], 9 | "defaultService": "mandrill" 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/providers/mailgun.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mailgun", 3 | "services": [ 4 | { 5 | "id": "v3", 6 | "baseUrl": "https://api.mailgun.net/v3" 7 | } 8 | ], 9 | "defaultService": "v3", 10 | "securitySchemes": [ 11 | { 12 | "id": "basic", 13 | "type": "http", 14 | "scheme": "basic" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /fixtures/providers/mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mock", 3 | "services": [ 4 | { 5 | "id": "default", 6 | "baseUrl": "noop.localhost" 7 | } 8 | ], 9 | "defaultService": "default" 10 | } 11 | -------------------------------------------------------------------------------- /fixtures/providers/provider-without-security.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "provider-without-security", 3 | "services": [ 4 | { 5 | "baseUrl": "https://empty.dev/api", 6 | "id": "empty" 7 | } 8 | ], 9 | "defaultService": "empty" 10 | } -------------------------------------------------------------------------------- /fixtures/providers/sendgrid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sendgrid", 3 | "services": [ 4 | { 5 | "id": "v3", 6 | "baseUrl": "https://api.sendgrid.com/v3" 7 | } 8 | ], 9 | "defaultService": "v3", 10 | "securitySchemes": [ 11 | { 12 | "id": "bearer_token", 13 | "type": "http", 14 | "scheme": "bearer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /fixtures/providers/swapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swapi", 3 | "services": [ 4 | { 5 | "baseUrl": "https://swapi.dev/api", 6 | "id": "swapi" 7 | } 8 | ], 9 | "securitySchemes": [ 10 | { 11 | "id": "api", 12 | "type": "apiKey", 13 | "in": "header", 14 | "name": "X-API-Key" 15 | }, 16 | { 17 | "id": "bearer", 18 | "type": "http", 19 | "scheme": "bearer" 20 | }, 21 | { 22 | "id": "basic", 23 | "type": "http", 24 | "scheme": "basic" 25 | }, 26 | { 27 | "id": "digest", 28 | "type": "http", 29 | "scheme": "digest" 30 | } 31 | ], 32 | "defaultService": "test" 33 | } 34 | -------------------------------------------------------------------------------- /fixtures/providers/unverified-swapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unverified-swapi", 3 | "services": [ 4 | { 5 | "baseUrl": "https://swapi.dev/api", 6 | "id": "swapi" 7 | } 8 | ], 9 | "securitySchemes": [ 10 | { 11 | "id": "api", 12 | "type": "apiKey", 13 | "in": "header", 14 | "name": "X-API-Key" 15 | }, 16 | { 17 | "id": "bearer", 18 | "type": "http", 19 | "scheme": "bearer" 20 | }, 21 | { 22 | "id": "basic", 23 | "type": "http", 24 | "scheme": "basic" 25 | }, 26 | { 27 | "id": "digest", 28 | "type": "http", 29 | "scheme": "digest" 30 | } 31 | ], 32 | "defaultService": "test" 33 | } 34 | -------------------------------------------------------------------------------- /fixtures/strict.suma: -------------------------------------------------------------------------------- 1 | // https://sfspec.surge.sh/map#sec-Map-Document 2 | """ 3 | Strict Map 4 | 5 | Example of the map syntax adhering to the strict syntax. 6 | """ 7 | 8 | profile = "example/profile@1.0.0" 9 | provider = "provider" 10 | 11 | // https://sfspec.surge.sh/map#sec-Usecase-Map 12 | "Map Foo 13 | Description of the map Foo" 14 | map Foo { 15 | // https://sfspec.surge.sh/map#sec-Set-Variables 16 | 17 | set if (!cond) { 18 | foo.a = (() => { return true; })() 19 | "foo" = 1 + 1 20 | "foo.bar".bar = call Op(foo.bar = 1) 21 | } 22 | 23 | set { 24 | foo = 1 25 | } 26 | 27 | foo = 1 28 | "foo.bar".bar = call Op(foo.bar = 1) 29 | 30 | // https://sfspec.surge.sh/map#sec-Operation-Call 31 | call FooOp( 32 | a.foo = (() => { 33 | const jessieValue = { 34 | foo: 1, 35 | bar: 2 + 3 36 | }; 37 | 38 | return jessieValue; 39 | })(), 40 | "a.var".bar = 12 41 | baz.baz = true 42 | ) if (true || !![false, 0, undefined, null]) { 43 | set { 44 | foo = false 45 | } 46 | } 47 | 48 | call Op(foo = 1, bar = 1 + 1) if (true) { 49 | // https://sfspec.surge.sh/map#SetOutcome 50 | // https://sfspec.surge.sh/map#SetMapOutcome 51 | 52 | // https://sfspec.surge.sh/map#MapResult 53 | map result if (cond) { 54 | foo = 1 55 | } 56 | return map result if (cond) { 57 | "foo" = 1 58 | } 59 | 60 | // https://sfspec.surge.sh/map#sec-Map-Error 61 | map error if (cond) { 62 | "foo.bar" = 1 63 | } 64 | return map error if (cond) { 65 | foo.bar = 1 66 | } 67 | } 68 | 69 | // https://sfspec.surge.sh/map#HTTPCall 70 | http GET "/api/{foo}/bar" { 71 | // https://sfspec.surge.sh/map#HTTPSecurity 72 | security "my_apikey" 73 | 74 | // https://sfspec.surge.sh/map#HTTPRequest 75 | request "application/json" { 76 | // https://sfspec.surge.sh/map#URLQuery 77 | query { 78 | foo = "hello", 79 | bar = "world" 80 | } 81 | 82 | // https://sfspec.surge.sh/map//HTTPHeaders 83 | headers { 84 | "User-Agent" = "superface v1" 85 | } 86 | 87 | // https://sfspec.surge.sh/map//HTTPBody 88 | body { 89 | foo = 1, 90 | bar = 1 + 1, 91 | "foo.bar".bar = "3" 92 | } 93 | } 94 | 95 | // https://sfspec.surge.sh/map#HTTPRespose 96 | response 200 { 97 | map result { 98 | foo = 1 99 | } 100 | } 101 | 102 | // https://sfspec.surge.sh/map#HTTPRespose 103 | response "application/json" { 104 | map error { 105 | foo = 1 106 | } 107 | } 108 | 109 | // https://sfspec.surge.sh/map#HTTPRespose 110 | response "*" "en-US" { 111 | return map result { 112 | foo = 1 113 | } 114 | } 115 | 116 | // https://sfspec.surge.sh/map#HTTPRespose 117 | response { 118 | return map error { 119 | foo = 1 120 | } 121 | } 122 | } 123 | 124 | http POST "/" { 125 | // https://sfspec.surge.sh/map#HTTPRequest 126 | request { 127 | // https://sfspec.surge.sh/map#HTTPBody 128 | body = [1, 2, 3] 129 | } 130 | 131 | response 404 "text/html" "en-US" { 132 | foo = 1 133 | } 134 | } 135 | 136 | http OPTIONS "/" { 137 | security none 138 | } 139 | } 140 | 141 | operation FooOp { 142 | return if (!![]) 1 143 | fail if ({}) 2 144 | 145 | // asd 146 | set { 147 | foo.bar."baz" = 1 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /fixtures/strict.supr: -------------------------------------------------------------------------------- 1 | """ 2 | Title 3 | 4 | Description 5 | """ 6 | name = "example/profile" 7 | version = "1.0.0" 8 | 9 | // https://sfspec.surge.sh/profile#sec-Use-case 10 | "usecase title" 11 | usecase Foo { 12 | input { 13 | "field title" 14 | field! string! 15 | // https://sfspec.surge.sh/profile#UnionModel 16 | field number | boolean, field boolean 17 | } 18 | 19 | // https://sfspec.surge.sh/profile#ScalarModel 20 | result number 21 | 22 | // https://sfspec.surge.sh/profile#sec-List-Model 23 | // https://sfspec.surge.sh/profile#sec-Alias-Model 24 | async result [FooModel] 25 | 26 | // https://sfspec.surge.sh/profile#EnumModel 27 | error enum { 28 | NOT_FOUND = 404, 29 | BAD_REQUEST = 400 30 | } 31 | } 32 | 33 | // https://sfspec.surge.sh/profile#sec-Named-Model 34 | // https://sfspec.surge.sh/profile#sec-Object-Model 35 | "Title" 36 | model FooModel { 37 | foo bar! | baz! 38 | } | [string] 39 | 40 | // https://sfspec.surge.sh/profile#sec-Named-Field 41 | "Title" 42 | field foo string! -------------------------------------------------------------------------------- /fixtures/valid-map.provider.suma: -------------------------------------------------------------------------------- 1 | // https://sfspec.surge.sh/map#sec-Map-Document 2 | """ 3 | Strict Map 4 | 5 | Example of the map syntax adhering to the strict syntax. 6 | """ 7 | 8 | profile = "example/profile@1.0.0" 9 | provider = "provider" 10 | 11 | // https://sfspec.surge.sh/map#sec-Usecase-Map 12 | "Map Foo 13 | Description of the map Foo" 14 | map Foo { 15 | // https://sfspec.surge.sh/map#sec-Set-Variables 16 | 17 | set if (!cond) { 18 | foo.a = (() => { return true; })() 19 | "foo" = 1 + 1 20 | "foo.bar".bar = call Op(foo.bar = 1) 21 | } 22 | 23 | set { 24 | foo = 1 25 | } 26 | 27 | foo = 1 28 | "foo.bar".bar = call Op(foo.bar = 1) 29 | 30 | // https://sfspec.surge.sh/map#sec-Operation-Call 31 | call FooOp( 32 | a.foo = (() => { 33 | const jessieValue = { 34 | foo: 1, 35 | bar: 2 + 3 36 | }; 37 | 38 | return jessieValue; 39 | })(), 40 | "a.var".bar = 12 41 | baz.baz = true 42 | ) if (true || !![false, 0, undefined, null]) { 43 | set { 44 | foo = false 45 | } 46 | } 47 | 48 | call Op(foo = 1, bar = 1 + 1) if (true) { 49 | // https://sfspec.surge.sh/map#SetOutcome 50 | // https://sfspec.surge.sh/map#SetMapOutcome 51 | 52 | // https://sfspec.surge.sh/map#MapResult 53 | map result if (cond) 1 54 | return map result if (cond) 1 55 | 56 | // https://sfspec.surge.sh/map#sec-Map-Error 57 | map error if (cond) 400 58 | return map error if (cond) 400 59 | } 60 | 61 | // https://sfspec.surge.sh/map#HTTPCall 62 | http GET "/api/{foo}/bar" { 63 | // https://sfspec.surge.sh/map#HTTPSecurity 64 | security "my_apikey" 65 | 66 | // https://sfspec.surge.sh/map#HTTPRequest 67 | request "application/json" { 68 | // https://sfspec.surge.sh/map#URLQuery 69 | query { 70 | foo = "hello", 71 | bar = "world" 72 | } 73 | 74 | // https://sfspec.surge.sh/map//HTTPHeaders 75 | headers { 76 | "User-Agent" = "superface v1" 77 | } 78 | 79 | // https://sfspec.surge.sh/map//HTTPBody 80 | body { 81 | foo = 1, 82 | bar = 1 + 1, 83 | "foo.bar".bar = "3" 84 | } 85 | } 86 | 87 | // https://sfspec.surge.sh/map#HTTPRespose 88 | response 100 { 89 | map result 1 90 | } 91 | 92 | // https://sfspec.surge.sh/map#HTTPRespose 93 | response "application/json" { 94 | map error 404 95 | } 96 | 97 | // https://sfspec.surge.sh/map#HTTPRespose 98 | response "*" "en-US" { 99 | return map result 1 100 | } 101 | 102 | // https://sfspec.surge.sh/map#HTTPRespose 103 | response { 104 | return map error 400 105 | } 106 | } 107 | 108 | http POST "/" { 109 | // https://sfspec.surge.sh/map#HTTPRequest 110 | request { 111 | // https://sfspec.surge.sh/map#HTTPBody 112 | body = [1, 2, 3] 113 | } 114 | 115 | response 404 "text/html" "en-US" { 116 | foo = 1 117 | } 118 | } 119 | 120 | http OPTIONS "/" { 121 | security none 122 | } 123 | } 124 | 125 | operation FooOp { 126 | return if (!![]) 1 127 | fail if ({}) 2 128 | 129 | // asd 130 | set { 131 | foo.bar."baz" = 1 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /fixtures/valid.suma: -------------------------------------------------------------------------------- 1 | profile = "example/profile@1.0" 2 | provider = "test" 3 | 4 | map Foo { 5 | 6 | map result if(input.field) 201 7 | map result 200 8 | 9 | } -------------------------------------------------------------------------------- /fixtures/with-examples.supr: -------------------------------------------------------------------------------- 1 | name = "star-trek/get-info" 2 | version = "2.2.2" 3 | 4 | usecase getCharacter { 5 | input { 6 | foo! string 7 | } 8 | 9 | result { 10 | bar string 11 | } 12 | 13 | error { 14 | baz! number! 15 | } 16 | 17 | example success { 18 | input { 19 | foo = "example" 20 | } 21 | result { 22 | bar = "result" 23 | } 24 | } 25 | 26 | example otherSuccess { 27 | input { 28 | foo = "second example" 29 | } 30 | result { 31 | bar = "second result" 32 | } 33 | } 34 | 35 | example fail { 36 | input { 37 | foo = "error" 38 | } 39 | error { 40 | baz = 12 41 | } 42 | } 43 | 44 | example otherFail { 45 | input { 46 | foo = "different error" 47 | } 48 | error { 49 | baz = 16 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /jest.config.integration.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | rootDir: 'src/', 5 | coveragePathIgnorePatterns: [ 6 | "/dist/", 7 | ], 8 | testTimeout: 10000, 9 | testMatch: ['**/*.integration.test.ts'], 10 | }; 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | rootDir: 'src/', 5 | coveragePathIgnorePatterns: [ 6 | "/dist/", 7 | ], 8 | testTimeout: 10000, 9 | testMatch: ['**/*.test.ts'], 10 | testPathIgnorePatterns: ['integration'], 11 | }; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@superfaceai/cli", 3 | "version": "4.1.3", 4 | "description": "Superface CLI utility", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/superfaceai/cli.git", 7 | "author": "Superface Team", 8 | "private": false, 9 | "bin": { 10 | "superface": "bin/superface", 11 | "sf": "bin/superface" 12 | }, 13 | "files": [ 14 | "bin/", 15 | "dist/" 16 | ], 17 | "scripts": { 18 | "postinstall": "echo \"Thanks for checking out Superface.ai!\nFor help, run: superface --help\nDocumentation: https://sfc.is/docs\nQuestions or feedback: https://sfc.is/discussions\n\"", 19 | "prebuild": "rimraf dist", 20 | "build": "tsc -p tsconfig.release.json --outDir dist", 21 | "test": "yarn test:fast && yarn test:integration", 22 | "test:clean": "jest --clear-cache && jest", 23 | "lint": "eslint src/", 24 | "lint:fix": "yarn lint --fix", 25 | "format": "prettier -c src/", 26 | "format:fix": "prettier --write src/", 27 | "prepush": "yarn build && yarn test && yarn lint && yarn format", 28 | "test:fast": "jest", 29 | "test:integration": "jest --config=jest.config.integration.js", 30 | "fixtures:recompile": "node ./dist/test/compile-fixtures.js" 31 | }, 32 | "devDependencies": { 33 | "@types/concat-stream": "^1.6.0", 34 | "@types/debug": "^4.1.5", 35 | "@types/handlebars": "^4.1.0", 36 | "@types/inquirer": "^7.3.1", 37 | "@types/jest": "^27.0.1", 38 | "@types/node": "^17.0.8", 39 | "@types/rimraf": "^3.0.0", 40 | "@typescript-eslint/eslint-plugin": "^5.43.0", 41 | "@typescript-eslint/parser": "^5.43.0", 42 | "concat-stream": "^2.0.0", 43 | "eslint": "^7.32.0", 44 | "eslint-config-prettier": "^8.5.0", 45 | "eslint-import-resolver-typescript": "^3.5.2", 46 | "eslint-plugin-import": "^2.26.0", 47 | "eslint-plugin-jest": "^26.1.5", 48 | "eslint-plugin-jest-formatting": "^3.1.0", 49 | "eslint-plugin-simple-import-sort": "^8.0.0", 50 | "jest": "27.1.0", 51 | "mockttp": "^3.6.0", 52 | "prettier": "^2.3.2", 53 | "ts-jest": "27.0.5" 54 | }, 55 | "dependencies": { 56 | "@oclif/command": "^1.8.0", 57 | "@oclif/config": "^1.17.0", 58 | "@oclif/plugin-warn-if-update-available": "^1.7.0", 59 | "@superfaceai/ast": "^1.3.0", 60 | "@superfaceai/parser": "^2.1.0", 61 | "@superfaceai/service-client": "5.2.1", 62 | "chalk": "^4.1.0", 63 | "cross-fetch": "^3.1.4", 64 | "debug": "^4.3.1", 65 | "handlebars": "^4.7.7", 66 | "inquirer": "^7.3.3", 67 | "nanospinner": "^1.1.0", 68 | "netrc-parser": "^3.1.6", 69 | "open": "^8.2.1", 70 | "rimraf": "^3.0.2", 71 | "typescript": "4.3.4" 72 | }, 73 | "oclif": { 74 | "commands": "dist/commands", 75 | "bin": "superface", 76 | "plugins": [ 77 | "@oclif/plugin-warn-if-update-available" 78 | ], 79 | "warn-if-update-available": { 80 | "timeoutInDays": 7, 81 | "message": "<%= config.name %> update available from <%= chalk.greenBright(config.version) %> to <%= chalk.greenBright(latest) %>." 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/commands/execute.ts: -------------------------------------------------------------------------------- 1 | import type { ILogger } from '../common'; 2 | import type { Flags } from '../common/command.abstract'; 3 | import { Command } from '../common/command.abstract'; 4 | import type { UserError } from '../common/error'; 5 | import { 6 | buildMapPath, 7 | buildRunFilePath, 8 | isInsideSuperfaceDir, 9 | } from '../common/file-structure'; 10 | import { SuperfaceClient } from '../common/http'; 11 | import { exists } from '../common/io'; 12 | import { ProfileId } from '../common/profile'; 13 | import { resolveProviderJson } from '../common/provider'; 14 | import { UX } from '../common/ux'; 15 | import { SupportedLanguages } from '../logic'; 16 | import { execute } from '../logic/execution'; 17 | import { resolveLanguage, resolveProfileSource } from './map'; 18 | 19 | export default class Execute extends Command { 20 | // TODO: add description 21 | public static description = 22 | 'Run the created integration in superface directory. Commands `prepare`, `new` and `map` must be run before this command. You can switch runner language via `language` flag (`js` by default).'; 23 | 24 | public static examples = [ 25 | 'superface execute resend communication/send-email', 26 | ]; 27 | 28 | public static args = [ 29 | { 30 | name: 'providerName', 31 | description: 'Name of provider.', 32 | required: true, 33 | }, 34 | { 35 | name: 'profileId', 36 | description: 'Id of profile, eg: starwars.character-information', 37 | required: true, 38 | }, 39 | { 40 | name: 'language', 41 | description: 'Language of the application code runner. Default is `js`.', 42 | required: false, 43 | default: SupportedLanguages.JS, 44 | options: Object.values(SupportedLanguages), 45 | hidden: true, 46 | }, 47 | ]; 48 | 49 | public static flags = { 50 | ...Command.flags, 51 | }; 52 | 53 | public async run(): Promise { 54 | const { flags, args } = this.parse(Execute); 55 | await super.initialize(flags); 56 | await this.execute({ 57 | logger: this.logger, 58 | userError: this.userError, 59 | flags, 60 | args, 61 | }); 62 | } 63 | 64 | public async execute({ 65 | logger, 66 | userError, 67 | // flags, 68 | args, 69 | }: { 70 | logger: ILogger; 71 | userError: UserError; 72 | flags: Flags; 73 | args: { providerName?: string; profileId?: string; language?: string }; 74 | }): Promise { 75 | const ux = UX.create(); 76 | const { providerName, profileId, language } = args; 77 | 78 | if (providerName === undefined || profileId === undefined) { 79 | throw userError( 80 | 'Missing provider name or profile ID. Usage: `superface execute PROVIDERNAME PROFILEID`', 81 | 1 82 | ); 83 | } 84 | 85 | if (!isInsideSuperfaceDir()) { 86 | throw userError('Command must be run inside superface directory', 1); 87 | } 88 | const resolvedLanguage = resolveLanguage(language, { userError }); 89 | 90 | ux.start('Loading provider definition'); 91 | const resolvedProviderJson = await resolveProviderJson(providerName, { 92 | userError, 93 | client: SuperfaceClient.getClient(), 94 | }); 95 | 96 | if (resolvedProviderJson.source === 'local') { 97 | ux.succeed( 98 | `Input arguments checked. Provider JSON resolved from local file ${resolvedProviderJson.path}` 99 | ); 100 | } else { 101 | ux.succeed( 102 | `Input arguments checked. Provider JSON resolved from Superface server` 103 | ); 104 | } 105 | 106 | const profile = await resolveProfileSource(profileId, { userError }); 107 | 108 | const parsedProfileId = ProfileId.fromScopeName( 109 | profile.scope, 110 | profile.name 111 | ).id; 112 | 113 | // Check that map exists 114 | if ( 115 | !(await exists( 116 | buildMapPath({ 117 | profileName: profile.name, 118 | providerName: resolvedProviderJson.providerJson.name, 119 | profileScope: profile.scope, 120 | }) 121 | )) 122 | ) { 123 | throw userError( 124 | `Map for profile ${parsedProfileId} and provider ${resolvedProviderJson.providerJson.name} does not exist.`, 125 | 1 126 | ); 127 | } 128 | 129 | // Check that runfile exists 130 | const runfile = buildRunFilePath({ 131 | profileName: profile.name, 132 | providerName: resolvedProviderJson.providerJson.name, 133 | profileScope: profile.scope, 134 | language: resolvedLanguage, 135 | }); 136 | if (!(await exists(runfile))) { 137 | throw userError( 138 | `Runfile for profile ${parsedProfileId} and provider ${resolvedProviderJson.providerJson.name} does not exist.`, 139 | 1 140 | ); 141 | } 142 | 143 | await execute(runfile, resolvedLanguage, { logger, userError }); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/commands/login.test.ts: -------------------------------------------------------------------------------- 1 | import { ServiceClient } from '@superfaceai/service-client'; 2 | 3 | import { MockLogger } from '..'; 4 | import { createUserError } from '../common/error'; 5 | import { getServicesUrl } from '../common/http'; 6 | import { login } from '../logic/login'; 7 | import { CommandInstance } from '../test/utils'; 8 | import Login from './login'; 9 | 10 | const mockRefreshToken = 'RT'; 11 | const mockBaseUrlWithExistingRecord = 'existing'; 12 | const mockBaseUrlWithEmptyRecord = 'empty'; 13 | 14 | jest.mock('../logic/login'); 15 | jest.mock('../common/http', () => ({ 16 | ...jest.requireActual>('../common/http'), 17 | getServicesUrl: jest.fn(), 18 | })); 19 | 20 | const mockLoadSync = jest.fn(); 21 | const mockSave = jest.fn(); 22 | const mockLoad = jest.fn(); 23 | 24 | jest.mock('netrc-parser', () => { 25 | return { 26 | // Netrc is not default export so we need this 27 | Netrc: jest.fn().mockImplementation(() => { 28 | return { 29 | loadSync: mockLoadSync, 30 | save: mockSave, 31 | load: mockLoad, 32 | machines: { 33 | [mockBaseUrlWithExistingRecord]: { 34 | password: mockRefreshToken, 35 | }, 36 | [mockBaseUrlWithEmptyRecord]: {}, 37 | }, 38 | }; 39 | }), 40 | }; 41 | }); 42 | 43 | describe('Login CLI command', () => { 44 | const originalValue = process.env.SUPERFACE_REFRESH_TOKEN; 45 | let logger: MockLogger; 46 | let instance: Login; 47 | const userError = createUserError(false, false); 48 | 49 | beforeEach(async () => { 50 | jest.restoreAllMocks(); 51 | logger = new MockLogger(); 52 | instance = CommandInstance(Login); 53 | }); 54 | 55 | afterAll(() => { 56 | if (originalValue !== undefined) { 57 | process.env.SUPERFACE_REFRESH_TOKEN = originalValue; 58 | } 59 | }); 60 | 61 | describe('when running login command', () => { 62 | it('calls login correctly - non existing record in netrc', async () => { 63 | jest.mocked(getServicesUrl).mockReturnValue(mockBaseUrlWithEmptyRecord); 64 | const logoutSpy = jest.spyOn(ServiceClient.prototype, 'logout'); 65 | 66 | await expect( 67 | instance.execute({ logger, userError, flags: { force: false } }) 68 | ).resolves.toBeUndefined(); 69 | expect(login).toHaveBeenCalledWith( 70 | { 71 | force: false, 72 | }, 73 | expect.anything() 74 | ); 75 | 76 | expect(logoutSpy).not.toHaveBeenCalled(); 77 | expect(logger.stdout).toContainEqual(['loggedInSuccessfully', []]); 78 | }); 79 | 80 | it('calls login correctly - non existing record in netrc and quiet flag', async () => { 81 | jest.mocked(getServicesUrl).mockReturnValue(mockBaseUrlWithEmptyRecord); 82 | const logoutSpy = jest.spyOn(ServiceClient.prototype, 'logout'); 83 | 84 | await expect( 85 | instance.execute({ 86 | logger, 87 | userError, 88 | flags: { 89 | force: false, 90 | quiet: true, 91 | }, 92 | }) 93 | ).resolves.toBeUndefined(); 94 | expect(login).toHaveBeenCalledWith( 95 | { 96 | force: false, 97 | }, 98 | expect.anything() 99 | ); 100 | 101 | expect(logoutSpy).not.toHaveBeenCalled(); 102 | expect(logger.stdout).toContainEqual(['loggedInSuccessfully', []]); 103 | }); 104 | 105 | it('calls login correctly - existing record in netrc and force flag', async () => { 106 | jest 107 | .mocked(getServicesUrl) 108 | .mockReturnValue(mockBaseUrlWithExistingRecord); 109 | const logoutSpy = jest.spyOn(ServiceClient.prototype, 'logout'); 110 | 111 | await expect( 112 | instance.execute({ logger, userError, flags: { force: true } }) 113 | ).resolves.toBeUndefined(); 114 | 115 | expect(login).toHaveBeenCalledWith( 116 | { 117 | force: true, 118 | }, 119 | expect.anything() 120 | ); 121 | 122 | expect(logger.stdout).toContainEqual(['alreadyLoggedIn', []]); 123 | expect(logger.stdout).toContainEqual(['loggedInSuccessfully', []]); 124 | expect(logoutSpy).toHaveBeenCalled(); 125 | }); 126 | 127 | it('calls login correctly - refresh token in env', async () => { 128 | process.env.SUPERFACE_REFRESH_TOKEN = mockRefreshToken; 129 | jest 130 | .mocked(getServicesUrl) 131 | .mockReturnValue(mockBaseUrlWithExistingRecord); 132 | const logoutSpy = jest.spyOn(ServiceClient.prototype, 'logout'); 133 | 134 | await expect( 135 | instance.execute({ logger, userError, flags: { force: false } }) 136 | ).resolves.toBeUndefined(); 137 | expect(login).toHaveBeenCalledWith( 138 | { 139 | force: false, 140 | }, 141 | expect.anything() 142 | ); 143 | 144 | expect(logger.stdout).toContainEqual(['usinfSfRefreshToken', []]); 145 | expect(logger.stdout).toContainEqual(['loggedInSuccessfully', []]); 146 | expect(logoutSpy).not.toHaveBeenCalled(); 147 | }); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /src/commands/login.ts: -------------------------------------------------------------------------------- 1 | import { flags as oclifFlags } from '@oclif/command'; 2 | import { Netrc } from 'netrc-parser'; 3 | 4 | import type { Flags } from '../common/command.abstract'; 5 | import { Command } from '../common/command.abstract'; 6 | import type { UserError } from '../common/error'; 7 | import { getServicesUrl, SuperfaceClient } from '../common/http'; 8 | import type { ILogger } from '../common/log'; 9 | import { login } from '../logic/login'; 10 | 11 | export default class Login extends Command { 12 | public static description = 'Login to superface server'; 13 | 14 | public static flags = { 15 | ...Command.flags, 16 | force: oclifFlags.boolean({ 17 | char: 'f', 18 | description: 19 | "When set to true user won't be asked to confirm browser opening", 20 | default: false, 21 | }), 22 | }; 23 | 24 | public static examples = ['$ superface login', '$ superface login -f']; 25 | 26 | public async run(): Promise { 27 | const { flags } = this.parse(Login); 28 | await super.initialize(flags); 29 | await this.execute({ 30 | logger: this.logger, 31 | userError: this.userError, 32 | flags, 33 | }); 34 | } 35 | 36 | public async execute({ 37 | logger, 38 | userError, 39 | flags, 40 | }: { 41 | logger: ILogger; 42 | flags: Flags; 43 | userError: UserError; 44 | }): Promise { 45 | if (process.env.SUPERFACE_REFRESH_TOKEN !== undefined) { 46 | logger.warn('usinfSfRefreshToken'); 47 | } else { 48 | const storeUrl = getServicesUrl(); 49 | // environment variable for specific netrc file 50 | const netrc = new Netrc(process.env.NETRC_FILEPATH); 51 | await netrc.load(); 52 | const previousEntry = netrc.machines[storeUrl]; 53 | try { 54 | // check if already logged in and logout 55 | if ( 56 | previousEntry !== undefined && 57 | previousEntry.password !== undefined 58 | ) { 59 | logger.info('alreadyLoggedIn'); 60 | // logout from service client 61 | await SuperfaceClient.getClient().logout(); 62 | } 63 | } catch (err) { 64 | logger.error('unknownError', err); 65 | } 66 | } 67 | 68 | await login( 69 | { 70 | force: flags.force, 71 | }, 72 | { logger, userError } 73 | ); 74 | 75 | logger.success('loggedInSuccessfully'); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/commands/logout.integration.test.ts: -------------------------------------------------------------------------------- 1 | import { getLocal } from 'mockttp'; 2 | import { Netrc } from 'netrc-parser'; 3 | import { join as joinPath } from 'path'; 4 | 5 | import { mkdir, rimraf } from '../common/io'; 6 | import { messages } from '../common/messages'; 7 | import { execCLI, setUpTempDir } from '../test/utils'; 8 | 9 | const mockServer = getLocal(); 10 | 11 | describe('Logout CLI command', () => { 12 | // File specific path 13 | const TEMP_PATH = joinPath('test', 'tmp'); 14 | let tempDir: string; 15 | let NETRC_FILENAME: string; 16 | 17 | beforeAll(async () => { 18 | await mkdir(TEMP_PATH, { recursive: true }); 19 | }); 20 | 21 | beforeEach(async () => { 22 | await mockServer.start(); 23 | 24 | // Test specific netrc 25 | tempDir = await setUpTempDir(TEMP_PATH, true); 26 | NETRC_FILENAME = '.netrc'; 27 | }); 28 | 29 | afterEach(async () => { 30 | await rimraf(tempDir); 31 | 32 | await mockServer.stop(); 33 | }); 34 | 35 | describe('when running logout command', () => { 36 | it('logs out user', async () => { 37 | // Set mock refresh token in netrc 38 | const netRc = new Netrc(joinPath(tempDir, NETRC_FILENAME)); 39 | await netRc.load(); 40 | netRc.machines[mockServer.url] = { password: 'rt' }; 41 | await netRc.save(); 42 | 43 | await mockServer.forDelete('/auth/signout').thenJson(200, {}); 44 | const result = await execCLI(tempDir, ['logout'], mockServer.url, { 45 | env: { NETRC_FILEPATH: NETRC_FILENAME }, 46 | }); 47 | 48 | expect(result.stdout).toContain(messages.loggoutSuccessful()); 49 | 50 | const savedNetRc = new Netrc(joinPath(tempDir, NETRC_FILENAME)); 51 | await savedNetRc.load(); 52 | expect(savedNetRc.machines[mockServer.url].password).toBeUndefined(); 53 | }); 54 | 55 | it('returns warning if user is not logged in', async () => { 56 | await mockServer.forDelete('/auth/signout').thenJson(401, {}); 57 | 58 | const result = await execCLI(tempDir, ['logout'], mockServer.url); 59 | 60 | expect(result.stdout).toContain( 61 | messages.superfaceServerError( 62 | 'Error', 63 | "No session found, couldn't log out" 64 | ) 65 | ); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/commands/logout.test.ts: -------------------------------------------------------------------------------- 1 | import { ServiceClient, ServiceClientError } from '@superfaceai/service-client'; 2 | 3 | import { MockLogger } from '../common'; 4 | import { createUserError } from '../common/error'; 5 | import { CommandInstance } from '../test/utils'; 6 | import Logout from './logout'; 7 | 8 | describe('Logout CLI command', () => { 9 | let logger: MockLogger; 10 | let instance: Logout; 11 | const userError = createUserError(false, false); 12 | 13 | beforeEach(async () => { 14 | logger = new MockLogger(); 15 | instance = CommandInstance(Logout); 16 | }); 17 | 18 | afterEach(async () => { 19 | jest.resetAllMocks(); 20 | }); 21 | 22 | describe('when running logout command', () => { 23 | it('calls signOut correctly, user logged in', async () => { 24 | const getInfoSpy = jest 25 | .spyOn(ServiceClient.prototype, 'signOut') 26 | .mockResolvedValue(null); 27 | 28 | await expect( 29 | instance.execute({ logger, userError, flags: {} }) 30 | ).resolves.toBeUndefined(); 31 | expect(getInfoSpy).toHaveBeenCalled(); 32 | expect(logger.stderr).toEqual([]); 33 | expect(logger.stdout).toContainEqual(['loggoutSuccessful', []]); 34 | }); 35 | 36 | it('calls getUserInfo correctly, user logged out', async () => { 37 | const getInfoSpy = jest 38 | .spyOn(ServiceClient.prototype, 'signOut') 39 | .mockRejectedValue( 40 | new ServiceClientError("No session found, couldn't log out") 41 | ); 42 | 43 | await expect( 44 | instance.execute({ logger, userError, flags: {} }) 45 | ).resolves.toBeUndefined(); 46 | expect(getInfoSpy).toHaveBeenCalled(); 47 | expect(logger.stderr).toEqual([]); 48 | expect(logger.stdout).toContainEqual([ 49 | 'superfaceServerError', 50 | ['Error', "No session found, couldn't log out"], 51 | ]); 52 | }); 53 | 54 | it('calls getUserInfo correctly, unknown error', async () => { 55 | const mockErr = new Error('test'); 56 | const getInfoSpy = jest 57 | .spyOn(ServiceClient.prototype, 'signOut') 58 | .mockRejectedValue(mockErr); 59 | 60 | await expect( 61 | instance.execute({ logger, userError, flags: {} }) 62 | ).rejects.toThrow('test'); 63 | expect(getInfoSpy).toHaveBeenCalled(); 64 | expect(logger.stderr).toEqual([]); 65 | expect(logger.stdout).toEqual([]); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/commands/logout.ts: -------------------------------------------------------------------------------- 1 | import { ServiceClientError } from '@superfaceai/service-client'; 2 | 3 | import type { Flags } from '../common/command.abstract'; 4 | import { Command } from '../common/command.abstract'; 5 | import type { UserError } from '../common/error'; 6 | import { SuperfaceClient } from '../common/http'; 7 | import type { ILogger } from '../common/log'; 8 | 9 | export default class Logout extends Command { 10 | public static strict = false; 11 | 12 | public static description = 'Logs out logged in user'; 13 | 14 | public static args = []; 15 | 16 | public static examples = ['$ superface logout']; 17 | 18 | public async run(): Promise { 19 | const { flags } = this.parse(Logout); 20 | await super.initialize(flags); 21 | await this.execute({ 22 | logger: this.logger, 23 | userError: this.userError, 24 | flags, 25 | }); 26 | } 27 | 28 | public async execute({ 29 | logger, 30 | userError, 31 | flags: _, 32 | }: { 33 | logger: ILogger; 34 | userError: UserError; 35 | flags: Flags; 36 | }): Promise { 37 | try { 38 | await SuperfaceClient.getClient().signOut(); 39 | logger.success('loggoutSuccessful'); 40 | } catch (error) { 41 | if (error instanceof ServiceClientError) { 42 | logger.warn('superfaceServerError', error.name, error.message); 43 | 44 | return; 45 | } 46 | throw userError(String(error), 1); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/commands/whoami.integration.test.ts: -------------------------------------------------------------------------------- 1 | import { ServiceApiError } from '@superfaceai/service-client'; 2 | import { getLocal } from 'mockttp'; 3 | import { join as joinPath } from 'path'; 4 | 5 | import { ContentType } from '../common/http'; 6 | import { mkdir, rimraf } from '../common/io'; 7 | import { messages } from '../common/messages'; 8 | import { execCLI, setUpTempDir } from '../test/utils'; 9 | 10 | const mockServer = getLocal(); 11 | 12 | describe('Whoami CLI command', () => { 13 | // File specific path 14 | const TEMP_PATH = joinPath('test', 'tmp'); 15 | let tempDir: string; 16 | 17 | beforeAll(async () => { 18 | await mkdir(TEMP_PATH, { recursive: true }); 19 | }); 20 | 21 | beforeEach(async () => { 22 | tempDir = await setUpTempDir(TEMP_PATH); 23 | await mockServer.start(); 24 | }); 25 | 26 | afterEach(async () => { 27 | await rimraf(tempDir); 28 | await mockServer.stop(); 29 | }); 30 | 31 | describe('when running whoami command', () => { 32 | it('returns info about logged in user', async () => { 33 | const mockUserInfo = { 34 | name: 'jakub.vacek', 35 | email: 'jakub.vacek@dxheroes.io', 36 | accounts: [], 37 | }; 38 | 39 | await mockServer 40 | .forGet('/id/user') 41 | .withHeaders({ Accept: ContentType.JSON }) 42 | .thenJson(200, mockUserInfo); 43 | const result = await execCLI(tempDir, ['whoami'], mockServer.url); 44 | 45 | expect(result.stdout).toMatch( 46 | messages.loggedInAs(mockUserInfo.name, mockUserInfo.email) 47 | ); 48 | }); 49 | 50 | it('returns warning if user is not logged in', async () => { 51 | const mockServerResponse = new ServiceApiError({ 52 | status: 401, 53 | instance: '', 54 | title: 'Unathorized', 55 | detail: 'unathorized', 56 | }); 57 | 58 | await mockServer 59 | .forGet('/id/user') 60 | .withHeaders({ Accept: ContentType.JSON }) 61 | .thenJson(401, mockServerResponse); 62 | const result = await execCLI(tempDir, ['whoami'], mockServer.url); 63 | 64 | expect(result.stdout).toMatch(messages.notLoggedIn()); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/commands/whoami.test.ts: -------------------------------------------------------------------------------- 1 | import { ServiceApiError, ServiceClient } from '@superfaceai/service-client'; 2 | 3 | import { MockLogger } from '../common'; 4 | import { createUserError } from '../common/error'; 5 | import { CommandInstance } from '../test/utils'; 6 | import Whoami from './whoami'; 7 | 8 | describe('Whoami CLI command', () => { 9 | let logger: MockLogger; 10 | let instance: Whoami; 11 | const userError = createUserError(false, false); 12 | 13 | beforeEach(async () => { 14 | logger = new MockLogger(); 15 | instance = CommandInstance(Whoami); 16 | }); 17 | 18 | afterEach(async () => { 19 | jest.resetAllMocks(); 20 | }); 21 | 22 | describe('when running whoami command', () => { 23 | it('calls getUserInfo correctly, user logged in', async () => { 24 | const mockUserInfo = { 25 | name: 'jakub.vacek', 26 | email: 'jakub.vacek@dxheroes.io', 27 | accounts: [], 28 | }; 29 | const getInfoSpy = jest 30 | .spyOn(ServiceClient.prototype, 'getUserInfo') 31 | .mockResolvedValue(mockUserInfo); 32 | 33 | await expect( 34 | instance.execute({ logger, userError, flags: {} }) 35 | ).resolves.toBeUndefined(); 36 | expect(getInfoSpy).toHaveBeenCalled(); 37 | expect(logger.stderr).toEqual([]); 38 | expect(logger.stdout).toContainEqual([ 39 | 'loggedInAs', 40 | [mockUserInfo.name, mockUserInfo.email], 41 | ]); 42 | }); 43 | 44 | it('calls getUserInfo correctly, user logged out', async () => { 45 | const mockServerResponse = new ServiceApiError({ 46 | status: 401, 47 | instance: '', 48 | title: 'Unathorized', 49 | detail: 'unathorized', 50 | }); 51 | const getInfoSpy = jest 52 | .spyOn(ServiceClient.prototype, 'getUserInfo') 53 | .mockRejectedValue(mockServerResponse); 54 | 55 | await expect( 56 | instance.execute({ logger, userError, flags: {} }) 57 | ).resolves.toBeUndefined(); 58 | expect(getInfoSpy).toHaveBeenCalled(); 59 | expect(logger.stderr).toEqual([]); 60 | expect(logger.stdout).toContainEqual(['notLoggedIn', []]); 61 | }); 62 | 63 | it('calls getUserInfo correctly, unknown Superface response', async () => { 64 | const mockServerResponse = new ServiceApiError({ 65 | status: 403, 66 | instance: '', 67 | title: 'Forbiden', 68 | detail: 'forbiden access', 69 | }); 70 | const getInfoSpy = jest 71 | .spyOn(ServiceClient.prototype, 'getUserInfo') 72 | .mockRejectedValue(mockServerResponse); 73 | 74 | await expect( 75 | instance.execute({ logger, userError, flags: {} }) 76 | ).resolves.toBeUndefined(); 77 | expect(getInfoSpy).toHaveBeenCalled(); 78 | expect(logger.stderr).toEqual([]); 79 | expect(logger.stdout).toContainEqual([ 80 | 'superfaceServerError', 81 | [mockServerResponse.name, mockServerResponse.message], 82 | ]); 83 | }); 84 | 85 | it('calls getUserInfo correctly, unknown error', async () => { 86 | const mockErr = new Error('test'); 87 | const getInfoSpy = jest 88 | .spyOn(ServiceClient.prototype, 'getUserInfo') 89 | .mockRejectedValue(mockErr); 90 | 91 | await expect( 92 | instance.execute({ logger, userError, flags: {} }) 93 | ).rejects.toThrow('test'); 94 | expect(getInfoSpy).toHaveBeenCalled(); 95 | expect(logger.stderr).toEqual([]); 96 | expect(logger.stdout).toEqual([]); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/commands/whoami.ts: -------------------------------------------------------------------------------- 1 | import { ServiceApiError } from '@superfaceai/service-client'; 2 | 3 | import type { Flags } from '../common/command.abstract'; 4 | import { Command } from '../common/command.abstract'; 5 | import type { UserError } from '../common/error'; 6 | import { SuperfaceClient } from '../common/http'; 7 | import type { ILogger } from '../common/log'; 8 | 9 | export default class Whoami extends Command { 10 | public static strict = true; 11 | 12 | public static description = 'Prints info about logged in user'; 13 | 14 | public static examples = ['$ superface whoami', '$ sf whoami']; 15 | 16 | public async run(): Promise { 17 | const { flags } = this.parse(Whoami); 18 | await super.initialize(flags); 19 | await this.execute({ 20 | logger: this.logger, 21 | userError: this.userError, 22 | flags, 23 | }); 24 | } 25 | 26 | public async execute({ 27 | logger, 28 | userError, 29 | flags: _, 30 | }: { 31 | logger: ILogger; 32 | userError: UserError; 33 | flags: Flags; 34 | }): Promise { 35 | try { 36 | const userInfo = await SuperfaceClient.getClient().getUserInfo(); 37 | logger.success('loggedInAs', userInfo.name, userInfo.email); 38 | } catch (error) { 39 | if (!(error instanceof ServiceApiError)) { 40 | throw userError(String(error), 1); 41 | } 42 | if (error.status === 401) { 43 | logger.warn('notLoggedIn'); 44 | } else { 45 | logger.warn('superfaceServerError', error.name, error.message); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/common/command.abstract.ts: -------------------------------------------------------------------------------- 1 | import { Command as OclifCommand, flags } from '@oclif/command'; 2 | import type * as Parser from '@oclif/parser'; 3 | 4 | import type { UserError } from './error'; 5 | import { createUserError } from './error'; 6 | import type { ILogger } from './log'; 7 | import { DummyLogger, StdoutLogger } from './log'; 8 | 9 | type FlagType = T extends Parser.flags.IOptionFlag 10 | ? V 11 | : T extends Parser.flags.IBooleanFlag 12 | ? V | undefined 13 | : never; 14 | 15 | type KeysOfType = { 16 | [key in keyof T]: SelectedType extends T[key] ? key : never; 17 | }[keyof T]; 18 | type Optional = Partial>>; 19 | type NonOptional = Omit>; 20 | type OptionalUndefined = Optional & NonOptional; 21 | 22 | export type Flags = OptionalUndefined<{ 23 | [key in keyof T]: FlagType; 24 | }>; 25 | 26 | export abstract class Command extends OclifCommand { 27 | protected logger: ILogger = new DummyLogger(); 28 | protected userError: UserError = createUserError(true, true); 29 | 30 | public static flags = { 31 | quiet: flags.boolean({ 32 | char: 'q', 33 | description: 34 | 'When set to true, disables the shell echo output of action.', 35 | default: false, 36 | }), 37 | noColor: flags.boolean({ 38 | description: 'When set to true, disables all colored output.', 39 | default: false, 40 | }), 41 | noEmoji: flags.boolean({ 42 | description: 'When set to true, disables displaying emoji in output.', 43 | default: false, 44 | }), 45 | help: flags.help({ char: 'h' }), 46 | }; 47 | 48 | public async initialize(flags: Flags): Promise { 49 | if (flags.quiet !== true) { 50 | this.logger = new StdoutLogger( 51 | flags.noColor !== true, 52 | flags.noEmoji !== true 53 | ); 54 | } 55 | 56 | if (flags.noEmoji === true) { 57 | this.userError = createUserError(false, true); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/common/common.test.ts: -------------------------------------------------------------------------------- 1 | import { DocumentType, inferDocumentType } from '@superfaceai/ast'; 2 | 3 | describe('inferDocumentType()', () => { 4 | it('infers map type', () => { 5 | expect(inferDocumentType('test.suma')).toEqual(DocumentType.MAP); 6 | }); 7 | 8 | it('infers profile type', () => { 9 | expect(inferDocumentType('test.supr')).toEqual(DocumentType.PROFILE); 10 | }); 11 | 12 | it('returns unknown for unknown types', () => { 13 | expect(inferDocumentType('test.supr.face')).toEqual(DocumentType.UNKNOWN); 14 | expect(inferDocumentType('test.slang')).toEqual(DocumentType.UNKNOWN); 15 | expect(inferDocumentType('test')).toEqual(DocumentType.UNKNOWN); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/common/document.interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface VersionStructure { 2 | major: number; 3 | minor: number; 4 | patch: number; 5 | label?: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/common/document.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_PROFILE_VERSION = { 2 | major: 1, 3 | minor: 0, 4 | patch: 0, 5 | }; 6 | export const DEFAULT_PROFILE_VERSION_STR = '1.0.0'; 7 | export const SF_API_URL_VARIABLE = 'SUPERFACE_API_URL'; 8 | export const SF_PRODUCTION = 'https://superface.ai'; 9 | -------------------------------------------------------------------------------- /src/common/error.ts: -------------------------------------------------------------------------------- 1 | import { CLIError } from '@oclif/errors'; 2 | import { inspect } from 'util'; 3 | 4 | import { VERSION } from '../index'; 5 | import { template } from './chalk-template'; 6 | import { UX } from './ux'; 7 | 8 | /** 9 | * User error. 10 | * 11 | * It is a normal occurence to return an user error. 12 | * 13 | * Has a positive exit code. 14 | */ 15 | export const createUserError = 16 | (emoji = true, addSuffix = true) => 17 | (message: string, code: number): CLIError => { 18 | // Make sure that UX is stoped before throwing an error. 19 | UX.clear(); 20 | 21 | if (code <= 0) { 22 | throw developerError('expected positive error code', 1); 23 | } 24 | 25 | let final = message; 26 | 27 | if (addSuffix) { 28 | final = 29 | final + 30 | template(`\n\nIf you need help with this error, please raise an issue on GitHub Discussions: {underline.bold https://sfc.is/discussions} 31 | 32 | Please include the following information: 33 | - command you ran 34 | - error message 35 | - version of the CLI, OS: superface cli/${VERSION} (${process.platform}-${process.arch}) ${process.release.name}-${process.version} 36 | `); 37 | } 38 | 39 | return new CLIError(emoji ? '❌ ' + final : final, { 40 | exit: code, 41 | }); 42 | }; 43 | export type UserError = ReturnType; 44 | 45 | /** 46 | * Developer error. 47 | * 48 | * It should only be returned from unexpected states and unreachable code. 49 | * 50 | * Has a negative exit code (the parameter `code` must be positive). 51 | */ 52 | export function developerError(message: string, code: number): CLIError { 53 | if (code <= 0) { 54 | throw developerError('expected positive error code', 1); 55 | } 56 | 57 | return new CLIError(`❌ Internal error: ${message}`, { exit: -code }); 58 | } 59 | 60 | export function assertIsGenericError( 61 | error: unknown 62 | ): asserts error is { message: string } { 63 | if (typeof error === 'object' && error !== null && 'message' in error) { 64 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 65 | const err: Record = error; 66 | if (typeof err.message === 'string') { 67 | return; 68 | } 69 | } 70 | 71 | throw developerError(`unexpected error: ${inspect(error)}`, 101); 72 | } 73 | export function assertIsIOError( 74 | error: unknown 75 | ): asserts error is { code: string } { 76 | if (typeof error === 'object' && error !== null && 'code' in error) { 77 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 78 | const err: Record = error; 79 | if (typeof err.code === 'string') { 80 | return; 81 | } 82 | } 83 | 84 | throw developerError(`unexpected error: ${inspect(error)}`, 102); 85 | } 86 | export function assertIsExecError( 87 | error: unknown 88 | ): asserts error is { stdout: string; stderr: string } { 89 | if (typeof error === 'object' && error !== null) { 90 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 91 | const err: Record = error; 92 | if (typeof err.stdout === 'string' && typeof err.stderr === 'string') { 93 | return; 94 | } 95 | } 96 | 97 | throw developerError(`unexpected error: ${inspect(error)}`, 103); 98 | } 99 | 100 | export function stringifyError(error: unknown): string { 101 | try { 102 | if (error instanceof Error) { 103 | const plainObject: Record = {}; 104 | Object.getOwnPropertyNames(error).forEach(function (key) { 105 | plainObject[key] = error[key as keyof Error]; 106 | }); 107 | 108 | return JSON.stringify(plainObject, null, 2); 109 | } 110 | } catch (e) { 111 | void e; 112 | } 113 | 114 | return inspect(error, true, null, true); 115 | } 116 | -------------------------------------------------------------------------------- /src/common/file-structure.test.ts: -------------------------------------------------------------------------------- 1 | import { SupportedLanguages } from '../logic'; 2 | import { 3 | buildMapPath, 4 | buildProfilePath, 5 | buildProjectDefinitionFilePath, 6 | buildProjectDotenvFilePath, 7 | buildProviderPath, 8 | buildRunFilePath, 9 | buildSuperfaceDirPath, 10 | } from './file-structure'; 11 | 12 | describe('fileStructure', () => { 13 | describe('buildSuperfaceDirPath', () => { 14 | it('builds superface dir path', () => { 15 | expect(buildSuperfaceDirPath()).toEqual( 16 | expect.stringContaining(`/superface`) 17 | ); 18 | }); 19 | }); 20 | 21 | describe('buildProfilePath', () => { 22 | it('builds profile path', () => { 23 | expect(buildProfilePath('scope', 'name')).toEqual( 24 | expect.stringContaining(`/superface/scope.name.profile`) 25 | ); 26 | 27 | expect(buildProfilePath(undefined, 'name')).toEqual( 28 | expect.stringContaining(`/superface/name.profile`) 29 | ); 30 | }); 31 | }); 32 | 33 | describe('buildProviderPath', () => { 34 | it('builds provider path', () => { 35 | expect(buildProviderPath('provider')).toEqual( 36 | expect.stringContaining(`/superface/provider.provider.json`) 37 | ); 38 | }); 39 | 40 | it('builds provider path with scope', () => { 41 | expect(buildProviderPath('scope.provider')).toEqual( 42 | expect.stringContaining(`/superface/scope.provider.provider.json`) 43 | ); 44 | }); 45 | }); 46 | 47 | describe('buildMapPath', () => { 48 | it('builds map path', () => { 49 | expect( 50 | buildMapPath({ profileName: 'profile', providerName: 'provider' }) 51 | ).toEqual(expect.stringContaining(`/superface/profile.provider.map.js`)); 52 | }); 53 | 54 | it('builds map path with scope', () => { 55 | expect( 56 | buildMapPath({ 57 | profileScope: 'scope', 58 | profileName: 'profile', 59 | providerName: 'provider', 60 | }) 61 | ).toEqual( 62 | expect.stringContaining(`/superface/scope.profile.provider.map.js`) 63 | ); 64 | }); 65 | }); 66 | 67 | describe('buildRunFilePath', () => { 68 | describe('JS', () => { 69 | it('builds runfile path', () => { 70 | expect( 71 | buildRunFilePath({ 72 | profileName: 'profile', 73 | providerName: 'provider', 74 | language: SupportedLanguages.JS, 75 | }) 76 | ).toEqual(expect.stringContaining(`/superface/profile.provider.mjs`)); 77 | }); 78 | 79 | it('builds runfile path with scope', () => { 80 | expect( 81 | buildRunFilePath({ 82 | profileScope: 'scope', 83 | profileName: 'profile', 84 | providerName: 'provider', 85 | language: SupportedLanguages.JS, 86 | }) 87 | ).toEqual( 88 | expect.stringContaining(`/superface/scope.profile.provider.mjs`) 89 | ); 90 | }); 91 | }); 92 | 93 | describe('Python', () => { 94 | it('builds runfile path', () => { 95 | expect( 96 | buildRunFilePath({ 97 | profileName: 'profile', 98 | providerName: 'provider', 99 | language: SupportedLanguages.PYTHON, 100 | }) 101 | ).toEqual(expect.stringContaining(`/superface/profile.provider.py`)); 102 | }); 103 | 104 | it('builds runfile path with scope', () => { 105 | expect( 106 | buildRunFilePath({ 107 | profileScope: 'scope', 108 | profileName: 'profile', 109 | providerName: 'provider', 110 | language: SupportedLanguages.PYTHON, 111 | }) 112 | ).toEqual( 113 | expect.stringContaining(`/superface/scope.profile.provider.py`) 114 | ); 115 | }); 116 | }); 117 | }); 118 | 119 | describe('buildProjectDefinitionFilePath', () => { 120 | it('builds project definition file path', () => { 121 | expect(buildProjectDefinitionFilePath()).toEqual( 122 | expect.stringContaining(`/superface/package.json`) 123 | ); 124 | }); 125 | }); 126 | 127 | describe('buildProjectDotenvFilePath', () => { 128 | it('builds project .env file path', () => { 129 | expect(buildProjectDotenvFilePath()).toEqual( 130 | expect.stringContaining(`/superface/.env`) 131 | ); 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /src/common/file-structure.ts: -------------------------------------------------------------------------------- 1 | import { basename, join, resolve } from 'path'; 2 | 3 | import { SupportedLanguages } from '../logic/application-code/application-code'; 4 | 5 | const DEFAULT_SUPERFACE_DIR = 'superface'; 6 | 7 | const PROFILE_EXTENSION = '.profile'; 8 | 9 | const PROVIDER_EXTENSION = '.provider.json'; 10 | 11 | const MAP_EXTENSION = '.map.js'; 12 | 13 | export function isInsideSuperfaceDir(): boolean { 14 | return basename(process.cwd()) === DEFAULT_SUPERFACE_DIR; 15 | } 16 | 17 | export function buildSuperfaceDirPath(): string { 18 | // If user is in superface dir, use it 19 | if (isInsideSuperfaceDir()) return resolve(process.cwd()); 20 | 21 | return join(resolve(process.cwd()), DEFAULT_SUPERFACE_DIR); 22 | } 23 | 24 | export function buildProfilePath( 25 | scope: string | undefined, 26 | name: string 27 | ): string { 28 | return join( 29 | buildSuperfaceDirPath(), 30 | `${scope !== undefined ? `${scope}.` : ''}${name}${PROFILE_EXTENSION}` 31 | ); 32 | } 33 | 34 | export function buildProviderPath(providerName: string): string { 35 | return join(buildSuperfaceDirPath(), `${providerName}${PROVIDER_EXTENSION}`); 36 | } 37 | 38 | export function buildMapPath({ 39 | profileScope, 40 | profileName, 41 | providerName, 42 | }: { 43 | profileScope?: string; 44 | profileName: string; 45 | providerName: string; 46 | }): string { 47 | return join( 48 | buildSuperfaceDirPath(), 49 | `${ 50 | profileScope !== undefined ? `${profileScope}.` : '' 51 | }${profileName}.${providerName}${MAP_EXTENSION}` 52 | ); 53 | } 54 | 55 | export function buildRunFilePath({ 56 | profileScope, 57 | profileName, 58 | providerName, 59 | language, 60 | }: { 61 | profileScope?: string; 62 | profileName: string; 63 | providerName: string; 64 | language: SupportedLanguages; 65 | }): string { 66 | const EXTENSION_MAP: { [key in SupportedLanguages]: string } = { 67 | js: '.mjs', 68 | python: '.py', 69 | }; 70 | 71 | const extension = EXTENSION_MAP[language]; 72 | 73 | return join( 74 | buildSuperfaceDirPath(), 75 | `${ 76 | profileScope !== undefined ? `${profileScope}.` : '' 77 | }${profileName}.${providerName}${extension}` 78 | ); 79 | } 80 | 81 | export function buildProjectDefinitionFilePath( 82 | language: SupportedLanguages = SupportedLanguages.JS 83 | ): string { 84 | const FILENAME_MAP: { [key in SupportedLanguages]: string } = { 85 | js: 'package.json', 86 | python: 'requirements.txt', 87 | }; 88 | 89 | return join(buildSuperfaceDirPath(), FILENAME_MAP[language]); 90 | } 91 | 92 | export function buildProjectDotenvFilePath(): string { 93 | return join(buildSuperfaceDirPath(), '.env'); 94 | } 95 | -------------------------------------------------------------------------------- /src/common/format.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import { formatPath } from './format'; 4 | 5 | describe('Format utils', () => { 6 | describe('formatPath', () => { 7 | const HOME_PATH = '/Users/admin'; 8 | const APP_PATH = join(HOME_PATH, 'Documents/my-app'); 9 | const SUPERFACE_DIR_PATH = join(APP_PATH, 'superface'); 10 | const PROFILE_PATH = join(SUPERFACE_DIR_PATH, 'scope.name.profile'); 11 | 12 | it('formats path to Profile from ~/', () => { 13 | expect(formatPath(PROFILE_PATH, HOME_PATH)).toEqual( 14 | 'Documents/my-app/superface/scope.name.profile' 15 | ); 16 | }); 17 | 18 | it('formats path to Profile from app root', () => { 19 | expect(formatPath(PROFILE_PATH, APP_PATH)).toEqual( 20 | 'superface/scope.name.profile' 21 | ); 22 | }); 23 | 24 | it('formats path to Profile from Superface directory', () => { 25 | expect(formatPath(PROFILE_PATH, SUPERFACE_DIR_PATH)).toEqual( 26 | 'scope.name.profile' 27 | ); 28 | }); 29 | 30 | it('formats path to app root from Superface directory', () => { 31 | expect(formatPath(APP_PATH, SUPERFACE_DIR_PATH)).toEqual('..'); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/common/format.ts: -------------------------------------------------------------------------------- 1 | import { relative } from 'path'; 2 | 3 | export function formatPath( 4 | absolutePath: string, 5 | relativeTo = process.cwd() 6 | ): string { 7 | return relative(relativeTo, absolutePath); 8 | } 9 | -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './document'; 2 | export * from './document.interfaces'; 3 | export * from './log'; 4 | -------------------------------------------------------------------------------- /src/common/log.ts: -------------------------------------------------------------------------------- 1 | import { green, grey, red, yellow } from 'chalk'; 2 | 3 | import type { MessageArgs, MessageKeys } from './messages'; 4 | import { messages } from './messages'; 5 | 6 | export interface ILogger { 7 | info(template: K, ...args: MessageArgs): void; 8 | warn(template: K, ...args: MessageArgs): void; 9 | success(template: K, ...args: MessageArgs): void; 10 | error(template: K, ...args: MessageArgs): void; 11 | } 12 | 13 | export class StdoutLogger implements ILogger { 14 | private readonly successPrefix = '🆗'; 15 | private readonly errorPrefix = '❌'; 16 | private readonly warningPrefix = '⚠️'; 17 | 18 | constructor( 19 | private readonly color: boolean, 20 | private readonly emoji: boolean 21 | ) {} 22 | 23 | public info( 24 | template: K, 25 | ...args: MessageArgs 26 | ): void { 27 | const message = this.getMessage(template, ...args); 28 | process.stdout.write(this.formatInfo(message)); 29 | } 30 | 31 | public success( 32 | template: K, 33 | ...args: MessageArgs 34 | ): void { 35 | const message = this.getMessage(template, ...args); 36 | process.stdout.write(this.formatSuccess(message)); 37 | } 38 | 39 | public warn( 40 | template: K, 41 | ...args: MessageArgs 42 | ): void { 43 | const message = this.getMessage(template, ...args); 44 | process.stdout.write(this.formatWarn(message)); 45 | } 46 | 47 | public error( 48 | template: K, 49 | ...args: MessageArgs 50 | ): void { 51 | const message = this.getMessage(template, ...args); 52 | process.stderr.write(this.formatError(message)); 53 | } 54 | 55 | private formatInfo(input: string): string { 56 | const message = this.color ? grey(input) : input; 57 | 58 | return message + '\n'; 59 | } 60 | 61 | private formatSuccess(input: string): string { 62 | const message = this.emoji ? `${this.successPrefix} ${input}` : input; 63 | 64 | return (this.color ? green(message) : message) + '\n'; 65 | } 66 | 67 | private formatWarn(input: string): string { 68 | const message = this.emoji ? `${this.warningPrefix} ${input}` : input; 69 | 70 | return (this.color ? yellow(message) : message) + '\n'; 71 | } 72 | 73 | private formatError(input: string): string { 74 | const message = this.emoji ? `${this.errorPrefix} ${input}` : input; 75 | 76 | return (this.color ? red(message) : message) + '\n'; 77 | } 78 | 79 | private getMessage( 80 | messsageTemplate: K, 81 | ...args: MessageArgs 82 | ): string { 83 | return (messages[messsageTemplate] as (...args: unknown[]) => string)( 84 | ...args 85 | ); 86 | } 87 | } 88 | 89 | /* eslint-disable @typescript-eslint/no-empty-function */ 90 | export class DummyLogger implements ILogger { 91 | public info( 92 | _template: K, 93 | ..._args: MessageArgs 94 | ): void {} 95 | 96 | public success( 97 | _template: K, 98 | ..._arg: MessageArgs 99 | ): void {} 100 | 101 | public warn( 102 | _template: K, 103 | ..._args: MessageArgs 104 | ): void {} 105 | 106 | public error( 107 | _template: K, 108 | ..._args: MessageArgs 109 | ): void {} 110 | } 111 | /* eslint-enable @typescript-eslint/no-empty-function */ 112 | 113 | export class MockLogger implements ILogger { 114 | public stdout: Array<[message: MessageKeys, args: MessageArgs]> = 115 | []; 116 | 117 | public stderr: Array<[message: MessageKeys, args: MessageArgs]> = 118 | []; 119 | 120 | public info( 121 | template: K, 122 | ...args: MessageArgs 123 | ): void { 124 | this.stdout.push([template, args]); 125 | } 126 | 127 | public success( 128 | template: K, 129 | ...args: MessageArgs 130 | ): void { 131 | this.stdout.push([template, args]); 132 | } 133 | 134 | public warn( 135 | template: K, 136 | ...args: MessageArgs 137 | ): void { 138 | this.stdout.push([template, args]); 139 | } 140 | 141 | public error( 142 | template: K, 143 | ...args: MessageArgs 144 | ): void { 145 | this.stderr.push([template, args]); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/common/netrc.test.ts: -------------------------------------------------------------------------------- 1 | import { getServicesUrl } from './http'; 2 | import { loadNetrc, saveNetrc } from './netrc'; 3 | 4 | jest.mock('./http'); 5 | 6 | const mockRefreshToken = 'RT'; 7 | const mockBaseUrlWithExistingRecord = 'existing'; 8 | const mockBaseUrlWithEmptyRecord = 'empty'; 9 | const mockBaseUrl = 'superface.ai'; 10 | 11 | const mockLoadSync = jest.fn(); 12 | const mockSave = jest.fn(); 13 | const mockLoad = jest.fn(); 14 | jest.mock('netrc-parser', () => { 15 | return { 16 | // Netrc is not default export so we need this 17 | Netrc: jest.fn().mockImplementation(() => { 18 | return { 19 | loadSync: mockLoadSync, 20 | save: mockSave, 21 | load: mockLoad, 22 | machines: { 23 | [mockBaseUrlWithExistingRecord]: { 24 | password: mockRefreshToken, 25 | }, 26 | [mockBaseUrlWithEmptyRecord]: {}, 27 | }, 28 | }; 29 | }), 30 | }; 31 | }); 32 | 33 | describe('NetRc functions', () => { 34 | describe('when loading netrc record', () => { 35 | it('calls netrc correctly with existing record', () => { 36 | jest 37 | .mocked(getServicesUrl) 38 | .mockReturnValue(mockBaseUrlWithExistingRecord); 39 | expect(loadNetrc()).toEqual({ 40 | baseUrl: mockBaseUrlWithExistingRecord, 41 | refreshToken: mockRefreshToken, 42 | }); 43 | expect(mockLoadSync).toHaveBeenCalled(); 44 | }); 45 | 46 | it('calls netrc correctly with empty record', () => { 47 | jest.mocked(getServicesUrl).mockReturnValue(mockBaseUrlWithEmptyRecord); 48 | expect(loadNetrc()).toEqual({ 49 | baseUrl: mockBaseUrlWithEmptyRecord, 50 | refreshToken: undefined, 51 | }); 52 | expect(mockLoadSync).toHaveBeenCalled(); 53 | }); 54 | 55 | it('calls netrc correctly with undefined record', () => { 56 | jest.mocked(getServicesUrl).mockReturnValue(mockBaseUrl); 57 | expect(loadNetrc()).toEqual({ 58 | baseUrl: mockBaseUrl, 59 | refreshToken: undefined, 60 | }); 61 | expect(mockLoadSync).toHaveBeenCalled(); 62 | }); 63 | }); 64 | 65 | describe('when saving netrc record', () => { 66 | it('calls netrc correctly', async () => { 67 | await expect( 68 | saveNetrc(mockBaseUrl, mockRefreshToken) 69 | ).resolves.toBeUndefined(); 70 | 71 | expect(mockLoad).toHaveBeenCalled(); 72 | expect(mockSave).toHaveBeenCalled(); 73 | }); 74 | 75 | it('calls netrc correctly with null refresh token', async () => { 76 | await expect(saveNetrc(mockBaseUrl, null)).resolves.toBeUndefined(); 77 | 78 | expect(mockLoad).toHaveBeenCalled(); 79 | expect(mockSave).toHaveBeenCalled(); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/common/netrc.ts: -------------------------------------------------------------------------------- 1 | import { Netrc } from 'netrc-parser'; 2 | 3 | import { getServicesUrl } from './http'; 4 | 5 | export function loadNetrc(): { 6 | baseUrl: string; 7 | refreshToken?: string; 8 | } { 9 | // environment variable for specific netrc file 10 | const netrc = new Netrc(process.env.NETRC_FILEPATH); 11 | const baseUrl = getServicesUrl(); 12 | netrc.loadSync(); 13 | const superfaceEntry = netrc.machines[baseUrl] ?? {}; 14 | 15 | return { 16 | baseUrl, 17 | refreshToken: superfaceEntry.password, 18 | }; 19 | } 20 | 21 | export async function saveNetrc( 22 | baseUrl: string, 23 | refreshToken: string | null 24 | ): Promise { 25 | // environment variable for specific netrc file 26 | const netrc = new Netrc(process.env.NETRC_FILEPATH); 27 | await netrc.load(); 28 | 29 | // Remove old record 30 | netrc.machines[baseUrl] = {}; 31 | 32 | netrc.machines[baseUrl].password = refreshToken ?? undefined; 33 | netrc.machines[baseUrl].baseUrl = baseUrl; 34 | await netrc.save(); 35 | } 36 | -------------------------------------------------------------------------------- /src/common/output-stream.test.ts: -------------------------------------------------------------------------------- 1 | import { mkdir, writeFile } from 'fs/promises'; 2 | import { join } from 'path'; 3 | 4 | import { rimraf, streamEnd, streamWrite } from '../common/io'; 5 | import { OutputStream } from '../common/output-stream'; 6 | 7 | // Mock only streamWrite and streamEnd response 8 | jest.mock('../common/io', () => ({ 9 | ...jest.requireActual>('../common/io'), 10 | streamWrite: jest.fn(), 11 | streamEnd: jest.fn(), 12 | })); 13 | 14 | jest.mock('../index'); 15 | 16 | describe('OutputStream', () => { 17 | const WORKING_DIR = join('fixtures', 'io'); 18 | 19 | let INITIAL_CWD: string; 20 | 21 | beforeAll(async () => { 22 | INITIAL_CWD = process.cwd(); 23 | process.chdir(WORKING_DIR); 24 | }); 25 | 26 | afterAll(async () => { 27 | // change cwd back 28 | process.chdir(INITIAL_CWD); 29 | }); 30 | 31 | afterEach(async () => { 32 | jest.resetAllMocks(); 33 | await rimraf('test'); 34 | }); 35 | 36 | describe('when writing to stream', () => { 37 | it('calls write to file correctly', async () => { 38 | const outputStream = new OutputStream('test/test.txt', { dirs: true }); 39 | await outputStream.write('testData'); 40 | expect(streamWrite).toHaveBeenCalledTimes(1); 41 | expect(streamWrite).toHaveBeenCalledWith(outputStream.stream, 'testData'); 42 | }, 10000); 43 | 44 | it('calls write to stdout correctly', async () => { 45 | const outputStream = new OutputStream('-'); 46 | await outputStream.write('testData'); 47 | expect(streamWrite).toHaveBeenCalledTimes(1); 48 | expect(streamWrite).toHaveBeenCalledWith(outputStream.stream, 'testData'); 49 | }, 10000); 50 | 51 | it('calls write to stderr correctly', async () => { 52 | const outputStream = new OutputStream('-2'); 53 | await outputStream.write('testData'); 54 | expect(streamWrite).toHaveBeenCalledTimes(1); 55 | expect(streamWrite).toHaveBeenCalledWith(outputStream.stream, 'testData'); 56 | }, 10000); 57 | }); 58 | 59 | describe('when calling cleanup', () => { 60 | it('calls streamEnd correctly', async () => { 61 | const outputStream = new OutputStream('test/test.json', { dirs: true }); 62 | await outputStream.cleanup(); 63 | expect(streamEnd).toHaveBeenCalledTimes(1); 64 | expect(streamEnd).toHaveBeenCalledWith(outputStream.stream); 65 | }, 10000); 66 | 67 | it('does not call streamEnd', async () => { 68 | const outputStream = new OutputStream('-'); 69 | await outputStream.cleanup(); 70 | expect(streamEnd).not.toHaveBeenCalled(); 71 | }, 10000); 72 | }); 73 | 74 | describe('when writing once', () => { 75 | it('calls streamEnd correctly', async () => { 76 | await OutputStream.writeOnce('test/test.json', 'testData', { 77 | dirs: true, 78 | }); 79 | expect(streamWrite).toHaveBeenCalledTimes(1); 80 | expect(streamWrite).toHaveBeenCalledWith(expect.anything(), 'testData'); 81 | expect(streamEnd).toHaveBeenCalledTimes(1); 82 | }, 10000); 83 | 84 | it('does not call streamEnd', async () => { 85 | const outputStream = new OutputStream('-'); 86 | await outputStream.cleanup(); 87 | expect(streamEnd).not.toHaveBeenCalled(); 88 | }, 10000); 89 | }); 90 | 91 | describe('when calling writeIfAbsent', () => { 92 | it('returns false if file exists and there is no force flag', async () => { 93 | await mkdir('test', { recursive: true }); 94 | await writeFile(join('test', 'test.txt'), 'testData'); 95 | 96 | expect( 97 | await OutputStream.writeIfAbsent(join('test', 'test.txt'), 'testData', { 98 | dirs: true, 99 | }) 100 | ).toEqual(false); 101 | }, 10000); 102 | 103 | it('returns true if file does not exist', async () => { 104 | expect( 105 | await OutputStream.writeIfAbsent('test/test.json', 'testData', { 106 | dirs: true, 107 | }) 108 | ).toEqual(true); 109 | expect(streamWrite).toHaveBeenCalledTimes(1); 110 | expect(streamWrite).toHaveBeenCalledWith(expect.anything(), 'testData'); 111 | }, 10000); 112 | 113 | it('returns true if there is force flag', async () => { 114 | // create file 115 | expect( 116 | await OutputStream.writeIfAbsent('test/test.json', 'testData1', { 117 | dirs: true, 118 | }) 119 | ).toEqual(true); 120 | expect(streamWrite).toHaveBeenCalledTimes(1); 121 | expect(streamWrite).toHaveBeenCalledWith(expect.anything(), 'testData1'); 122 | expect( 123 | await OutputStream.writeIfAbsent('test/test.json', 'testData2', { 124 | dirs: true, 125 | force: true, 126 | }) 127 | ).toEqual(true); 128 | expect(streamWrite).toHaveBeenCalledTimes(2); 129 | expect(streamWrite).toHaveBeenCalledWith(expect.anything(), 'testData2'); 130 | }, 10000); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /src/common/output-stream.ts: -------------------------------------------------------------------------------- 1 | import createDebug from 'debug'; 2 | import * as fs from 'fs'; 3 | import { dirname } from 'path'; 4 | import type { Writable } from 'stream'; 5 | 6 | import type { WritingOptions } from './io'; 7 | import { exists, streamEnd, streamWrite } from './io'; 8 | 9 | const outputStreamDebug = createDebug('superface:output-stream'); 10 | export class OutputStream { 11 | private readonly name: string; 12 | public readonly stream: Writable; 13 | public readonly isStdStream: boolean; 14 | public readonly isTTY: boolean; 15 | 16 | /** 17 | * Constructs the output object. 18 | * 19 | * `path` accepts 2 special values: 20 | * * `-` - initializes output for stdout 21 | * * `-2` - initializes output for stderr 22 | * 23 | * All other `path` values are treated as file system path. 24 | * 25 | * When `append` is true the file at `path` is opened in append mode rather than in write (truncate) mode. 26 | */ 27 | constructor(path: string, options?: WritingOptions) { 28 | switch (path) { 29 | case '-': 30 | outputStreamDebug('Opening stdout'); 31 | this.name = 'stdout'; 32 | this.stream = process.stdout; 33 | this.isStdStream = true; 34 | this.isTTY = process.stdout.isTTY; 35 | break; 36 | 37 | case '-2': 38 | outputStreamDebug('Opening stderr'); 39 | this.name = 'stderr'; 40 | this.stream = process.stderr; 41 | this.isStdStream = true; 42 | this.isTTY = process.stdout.isTTY; 43 | break; 44 | 45 | default: 46 | outputStreamDebug( 47 | `Opening/creating "${path}" in ${ 48 | options?.append !== undefined ? 'append' : 'write' 49 | } mode` 50 | ); 51 | if (options?.dirs === true) { 52 | const dir = dirname(path); 53 | fs.mkdirSync(dir, { recursive: true }); 54 | } 55 | 56 | this.name = path; 57 | this.stream = fs.createWriteStream(path, { 58 | flags: options?.append !== undefined ? 'a' : 'w', 59 | mode: 0o644, 60 | encoding: 'utf-8', 61 | }); 62 | this.isStdStream = false; 63 | this.isTTY = process.stdout.isTTY; 64 | break; 65 | } 66 | } 67 | 68 | public write(data: string): Promise { 69 | outputStreamDebug(`Writing ${data.length} characters to "${this.name}"`); 70 | 71 | return streamWrite(this.stream, data); 72 | } 73 | 74 | public cleanup(): Promise { 75 | outputStreamDebug(`Closing stream "${this.name}"`); 76 | 77 | // TODO: Should we also end stdout or stderr? 78 | if (!this.isStdStream) { 79 | return streamEnd(this.stream); 80 | } 81 | 82 | return Promise.resolve(); 83 | } 84 | 85 | public static async writeOnce( 86 | path: string, 87 | data: string, 88 | options?: WritingOptions 89 | ): Promise { 90 | const stream = new OutputStream(path, options); 91 | 92 | await stream.write(data); 93 | 94 | return stream.cleanup(); 95 | } 96 | 97 | /** 98 | * Creates file with given contents if it doesn't exist. 99 | * 100 | * Returns whether the file was created. 101 | * 102 | * For convenience the `force` option can be provided 103 | * to force the creation. 104 | * 105 | * The `dirs` option additionally recursively creates 106 | * directories up until the file path. 107 | */ 108 | public static async writeIfAbsent( 109 | path: string, 110 | data: string | (() => string), 111 | options?: WritingOptions 112 | ): Promise { 113 | if (options?.force === true || !(await exists(path))) { 114 | const dat = typeof data === 'string' ? data : data(); 115 | 116 | await OutputStream.writeOnce(path, dat, options); 117 | 118 | return true; 119 | } 120 | 121 | return false; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/common/package-manager.ts: -------------------------------------------------------------------------------- 1 | import { join, normalize, relative } from 'path'; 2 | 3 | import { execShell, exists } from './io'; 4 | import type { ILogger } from './log'; 5 | 6 | interface IPackageManager { 7 | packageJsonExists(): Promise; 8 | getUsedPm(): Promise<'npm' | 'yarn' | undefined>; 9 | init(initPm: 'npm' | 'yarn'): Promise; 10 | installPackage(packageName: string): Promise; 11 | } 12 | 13 | export class PackageManager implements IPackageManager { 14 | private usedPackageManager: 'npm' | 'yarn' | undefined = undefined; 15 | private path: string | undefined = undefined; 16 | 17 | constructor(private readonly logger: ILogger) {} 18 | 19 | public async packageJsonExists(): Promise { 20 | const path = await this.getPath(); 21 | if (path !== undefined && (await exists(join(path, 'package.json')))) { 22 | return true; 23 | } 24 | 25 | return false; 26 | } 27 | 28 | public async getUsedPm(): Promise<'npm' | 'yarn' | undefined> { 29 | if (this.usedPackageManager) { 30 | return this.usedPackageManager; 31 | } 32 | const path = await this.getPath(); 33 | if (path === undefined) { 34 | return; 35 | } 36 | 37 | // Try to find yarn.lock 38 | if (await exists(join(path, 'yarn.lock'))) { 39 | this.usedPackageManager = 'yarn'; 40 | 41 | return 'yarn'; 42 | } 43 | 44 | // Try to find package-lock.json 45 | if (await exists(join(path, 'package-lock.json'))) { 46 | this.usedPackageManager = 'npm'; 47 | 48 | return 'npm'; 49 | } 50 | 51 | return; 52 | } 53 | 54 | public async init(initPm: 'yarn' | 'npm'): Promise { 55 | const pm = await this.getUsedPm(); 56 | if (pm && pm === initPm) { 57 | this.logger.error('pmAlreadyInitialized', pm); 58 | 59 | return false; 60 | } 61 | const command = initPm === 'yarn' ? 'yarn init -y' : 'npm init -y'; 62 | 63 | this.logger.info('initPmOnPath', initPm, process.cwd()); 64 | const result = await execShell(command); 65 | if (result.stderr !== '') { 66 | this.logger.error('shellCommandError', command, result.stderr.trimEnd()); 67 | } 68 | 69 | if (result.stdout !== '') { 70 | this.logger.info('stdout', result.stdout.trimEnd()); 71 | } 72 | 73 | // Set used PM after init 74 | this.usedPackageManager = initPm; 75 | 76 | return true; 77 | } 78 | 79 | public async installPackage(packageName: string): Promise { 80 | if (!(await this.packageJsonExists())) { 81 | this.logger.error('pmNotInitialized', packageName); 82 | 83 | return false; 84 | } 85 | const pm = await this.getUsedPm(); 86 | 87 | const command = 88 | pm === 'yarn' ? `yarn add ${packageName}` : `npm install ${packageName}`; 89 | 90 | const path = (await this.getPath()) ?? process.cwd(); 91 | // Install package to package.json on discovered path or on cwd 92 | this.logger.info('installPackageOnPath', packageName, path, command); 93 | const result = await execShell(command, { cwd: path }); 94 | if (result.stderr !== '') { 95 | this.logger.error('shellCommandError', command, result.stderr.trimEnd()); 96 | } 97 | 98 | if (result.stdout !== '') { 99 | this.logger.info('stdout', result.stdout.trimEnd()); 100 | } 101 | 102 | return true; 103 | } 104 | 105 | private async getPath(): Promise { 106 | if (this.path !== undefined) { 107 | return this.path; 108 | } 109 | 110 | const npmPrefix = await execShell('npm prefix'); 111 | 112 | if (npmPrefix.stderr !== '') { 113 | this.logger.error('shellCommandError', 'npm prefix', npmPrefix.stderr); 114 | 115 | return; 116 | } 117 | this.path = 118 | relative(process.cwd(), npmPrefix.stdout.trim()) || normalize('./'); 119 | 120 | return this.path; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/common/profile.ts: -------------------------------------------------------------------------------- 1 | import { parseDocumentId } from '@superfaceai/parser'; 2 | 3 | import type { UserError } from './error'; 4 | 5 | export class ProfileId { 6 | public readonly scope?: string; 7 | public readonly name: string; 8 | public readonly id: string; 9 | 10 | public static fromId( 11 | profileId: string, 12 | { userError }: { userError: UserError } 13 | ): ProfileId { 14 | const parsed = parseDocumentId(profileId); 15 | if (parsed.kind === 'error') { 16 | throw userError(`Invalid profile id: ${parsed.message}`, 1); 17 | } 18 | 19 | return ProfileId.fromScopeName(parsed.value.scope, parsed.value.middle[0]); 20 | } 21 | 22 | public static fromScopeName( 23 | scope: string | undefined, 24 | name: string 25 | ): ProfileId { 26 | return new ProfileId(scope, name); 27 | } 28 | 29 | private constructor(scope: string | undefined, name: string) { 30 | this.scope = scope; 31 | this.name = name; 32 | this.id = scope !== undefined ? `${scope}/${name}` : name; 33 | } 34 | 35 | public withVersion(version?: string): string { 36 | return `${this.id}${version !== undefined ? `@${version}` : ''}`; 37 | } 38 | 39 | public toString(): string { 40 | return this.id; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/common/provider.ts: -------------------------------------------------------------------------------- 1 | import type { ProviderJson } from '@superfaceai/ast'; 2 | import { 3 | AssertionError, 4 | assertProviderJson, 5 | isValidProviderName, 6 | } from '@superfaceai/ast'; 7 | import type { ServiceClient } from '@superfaceai/service-client'; 8 | 9 | import type { UserError } from './error'; 10 | import { stringifyError } from './error'; 11 | import { buildProviderPath } from './file-structure'; 12 | import { exists, readFile } from './io'; 13 | import { OutputStream } from './output-stream'; 14 | 15 | // TODO: move to common 16 | export async function resolveProviderJson( 17 | providerName: string, 18 | { userError, client }: { userError: UserError; client: ServiceClient } 19 | ): Promise< 20 | { 21 | providerJson: ProviderJson; 22 | } & ({ source: 'local'; path: string } | { source: 'remote' }) 23 | > { 24 | if (!isValidProviderName(providerName)) { 25 | throw userError('Invalid provider name', 1); 26 | } 27 | 28 | let resolvedProviderJson: { 29 | providerJson: ProviderJson; 30 | } & ({ source: 'local'; path: string } | { source: 'remote' }); 31 | 32 | if (!(await exists(buildProviderPath(providerName)))) { 33 | resolvedProviderJson = { 34 | providerJson: await resolveRemote(providerName, { userError, client }), 35 | source: 'remote', 36 | }; 37 | } else { 38 | const localProviderJson = await resolveLocal(providerName, { userError }); 39 | resolvedProviderJson = { 40 | providerJson: localProviderJson.providerJson, 41 | path: localProviderJson.path, 42 | source: 'local', 43 | }; 44 | } 45 | 46 | if (providerName !== resolvedProviderJson.providerJson.name) { 47 | throw userError( 48 | `Provider name in provider.json file does not match provider name in command.`, 49 | 1 50 | ); 51 | } 52 | 53 | if ( 54 | resolvedProviderJson.providerJson.services.length === 1 && 55 | resolvedProviderJson.providerJson.services[0].baseUrl.includes('TODO') 56 | ) { 57 | if (resolvedProviderJson.source === 'local') { 58 | throw userError( 59 | `Provider.json file is not properly configured. Please make sure to replace 'TODO' in baseUrl with the actual base url of the API.`, 60 | 1 61 | ); 62 | } 63 | 64 | throw userError( 65 | `Provider.json file saved to: ${buildProviderPath( 66 | providerName 67 | )} but it is not properly configured. Please make sure to replace 'TODO' in baseUrl with the actual base url of the API.`, 68 | 1 69 | ); 70 | } 71 | 72 | return resolvedProviderJson; 73 | } 74 | 75 | async function resolveRemote( 76 | providerName: string, 77 | { userError, client }: { userError: UserError; client: ServiceClient } 78 | ): Promise { 79 | let resolvedProviderJson: ProviderJson | undefined; 80 | 81 | let providerResponse: Response; 82 | try { 83 | providerResponse = await client.fetch( 84 | `/authoring/providers/${providerName}`, 85 | { 86 | method: 'GET', 87 | headers: { 88 | accept: 'application/json', 89 | }, 90 | } 91 | ); 92 | } catch (e) { 93 | throw userError( 94 | `Failed to fetch provider.json file from Superface API. ${stringifyError( 95 | e 96 | )}`, 97 | 1 98 | ); 99 | } 100 | 101 | if (providerResponse.status === 200) { 102 | try { 103 | const response: unknown = await providerResponse.json(); 104 | 105 | assertProviderResponse(response); 106 | 107 | resolvedProviderJson = assertProviderJson(response.definition); 108 | } catch (e) { 109 | if (e instanceof AssertionError) { 110 | throw userError(`Invalid provider.json. ${e.message}`, 1); 111 | } 112 | throw userError(`Invalid provider.json - invalid JSON`, 1); 113 | } 114 | } else if (providerResponse.status === 404) { 115 | throw userError( 116 | `Provider ${providerName} does not exist. Make sure to run "superface prepare" before running this command.`, 117 | 1 118 | ); 119 | } else { 120 | throw userError( 121 | `Failed to fetch provider.json file from Superface API. ${stringifyError( 122 | providerResponse 123 | )}`, 124 | 1 125 | ); 126 | } 127 | 128 | await OutputStream.writeOnce( 129 | buildProviderPath(resolvedProviderJson.name), 130 | JSON.stringify(resolvedProviderJson, null, 2) 131 | ); 132 | 133 | return resolvedProviderJson; 134 | } 135 | 136 | async function resolveLocal( 137 | providerName: string, 138 | { userError }: { userError: UserError } 139 | ): Promise<{ 140 | providerJson: ProviderJson; 141 | path: string; 142 | }> { 143 | let resolvedProviderJson: ProviderJson | undefined; 144 | const path = buildProviderPath(providerName); 145 | const providerJsonFile = await readFile(path, 'utf-8'); 146 | let providerJson: ProviderJson; 147 | try { 148 | providerJson = JSON.parse(providerJsonFile) as ProviderJson; 149 | } catch (e) { 150 | throw userError(`Invalid provider.json file - invalid JSON`, 1); 151 | } 152 | 153 | try { 154 | resolvedProviderJson = assertProviderJson(providerJson); 155 | } catch (e) { 156 | if (e instanceof AssertionError) { 157 | throw userError(`Invalid provider.json file. ${e.message}`, 1); 158 | } 159 | throw userError(`Invalid provider.json file.`, 1); 160 | } 161 | 162 | return { 163 | providerJson: resolvedProviderJson, 164 | path, 165 | }; 166 | } 167 | 168 | function assertProviderResponse(response: unknown): asserts response is { 169 | definition: unknown; 170 | } { 171 | if ( 172 | typeof response !== 'object' || 173 | response === null || 174 | !('definition' in response) 175 | ) { 176 | throw new Error('Invalid provider.json response'); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/common/ux.ts: -------------------------------------------------------------------------------- 1 | import { green, hex, red } from 'chalk'; 2 | import type { Spinner } from 'nanospinner'; 3 | import { createSpinner } from 'nanospinner'; 4 | 5 | import { template } from '../common/chalk-template'; 6 | 7 | const WARN_COLOR = '#B48817'; 8 | 9 | export class UX { 10 | private static instance: UX | undefined; 11 | 12 | private readonly spinner: Spinner; 13 | 14 | private lastText = ''; 15 | 16 | private constructor() { 17 | this.spinner = createSpinner(undefined, { color: 'cyan', interval: 50 }); 18 | UX.instance = this; 19 | } 20 | 21 | public start(text: string): void { 22 | this.lastText = ''; 23 | this.spinner.start({ text }); 24 | } 25 | 26 | public succeed(text: string): void { 27 | this.spinner.success({ text: green(template(text)), mark: green('✔') }); 28 | } 29 | 30 | public fail(text: string): void { 31 | this.spinner.error({ text: red(template(text)), mark: red('✖') }); 32 | } 33 | 34 | public info(text: string): void { 35 | if (text.trim() !== this.lastText.trim()) { 36 | this.spinner.clear(); 37 | this.spinner.update({ text }); 38 | } 39 | 40 | this.lastText = text; 41 | } 42 | 43 | public warn(text: string): void { 44 | this.spinner.warn({ 45 | text: hex(WARN_COLOR)(template(text)), 46 | mark: hex(WARN_COLOR)('⚠'), 47 | }); 48 | } 49 | 50 | public stop(): void { 51 | this.spinner.stop(); 52 | } 53 | 54 | public static create(): UX { 55 | if (UX.instance === undefined) { 56 | UX.instance = new UX(); 57 | } 58 | 59 | return UX.instance; 60 | } 61 | 62 | public static clear(): void { 63 | UX.instance?.spinner.clear(); 64 | UX.instance?.spinner.stop(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-var-requires 2 | const packageJson = require('../package.json'); 3 | 4 | export * from './logic'; 5 | export * from './common'; 6 | // export { run } from '@oclif/command'; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access 9 | export const VERSION: string = packageJson.version; 10 | -------------------------------------------------------------------------------- /src/logic/application-code/application-code.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IntegrationParameter, 3 | ProfileDocumentNode, 4 | ProviderJson, 5 | SecurityScheme, 6 | UseCaseDefinitionNode, 7 | } from '@superfaceai/ast'; 8 | 9 | import type { ILogger } from '../../common'; 10 | import type { UserError } from '../../common/error'; 11 | import { stringifyError } from '../../common/error'; 12 | import { prepareUseCaseInput } from './input/prepare-usecase-input'; 13 | import { jsApplicationCode } from './js'; 14 | import { pythonApplicationCode } from './python'; 15 | 16 | export enum SupportedLanguages { 17 | PYTHON = 'python', 18 | JS = 'js', 19 | } 20 | 21 | export function getLanguageName(language: SupportedLanguages): string { 22 | const LANGUAGE_MAP: { 23 | [key in SupportedLanguages]: string; 24 | } = { 25 | js: 'JavaScript', 26 | python: 'Python', 27 | }; 28 | 29 | return LANGUAGE_MAP[language]; 30 | } 31 | 32 | export type ApplicationCodeWriter = ( 33 | { 34 | profile, 35 | useCaseName, 36 | provider, 37 | input, 38 | parameters, 39 | security, 40 | }: { 41 | profile: { 42 | name: string; 43 | scope?: string; 44 | }; 45 | useCaseName: string; 46 | provider: string; 47 | // TODO: more language independent type for input? 48 | input: string; 49 | parameters?: IntegrationParameter[]; 50 | security?: SecurityScheme[]; 51 | }, 52 | { logger }: { logger: ILogger } 53 | ) => { 54 | code: string; 55 | requiredParameters: string[]; 56 | requiredSecurity: string[]; 57 | }; 58 | 59 | export async function writeApplicationCode( 60 | { 61 | providerJson, 62 | profileAst, 63 | language, 64 | }: // useCaseName, 65 | { 66 | providerJson: ProviderJson; 67 | profileAst: ProfileDocumentNode; 68 | language: SupportedLanguages; 69 | }, 70 | { logger, userError }: { logger: ILogger; userError: UserError } 71 | ): Promise<{ 72 | code: string; 73 | requiredParameters: string[]; 74 | requiredSecurity: string[]; 75 | }> { 76 | const useCases = profileAst.definitions.filter( 77 | (definition): definition is UseCaseDefinitionNode => { 78 | return definition.kind === 'UseCaseDefinition'; 79 | } 80 | ); 81 | 82 | if (useCases.length === 0) { 83 | throw userError( 84 | `No use cases found in profile ${profileAst.header.name}`, 85 | 1 86 | ); 87 | } 88 | 89 | if (useCases.length > 1) { 90 | throw userError( 91 | `Multiple use cases found in profile ${profileAst.header.name}. Currently only one use case is per profile file is supported.`, 92 | 1 93 | ); 94 | } 95 | 96 | const useCaseName = useCases[0].useCaseName; 97 | 98 | // TODO: this should be language independent and also take use case name as input 99 | let inputExample: string; 100 | try { 101 | inputExample = prepareUseCaseInput(profileAst, language); 102 | } catch (error) { 103 | // TODO: fallback to empty object? 104 | throw userError( 105 | `Input example construction failed: ${stringifyError(error)}`, 106 | 1 107 | ); 108 | } 109 | 110 | const APPLICATION_CODE_MAP: { 111 | [key in SupportedLanguages]: ApplicationCodeWriter; 112 | } = { 113 | js: jsApplicationCode, 114 | python: pythonApplicationCode, 115 | }; 116 | 117 | return APPLICATION_CODE_MAP[language]( 118 | { 119 | profile: { 120 | name: profileAst.header.name, 121 | scope: profileAst.header.scope, 122 | }, 123 | useCaseName, 124 | provider: providerJson.name, 125 | input: inputExample, 126 | parameters: providerJson.parameters, 127 | security: providerJson.securitySchemes, 128 | }, 129 | { logger } 130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /src/logic/application-code/dotenv/dotenv.ts: -------------------------------------------------------------------------------- 1 | import type { IntegrationParameter, SecurityScheme } from '@superfaceai/ast'; 2 | import { 3 | prepareProviderParameters, 4 | prepareSecurityValues, 5 | } from '@superfaceai/ast'; 6 | 7 | import { ONESDK_LOG_COMMENT, ONESDK_LOG_ENV } from './onesdk-log'; 8 | import { 9 | ONESDK_TOKEN_COMMENT, 10 | ONESDK_TOKEN_ENV, 11 | ONESDK_TOKEN_UNAVAILABLE_COMMENT, 12 | } from './onesdk-token'; 13 | 14 | export type NewDotenv = { 15 | content: string; 16 | newEmptyEnvVariables: string[]; 17 | }; 18 | 19 | type EnvVar = { 20 | name: string; 21 | value: string | undefined; 22 | comment: string | undefined; 23 | }; 24 | 25 | export function createNewDotenv({ 26 | previousDotenv, 27 | providerName, 28 | parameters, 29 | security, 30 | token, 31 | logEnabled, 32 | }: { 33 | previousDotenv?: string; 34 | providerName: string; 35 | parameters?: IntegrationParameter[]; 36 | security?: SecurityScheme[]; 37 | token?: string | null; 38 | logEnabled?: boolean; 39 | }): NewDotenv { 40 | const previousContent = previousDotenv ?? ''; 41 | 42 | const parameterEnvs = getParameterEnvs(providerName, parameters); 43 | const securityEnvs = getSecurityEnvs(providerName, security); 44 | const tokenEnv = makeTokenEnv(token); 45 | const logEnv = makeLogEnv(logEnabled === true ? 'on' : 'off'); 46 | 47 | const newEnvsOnly = makeFilterForNewEnvs(previousContent); 48 | 49 | const newEnvVariables = [tokenEnv, logEnv, ...parameterEnvs, ...securityEnvs] 50 | .filter(uniqueEnvsOnly) 51 | .filter(newEnvsOnly); 52 | 53 | return { 54 | content: serializeContent(previousContent, newEnvVariables), 55 | newEmptyEnvVariables: newEnvVariables 56 | .filter(e => e.value === undefined) 57 | .map(e => e.name), 58 | }; 59 | } 60 | 61 | function makeLogEnv(logValue = 'off'): EnvVar { 62 | return { 63 | name: ONESDK_LOG_ENV, 64 | value: logValue, 65 | comment: ONESDK_LOG_COMMENT, 66 | }; 67 | } 68 | 69 | function makeTokenEnv(token?: string | null): EnvVar { 70 | return { 71 | name: ONESDK_TOKEN_ENV, 72 | value: token ?? undefined, 73 | comment: 74 | token !== undefined && token !== null 75 | ? ONESDK_TOKEN_COMMENT 76 | : ONESDK_TOKEN_UNAVAILABLE_COMMENT, 77 | }; 78 | } 79 | 80 | function uniqueEnvsOnly(env: EnvVar, ix: number, arr: EnvVar[]): boolean { 81 | return arr.findIndex(e => e.name === env.name) === ix; 82 | } 83 | 84 | function makeFilterForNewEnvs(content: string): (e: EnvVar) => boolean { 85 | const existingEnvs = new Set( 86 | content 87 | .split('\n') 88 | .map(line => line.match(/^(\w+)=/)?.[1]) 89 | .filter((s: string | undefined): s is string => Boolean(s)) 90 | .map(t => t.toLowerCase()) 91 | ); 92 | 93 | // returns true for envs that are NOT present in the `content` 94 | return env => { 95 | return !existingEnvs.has(env.name.toLowerCase()); 96 | }; 97 | } 98 | 99 | function serializeContent(previousContent: string, newEnvs: EnvVar[]): string { 100 | const newEnvContent = newEnvs.map(serializeEnvVar).join('\n').trim(); 101 | 102 | const newContent = [previousContent, newEnvContent] 103 | .filter(Boolean) 104 | .join('\n'); 105 | 106 | return newEnvContent ? newContent + '\n' : newContent; 107 | } 108 | 109 | function serializeEnvVar(env: EnvVar): string { 110 | const comment = 111 | env.comment !== undefined 112 | ? '\n' + 113 | env.comment 114 | .split('\n') 115 | .map(commentLine => `# ${commentLine}`) 116 | .join('\n') 117 | : ''; 118 | 119 | return `${comment ? comment + '\n' : ''}${env.name}=${env.value ?? ''}`; 120 | } 121 | 122 | function getParameterEnvs( 123 | providerName: string, 124 | parameters?: IntegrationParameter[] 125 | ): EnvVar[] { 126 | const params = parameters || []; 127 | 128 | const parameterEnvs = prepareProviderParameters(providerName, params); 129 | 130 | return params.map(param => ({ 131 | name: removeDollarSign(parameterEnvs[param.name]), 132 | value: param.default ?? undefined, 133 | comment: param.description ?? undefined, 134 | })); 135 | } 136 | 137 | function getSecurityEnvs( 138 | providerName: string, 139 | security?: SecurityScheme[] 140 | ): EnvVar[] { 141 | const securityValues = prepareSecurityValues(providerName, security || []); 142 | 143 | return securityValues 144 | .map(({ id: _, ...securityValue }) => securityValue) 145 | .flatMap(securityValue => Object.values(securityValue) as string[]) 146 | .map(removeDollarSign) 147 | .map(name => ({ name, value: undefined, comment: undefined })); 148 | } 149 | 150 | const removeDollarSign = (text: string): string => 151 | text.startsWith('$') ? text.slice(1) : text; 152 | -------------------------------------------------------------------------------- /src/logic/application-code/dotenv/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dotenv'; 2 | export * from './onesdk-token'; 3 | -------------------------------------------------------------------------------- /src/logic/application-code/dotenv/onesdk-log.ts: -------------------------------------------------------------------------------- 1 | export const ONESDK_LOG_ENV = 'ONESDK_LOG'; 2 | export const ONESDK_LOG_COMMENT = 'Set to "on" to enable debug logging'; 3 | -------------------------------------------------------------------------------- /src/logic/application-code/dotenv/onesdk-token.ts: -------------------------------------------------------------------------------- 1 | export const ONESDK_TOKEN_ENV = 'SUPERFACE_ONESDK_TOKEN'; 2 | export const ONESDK_TOKEN_COMMENT = 3 | 'The token for monitoring your Comlinks at https://superface.ai'; 4 | export const ONESDK_TOKEN_UNAVAILABLE_COMMENT = 5 | 'Set your OneSDK token to monitor your usage out-of-the-box. Get yours at https://superface.ai'; 6 | -------------------------------------------------------------------------------- /src/logic/application-code/index.ts: -------------------------------------------------------------------------------- 1 | export * from './application-code'; 2 | export * from './dotenv'; 3 | -------------------------------------------------------------------------------- /src/logic/application-code/input/example/build.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ComlinkLiteralNode, 3 | NamedFieldDefinitionNode, 4 | NamedModelDefinitionNode, 5 | Type, 6 | UseCaseDefinitionNode, 7 | } from '@superfaceai/ast'; 8 | 9 | import { parse as buildExampleFromAst } from './structure-tree'; 10 | import type { UseCaseExample } from './usecase-example'; 11 | 12 | function extractExample( 13 | exampleLiteral: ComlinkLiteralNode | undefined, 14 | typeDefinition: Type | undefined, 15 | namedModelDefinitionsCache: { 16 | [key: string]: NamedModelDefinitionNode; 17 | }, 18 | namedFieldDefinitionsCache: { 19 | [key: string]: NamedFieldDefinitionNode; 20 | } 21 | ): UseCaseExample | undefined { 22 | if (typeDefinition !== undefined) { 23 | return buildExampleFromAst( 24 | typeDefinition, 25 | namedModelDefinitionsCache, 26 | namedFieldDefinitionsCache, 27 | exampleLiteral 28 | ); 29 | } 30 | 31 | return undefined; 32 | } 33 | 34 | export function buildUseCaseExamples( 35 | useCase: UseCaseDefinitionNode, 36 | namedModelDefinitionsCache: { 37 | [key: string]: NamedModelDefinitionNode; 38 | }, 39 | namedFieldDefinitionsCache: { 40 | [key: string]: NamedFieldDefinitionNode; 41 | } 42 | ): { 43 | errorExamples: { 44 | input?: UseCaseExample; 45 | error?: UseCaseExample; 46 | }[]; 47 | successExamples: { 48 | input?: UseCaseExample; 49 | result?: UseCaseExample; 50 | }[]; 51 | } { 52 | const profileExamples = findUseCaseExamples(useCase); 53 | 54 | // Fall back to empty object when there are no examples in profile to ensure we get at least one example generated from type 55 | const profileErrorExamples = 56 | profileExamples.errorExamples !== undefined && 57 | profileExamples.errorExamples.length > 0 58 | ? profileExamples.errorExamples 59 | : [{}]; 60 | const profileSuccessExamples = 61 | profileExamples.successExamples !== undefined && 62 | profileExamples.successExamples.length > 0 63 | ? profileExamples.successExamples 64 | : [{}]; 65 | 66 | return { 67 | errorExamples: profileErrorExamples.map(example => ({ 68 | input: extractExample( 69 | example?.input, 70 | useCase.input?.value, 71 | namedModelDefinitionsCache, 72 | namedFieldDefinitionsCache 73 | ), 74 | error: extractExample( 75 | example?.error, 76 | useCase.error?.value, 77 | namedModelDefinitionsCache, 78 | namedFieldDefinitionsCache 79 | ), 80 | })), 81 | successExamples: profileSuccessExamples.map(example => ({ 82 | input: extractExample( 83 | example?.input, 84 | useCase.input?.value, 85 | namedModelDefinitionsCache, 86 | namedFieldDefinitionsCache 87 | ), 88 | result: extractExample( 89 | example?.result, 90 | useCase.result?.value, 91 | namedModelDefinitionsCache, 92 | namedFieldDefinitionsCache 93 | ), 94 | })), 95 | }; 96 | } 97 | 98 | function findUseCaseExamples(usecase: UseCaseDefinitionNode): { 99 | errorExamples?: { 100 | input?: ComlinkLiteralNode; 101 | error?: ComlinkLiteralNode; 102 | }[]; 103 | successExamples?: { 104 | input?: ComlinkLiteralNode; 105 | result?: ComlinkLiteralNode; 106 | }[]; 107 | } { 108 | const examples: { 109 | successExamples?: { 110 | input?: ComlinkLiteralNode; 111 | result?: ComlinkLiteralNode; 112 | }[]; 113 | errorExamples?: { 114 | input?: ComlinkLiteralNode; 115 | error?: ComlinkLiteralNode; 116 | }[]; 117 | } = { successExamples: undefined, errorExamples: undefined }; 118 | 119 | if (usecase.examples === undefined || usecase.examples.length === 0) 120 | return examples; 121 | 122 | const exampleNodes = usecase.examples.filter( 123 | slot => 124 | slot.kind === 'UseCaseSlotDefinition' && 125 | slot.value.kind === 'UseCaseExample' 126 | ); 127 | const successExampleNodes = exampleNodes 128 | .filter(example => example.value?.error === undefined) 129 | .map(e => e.value); 130 | 131 | const errorExampleNodes = exampleNodes 132 | .filter(example => Boolean(example.value?.error)) 133 | .map(e => e.value); 134 | 135 | if (successExampleNodes.length !== 0) { 136 | examples.successExamples = successExampleNodes.map(node => ({ 137 | input: node.input?.value, 138 | result: node.result?.value, 139 | })); 140 | } 141 | 142 | if (errorExampleNodes.length !== 0) { 143 | examples.errorExamples = errorExampleNodes.map(node => ({ 144 | input: node.input?.value, 145 | error: node.error?.value, 146 | })); 147 | } 148 | 149 | return examples; 150 | } 151 | -------------------------------------------------------------------------------- /src/logic/application-code/input/example/structure-tree/index.ts: -------------------------------------------------------------------------------- 1 | export { parse } from './parse'; 2 | -------------------------------------------------------------------------------- /src/logic/application-code/input/example/usecase-example.ts: -------------------------------------------------------------------------------- 1 | export type ExampleScalar = ExampleBoolean | ExampleNumber | ExampleString; 2 | 3 | type ExampleString = { 4 | kind: 'string'; 5 | value: string; 6 | required: boolean; 7 | }; 8 | 9 | type ExampleNumber = { 10 | kind: 'number'; 11 | value: number; 12 | required: boolean; 13 | }; 14 | 15 | type ExampleBoolean = { 16 | kind: 'boolean'; 17 | value: boolean; 18 | required: boolean; 19 | }; 20 | 21 | export type ExampleObject = { 22 | kind: 'object'; 23 | properties: ({ name: string } & ( 24 | | ExampleArray 25 | | ExampleScalar 26 | | ExampleNone 27 | | ExampleObject 28 | ))[]; 29 | }; 30 | 31 | export type ExampleArray = { 32 | kind: 'array'; 33 | items: (ExampleArray | ExampleObject | ExampleScalar | ExampleNone)[]; 34 | required: boolean; 35 | }; 36 | 37 | export type ExampleNone = { 38 | kind: 'none'; 39 | }; 40 | 41 | export type UseCaseExample = 42 | | ExampleArray 43 | | ExampleObject 44 | | ExampleScalar 45 | | ExampleNone; 46 | -------------------------------------------------------------------------------- /src/logic/application-code/input/prepare-usecase-input.test.ts: -------------------------------------------------------------------------------- 1 | import { parseProfile, Source } from '@superfaceai/parser'; 2 | 3 | import { SupportedLanguages } from '../application-code'; 4 | import { prepareUseCaseInput } from './prepare-usecase-input'; 5 | 6 | describe('prepareUseCaseInput', () => { 7 | const mockProfileSource = `name = "test" 8 | version = "0.0.0" 9 | 10 | usecase Test safe { 11 | 12 | input { 13 | a! string! 14 | b! number 15 | c! boolean 16 | d! [number] 17 | e [string!]! 18 | f! [boolean] 19 | g! { 20 | a number 21 | b number 22 | }! 23 | h { 24 | a string 25 | b string 26 | } 27 | i { 28 | a boolean 29 | b boolean 30 | }! 31 | j! [{ 32 | k! string 33 | m { 34 | n! number 35 | } 36 | }] 37 | l { 38 | a [{ 39 | b [boolean] 40 | c! { 41 | d! number 42 | } 43 | }] 44 | } 45 | fieldA! 46 | fieldB! 47 | fieldC! 48 | fieldD! 49 | 50 | r modelA! 51 | } 52 | example InputExample { 53 | input { 54 | a = 'Luke', 55 | b = 1.2, 56 | c = true, 57 | d = [1], 58 | e = ['a', 'b'], 59 | f = [true, false], 60 | g = { a = 1, b = 2 }, 61 | h = { a = 'a', b = 'b' }, 62 | i = { a = true, b = false }, 63 | j = [{ k = 'a', m = { n = 1 } }], 64 | l = { a = [{ b = [ true ], c = { d = 12 }}] }, 65 | fieldA = 'a', 66 | fieldB = 1, 67 | fieldC = true, 68 | fieldD = "A", 69 | r = { a = 'a' } 70 | } 71 | } 72 | } 73 | 74 | field fieldA string! 75 | field fieldB number 76 | field fieldC boolean 77 | field fieldD enum { 78 | A 79 | B 80 | } 81 | 82 | model modelA { 83 | a string! 84 | }`; 85 | 86 | const ast = parseProfile(new Source(mockProfileSource, 'test.supr')); 87 | 88 | describe('for js', () => { 89 | it('should prepare input for use case', () => { 90 | const input = prepareUseCaseInput(ast, SupportedLanguages.JS); 91 | expect(input).toEqual(`{ 92 | a: 'Luke', 93 | b: 1.2, 94 | c: true, 95 | d: [ 96 | 1, 97 | ], 98 | e: [ 99 | 'a', 100 | 'b', 101 | ], 102 | f: [ 103 | true, 104 | false, 105 | ], 106 | g: { 107 | a: 1, 108 | b: 2, 109 | }, 110 | h: { 111 | a: 'a', 112 | b: 'b', 113 | }, 114 | i: { 115 | a: true, 116 | b: false, 117 | }, 118 | j: [ 119 | { 120 | k: 'a', 121 | m: { 122 | n: 1, 123 | }, 124 | }, 125 | ], 126 | l: { 127 | a: [ 128 | { 129 | b: [ 130 | true, 131 | ], 132 | c: { 133 | d: 12, 134 | }, 135 | }, 136 | ], 137 | }, 138 | fieldA: 'a', 139 | fieldB: 1, 140 | fieldC: true, 141 | fieldD: 'A', 142 | r: { 143 | a: 'a', 144 | }, 145 | }`); 146 | }); 147 | }); 148 | 149 | describe('for python', () => { 150 | it('should prepare input for use case', () => { 151 | const input = prepareUseCaseInput(ast, SupportedLanguages.PYTHON); 152 | expect(input).toEqual(`{ 153 | "a": 'Luke', 154 | "b": 1.2, 155 | "c": True, 156 | "d": [ 157 | 1, 158 | ], 159 | "e": [ 160 | 'a', 161 | 'b', 162 | ], 163 | "f": [ 164 | True, 165 | False, 166 | ], 167 | "g": { 168 | "a": 1, 169 | "b": 2, 170 | }, 171 | "h": { 172 | "a": 'a', 173 | "b": 'b', 174 | }, 175 | "i": { 176 | "a": True, 177 | "b": False, 178 | }, 179 | "j": [ 180 | { 181 | "k": 'a', 182 | "m": { 183 | "n": 1, 184 | }, 185 | }, 186 | ], 187 | "l": { 188 | "a": [ 189 | { 190 | "b": [ 191 | True, 192 | ], 193 | "c": { 194 | "d": 12, 195 | }, 196 | }, 197 | ], 198 | }, 199 | "fieldA": 'a', 200 | "fieldB": 1, 201 | "fieldC": True, 202 | "fieldD": 'A', 203 | "r": { 204 | "a": 'a', 205 | }, 206 | }`); 207 | }); 208 | }); 209 | }); 210 | -------------------------------------------------------------------------------- /src/logic/application-code/input/prepare-usecase-input.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | NamedFieldDefinitionNode, 3 | NamedModelDefinitionNode, 4 | ProfileDocumentNode, 5 | UseCaseDefinitionNode, 6 | } from '@superfaceai/ast'; 7 | 8 | import type { SupportedLanguages } from '../application-code'; 9 | import { buildUseCaseExamples } from './example/build'; 10 | import type { UseCaseExample } from './example/usecase-example'; 11 | import INPUT_TEMPLATE from './templates'; 12 | import { makeRenderer } from './templates/template-renderer'; 13 | 14 | export function prepareUseCaseInput( 15 | ast: ProfileDocumentNode, 16 | language: SupportedLanguages 17 | ): string { 18 | const namedModelDefinitionsCache: { 19 | [key: string]: NamedModelDefinitionNode; 20 | } = {}; 21 | 22 | const namedFieldDefinitionsCache: { 23 | [key: string]: NamedFieldDefinitionNode; 24 | } = {}; 25 | 26 | ast.definitions.forEach(definition => { 27 | if (definition.kind === 'NamedFieldDefinition') { 28 | namedFieldDefinitionsCache[definition.fieldName] = definition; 29 | } else if (definition.kind === 'NamedModelDefinition') { 30 | namedModelDefinitionsCache[definition.modelName] = definition; 31 | } 32 | }); 33 | 34 | // const errorExamples: { 35 | // input?: UseCaseExample; 36 | // error?: UseCaseExample; 37 | // }[] = []; 38 | 39 | const successExamples: { 40 | input?: UseCaseExample; 41 | result?: UseCaseExample; 42 | }[] = []; 43 | 44 | ast.definitions 45 | .filter((definition): definition is UseCaseDefinitionNode => { 46 | return definition.kind === 'UseCaseDefinition'; 47 | }) 48 | .forEach(useCase => { 49 | const { 50 | // errorExamples: errorExamplesForUseCase, 51 | successExamples: successExamplesForUseCase, 52 | } = buildUseCaseExamples( 53 | useCase, 54 | namedModelDefinitionsCache, 55 | namedFieldDefinitionsCache 56 | ); 57 | 58 | // errorExamples.push(...errorExamplesForUseCase); 59 | successExamples.push(...successExamplesForUseCase); 60 | }); 61 | 62 | // const QUOTES_MAP: { 63 | // [key in SupportedLanguages]: string; 64 | // } = { 65 | // js: '', 66 | // python: '"', 67 | // }; 68 | 69 | const inputExample = successExamples.find(e => e.input !== undefined)?.input; 70 | 71 | const render = makeRenderer(INPUT_TEMPLATE, 'Input'); 72 | 73 | return render({ input: inputExample, language }); 74 | } 75 | -------------------------------------------------------------------------------- /src/logic/application-code/input/templates/array.ts: -------------------------------------------------------------------------------- 1 | export default '[{{#each items}}{{newLine (inc ../indent 2) }}{{#ifeq kind "string"}}{{>String language=../language }},{{/ifeq}}{{#ifeq kind "number"}}{{>Number language=../language }},{{/ifeq}}{{#ifeq kind "boolean"}}{{>Boolean language=../language }},{{/ifeq}}{{#ifeq kind "object"}}{{>Object use=":" indent=(inc ../indent 2) language=../language }},{{/ifeq}}{{#ifeq kind "array"}}{{>Array use=":" indent= (inc ../indent 2) language=../language }},{{/ifeq}}{{/each}}{{newLine indent}}]'; 2 | -------------------------------------------------------------------------------- /src/logic/application-code/input/templates/boolean.ts: -------------------------------------------------------------------------------- 1 | export default '{{booleanValue language value}}'; 2 | -------------------------------------------------------------------------------- /src/logic/application-code/input/templates/index.ts: -------------------------------------------------------------------------------- 1 | import array from './array'; 2 | import boolean from './boolean'; 3 | import input from './input'; 4 | import number from './number'; 5 | import object from './object'; 6 | import string from './string'; 7 | import type { Template } from './template-renderer'; 8 | 9 | const templateSet: Template[] = [ 10 | { name: 'Input', template: input }, 11 | { name: 'Object', template: object }, 12 | { name: 'Array', template: array }, 13 | { name: 'String', template: string }, 14 | { name: 'Number', template: number }, 15 | { name: 'Boolean', template: boolean }, 16 | ]; 17 | 18 | export default templateSet; 19 | -------------------------------------------------------------------------------- /src/logic/application-code/input/templates/input.ts: -------------------------------------------------------------------------------- 1 | export default '{{#if input}}{{#ifeq input.kind "object"}}{{>Object input use=":" indent=6 language=language}}{{/ifeq}}{{#ifeq input.kind "array"}}{{>Array input use=":" indent=6 language=language}}{{/ifeq}}{{/if}}'; 2 | -------------------------------------------------------------------------------- /src/logic/application-code/input/templates/number.ts: -------------------------------------------------------------------------------- 1 | export default '{{value}}'; 2 | -------------------------------------------------------------------------------- /src/logic/application-code/input/templates/object.ts: -------------------------------------------------------------------------------- 1 | export default '{{openObject}}{{#each properties}}{{#ifeq kind "string"}}{{newLine (inc ../indent 2) }}{{quotes ../language}}{{name}}{{quotes ../language}}{{../use}} {{>String language=../language }},{{/ifeq}}{{#ifeq kind "number"}}{{newLine (inc ../indent 2) }}{{quotes ../language}}{{name}}{{quotes ../language}}{{../use}} {{>Number language=../language }},{{/ifeq}}{{#ifeq kind "boolean"}}{{newLine (inc ../indent 2) }}{{quotes ../language}}{{name}}{{quotes ../language}}{{../use}} {{>Boolean language=../language }},{{/ifeq}}{{#ifeq kind "object"}}{{newLine (inc ../indent 2) }}{{quotes ../language}}{{name}}{{quotes ../language}}{{../use}} {{>Object use=":" indent= (inc ../indent 2) language=../language }},{{/ifeq}}{{#ifeq kind "array"}}{{newLine (inc ../indent 2) }}{{quotes ../language}}{{name}}{{quotes ../language}}{{../use}} {{>Array use=":" indent= (inc ../indent 2) language=../language}},{{/ifeq}}{{/each}}{{newLine indent}}{{closeObject}}'; 2 | -------------------------------------------------------------------------------- /src/logic/application-code/input/templates/string.ts: -------------------------------------------------------------------------------- 1 | export default '{{escapedString value}}'; 2 | -------------------------------------------------------------------------------- /src/logic/application-code/input/templates/template-renderer/helpers.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import { SupportedLanguages } from '../../../application-code'; 4 | 5 | //TODO: add types 6 | export const HELPERS = [ 7 | { 8 | name: 'assign', 9 | helper: function (name: any, value: any, options: any) { 10 | if (!options.data.root) { 11 | options.data.root = {}; 12 | } 13 | options.data.root[name] = value; 14 | }, 15 | }, 16 | { 17 | name: 'ifeq', 18 | helper: function (a: any, b: any, options: any) { 19 | // @ts-ignore 20 | if (a === b) return options.fn(this); 21 | // @ts-ignore 22 | return options.inverse(this); 23 | }, 24 | }, 25 | { 26 | name: 'switch', 27 | helper: function (value: any, options: any) { 28 | // @ts-ignore 29 | this.switch_value = value; 30 | // @ts-ignore 31 | this.switch_break = false; 32 | // @ts-ignore 33 | return options.fn(this); 34 | }, 35 | }, 36 | { 37 | name: 'case', 38 | helper: function (value: any, options: any) { 39 | // @ts-ignore 40 | if (value == this.switch_value) { 41 | // @ts-ignore 42 | this.switch_break = true; 43 | // @ts-ignore 44 | return options.fn(this); 45 | } 46 | }, 47 | }, 48 | { 49 | name: 'default', 50 | helper: function (options: any) { 51 | // @ts-ignore 52 | if (this.switch_break == false) { 53 | // @ts-ignore 54 | return options.fn(this); 55 | } 56 | }, 57 | }, 58 | { 59 | name: 'typeof', 60 | helper: function (value: any) { 61 | return typeof value; 62 | }, 63 | }, 64 | { 65 | name: 'escapedString', 66 | helper: function (value: any) { 67 | if (value === '') return '""'; 68 | if (!value) return value; 69 | if (value.includes("'")) { 70 | if (value.includes('"')) { 71 | return `"${value.replace(/\"/g, '\\"')}"`; 72 | } 73 | 74 | return `"${value}"`; 75 | } 76 | 77 | return `'${value}'`; 78 | }, 79 | }, 80 | { 81 | name: 'newLine', 82 | helper: function (value: number) { 83 | return `\n${' '.repeat(value)}`; 84 | }, 85 | }, 86 | { 87 | name: 'inc', 88 | helper: function (value: any = 0, amount = 2) { 89 | return parseInt(value) + amount; 90 | }, 91 | }, 92 | { 93 | name: 'quotes', 94 | helper: function (language: string) { 95 | if (language === SupportedLanguages.PYTHON) { 96 | return `"`; 97 | } 98 | return ''; 99 | }, 100 | }, 101 | { 102 | name: 'comment', 103 | helper: function (language: string) { 104 | if (language === SupportedLanguages.PYTHON) { 105 | return `#`; 106 | } 107 | return '//'; 108 | }, 109 | }, 110 | 111 | { 112 | name: 'booleanValue', 113 | helper: function (language: string, value: boolean) { 114 | if (language === SupportedLanguages.PYTHON) { 115 | return value ? 'True' : 'False'; 116 | } 117 | return value; 118 | }, 119 | }, 120 | 121 | { 122 | name: 'openObject', 123 | helper: function () { 124 | return `{`; 125 | }, 126 | }, 127 | { 128 | name: 'closeObject', 129 | helper: function () { 130 | return `}`; 131 | }, 132 | }, 133 | ]; 134 | 135 | export default HELPERS; 136 | -------------------------------------------------------------------------------- /src/logic/application-code/input/templates/template-renderer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './template.interface'; 2 | export * from './renderer'; 3 | -------------------------------------------------------------------------------- /src/logic/application-code/input/templates/template-renderer/renderer.ts: -------------------------------------------------------------------------------- 1 | import Handlebars from 'handlebars'; 2 | 3 | import { HELPERS } from './helpers'; 4 | import type { Template } from './template.interface'; 5 | 6 | export function makeRenderer( 7 | templates: Template[] = [], 8 | entryPartial = 'index', 9 | helpers = HELPERS 10 | ): Handlebars.TemplateDelegate { 11 | const engine = Handlebars.create(); 12 | 13 | helpers.forEach(({ name, helper }) => { 14 | engine.registerHelper(name, helper); 15 | }); 16 | 17 | templates.forEach(({ name, template }) => { 18 | engine.registerPartial(name, template); 19 | }); 20 | 21 | return engine.compile(`{{>${entryPartial}}}`, { 22 | noEscape: true, 23 | strict: true, 24 | explicitPartialContext: false, 25 | ignoreStandalone: false, 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/logic/application-code/input/templates/template-renderer/template.interface.ts: -------------------------------------------------------------------------------- 1 | export type Template = { 2 | name: string; 3 | template: string; 4 | }; 5 | -------------------------------------------------------------------------------- /src/logic/application-code/js/application-code.test.ts: -------------------------------------------------------------------------------- 1 | import { MockLogger } from '../../../common'; 2 | import { buildSuperfaceDirPath } from '../../../common/file-structure'; 3 | import { jsApplicationCode } from './application-code'; 4 | 5 | jest.mock('../../../common/file-structure'); 6 | 7 | describe('jsApplicationCode', () => { 8 | afterEach(() => { 9 | jest.resetAllMocks(); 10 | }); 11 | 12 | it('should return correct application code for unix', async () => { 13 | jest 14 | .mocked(buildSuperfaceDirPath) 15 | .mockReturnValue('/Users/test/cli-test/superface'); 16 | const scope = 'test-scope'; 17 | const name = 'test-name'; 18 | const provider = 'test-provider'; 19 | const useCaseName = 'test-use-case-name'; 20 | 21 | const logger = new MockLogger(); 22 | 23 | const result = jsApplicationCode( 24 | { 25 | profile: { 26 | scope, 27 | name, 28 | }, 29 | useCaseName, 30 | provider, 31 | input: '{}', 32 | parameters: [], 33 | security: [], 34 | }, 35 | { logger } 36 | ); 37 | 38 | expect(result).toEqual({ 39 | code: `import { config } from "dotenv"; 40 | // Load OneClient from SDK 41 | import { 42 | OneClient, 43 | PerformError, 44 | UnexpectedError, 45 | ValidationError, 46 | } from "@superfaceai/one-sdk"; 47 | 48 | // Load environment variables from .env file 49 | config(); 50 | 51 | const client = new OneClient({ 52 | // The token for monitoring your Comlinks at https://superface.ai 53 | token: process.env.SUPERFACE_ONESDK_TOKEN, 54 | // Path to Comlinks within your project 55 | assetsPath: "/Users/test/cli-test/superface", 56 | }); 57 | 58 | // Load Comlink profile and use case 59 | const profile = await client.getProfile("${scope}/${name}"); 60 | const useCase = profile.getUseCase("${useCaseName}"); 61 | 62 | try { 63 | // Execute use case 64 | const result = await useCase.perform( 65 | // Use case input 66 | {}, 67 | { 68 | provider: "${provider}", 69 | parameters: {}, 70 | // Security values for provider 71 | security: {}, 72 | } 73 | ); 74 | 75 | console.log("RESULT:", JSON.stringify(result, null, 2)); 76 | } catch (e) { 77 | if (e instanceof PerformError) { 78 | console.log("ERROR RESULT:", e.errorResult); 79 | } else if (e instanceof ValidationError) { 80 | console.error("VALIDATION ERROR:", e.message); 81 | } else if (e instanceof UnexpectedError) { 82 | console.error("ERROR:", e); 83 | } else { 84 | throw e; 85 | } 86 | } 87 | `, 88 | requiredParameters: [], 89 | requiredSecurity: [], 90 | }); 91 | }); 92 | 93 | it('should return correct application code for windows', async () => { 94 | jest 95 | .mocked(buildSuperfaceDirPath) 96 | .mockReturnValue('C:\\Users\\my\\cli-test\\superface'); 97 | const scope = 'test-scope'; 98 | const name = 'test-name'; 99 | const provider = 'test-provider'; 100 | const useCaseName = 'test-use-case-name'; 101 | 102 | const logger = new MockLogger(); 103 | 104 | const result = jsApplicationCode( 105 | { 106 | profile: { 107 | scope, 108 | name, 109 | }, 110 | useCaseName, 111 | provider, 112 | input: '{}', 113 | parameters: [], 114 | security: [], 115 | }, 116 | { logger } 117 | ); 118 | 119 | expect(result).toEqual({ 120 | code: `import { config } from "dotenv"; 121 | // Load OneClient from SDK 122 | import { 123 | OneClient, 124 | PerformError, 125 | UnexpectedError, 126 | ValidationError, 127 | } from "@superfaceai/one-sdk"; 128 | 129 | // Load environment variables from .env file 130 | config(); 131 | 132 | const client = new OneClient({ 133 | // The token for monitoring your Comlinks at https://superface.ai 134 | token: process.env.SUPERFACE_ONESDK_TOKEN, 135 | // Path to Comlinks within your project 136 | assetsPath: "C:\\\\Users\\\\my\\\\cli-test\\\\superface", 137 | }); 138 | 139 | // Load Comlink profile and use case 140 | const profile = await client.getProfile("${scope}/${name}"); 141 | const useCase = profile.getUseCase("${useCaseName}"); 142 | 143 | try { 144 | // Execute use case 145 | const result = await useCase.perform( 146 | // Use case input 147 | {}, 148 | { 149 | provider: "${provider}", 150 | parameters: {}, 151 | // Security values for provider 152 | security: {}, 153 | } 154 | ); 155 | 156 | console.log("RESULT:", JSON.stringify(result, null, 2)); 157 | } catch (e) { 158 | if (e instanceof PerformError) { 159 | console.log("ERROR RESULT:", e.errorResult); 160 | } else if (e instanceof ValidationError) { 161 | console.error("VALIDATION ERROR:", e.message); 162 | } else if (e instanceof UnexpectedError) { 163 | console.error("ERROR:", e); 164 | } else { 165 | throw e; 166 | } 167 | } 168 | `, 169 | requiredParameters: [], 170 | requiredSecurity: [], 171 | }); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /src/logic/application-code/js/application-code.ts: -------------------------------------------------------------------------------- 1 | import type { IntegrationParameter, SecurityScheme } from '@superfaceai/ast'; 2 | 3 | import { buildSuperfaceDirPath } from '../../../common/file-structure'; 4 | import { ProfileId } from '../../../common/profile'; 5 | import type { ApplicationCodeWriter } from '../application-code'; 6 | import { ONESDK_TOKEN_COMMENT, ONESDK_TOKEN_ENV } from '../dotenv'; 7 | import { prepareParameters } from './parameters'; 8 | import { prepareSecurity } from './security'; 9 | 10 | export const jsApplicationCode: ApplicationCodeWriter = ({ 11 | profile, 12 | useCaseName, 13 | provider, 14 | input, 15 | parameters, 16 | security, 17 | }: { 18 | profile: { 19 | name: string; 20 | scope?: string; 21 | }; 22 | useCaseName: string; 23 | provider: string; 24 | // TODO: more language independent type for input? 25 | input: string; 26 | parameters?: IntegrationParameter[]; 27 | security?: SecurityScheme[]; 28 | }) => { 29 | const pathToSdk = '@superfaceai/one-sdk'; 30 | 31 | const profileId = ProfileId.fromScopeName(profile.scope, profile.name).id; 32 | 33 | const preparedParameters = prepareParameters(provider, parameters); 34 | const preparedSecurity = prepareSecurity(provider, security); 35 | 36 | const code = `import { config } from "dotenv"; 37 | // Load OneClient from SDK 38 | import { 39 | OneClient, 40 | PerformError, 41 | UnexpectedError, 42 | ValidationError, 43 | } from "${pathToSdk}"; 44 | 45 | // Load environment variables from .env file 46 | config(); 47 | 48 | const client = new OneClient({ 49 | // ${ONESDK_TOKEN_COMMENT} 50 | token: process.env.${ONESDK_TOKEN_ENV}, 51 | // Path to Comlinks within your project 52 | assetsPath: "${escapeAssetspath(buildSuperfaceDirPath())}", 53 | }); 54 | 55 | // Load Comlink profile and use case 56 | const profile = await client.getProfile("${profileId}"); 57 | const useCase = profile.getUseCase("${useCaseName}"); 58 | 59 | try { 60 | // Execute use case 61 | const result = await useCase.perform( 62 | // Use case input 63 | ${input}, 64 | { 65 | provider: "${provider}", 66 | parameters: ${preparedParameters.parametersString}, 67 | // Security values for provider 68 | security: ${preparedSecurity.securityString}, 69 | } 70 | ); 71 | 72 | console.log("RESULT:", JSON.stringify(result, null, 2)); 73 | } catch (e) { 74 | if (e instanceof PerformError) { 75 | console.log("ERROR RESULT:", e.errorResult); 76 | } else if (e instanceof ValidationError) { 77 | console.error("VALIDATION ERROR:", e.message); 78 | } else if (e instanceof UnexpectedError) { 79 | console.error("ERROR:", e); 80 | } else { 81 | throw e; 82 | } 83 | } 84 | `; 85 | 86 | return { 87 | code, 88 | requiredParameters: preparedParameters.required, 89 | requiredSecurity: preparedSecurity.required, 90 | }; 91 | }; 92 | 93 | function escapeAssetspath(path: string): string { 94 | // Escape backslashes for Windows 95 | return path.replace(/\\/g, '\\\\'); 96 | } 97 | -------------------------------------------------------------------------------- /src/logic/application-code/js/index.ts: -------------------------------------------------------------------------------- 1 | export { jsApplicationCode } from './application-code'; 2 | -------------------------------------------------------------------------------- /src/logic/application-code/js/parameters.test.ts: -------------------------------------------------------------------------------- 1 | import { prepareParameters } from './parameters'; 2 | 3 | describe('prepareParameters', () => { 4 | it('should return empty object if parameters are undefined', () => { 5 | expect(prepareParameters('provider', undefined)).toEqual({ 6 | parametersString: '{}', 7 | required: [], 8 | }); 9 | }); 10 | 11 | it('should return empty object if parameters are empty', () => { 12 | expect(prepareParameters('provider', [])).toEqual({ 13 | parametersString: '{}', 14 | required: [], 15 | }); 16 | }); 17 | 18 | it('should return correct parameters string', () => { 19 | expect( 20 | prepareParameters('provider', [{ name: 'test', default: 'value' }]) 21 | ).toEqual({ 22 | parametersString: '{ test: process.env.PROVIDER_TEST }', 23 | required: ['$PROVIDER_TEST'], 24 | }); 25 | }); 26 | 27 | it('should return correct parameters string with multiple parameters', () => { 28 | expect( 29 | prepareParameters('provider', [ 30 | { name: 'test' }, 31 | { name: 'test2', default: 'value' }, 32 | ]) 33 | ).toEqual({ 34 | parametersString: 35 | '{ test: process.env.PROVIDER_TEST, test2: process.env.PROVIDER_TEST2 }', 36 | required: ['$PROVIDER_TEST', '$PROVIDER_TEST2'], 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/logic/application-code/js/parameters.ts: -------------------------------------------------------------------------------- 1 | import type { IntegrationParameter } from '@superfaceai/ast'; 2 | import { prepareProviderParameters } from '@superfaceai/ast'; 3 | 4 | export function prepareParameters( 5 | providerName: string, 6 | parameters: IntegrationParameter[] | undefined 7 | ): { parametersString: string; required: string[] } { 8 | if (!parameters) { 9 | return { parametersString: '{}', required: [] }; 10 | } 11 | 12 | const required: string[] = []; 13 | 14 | const parametersMap = prepareProviderParameters(providerName, parameters); 15 | 16 | if (Object.keys(parametersMap).length === 0) { 17 | return { parametersString: '{}', required }; 18 | } 19 | Object.values(parametersMap).forEach(value => { 20 | required.push(value); 21 | }); 22 | 23 | const parametersString = 24 | '{ ' + 25 | Object.entries(parametersMap) 26 | .map( 27 | ([key, value]) => 28 | `${key}: process.env.${ 29 | value.startsWith('$') ? value.slice(1) : value 30 | }` 31 | ) 32 | .join(', ') + 33 | ' }'; 34 | 35 | return { parametersString, required }; 36 | } 37 | -------------------------------------------------------------------------------- /src/logic/application-code/js/security.test.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ApiKeySecurityScheme, 3 | BasicAuthSecurityScheme, 4 | BearerTokenSecurityScheme, 5 | DigestSecurityScheme, 6 | } from '@superfaceai/ast'; 7 | import { ApiKeyPlacement, HttpScheme, SecurityType } from '@superfaceai/ast'; 8 | 9 | import { prepareSecurity } from './security'; 10 | 11 | describe('prepareSecurity', () => { 12 | const headerApiKey: ApiKeySecurityScheme = { 13 | id: 'apikey-header', 14 | type: SecurityType.APIKEY, 15 | in: ApiKeyPlacement.HEADER, 16 | }; 17 | 18 | const queryApiKey: ApiKeySecurityScheme = { 19 | id: 'apikey-query', 20 | type: SecurityType.APIKEY, 21 | in: ApiKeyPlacement.QUERY, 22 | }; 23 | 24 | const bodyApiKey: ApiKeySecurityScheme = { 25 | id: 'apikey-body', 26 | type: SecurityType.APIKEY, 27 | in: ApiKeyPlacement.BODY, 28 | }; 29 | 30 | const bodyApiKeyWithName: ApiKeySecurityScheme = { 31 | id: 'apikey-body', 32 | type: SecurityType.APIKEY, 33 | in: ApiKeyPlacement.BODY, 34 | name: 'apiKey', 35 | }; 36 | 37 | const bearerHttp: BearerTokenSecurityScheme = { 38 | id: 'bearer', 39 | scheme: HttpScheme.BEARER, 40 | type: SecurityType.HTTP, 41 | }; 42 | 43 | const digestHttp: DigestSecurityScheme = { 44 | id: 'digest', 45 | scheme: HttpScheme.DIGEST, 46 | type: SecurityType.HTTP, 47 | }; 48 | 49 | const basicHttp: BasicAuthSecurityScheme = { 50 | id: 'basic', 51 | scheme: HttpScheme.BASIC, 52 | type: SecurityType.HTTP, 53 | }; 54 | 55 | it('returns empty object if security schemes are undefined', () => { 56 | expect(prepareSecurity('test', undefined)).toEqual({ 57 | required: [], 58 | securityString: '{}', 59 | }); 60 | }); 61 | 62 | it('returns empty object if no security schemes are provided', () => { 63 | expect(prepareSecurity('test', [])).toEqual({ 64 | required: [], 65 | securityString: '{}', 66 | }); 67 | }); 68 | 69 | it('correctly prepares security string for api key security', () => { 70 | expect( 71 | prepareSecurity('test', [headerApiKey, queryApiKey, bodyApiKey]) 72 | ).toEqual({ 73 | required: ['$TEST_API_KEY', '$TEST_API_KEY', '$TEST_API_KEY'], 74 | securityString: 75 | "{ 'apikey-header': { apikey: process.env.TEST_API_KEY }, 'apikey-query': { apikey: process.env.TEST_API_KEY }, 'apikey-body': { apikey: process.env.TEST_API_KEY } }", 76 | }); 77 | }); 78 | 79 | it('correctly prepares security string for api key security with name', () => { 80 | expect(prepareSecurity('test', [bodyApiKeyWithName])).toEqual({ 81 | required: ['$TEST_API_KEY'], 82 | securityString: "{ 'apikey-body': { apikey: process.env.TEST_API_KEY } }", 83 | }); 84 | }); 85 | 86 | it('correctly prepares security string for bearer security', () => { 87 | expect(prepareSecurity('test', [bearerHttp])).toEqual({ 88 | required: ['$TEST_TOKEN'], 89 | securityString: '{ bearer: { token: process.env.TEST_TOKEN } }', 90 | }); 91 | }); 92 | 93 | it('correctly prepares security string for basic security', () => { 94 | expect(prepareSecurity('test', [basicHttp])).toEqual({ 95 | required: ['$TEST_USERNAME', '$TEST_PASSWORD'], 96 | securityString: 97 | '{ basic: { username: process.env.TEST_USERNAME, password: process.env.TEST_PASSWORD } }', 98 | }); 99 | }); 100 | 101 | it('correctly prepares security string for digest security', () => { 102 | expect(prepareSecurity('test', [digestHttp])).toEqual({ 103 | required: ['$TEST_USERNAME', '$TEST_PASSWORD'], 104 | securityString: 105 | '{ digest: { username: process.env.TEST_USERNAME, password: process.env.TEST_PASSWORD } }', 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /src/logic/application-code/js/security.ts: -------------------------------------------------------------------------------- 1 | import type { SecurityScheme } from '@superfaceai/ast'; 2 | import { prepareSecurityValues } from '@superfaceai/ast'; 3 | 4 | export function prepareSecurity( 5 | providerName: string, 6 | security: SecurityScheme[] | undefined 7 | ): { 8 | securityString: string; 9 | required: string[]; 10 | } { 11 | if (!security || security.length === 0) { 12 | return { securityString: '{}', required: [] }; 13 | } 14 | const securityValues = prepareSecurityValues(providerName, security); 15 | 16 | const required: string[] = []; 17 | const result: string[] = []; 18 | 19 | // TODO: selecting single security scheme is not supported yet 20 | for (const securityValue of securityValues) { 21 | const { id, ...securityValueWithoutId } = securityValue; 22 | 23 | let escapedId = id; 24 | // Escape id 25 | if (id.includes('-')) { 26 | escapedId = `'${id}'`; 27 | } 28 | 29 | const valueString: string[] = []; 30 | Object.entries(securityValueWithoutId).forEach( 31 | ([key, value]: [string, string]) => { 32 | required.push(value); 33 | 34 | valueString.push( 35 | `${key}: process.env.${ 36 | value.startsWith('$') ? value.slice(1) : value 37 | }` 38 | ); 39 | } 40 | ); 41 | 42 | result.push(`${escapedId}: { ${valueString.join(', ')} }`); 43 | } 44 | 45 | return { securityString: '{ ' + result.join(', ') + ' }', required }; 46 | } 47 | -------------------------------------------------------------------------------- /src/logic/application-code/python/application-code.test.ts: -------------------------------------------------------------------------------- 1 | import { MockLogger } from '../../../common'; 2 | import { buildSuperfaceDirPath } from '../../../common/file-structure'; 3 | import { pythonApplicationCode } from './application-code'; 4 | 5 | describe('pythonApplicationCode', () => { 6 | it('should return correct application code', async () => { 7 | const scope = 'test-scope'; 8 | const name = 'test-name'; 9 | const provider = 'test-provider'; 10 | const useCaseName = 'test-use-case-name'; 11 | 12 | const logger = new MockLogger(); 13 | 14 | const result = pythonApplicationCode( 15 | { 16 | profile: { 17 | scope, 18 | name, 19 | }, 20 | useCaseName, 21 | provider, 22 | input: '{}', 23 | parameters: [], 24 | security: [], 25 | }, 26 | { logger } 27 | ); 28 | 29 | expect(result).toEqual({ 30 | code: `import os 31 | import sys 32 | from dotenv import load_dotenv 33 | from one_sdk import OneClient, PerformError, UnexpectedError, ValidationError 34 | 35 | load_dotenv() 36 | 37 | client = OneClient( 38 | # The token for monitoring your Comlinks at https://superface.ai 39 | token = os.getenv("SUPERFACE_ONESDK_TOKEN"), 40 | # Path to Comlinks within your project 41 | assets_path = "${buildSuperfaceDirPath()}" 42 | ) 43 | 44 | # Load Comlink profile and use case 45 | profile = client.get_profile("${scope}/${name}") 46 | use_case = profile.get_usecase("${useCaseName}") 47 | 48 | try: 49 | result = use_case.perform( 50 | {}, 51 | provider = "${provider}", 52 | parameters = {}, 53 | security = {} 54 | ) 55 | print(f"RESULT: {result}") 56 | except PerformError as e: 57 | print(f"ERROR RESULT: {e.error_result}") 58 | except ValidationError as e: 59 | print(f"INVALID INPUT: {e.message}", file = sys.stderr) 60 | except UnexpectedError as e: 61 | print(f"ERROR: {e}", file=sys.stderr) 62 | finally: 63 | client.send_metrics_to_superface()`, 64 | requiredParameters: [], 65 | requiredSecurity: [], 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/logic/application-code/python/application-code.ts: -------------------------------------------------------------------------------- 1 | import type { IntegrationParameter, SecurityScheme } from '@superfaceai/ast'; 2 | 3 | import { buildSuperfaceDirPath } from '../../../common/file-structure'; 4 | import { ProfileId } from '../../../common/profile'; 5 | import type { ApplicationCodeWriter } from '../application-code'; 6 | import { ONESDK_TOKEN_COMMENT, ONESDK_TOKEN_ENV } from '../dotenv'; 7 | import { prepareParameters } from './parameters'; 8 | import { prepareSecurity } from './security'; 9 | 10 | export const pythonApplicationCode: ApplicationCodeWriter = ({ 11 | profile, 12 | useCaseName, 13 | provider, 14 | input, 15 | parameters, 16 | security, 17 | }: { 18 | profile: { 19 | name: string; 20 | scope?: string; 21 | }; 22 | useCaseName: string; 23 | provider: string; 24 | input: string; 25 | parameters?: IntegrationParameter[]; 26 | security?: SecurityScheme[]; 27 | }) => { 28 | const profileId = ProfileId.fromScopeName(profile.scope, profile.name).id; 29 | 30 | const preparedParameters = prepareParameters(provider, parameters); 31 | const preparedSecurity = prepareSecurity(provider, security); 32 | 33 | const code = `import os 34 | import sys 35 | from dotenv import load_dotenv 36 | from one_sdk import OneClient, PerformError, UnexpectedError, ValidationError 37 | 38 | load_dotenv() 39 | 40 | client = OneClient( 41 | # ${ONESDK_TOKEN_COMMENT} 42 | token = os.getenv("${ONESDK_TOKEN_ENV}"), 43 | # Path to Comlinks within your project 44 | assets_path = "${buildSuperfaceDirPath()}" 45 | ) 46 | 47 | # Load Comlink profile and use case 48 | profile = client.get_profile("${profileId}") 49 | use_case = profile.get_usecase("${useCaseName}") 50 | 51 | try: 52 | result = use_case.perform( 53 | ${input}, 54 | provider = "${provider}", 55 | parameters = ${preparedParameters.parametersString}, 56 | security = ${preparedSecurity.securityString} 57 | ) 58 | print(f"RESULT: {result}") 59 | except PerformError as e: 60 | print(f"ERROR RESULT: {e.error_result}") 61 | except ValidationError as e: 62 | print(f"INVALID INPUT: {e.message}", file = sys.stderr) 63 | except UnexpectedError as e: 64 | print(f"ERROR: {e}", file=sys.stderr) 65 | finally: 66 | client.send_metrics_to_superface()`; 67 | 68 | return { 69 | code, 70 | requiredParameters: preparedParameters.required, 71 | requiredSecurity: preparedSecurity.required, 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /src/logic/application-code/python/index.ts: -------------------------------------------------------------------------------- 1 | export { pythonApplicationCode } from './application-code'; 2 | -------------------------------------------------------------------------------- /src/logic/application-code/python/parameters.test.ts: -------------------------------------------------------------------------------- 1 | import { prepareParameters } from './parameters'; 2 | 3 | describe('prepareParameters', () => { 4 | it('should return empty object if parameters are undefined', () => { 5 | expect(prepareParameters('provider', undefined)).toEqual({ 6 | parametersString: '{}', 7 | required: [], 8 | }); 9 | }); 10 | 11 | it('should return empty object if parameters are empty', () => { 12 | expect(prepareParameters('provider', [])).toEqual({ 13 | parametersString: '{}', 14 | required: [], 15 | }); 16 | }); 17 | 18 | it('should return correct parameters string', () => { 19 | expect( 20 | prepareParameters('provider', [{ name: 'test', default: 'value' }]) 21 | ).toEqual({ 22 | parametersString: `{ "test": os.getenv('PROVIDER_TEST') }`, 23 | required: ['$PROVIDER_TEST'], 24 | }); 25 | }); 26 | 27 | it('should return correct parameters string with multiple parameters', () => { 28 | expect( 29 | prepareParameters('provider', [ 30 | { name: 'test' }, 31 | { name: 'test2', default: 'value' }, 32 | ]) 33 | ).toEqual({ 34 | parametersString: `{ "test": os.getenv('PROVIDER_TEST'), "test2": os.getenv('PROVIDER_TEST2') }`, 35 | required: ['$PROVIDER_TEST', '$PROVIDER_TEST2'], 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/logic/application-code/python/parameters.ts: -------------------------------------------------------------------------------- 1 | import type { IntegrationParameter } from '@superfaceai/ast'; 2 | import { prepareProviderParameters } from '@superfaceai/ast'; 3 | 4 | export function prepareParameters( 5 | providerName: string, 6 | parameters: IntegrationParameter[] | undefined 7 | ): { 8 | parametersString: string; 9 | required: string[]; 10 | } { 11 | if (!parameters || parameters.length === 0) { 12 | return { parametersString: '{}', required: [] }; 13 | } 14 | 15 | const parametersMap = prepareProviderParameters(providerName, parameters); 16 | const required: string[] = []; 17 | 18 | if (Object.keys(parametersMap).length === 0) { 19 | return { parametersString: '{}', required: [] }; 20 | } 21 | Object.values(parametersMap).forEach(value => { 22 | required.push(value); 23 | }); 24 | 25 | return { 26 | parametersString: 27 | '{ ' + 28 | Object.entries(parametersMap) 29 | .map( 30 | ([key, value]) => 31 | `"${key}": os.getenv('${ 32 | value.startsWith('$') ? value.slice(1) : value 33 | }')` 34 | ) 35 | .join(', ') + 36 | ' }', 37 | required, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/logic/application-code/python/security.test.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ApiKeySecurityScheme, 3 | BasicAuthSecurityScheme, 4 | BearerTokenSecurityScheme, 5 | DigestSecurityScheme, 6 | } from '@superfaceai/ast'; 7 | import { ApiKeyPlacement, HttpScheme, SecurityType } from '@superfaceai/ast'; 8 | 9 | import { prepareSecurity } from './security'; 10 | 11 | describe('prepareSecurity', () => { 12 | const headerApiKey: ApiKeySecurityScheme = { 13 | id: 'apikey-header', 14 | type: SecurityType.APIKEY, 15 | in: ApiKeyPlacement.HEADER, 16 | }; 17 | 18 | const queryApiKey: ApiKeySecurityScheme = { 19 | id: 'apikey-query', 20 | type: SecurityType.APIKEY, 21 | in: ApiKeyPlacement.QUERY, 22 | }; 23 | 24 | const bodyApiKey: ApiKeySecurityScheme = { 25 | id: 'apikey-body', 26 | type: SecurityType.APIKEY, 27 | in: ApiKeyPlacement.BODY, 28 | }; 29 | 30 | const bodyApiKeyWithName: ApiKeySecurityScheme = { 31 | id: 'apikey-body', 32 | type: SecurityType.APIKEY, 33 | in: ApiKeyPlacement.BODY, 34 | name: 'apiKey', 35 | }; 36 | 37 | const bearerHttp: BearerTokenSecurityScheme = { 38 | id: 'bearer', 39 | scheme: HttpScheme.BEARER, 40 | type: SecurityType.HTTP, 41 | }; 42 | 43 | const digestHttp: DigestSecurityScheme = { 44 | id: 'digest', 45 | scheme: HttpScheme.DIGEST, 46 | type: SecurityType.HTTP, 47 | }; 48 | 49 | const basicHttp: BasicAuthSecurityScheme = { 50 | id: 'basic', 51 | scheme: HttpScheme.BASIC, 52 | type: SecurityType.HTTP, 53 | }; 54 | 55 | it('returns empty object if security schemes are undefined', () => { 56 | expect(prepareSecurity('test', undefined)).toEqual({ 57 | required: [], 58 | securityString: '{}', 59 | }); 60 | }); 61 | 62 | it('returns empty object if no security schemes are provided', () => { 63 | expect(prepareSecurity('test', [])).toEqual({ 64 | required: [], 65 | securityString: '{}', 66 | }); 67 | }); 68 | 69 | it('correctly prepares security string for api key security', () => { 70 | expect( 71 | prepareSecurity('test', [headerApiKey, queryApiKey, bodyApiKey]) 72 | ).toEqual({ 73 | required: ['$TEST_API_KEY', '$TEST_API_KEY', '$TEST_API_KEY'], 74 | securityString: `{ "apikey-header": { "apikey": os.getenv('TEST_API_KEY') }, "apikey-query": { "apikey": os.getenv('TEST_API_KEY') }, "apikey-body": { "apikey": os.getenv('TEST_API_KEY') } }`, 75 | }); 76 | }); 77 | 78 | it('correctly prepares security string for api key security with name', () => { 79 | expect(prepareSecurity('test', [bodyApiKeyWithName])).toEqual({ 80 | required: ['$TEST_API_KEY'], 81 | securityString: `{ "apikey-body": { "apikey": os.getenv('TEST_API_KEY') } }`, 82 | }); 83 | }); 84 | 85 | it('correctly prepares security string for bearer security', () => { 86 | expect(prepareSecurity('test', [bearerHttp])).toEqual({ 87 | required: ['$TEST_TOKEN'], 88 | securityString: `{ "bearer": { "token": os.getenv('TEST_TOKEN') } }`, 89 | }); 90 | }); 91 | 92 | it('correctly prepares security string for basic security', () => { 93 | expect(prepareSecurity('test', [basicHttp])).toEqual({ 94 | required: ['$TEST_USERNAME', '$TEST_PASSWORD'], 95 | securityString: `{ "basic": { "username": os.getenv('TEST_USERNAME'), "password": os.getenv('TEST_PASSWORD') } }`, 96 | }); 97 | }); 98 | 99 | it('correctly prepares security string for digest security', () => { 100 | expect(prepareSecurity('test', [digestHttp])).toEqual({ 101 | required: ['$TEST_USERNAME', '$TEST_PASSWORD'], 102 | securityString: `{ "digest": { "username": os.getenv('TEST_USERNAME'), "password": os.getenv('TEST_PASSWORD') } }`, 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /src/logic/application-code/python/security.ts: -------------------------------------------------------------------------------- 1 | import type { SecurityScheme } from '@superfaceai/ast'; 2 | import { prepareSecurityValues } from '@superfaceai/ast'; 3 | 4 | export function prepareSecurity( 5 | providerName: string, 6 | security: SecurityScheme[] | undefined 7 | ): { 8 | securityString: string; 9 | required: string[]; 10 | } { 11 | if (!security || security.length === 0) { 12 | return { securityString: '{}', required: [] }; 13 | } 14 | 15 | const securityValues = prepareSecurityValues(providerName, security); 16 | 17 | const result: string[] = []; 18 | const required: string[] = []; 19 | 20 | // TODO: selecting single security scheme is not supported yet 21 | for (const securityValue of securityValues) { 22 | const { id, ...securityValueWithoutId } = securityValue; 23 | 24 | const valueString: string[] = []; 25 | Object.entries(securityValueWithoutId).forEach( 26 | ([key, value]: [string, string]) => { 27 | required.push(value); 28 | 29 | valueString.push( 30 | `"${key}": os.getenv('${ 31 | value.startsWith('$') ? value.slice(1) : value 32 | }')` 33 | ); 34 | } 35 | ); 36 | 37 | result.push(`"${id}": { ${valueString.join(', ')} }`); 38 | } 39 | 40 | return { securityString: '{ ' + result.join(', ') + ' }', required }; 41 | } 42 | -------------------------------------------------------------------------------- /src/logic/execution/execute.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | 3 | import type { ILogger } from '../../common'; 4 | import type { UserError } from '../../common/error'; 5 | import type { SupportedLanguages } from '../application-code'; 6 | import { executeRunner } from './runner'; 7 | 8 | export async function execute( 9 | file: string, 10 | language: SupportedLanguages, 11 | { userError, logger }: { userError: UserError; logger: ILogger } 12 | ): Promise { 13 | const command = prepareCommand(file, language); 14 | 15 | logger.info( 16 | 'executingCommand', 17 | `${command.command} ${command.args.join(' ')}` 18 | ); 19 | 20 | const execution = spawn(command.command, command.args, { 21 | stdio: 'inherit', 22 | env: { 23 | ...process.env, 24 | }, 25 | }); 26 | 27 | await executeRunner(execution, command.command, { userError, logger }); 28 | } 29 | 30 | function prepareCommand( 31 | file: string, 32 | language: SupportedLanguages 33 | ): { command: string; args: string[] } { 34 | const COMMAND_MAP: { 35 | [key in SupportedLanguages]: { command: string; args: string[] }; 36 | } = { 37 | js: { 38 | command: 'node', 39 | args: ['--no-warnings', '--experimental-wasi-unstable-preview1', file], 40 | }, 41 | python: { 42 | command: 'python3', 43 | args: [file], 44 | }, 45 | }; 46 | 47 | return COMMAND_MAP[language]; 48 | } 49 | -------------------------------------------------------------------------------- /src/logic/execution/index.ts: -------------------------------------------------------------------------------- 1 | export { execute } from './execute'; 2 | -------------------------------------------------------------------------------- /src/logic/execution/runner.test.ts: -------------------------------------------------------------------------------- 1 | import type { ChildProcess } from 'node:child_process'; 2 | import { EventEmitter, Readable, Writable } from 'node:stream'; 3 | 4 | import { MockLogger } from '../../common'; 5 | import { createUserError } from '../../common/error'; 6 | import { executeRunner } from './runner'; 7 | 8 | // TODO: finish tests 9 | describe('executeRunner', () => { 10 | it.skip('executes integration', async () => { 11 | const mockChildProcess = new EventEmitter() as ChildProcess; 12 | 13 | mockChildProcess.stdin = new Writable({ 14 | write(_data, _enc, callback) { 15 | callback(); 16 | }, 17 | final(_callback) { 18 | // mimic the child process exiting 19 | mockChildProcess.emit('close'); 20 | }, 21 | }); 22 | 23 | mockChildProcess.stdout = new Readable({ 24 | read() { 25 | this.push(' 17 18 19'); 26 | 27 | this.push(null); 28 | // collect and verify the data that wc outputs to STDOUT 29 | }, 30 | }); 31 | 32 | mockChildProcess.stderr = new Readable({ 33 | read() { 34 | // collect and verify the data that wc outputs to STDERR 35 | }, 36 | }); 37 | 38 | const mockLogger = new MockLogger(); 39 | const userError = createUserError(false, false); 40 | 41 | await executeRunner(mockChildProcess, 'node', { 42 | logger: mockLogger, 43 | userError, 44 | }); 45 | 46 | expect(mockLogger.info).toHaveBeenCalledWith( 47 | 'childProcessExited', 48 | expect.anything() 49 | ); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/logic/execution/runner.ts: -------------------------------------------------------------------------------- 1 | import type { ChildProcess } from 'child_process'; 2 | 3 | import type { ILogger } from '../../common'; 4 | import type { UserError } from '../../common/error'; 5 | 6 | export function executeRunner( 7 | childProcess: ChildProcess, 8 | command: string, 9 | { userError, logger }: { userError: UserError; logger: ILogger } 10 | ) { 11 | return new Promise((resolve, reject) => { 12 | childProcess.stdout?.on('data', output => { 13 | logger.info('childProcessOutput', String(output)); 14 | }); 15 | 16 | childProcess.stderr?.on('data', output => { 17 | logger.error('childProcessOutput', String(output)); 18 | }); 19 | 20 | childProcess.on('close', code => { 21 | if (code !== 0) { 22 | logger.error('childProcessExited', code); 23 | 24 | reject(userError(`Failed to execute ${command}`, 1)); 25 | } 26 | 27 | logger.success('childProcessExited', code); 28 | 29 | resolve(undefined); 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /src/logic/index.ts: -------------------------------------------------------------------------------- 1 | export * from './application-code'; 2 | -------------------------------------------------------------------------------- /src/logic/login.ts: -------------------------------------------------------------------------------- 1 | import { VerificationStatus } from '@superfaceai/service-client'; 2 | import inquirer from 'inquirer'; 3 | import * as open from 'open'; 4 | 5 | import type { UserError } from '../common/error'; 6 | import { SuperfaceClient } from '../common/http'; 7 | import type { ILogger } from '../common/log'; 8 | 9 | export async function login( 10 | { force }: { force?: boolean }, 11 | { logger, userError }: { logger: ILogger; userError: UserError } 12 | ): Promise { 13 | const client = SuperfaceClient.getClient(); 14 | // get verification url, browser url and expiresAt 15 | const initResponse = await client.cliLogin(); 16 | 17 | if (!initResponse.success) { 18 | throw userError( 19 | `Attempt to login ended with: ${initResponse.title}${ 20 | initResponse.detail !== undefined ? `: ${initResponse.detail}` : '' 21 | }`, 22 | 1 23 | ); 24 | } 25 | // open browser on browser url /auth/cli/browser 26 | 27 | let openBrowser = true; 28 | if (force !== true) { 29 | const prompt: { open: boolean } = await inquirer.prompt({ 30 | name: 'open', 31 | message: 'Do you want to open browser with Superface login page?', 32 | type: 'confirm', 33 | default: true, 34 | }); 35 | openBrowser = prompt.open; 36 | } 37 | const showUrl = () => { 38 | logger.warn('openUrl', initResponse.browserUrl); 39 | }; 40 | if (openBrowser && force !== true) { 41 | const childProcess = await open.default(initResponse.browserUrl, { 42 | wait: false, 43 | }); 44 | childProcess.on('error', err => { 45 | logger.error('errorMessage', err.message); 46 | showUrl(); 47 | }); 48 | childProcess.on('close', code => { 49 | if (code !== 0) { 50 | showUrl(); 51 | } 52 | }); 53 | } else { 54 | showUrl(); 55 | } 56 | 57 | // poll verification url 58 | const verifyResponse = await client.verifyCliLogin(initResponse.verifyUrl, { 59 | pollingTimeoutSeconds: 3600, 60 | }); 61 | if (verifyResponse.verificationStatus !== VerificationStatus.CONFIRMED) { 62 | throw userError( 63 | `Unable to get auth token, request ended with status: ${verifyResponse.verificationStatus}`, 64 | 1 65 | ); 66 | } 67 | if (!verifyResponse.authToken) { 68 | throw userError( 69 | `Request ended with status: ${verifyResponse.verificationStatus} but does not contain auth token`, 70 | 1 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/logic/map.ts: -------------------------------------------------------------------------------- 1 | import type { ProviderJson } from '@superfaceai/ast'; 2 | import type { ServiceClient } from '@superfaceai/service-client'; 3 | 4 | import type { UserError } from '../common/error'; 5 | import { SuperfaceClient } from '../common/http'; 6 | import { pollUrl } from '../common/polling'; 7 | import type { UX } from '../common/ux'; 8 | 9 | type MapPreparationResponse = { 10 | source: string; 11 | }; 12 | 13 | function assertMapResponse( 14 | input: unknown, 15 | { userError }: { userError: UserError } 16 | ): asserts input is MapPreparationResponse { 17 | if (typeof input === 'object' && input !== null && 'source' in input) { 18 | const tmp = input as { source: string }; 19 | 20 | if (typeof tmp.source === 'string') { 21 | return; 22 | } 23 | } 24 | 25 | throw userError(`Unexpected response received`, 1); 26 | } 27 | 28 | export async function mapProviderToProfile( 29 | { 30 | providerJson, 31 | profile, 32 | options, 33 | }: { 34 | providerJson: ProviderJson; 35 | profile: string; 36 | options: { 37 | quiet?: boolean; 38 | timeout: number; 39 | }; 40 | }, 41 | { ux, userError }: { ux: UX; userError: UserError } 42 | ): Promise { 43 | const client = SuperfaceClient.getClient(); 44 | 45 | const jobUrl = await startMapPreparation( 46 | // TODO: add old map if exists 47 | { providerJson, profile, map: undefined }, 48 | { client, userError } 49 | ); 50 | 51 | const resultUrl = await pollUrl( 52 | { 53 | url: jobUrl, 54 | options: { 55 | quiet: options.quiet, 56 | pollingTimeoutSeconds: options.timeout, 57 | }, 58 | }, 59 | { client, ux, userError } 60 | ); 61 | 62 | return (await finishMapPreparation(resultUrl, { client, userError })).source; 63 | } 64 | 65 | async function startMapPreparation( 66 | { 67 | profile, 68 | providerJson, 69 | map, 70 | }: { 71 | profile: string; 72 | providerJson: ProviderJson; 73 | map?: string; 74 | }, 75 | { client, userError }: { client: ServiceClient; userError: UserError } 76 | ): Promise { 77 | const jobUrlResponse = await client.fetch(`/authoring/maps`, { 78 | method: 'POST', 79 | headers: { 80 | 'Content-Type': 'application/json', 81 | }, 82 | body: JSON.stringify({ 83 | map, 84 | provider: providerJson, 85 | profile, 86 | }), 87 | }); 88 | 89 | if (jobUrlResponse.status !== 202) { 90 | if (jobUrlResponse.status === 401) { 91 | throw userError( 92 | `This command is available to authenticated users only. Please log in using \`superface login\``, 93 | 1 94 | ); 95 | } 96 | throw userError( 97 | `Unexpected status code ${jobUrlResponse.status} received`, 98 | 1 99 | ); 100 | } 101 | 102 | const responseBody = (await jobUrlResponse.json()) as Record; 103 | 104 | if ( 105 | typeof responseBody === 'object' && 106 | responseBody !== null && 107 | 'href' in responseBody && 108 | typeof responseBody.href === 'string' 109 | ) { 110 | return responseBody.href; 111 | } else { 112 | throw Error( 113 | `Unexpected response body ${JSON.stringify(responseBody)} received` 114 | ); 115 | } 116 | } 117 | 118 | async function finishMapPreparation( 119 | resultUrl: string, 120 | { client, userError }: { client: ServiceClient; userError: UserError } 121 | ): Promise { 122 | const resultResponse = await client.fetch(resultUrl, { 123 | method: 'GET', 124 | headers: { 125 | accept: 'application/json', 126 | }, 127 | // Url from server is complete, so we don't need to add baseUrl 128 | baseUrl: '', 129 | }); 130 | 131 | if (resultResponse.status !== 200) { 132 | throw userError( 133 | `Unexpected status code ${resultResponse.status} received`, 134 | 1 135 | ); 136 | } 137 | 138 | const body = (await resultResponse.json()) as unknown; 139 | 140 | assertMapResponse(body, { userError }); 141 | 142 | return body; 143 | } 144 | -------------------------------------------------------------------------------- /src/logic/new.ts: -------------------------------------------------------------------------------- 1 | import type { ProviderJson } from '@superfaceai/ast'; 2 | import { parseDocumentId, parseProfile, Source } from '@superfaceai/parser'; 3 | import type { ServiceClient } from '@superfaceai/service-client'; 4 | 5 | import type { UserError } from '../common/error'; 6 | import { SuperfaceClient } from '../common/http'; 7 | import { pollUrl } from '../common/polling'; 8 | import type { UX } from '../common/ux'; 9 | 10 | type ProfilePreparationResponse = { 11 | // Id of the profile with . separated scope and name 12 | id: string; 13 | // TODO: get AST from server to avoid parsing (possible problems with AST/Parser versioning)? 14 | profile: { 15 | source: string; 16 | }; 17 | }; 18 | 19 | function assertProfileResponse( 20 | input: unknown, 21 | { userError }: { userError: UserError } 22 | ): asserts input is ProfilePreparationResponse { 23 | if ( 24 | typeof input === 'object' && 25 | input !== null && 26 | 'id' in input && 27 | 'profile' in input 28 | ) { 29 | const tmp = input as { id: string; profile: { source?: string } }; 30 | 31 | if (typeof tmp.profile?.source !== 'string') { 32 | throw userError( 33 | `Unexpected response received - missing profile source: ${JSON.stringify( 34 | tmp, 35 | null, 36 | 2 37 | )}`, 38 | 1 39 | ); 40 | } 41 | 42 | try { 43 | parseProfile(new Source(tmp.profile.source)); 44 | } catch (e) { 45 | throw userError( 46 | `Unexpected response received - unable to parse profile source: ${ 47 | tmp.profile.source 48 | }, error: ${JSON.stringify(e, null, 2)}`, 49 | 1 50 | ); 51 | } 52 | 53 | // TODO: validate id format? 54 | if (typeof tmp.id === 'string') { 55 | return; 56 | } 57 | } 58 | 59 | throw Error(`Unexpected response received`); 60 | } 61 | 62 | export async function newProfile( 63 | { 64 | providerJson, 65 | prompt, 66 | profileName, 67 | profileScope, 68 | options, 69 | }: { 70 | providerJson: ProviderJson; 71 | prompt: string; 72 | profileName?: string; 73 | profileScope?: string; 74 | options: { quiet?: boolean; timeout: number }; 75 | }, 76 | { userError, ux }: { userError: UserError; ux: UX } 77 | ): Promise<{ source: string; scope?: string; name: string }> { 78 | const client = SuperfaceClient.getClient(); 79 | 80 | const jobUrl = await startProfilePreparation( 81 | { providerJson, prompt, profileName, profileScope }, 82 | { client, userError } 83 | ); 84 | 85 | const resultUrl = await pollUrl( 86 | { 87 | url: jobUrl, 88 | options: { 89 | quiet: options.quiet, 90 | pollingTimeoutSeconds: options.timeout, 91 | }, 92 | }, 93 | { client, userError, ux } 94 | ); 95 | 96 | const profileResponse = await finishProfilePreparation(resultUrl, { 97 | client, 98 | userError, 99 | }); 100 | 101 | // Supports both . and / in profile id 102 | const parsedProfileId = parseDocumentId( 103 | profileResponse.id.replace(/\./, '/') 104 | ); 105 | if (parsedProfileId.kind == 'error') { 106 | throw userError(`Invalid profile id: ${parsedProfileId.message}`, 1); 107 | } 108 | 109 | return { 110 | source: profileResponse.profile.source, 111 | scope: parsedProfileId.value.scope, 112 | name: parsedProfileId.value.middle[0], 113 | }; 114 | } 115 | 116 | async function startProfilePreparation( 117 | { 118 | providerJson, 119 | prompt, 120 | profileName, 121 | profileScope, 122 | }: { 123 | providerJson: ProviderJson; 124 | prompt: string; 125 | profileName?: string; 126 | profileScope?: string; 127 | }, 128 | { client, userError }: { client: ServiceClient; userError: UserError } 129 | ): Promise { 130 | const jobUrlResponse = await client.fetch(`/authoring/profiles`, { 131 | method: 'POST', 132 | headers: { 133 | 'Content-Type': 'application/json', 134 | }, 135 | body: JSON.stringify({ 136 | prompt, 137 | provider: providerJson, 138 | profile_name: profileName, 139 | profile_scope: profileScope, 140 | }), 141 | }); 142 | 143 | if (jobUrlResponse.status !== 202) { 144 | if (jobUrlResponse.status === 401) { 145 | throw userError( 146 | `You are not authorized. Please login using 'superface login'.`, 147 | 1 148 | ); 149 | } 150 | throw userError( 151 | `Unexpected status code ${jobUrlResponse.status} received`, 152 | 1 153 | ); 154 | } 155 | 156 | const responseBody = (await jobUrlResponse.json()) as Record; 157 | 158 | if ( 159 | typeof responseBody === 'object' && 160 | responseBody !== null && 161 | 'href' in responseBody && 162 | typeof responseBody.href === 'string' 163 | ) { 164 | return responseBody.href; 165 | } else { 166 | throw userError( 167 | `Unexpected response body ${JSON.stringify(responseBody)} received`, 168 | 1 169 | ); 170 | } 171 | } 172 | 173 | async function finishProfilePreparation( 174 | resultUrl: string, 175 | { client, userError }: { client: ServiceClient; userError: UserError } 176 | ): Promise { 177 | const resultResponse = await client.fetch(resultUrl, { 178 | method: 'GET', 179 | headers: { 180 | accept: 'application/json', 181 | }, 182 | // Url from server is complete, so we don't need to add baseUrl 183 | baseUrl: '', 184 | }); 185 | 186 | if (resultResponse.status !== 200) { 187 | throw userError( 188 | `Unexpected status code ${resultResponse.status} received`, 189 | 1 190 | ); 191 | } 192 | 193 | const body = (await resultResponse.json()) as unknown; 194 | 195 | assertProfileResponse(body, { userError }); 196 | 197 | return body; 198 | } 199 | -------------------------------------------------------------------------------- /src/logic/project/index.ts: -------------------------------------------------------------------------------- 1 | export * from './prepare-project'; 2 | -------------------------------------------------------------------------------- /src/logic/project/js/index.ts: -------------------------------------------------------------------------------- 1 | export { prepareJsProject } from './js'; 2 | -------------------------------------------------------------------------------- /src/logic/project/js/js.test.ts: -------------------------------------------------------------------------------- 1 | import { buildProjectDefinitionFilePath } from '../../../common/file-structure'; 2 | import { exists } from '../../../common/io'; 3 | import { OutputStream } from '../../../common/output-stream'; 4 | import { SupportedLanguages } from '../../application-code'; 5 | import { prepareJsProject } from './js'; 6 | 7 | jest.mock('../../../common/output-stream'); 8 | jest.mock('../../../common/io'); 9 | 10 | describe('prepareJsProject', () => { 11 | const originalWriteOnce = OutputStream.writeOnce; 12 | 13 | let mockWriteOnce: jest.Mock; 14 | 15 | beforeAll(() => { 16 | // Mock static side of OutputStream 17 | mockWriteOnce = jest.fn(); 18 | OutputStream.writeOnce = mockWriteOnce; 19 | }); 20 | 21 | afterAll(() => { 22 | // Restore static side of OutputStream 23 | OutputStream.writeOnce = originalWriteOnce; 24 | }); 25 | 26 | beforeEach(() => { 27 | jest.clearAllMocks(); 28 | }); 29 | 30 | it('creates package.json if it does not exist', async () => { 31 | jest.mocked(exists).mockResolvedValueOnce(false); 32 | 33 | await expect( 34 | prepareJsProject('3.0.0-alpha.12', '^16.0.3') 35 | ).resolves.toEqual({ 36 | saved: true, 37 | dependencyInstallCommand: expect.any(String), 38 | languageDependency: expect.any(String), 39 | path: expect.stringContaining('superface/package.json'), 40 | }); 41 | 42 | expect(mockWriteOnce).toHaveBeenCalledWith( 43 | buildProjectDefinitionFilePath(SupportedLanguages.JS), 44 | expect.any(String) 45 | ); 46 | }); 47 | 48 | it('does not create package.json if it exists', async () => { 49 | jest.mocked(exists).mockResolvedValueOnce(true); 50 | 51 | await expect( 52 | prepareJsProject('3.0.0-alpha.12', '^16.0.3') 53 | ).resolves.toEqual({ 54 | saved: false, 55 | dependencyInstallCommand: expect.any(String), 56 | languageDependency: expect.any(String), 57 | path: expect.stringContaining('superface/package.json'), 58 | }); 59 | 60 | expect(mockWriteOnce).not.toHaveBeenCalled(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/logic/project/js/js.ts: -------------------------------------------------------------------------------- 1 | import { buildProjectDefinitionFilePath } from '../../../common/file-structure'; 2 | import { exists } from '../../../common/io'; 3 | import { OutputStream } from '../../../common/output-stream'; 4 | import { SupportedLanguages } from '../../application-code'; 5 | 6 | export async function prepareJsProject( 7 | // https://www.npmjs.com/package/@superfaceai/one-sdk?activeTab=versions 8 | sdkVersion = 'beta', // get latest beta using the `beta` tag 9 | dotenvVersion = '^16.0.3' 10 | ): Promise<{ 11 | saved: boolean; 12 | dependencyInstallCommand: string; 13 | languageDependency: string; 14 | path: string; 15 | }> { 16 | const packageJson = `{ 17 | "name": "auto-generated-superface-project", 18 | "version": "1.0.0", 19 | "description": "", 20 | "main": "index.js", 21 | "engines" : { 22 | "node" : ">=18.0.0" 23 | }, 24 | "keywords": [], 25 | "author": "", 26 | "license": "ISC", 27 | "dependencies": { 28 | "@superfaceai/one-sdk": "${sdkVersion}", 29 | "dotenv": "${dotenvVersion}" 30 | } 31 | }`; 32 | 33 | const packageJsonPath = buildProjectDefinitionFilePath(SupportedLanguages.JS); 34 | 35 | const languageDependency = 'Node.js > 18.0.0'; 36 | const dependencyInstallCommand = 'npm install'; 37 | 38 | if (!(await exists(packageJsonPath))) { 39 | await OutputStream.writeOnce(packageJsonPath, packageJson); 40 | 41 | return { 42 | saved: true, 43 | dependencyInstallCommand, 44 | languageDependency, 45 | path: packageJsonPath, 46 | }; 47 | } 48 | 49 | return { 50 | saved: false, 51 | dependencyInstallCommand, 52 | languageDependency, 53 | path: packageJsonPath, 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/logic/project/prepare-project.ts: -------------------------------------------------------------------------------- 1 | import type { SupportedLanguages } from '../application-code'; 2 | import { prepareJsProject } from './js'; 3 | import { preparePythonProject } from './python'; 4 | 5 | export async function prepareProject(language: SupportedLanguages): Promise<{ 6 | saved: boolean; 7 | dependencyInstallCommand: string; 8 | languageDependency: string; 9 | path: string; 10 | }> { 11 | const PROJECT_PREPARATION_MAP: { 12 | [key in SupportedLanguages]: () => Promise<{ 13 | saved: boolean; 14 | dependencyInstallCommand: string; 15 | languageDependency: string; 16 | path: string; 17 | }>; 18 | } = { 19 | js: prepareJsProject, 20 | python: preparePythonProject, 21 | }; 22 | 23 | return PROJECT_PREPARATION_MAP[language](); 24 | } 25 | -------------------------------------------------------------------------------- /src/logic/project/python/index.ts: -------------------------------------------------------------------------------- 1 | export * from './python'; 2 | -------------------------------------------------------------------------------- /src/logic/project/python/python.test.ts: -------------------------------------------------------------------------------- 1 | import { buildProjectDefinitionFilePath } from '../../../common/file-structure'; 2 | import { exists } from '../../../common/io'; 3 | import { OutputStream } from '../../../common/output-stream'; 4 | import { SupportedLanguages } from '../../application-code'; 5 | import { preparePythonProject } from './python'; 6 | 7 | jest.mock('../../../common/output-stream'); 8 | jest.mock('../../../common/io'); 9 | 10 | describe('preparePythonProject', () => { 11 | const originalWriteOnce = OutputStream.writeOnce; 12 | 13 | let mockWriteOnce: jest.Mock; 14 | 15 | beforeAll(() => { 16 | // Mock static side of OutputStream 17 | mockWriteOnce = jest.fn(); 18 | OutputStream.writeOnce = mockWriteOnce; 19 | }); 20 | 21 | afterAll(() => { 22 | // Restore static side of OutputStream 23 | OutputStream.writeOnce = originalWriteOnce; 24 | }); 25 | 26 | beforeEach(() => { 27 | jest.clearAllMocks(); 28 | }); 29 | 30 | it('creates package.json if it does not exist', async () => { 31 | jest.mocked(exists).mockResolvedValueOnce(false); 32 | 33 | await expect(preparePythonProject('1.0.0b1')).resolves.toEqual({ 34 | saved: true, 35 | dependencyInstallCommand: expect.any(String), 36 | languageDependency: expect.any(String), 37 | path: expect.stringContaining('superface/requirements.txt'), 38 | }); 39 | 40 | expect(mockWriteOnce).toHaveBeenCalledWith( 41 | buildProjectDefinitionFilePath(SupportedLanguages.PYTHON), 42 | expect.any(String) 43 | ); 44 | }); 45 | 46 | it('does not create package.json if it exists', async () => { 47 | jest.mocked(exists).mockResolvedValueOnce(true); 48 | 49 | await expect(preparePythonProject('1.0.0b1')).resolves.toEqual({ 50 | saved: false, 51 | dependencyInstallCommand: expect.any(String), 52 | languageDependency: expect.any(String), 53 | path: expect.stringContaining('superface/requirements.txt'), 54 | }); 55 | 56 | expect(mockWriteOnce).not.toHaveBeenCalled(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/logic/project/python/python.ts: -------------------------------------------------------------------------------- 1 | import { buildProjectDefinitionFilePath } from '../../../common/file-structure'; 2 | import { exists } from '../../../common/io'; 3 | import { OutputStream } from '../../../common/output-stream'; 4 | import { SupportedLanguages } from '../../application-code'; 5 | 6 | export async function preparePythonProject( 7 | sdkVersion = '1b' // beta on major 1 8 | ): Promise<{ 9 | saved: boolean; 10 | dependencyInstallCommand: string; 11 | languageDependency: string; 12 | path: string; 13 | }> { 14 | const requirements = `one-sdk>=${sdkVersion} 15 | python-dotenv==1.0.0 16 | Brotli==1.0.9 17 | certifi==2023.5.7 18 | charset-normalizer==3.1.0 19 | idna==3.4 20 | urllib3==2.0.3 21 | wasmtime==10.0.0`; 22 | 23 | const requirementsPath = buildProjectDefinitionFilePath( 24 | SupportedLanguages.PYTHON 25 | ); 26 | 27 | const languageDependency = 'Python >= 3.8'; 28 | const dependencyInstallCommand = 'python3 -m pip install -r requirements.txt'; 29 | 30 | if (!(await exists(requirementsPath))) { 31 | await OutputStream.writeOnce(requirementsPath, requirements); 32 | 33 | return { 34 | saved: true, 35 | languageDependency, 36 | dependencyInstallCommand, 37 | path: requirementsPath, 38 | }; 39 | } 40 | 41 | return { 42 | saved: false, 43 | languageDependency, 44 | dependencyInstallCommand, 45 | path: requirementsPath, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/test/compile-fixtures.ts: -------------------------------------------------------------------------------- 1 | import type { MapDocumentNode, ProfileDocumentNode } from '@superfaceai/ast'; 2 | import { EXTENSIONS } from '@superfaceai/ast'; 3 | import { parseMap, parseProfile, Source } from '@superfaceai/parser'; 4 | import { dirname, join as joinPath } from 'path'; 5 | 6 | import { exists, mkdir, readFile } from '../common/io'; 7 | import { OutputStream } from '../common/output-stream'; 8 | 9 | export async function compileFixtureAsts(): Promise { 10 | // This fixtures we use in our integration test and we need keep them in sync with our Parser/AST version 11 | const fixtures: Record = { 12 | // Strict 13 | strictProfile: { 14 | source: joinPath('fixtures', 'strict.supr'), 15 | ast: joinPath('fixtures', 'compiled', 'strict.supr.ast.json'), 16 | }, 17 | strictMap: { 18 | source: joinPath('fixtures', 'strict.suma'), 19 | ast: joinPath('fixtures', 'compiled', 'strict.suma.ast.json'), 20 | }, 21 | 22 | withExcamples: { 23 | source: joinPath('fixtures', 'with-examples.supr'), 24 | ast: joinPath('fixtures', 'compiled', 'with-examples.supr.ast.json'), 25 | }, 26 | 27 | // Starwars 28 | starwarsProfile: { 29 | source: joinPath( 30 | 'fixtures', 31 | 'profiles', 32 | 'starwars', 33 | 'character-information.supr' 34 | ), 35 | ast: joinPath( 36 | 'fixtures', 37 | 'profiles', 38 | 'starwars', 39 | 'character-information.supr.ast.json' 40 | ), 41 | }, 42 | starwarsProfileWithVersion: { 43 | source: joinPath( 44 | 'fixtures', 45 | 'profiles', 46 | 'starwars', 47 | 'character-information@1.0.2.supr' 48 | ), 49 | ast: joinPath( 50 | 'fixtures', 51 | 'profiles', 52 | 'starwars', 53 | 'character-information@1.0.2.supr.ast.json' 54 | ), 55 | }, 56 | starwarsMap: { 57 | source: joinPath( 58 | 'fixtures', 59 | 'profiles', 60 | 'starwars', 61 | 'maps', 62 | 'swapi.character-information.suma' 63 | ), 64 | ast: joinPath( 65 | 'fixtures', 66 | 'profiles', 67 | 'starwars', 68 | 'maps', 69 | 'swapi.character-information.suma.ast.json' 70 | ), 71 | }, 72 | starwarsMapWithVersion: { 73 | source: joinPath( 74 | 'fixtures', 75 | 'profiles', 76 | 'starwars', 77 | 'maps', 78 | 'swapi.character-information@1.0.2.suma' 79 | ), 80 | ast: joinPath( 81 | 'fixtures', 82 | 'profiles', 83 | 'starwars', 84 | 'maps', 85 | 'swapi.character-information@1.0.2.suma.ast.json' 86 | ), 87 | }, 88 | starWarsMapWithUnverifiedProvider: { 89 | source: joinPath( 90 | 'fixtures', 91 | 'profiles', 92 | 'starwars', 93 | 'maps', 94 | 'unverified-swapi.character-information.suma' 95 | ), 96 | ast: joinPath( 97 | 'fixtures', 98 | 'profiles', 99 | 'starwars', 100 | 'maps', 101 | 'unverified-swapi.character-information.suma.ast.json' 102 | ), 103 | }, 104 | spaceshipProfile: { 105 | source: joinPath( 106 | 'fixtures', 107 | 'profiles', 108 | 'starwars', 109 | 'spaceship-information.supr' 110 | ), 111 | ast: joinPath( 112 | 'fixtures', 113 | 'profiles', 114 | 'starwars', 115 | 'spaceship-information.supr.ast.json' 116 | ), 117 | }, 118 | 119 | // Communication 120 | communicationProfile: { 121 | source: joinPath( 122 | 'fixtures', 123 | 'profiles', 124 | 'communication', 125 | 'send-email@1.0.1.supr' 126 | ), 127 | ast: joinPath( 128 | 'fixtures', 129 | 'profiles', 130 | 'communication', 131 | 'send-email@1.0.1.supr.ast.json' 132 | ), 133 | }, 134 | }; 135 | 136 | for (const fixture of Object.values(fixtures)) { 137 | if (!(await exists(fixture.source))) { 138 | throw new Error(`Path: ${fixture.source} does not exists`); 139 | } 140 | const source = await readFile(fixture.source, { 141 | encoding: 'utf-8', 142 | }); 143 | 144 | let ast: MapDocumentNode | ProfileDocumentNode; 145 | if (fixture.source.endsWith(EXTENSIONS.profile.source)) { 146 | ast = parseProfile(new Source(source, fixture.source)); 147 | } else if (fixture.source.endsWith(EXTENSIONS.map.source)) { 148 | ast = parseMap(new Source(source, fixture.source)); 149 | } else { 150 | throw new Error( 151 | `Source path: ${fixture.source} has unexpected extension` 152 | ); 153 | } 154 | if ( 155 | !fixture.ast.endsWith(EXTENSIONS.profile.build) && 156 | !fixture.ast.endsWith(EXTENSIONS.map.build) 157 | ) { 158 | throw new Error(`AST path: ${fixture.ast} has unexpected extension`); 159 | } 160 | 161 | if (!(await exists(fixture.ast))) { 162 | await mkdir(dirname(fixture.ast), { recursive: true }); 163 | } 164 | 165 | await OutputStream.writeOnce( 166 | fixture.ast, 167 | JSON.stringify(ast, undefined, 2) 168 | ); 169 | } 170 | console.log('OK'); 171 | } 172 | 173 | if (require.main === module) { 174 | void compileFixtureAsts(); 175 | } 176 | -------------------------------------------------------------------------------- /src/test/map-document-node.ts: -------------------------------------------------------------------------------- 1 | import type { MapDocumentNode } from '@superfaceai/ast'; 2 | 3 | export const mockMapDocumentNode = (options?: { 4 | name?: string; 5 | scope?: string; 6 | version?: { 7 | major: number; 8 | minor: number; 9 | patch: number; 10 | label?: string; 11 | }; 12 | providerName?: string; 13 | }): MapDocumentNode => ({ 14 | kind: 'MapDocument', 15 | astMetadata: { 16 | sourceChecksum: 'checksum', 17 | astVersion: { 18 | major: 1, 19 | minor: 0, 20 | patch: 0, 21 | }, 22 | parserVersion: { 23 | major: 1, 24 | minor: 0, 25 | patch: 0, 26 | }, 27 | }, 28 | header: { 29 | kind: 'MapHeader', 30 | profile: { 31 | scope: options?.scope, 32 | name: options?.name ?? 'test', 33 | version: { 34 | major: options?.version?.major ?? 1, 35 | minor: options?.version?.minor ?? 0, 36 | patch: options?.version?.patch ?? 0, 37 | label: options?.version?.label, 38 | }, 39 | }, 40 | provider: options?.providerName ?? 'test-provider', 41 | }, 42 | definitions: [], 43 | }); 44 | -------------------------------------------------------------------------------- /src/test/mock-std.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi'; 2 | 3 | export type MockStd = { 4 | implementation(input: string | Uint8Array): boolean; 5 | readonly output: string; 6 | }; 7 | 8 | export const mockStd: () => MockStd = () => { 9 | const writes: string[] = []; 10 | 11 | return { 12 | implementation(input: string | Uint8Array): boolean { 13 | if (Array.isArray(input)) { 14 | writes.push(String.fromCharCode.apply(null, input)); 15 | } else if (typeof input === 'string') { 16 | writes.push(input); 17 | } 18 | 19 | return true; 20 | }, 21 | get output(): string { 22 | return writes.map(stripAnsi).join(''); 23 | }, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/test/profile-document-node.ts: -------------------------------------------------------------------------------- 1 | import type { ProfileDocumentNode } from '@superfaceai/ast'; 2 | 3 | export const mockProfileDocumentNode = (options?: { 4 | name?: string; 5 | scope?: string; 6 | version?: { 7 | major: number; 8 | minor: number; 9 | patch: number; 10 | label?: string; 11 | }; 12 | usecaseName?: string; 13 | }): ProfileDocumentNode => ({ 14 | kind: 'ProfileDocument', 15 | astMetadata: { 16 | sourceChecksum: 'checksum', 17 | astVersion: { 18 | major: 1, 19 | minor: 0, 20 | patch: 0, 21 | }, 22 | parserVersion: { 23 | major: 1, 24 | minor: 0, 25 | patch: 0, 26 | }, 27 | }, 28 | header: { 29 | kind: 'ProfileHeader', 30 | scope: options?.scope, 31 | name: options?.name ?? 'test', 32 | version: { 33 | major: options?.version?.major ?? 1, 34 | minor: options?.version?.minor ?? 0, 35 | patch: options?.version?.patch ?? 0, 36 | label: options?.version?.label, 37 | }, 38 | }, 39 | definitions: [ 40 | { 41 | kind: 'UseCaseDefinition', 42 | useCaseName: options?.usecaseName ?? 'Test', 43 | safety: 'safe', 44 | result: { 45 | kind: 'UseCaseSlotDefinition', 46 | value: { 47 | kind: 'ObjectDefinition', 48 | fields: [ 49 | { 50 | kind: 'FieldDefinition', 51 | fieldName: 'message', 52 | required: true, 53 | type: { 54 | kind: 'NonNullDefinition', 55 | type: { 56 | kind: 'PrimitiveTypeName', 57 | name: 'string', 58 | }, 59 | }, 60 | }, 61 | ], 62 | }, 63 | }, 64 | }, 65 | ], 66 | }); 67 | -------------------------------------------------------------------------------- /src/test/provider-json.ts: -------------------------------------------------------------------------------- 1 | import type { SecurityScheme } from '@superfaceai/ast'; 2 | import { ApiKeyPlacement, HttpScheme, SecurityType } from '@superfaceai/ast'; 3 | 4 | export const mockProviderJson = (options?: { 5 | name?: string; 6 | security?: SecurityScheme[]; 7 | }) => ({ 8 | name: options?.name ?? 'provider', 9 | services: [{ id: 'test-service', baseUrl: 'service/base/url' }], 10 | securitySchemes: options?.security ?? [ 11 | { 12 | type: SecurityType.HTTP, 13 | id: 'basic', 14 | scheme: HttpScheme.BASIC, 15 | }, 16 | { 17 | id: 'api', 18 | type: SecurityType.APIKEY, 19 | in: ApiKeyPlacement.HEADER, 20 | name: 'Authorization', 21 | }, 22 | { 23 | id: 'bearer', 24 | type: SecurityType.HTTP, 25 | scheme: HttpScheme.BEARER, 26 | bearerFormat: 'some', 27 | }, 28 | { 29 | id: 'digest', 30 | type: SecurityType.HTTP, 31 | scheme: HttpScheme.DIGEST, 32 | }, 33 | ], 34 | defaultService: 'test-service', 35 | }); 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "downlevelIteration": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "lib": [ 8 | "ES2019" 9 | ], 10 | "rootDir": "src", 11 | "module": "CommonJS", 12 | "moduleResolution": "node", 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitReturns": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "resolveJsonModule": true, 18 | "sourceMap": true, 19 | "strict": true, 20 | "strictPropertyInitialization": true, 21 | "target": "ES2019", 22 | "typeRoots": [ 23 | "node_modules/@types" 24 | ] 25 | }, 26 | "include": [ 27 | "src/**/*.ts" 28 | ] 29 | } -------------------------------------------------------------------------------- /tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["src/**/*.test.ts"] 4 | } 5 | --------------------------------------------------------------------------------