├── .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 |
--------------------------------------------------------------------------------