@sap/cds-dk 6.2.3 → 6.3.1

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/bin/bind.js +4 -4
  3. package/bin/deploy/to-h2.js +4 -0
  4. package/bin/deploy/to-sqlite.js +6 -4
  5. package/bin/help.js +1 -1
  6. package/bin/login.js +1 -0
  7. package/bin/push.js +29 -7
  8. package/lib/bind/userCdsrcJsonStorageProvider.js +4 -4
  9. package/lib/client/activate.js +1 -1
  10. package/lib/client/base_command.js +10 -10
  11. package/lib/client/helper/dependency_helper.js +10 -1
  12. package/lib/client/helper/logging.js +6 -2
  13. package/lib/client/pull.js +6 -6
  14. package/lib/client/push.js +33 -14
  15. package/lib/client/settings_manager.js +53 -22
  16. package/lib/compile/openapi/csdl2openapi.js +1 -1
  17. package/lib/compile/to-xsuaa.js +1 -1
  18. package/lib/import/odata/v2parser.js +37 -1
  19. package/lib/import/odata/v4parser.js +101 -23
  20. package/lib/init/constants.js +1 -1
  21. package/lib/init/index.js +23 -45
  22. package/lib/init/template/approuter/index.js +44 -48
  23. package/lib/init/template/auditlog/index.js +14 -10
  24. package/lib/init/template/cf-manifest/index.js +9 -11
  25. package/lib/init/template/connectivity/index.js +10 -10
  26. package/lib/init/template/data/index.js +20 -19
  27. package/lib/init/template/destinations/index.js +14 -9
  28. package/lib/init/template/enterprise-messaging/index.js +14 -9
  29. package/lib/init/template/extensibility/index.js +14 -9
  30. package/lib/init/template/hana/index.js +51 -57
  31. package/lib/init/template/helm/index.js +27 -59
  32. package/lib/init/template/helm/subchart/templates/_helpers.tpl +38 -0
  33. package/lib/init/template/helm/subchart/templates/deployment.yaml +12 -12
  34. package/lib/init/template/helm/subchart/values.md +565 -194
  35. package/lib/init/template/helm/subchart/values.schema.json +163 -8
  36. package/lib/init/template/helm/subchart/values.yaml +17 -1
  37. package/lib/init/template/html5-repo/index.js +15 -11
  38. package/lib/init/template/java/index.js +15 -11
  39. package/lib/init/template/kibana-logging/index.js +30 -31
  40. package/lib/init/template/lint/index.js +17 -18
  41. package/lib/init/template/mta/index.js +18 -40
  42. package/lib/init/template/mtx/index.js +2 -3
  43. package/lib/init/template/multitenancy/index.js +27 -20
  44. package/lib/init/template/nodejs/index.js +18 -20
  45. package/lib/init/template/notebook/index.js +5 -434
  46. package/lib/init/template/pipeline/index.js +10 -10
  47. package/lib/init/template/samples/index.js +7 -16
  48. package/lib/init/template/templateBase.js +17 -18
  49. package/lib/init/template/toggles/files/cdsrc.json +5 -0
  50. package/lib/init/template/toggles/files/cdsrc.json.hbs +7 -0
  51. package/lib/init/template/toggles/index.js +12 -6
  52. package/lib/init/template/xsuaa/index.js +44 -41
  53. package/lib/init/types.d.ts +0 -59
  54. package/lib/init/util/commandUtil.js +6 -7
  55. package/lib/init/util/fsUtil.js +23 -109
  56. package/lib/init/util/merge.js +319 -0
  57. package/lib/init/util/mvnArchetypeUtil.js +6 -5
  58. package/lib/init/util/projectReader.js +125 -300
  59. package/lib/init/util/spinner.js +10 -8
  60. package/lib/init/util/templateUtil.js +25 -368
  61. package/lib/lint/Linter.js +2 -3
  62. package/lib/lint/checks.js +0 -1
  63. package/lib/lint/io.js +18 -34
  64. package/npm-shrinkwrap.json +1132 -1190
  65. package/package.json +3 -3
  66. package/lib/init/template/notebook/cap_magic.py +0 -118
  67. package/lib/init/template/notebook/files/{{{name}}}.ipynb.hbs +0 -90
  68. package/lib/init/template/notebook/files/{{{name}}}.json +0 -102
  69. package/lib/init/template/notebook/notebook.js +0 -394
  70. package/lib/init/util/logger.js +0 -42
package/CHANGELOG.md CHANGED
@@ -7,6 +7,42 @@ 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.3.1 - 2022-11-04
11
+
12
+ ### Changed
13
+
14
+ - `cds init` uses latest Maven Java archetype version 1.29.0 for creating Java projects.
15
+ - Use `@sap/cds` 6.3.1
16
+ - Use `@sap/cds-mtxs` 1.3.1
17
+
18
+ ### Fixed
19
+
20
+ - MTX Client no longer reports 'Request failed with status code 401' when sending a passcode to old MTX server.
21
+
22
+ ## Version 6.3.0 - 2022-11-02
23
+
24
+ ### Added
25
+
26
+ - `cds deploy --to h2 --dry` to create drop/create DDL for H2. Full deployment (w/o `--dry`) is not yet supported, though.
27
+ - `cds push <file>|<URL>` to specify a custom extension archive by local path or download URL.
28
+
29
+ ### Changed
30
+
31
+ - `cds add` support for classic Java projects is now removed.
32
+ - `cds import` now adds the annotation `@cds.external` for all the definitions in the CSN along with the service.
33
+ - `cds import` now adds `notNull` entry for all the parameters and properties in the CSN.
34
+ - MTX Client now trims a passcode entered via prompt for convenience when pasting
35
+
36
+ ### Fixed
37
+
38
+ - MTX Client now ignores a username potentially saved for the current project, if a passcode is given
39
+ - MTX Client now treats keytar as optional unless explicitly running `cds login`
40
+ - `cds import` now caputres the documentation properly for all the EntitySet referring to same EntityType.
41
+ - `cds deploy --to sql` now produces 'plain' SQL again, suitable for e.g. H2. In 6.2 it produced 'sqlite' dialect, erroneously.
42
+ - `cds compile --to openapi` now fixes the duplication of fields in `required` section.
43
+ - `cds lint` now recognizes ESLint configurations from _package.json_.
44
+
45
+
10
46
  ## Version 6.2.3 - 2022-10-21
11
47
 
12
48
  ### Changed
@@ -28,6 +64,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
28
64
 
29
65
  - `cds deploy --to sql` now honors the `cds.sql.dialect` configuration to specify SQL dialects like `postgres`.
30
66
 
67
+ ### Removed
68
+ - `cds add notebook` removed in favor of custom notebooks in VS Code (now part of the CDS Editor)
69
+
31
70
  ## Version 6.2.1 - 2022-10-10
32
71
 
33
72
  ### Fixed
package/bin/bind.js CHANGED
@@ -89,7 +89,7 @@ async function cds_bind(args, options, logger = LOG) {
89
89
  throw new Error(`The option '--kind' cannot be specified together with multiple targets ('--to') services. Use one service per call or omit the '--kind' option.`);
90
90
  }
91
91
 
92
- const spinner = new Spinner(logger);
92
+ const spinner = new Spinner();
93
93
 
94
94
  let on;
95
95
  if (options.on === 'k8s' || options.on === 'kubernetes') {
@@ -139,7 +139,7 @@ async function cds_bind(args, options, logger = LOG) {
139
139
  const kindText = kind && `with kind ${highlight(kind)}`;
140
140
  const nameText = !isJavaProject && name && highlight(name);
141
141
 
142
- logger.log(['Binding', nameText, 'to', platformText(binding), bindingText(binding, highlight), kindText].filter(t => t).join(' '));
142
+ logger.info(['Binding', nameText, 'to', platformText(binding), bindingText(binding, highlight), kindText].filter(t => t).join(' '));
143
143
 
144
144
  if (!resolvedService.name) {
145
145
  const { binding } = resolvedService;
@@ -156,8 +156,8 @@ async function cds_bind(args, options, logger = LOG) {
156
156
  logger
157
157
  });
158
158
 
159
- logger.log();
160
- logger.log(`${info('TIP:')} Run with cloud bindings: ${bold(runCmd(options, isJavaProject))}`);
159
+ logger.info();
160
+ logger.info(`${info('TIP:')} Run with cloud bindings: ${bold(runCmd(options, isJavaProject))}`);
161
161
  }
162
162
 
163
163
  async function cds_bind_exec(command) { // eslint-disable-lint
@@ -0,0 +1,4 @@
1
+ exports.deploy = function deploy_to_sql (model, db, options) {
2
+ options.dialect = 'h2'
3
+ return require('./to-sql') .deploy (model, db, options)
4
+ }
@@ -6,10 +6,12 @@ exports.deploy = function deploy_to_sqlite (_model, _db, options) {
6
6
  const model = _model || conf.model || cds.resolve(!fts ? '*' : ['*',fts], false)
7
7
  const file = _database4 (_db, _dbrl, conf)
8
8
 
9
- const dialect = _dbrl && _dbrl.match(/^(\w+)/) && RegExp.$1
10
- if (dialect) options.dialect = (dialect === 'sql')
11
- ? conf.dialect || cds.env.sql.dialect
12
- : dialect
9
+ // current 'sqlite' default is bad, as we want to default to 'plain', so ignore it
10
+ const env_dialect = cds.env.sql.dialect !== 'sqlite' ? cds.env.sql.dialect : 'plain'
11
+ const to_dialect = _dbrl && _dbrl.match(/^(\w+)/) && RegExp.$1
12
+ if (to_dialect) options.dialect = (to_dialect === 'sql')
13
+ ? conf.dialect || env_dialect
14
+ : to_dialect
13
15
  if (options.dialect === 'postgres' && !options.dry) console.error('\nDeploying to PostgreSQL is not yet supported\n')
14
16
 
15
17
  return cds.deploy(model) .to ('sqlite:'+file, options)
package/bin/help.js CHANGED
@@ -10,7 +10,7 @@ module.exports = exports = Object.assign (help, {help:`
10
10
 
11
11
  *i* | *init* jump-start cds-based projects
12
12
  *a* | *add* add a feature to an existing project
13
- *y* | *bind* [beta] bind a service to a hana instance
13
+ *y* | *bind* bind application to remote services
14
14
  *m* | *import* add models from external sources
15
15
  *c* | *compile* compile cds models to different outputs
16
16
  *p* | *parse* parses given cds models
package/bin/login.js CHANGED
@@ -82,6 +82,7 @@ module.exports = Object.assign(login, {
82
82
 
83
83
  For security reasons, username will only be saved for app URLs that look like localhost.
84
84
  The same is true for an empty password. Non-empty passwords will never be saved.
85
+ If saved, a username will be associated with the project folder, not with the (potentially ambiguous) URL.
85
86
 
86
87
  Note: Productive secrets should not be specified on a command line, since this might expose them to
87
88
  unauthorized users on the same machine!
package/bin/push.js CHANGED
@@ -5,25 +5,29 @@ module.exports = Object.assign(push, {
5
5
  help: `
6
6
  # SYNOPSIS
7
7
 
8
- *cds push* [ <extension-project-directory> ]
8
+ *cds push* [ <extension-project-directory> | <extension-archive> | <extension-url> ]
9
9
 
10
10
  Push your extension to the SaaS app in order to enable or update it.
11
11
 
12
+ You may specify a project directory or a custom extension archive, either as a
13
+ local file or as a URL to download it from.
14
+ A download URL may contain Basic-Auth credentials as in 'https://user:pass@host'.
15
+
12
16
  By default, the current working directory is used as project directory.
13
17
 
14
- Authentication data will not be saved.
18
+ Any authentication data will not be saved.
15
19
 
16
20
 
17
21
  # OPTIONS
18
22
 
19
23
  *--to* <url>
20
24
 
21
- Specify the URL of the extensible SaaS app. By default, it is determined from
22
- the current user's active CDS or Cloud Foundry login.
25
+ Specify the URL of the extensible SaaS app. Determined from the current user's active
26
+ CDS or Cloud Foundry login by default.
23
27
 
24
28
  *-s* | *--subdomain* <tenant-subdomain>
25
29
 
26
- Specify your tenant's subdomain. By default, it's determined from project settings.
30
+ The tenant subdomain. Determined from project settings by default.
27
31
 
28
32
  *-p* | *--passcode* <passcode>
29
33
 
@@ -37,6 +41,7 @@ module.exports = Object.assign(push, {
37
41
 
38
42
  Client ID and optionally Client Secret for authentication.
39
43
 
44
+
40
45
  # SEE ALSO
41
46
 
42
47
  *cds pull* to get the CDS model of the SaaS app.
@@ -47,17 +52,34 @@ module.exports = Object.assign(push, {
47
52
 
48
53
 
49
54
  async function push([projectFolder], options = {}) {
55
+ const { statSync } = require('fs');
56
+ const { resolve } = require('path');
57
+ function isDir(dir) {
58
+ return statSync(resolve(dir), { throwIfNoEntry: false })?.isDirectory();
59
+ }
60
+
50
61
  const [username, password] = options.user?.split(':') ?? [];
51
62
  delete options.user;
52
63
  const [clientid, clientsecret] = options.client?.split(':') ?? [];
53
64
  delete options.client;
54
65
 
55
- await require('../lib/client/push').run({
66
+ let extArchive;
67
+ if (projectFolder && !isDir(projectFolder)) {
68
+ extArchive = projectFolder;
69
+ projectFolder = undefined;
70
+ }
71
+
72
+ const args = [{
56
73
  ...options,
57
74
  projectFolder,
58
75
  username,
59
76
  password,
60
77
  clientid,
61
78
  clientsecret
62
- })
79
+ }];
80
+ if (extArchive) {
81
+ args.push({ extArchive });
82
+ }
83
+
84
+ await require('../lib/client/push').run(...args);
63
85
  }
@@ -47,7 +47,7 @@ class UserCdsrcJsonStorageProvider {
47
47
  }
48
48
  }
49
49
 
50
- logger.log(`adding entry '${file}' to ${GIT_IGNORE_FILE}.`);
50
+ logger.info(`adding entry '${file}' to ${GIT_IGNORE_FILE}.`);
51
51
  let gitignore = await this.readFileSafely(GIT_IGNORE_FILE);
52
52
  gitignore = gitignore + `
53
53
  # added by cds bind
@@ -80,7 +80,7 @@ ${file}
80
80
  // if (options['save-credentials'] && services.find(service => service.credentials) && process.stdin.isTTY) {
81
81
  // const answer = await this.askBooleanQuestion(warn(`IF YOU CONTINUE, SENSITIVE DATA WILL BE WRITTEN TO AN UNENCRYPTED FILE [${jsonPath}]! Do you want to continue [y/N] ?`));
82
82
  // if (!answer) {
83
- // options.logger.log(`aborting cds bind; not writing any sensitive data to file [${jsonPath}]`);
83
+ // options.logger.info(`aborting cds bind; not writing any sensitive data to file [${jsonPath}]`);
84
84
  // return;
85
85
  // }
86
86
  // }
@@ -115,9 +115,9 @@ ${file}
115
115
 
116
116
  await this.writeJsonFile(jsonPath, jsonObject, options.logger);
117
117
  if (options.profile) {
118
- options.logger.log(`Saving bindings to ${highlight(jsonPath)} in profile ${highlight(options.profile)}`);
118
+ options.logger.info(`Saving bindings to ${highlight(jsonPath)} in profile ${highlight(options.profile)}`);
119
119
  } else {
120
- options.logger.log(`Saving bindings to ${highlight(jsonPath)}`);
120
+ options.logger.info(`Saving bindings to ${highlight(jsonPath)}`);
121
121
  }
122
122
  }
123
123
 
@@ -66,7 +66,7 @@ module.exports = class Activate extends BaseCommand {
66
66
  await this.runCustomFileUpload(params);
67
67
  }
68
68
  } catch (error) {
69
- await this.handleHttpError(error, params);
69
+ this.handleHttpError(error, params);
70
70
  }
71
71
  }
72
72
 
@@ -15,20 +15,20 @@ module.exports = class BaseCommand {
15
15
  return exists;
16
16
  }
17
17
 
18
- static async handleHttpError(error, params) {
19
- if (!('isAxiosError' in error)) {
20
- throw error;
21
- }
22
-
23
- switch (error.response && error.response.status) {
18
+ static handleHttpError(error, params, url) {
19
+ url = url ?? params.get('appUrl');
20
+ const prefix = `Request to ${url} failed`;
21
+ switch (error.response?.status) {
24
22
  case 401:
25
- throw new CliError('Request failed: authorization might have expired. Please retry with valid passcode' +
26
- (params.has('passcodeUrl') ? ` from ${params.get('passcodeUrl')}` : ''), error);
23
+ throw new CliError(prefix + ': invalid authentication. Retry with valid passcode' +
24
+ params.has('passcodeUrl') ? ` from ${params.get('passcodeUrl')}` : '', error);
25
+ case 403:
26
+ throw new CliError(prefix + ': insufficient authorization. Ensure client has all necessary roles', error);
27
27
  case 404:
28
- throw new CliError(`Request failed. Please check application URL ${params.get('appUrl')}`, error);
28
+ throw new CliError(prefix + ': resource not found.');
29
29
  case 422:
30
30
  // Compilation or native-extension error.
31
- throw new CliError(undefined, error);
31
+ throw new CliError(prefix, error);
32
32
  default:
33
33
  // Unexpected error should be transparent to user.
34
34
  throw error;
@@ -15,7 +15,7 @@ function globalModulesDir() {
15
15
  return cachedDir;
16
16
  }
17
17
 
18
- module.exports = function requireGlobal(packageId) {
18
+ function requireGlobal(packageId) {
19
19
  try {
20
20
  const packagePath = require.resolve(packageId, { paths: [globalModulesDir()] });
21
21
  return require(packagePath);
@@ -24,3 +24,12 @@ module.exports = function requireGlobal(packageId) {
24
24
  log.debug(`Failed to find module '${packageId}' in ${globalModulesDir()}`, error);
25
25
  }
26
26
  }
27
+
28
+ function resetCache() {
29
+ cachedDir = undefined;
30
+ }
31
+
32
+ module.exports = {
33
+ requireGlobal,
34
+ resetCache
35
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  function debug(debugText, error) {
4
4
  if (process.env.DEBUG) {
5
- console.error(getMessage(debugText, error));
5
+ console.log(getMessage(debugText, error));
6
6
  }
7
7
  }
8
8
 
@@ -11,7 +11,11 @@ function getMessage(errorText, error) {
11
11
  if (!error) {
12
12
  return message;
13
13
  }
14
- message += '\n' + (error.response?.data?.error?.message || error.message);
14
+ message += '\n' + (
15
+ error.response?.data?.message // response body == error
16
+ || error.response?.data?.error?.message // response body == { error }
17
+ || error.message // no response attached
18
+ );
15
19
  if (process.env.DEBUG && error.stack) {
16
20
  message += '\n' + error.stack;
17
21
  }
@@ -7,13 +7,13 @@ const axios = require('axios');
7
7
 
8
8
  const { login } = require('./auth_manager');
9
9
  const BaseCommand = require('./base_command');
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
  if (!tar) {
15
- console.error(`cds pull requires @sap/cds version >= 6.2. Current version is ${cds.version}\n`);
16
- return process.exitCode = 1;
16
+ throw new CliError(`cds pull requires @sap/cds version >= 6.2. Current version is ${cds.version}\n`);
17
17
  }
18
18
 
19
19
  const params = await login(paramValues);
@@ -26,8 +26,8 @@ module.exports = class Pull extends BaseCommand {
26
26
  fs.rmSync(target, { force: true, recursive: true });
27
27
  fs.mkdirSync(target, { recursive: true });
28
28
 
29
- const tgzBuffer = Buffer.from(await this.getTgz(params));
30
- return tar.xz(tgzBuffer).to(target);
29
+ const baseModelTgz = await this.getTgz(params);
30
+ return tar.xz(baseModelTgz).to(target);
31
31
  }
32
32
 
33
33
  static getAppPackageName(projectFolder) {
@@ -40,7 +40,7 @@ module.exports = class Pull extends BaseCommand {
40
40
  const options = { ...params.get('reqAuth'), responseType: 'arraybuffer' };
41
41
 
42
42
  return axios.post(pullUrl, {}, options)
43
- .then(response => response.data)
44
- .catch(error => this.handleHttpError(error, params));
43
+ .then(response => Buffer.from(response.data))
44
+ .catch(error => this.handleHttpError(error, params, pullUrl));
45
45
  }
46
46
  }
@@ -5,26 +5,45 @@ const axios = require('axios');
5
5
 
6
6
  const { login } = require('./auth_manager');
7
7
  const BaseCommand = require('./base_command');
8
- const { CliError } = require('./helper/errors'); // REVISIT: add and use a this.error() method instead
8
+ const { CliError } = require('./helper/errors');
9
+ const { schemaRegex } = require('./helper/url_helper');
9
10
 
10
11
  module.exports = class Push extends BaseCommand {
11
12
 
12
- static async run(paramValues) {
13
+ static async run(paramValues, pushOptions) {
13
14
  const params = await login(paramValues);
14
- const projectFolder = params.get('projectFolder');
15
- const tar = join(projectFolder, 'gen', 'extension.tgz')
16
- const url = params.get('appUrl')
17
- const subdomain = params.get('subdomain') // REVISIT: Why are subdomains not transparently encoded in URLs?
18
- const target = { url }; if (subdomain) target.subdomain = subdomain
19
- const source = { src: local(tar) }
20
- console.log(`\nPushing extension from`, source, 'to', target);
21
-
22
- const tgz = fs.readFileSync(tar, { encoding: 'base64' });
23
- const extensionName = this.getExtensionName(projectFolder);
24
- await this.pushTgz(params, tgz, extensionName);
15
+
16
+ const { src, content } = await this.readSource(params, pushOptions);
17
+
18
+ const extensionName = this.getExtensionName(params.get('projectFolder'));
19
+ const url = params.get('appUrl');
20
+ const subdomain = params.get('subdomain');
21
+ const target = Object.assign({ url }, subdomain && { subdomain });
22
+ console.log(`\nPushing extension '${extensionName}' from`, { src }, 'to', target);
23
+
24
+ await this.pushTgz(params, content, extensionName);
25
25
  console.log ('Activation succeeded.\n')
26
26
  }
27
27
 
28
+ static async readSource(params, pushOptions) {
29
+ let src = pushOptions?.extArchive ?? join(params.get('projectFolder'), 'gen', 'extension.tgz');
30
+ let content;
31
+ if (fs.existsSync(src)) {
32
+ content = fs.readFileSync(src, { encoding: 'base64' });
33
+ src = local(src);
34
+ } else {
35
+ if (!schemaRegex.test(src)) {
36
+ throw new CliError(`Nonexistent path: ${src}`);
37
+ }
38
+ try {
39
+ content = (await axios.get(src, { responseEncoding: 'base64' })).data;
40
+ } catch (error) {
41
+ this.handleHttpError(error, params, src);
42
+ }
43
+ }
44
+ return { src, content };
45
+ }
46
+
28
47
  static getExtensionName(projectFolder) {
29
48
  try {
30
49
  return require(join(projectFolder, 'package.json')).name;
@@ -41,6 +60,6 @@ module.exports = class Push extends BaseCommand {
41
60
  extension: tgz,
42
61
  tag: extensionName
43
62
  }, options)
44
- .catch(error => this.handleHttpError(error, params));
63
+ .catch(error => this.handleHttpError(error, params, pushUrl));
45
64
  }
46
65
  }
@@ -3,8 +3,8 @@ const os = require('os');
3
3
  const path = require('path');
4
4
  const axios = require('axios');
5
5
 
6
- const requireGlobal = require('./helper/dependency_helper');
7
- const keytar = requireGlobal('keytar');
6
+ const { requireGlobal } = require('./helper/dependency_helper');
7
+ let keytar;
8
8
 
9
9
  const log = require('./helper/logging');
10
10
  const { getAppFromSuggestions, getAppName, getSubdomain } = require('./helper/cf_helper');
@@ -115,7 +115,7 @@ async function getTokenBaseUrl(params, renewUrl) {
115
115
  existingPath = path;
116
116
  break;
117
117
  } catch (error) {
118
- if (error.response?.status === 404) {
118
+ if ([401, 404].includes(error.response?.status)) {
119
119
  continue;
120
120
  }
121
121
  throw error;
@@ -159,13 +159,15 @@ class SettingsManager {
159
159
  log.debug(`Saving settings for project ${params.get('projectFolder')}`);
160
160
 
161
161
  const paramsMap = params.toValueMap(Persistence.setting);
162
- if (localhostRegex.test(params.get('appUrl'))) {
163
- console.log(`Saving username${(params.get('isEmptyPassword') ? ' and empty-password hint' : '')} with project settings.`);
164
- } else {
165
- // Save only against localhost for security reasons.
166
- log.debug('Not saving username because app is not recognized to run on localhost.');
167
- delete paramsMap.username;
168
- delete paramsMap.isEmptyPassword;
162
+ if (params.has('username')) {
163
+ // Save username only for localhost for security reasons.
164
+ if (localhostRegex.test(params.get('appUrl'))) {
165
+ console.log(`Saving username${(params.get('isEmptyPassword') ? ' and empty-password hint' : '')} with project settings.`);
166
+ } else {
167
+ log.debug('Not saving username because app is not recognized to run on localhost.');
168
+ delete paramsMap.username;
169
+ delete paramsMap.isEmptyPassword;
170
+ }
169
171
  }
170
172
  await this._saveToFile(params.get('projectFolder'), paramsMap);
171
173
 
@@ -196,6 +198,12 @@ class SettingsManager {
196
198
  log.debug(`Loaded project settings for ${params.get('projectFolder')}: ${loadedSettings.format()}`);
197
199
  const loadedAppUrl = loadedSettings.get('appUrl');
198
200
 
201
+ if (params.has('passcode') && loadedSettings.has('username')) {
202
+ console.log(`Unsetting username${(loadedSettings.has('isEmptyPassword') ? ' and empty-password hint' : '')}`);
203
+ loadedSettings.delete('username');
204
+ loadedSettings.delete('isEmptyPassword');
205
+ }
206
+
199
207
  params.mergeLower(loadedSettings);
200
208
 
201
209
  await this.updateUrls(params, logout, loadedAppUrl);
@@ -206,6 +214,12 @@ class SettingsManager {
206
214
 
207
215
  static async updateUrls(params, logout, loadedAppUrl) {
208
216
  async function addAppUrlAndSubdomain() {
217
+ function needsSubdomain() {
218
+ // Subdomain needed for token retrieval.
219
+ return params.has('appUrl') && !localhostRegex.test(params.get('appUrl')) && !params.has('username');
220
+ }
221
+
222
+ // Prefix URL schema
209
223
  if (params.has('appUrl')) {
210
224
  const appUrl = params.get('appUrl');
211
225
  if (!schemaRegex.test(appUrl)) {
@@ -217,11 +231,16 @@ class SettingsManager {
217
231
  log.debug(`Added schema to app URL: ${params.get('appUrl')}`);
218
232
  }
219
233
  }
234
+
220
235
  if (!logout) {
236
+ // Get appUrl from CF
221
237
  if (!params.has('appUrl')) {
222
238
  log.debug('App URL not given');
223
239
  await getAppUrlAndSubdomainFromSuggestions(params);
224
- } else if (!params.has('subdomain') && !localhostRegex.test(params.get('appUrl'))) {
240
+ }
241
+
242
+ // Get subdomain from CF
243
+ if (!params.has('subdomain') && needsSubdomain()) {
225
244
  log.debug('Subdomain not given');
226
245
  const appName = await getAppName(params.get('appUrl'));
227
246
  if (appName) {
@@ -229,10 +248,14 @@ class SettingsManager {
229
248
  }
230
249
  }
231
250
  }
251
+
252
+ // Check required param: appUrl
232
253
  if (!params.has('appUrl')) {
233
254
  throw new CliError('App URL not given. Please specify it or log in to Cloud Foundry and repeat this command.');
234
255
  }
235
- if (!params.has('subdomain') && !params.has('username')) {
256
+
257
+ // Check required param: subdomain
258
+ if (!params.has('subdomain') && needsSubdomain()) {
236
259
  const subdomain = extractSubdomain(params.get('appUrl'));
237
260
  if (!subdomain) {
238
261
  throw new CliError('Subdomain not given');
@@ -263,16 +286,25 @@ class SettingsManager {
263
286
  }
264
287
  return;
265
288
  }
266
- if (params.get('username') || keytar) {
289
+ if (params.get('username')) {
267
290
  return;
268
291
  }
269
- if (params.get('tokenStorage') === 'keyring') {
292
+ keytar = requireGlobal('keytar');
293
+ if (keytar) {
294
+ return;
295
+ }
296
+ // keytar missing
297
+
298
+ if (params.get('tokenStorage') === 'keyring' && params.get('saveData')) {
299
+ // Explicit login w/o plain-text storage enabled
270
300
  throw new CliError(capitalize(TOKEN_STORAGE_DESC.keyring) + ' requested but keytar not installed. ' +
271
301
  'Run `npm install -g keytar` or switch to ' + TOKEN_STORAGE_DESC.plain + ' by adding `--plain` (discouraged).');
272
302
  }
273
- if (params.get('renewLogin') || logout) {
274
- console.log('Disabling', TOKEN_STORAGE_DESC.keyring, 'functionality: `keytar` module not found. Run `npm install -g keytar` to install it.');
275
- }
303
+
304
+ const doLog = params.get('saveData') || logout
305
+ ? console.log.bind(console) // Explicit login/logout call
306
+ : log.debug;
307
+ doLog(`Disabling ${TOKEN_STORAGE_DESC.keyring} functionality: keytar not found. Run \`npm install -g keytar\` to install it.`);
276
308
  }
277
309
 
278
310
  static async addAuth(params, logout) {
@@ -293,7 +325,7 @@ class SettingsManager {
293
325
  }
294
326
  async function addPasscode() {
295
327
  const prompt = `Passcode${params.get('passcodeUrl') ? ' (visit ' + params.get('passcodeUrl') + ' to generate)' : ''}: `;
296
- params.set('passcode', await Question.askQuestion(prompt, undefined, true));
328
+ params.set('passcode', (await Question.askQuestion(prompt, undefined, true)).trim());
297
329
  console.log();
298
330
  if (params.get('passcode') === '') {
299
331
  throw new CliError('Passcode cannot be empty');
@@ -337,11 +369,10 @@ class SettingsManager {
337
369
  }
338
370
  if (params.get('password') === '') {
339
371
  params.set('isEmptyPassword', true);
340
- // TODO check password only in production env
341
- // throw new CliError('Password cannot be empty');
342
372
  } else {
343
373
  params.delete('isEmptyPassword');
344
374
  }
375
+ log.debug('Ignoring any saved authentication data because username is given');
345
376
  return;
346
377
  } else {
347
378
  params.delete('isEmptyPassword');
@@ -358,7 +389,7 @@ class SettingsManager {
358
389
 
359
390
  setRelevantAuth(auth);
360
391
 
361
- if (!params.has('token') && !params.has('passcode') && !params.has('clientsecret') && !logout) {
392
+ if (!logout && !params.has('token') && !params.has('passcode') && !params.has('clientsecret')) {
362
393
  await addPasscode();
363
394
  }
364
395
  }
@@ -368,7 +399,7 @@ class SettingsManager {
368
399
  await this.loadAndMergeSettings(allParams, true);
369
400
 
370
401
  const target = `URL ${allParams.get('appUrl')}, subdomain '${allParams.get('subdomain')}'`;
371
- let fromStorage = fromOtherStorage && other(params.get('tokenStorage'))
402
+ let fromStorage = fromOtherStorage && other(params.get('tokenStorage'));
372
403
  const deleteBoth = !fromStorage && allParams.get('tokenStorage') && allParams.get('clearOtherTokenStorage');
373
404
 
374
405
  log.debug(`Deleting authentication data${fromOtherStorage ? ' from other storage' : ''} for ${target}`);
@@ -1830,7 +1830,7 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot
1830
1830
  schemas[schemaName].properties = schemaProperties;
1831
1831
 
1832
1832
  if (suffix === SUFFIX.create && required.length > 0)
1833
- schemas[schemaName].required = required;
1833
+ schemas[schemaName].required = [...new Set(required)];
1834
1834
 
1835
1835
  const description = type[voc.Core.Description];
1836
1836
  if (description) {
@@ -3,7 +3,7 @@ const fs = require('fs')
3
3
  const path = require('path')
4
4
 
5
5
  const hardcoded = {
6
- Scopes: ['system-user', 'authenticated-user', 'registered-user', 'identified-user', 'internal-user', 'any'],
6
+ Scopes: ['system-user', 'authenticated-user', 'registered-user', 'identified-user', 'internal-user', 'privileged-user', 'any'],
7
7
  Attributes: ['id', 'tenant', 'ext_attr']
8
8
  }
9
9