├── .eslintrc.yaml ├── index.js ├── .editorconfig ├── CODE_OF_CONDUCT.md ├── .sonarcloud.properties ├── tsconfig.json ├── lib ├── cli │ ├── cli │ │ ├── version.js │ │ ├── middlewares │ │ │ ├── base.js │ │ │ └── logger.js │ │ └── commands │ │ │ ├── init.js │ │ │ ├── deployer.js │ │ │ └── base.js │ ├── utils │ │ └── fsHelper.js │ └── init │ │ └── init.js ├── types │ ├── sap-cp-neo │ │ ├── sapCpNeoType.js │ │ └── SapCpNeoDeployer.js │ ├── sap-cp-cf │ │ ├── sapCpCfType.js │ │ └── SapCpCfDeployer.js │ ├── sap-netweaver │ │ ├── sapNetWeaverType.js │ │ ├── ODataResourceManager.js │ │ ├── SapNetWeaverDeployer.js │ │ ├── AdtResourceManager.js │ │ ├── ODataClient.js │ │ └── AdtClient.js │ ├── typeRepository.js │ └── AbstractDeployer.js ├── index.js └── deployer │ └── deployer.js ├── .github └── workflows │ ├── test.yml │ ├── release.yml │ ├── scorecards-analysis.yml │ └── codeql-analysis.yml ├── SECURITY.md ├── .gitignore ├── package.json ├── bin └── ui5-deployer.js ├── CONTRIBUTING.md ├── CHANGELOG.md ├── README.md └── LICENSE /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: 3 | - eslint-config-mlauffer-nodejs 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module @ui5/cli 5 | * @private 6 | */ 7 | module.exports = { 8 | init: require('./lib/cli/init/init').init 9 | }; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_size = 2 6 | indent_style = space 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | charset = utf-8 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project is governed by the [Contributor Covenant version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). All contributors and participants agree to abide by its terms. 4 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | # Organization and project keys are displayed in the right sidebar of the project homepage 2 | sonar.organization=mauriciolauffer 3 | sonar.projectKey=mauriciolauffer_ui5-deployer 4 | sonar.sources=lib,bin 5 | sonar.tests=test 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES2020", 5 | "alwaysStrict": true, 6 | "noEmit": true, 7 | "checkJs": true, 8 | "allowJs": true, 9 | "types": [ 10 | "@types/node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/cli/cli/version.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | let version; 5 | 6 | // This module holds the CLI's version information (set via ui5.js) for later retrieval (e.g. from middlewares/logger) 7 | module.exports = { 8 | set: function(v) { 9 | version = v; 10 | }, 11 | get: function() { 12 | return version; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [pull_request] 4 | 5 | permissions: read-all 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 16 15 | cache: npm 16 | - run: npm ci --ignore-scripts 17 | - run: npm test 18 | -------------------------------------------------------------------------------- /lib/cli/cli/middlewares/base.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | const logger = require('./logger'); 5 | /** 6 | * Base middleware for CLI commands. 7 | * 8 | * This middleware should be executed for every CLI command to enable basic features (e.g. logging). 9 | * 10 | * @param {object} argv The CLI arguments 11 | * @returns {object} 12 | */ 13 | module.exports = function(argv) { 14 | logger.init(argv); 15 | return {}; 16 | }; 17 | -------------------------------------------------------------------------------- /lib/types/sap-cp-neo/sapCpNeoType.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const SapCpNeoDeployer = require('./SapCpNeoDeployer'); 4 | 5 | module.exports = { 6 | deploy: function({resourceCollections, project, parentLogger}) { 7 | parentLogger.info('to SAP Cloud Platform NEO'); 8 | return new SapCpNeoDeployer({resourceCollections, project, parentLogger}).deploy(); 9 | }, 10 | 11 | // Export type classes for extensibility 12 | Deployer: SapCpNeoDeployer 13 | }; 14 | -------------------------------------------------------------------------------- /lib/types/sap-cp-cf/sapCpCfType.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const SapCpCfDeployer = require('./SapCpCfDeployer'); 4 | 5 | module.exports = { 6 | deploy: function({resourceCollections, project, parentLogger}) { 7 | parentLogger.info('to SAP Cloud Platform Cloud Foundry'); 8 | return new SapCpCfDeployer({resourceCollections, project, parentLogger}).deploy(); 9 | }, 10 | 11 | // Export type classes for extensibility 12 | Deployer: SapCpCfDeployer 13 | }; 14 | -------------------------------------------------------------------------------- /lib/types/sap-netweaver/sapNetWeaverType.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const SapNetWeaverDeployer = require('./SapNetWeaverDeployer'); 4 | 5 | module.exports = { 6 | deploy: function({resourceCollections, project, parentLogger}) { 7 | parentLogger.info('to SAP Netweaver'); 8 | return new SapNetWeaverDeployer({resourceCollections, project, parentLogger}).deploy(); 9 | }, 10 | 11 | // Export type classes for extensibility 12 | Deployer: SapNetWeaverDeployer 13 | }; 14 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | To report a security issue, contact me on [Twitter](https://twitter.com/mauriciolauffer) or [LinkedIn](https://linkedin.com/in/mauriciolauffer) with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue. 4 | 5 | If the issue is confirmed as a vulnerability, a Security Advisory will be open and acknowledge your contributions as part of it. This project follows a 90 day disclosure timeline. 6 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module ui5-deployer 5 | * @public 6 | */ 7 | module.exports = { 8 | deployer: require('./deployer/deployer'), 9 | /** 10 | * @private 11 | * @see module:ui5-deployer.types 12 | * @namespace 13 | */ 14 | types: { 15 | AbstractDeployer: require('./types/AbstractDeployer'), 16 | sapNetWeaver: require('./types/sap-netweaver/sapNetWeaverType'), 17 | sapCpNeo: require('./types/sap-cp-neo/sapCpNeoType'), 18 | sapCpCf: require('./types/sap-cp-cf/sapCpCfType'), 19 | typeRepository: require('./types/typeRepository') 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /lib/cli/cli/middlewares/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | /** 5 | * Logger middleware used as one of default middlewares by tooling 6 | * 7 | * @param {object} argv logger arguments 8 | * @returns {object} logger instance or null 9 | */ 10 | function init(argv) { 11 | if (!argv.verbose && !argv.loglevel) return null; 12 | 13 | const logger = require('@ui5/logger'); 14 | if (argv.loglevel) { 15 | logger.setLevel(argv.loglevel); 16 | } 17 | if (argv.verbose) { 18 | logger.setLevel('verbose'); 19 | const version = require('../version').get(); 20 | logger.getLogger('cli:middlewares:base').verbose(`using @ui5/cli version ${version}`); 21 | logger.getLogger('cli:middlewares:base').verbose(`using node version ${process.version}`); 22 | } 23 | return logger; 24 | } 25 | 26 | module.exports = {init}; 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | branches: [master, main] 5 | 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: GoogleCloudPlatform/release-please-action@v3 11 | with: 12 | release-type: node 13 | package-name: ui5-deployer 14 | - uses: actions/checkout@v3 15 | if: ${{ steps.release.outputs.release_created }} 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 16 19 | cache: npm 20 | if: ${{ steps.release.outputs.release_created }} 21 | - run: npm ci --ignore-scripts 22 | if: ${{ steps.release.outputs.release_created }} 23 | - run: npm publish 24 | env: 25 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 26 | if: ${{ steps.release.outputs.release_created }} 27 | -------------------------------------------------------------------------------- /lib/cli/cli/commands/init.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | // Init 5 | const initCommand = { 6 | command: 'init', 7 | describe: 'Initialize the UI5 Tooling configuration for an application or library project.', 8 | middlewares: [require('../middlewares/base.js')], 9 | }; 10 | 11 | initCommand.handler = async function() { 12 | const fsHelper = require('../../utils/fsHelper'); 13 | const init = require('../../init/init'); 14 | const promisify = require('util').promisify; 15 | const path = require('path'); 16 | const fs = require('fs'); 17 | const jsYaml = require('js-yaml'); 18 | const writeFile = promisify(fs.writeFile); 19 | 20 | const yamlPath = path.resolve('./ui5.yaml'); 21 | if (await fsHelper.exists(yamlPath)) { 22 | throw new Error('Initialization not possible: ui5.yaml already exists'); 23 | } 24 | 25 | const projectConfig = await init.init(); 26 | const yaml = jsYaml.safeDump(projectConfig); 27 | 28 | await writeFile(yamlPath, yaml); 29 | console.log(`Wrote ui5.yaml to ${yamlPath}:\n`); 30 | console.log(yaml); 31 | }; 32 | 33 | module.exports = initCommand; 34 | -------------------------------------------------------------------------------- /lib/cli/utils/fsHelper.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | const {promisify} = require('util'); 5 | const fs = require('fs'); 6 | const stat = promisify(fs.stat); 7 | const path = require('path'); 8 | 9 | /** 10 | * Checks if a file or path exists 11 | * 12 | * @private 13 | * @param {string} filePath Path to check 14 | * @returns {Promise} Promise resolving with true if the file or path exists 15 | */ 16 | async function exists(filePath) { 17 | try { 18 | await stat(filePath); 19 | return true; 20 | } catch (err) { 21 | // "File or directory does not exist" 22 | if (err.code === 'ENOENT') { 23 | return false; 24 | } else { 25 | throw err; 26 | } 27 | } 28 | } 29 | 30 | /** 31 | * Checks if a list of paths exists 32 | * 33 | * @private 34 | * @param {Array} paths List of paths to check 35 | * @param {string} cwd Current working directory 36 | * @returns {Promise} Resolving with an array of booleans for each path 37 | */ 38 | async function pathsExist(paths, cwd) { 39 | return await Promise.all(paths.map((p) => exists(path.join(cwd, p)))); 40 | } 41 | 42 | module.exports = { 43 | exists, 44 | pathsExist, 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .idea/ 64 | 65 | #vs code debugging 66 | .vscode 67 | -------------------------------------------------------------------------------- /lib/types/typeRepository.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sapNetWeaverType = require('./sap-netweaver/sapNetWeaverType'); 4 | const sapCpNeoType = require('./sap-cp-neo/sapCpNeoType'); 5 | const sapCpCfType = require('./sap-cp-cf/sapCpCfType'); 6 | 7 | const types = { 8 | 'sap-netweaver': sapNetWeaverType, 9 | 'sap-cp-neo': sapCpNeoType, 10 | 'sap-cp-cf': sapCpCfType 11 | }; 12 | 13 | /** 14 | * Gets a type 15 | * 16 | * @param {string} typeName unique identifier for the type 17 | * @returns {object} type identified by name 18 | * @throws {Error} if not found 19 | */ 20 | function getType(typeName) { 21 | // eslint-disable-next-line security/detect-object-injection 22 | const type = types[typeName]; 23 | 24 | if (!type) { 25 | throw new Error('Unknown type *' + typeName + '*'); 26 | } 27 | return type; 28 | } 29 | 30 | /** 31 | * Adds a type 32 | * 33 | * @param {string} typeName unique identifier for the type 34 | * @param {object} type project type 35 | * @throws {Error} if duplicate with same name was found 36 | */ 37 | function addType(typeName, type) { 38 | // eslint-disable-next-line security/detect-object-injection 39 | if (types[typeName]) { 40 | throw new Error('Type already registered *' + typeName + '*'); 41 | } 42 | // eslint-disable-next-line security/detect-object-injection 43 | types[typeName] = type; 44 | } 45 | 46 | module.exports = { 47 | getType: getType, 48 | addType: addType 49 | }; 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui5-deployer", 3 | "version": "0.4.1", 4 | "description": "UI5 Deployer", 5 | "author": "Mauricio Lauffer", 6 | "license": "Apache-2.0", 7 | "bin": { 8 | "ui5-deployer": "bin/ui5-deployer.js" 9 | }, 10 | "main": "index.js", 11 | "files": [ 12 | "bin/**", 13 | "lib/**", 14 | "index.js" 15 | ], 16 | "keywords": [ 17 | "openui5", 18 | "sapui5", 19 | "ui5", 20 | "deploy", 21 | "deployment", 22 | "deployer", 23 | "development", 24 | "tool" 25 | ], 26 | "scripts": { 27 | "test": "npm run lint", 28 | "lint": "eslint .", 29 | "lint:fix": "eslint . --fix" 30 | }, 31 | "dependencies": { 32 | "@ui5/fs": "^2.0.6", 33 | "@ui5/logger": "^2.0.1", 34 | "@ui5/project": "^2.6.0", 35 | "archiver": "^5.3.1", 36 | "dotenv": "^16.0.0", 37 | "got": "^11.8.3", 38 | "import-local": "^3.1.0", 39 | "install": "^0.13.0", 40 | "isbinaryfile": "^5.0.0", 41 | "js-yaml": "^4.1.0", 42 | "tough-cookie": "^4.0.0", 43 | "xmldoc": "^1.1.2", 44 | "yargs": "^17.4.1" 45 | }, 46 | "devDependencies": { 47 | "@types/node": "^16.11.27", 48 | "eslint": "^8.14.0", 49 | "eslint-config-mlauffer-nodejs": "^1.2.2" 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "url": "https://github.com/mauriciolauffer/ui5-deployer.git" 54 | }, 55 | "bugs": { 56 | "url": "https://github.com/mauriciolauffer/ui5-deployer/issues" 57 | }, 58 | "engines": { 59 | "node": ">= 10", 60 | "npm": ">= 5" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/scorecards-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Scorecards supply-chain security 2 | on: 3 | # Only the default branch is supported. 4 | branch_protection_rule: 5 | schedule: 6 | - cron: '10 5 * * 1' 7 | push: 8 | branches: [ master ] 9 | 10 | # Declare default permissions as read only. 11 | permissions: read-all 12 | 13 | jobs: 14 | analysis: 15 | name: Scorecards analysis 16 | runs-on: ubuntu-latest 17 | permissions: 18 | # Needed to upload the results to code-scanning dashboard. 19 | security-events: write 20 | 21 | steps: 22 | - name: "Checkout code" 23 | uses: actions/checkout@v3 24 | with: 25 | persist-credentials: false 26 | 27 | - name: "Run analysis" 28 | uses: ossf/scorecard-action@v1 29 | with: 30 | results_file: results.sarif 31 | results_format: sarif 32 | # Read-only PAT token. To create it, 33 | # follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation. 34 | repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} 35 | # Publish the results to enable scorecard badges. For more details, see 36 | # https://github.com/ossf/scorecard-action#publishing-results. 37 | # For private repositories, `publish_results` will automatically be set to `false`, 38 | # regardless of the value entered here. 39 | publish_results: true 40 | 41 | # Upload the results to GitHub's code scanning dashboard. 42 | - name: "Upload to code-scanning" 43 | uses: github/codeql-action/upload-sarif@v1 44 | with: 45 | sarif_file: results.sarif 46 | -------------------------------------------------------------------------------- /lib/types/sap-cp-neo/SapCpNeoDeployer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AbstractDeployer = require('../AbstractDeployer'); 4 | const path = require('path'); 5 | // eslint-disable-next-line security/detect-child-process 6 | const {spawn} = require('child_process'); 7 | 8 | /** 9 | * Deployer Class for SAP Cloud Platform, NEO environment 10 | * 11 | * @augments AbstractDeployer 12 | */ 13 | class SapCpNeoDeployer extends AbstractDeployer { 14 | /** 15 | * Deploys the project to a remote SAP Cloud Platform, NEO environment 16 | * 17 | * @returns {Promise} Returns promise with deployment results 18 | */ 19 | deploy() { 20 | return this.getLocalResources() 21 | .then((resources) => { 22 | const neoCliPath = path.normalize(this.project.deployer.sapCloudPlatform.neo.cliPath || ''); 23 | const neoDeploy = spawn(this.buildNeoCommand(this.project, this.project.deployer.sourcePath), {shell: true, cwd: neoCliPath}); 24 | return this.promisifyChildProcess(neoDeploy); 25 | }); 26 | } 27 | 28 | /** 29 | * Builds the NEO CLI command for deployment 30 | * 31 | * @param {object} project Project configuration 32 | * @param {string} filename Path to the file to be deployed 33 | * @returns {string} Returns the command to be executed 34 | */ 35 | buildNeoCommand(project, filename) { 36 | const mtaFilePath = path.join(project.path, filename); 37 | return ['neo deploy-mta', 38 | '--host', project.deployer.connection.url, 39 | '--account', project.deployer.sapCloudPlatform.neo.account, 40 | '--user', project.deployer.credentials.username, 41 | '--password', project.deployer.credentials.password, 42 | '--source', mtaFilePath, 43 | '--synchronous'].join(' '); 44 | } 45 | } 46 | 47 | module.exports = SapCpNeoDeployer; 48 | -------------------------------------------------------------------------------- /lib/types/sap-netweaver/ODataResourceManager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const archiver = require('archiver'); 5 | 6 | /** 7 | * Class to handle project's resources 8 | */ 9 | class ODataResourceManager { 10 | /** 11 | * Constructor 12 | * 13 | * @param {object} parameters Parameters 14 | * @param {object} parameters.project Project configuration 15 | * @param {module:@ui5/fs.adapters.FileSystem} parameters.workspace Workspace Resource 16 | * @param {object} parameters.parentLogger Logger to use 17 | */ 18 | constructor({project, workspace, parentLogger}) { 19 | this._project = project; 20 | this._workspace = workspace; 21 | this.logger = parentLogger; 22 | this._localResources = []; 23 | } 24 | 25 | /** 26 | * Prepare resources to be uploaded 27 | * 28 | * @param {Array} localResources Local resources 29 | */ 30 | async prepareResources(localResources) { 31 | this._localResources = localResources; 32 | return this.createArchive(); 33 | } 34 | 35 | async createArchive() { 36 | const archivePath = `${ this._workspace._fsBasePath }/${ this._project.metadata.name }-archive.zip`; 37 | const output = fs.createWriteStream(archivePath); 38 | const archive = archiver('zip', { 39 | store: true 40 | }); 41 | archive.on('error', function(err) { 42 | throw err; 43 | }); 44 | archive.pipe(output); 45 | await this.zipFiles(archive); 46 | await archive.finalize(); 47 | this.logger.info('Archive has been created:', archivePath); 48 | return archivePath; 49 | } 50 | 51 | async zipFiles(archive) { 52 | for (const resource of this._localResources) { 53 | archive.append(await resource.getBuffer(), {name: resource.getPath()}); 54 | } 55 | } 56 | } 57 | 58 | module.exports = ODataResourceManager; 59 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | schedule: 19 | - cron: '10 5 * * 1' 20 | 21 | permissions: read-all 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | security-events: write 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | language: [ 'javascript' ] 34 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 35 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v3 40 | with: 41 | persist-credentials: false 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v1 55 | -------------------------------------------------------------------------------- /lib/cli/cli/commands/deployer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | const baseMiddleware = require('../middlewares/base.js'); 5 | 6 | const deploy = { 7 | command: 'deploy', 8 | describe: 'Deploy project in current directory', 9 | handler: handleDeploy, 10 | middlewares: [baseMiddleware], 11 | }; 12 | 13 | deploy.builder = function(cli) { 14 | return cli 15 | .option('transport-request', { 16 | describe: 'ABAP Transport Request', 17 | default: '', 18 | type: 'string', 19 | }) 20 | .option('username', { 21 | describe: 'Username to log into the target system', 22 | default: '', 23 | type: 'string', 24 | }) 25 | .option('password', { 26 | describe: 'Password to log into the target system', 27 | default: '', 28 | type: 'string', 29 | }) 30 | .option('space', { 31 | describe: 'Cloud Foundry space to deploy the app into', 32 | default: '', 33 | type: 'string', 34 | }) 35 | .example('ui5 deploy', 'Deploy project with all parameters from ui5.yaml file.') 36 | .example('ui5 deploy --transport-request=ABAPDK99999', 'Deploy project with the given ABAP Transport Request') 37 | .example('ui5 deploy --username=MyUsername --password=MyPassword', 'Deploy project with the given credentials') 38 | .example('ui5 deploy --space=dev', 'Deploy project into the given Cloud Foundry space'); 39 | }; 40 | 41 | async function handleDeploy(argv) { 42 | const normalizer = require('@ui5/project').normalizer; 43 | const deployer = require('../../../index').deployer; 44 | const logger = require('@ui5/logger'); 45 | 46 | logger.setShowProgress(true); 47 | 48 | const normalizerOptions = { 49 | translatorName: argv.translator, 50 | configPath: argv.config, 51 | }; 52 | 53 | const tree = await normalizer.generateProjectTree(normalizerOptions); 54 | await deployer.deploy({ 55 | tree: tree, 56 | transportRequest: argv['transport-request'], 57 | username: argv.username, 58 | password: argv.password, 59 | space: argv.space 60 | }); 61 | } 62 | 63 | module.exports = deploy; 64 | -------------------------------------------------------------------------------- /lib/types/sap-cp-cf/SapCpCfDeployer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AbstractDeployer = require('../AbstractDeployer'); 4 | const path = require('path'); 5 | // eslint-disable-next-line security/detect-child-process 6 | const {spawn} = require('child_process'); 7 | 8 | /** 9 | * Deployer Class for SAP Cloud Platform, Cloud Foundry environment 10 | * 11 | * @augments AbstractDeployer 12 | */ 13 | class SapCpCfDeployer extends AbstractDeployer { 14 | /** 15 | * Deploys the project to a remote SAP Cloud Platform, Cloud Foundry environment 16 | * 17 | * @returns {Promise} Returns promise with deployment results 18 | */ 19 | deploy() { 20 | return this.getLocalResources() 21 | .then(() => { 22 | const cfCliPath = path.normalize(this.project.deployer.sapCloudPlatform.cloudFoundry.cliPath || ''); 23 | const cfLogin = spawn(this.buildLoginCommand(this.project), {shell: true, cwd: cfCliPath}); 24 | return this.promisifyChildProcess(cfLogin, this.logger) 25 | .then(() => { 26 | const cfPush = spawn(this.buildPushCommand(this.project.deployer.sourcePath), {shell: true, cwd: cfCliPath}); 27 | return this.promisifyChildProcess(cfPush, this.logger); 28 | }); 29 | }); 30 | } 31 | 32 | /** 33 | * Builds the CF CLI command for deployment 34 | * 35 | * @param {object} project Project configuration 36 | * @returns {string} Returns the command to be executed 37 | */ 38 | buildLoginCommand(project) { 39 | return ['cf login', 40 | '-a', project.deployer.connection.url, 41 | '-u', project.deployer.credentials.username, 42 | '-p', project.deployer.credentials.password, 43 | '-o', project.deployer.sapCloudPlatform.cloudFoundry.org, 44 | '-s', project.deployer.sapCloudPlatform.cloudFoundry.space].join(' '); 45 | } 46 | 47 | /** 48 | * Builds the CF CLI command for deployment 49 | * 50 | * @param {string} sourcePath Path to the file to be deployed 51 | * @returns {string} Returns the command to be executed 52 | */ 53 | buildPushCommand(sourcePath) { 54 | return ['cf push', 55 | '-f', sourcePath].join(' '); 56 | } 57 | } 58 | 59 | module.exports = SapCpCfDeployer; 60 | -------------------------------------------------------------------------------- /lib/types/AbstractDeployer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Base class for the deployer implementation of a project type 5 | * 6 | * @abstract 7 | */ 8 | class AbstractDeployer { 9 | /** 10 | * Constructor 11 | * 12 | * @param {object} parameters Parameters 13 | * @param {object} parameters.resourceCollections Resource collections 14 | * @param {object} parameters.project Project configuration 15 | * @param {object} parameters.parentLogger Logger to use 16 | */ 17 | constructor({resourceCollections, project, parentLogger}) { 18 | if (new.target === AbstractDeployer) { 19 | throw new TypeError('Class *AbstractDeployer* is abstract'); 20 | } 21 | 22 | this.project = project; 23 | this.logger = parentLogger.createSubLogger(project.type + ' ' + project.metadata.name, 0.2); 24 | this.resourceCollections = resourceCollections; 25 | } 26 | 27 | /** 28 | * Gets project's local resources 29 | * 30 | * @returns {module:@ui5/fs.adapters.FileSystem} Returns promise chain with workspace resources 31 | */ 32 | getLocalResources() { 33 | return this.resourceCollections.workspace.byGlob('**'); 34 | } 35 | 36 | /** 37 | * Deploys the project to a remote server 38 | * 39 | * @abstract 40 | * @returns {Promise} Returns promise with deployment results 41 | */ 42 | deploy() { 43 | throw new Error('Function *deploy* is not implemented'); 44 | } 45 | 46 | /** 47 | * Encapsulates child processes in Promises 48 | * 49 | * @abstract 50 | * @param {module:child_process} spawn Child Process to be executed 51 | * @returns {Promise} Returns promise with child process results 52 | */ 53 | promisifyChildProcess(spawn) { 54 | return new Promise((resolve, reject) => { 55 | spawn.addListener('error', reject); 56 | spawn.addListener('exit', (code) => { 57 | const message = `Child process exited with code ${code}`; 58 | return (code === 0) ? resolve(message) : reject(message); 59 | }); 60 | spawn.stdout.on('data', (data) => { 61 | this.logger.info(data.toString()); 62 | }); 63 | spawn.stderr.on('data', (data) => { 64 | this.logger.error(data.toString()); 65 | }); 66 | spawn.on('error', (err) => { 67 | this.logger.error('Failed to start subprocess.'); 68 | }); 69 | }); 70 | } 71 | } 72 | 73 | module.exports = AbstractDeployer; 74 | -------------------------------------------------------------------------------- /bin/ui5-deployer.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-disable */ 4 | 'use strict'; 5 | 6 | // The following block should be compatible to as many Node.js versions as possible 7 | /* eslint-disable no-var */ 8 | var pkg = require('../package.json'); 9 | var semver = require('semver'); 10 | var nodeVersion = process.version; 11 | /* eslint-enable no-var */ 12 | if (pkg.engines && pkg.engines.node && !semver.satisfies(nodeVersion, pkg.engines.node)) { 13 | console.log('==================== UNSUPPORTED NODE.JS VERSION ===================='); 14 | console.log('You are using an unsupported version of Node.js'); 15 | console.log('Detected version ' + nodeVersion + ' but ' + pkg.name + ' requires ' + pkg.engines.node); 16 | console.log(''); 17 | console.log('=> Please upgrade to a supported version of Node.js to use this tool'); 18 | console.log('====================================================================='); 19 | process.exit(1); 20 | } 21 | 22 | // Timeout is required to log info when importing from local installation 23 | setTimeout(() => { 24 | if (!process.env.UI5_CLI_NO_LOCAL) { 25 | const importLocal = require('import-local'); 26 | // Prefer a local installation of @ui5/cli. 27 | // This will invoke the local CLI, so no further action required 28 | if (importLocal(__filename)) { 29 | if (process.argv.includes('--verbose')) { 30 | console.info(`INFO: This project contains an individual ${pkg.name} installation which ` + 31 | 'will be used over the global one.'); 32 | console.info(''); 33 | } else { 34 | console.info(`INFO: Using local ${pkg.name} installation`); 35 | console.info(''); 36 | } 37 | return; 38 | } 39 | } 40 | 41 | const cli = require('yargs'); 42 | 43 | // Explicitly set CLI version as the yargs default might 44 | // be wrong in case a local CLI installation is used 45 | // Also add CLI location 46 | const version = `${pkg.version} (from ${__filename})`; 47 | require('../lib/cli/cli/version').set(version); 48 | cli.version(version); 49 | 50 | // Explicitly set script name to prevent windows from displaying "ui5.js" 51 | cli.scriptName('ui5-deployer'); 52 | 53 | // CLI modules 54 | cli.commandDir('../lib/cli/cli/commands'); 55 | 56 | // Format terminal output to full available width 57 | cli.wrap(cli.terminalWidth()); 58 | 59 | // yargs registers a get method on the argv property. 60 | // The property needs to be accessed to initialize everything. 61 | cli.argv; 62 | }, 0); 63 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | Patches and contributions are welcome to this project. There are just a few small guidelines you need to follow. 4 | 5 | Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE.md). 6 | 7 | When you contribute code, you affirm that the contribution is your original work and that you license the work to the project under the project's open source license. Whether or not you state this explicitly, by submitting any copyrighted material via pull request, email, or other means you agree to license the material under the project's open source license and warrant that you have the legal authority to do so. 8 | 9 | ## Code of Conduct 10 | 11 | Please note that this project is released with a [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. 12 | 13 | ## Versioning 14 | 15 | This library follows [Semantic Versioning](http://semver.org). 16 | 17 | ## Code Reviews 18 | 19 | All submissions, including submissions by project members, require review. The project uses GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. 20 | 21 | ## Contributing 22 | 23 | 1. Look through the existing issues and see if your idea is something new. 24 | 2. Create a new issue, or comment on an existing issue that you would like to help solve: 25 | * it's usually best to get some feedback before proceeding to write code. 26 | 3. fork the repo, and clone it to your computer: 27 | * GitHub has [great documentation](https://help.github.com/articles/using-pull-requests/) regarding writing your first pull request. 28 | 4. make sure that you write unit-test for any code that you write for the project: 29 | * ESLint is the main SAST tool in this project. 30 | * look through the test suite in `/test` folder to get an idea for how to write unit-tests for this codebase. 31 | 32 | ## Before you begin 33 | 34 | 1. [Install Node.js LTS](https://nodejs.org/en/). 35 | 36 | ### How to test 37 | 38 | 1. Install dependencies: 39 | 40 | npm install 41 | 42 | 2. Lint the codebase: 43 | 44 | npm run lint 45 | 46 | 3. Run the tests: 47 | 48 | npm test 49 | 50 | ### CI/CD 51 | 52 | The project uses GitHub Actions for its CI/CD pipeline. There is no need to build and publish from local machines as this will be taken care by CI/CD. However, one should build locally for testing purposes. 53 | -------------------------------------------------------------------------------- /lib/cli/init/init.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | const {promisify} = require('util'); 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const readFile = promisify(fs.readFile); 8 | const fsHelper = require('../utils/fsHelper'); 9 | 10 | /** 11 | * Reads the package.json file and returns its content 12 | * 13 | * @private 14 | * @param {string} filePath Path to package.json 15 | * @returns {object} Package json content 16 | */ 17 | async function readPackageJson(filePath) { 18 | const content = await readFile(filePath, 'utf8'); 19 | return JSON.parse(content); 20 | } 21 | 22 | /** 23 | * Determines the project type from the provided parameters 24 | * 25 | * @private 26 | * @param {boolean} hasWebapp Webapp folder exists 27 | * @param {boolean} hasSrc Src folder exists 28 | * @param {boolean} hasTest Test folder exists 29 | * @returns {string} Project type 30 | */ 31 | function getProjectType(hasWebapp, hasSrc, hasTest) { 32 | let errorReason; 33 | if (hasWebapp) { 34 | // Mixed folders of application and library 35 | if (hasSrc && hasTest) { 36 | errorReason = 'Found \'webapp\', \'src\' and \'test\' folders.\n'; 37 | } else if (hasSrc) { 38 | errorReason = 'Found \'webapp\' and \'src\' folders.\n'; 39 | } else if (hasTest) { 40 | errorReason = 'Found \'webapp\' and \'test\' folders.\n'; 41 | } else { 42 | return 'application'; 43 | } 44 | } else if (hasSrc) { 45 | return 'library'; 46 | } else if (hasTest) { 47 | // Only test folder 48 | errorReason = 'Found \'test\' folder but no \'src\' folder.\n'; 49 | } else { 50 | // No folders at all 51 | errorReason = 'Could not find \'webapp\' or \'src\' / \'test\' folders.\n'; 52 | } 53 | if (errorReason) { 54 | let message = `Could not detect project type: ${errorReason}`; 55 | message += 'Applications should only have a \'webapp\' folder.\n'; 56 | message += 'Libraries should only have a \'src\' and (optional) \'test\' folder.'; 57 | throw new Error(message); 58 | } 59 | } 60 | 61 | /** 62 | * Initiates the projects ui5.yaml configuration file. 63 | * 64 | * Checks the package.json and tries to determine the project type. If the ui5.yaml file does not exist, 65 | * it is created with the basic project configuration. 66 | * 67 | * @module @ui5/cli/init 68 | * @param {string} cwd Current working directory 69 | * @returns {Promise} Promise resolving with the project configuration object 70 | */ 71 | async function init({cwd = './'} = {}) { 72 | const projectConfig = { 73 | specVersion: '2.0', 74 | metadata: {}, 75 | }; 76 | let pkg; 77 | 78 | try { 79 | pkg = await readPackageJson(path.join(cwd, 'package.json')); 80 | } catch (err) { 81 | if (err.code === 'ENOENT') { 82 | throw new Error('Initialization not possible: Missing package.json file'); 83 | } else { 84 | throw err; 85 | } 86 | } 87 | 88 | if (pkg && pkg.name) { 89 | projectConfig.metadata.name = pkg.name; 90 | } else { 91 | throw new Error('Initialization not possible: Missing \'name\' in package.json'); 92 | } 93 | 94 | const [hasWebapp, hasSrc, hasTest] = await fsHelper.pathsExist(['webapp', 'src', 'test'], cwd); 95 | projectConfig.type = getProjectType(hasWebapp, hasSrc, hasTest); 96 | 97 | return projectConfig; 98 | } 99 | 100 | module.exports = { 101 | init: init, 102 | }; 103 | -------------------------------------------------------------------------------- /lib/cli/cli/commands/base.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | const cli = require('yargs'); 5 | 6 | cli.usage('Usage: ui5 [options]') 7 | .demandCommand(1, 'Command required! Please have a look at the help documentation above.') 8 | .option('config', { 9 | describe: 'Path to configuration file', 10 | type: 'string', 11 | }) 12 | .option('translator', { 13 | describe: 'Translator to use. Including optional colon separated translator parameters.', 14 | alias: 't8r', 15 | default: 'npm', 16 | type: 'string', 17 | }) 18 | .option('verbose', { 19 | describe: 'Enable verbose logging.', 20 | type: 'boolean', 21 | }) 22 | .option('loglevel', { 23 | alias: 'log-level', 24 | describe: 'Set the logging level (error|warn|info|verbose|silly).', 25 | default: 'info', 26 | type: 'string', 27 | }) 28 | .showHelpOnFail(true) 29 | .strict(true) 30 | .alias('help', 'h') 31 | .alias('version', 'v') 32 | .example('ui5 --translator static:/path/to/projectDependencies.yaml', 33 | 'Execute command using a "static" translator with translator parameters') 34 | .example('ui5 --config /path/to/ui5.yaml', 35 | 'Execute command using a project configuration from custom path') 36 | .fail(function(msg, err, yargs) { 37 | const chalk = require('chalk'); 38 | if (err) { 39 | // Exception 40 | const logger = require('@ui5/logger'); 41 | if (logger.isLevelEnabled('error')) { 42 | console.log(''); 43 | console.log(chalk.bold.red('⚠️ Process Failed With Error')); 44 | 45 | console.log(''); 46 | console.log(chalk.underline('Error Message:')); 47 | console.log(err.message); 48 | 49 | if (logger.isLevelEnabled('verbose')) { 50 | console.log(''); 51 | console.log(chalk.underline('Stack Trace:')); 52 | console.log(err.stack); 53 | 54 | // Try to guess responsible module from stack trace file paths 55 | // This should work for the following paths: 56 | // - @ui5/cli (npm consumption) 57 | // - ui5-cli (local repository consumption) 58 | // - lib/cli (local consumption without repository name in path, i.e. Azure CI) 59 | const moduleRegExp = /@?(?:ui5|lib).(?:logger|fs|builder|server|project|cli)/ig; 60 | 61 | // Only check the lowest stack entry 62 | const rootStackEntry = err.stack.split('\n')[1]; 63 | const match = rootStackEntry.match(moduleRegExp); 64 | if (match) { 65 | // Use the last match of the line because of cases like this: 66 | // node_modules/@ui5/cli/node_modules/@ui5/builder/lib/ => should match the builder 67 | let moduleNameGuess = match[match.length - 1]; 68 | 69 | // Normalize match 70 | moduleNameGuess = moduleNameGuess.replace(/.*(?:ui5|lib).(.*)/i, 'ui5-$1').toLowerCase(); 71 | const newIssueUrl = `https://github.com/SAP/${moduleNameGuess}/issues/new`; 72 | console.log(''); 73 | console.log( 74 | chalk.dim( 75 | `If you think this is an issue of the UI5 Tooling, you might report it using the ` + 76 | `following URL: `) + 77 | chalk.dim.bold.underline(newIssueUrl)); 78 | } 79 | } else { 80 | console.log(''); 81 | console.log(chalk.dim( 82 | `For details, execute the same command again with an additional '--verbose' parameter`)); 83 | } 84 | } 85 | } else { 86 | // Yargs error 87 | console.log(chalk.bold.yellow('Command Failed:')); 88 | console.log(`${msg}`); 89 | console.log(''); 90 | console.log(chalk.dim(`See 'ui5 --help' or 'ui5 build --help' for help`)); 91 | } 92 | process.exit(1); 93 | }); 94 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### [0.4.1](https://github.com/mauriciolauffer/ui5-deployer/compare/v0.4.0...v0.4.1) (2022-04-25) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * Improve error log + add node v12 support ([6b9e384](https://github.com/mauriciolauffer/ui5-deployer/commit/6b9e38409df2b75e2b7ccab683dfc70c6cd51d1b)) 9 | 10 | ## [0.4.0](https://github.com/mauriciolauffer/ui5-deployer/compare/v0.3.0...v0.4.0) (2022-04-22) 11 | 12 | 13 | ### Features 14 | 15 | * CLI is not an extra pkg + replace requestjs ([#14](https://github.com/mauriciolauffer/ui5-deployer/issues/14)) ([0538fed](https://github.com/mauriciolauffer/ui5-deployer/commit/0538fed9c5f7415eeffa453e8b735e2b28ad8956)) 16 | * Deploy to SAP CP NEO and CF ([36808d5](https://github.com/mauriciolauffer/ui5-deployer/commit/36808d5b73136f25854c9f75882dd8c27acd52b7)) 17 | * Deploy via OData /UI5/ABAP_REPOSITORY_SRV ([#26](https://github.com/mauriciolauffer/ui5-deployer/issues/26)) ([c9d6eaf](https://github.com/mauriciolauffer/ui5-deployer/commit/c9d6eafb2d70fbbd6cec08c6fefc90e5628bf3dc)) 18 | * ENV vars for NEO cli ([#20](https://github.com/mauriciolauffer/ui5-deployer/issues/20)) ([9fb9ddf](https://github.com/mauriciolauffer/ui5-deployer/commit/9fb9ddf9c01bb30d64bdee7d7c6141fc7f69a9b3)) 19 | * More error logs ([6d7d459](https://github.com/mauriciolauffer/ui5-deployer/commit/6d7d45988cfd864d9bf55ee690de14c5f152f201)) 20 | * readme with SAP CP instructions ([9782801](https://github.com/mauriciolauffer/ui5-deployer/commit/9782801996c139e9666c2b74d2f3910ff838b777)) 21 | * Skip ADT validations ([0256bbb](https://github.com/mauriciolauffer/ui5-deployer/commit/0256bbb9f9a17c280d146239eaddbdf2149aa2fc)) 22 | * Support resource exclusion ([13e1d7d](https://github.com/mauriciolauffer/ui5-deployer/commit/13e1d7d0dee0238e160c81abad108d21cb5b8d16)) 23 | * Support ui5.yaml customConfiguration ([da2272d](https://github.com/mauriciolauffer/ui5-deployer/commit/da2272d3a7a948f2a33812ab93ed085014af2557)) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * Bump dependencies ([774a402](https://github.com/mauriciolauffer/ui5-deployer/commit/774a4025cb5d9b24d1a5bd29d48770e8527c7088)) 29 | * Do not encode BSP name when creating ([e77b89a](https://github.com/mauriciolauffer/ui5-deployer/commit/e77b89a730f7aa988392e491c5b249730bb92137)) 30 | * Encode BSP App ([0f36cd7](https://github.com/mauriciolauffer/ui5-deployer/commit/0f36cd703a0d2e58b125968b6f41d8292f2c62f6)) 31 | * Get parameters after customConfiguration ([f4ca57e](https://github.com/mauriciolauffer/ui5-deployer/commit/f4ca57eccc0c6030eb5b943495c94a87696ff1d3)) 32 | * jsdoc annotations ([84b5c4e](https://github.com/mauriciolauffer/ui5-deployer/commit/84b5c4e755915319b1d38aecb51f53220f42a0d3)) 33 | * lint fix ([d89832b](https://github.com/mauriciolauffer/ui5-deployer/commit/d89832b918f6305ec83960aca3ab898921257df2)) 34 | * Missing isbinaryfile dependency ([#28](https://github.com/mauriciolauffer/ui5-deployer/issues/28)) ([6846e23](https://github.com/mauriciolauffer/ui5-deployer/commit/6846e23abbd04a49a8e7cf1409652edba94d45e9)) 35 | * Remove package-lock.json ([a1b42f8](https://github.com/mauriciolauffer/ui5-deployer/commit/a1b42f8f7208f76ddea267e8f8095a0f91cea0a9)) 36 | 37 | ## [0.3.0](https://github.com/mauriciolauffer/ui5-deployer/compare/v0.2.2...v0.3.0) (2022-04-22) 38 | 39 | 40 | ### Features 41 | 42 | * CLI is not an extra pkg + replace requestjs ([#14](https://github.com/mauriciolauffer/ui5-deployer/issues/14)) ([0538fed](https://github.com/mauriciolauffer/ui5-deployer/commit/0538fed9c5f7415eeffa453e8b735e2b28ad8956)) 43 | * Deploy via OData /UI5/ABAP_REPOSITORY_SRV ([#26](https://github.com/mauriciolauffer/ui5-deployer/issues/26)) ([c9d6eaf](https://github.com/mauriciolauffer/ui5-deployer/commit/c9d6eafb2d70fbbd6cec08c6fefc90e5628bf3dc)) 44 | * ENV vars for NEO cli ([#20](https://github.com/mauriciolauffer/ui5-deployer/issues/20)) ([9fb9ddf](https://github.com/mauriciolauffer/ui5-deployer/commit/9fb9ddf9c01bb30d64bdee7d7c6141fc7f69a9b3)) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * Bump dependencies ([774a402](https://github.com/mauriciolauffer/ui5-deployer/commit/774a4025cb5d9b24d1a5bd29d48770e8527c7088)) 50 | * Missing isbinaryfile dependency ([#28](https://github.com/mauriciolauffer/ui5-deployer/issues/28)) ([6846e23](https://github.com/mauriciolauffer/ui5-deployer/commit/6846e23abbd04a49a8e7cf1409652edba94d45e9)) 51 | -------------------------------------------------------------------------------- /lib/types/sap-netweaver/SapNetWeaverDeployer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AdtClient = require('./AdtClient'); 4 | const AdtResourceManager = require('./AdtResourceManager'); 5 | const ODataClient = require('./ODataClient'); 6 | const ODataResourceManager = require('./ODataResourceManager'); 7 | const AbstractDeployer = require('../AbstractDeployer'); 8 | 9 | /** 10 | * Deployer Class for SAP NetWeaver 11 | * 12 | * @augments AbstractDeployer 13 | */ 14 | class SapNetWeaverDeployer extends AbstractDeployer { 15 | /** 16 | * Deploys the project to a remote SAP NetWeaver server 17 | * 18 | * @returns {Promise} Returns promise with deployment results 19 | */ 20 | async deploy() { 21 | if (this.project.deployer.abapRepository.method === 'odata') { 22 | return this.deployByODataMethod(); 23 | } else { 24 | return this.deployByAdtMethod(); 25 | } 26 | } 27 | 28 | /** 29 | * Deploy to remote server via OData 30 | * 31 | * @returns {Promise} Returns promise with deployment results 32 | */ 33 | async deployByODataMethod() { 34 | const odataClient = this.buildODataClient({ 35 | project: this.project, 36 | parentLogger: this.logger 37 | }); 38 | const odataResourceManager = this.buildODataResourceManager({ 39 | project: this.project, 40 | parentLogger: this.logger, 41 | workspace: this.resourceCollections.workspace 42 | }); 43 | await odataClient.connect(); 44 | const localResources = await this.getLocalResources(); 45 | const archivePath = await odataResourceManager.prepareResources(localResources); 46 | return odataClient.syncRemoteServer(archivePath); 47 | } 48 | 49 | /** 50 | * Deploy to remote server via ADT 51 | * 52 | * @returns {Promise} Returns promise with deployment results 53 | */ 54 | async deployByAdtMethod() { 55 | const adtClient = this.buildAdtClient({ 56 | project: this.project, 57 | parentLogger: this.logger 58 | }); 59 | const adtResourceManager = this.buildAdtResourceManager({ 60 | adtClient, 61 | project: this.project 62 | }); 63 | await adtClient.connect(); 64 | const localResources = await this.getLocalResources(); 65 | await adtResourceManager.saveResources(localResources); 66 | return adtClient.appIndexCalculation(); 67 | } 68 | 69 | /** 70 | * Builds an instance of the ADT Client 71 | * 72 | * @param {object} parameters Parameters 73 | * @param {object} parameters.project Project 74 | * @param {object} parameters.parentLogger Parent logger 75 | * @returns {AdtClient} Returns ADT Client 76 | */ 77 | buildAdtClient({project, parentLogger}) { 78 | return new AdtClient({project, parentLogger}); 79 | } 80 | 81 | /** 82 | * Builds an instance of the ADT Resource Manager 83 | * 84 | * @param {object} parameters Parameters 85 | * @param {object} parameters.adtClient SAP ADT Client 86 | * @param {object} parameters.project Project 87 | * @returns {AdtResourceManager} Returns ADT Resource Manager 88 | */ 89 | buildAdtResourceManager({adtClient, project}) { 90 | return new AdtResourceManager({adtClient, project}); 91 | } 92 | 93 | /** 94 | * Builds an instance of the OData Client 95 | * 96 | * @param {object} parameters Parameters 97 | * @param {object} parameters.project Project 98 | * @param {object} parameters.parentLogger Parent logger 99 | * @returns {ODataClient} Returns OData Client 100 | */ 101 | buildODataClient({project, parentLogger}) { 102 | return new ODataClient({project, parentLogger}); 103 | } 104 | 105 | /** 106 | * Builds an instance of the OData Resource Manager 107 | * 108 | * @param {object} parameters Parameters 109 | * @param {object} parameters.project Project 110 | * @param {object} parameters.parentLogger Parent logger 111 | * @param {object} parameters.workspace Workspace 112 | * @returns {ODataResourceManager} Returns OData Resource Manager 113 | */ 114 | buildODataResourceManager({project, parentLogger, workspace}) { 115 | return new ODataResourceManager({project, parentLogger, workspace}); 116 | } 117 | } 118 | 119 | module.exports = SapNetWeaverDeployer; 120 | -------------------------------------------------------------------------------- /lib/deployer/deployer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const logger = require('@ui5/logger').getGroupLogger('deployer:deployer'); 4 | const {resourceFactory} = require('@ui5/fs'); 5 | const typeRepository = require('../types/typeRepository'); 6 | 7 | require('dotenv').config(); 8 | 9 | /** 10 | * Calculates the elapsed deploy time and returns a prettified output 11 | * 12 | * @private 13 | * @param {Array[number]} startTime Array provided by process.hrtime() 14 | * @returns {string} Difference between now and the provided time array as formatted string 15 | */ 16 | function getElapsedTime(startTime) { 17 | const prettyHrtime = require('pretty-hrtime'); 18 | const timeDiff = process.hrtime(startTime); 19 | return prettyHrtime(timeDiff); 20 | } 21 | 22 | /** 23 | * Set configuration from ENV 24 | * 25 | * @param {object} deployerConfig Configuration 26 | * @param {object} deployerConfig.credentials Credentials 27 | * @param {object} deployerConfig.abapRepository ABAP target 28 | * @param {object} deployerConfig.sapCloudPlatform SAP Cloud Platform target 29 | */ 30 | function setPropertiesWithEnv(deployerConfig) { 31 | if (process.env.UI5_DEPLOYER_USERNAME) { 32 | deployerConfig.credentials.username = process.env.UI5_DEPLOYER_USERNAME; 33 | } 34 | if (process.env.UI5_DEPLOYER_PASSWORD) { 35 | deployerConfig.credentials.password = process.env.UI5_DEPLOYER_PASSWORD; 36 | } 37 | if (process.env.UI5_DEPLOYER_ABAP_TR && deployerConfig.abapRepository) { 38 | deployerConfig.abapRepository.transportRequest = process.env.UI5_DEPLOYER_ABAP_TR; 39 | } 40 | if (process.env.UI5_DEPLOYER_NEO_CLIPATH && deployerConfig.sapCloudPlatform && deployerConfig.sapCloudPlatform.neo) { 41 | deployerConfig.sapCloudPlatform.neo.cliPath = process.env.UI5_DEPLOYER_NEO_CLIPATH; 42 | } 43 | if (process.env.UI5_DEPLOYER_CF_SPACE && deployerConfig.sapCloudPlatform && deployerConfig.sapCloudPlatform.cloudFoundry) { 44 | deployerConfig.sapCloudPlatform.cloudFoundry.space = process.env.UI5_DEPLOYER_CF_SPACE; 45 | } 46 | } 47 | 48 | /** 49 | * Set configuration from CLI 50 | * 51 | * @param {object} deployerConfig Configuration 52 | * @param {string} transportRequest Transport Request 53 | * @param {string} username Username 54 | * @param {string} password Password 55 | * @param {string} space Cloud Foundry Space 56 | */ 57 | function setPropertiesWithCLI(deployerConfig, transportRequest, username, password, space) { 58 | if (transportRequest && deployerConfig.abapRepository) { 59 | deployerConfig.abapRepository.transportRequest = transportRequest; 60 | } 61 | if (username) { 62 | deployerConfig.credentials.username = username; 63 | } 64 | if (password) { 65 | deployerConfig.credentials.password = password; 66 | } 67 | if (space && deployerConfig.sapCloudPlatform && deployerConfig.sapCloudPlatform.cloudFoundry) { 68 | deployerConfig.sapCloudPlatform.cloudFoundry.space = space; 69 | } 70 | } 71 | 72 | /** 73 | * Get files to be ignored 74 | * 75 | * @param {object} deployerConfig Configuration 76 | * @param {object} deployerConfig.resources Resources 77 | * @param {string} deployerConfig.sourcePath Source path 78 | * @returns {string[]} Files to be ignored 79 | */ 80 | function getExclusions(deployerConfig) { 81 | if (deployerConfig.resources && deployerConfig.resources.excludes) { 82 | return deployerConfig.resources.excludes 83 | .map((excludedPath) => excludedPath.replace(deployerConfig.sourcePath, '/')); 84 | } else { 85 | return []; 86 | } 87 | } 88 | 89 | /** 90 | * Deployer 91 | * 92 | * @public 93 | * @namespace 94 | * @alias module:ui5-deployer.deployer 95 | */ 96 | module.exports = { 97 | /** 98 | * Configures the project deploy and starts it. 99 | * 100 | * @public 101 | * @param {object} parameters Parameters 102 | * @param {object} parameters.tree Dependency tree 103 | * @param {string} parameters.transportRequest ABAP Transport Request 104 | * @param {string} parameters.username Username to log into the target system 105 | * @param {string} parameters.password Password to log into the target system 106 | * @param {string} parameters.space Cloud Foundry space 107 | * @returns {Promise} Promise resolving to undefined once deploy has finished 108 | */ 109 | async deploy({tree, transportRequest, username, password, space}) { 110 | logger.info(`Deploying project ${tree.metadata.name}`); 111 | const startTime = process.hrtime(); 112 | const project = tree; 113 | if (parseFloat(tree.specVersion) > 2 || (tree.customConfiguration && tree.customConfiguration.deployer)) { 114 | project.deployer = tree.customConfiguration.deployer; 115 | } 116 | if (!project.deployer.credentials) { 117 | project.deployer.credentials = {}; 118 | } 119 | setPropertiesWithEnv(project.deployer); 120 | setPropertiesWithCLI(project.deployer, transportRequest, username, password, space); 121 | const excludes = getExclusions(project.deployer); 122 | const projectType = typeRepository.getType(project.deployer.type); 123 | const workspace = resourceFactory.createAdapter({ 124 | fsBasePath: './' + project.deployer.sourcePath, 125 | virBasePath: '/', 126 | excludes: excludes 127 | }); 128 | 129 | try { 130 | await projectType.deploy({ 131 | resourceCollections: { 132 | workspace 133 | }, 134 | project, 135 | parentLogger: logger 136 | }); 137 | logger.verbose('Finished deploying project %s. Writing out files...', project.metadata.name); 138 | logger.info(`Deploy succeeded in ${getElapsedTime(startTime)}`); 139 | } catch (err) { 140 | logger.error(err); 141 | logger.error(`Deploy failed in ${getElapsedTime(startTime)}`); 142 | throw err; 143 | } 144 | } 145 | }; 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This is a custom module integrated with ui5-tooling to deploy Fiori/UI5 apps to SAP environments. This module is under development and it is not part of the official SAP ui5-tooling. For now, it uses a custom version of ui5-cli for deployment capabilities. It's heavily inspired on ui5-build. 4 | 5 | Feel free to contribute to the project. PRs are welcome =) 6 | 7 | ## Deploy to 8 | 9 | You should be able to deploy to: 10 | 11 | - SAP Netweaver: ABAP server (via OData or ADT) 12 | - SAP Cloud Platform: NEO environment 13 | - SAP Cloud Platform: Cloud Foundry environment 14 | 15 | ## Project Configuration 16 | 17 | Typically located in a ui5.yaml file per project. You have 3 options for remote systems. They share some configuration, but each one has its own specific details. 18 | 19 | This is the basic setup for ui5-deployer shared across all remote system options, it does not include the specific details for a given remote system. Some of the properties are not required, some are. 20 | 21 | ### Properties 22 | 23 | #### *\* 24 | 25 | The ui5.yaml file from your Fiori/UI5 application/library should have a new section called **deployer**. Deployer section has the following parameters: 26 | 27 | #### ui5.yaml file for *deployer* 28 | 29 | `deployer`: root attribute, all deployer details go under it 30 | - `type`: Indicates the remote system where the project will be deployed. Must be `sap-netweaver` || `sap-cp-neo` || `sap-cp-cf` 31 | - `sourcePath`: Path to the folder where your production ready project is 32 | - `connection`: Connection details to the remote system 33 | - `url`: URL endpoint to connect to the remote system 34 | - `proxy` (optional): an HTTP proxy to be used 35 | - `strictSSL` (optional): if true, requires SSL certificates to be valid. Note: to use your own certificate authority, you need to specify the path to the SSL Certificate. Must be `true` || `false`. Default is `false`. 36 | - `SSLCertificatePath` (optional): path to the SSL Certificate in case you are using your own certificate authority 37 | - `credentials`: Credentials to be used when accessing the remote system. This section will be removed soon as it might be a huge security issue. One might end up pushing username/password to the git repo. Username/password should be passed via CLI command only. 38 | - `username`: Username 39 | - `password`: Password 40 | - `sapCloudPlatform` (optional): SAP Cloud Platform target 41 | - `neo` (optional): NEO environment target 42 | - `account`: SAP CP NEO Account 43 | - `cliPath` (optional): In case neo CLI is not global, inform the path to it. 44 | - `cloudFoundry` (optional): CF environment target 45 | - `org`: Organization 46 | - `space`: Space 47 | - `cliPath` (optional): In case cf CLI is not global, inform the path to it. 48 | - `abapRepository` (optional): SAP NetWeaver ABAP Repository target 49 | - `client`: SAP client/mandt 50 | - `language`: SAP Logon Language 51 | - `transportRequest`: ABAP Transport Request Number 52 | - `package`: ABAP Package 53 | - `bspApplication`: BSP Application name 54 | - `bspApplicationText`: BSP Application description 55 | - `method`: `adt` || `odata`. Default is `adt`. ADT API endpoint is `/sap/bc/adt`. OData API endpoint is `/sap/opu/odata/UI5/ABAP_REPOSITORY_SRV`. ADT is the default to avoid breaking old projects. However, OData is recommended for better performance, it doesn't send multiples files, it sends just a single ***.zip** containing the whole project. 56 | - `skipAdtValidations` (optional): Does not validate the existence of some ADT APIs, ABAP packages and Transport Requests used during deployment. Used for older ABAP versions where these ADT APIs are not available. Must be `true` || `false`. Default is `false`. 57 | - `appIndexCalculate` (optional): Calculation of SAPUI5 Application Index for SAPUI5 Repositories (/UI5/APP_INDEX_CALCULATE). See [SAPUI5 Application Index](https://sapui5.hana.ondemand.com/#/topic/c5e7098474274d3eb7379047ab792f1f). Must be `true` || `false`. Default is `false`. 58 | 59 | ## SAP Netweaver: ABAP server 60 | 61 | ```yml 62 | specVersion: '1.0' 63 | metadata: 64 | name: ui5-deployer-app-test 65 | type: application 66 | customConfiguration: 67 | deployer: 68 | type: sap-netweaver 69 | sourcePath: dist/ # Path to the project to be deployed 70 | resources: 71 | excludes: 72 | - "dist/path_to_excluded/**" 73 | connection: 74 | url: https://dev.my-sap-server.com 75 | proxy: https://my.proxy.com:43000 76 | strictSSL: true 77 | SSLCertificatePath: /certs/my-ssl-certificate.pem 78 | credentials: 79 | username: MyUsername 80 | password: MyPassword 81 | abapRepository: 82 | client: 100 83 | language: EN 84 | transportRequest: ABAPDK999999 85 | package: ZMYPACKAGE 86 | bspApplication: ZDEPLOYAPP001 87 | bspApplicationText: TEST DEPLOY APP x1 88 | method: odata 89 | skipAdtValidations: true 90 | appIndexCalculate: true 91 | ``` 92 | 93 | ## SAP Cloud Platform: NEO environment 94 | 95 | ```yml 96 | specVersion: '1.0' 97 | metadata: 98 | name: ui5-deployer-app-test 99 | type: application 100 | customConfiguration: 101 | deployer: 102 | type: sap-cp-neo 103 | sourcePath: /dist/*.mtar # Path to the .mtar file to be deployed 104 | connection: 105 | url: https://hanatrial.ondemand.com 106 | credentials: 107 | username: MyUsername 108 | password: MyPassword 109 | sapCloudPlatform: 110 | neo: 111 | account: myNEO12345Account 112 | cliPath: C:\neo-java-web-sdk\tools 113 | ``` 114 | 115 | ## SAP Cloud Platform: Cloud Foundry environment 116 | 117 | ```yml 118 | specVersion: '1.0' 119 | metadata: 120 | name: ui5-deployer-app-test 121 | type: application 122 | customConfiguration: 123 | deployer: 124 | type: sap-cp-cf 125 | sourcePath: /dist # Path to the manifest.yml file: https://docs.cloudfoundry.org/devguide/deploy-apps/manifest.html 126 | connection: 127 | url: https://api.cf.eu10.hana.ondemand.com 128 | credentials: 129 | username: MyUsername 130 | password: MyPassword 131 | sapCloudPlatform: 132 | cloudFoundry: 133 | org: myORG 134 | space: mySPACE 135 | cliPath: C:\cf-cli\tools 136 | ``` 137 | 138 | ### For projects using ui5.yaml specVersion 2.1 or higher 139 | 140 | Projects using ui5.yaml specVersion 2.1 or higher must use the new `customConfiguration` property. 141 | https://sap.github.io/ui5-tooling/pages/Configuration/#custom-configuration 142 | 143 | PS: you can also use the `customConfiguration` setup for any other specVersion, but 2.0 144 | 145 | ```yml 146 | specVersion: '2.1' 147 | metadata: 148 | name: ui5-deployer-app-test 149 | type: application 150 | customConfiguration: 151 | deployer: 152 | type: sap-netweaver 153 | sourcePath: dist/ # Path to the project to be deployed 154 | connection: 155 | url: https://dev.my-sap-server.com 156 | strictSSL: false 157 | credentials: 158 | username: MyUsername 159 | password: MyPassword 160 | abapRepository: 161 | client: 100 162 | language: EN 163 | transportRequest: ABAPDK999999 164 | package: ZMYPACKAGE 165 | bspApplication: ZDEPLOYAPP001 166 | bspApplicationText: TEST DEPLOY APP x1 167 | ``` 168 | 169 | ## Installing 170 | 171 | Install ui5-deployer as a devDependency in your project.json 172 | 173 | ```shell script 174 | $ npm i --save-dev ui5-deployer 175 | ``` 176 | 177 | ## Getting Started 178 | 179 | Pick one of the remote systems above and edit your ui5.yaml file according to it. 180 | 181 | You have the option to use all parameters as is from the ui5.yaml file or overwrite few of them when executing ui5-cli. 182 | 183 | You can overwrite: `abapRepository.transportRequest` || `credentials.username` || `credentials.password` || `sapCloudPlatform.cloudFoundry.space` 184 | 185 | ```shell script 186 | $ ui5-deployer deploy 187 | ``` 188 | 189 | ```shell script 190 | $ ui5-deployer deploy --transport-request=ABAPDK99999 191 | ``` 192 | 193 | ```shell script 194 | $ ui5-deployer deploy --username=MyUsername --password=MyPassword 195 | ``` 196 | 197 | ```shell script 198 | $ ui5-deployer deploy --space=dev 199 | ``` 200 | 201 | You can see an example here: 202 | 203 | 204 | The modified ui5-cli can be found here: 205 | 206 | ### Support to Environment Variables 207 | 208 | The aforementioned properties can also be set via Environment Variables. This option follows the [Twelve-Factor App](http://12factor.net/config) best practices. The tool also supports `.env` files. 209 | 210 | The expected Environment Variables are: 211 | 212 | ```dosini 213 | UI5_DEPLOYER_USERNAME=MY_SAP_USER 214 | UI5_DEPLOYER_PASSWORD=MY_SAP_PASSWORD 215 | UI5_DEPLOYER_ABAP_TR=ABAPDK999999 216 | UI5_DEPLOYER_NEO_CLIPATH=/path/to/neo/cli/ 217 | UI5_DEPLOYER_CF_SPACE=dev 218 | ``` 219 | 220 | If you are using `.env` files, do not push them to your git repo as you may expose the secrets to everbody! Make sure to add `.env` to your `.gitignore` file. 221 | 222 | ## Build and Test 223 | 224 | TODO: Describe and show how to build your code and run the tests. 225 | -------------------------------------------------------------------------------- /lib/types/sap-netweaver/AdtResourceManager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {XmlDocument} = require('xmldoc'); 4 | const ESCAPED_FORWARDSLASH = '%2f'; 5 | 6 | /** 7 | * Returns resources to be updated 8 | * 9 | * @param {Array} localResources Local resources 10 | * @param {Array} remoteResources Remote resources 11 | * @returns {Array} Local resources to be updated in the remote server 12 | */ 13 | function getResourcesToBeUpdated(localResources = [], remoteResources = []) { 14 | return localResources.filter((local) => remoteResources.find((remote) => { 15 | // eslint-disable-next-line security/detect-non-literal-regexp 16 | const regxp = new RegExp(remote + '$'); 17 | return regxp.test(local); 18 | })); 19 | } 20 | 21 | /** 22 | * Returns resources to be created 23 | * 24 | * @param {Array} localResources Local resources 25 | * @param {Array} remoteResources Remote resources 26 | * @returns {Array} Local resources to be created in the remote server 27 | */ 28 | function getResourcesToBeCreated(localResources = [], remoteResources = []) { 29 | return localResources.filter((local) => { 30 | const foundIndex = remoteResources.findIndex((remote) => { 31 | // eslint-disable-next-line security/detect-non-literal-regexp 32 | const regxp = new RegExp(remote + '$'); 33 | return regxp.test(local); 34 | }); 35 | return foundIndex < 0; 36 | }); 37 | } 38 | 39 | /** 40 | * Returns resources to be deleted 41 | * 42 | * @param {Array} localResources Local resources 43 | * @param {Array} remoteResources Remote resources 44 | * @returns {Array} Remote resources to be delete from the remote server 45 | */ 46 | function getResourcesToBeDeleted(localResources = [], remoteResources = []) { 47 | return remoteResources.filter((remote) => { 48 | const foundIndex = localResources.findIndex((local) => { 49 | // eslint-disable-next-line security/detect-non-literal-regexp 50 | const regxp = new RegExp(remote + '$'); 51 | return regxp.test(local); 52 | }); 53 | return foundIndex < 0; 54 | }); 55 | } 56 | 57 | /** 58 | * Returns array of folders sorted from the highest level to the lowest 59 | * 60 | * @param {Array} a Array 61 | * @param {Array} b Array 62 | * @returns {number} Array sorted from root to bottom 63 | */ 64 | function sortFromRootToBottomFolder(a, b) { 65 | return a.split('/').length - b.split('/').length; 66 | } 67 | 68 | /** 69 | * Returns array of folders sorted from the lowest level to the highest 70 | * 71 | * @param {Array} a Array 72 | * @param {Array} b Array 73 | * @returns {number} Array sorted from bottom to the root 74 | */ 75 | function sortFromBottomToRootFolder(a, b) { 76 | return b.split('/').length - a.split('/').length; 77 | } 78 | 79 | /** 80 | * Class to handle project's resources 81 | */ 82 | class AdtResourceManager { 83 | /** 84 | * Constructor 85 | * 86 | * @param {object} parameters Parameters 87 | * @param {object} parameters.adtClient ADT Client 88 | * @param {object} parameters.project Project configuration 89 | */ 90 | constructor({adtClient, project}) { 91 | this._adtClient = adtClient; 92 | this._project = project; 93 | this._localResources = []; 94 | this._localFolders = []; 95 | this._localFiles = []; 96 | this._remoteFolders = []; 97 | this._remoteFiles = []; 98 | this.crud = { 99 | files: {create: [], update: [], delete: []}, 100 | folders: {create: [], update: [], delete: []} 101 | }; 102 | } 103 | 104 | /** 105 | * Save local resources into a remote server 106 | * 107 | * @param {Array} localResources Local resources 108 | */ 109 | async saveResources(localResources) { 110 | this._localResources = localResources; 111 | const path = encodeURIComponent(this._project.deployer.abapRepository.bspApplication); 112 | await this._getRemoteResources(path); 113 | this._getLocalResources(); 114 | this._defineCrudOperations(); 115 | await this._syncResources(); 116 | } 117 | 118 | /** 119 | * Gets all remote resources, folders and files 120 | * 121 | * @param {string} path Path to fetch the resources from 122 | */ 123 | async _getRemoteResources(path) { 124 | return this._adtClient.getResources(path) 125 | .then((response) => { 126 | // eslint-disable-next-line security/detect-non-literal-regexp 127 | const regex = new RegExp(ESCAPED_FORWARDSLASH, 'g'); 128 | const bspApplication = this._project.deployer.abapRepository.bspApplication; 129 | const xml = new XmlDocument(response.body); 130 | const xmlNodes = xml.childrenNamed('atom:entry'); 131 | const folderNodes = xmlNodes.filter((node) => node.valueWithPath('atom:category@term') === 'folder'); 132 | const fileNodes = xmlNodes.filter((node) => node.valueWithPath('atom:category@term') === 'file'); 133 | const remoteFiles = fileNodes.map((node) => node.valueWithPath('atom:id').replace(regex, '/').replace(bspApplication, '')); 134 | const remoteFolders = folderNodes.map((node) => node.valueWithPath('atom:id').replace(regex, '/').replace(bspApplication, '')); 135 | this._remoteFiles.push(...remoteFiles); 136 | this._remoteFolders.push(...remoteFolders); 137 | return Promise.all( 138 | folderNodes.map((folder) => this._getRemoteResources(folder.valueWithPath('atom:id'))) 139 | ); 140 | }); 141 | } 142 | 143 | /** 144 | * Gets local resources, folders and files 145 | */ 146 | _getLocalResources() { 147 | this._localFiles = this._getLocalFiles(); 148 | this._localFolders = this._getLocalFolders(); 149 | } 150 | 151 | /** 152 | * Returns local files 153 | * 154 | * @returns {Array} Local files 155 | */ 156 | _getLocalFiles() { 157 | return this._localResources.map((resource) => resource._path); 158 | } 159 | 160 | /** 161 | * Returns local folders 162 | * 163 | * @returns {Array} Local remotes 164 | */ 165 | _getLocalFolders() { 166 | const folders = []; 167 | this._localResources.map((resource) => { 168 | const pathParts = resource._path.split('/'); 169 | for (let i = 0; pathParts.length > 2; i++) { 170 | pathParts.pop(); 171 | folders.push(pathParts.join('/')); 172 | } 173 | }); 174 | return [...new Set(folders)]; 175 | } 176 | 177 | /** 178 | * Defines CRUD operations to be executed for all resources (local and remote) 179 | */ 180 | _defineCrudOperations() { 181 | this._localFiles.sort().sort(sortFromRootToBottomFolder); 182 | this._localFolders.sort().sort(sortFromRootToBottomFolder); 183 | this._remoteFiles.sort().sort(sortFromRootToBottomFolder); 184 | this._remoteFolders.sort().sort(sortFromRootToBottomFolder); 185 | this._defineCrudOperationForFolders(); 186 | this._defineCrudOperationForFiles(); 187 | } 188 | 189 | /** 190 | * Defines CRUD operations to be executed for all folders 191 | */ 192 | _defineCrudOperationForFolders() { 193 | this.crud.folders.create = getResourcesToBeCreated(this._localFolders, this._remoteFolders); 194 | this.crud.folders.update = getResourcesToBeUpdated(this._localFolders, this._remoteFolders); 195 | this.crud.folders.delete = getResourcesToBeDeleted(this._localFolders, this._remoteFolders); 196 | this.crud.folders.delete.sort(sortFromBottomToRootFolder); 197 | } 198 | 199 | /** 200 | * Defines CRUD operations to be executed for all files 201 | */ 202 | _defineCrudOperationForFiles() { 203 | this.crud.files.create = getResourcesToBeCreated(this._localFiles, this._remoteFiles); 204 | this.crud.files.update = getResourcesToBeUpdated(this._localFiles, this._remoteFiles); 205 | this.crud.files.delete = getResourcesToBeDeleted(this._localFiles, this._remoteFiles); 206 | } 207 | 208 | /** 209 | * Executes CRUD operations, synchronizes local and remote resources 210 | */ 211 | async _syncResources() { 212 | await this.deleteRemoteResources(); 213 | await this.updateRemoteResources(); 214 | await this.createRemoteResources(); 215 | } 216 | 217 | /** 218 | * Creates remote resources 219 | */ 220 | async createRemoteResources() { 221 | for (const folder of this.crud.folders.create) { 222 | await this._adtClient.createFolder(folder); 223 | } 224 | for (const file of this.crud.files.create) { 225 | const localResource = this._localResources.find((resource) => file === resource._path); 226 | if (localResource) { 227 | await this._adtClient.createFile(file, await localResource.getBuffer()); 228 | } 229 | } 230 | } 231 | 232 | /** 233 | * Updates remote resources 234 | */ 235 | async updateRemoteResources() { 236 | for (const file of this.crud.files.update) { 237 | const localResource = this._localResources.find((resource) => file === resource._path); 238 | if (localResource) { 239 | await this._adtClient.updateFile(file, await localResource.getBuffer()); 240 | } 241 | } 242 | } 243 | 244 | /** 245 | * Deletes remote resources 246 | */ 247 | async deleteRemoteResources() { 248 | for (const file of this.crud.files.delete) { 249 | await this._adtClient.deleteFile(file); 250 | } 251 | for (const folder of this.crud.folders.delete) { 252 | await this._adtClient.deleteFolder(folder); 253 | } 254 | } 255 | } 256 | 257 | module.exports = AdtResourceManager; 258 | -------------------------------------------------------------------------------- /lib/types/sap-netweaver/ODataClient.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const got = require('got'); 4 | const {CookieJar} = require('tough-cookie'); 5 | const fs = require('fs'); 6 | const {readFile} = fs.promises; 7 | const ODATA_PATH = 'sap/opu/odata/UI5/ABAP_REPOSITORY_SRV'; 8 | const METADATA_PATH = ODATA_PATH + '/$metadata'; 9 | const contentType = { 10 | atomXml: 'application/atom+xml', 11 | octetStream: 'application/octet-stream', 12 | json: 'application/json' 13 | }; 14 | 15 | /** 16 | * Class to handle SAP ABAP Repositories OData APIs 17 | * 18 | */ 19 | class ODataClient { 20 | /** 21 | * Constructor 22 | * 23 | * @param {object} parameters Parameters 24 | * @param {object} parameters.project Project configuration 25 | * @param {object} parameters.parentLogger Logger to use 26 | */ 27 | constructor({project, parentLogger}) { 28 | this.logger = parentLogger; 29 | this.project = project; 30 | this._credentials = project.deployer.credentials; 31 | this._abapRepository = project.deployer.abapRepository; 32 | this._connection = project.deployer.connection; 33 | this._csrfToken = ''; 34 | this._client = this._getDefaultRequest(project.deployer); 35 | } 36 | 37 | /** 38 | * Returns client/request object 39 | * 40 | * @returns {object} client/request object 41 | */ 42 | getClient() { 43 | return this._client; 44 | } 45 | 46 | /** 47 | * Connects to the server and deploys the project 48 | */ 49 | async connect() { 50 | await this._authenticate(this._credentials); 51 | } 52 | 53 | async syncRemoteServer(archivePath) { 54 | const response = await this._getBspApplication(); 55 | const payload = await this.getPayload(archivePath); 56 | if (response && response.body) { 57 | await this._updateBspApplication(payload); 58 | } else { 59 | await this._createBspApplication(payload); 60 | } 61 | } 62 | 63 | /** 64 | * Authenticates given credentials against the backend 65 | * 66 | * @param {object} credentials Credentials 67 | * @param {object} credentials.username Username 68 | * @param {object} credentials.password Password 69 | */ 70 | async _authenticate({username, password}) { 71 | this.logger.info('Connecting to', this._connection.url); 72 | this.logger.info(`OData API: ${ this._connection.url }/${ ODATA_PATH }`); 73 | const authToken = username + ':' + password; 74 | const reqOptions = { 75 | url: METADATA_PATH, 76 | headers: { 77 | 'Authorization': 'Basic ' + Buffer.from(authToken).toString('base64'), 78 | 'x-csrf-token': 'Fetch' 79 | } 80 | }; 81 | const response = await this._client.get(reqOptions); 82 | this._csrfToken = response.headers['x-csrf-token']; 83 | return response; 84 | } 85 | 86 | /** 87 | * Gets ABAP BSP Application 88 | * 89 | * @returns {Promise} HTTP response 90 | */ 91 | async _getBspApplication() { 92 | const path = `${ ODATA_PATH }/Repositories('${ encodeURIComponent(this._abapRepository.bspApplication) }')`; 93 | this.logger.info('Getting BSP Application:', this._abapRepository.bspApplication); 94 | this.logger.info(path); 95 | const reqOptions = { 96 | url: path 97 | }; 98 | try { 99 | return await this._client.get(reqOptions); 100 | } catch (err) { 101 | if (err.response.statusCode !== 404) { 102 | throw err; 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * Creates ABAP BSP Application 109 | * 110 | * @param {object} payload Payload 111 | * @returns {Promise} HTTP response 112 | */ 113 | async _createBspApplication(payload) { 114 | const path = `${ ODATA_PATH }/Repositories`; 115 | this.logger.info('Creating BSP Application', path); 116 | const reqOptions = this.getRequestOptions(); 117 | reqOptions.url = path; 118 | reqOptions.body = payload; 119 | return this._client.post(reqOptions); 120 | } 121 | 122 | /** 123 | * Updates ABAP BSP Application 124 | * 125 | * @param {object} payload Payload 126 | * @returns {Promise} HTTP response 127 | */ 128 | async _updateBspApplication(payload) { 129 | const path = `${ ODATA_PATH }/Repositories('${ encodeURIComponent(this._abapRepository.bspApplication) }')`; 130 | this.logger.info('Updating BSP Application', path); 131 | const reqOptions = this.getRequestOptions(); 132 | reqOptions.url = path; 133 | reqOptions.body = payload; 134 | return this._client.put(reqOptions); 135 | } 136 | 137 | /** 138 | * Builds and returns the payload 139 | * 140 | * @param {string} archivePath Path to read the archive file 141 | * @returns {Promise} Payload containing app details + the archive file content converted to base64 142 | */ 143 | async getPayload(archivePath) { 144 | const archiveFile = await readFile(archivePath, {encoding: 'base64'}); 145 | return [ 146 | '', 150 | `https://ffs.finlync.com/sap/opu/odata/UI5/ABAP_REPOSITORY_SRV/Repositories('${ this._abapRepository.bspApplication }')`, 151 | `Repositories('${ this._abapRepository.bspApplication }')`, 152 | `${ new Date().toISOString() }`, 153 | '', 154 | ``, 155 | '', 156 | '', 157 | `${ this._abapRepository.bspApplication }`, 158 | `${ this._abapRepository.package === null || this._abapRepository.package === void 0 ? void 0 : this._abapRepository.package.toUpperCase() }`, 159 | `${ this._abapRepository.bspApplicationText }`, 160 | `${ archiveFile }`, 161 | '', 162 | '', 163 | '', 164 | '' 165 | ].join(' '); 166 | } 167 | 168 | /** 169 | * Get OData request options 170 | * 171 | * @returns {object} OData request options 172 | */ 173 | getRequestOptions() { 174 | return { 175 | searchParams: { 176 | CodePage: 'UTF8', 177 | CondenseMessagesInHttpResponseHeader: 'X', 178 | format: 'json', 179 | TransportRequest: this._abapRepository.transportRequest 180 | }, 181 | headers: { 182 | 'Content-Type': contentType.atomXml, 183 | 'x-csrf-token': this._csrfToken, 184 | 'type': 'entry', 185 | 'charset': 'UTF8' 186 | } 187 | }; 188 | } 189 | 190 | /** 191 | * Returns a client/request with new default values 192 | * 193 | * @param {object} options Parameters to be set as default for HTTP requests 194 | * @returns {object} Client/Request object 195 | */ 196 | _getDefaultRequest(options = {}) { 197 | const query = {}; 198 | if (options.abapRepository && options.abapRepository.client) { 199 | query['sap-client'] = options.abapRepository.client; 200 | } 201 | if (options.abapRepository && options.abapRepository.language) { 202 | query['sap-language'] = options.abapRepository.language.toUpperCase(); 203 | } 204 | const cookieJar = new CookieJar(); 205 | const reqOptions = { 206 | prefixUrl: options.connection.url, 207 | decompress: true, 208 | cookieJar, 209 | searchParams: query, 210 | retry: 0, 211 | headers: { 212 | accept: '*/*', 213 | Connection: 'keep-alive' 214 | }, 215 | https: { 216 | rejectUnauthorized: !!options.connection.strictSSL 217 | }, 218 | hooks: { 219 | beforeError: [ 220 | (err) => { 221 | this._responseError(err.response); 222 | return err; 223 | } 224 | ] 225 | } 226 | }; 227 | if (options.proxy) { 228 | throw new Error('Proxy not supported at the moment!'); 229 | // TODO: Proxy not supported now, must be reimplemented! 230 | // reqOptions.proxy = options.connection.proxy; 231 | } 232 | if (options.connection.strictSSL && options.connection.SSLCertificatePath) { 233 | // eslint-disable-next-line security/detect-non-literal-fs-filename 234 | reqOptions.https.certificateAuthority = fs.readFileSync(options.connection.SSLCertificatePath); 235 | } 236 | return got.extend(reqOptions); 237 | } 238 | 239 | /** 240 | * Triggers response error 241 | * 242 | * @param {object} response HTTP response 243 | * @throws Will throw an error for failed HTTP responses 244 | */ 245 | _responseError(response) { 246 | try { 247 | this.logger.error(response.statusCode, response.statusMessage); 248 | this.logger.error('Request:'); 249 | this.logger.error(response.request.options.url.href); 250 | this.logger.error('Request headers:'); 251 | this.logger.error(response.request.options.headers); 252 | this.logger.error('Response headers:'); 253 | this.logger.error(response.headers); 254 | if (response.statusCode !== 404) { 255 | this.logger.error('Response body:'); 256 | this.logger.error(response.body); 257 | } 258 | } catch (err) { 259 | this.logger.error('Error in request:'); 260 | this.logger.error(response); 261 | } 262 | } 263 | } 264 | 265 | module.exports = ODataClient; 266 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lib/types/sap-netweaver/AdtClient.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const got = require('got'); 4 | const {CookieJar} = require('tough-cookie'); 5 | const fs = require('fs'); 6 | const {isBinaryFile} = require('isbinaryfile'); 7 | const ADT_PATH = 'sap/bc/adt'; 8 | const BSP_PATH = ADT_PATH + '/filestore/ui5-bsp/objects'; 9 | const CTS_PATH = ADT_PATH + '/cts/transportrequests'; 10 | const CTS_CHECKS_PATH = '/consistencychecks'; 11 | const PACKAGE_PATH = ADT_PATH + '/packages'; 12 | const CONTENT_PATH = '/content'; 13 | const DISCOVERY_PATH = ADT_PATH + '/discovery'; 14 | const APP_INDEX_CALCULATE = 'sap/bc/adt/filestore/ui5-bsp/appindex'; 15 | const escapedForwardSlash = '%2f'; 16 | const fileCharset = 'UTF-8'; 17 | const contentType = { 18 | applicationOctetStream: 'application/octet-stream' 19 | }; 20 | 21 | /** 22 | * Class to handle SAP ABAP Development Tools (ADT) APIs 23 | * 24 | */ 25 | class AdtClient { 26 | /** 27 | * Constructor 28 | * 29 | * @param {object} parameters Parameters 30 | * @param {object} parameters.project Project configuration 31 | * @param {object} parameters.parentLogger Logger to use 32 | */ 33 | constructor({project, parentLogger}) { 34 | this.logger = parentLogger; 35 | this.project = project; 36 | this._credentials = project.deployer.credentials; 37 | this._abapRepository = project.deployer.abapRepository; 38 | this._connection = project.deployer.connection; 39 | this._csrfToken = ''; 40 | this._client = this._getDefaultRequest(project.deployer); 41 | } 42 | 43 | /** 44 | * Returns client/request object 45 | * 46 | * @returns {object} client/request object 47 | */ 48 | getClient() { 49 | return this._client; 50 | } 51 | 52 | /** 53 | * Connects to the server and deploys the project 54 | */ 55 | async connect() { 56 | const response = await this._authenticate(this._credentials); 57 | this._validateAdtDiscovery(response); 58 | await this._getPackage(this._abapRepository.package); 59 | if (this._isLocalPackage(this._abapRepository.package)) { 60 | this._abapRepository.transportRequest = ''; 61 | } else { 62 | await this._getTransportRequest(this._abapRepository.transportRequest); 63 | } 64 | await this._getBspApplication(this._abapRepository.bspApplication); 65 | } 66 | 67 | /** 68 | * Validates ADT discovery XML file 69 | * 70 | * @param {object} response HTTP response 71 | * @throws Will throw an error if ADT doesn't support all needed operations 72 | */ 73 | _validateAdtDiscovery(response) { 74 | if (this._abapRepository.skipAdtValidations) { 75 | this.logger.warn('All ADT validations will be skipped!'); 76 | return; 77 | } 78 | let isValid = true; 79 | const errorMessage = ' not found in discovery!'; 80 | if (!this._validateAdtPath(BSP_PATH, response.body)) { 81 | isValid = false; 82 | this.logger.error(BSP_PATH + errorMessage); 83 | } 84 | if (!this._validateAdtPath(CTS_PATH, response.body)) { 85 | isValid = false; 86 | this.logger.error(CTS_PATH + errorMessage); 87 | } 88 | if (!this._validateAdtPath(PACKAGE_PATH, response.body)) { 89 | isValid = false; 90 | this.logger.error(PACKAGE_PATH + errorMessage); 91 | } 92 | if (!isValid) { 93 | this.logger.error('For more information, check ' + DISCOVERY_PATH); 94 | throw new Error('ADT does not have all required services available!'); 95 | } 96 | } 97 | 98 | /** 99 | * Validates ADT paths used by this class 100 | * 101 | * @param {string} path ADT path to be validated 102 | * @param {string} xml ADT path to be validated 103 | * @returns {boolean} True if the ADT path is valid 104 | */ 105 | _validateAdtPath(path, xml) { 106 | // eslint-disable-next-line security/detect-non-literal-regexp 107 | const regxp = new RegExp(''); 108 | return regxp.test(xml); 109 | } 110 | 111 | /** 112 | * Authenticates given credentials against the backend 113 | * 114 | * @param {object} credentials Credentials 115 | * @param {string} credentials.username Username 116 | * @param {string} credentials.password Password 117 | */ 118 | async _authenticate({username, password}) { 119 | this.logger.info('Connecting to', this._connection.url); 120 | const authToken = username + ':' + password; 121 | const reqOptions = { 122 | url: DISCOVERY_PATH, 123 | headers: { 124 | 'Authorization': 'Basic ' + Buffer.from(authToken).toString('base64'), 125 | 'x-csrf-token': 'Fetch' 126 | } 127 | }; 128 | const response = await this._client.get(reqOptions); 129 | this._csrfToken = response.headers['x-csrf-token']; 130 | return response; 131 | } 132 | 133 | /** 134 | * Gets ABAP Package 135 | * 136 | * @param {string} abapPackage ABAP Package 137 | * @returns {Promise} HTTP response 138 | */ 139 | async _getPackage(abapPackage) { 140 | this.logger.info('Getting ABAP package', PACKAGE_PATH + '/' + abapPackage); 141 | if (this._abapRepository.skipAdtValidations) { 142 | return; 143 | } 144 | const reqOptions = { 145 | url: PACKAGE_PATH + '/' + encodeURIComponent(abapPackage), 146 | headers: { 147 | 'x-csrf-token': this._csrfToken 148 | } 149 | }; 150 | return this._client.get(reqOptions); 151 | } 152 | 153 | /** 154 | * Gets ABAP Transport Request 155 | * 156 | * @param {string} transportRequest ABAP Transport Request 157 | * @returns {Promise} HTTP response 158 | */ 159 | async _getTransportRequest(transportRequest) { 160 | this.logger.info('Getting ABAP Transport Request', CTS_PATH + '/' + transportRequest); 161 | if (this._abapRepository.skipAdtValidations) { 162 | return; 163 | } 164 | const reqOptions = { 165 | url: CTS_PATH + '/' + encodeURIComponent(transportRequest) + CTS_CHECKS_PATH, 166 | headers: { 167 | 'x-csrf-token': this._csrfToken 168 | } 169 | }; 170 | return this._client.post(reqOptions); 171 | } 172 | 173 | /** 174 | * Gets ABAP BSP Application 175 | * 176 | * @param {string} bspApplication ABAP BSP Application 177 | * @returns {Promise} HTTP response 178 | */ 179 | async _getBspApplication(bspApplication) { 180 | this.logger.info('Getting BSP Application', BSP_PATH + '/' + bspApplication); 181 | const reqOptions = { 182 | url: BSP_PATH + '/' + encodeURIComponent(bspApplication) 183 | }; 184 | try { 185 | return await this._client.get(reqOptions); 186 | } catch (err) { 187 | if (err.response.statusCode === 404) { 188 | return await this._createBspApplication(); 189 | } else { 190 | throw err; 191 | } 192 | } 193 | } 194 | 195 | /** 196 | * Creates ABAP BSP Application 197 | * 198 | * @returns {Promise} HTTP response 199 | */ 200 | async _createBspApplication() { 201 | this.logger.info('Creating BSP Application', BSP_PATH + '/' + this._abapRepository.bspApplication); 202 | const path = BSP_PATH + '/%20' + CONTENT_PATH; 203 | const reqOptions = { 204 | url: path, 205 | searchParams: { 206 | type: 'folder', 207 | isBinary: false, 208 | name: this._abapRepository.bspApplication, 209 | description: this._abapRepository.bspApplicationText, 210 | devclass: this._abapRepository.package, 211 | corrNr: this._abapRepository.transportRequest 212 | }, 213 | headers: { 214 | 'Content-Type': contentType.applicationOctetStream, 215 | 'x-csrf-token': this._csrfToken 216 | } 217 | }; 218 | return this._client.post(reqOptions); 219 | } 220 | 221 | /** 222 | * Gets remote resources 223 | * 224 | * @param {string} resourcePath Path to the remote resource 225 | * @returns {Promise} HTTP request 226 | */ 227 | async getResources(resourcePath) { 228 | // eslint-disable-next-line security/detect-non-literal-regexp 229 | const regex = new RegExp(escapedForwardSlash, 'g'); 230 | this.logger.info('Getting files from', resourcePath.replace(regex, '/')); 231 | const reqOptions = { 232 | url: BSP_PATH + '/' + resourcePath + CONTENT_PATH 233 | }; 234 | return this._client.get(reqOptions); 235 | } 236 | 237 | /** 238 | * Creates a remote folder 239 | * 240 | * @param {string} folderPath Folder to be created 241 | * @returns {Promise} HTTP response 242 | */ 243 | async createFolder(folderPath) { 244 | this.logger.info('Creating folder', this._abapRepository.bspApplication + folderPath); 245 | const folderStructure = folderPath.split('/'); 246 | const folderName = folderStructure.pop(); 247 | const path = BSP_PATH + '/' + encodeURIComponent(this._abapRepository.bspApplication) + encodeURIComponent(folderStructure.join('/')) + CONTENT_PATH; 248 | const reqOptions = { 249 | url: path, 250 | searchParams: { 251 | type: 'folder', 252 | isBinary: false, 253 | name: folderName, 254 | devclass: this._abapRepository.package, 255 | corrNr: this._abapRepository.transportRequest 256 | }, 257 | headers: { 258 | 'Content-Type': contentType.applicationOctetStream, 259 | 'x-csrf-token': this._csrfToken, 260 | 'If-Match': '*' 261 | } 262 | }; 263 | return this._client.post(reqOptions); 264 | } 265 | 266 | /** 267 | * Deletes a remote folder 268 | * 269 | * @param {string} folderPath Folder to be deleted 270 | * @returns {Promise} HTTP response 271 | */ 272 | async deleteFolder(folderPath) { 273 | this.logger.info('Deleting folder', this._abapRepository.bspApplication + folderPath); 274 | const path = BSP_PATH + '/' + encodeURIComponent(this._abapRepository.bspApplication) + encodeURIComponent(folderPath) + CONTENT_PATH; 275 | const reqOptions = { 276 | url: path, 277 | searchParams: { 278 | deleteChildren: true, 279 | corrNr: this._abapRepository.transportRequest 280 | }, 281 | headers: { 282 | 'Content-Type': contentType.applicationOctetStream, 283 | 'x-csrf-token': this._csrfToken, 284 | 'If-Match': '*' 285 | } 286 | }; 287 | return this._client.delete(reqOptions); 288 | } 289 | 290 | /** 291 | * Creates remote file 292 | * 293 | * @param {string} filePath File path to be created 294 | * @param {string} fileContent File content to be created 295 | * @returns {Promise} HTTP response 296 | */ 297 | async createFile(filePath, fileContent) { 298 | this.logger.info('Creating file', this._abapRepository.bspApplication + filePath); 299 | const isBinary = await isBinaryFile(fileContent); 300 | const folderStructure = filePath.split('/'); 301 | const fileName = folderStructure.pop(); 302 | const path = BSP_PATH + '/' + encodeURIComponent(this._abapRepository.bspApplication) + encodeURIComponent(folderStructure.join('/')) + CONTENT_PATH; 303 | const reqOptions = { 304 | url: path, 305 | body: (fileContent.length > 0) ? fileContent : ' ', 306 | searchParams: { 307 | type: 'file', 308 | isBinary, 309 | name: fileName, 310 | charset: fileCharset, 311 | devclass: this._abapRepository.package, 312 | corrNr: this._abapRepository.transportRequest 313 | }, 314 | headers: { 315 | 'Content-Type': contentType.applicationOctetStream, 316 | 'x-csrf-token': this._csrfToken, 317 | 'If-Match': '*' 318 | } 319 | }; 320 | return this._client.post(reqOptions); 321 | } 322 | 323 | /** 324 | * Updates remote file 325 | * 326 | * @param {string} filePath File path to be updated 327 | * @param {string} fileContent File content to be updated 328 | * @returns {Promise} HTTP response 329 | */ 330 | async updateFile(filePath, fileContent) { 331 | this.logger.info('Updating file', this._abapRepository.bspApplication + filePath); 332 | const path = BSP_PATH + '/' + encodeURIComponent(this._abapRepository.bspApplication) + encodeURIComponent(filePath) + CONTENT_PATH; 333 | const isBinary = await isBinaryFile(fileContent); 334 | const reqOptions = { 335 | url: path, 336 | body: (fileContent.length > 0) ? fileContent : ' ', 337 | searchParams: { 338 | charset: fileCharset, 339 | isBinary, 340 | corrNr: this._abapRepository.transportRequest 341 | }, 342 | headers: { 343 | 'Content-Type': contentType.applicationOctetStream, 344 | 'x-csrf-token': this._csrfToken, 345 | 'If-Match': '*' 346 | } 347 | }; 348 | return this._client.put(reqOptions); 349 | } 350 | 351 | /** 352 | * Deletes remote file 353 | * 354 | * @param {string} filePath File path to be deleted 355 | * @returns {Promise} HTTP response 356 | */ 357 | async deleteFile(filePath) { 358 | this.logger.info('Deleting file', this._abapRepository.bspApplication + filePath); 359 | const path = BSP_PATH + '/' + encodeURIComponent(this._abapRepository.bspApplication) + encodeURIComponent(filePath) + CONTENT_PATH; 360 | const reqOptions = { 361 | url: path, 362 | searchParams: { 363 | corrNr: this._abapRepository.transportRequest 364 | }, 365 | headers: { 366 | 'Content-Type': contentType.applicationOctetStream, 367 | 'x-csrf-token': this._csrfToken, 368 | 'If-Match': '*' 369 | } 370 | }; 371 | return this._client.delete(reqOptions); 372 | } 373 | 374 | /** 375 | * Calculates Application Index (ABAP report /UI5/APP_INDEX_CALCULATE) 376 | * 377 | * @returns {Promise} HTTP response 378 | */ 379 | async appIndexCalculation() { 380 | if (this._abapRepository.appIndexCalculate) { 381 | this.logger.info('Calculating app index'); 382 | const reqOptions = { 383 | url: APP_INDEX_CALCULATE + '/' + encodeURIComponent(this._abapRepository.bspApplication), 384 | headers: { 385 | 'Content-Type': contentType.applicationOctetStream, 386 | 'x-csrf-token': this._csrfToken 387 | } 388 | }; 389 | return this._client.post(reqOptions); 390 | } 391 | } 392 | 393 | /** 394 | * Checks whether a given ABAP Package is local 395 | * 396 | * @param {string} abapPackage ABAP Package 397 | * @returns {boolean} True if it's a local ABAP Package 398 | */ 399 | _isLocalPackage(abapPackage) { 400 | return abapPackage.substring(0, 1) === '$'; 401 | } 402 | 403 | /** 404 | * Returns a client/request with new default values 405 | * 406 | * @param {object} options Parameters to be set as default for HTTP requests 407 | * @returns {object} Client/Request object 408 | */ 409 | _getDefaultRequest(options = {}) { 410 | const query = {}; 411 | if (options.abapRepository && options.abapRepository.client) { 412 | query['sap-client'] = options.abapRepository.client; 413 | } 414 | if (options.abapRepository && options.abapRepository.language) { 415 | query['sap-language'] = options.abapRepository.language.toUpperCase(); 416 | } 417 | const cookieJar = new CookieJar(); 418 | const reqOptions = { 419 | prefixUrl: options.connection.url, 420 | decompress: true, 421 | cookieJar, 422 | searchParams: query, 423 | retry: 0, 424 | headers: { 425 | accept: '*/*', 426 | Connection: 'keep-alive' 427 | }, 428 | https: { 429 | rejectUnauthorized: !!options.connection.strictSSL 430 | }, 431 | hooks: { 432 | beforeError: [ 433 | (err) => { 434 | this._responseError(err.response); 435 | return err; 436 | } 437 | ] 438 | } 439 | }; 440 | if (options.proxy) { 441 | throw new Error('Proxy not supported at the moment!'); 442 | // TODO: Proxy not supported now, must be reimplemented! 443 | // reqOptions.proxy = options.connection.proxy; 444 | } 445 | if (options.connection.strictSSL && options.connection.SSLCertificatePath) { 446 | // eslint-disable-next-line security/detect-non-literal-fs-filename 447 | reqOptions.https.certificateAuthority = fs.readFileSync(options.connection.SSLCertificatePath); 448 | } 449 | return got.extend(reqOptions); 450 | } 451 | 452 | /** 453 | * Triggers response error 454 | * 455 | * @param {object} response HTTP response 456 | * @throws Will throw an error for failed HTTP responses 457 | */ 458 | _responseError(response) { 459 | try { 460 | this.logger.error(response.statusCode, response.statusMessage); 461 | this.logger.error('Request:'); 462 | this.logger.error(response.request.options.url.href); 463 | this.logger.error('Request headers:'); 464 | this.logger.error(response.request.options.headers); 465 | this.logger.error('Response headers:'); 466 | this.logger.error(response.headers); 467 | this.logger.error('Response body:'); 468 | this.logger.error(response.body); 469 | } catch (err) { 470 | this.logger.error('Error in request:'); 471 | this.logger.error(response); 472 | } 473 | } 474 | } 475 | 476 | module.exports = AdtClient; 477 | --------------------------------------------------------------------------------