@sap/cds-dk 6.1.1 → 6.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ This project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
8
  The format is based on [Keep a Changelog](http://keepachangelog.com/).
9
9
 
10
+ ## Version 6.1.2 - 2022-08-25
11
+
12
+ ### Changed
13
+ - `cds init` uses latest Maven Java archetype version 1.27.1 for creating Java projects.
14
+
15
+ ### Fixed
16
+ - `cds add helm` fixed JSON Schema for `srv` property in values.yaml
17
+ - `cds add helm` fixed env property errors for `hana_deployer` and `html5_apps_deployer`
18
+ - `cds add data` now creates proper data file names for entites in a `context`, i.e. `sap.common-Countries.csv` instead of `sap-common-Countries.csv`
19
+ - `cds import` now captures the parameter/property of collection type differently and the associated annotations are pulled out of the `items` object of the parameter/property entry in the csn.
20
+ - Remove redundant `console.log()` statement in `cds lint`
21
+ - `cds push` now shows complete error messages from extension validation
22
+ - `cds push` and related commands now show properly formatted error messages and also fail with a non-zero exit code in error situations
23
+
10
24
  ## Version 6.1.1 - 2022-08-11
11
25
 
12
26
  ### Added
@@ -283,7 +283,7 @@ function _log (messages, options) {
283
283
  const { sortMessagesSeverityAware, deduplicateMessages } = cds.compiler
284
284
  deduplicateMessages(messages)
285
285
  messages = sortMessagesSeverityAware (messages)
286
- cds.utils._log (messages, options)
286
+ cds._log (messages, options)
287
287
  }
288
288
 
289
289
  /* eslint no-console:0 */
package/bin/login.js CHANGED
@@ -1,8 +1,8 @@
1
1
  const { bold } = require('../lib/util/term');
2
2
 
3
3
  module.exports = Object.assign(login, {
4
- options: ['--subdomain', '--directory', '--passcode', '--clientid'],
5
- shortcuts: ['-s', '-d', '-p', '-c'],
4
+ options: ['--subdomain', '--directory', '--passcode', '--user', '--clientid'],
5
+ shortcuts: ['-s', '-d', '-p', '-u', '-c'],
6
6
  flags: ['--plain'],
7
7
  get help() {
8
8
  const config = require('../lib/client/settings_manager').SettingsManager.config
@@ -71,10 +71,16 @@ module.exports = Object.assign(login, {
71
71
 
72
72
  Tenant-specific passcode for authentication.
73
73
 
74
- *-u* | *--username* <name>
74
+ *-u* | *--user* <name>[:[<password>]]
75
75
 
76
- Username for authentication with Basic Auth in test scenarios. For security reasons,
77
- username will only be saved for app URLs that look like localhost.
76
+ Username and optionally password for authentication with Basic Auth in test scenarios.
77
+ See https://cap.cloud.sap/docs/guides/multitenancy/#send-subscribe-events for information
78
+ on suitable mock users.
79
+
80
+ When typing the colon but omitting <password>, the password is assumed empty.
81
+
82
+ For security reasons, username will only be saved for app URLs that look like localhost.
83
+ The same is true for an empty password. Non-empty passwords will never be saved.
78
84
 
79
85
  *-c* | *--clientid* <clientid>
80
86
 
@@ -105,8 +111,12 @@ module.exports = Object.assign(login, {
105
111
 
106
112
 
107
113
  async function login ([url], options = {}) {
114
+ const [username, password] = options.user?.split(':') ?? [];
115
+ delete options.user;
108
116
  Object.assign(options, {
109
117
  url,
118
+ username,
119
+ password,
110
120
  tokenStorage: options.plain ? 'plain' : 'keyring',
111
121
  renewLogin: true,
112
122
  saveData: true
package/bin/subscribe.js CHANGED
@@ -36,7 +36,7 @@ async function subscribe([tenant], { to: url, user } = {}) {
36
36
  if (!tenant) throw new Error('Must provide a tenant')
37
37
  if (!url) throw new Error('Must provide an app URL via `--to`') // for now, if we don't infer it from the project
38
38
  if (typeof user !== 'string') throw new Error('Must provide a username')
39
- const [, username, , password = ''] = /([^:]+)(:(.*))?/.exec(user);
39
+ const [username, password = ''] = user.split(':');
40
40
  Logger.log(`Subscribing to ${url} for tenant '${tenant}' with user '${username}'${password === '' ? ' and empty password' : ''}`);
41
41
  const options = { auth: { username, password } };
42
42
  return require('axios').post (url+'/-/cds/deployment/subscribe', { tenant }, options);
package/lib/cds.js CHANGED
@@ -1,9 +1,8 @@
1
- /** @type { import('@sap/cds/apis/cds') } */
1
+ /** @type { import('@sap/cds/lib') } */
2
2
  const cds = module.exports = require(_local ('@sap/cds'))
3
3
 
4
4
  // for internal use in cds-dk only
5
- cds.utils._log = require(_local ('@sap/cds/bin/utils/log'))
6
-
5
+ cds._log = require(_local ('@sap/cds/bin/utils/log'))
7
6
 
8
7
  function _local (id) {
9
8
  return require.resolve (id, {paths:[
@@ -6,6 +6,7 @@ const AuthManager = require('./auth_manager');
6
6
  const BaseCommand = require('./base_command');
7
7
  const { collectFileContent, collectFiles, toUnixPath } = require('./helper/fs_helper');
8
8
  const Logger = require('./helper/logging');
9
+ const { CliError } = require('./helper/errors');
9
10
 
10
11
  const COMMAND_URL_SUFFIX = 'asyncActivate';
11
12
  const COMMAND_URL_SUFFIX_SYNC = 'activate';
@@ -27,7 +28,7 @@ async function checkFinished(jobId, statusUrl, params, finisher) {
27
28
  finisher.finished = true;
28
29
  clearInterval(finisher.timer);
29
30
  if (status === JOB_STATUS.FAILED) {
30
- return finisher.reject(new Error(`Job ${jobId} failed: ${error.message || JSON.stringify(error)}`));
31
+ return finisher.reject(new CliError(`Job ${jobId} failed: ${error.message || JSON.stringify(error)}`));
31
32
  }
32
33
  Logger.log('Extension active');
33
34
  finisher.resolve();
@@ -55,17 +56,17 @@ module.exports = class Activate extends BaseCommand {
55
56
  Logger.log(`Activating extension located at ${params.get('projectFolder')} to URL ${params.get('appUrl')}`);
56
57
  }
57
58
 
58
- if (!await Activate.checkProject(params.get('projectFolder'))) {
59
- throw new Error(params.get('projectFolder') + ' is not a valid extension project');
59
+ if (!await this.checkProject(params.get('projectFolder'))) {
60
+ throw new CliError(params.get('projectFolder') + ' is not a valid extension project');
60
61
  }
61
62
 
62
63
  try {
63
- await Activate.runActivate(injection.cds, params);
64
+ await this.runActivate(injection.cds, params);
64
65
  if (params.get('wsdl')) {
65
- await Activate.runCustomFileUpload(params);
66
+ await this.runCustomFileUpload(params);
66
67
  }
67
68
  } catch (error) {
68
- Activate.handleHttpError(error, params);
69
+ await this.handleHttpError(error, params);
69
70
  }
70
71
  } catch (error) {
71
72
  Logger.error(undefined, error);
@@ -153,7 +154,7 @@ module.exports = class Activate extends BaseCommand {
153
154
  return wsdlFiles;
154
155
  } catch (error) {
155
156
  Logger.error(undefined, error);
156
- throw new Error('No custom files found');
157
+ throw new CliError('No custom files found');
157
158
  }
158
159
  }
159
160
 
@@ -161,7 +162,7 @@ module.exports = class Activate extends BaseCommand {
161
162
  let activationUrl;
162
163
  const isSync = params.get('sync');
163
164
 
164
- return Activate.collectExtensionFiles(cds, params.get('projectFolder'))
165
+ return this.collectExtensionFiles(cds, params.get('projectFolder'))
165
166
  .then(allFiles => {
166
167
  if (allFiles.size === 0) {
167
168
  return null;
@@ -169,8 +170,8 @@ module.exports = class Activate extends BaseCommand {
169
170
 
170
171
  Logger.log('Activating extension...');
171
172
 
172
- const urlSuffix = isSync ? Activate.COMMAND_URL_SUFFIX_SYNC : Activate.COMMAND_URL_SUFFIX;
173
- activationUrl = Activate.getMtxApiUrl(params.get('appUrl'), urlSuffix);
173
+ const urlSuffix = isSync ? this.COMMAND_URL_SUFFIX_SYNC : this.COMMAND_URL_SUFFIX;
174
+ activationUrl = this.getMtxApiUrl(params.get('appUrl'), urlSuffix);
174
175
  const requestOptions = Object.assign({
175
176
  headers: {}
176
177
  }, params.get('reqAuth'));
@@ -189,17 +190,17 @@ module.exports = class Activate extends BaseCommand {
189
190
  const { jobID: jobId, statusPath } = response.data;
190
191
 
191
192
  if (jobId === undefined) {
192
- throw new Error('Activation failed: did not receive job ID');
193
+ throw new CliError('Activation failed: did not receive job ID');
193
194
  }
194
195
 
195
196
  if (!statusPath) {
196
- throw new Error(`Job ${jobId} failed: did not receive job-status path`);
197
+ throw new CliError(`Job ${jobId} failed: did not receive job-status path`);
197
198
  }
198
199
 
199
200
  Logger.debug(`Job ID: ${jobId}`);
200
201
 
201
202
  return new Promise((resolve, reject) => {
202
- const statusUrl = Activate.concatUrls(params.get('appUrl'), statusPath);
203
+ const statusUrl = this.concatUrls(params.get('appUrl'), statusPath);
203
204
  const finisher = {
204
205
  resolve,
205
206
  reject,
@@ -214,7 +215,7 @@ module.exports = class Activate extends BaseCommand {
214
215
  static async runCustomFileUpload(params) {
215
216
  Logger.log(`Uploading custom files to ${params.get('appUrl')}...`);
216
217
 
217
- const customFiles = Activate.collectCustomFiles(params.get('projectFolder'));
218
+ const customFiles = this.collectCustomFiles(params.get('projectFolder'));
218
219
 
219
220
  const customFilesSerialized = [];
220
221
  customFiles.forEach((value, key) => {
@@ -225,7 +226,7 @@ module.exports = class Activate extends BaseCommand {
225
226
  });
226
227
  });
227
228
 
228
- const customFileUrl = Activate.getMtxApiUrl(params.get('appUrl'), Activate.UPDATE_CUSTOM_FILE_URL_SUFFIX);
229
+ const customFileUrl = this.getMtxApiUrl(params.get('appUrl'), this.UPDATE_CUSTOM_FILE_URL_SUFFIX);
229
230
  const requestOptions = Object.assign({
230
231
  headers: {},
231
232
  }, params.get('reqAuth'));
@@ -4,6 +4,7 @@ const axios = require('axios');
4
4
  const Logger = require('./helper/logging');
5
5
  const { SettingsManager } = require('./settings_manager');
6
6
  const { ParamCollection } = require('./params');
7
+ const { CliError } = require('./helper/errors');
7
8
 
8
9
  function addTokenInfo(params, authData) {
9
10
  params.set('token', authData.access_token);
@@ -60,7 +61,7 @@ async function retrieveTokenOrPasscodeUrl(params) {
60
61
  try {
61
62
  response = await axios.get(tokenUrl);
62
63
  } catch (error) {
63
- throw new Error(`Failed to get authentication token: ${error}.\nResponse body: ${util.inspect(error.response && error.response.data)}`);
64
+ throw new CliError(`Failed to get authentication token: ${error}.\nResponse body: ${util.inspect(error.response && error.response.data)}`);
64
65
  }
65
66
  authData = response.data;
66
67
 
@@ -89,7 +90,7 @@ module.exports = class AuthManager {
89
90
  if (params.get('saveData')) {
90
91
  await SettingsManager.saveSettings(params);
91
92
  }
92
- throw new Error('Failed to login: no valid token or passcode provided. ' +
93
+ throw new CliError('Failed to login: no valid token or passcode provided. ' +
93
94
  'Get a passcode' + (params.get('passcodeUrl') ? ' from ' + params.get('passcodeUrl') : '') +
94
95
  ' and supply the code when re-running the command');
95
96
  }
@@ -1,7 +1,7 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs').promises;
3
3
 
4
- const Logger = require('./helper/logging');
4
+ const { CliError } = require('./helper/errors');
5
5
 
6
6
  const MTX_URL_PATH_PREFIX = 'mtx/v1/model/';
7
7
  const CUSTOM_FILE_URL_SUFFIX = 'customTenantContent';
@@ -22,15 +22,13 @@ module.exports = class BaseCommand {
22
22
 
23
23
  switch (error.response && error.response.status) {
24
24
  case 401:
25
- Logger.error('Request failed: authorization might have expired. Please retry with valid passcode' +
25
+ throw new CliError('Request failed: authorization might have expired. Please retry with valid passcode' +
26
26
  (params.has('passcodeUrl') ? ` from ${params.get('passcodeUrl')}` : ''), error);
27
- break;
28
27
  case 404:
29
- Logger.error(`Request failed. Please check application URL ${params.get('appUrl')}`, error);
30
- break;
28
+ throw new CliError(`Request failed. Please check application URL ${params.get('appUrl')}`, error);
31
29
  default:
32
30
  // Including 422 (compilation or native-extension error).
33
- Logger.error(undefined, error);
31
+ throw new CliError(undefined, error);
34
32
  }
35
33
  }
36
34
 
@@ -7,6 +7,7 @@ const BaseCommand = require('./base_command');
7
7
  const { normalizePath, isParent } = require('./helper/fs_helper');
8
8
  const Logger = require('./helper/logging');
9
9
  const Question = require('./helper/question');
10
+ const { CliError } = require('./helper/errors');
10
11
 
11
12
  const COMMAND_URL_SUFFIX = 'content';
12
13
  const CUSTOM_FILE_FOLDERS = ['wsdl'];
@@ -88,7 +89,7 @@ module.exports = class Extend extends BaseCommand {
88
89
 
89
90
  const runOnExistingProject = await Extend.checkProject(params.get('projectFolder'));
90
91
  if (runOnExistingProject && !(params.get('force') || params.get('templates'))) {
91
- throw new Error(folderExists(params.get('projectFolder')));
92
+ throw new CliError(folderExists(params.get('projectFolder')));
92
93
  }
93
94
 
94
95
  try {
@@ -0,0 +1,11 @@
1
+ const Logger = require('./logging');
2
+ class CliError extends Error {
3
+ stack = '';
4
+ constructor(message, error) {
5
+ super(Logger.getMessage(message, error));
6
+ }
7
+ }
8
+
9
+ module.exports = {
10
+ CliError
11
+ };
@@ -17,20 +17,13 @@ module.exports = class Logger {
17
17
  }
18
18
 
19
19
  static getMessage(errorText, error) {
20
- let message = errorText && errorText + '\n' || '';
20
+ let message = errorText ? errorText + '\n' : '';
21
21
  if (!error) {
22
22
  return message;
23
23
  }
24
- if (process.env.DEBUG) {
25
- if ('response' in error && error.response.data) {
26
- const body = error.response.data;
27
- if (body.error && body.error.message) {
28
- message += body.error.message + '\n';
29
- }
30
- }
24
+ message += error.response?.data?.error?.message || error.message;
25
+ if (process.env.DEBUG && error.stack) {
31
26
  message += '\n' + error.stack;
32
- } else {
33
- message += error.message;
34
27
  }
35
28
  return message;
36
29
  }
@@ -1,5 +1,6 @@
1
1
  const Logger = require('./helper/logging');
2
2
  const { escapeRegex } = require('./helper/string_helper');
3
+ const { CliError } = require('./helper/errors');
3
4
 
4
5
 
5
6
  // PARAM BASE TYPES
@@ -104,10 +105,10 @@ class Param {
104
105
  expectedType = typeof this.allowedValues[0];
105
106
  }
106
107
  if (typeof val !== expectedType) {
107
- throw new Error(`Failed to set '${(this.name)}' = ${JSON.stringify(val)}: expected value of type ${expectedType}`);
108
+ throw new CliError(`Failed to set '${(this.name)}' = ${JSON.stringify(val)}: expected value of type ${expectedType}`);
108
109
  }
109
110
  if (this.allowedValues && !(this.allowedValues).includes(val)) {
110
- throw new Error(`Failed to set '${(this.name)}' = ${JSON.stringify(val)}: expected one of ${JSON.stringify(this.allowedValues)}`);
111
+ throw new CliError(`Failed to set '${(this.name)}' = ${JSON.stringify(val)}: expected one of ${JSON.stringify(this.allowedValues)}`);
111
112
  }
112
113
  }
113
114
 
@@ -246,7 +247,7 @@ class ParamCollection {
246
247
  */
247
248
  checkFrozen() {
248
249
  if (this.isFrozen) {
249
- throw new Error('cannot modify read-only parameter collection');
250
+ throw new CliError('cannot modify read-only parameter collection');
250
251
  }
251
252
  }
252
253
 
@@ -267,7 +268,7 @@ class ParamCollection {
267
268
  static mainName(name) {
268
269
  const paramDef = ParamDefs[name];
269
270
  if (!paramDef) {
270
- throw new Error(`Unknown parameter: '${name}'`);
271
+ throw new CliError(`Unknown parameter: '${name}'`);
271
272
  }
272
273
  return paramDef.mainName;
273
274
  }
@@ -7,13 +7,13 @@ const { unpackTarArchive } = require('@sap/cds/lib/utils/resources');
7
7
  const { login } = require('./auth_manager');
8
8
  const BaseCommand = require('./base_command');
9
9
  const Logger = require('./helper/logging');
10
+ const { CliError } = require('./helper/errors');
10
11
 
11
12
  module.exports = class Pull extends BaseCommand {
12
13
 
13
14
  static async run(paramValues) {
14
15
  try {
15
16
  const params = await login(paramValues);
16
-
17
17
  Logger.log(`Pulling base model from ${params.get('appUrl')}, for subdomain '${params.get('subdomain')}', to project folder '${params.get('projectFolder')}'`);
18
18
 
19
19
  const projectFolder = params.get('projectFolder');
@@ -24,8 +24,8 @@ module.exports = class Pull extends BaseCommand {
24
24
  const tgzBuffer = Buffer.from(await this.getTgz(params));
25
25
  return unpackTarArchive(tgzBuffer, targetDir, false);
26
26
 
27
- } catch (mainError) {
28
- Logger.error(undefined, mainError);
27
+ } catch (error) {
28
+ throw new CliError(undefined, error);
29
29
  }
30
30
  }
31
31
 
@@ -41,11 +41,8 @@ module.exports = class Pull extends BaseCommand {
41
41
  const pullUrl = `${(params.get('appUrl'))}/-/cds/extensibility/pull`;
42
42
  const options = { ...params.get('reqAuth'), responseType: 'arraybuffer' };
43
43
 
44
- try {
45
- const response = await axios.post(pullUrl, {}, options);
46
- return response.data;
47
- } catch (error) {
48
- return this.handleHttpError(error, params);
49
- }
44
+ return axios.post(pullUrl, {}, options)
45
+ .then(response => response.data)
46
+ .catch(error => this.handleHttpError(error, params));
50
47
  }
51
48
  }
@@ -5,23 +5,22 @@ const axios = require('axios');
5
5
  const { login } = require('./auth_manager');
6
6
  const BaseCommand = require('./base_command');
7
7
  const Logger = require('./helper/logging');
8
+ const { CliError } = require('./helper/errors');
8
9
 
9
10
  module.exports = class Push extends BaseCommand {
10
11
 
11
12
  static async run(paramValues) {
12
13
  try {
13
14
  const params = await login(paramValues);
14
-
15
15
  const projectFolder = params.get('projectFolder');
16
16
  Logger.log(`Pushing extension from project folder '${projectFolder}', for subdomain '${params.get('subdomain')}', to app URL ${params.get('appUrl')}`);
17
17
 
18
- const tgzPath = join(projectFolder, 'gen', 'extension.tgz');
19
- const tgz = fs.readFileSync(tgzPath, { encoding: 'base64' });
18
+ const tgz = fs.readFileSync(join(projectFolder, 'gen', 'extension.tgz'), { encoding: 'base64' });
20
19
  const extensionName = this.getExtensionName(projectFolder);
21
20
  return this.pushTgz(params, tgz, extensionName);
22
21
 
23
- } catch (mainError) {
24
- Logger.error(undefined, mainError);
22
+ } catch (error) {
23
+ throw new CliError(undefined, error);
25
24
  }
26
25
  }
27
26
 
@@ -29,7 +28,7 @@ module.exports = class Push extends BaseCommand {
29
28
  try {
30
29
  return require(join(projectFolder, 'package.json')).name;
31
30
  } catch (error) {
32
- throw new Error(`Extension project at ${projectFolder} is missing package.json`);
31
+ throw new CliError(`Extension project at ${projectFolder} is missing package.json`);
33
32
  }
34
33
  }
35
34
 
@@ -37,13 +36,10 @@ module.exports = class Push extends BaseCommand {
37
36
  const pushUrl = `${params.get('appUrl')}/-/cds/extensibility/push`;
38
37
  const options = { ...params.get('reqAuth') };
39
38
 
40
- try {
41
- return axios.post(pushUrl, {
42
- extension: tgz,
43
- tag: extensionName
44
- }, options);
45
- } catch (error) {
46
- return this.handleHttpError(error, params);
47
- }
39
+ return axios.post(pushUrl, {
40
+ extension: tgz,
41
+ tag: extensionName
42
+ }, options)
43
+ .catch(error => this.handleHttpError(error, params));
48
44
  }
49
45
  }
@@ -9,6 +9,7 @@ const { getAppFromSuggestions, getAppName, getSubdomain } = require('./helper/cf
9
9
  const isBas = require('./helper/env_helper');
10
10
  const Logger = require('./helper/logging');
11
11
  const Question = require('./helper/question');
12
+ const { CliError } = require('./helper/errors');
12
13
  const { httpSchema, httpsSchema, extractSubdomain, schemaRegex, localhostRegex } = require('./helper/url_helper');
13
14
  const { ParamCollection, Persistence } = require('./params');
14
15
  const { capitalize } = require('./helper/string_helper');
@@ -95,7 +96,7 @@ async function getTokenBaseUrl(params, renewUrl) {
95
96
  return params.get('tokenUrl');
96
97
  }
97
98
  if (!params.has('appUrl')) {
98
- throw new Error('Failed to determine token URL: app URL not given');
99
+ throw new CliError('Failed to determine token URL: app URL not given');
99
100
  }
100
101
  const baseUrl = new URL(params.get('appUrl'));
101
102
  const cleanPathname = pathname => baseUrl.pathname.replace(/\/?$/, pathname);
@@ -116,7 +117,7 @@ async function getTokenBaseUrl(params, renewUrl) {
116
117
  if (e.response?.status === 404) {
117
118
  continue;
118
119
  } else {
119
- throw Error(`Failed with root cause ${e.message}`);
120
+ throw CliError(`Failed with root cause ${e.message}`);
120
121
  }
121
122
  }
122
123
  }
@@ -230,12 +231,12 @@ class SettingsManager {
230
231
  }
231
232
  }
232
233
  if (!params.has('appUrl')) {
233
- throw new Error('App URL not given. Please specify it or log in to Cloud Foundry and repeat this command.');
234
+ throw new CliError('App URL not given. Please specify it or log in to Cloud Foundry and repeat this command.');
234
235
  }
235
236
  if (!params.has('subdomain')) {
236
237
  const subdomain = extractSubdomain(params.get('appUrl'));
237
238
  if (!subdomain) {
238
- throw new Error('Subdomain not given');
239
+ throw new CliError('Subdomain not given');
239
240
  }
240
241
  params.set('subdomain', subdomain);
241
242
  Logger.log(`Subdomain determined from app URL: ${subdomain}`);
@@ -267,7 +268,7 @@ class SettingsManager {
267
268
  return;
268
269
  }
269
270
  if (params.get('tokenStorage') === 'keyring') {
270
- throw new Error(`${capitalize(TOKEN_STORAGE_DESC.keyring)} requested but keytar not installed. Run 'npm install -g keytar' or ` +
271
+ throw new CliError(`${capitalize(TOKEN_STORAGE_DESC.keyring)} requested but keytar not installed. Run 'npm install -g keytar' or ` +
271
272
  `switch to ${TOKEN_STORAGE_DESC.plain} by adding '--plain' (discouraged).`);
272
273
  }
273
274
  if (params.get('renewLogin') || logout) {
@@ -288,7 +289,7 @@ class SettingsManager {
288
289
  params.set('clientsecret', await Question.askQuestion('clientsecret: ', undefined, true));
289
290
  console.log();
290
291
  if (params.get('clientsecret') === '') {
291
- throw new Error('clientsecret cannot be empty');
292
+ throw new CliError('clientsecret cannot be empty');
292
293
  }
293
294
  }
294
295
  async function addPasscode() {
@@ -296,7 +297,7 @@ class SettingsManager {
296
297
  params.set('passcode', await Question.askQuestion(prompt, undefined, true));
297
298
  console.log();
298
299
  if (params.get('passcode') === '') {
299
- throw new Error('Passcode cannot be empty');
300
+ throw new CliError('Passcode cannot be empty');
300
301
  }
301
302
  }
302
303
  function setRelevantAuth(auth) {
@@ -338,7 +339,7 @@ class SettingsManager {
338
339
  if (params.get('password') === '') {
339
340
  params.set('isEmptyPassword', true);
340
341
  // TODO check password only in production env
341
- // throw new Error('Password cannot be empty');
342
+ // throw new CliError('Password cannot be empty');
342
343
  } else {
343
344
  params.delete('isEmptyPassword');
344
345
  }
@@ -448,38 +448,63 @@ function _getServiceEntityProperty(
448
448
  propertyJson = propertyJson + '"key": true,\n';
449
449
  }
450
450
  if (isCollection === 1) {
451
- propertyJson = propertyJson + '"items": { \n'
452
- }
453
- propertyJson = propertyJson + '"type":"' + cdsDataType + '"';
451
+ //if annotations to be added
452
+ if (edmxncdsdatatype[dataType + "_a"] && cdsDataType != "cds.Date") {
453
+ if (dataType === 'Edm.Stream')
454
+ propertyJson = propertyJson + '"@Core.MediaType": "application/octet-stream",\n';
455
+ else
456
+ propertyJson = propertyJson + '"@odata.Type": "' + dataType + '",\n';
457
+ }
454
458
 
455
- //if annotations to be added
456
- if (edmxncdsdatatype[dataType + "_a"] && cdsDataType != "cds.Date") {
457
- if (dataType === 'Edm.Stream')
458
- propertyJson = propertyJson + ',\n"@Core.MediaType": "application/octet-stream"';
459
- else
460
- propertyJson = propertyJson + ',\n"@odata.Type": "' + dataType + '"';
461
- }
459
+ //adding precision in case of DateTime datatypes
460
+ if ((dataType === "Edm.DateTimeOffset" || dataType === "Edm.DateTime") && cdsDataType != "cds.Date") {
461
+ if (!precision || precision === -1) precision = 0;
462
+ propertyJson = propertyJson + '"@odata.Precision": ' + precision + ',\n';
463
+ }
462
464
 
463
- if (length && length > 0) {
464
- propertyJson = propertyJson + ',\n"length":' + length;
465
- } else if (precision && precision > 0 && hasInvalidPrecision === false) {
466
- propertyJson = propertyJson + ',\n"precision":' + precision;
467
- if (scale && scale > 0) {
468
- propertyJson = propertyJson + ',\n"scale":' + scale;
469
- } else {
470
- propertyJson = propertyJson + ',\n"scale":' + 0;
465
+ propertyJson = propertyJson + '"items": { \n'
466
+ propertyJson = propertyJson + '"type":"' + cdsDataType + '"';
467
+
468
+ if (length && length > 0) {
469
+ propertyJson = propertyJson + ',\n"length":' + length;
470
+ } else if (precision && precision > 0 && hasInvalidPrecision === false) {
471
+ propertyJson = propertyJson + ',\n"precision":' + precision;
472
+ if (scale && scale > 0) {
473
+ propertyJson = propertyJson + ',\n"scale":' + scale;
474
+ } else {
475
+ propertyJson = propertyJson + ',\n"scale":' + 0;
476
+ }
471
477
  }
478
+ propertyJson = propertyJson + '\n}'
472
479
  }
480
+ else{
481
+ propertyJson = propertyJson + '"type":"' + cdsDataType + '"';
482
+ //if annotations to be added
483
+ if (edmxncdsdatatype[dataType + "_a"] && cdsDataType != "cds.Date") {
484
+ if (dataType === 'Edm.Stream')
485
+ propertyJson = propertyJson + ',\n"@Core.MediaType": "application/octet-stream"';
486
+ else
487
+ propertyJson = propertyJson + ',\n"@odata.Type": "' + dataType + '"';
488
+ }
473
489
 
474
- //adding precision in case of DateTime datatypes
475
- if ((dataType === "Edm.DateTimeOffset" || dataType === "Edm.DateTime") && cdsDataType != "cds.Date") {
476
- if (!precision || precision === -1) precision = 0;
477
- propertyJson = propertyJson + ',\n"@odata.Precision": ' + precision;
478
- }
490
+ if (length && length > 0) {
491
+ propertyJson = propertyJson + ',\n"length":' + length;
492
+ } else if (precision && precision > 0 && hasInvalidPrecision === false) {
493
+ propertyJson = propertyJson + ',\n"precision":' + precision;
494
+ if (scale && scale > 0) {
495
+ propertyJson = propertyJson + ',\n"scale":' + scale;
496
+ } else {
497
+ propertyJson = propertyJson + ',\n"scale":' + 0;
498
+ }
499
+ }
479
500
 
480
- if (isCollection === 1) {
481
- propertyJson = propertyJson + '\n}'
501
+ //adding precision in case of DateTime datatypes
502
+ if ((dataType === "Edm.DateTimeOffset" || dataType === "Edm.DateTime") && cdsDataType != "cds.Date") {
503
+ if (!precision || precision === -1) precision = 0;
504
+ propertyJson = propertyJson + ',\n"@odata.Precision": ' + precision;
505
+ }
482
506
  }
507
+
483
508
  if (documentation != -1 && documentation) {
484
509
  propertyJson = propertyJson + ',\n"doc":"' + documentation + '"';
485
510
  }
@@ -138,6 +138,13 @@ function _includeExternalNamespaceAnnotation(source, target) {
138
138
 
139
139
  function _typeWithCollections(property, type, isCollection) {
140
140
  if (isCollection === true) {
141
+ let typeKeys = Object.keys(type);
142
+ for (let i = 0; i < typeKeys.length; i++) {
143
+ if(typeKeys[i].startsWith("@")){
144
+ property[typeKeys[i]] = type[typeKeys[i]];
145
+ delete type[typeKeys[i]];
146
+ }
147
+ }
141
148
  property.items = type;
142
149
  } else {
143
150
  Object.assign(property, type);
@@ -1,6 +1,6 @@
1
1
  const os = require('os');
2
2
 
3
- const MAVEN_ARCHETYPE_VERSION = '1.27.0';
3
+ const MAVEN_ARCHETYPE_VERSION = '1.27.1';
4
4
 
5
5
  const constants = {
6
6
  IS_WIN: (os.platform() === 'win32'),
@@ -233,13 +233,10 @@ const approuterExtensibility = {
233
233
  in: 'routes',
234
234
  where: [{
235
235
  property: 'source',
236
- isEqualTo: '^/extend/(.*)'
236
+ isEqualTo: '^/-/cds/.*'
237
237
  }, {
238
238
  property: 'destination',
239
239
  isEqualTo: 'mtx-api',
240
- }, {
241
- property: 'target',
242
- isEqualTo: '$1',
243
240
  }, {
244
241
  property: 'authenticationType',
245
242
  isEqualTo: 'none',
@@ -251,13 +248,10 @@ const approuterExtensibilityJava = {
251
248
  in: 'routes',
252
249
  where: [{
253
250
  property: 'source',
254
- isEqualTo: '^/extend/(.*)'
251
+ isEqualTo: '^/-/cds/.*'
255
252
  }, {
256
253
  property: 'destination',
257
254
  isEqualTo: 'mtx-api',
258
- }, {
259
- property: 'target',
260
- isEqualTo: '$1',
261
255
  }, {
262
256
  property: 'authenticationType',
263
257
  isEqualTo: 'none',
@@ -1,6 +1,13 @@
1
1
  {
2
2
  "authenticationMethod": "route",
3
3
  "routes": [
4
+ {{#cap.isMultitenant}}
5
+ {
6
+ "source": "^/-/cds/.*",
7
+ "destination": "mtx-api",
8
+ "authenticationType": "none"
9
+ },
10
+ {{/cap.isMultitenant}}
4
11
  {
5
12
  "source": "^/app/(.*)$",
6
13
  "target": "$1",
@@ -14,13 +21,6 @@
14
21
  "destination": "srv-api",
15
22
  "authenticationType": "xsuaa",
16
23
  "csrfProtection": true
17
- }{{#cap.isMultitenant}},
18
- {
19
- "source": "^/extend/(.*)",
20
- "destination": "mtx-api",
21
- "target": "$1",
22
- "authenticationType": "none"
23
24
  }
24
- {{/cap.isMultitenant}}
25
25
  ]
26
26
  }
@@ -45,8 +45,8 @@ module.exports = class ApprouterTemplate extends TemplateBase {
45
45
  }
46
46
 
47
47
  const appPath = join(this.projectPath, projectDescriptor.ui.appPath || 'app')
48
- const additions = isExtensible && isJava ? [approuterExtensibilityJava] :
49
- isExtensible && isNodejs ? [approuterExtensibility] : []
48
+ const additions = isExtensible && isJava ? [{ ...approuterExtensibilityJava, at: 0 }] :
49
+ isExtensible && isNodejs ? [{ ...approuterExtensibility, at: 0 }] : []
50
50
  await this.templateUtil.mergeJSON(
51
51
  `${appPath}/xs-app.json`,
52
52
  `${__dirname}/files/xs-app.json.hbs`,
@@ -148,9 +148,8 @@ function getNamespace(csn, artifactName) {
148
148
 
149
149
  // First step is not a namespace (we faked those in the CSN)
150
150
  // No subsequent step can be a namespace then
151
- if (art && art.kind !== 'namespace') {
151
+ if (art && art.kind !== 'namespace' && art.kind !== 'context')
152
152
  return null;
153
- }
154
153
 
155
154
 
156
155
  for (let i = 1; i < parts.length; i++) {
@@ -159,9 +158,8 @@ function getNamespace(csn, artifactName) {
159
158
  seen = `${seen}.${parts[i]}`;
160
159
  // This might not be - if it isn't, return the result.
161
160
  const currentArtifact = csn.definitions[seen];
162
- if (currentArtifact && currentArtifact.kind !== 'namespace') {
161
+ if (currentArtifact && currentArtifact.kind !== 'namespace' && currentArtifact.kind !== 'context')
163
162
  return previousArtifactName;
164
- }
165
163
  }
166
164
  // We came till here - so the full artifactName is a namespace
167
165
  return artifactName;
@@ -174,7 +174,7 @@ module.exports = class TemplateUtil {
174
174
  } else if (YAML.isPair(node) && node.value?.items) {
175
175
  collectionStack.unshift(node.value)
176
176
  } else if (YAML.isScalar(node) && semantics.forceOverwrite && templateNode) {
177
- node.value = templateNode.value
177
+ node.value = templateNode.value ?? templateNode.items?.[0]?.value?.value
178
178
  }
179
179
 
180
180
  if (node.items) {
@@ -293,13 +293,13 @@ module.exports = class TemplateUtil {
293
293
  mergeCollection: (targetNode, templateNode) => {
294
294
  if (YAML.isSeq(templateNode)) {
295
295
  let [,parent] = collectionStack
296
- additions?.filter(existence => {
297
- if (typeof existence.in === 'string' || Array.isArray(existence.in) && typeof existence.in[0] === 'string') return true
298
- const inExistence = Array.isArray(existence.in) ? existenceMap.get(existence.in[0].ref) : existenceMap.get(existence.in.ref)
296
+ additions?.filter(addition => {
297
+ if (typeof addition.in === 'string' || Array.isArray(addition.in) && typeof addition.in[0] === 'string') return true
298
+ const inExistence = Array.isArray(addition.in) ? existenceMap.get(addition.in[0].ref) : existenceMap.get(addition.in.ref)
299
299
  return inExistence?.node === parent
300
300
  })
301
- .filter(existence => {
302
- const keyPath = typeof existence.in === 'string' ? existence.in : Array.isArray(existence.in) && typeof existence.in[0] === 'string' ? existence.in[0] : existence.in[1]
301
+ .filter(addition => {
302
+ const keyPath = typeof addition.in === 'string' ? addition.in : Array.isArray(addition.in) && typeof addition.in[0] === 'string' ? addition.in[0] : addition.in[1]
303
303
  const keys = keyPath.split('.')
304
304
  if (keys.length > collectionStack.length + 1) return
305
305
  if (keys.length > 1) parent = collectionStack[keys.length]
@@ -308,9 +308,9 @@ module.exports = class TemplateUtil {
308
308
  .filter(({ ref }) => {
309
309
  return !existenceMap.get(ref)
310
310
  })
311
- .forEach(({ ref }) => {
311
+ .forEach(({ ref, at }) => {
312
312
  const templateNode = templateExistenceMap.get(ref).node
313
- targetNode.add(templateNode)
313
+ at !== undefined ? targetNode.items.splice(at, 0, templateNode) : targetNode.add(templateNode)
314
314
  })
315
315
  }
316
316
  },
@@ -33,9 +33,11 @@ class ChartAdder {
33
33
  }
34
34
 
35
35
  async copySchema() {
36
- let schema = JSON.parse(await fs.readFile(path.join(this.resources, 'values.schema.json'), { encoding: 'utf8' }))
36
+ let valuesSchema = JSON.parse(await fs.readFile(path.join(this.resources, 'schemas', 'values.schema.json'), { encoding: 'utf8' }))
37
+ let valuesRootSchema = JSON.parse(await fs.readFile(path.join(this.resources, 'schemas', 'values.root.schema.json'), { encoding: 'utf8' }))
37
38
 
38
- await this.builder.writeChartFile('values.schema.json', JSON.stringify(schema, null, 4));
39
+ await this.builder.writeChartFile('values.schema.json', JSON.stringify(valuesSchema, null, 4));
40
+ await this.builder.writeChartFile('values.root.schema.json', JSON.stringify(valuesRootSchema, null, 4));
39
41
  }
40
42
 
41
43
  async addChartValues() {
@@ -53,7 +53,7 @@ class ChartBuilder {
53
53
  } else {
54
54
 
55
55
  const header = `
56
- # yaml-language-server: $schema=./values.schema.json
56
+ # yaml-language-server: $schema=./values.root.schema.json
57
57
 
58
58
  `
59
59
  data = header + data;
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "allOf": [{ "$ref": "./values.schema.json" }],
4
+ "properties": {
5
+ "srv": {
6
+ "$id": "#/properties/srv",
7
+ "description": "Deployment",
8
+ "$ref": "./charts/web-application/values.schema.json"
9
+ }
10
+ }
11
+ }
@@ -124,6 +124,21 @@
124
124
  "resources": {
125
125
  "$id": "#/properties/hana_deployer/resources",
126
126
  "$ref": "#/definitions/Resources"
127
+ },
128
+ "env": {
129
+ "$id": "#/properties/hana_deployer/env",
130
+ "description": "Key-value map of environment variables, which should be added to the Pod spec.",
131
+ "type": "object",
132
+ "patternProperties": {
133
+ "^[-._a-zA-Z][-._a-zA-Z0-9]*$": {
134
+ "$comment": "regex above copied from https://github.com/kubernetes/kubernetes/blob/ea0764452222146c47ec826977f49d7001b0ea8c/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go#L402",
135
+ "not": {
136
+ "type": ["null", "object", "array"]
137
+ }
138
+ }
139
+ },
140
+ "additionalProperties": false,
141
+ "default": {}
127
142
  }
128
143
  }
129
144
  },
@@ -150,6 +165,21 @@
150
165
  "$id": "#/properties/html5_apps_deployer/resources",
151
166
  "$ref": "#/definitions/Resources"
152
167
  },
168
+ "env": {
169
+ "$id": "#/properties/html5_apps_deployer/env",
170
+ "description": "Key-value map of environment variables, which should be added to the Pod spec.",
171
+ "type": "object",
172
+ "patternProperties": {
173
+ "^[-._a-zA-Z][-._a-zA-Z0-9]*$": {
174
+ "$comment": "regex above copied from https://github.com/kubernetes/kubernetes/blob/ea0764452222146c47ec826977f49d7001b0ea8c/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go#L402",
175
+ "not": {
176
+ "type": ["null", "object", "array"]
177
+ }
178
+ }
179
+ },
180
+ "additionalProperties": false,
181
+ "default": {}
182
+ },
153
183
  "backendDestinations": {
154
184
  "$id": "#/properties/html5_apps_deployer/backendDestinations",
155
185
  "title": "Backend Destinations for HTML5 Application",
package/lib/lint/io.js CHANGED
@@ -63,7 +63,6 @@ module.exports = {
63
63
  const isFilePath = typeof configIn === "string";
64
64
  if (isFilePath) {
65
65
  if (await fsUtil.pathExists(configIn)) {
66
- console.log('+++', configIn)
67
66
  configContents = (await this.readEslintConfig(configIn)) || {};
68
67
  }
69
68
  } else {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@sap/cds-dk",
3
- "version": "6.1.1",
3
+ "version": "6.1.2",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@sap/cds-dk",
9
- "version": "6.1.1",
9
+ "version": "6.1.2",
10
10
  "license": "SEE LICENSE IN LICENSE",
11
11
  "dependencies": {
12
12
  "@sap/cds": "^6.1.0",
@@ -162,8 +162,8 @@
162
162
  }
163
163
  },
164
164
  "node_modules/@sap/cds": {
165
- "version": "6.1.0",
166
- "integrity": "sha512-H+Gb2uBZ0qM8pKCGYkT7+yCZ3psbuUGtGFZbev9CMgTSf5XI4e7mfw7ledC2XDHb2iBGHC/G/6LSppIpyustVw==",
165
+ "version": "6.1.1",
166
+ "integrity": "sha512-oOdwHpzPnpt10bJ94E8Umq8Y9uuA+1mpEH9zjQNB/jynNqSMuvM1/68ZAsndhL1pSgw49o8W6b83cKJNUVyzfw==",
167
167
  "license": "SEE LICENSE IN LICENSE",
168
168
  "dependencies": {
169
169
  "@sap/cds-compiler": "^3.0.0",
@@ -177,8 +177,8 @@
177
177
  }
178
178
  },
179
179
  "node_modules/@sap/cds-compiler": {
180
- "version": "3.1.0",
181
- "integrity": "sha512-utabOEdAr2EYie82yWzlztgd7T99J/xGrt3VX8AJ87ieN3EsUU/M94l52hyp6rDxCWiz470IEa2EZgBUNgg8fw==",
180
+ "version": "3.1.2",
181
+ "integrity": "sha512-csH3aEs4aPCzAtc75Jx/Ayym2A06JgAeaDUFLXI867/Vsp2F0g7MJxuCkNhpa+zVZsXcyB0Kxzzv4xU7zuJXsQ==",
182
182
  "license": "SEE LICENSE IN LICENSE",
183
183
  "dependencies": {
184
184
  "antlr4": "4.9.3"
@@ -931,8 +931,8 @@
931
931
  }
932
932
  },
933
933
  "node_modules/eslint": {
934
- "version": "8.21.0",
935
- "integrity": "sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==",
934
+ "version": "8.22.0",
935
+ "integrity": "sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==",
936
936
  "license": "MIT",
937
937
  "dependencies": {
938
938
  "@eslint/eslintrc": "^1.3.0",
@@ -1275,8 +1275,8 @@
1275
1275
  }
1276
1276
  },
1277
1277
  "node_modules/flatted": {
1278
- "version": "3.2.6",
1279
- "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==",
1278
+ "version": "3.2.7",
1279
+ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
1280
1280
  "license": "ISC"
1281
1281
  },
1282
1282
  "node_modules/follow-redirects": {
@@ -3252,16 +3252,16 @@
3252
3252
  }
3253
3253
  },
3254
3254
  "@sap/cds": {
3255
- "version": "6.1.0",
3256
- "integrity": "sha512-H+Gb2uBZ0qM8pKCGYkT7+yCZ3psbuUGtGFZbev9CMgTSf5XI4e7mfw7ledC2XDHb2iBGHC/G/6LSppIpyustVw==",
3255
+ "version": "6.1.1",
3256
+ "integrity": "sha512-oOdwHpzPnpt10bJ94E8Umq8Y9uuA+1mpEH9zjQNB/jynNqSMuvM1/68ZAsndhL1pSgw49o8W6b83cKJNUVyzfw==",
3257
3257
  "requires": {
3258
3258
  "@sap/cds-compiler": "^3.0.0",
3259
3259
  "@sap/cds-foss": "^4"
3260
3260
  }
3261
3261
  },
3262
3262
  "@sap/cds-compiler": {
3263
- "version": "3.1.0",
3264
- "integrity": "sha512-utabOEdAr2EYie82yWzlztgd7T99J/xGrt3VX8AJ87ieN3EsUU/M94l52hyp6rDxCWiz470IEa2EZgBUNgg8fw==",
3263
+ "version": "3.1.2",
3264
+ "integrity": "sha512-csH3aEs4aPCzAtc75Jx/Ayym2A06JgAeaDUFLXI867/Vsp2F0g7MJxuCkNhpa+zVZsXcyB0Kxzzv4xU7zuJXsQ==",
3265
3265
  "requires": {
3266
3266
  "antlr4": "4.9.3"
3267
3267
  }
@@ -3744,8 +3744,8 @@
3744
3744
  "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
3745
3745
  },
3746
3746
  "eslint": {
3747
- "version": "8.21.0",
3748
- "integrity": "sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==",
3747
+ "version": "8.22.0",
3748
+ "integrity": "sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==",
3749
3749
  "requires": {
3750
3750
  "@eslint/eslintrc": "^1.3.0",
3751
3751
  "@humanwhocodes/config-array": "^0.10.4",
@@ -3994,8 +3994,8 @@
3994
3994
  }
3995
3995
  },
3996
3996
  "flatted": {
3997
- "version": "3.2.6",
3998
- "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ=="
3997
+ "version": "3.2.7",
3998
+ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
3999
3999
  },
4000
4000
  "follow-redirects": {
4001
4001
  "version": "1.15.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds-dk",
3
- "version": "6.1.1",
3
+ "version": "6.1.2",
4
4
  "description": "Command line client and development toolkit for the SAP Cloud Application Programming Model",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [