@sap/cds 5.7.5 → 5.8.0

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 (141) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/app/fiori/routes.js +1 -1
  3. package/bin/deploy/to-hana/cfUtil.js +251 -138
  4. package/bin/deploy/to-hana/gitUtil.js +55 -0
  5. package/bin/deploy/to-hana/hana.js +92 -93
  6. package/bin/deploy/to-hana/hdiDeployUtil.js +42 -27
  7. package/bin/deploy/to-hana/index.js +14 -13
  8. package/bin/mtx/in-cds.js +1 -0
  9. package/bin/serve.js +1 -1
  10. package/bin/version.js +1 -0
  11. package/lib/compile/cdsc.js +0 -6
  12. package/lib/compile/resolve.js +1 -1
  13. package/lib/compile/to/srvinfo.js +1 -1
  14. package/lib/core/classes.js +21 -1
  15. package/lib/env/index.js +3 -2
  16. package/lib/env/requires.js +4 -0
  17. package/lib/i18n/localize.js +5 -8
  18. package/lib/index.js +1 -0
  19. package/lib/log/errors.js +1 -1
  20. package/lib/ql/SELECT.js +2 -2
  21. package/lib/req/cds-context.js +1 -1
  22. package/lib/req/context.js +1 -1
  23. package/lib/serve/Transaction.js +9 -5
  24. package/lib/serve/index.js +13 -21
  25. package/lib/utils/tests.js +90 -66
  26. package/libx/_runtime/audit/generic/personal/modification.js +0 -8
  27. package/libx/_runtime/auth/index.js +7 -6
  28. package/libx/_runtime/auth/strategies/dwc.js +43 -0
  29. package/libx/_runtime/auth/utils.js +24 -0
  30. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -32
  31. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -38
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +1 -2
  38. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +0 -1
  39. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -2
  40. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +9 -0
  41. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +17 -30
  42. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +12 -1
  43. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +2 -1
  44. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
  45. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +2 -5
  46. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
  47. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
  48. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +1 -1
  52. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  53. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  54. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  55. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -4
  56. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  57. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  58. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +60 -18
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +7 -5
  60. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  61. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  62. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  63. package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
  64. package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
  65. package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
  66. package/libx/_runtime/common/aspects/Association.js +16 -0
  67. package/libx/_runtime/common/composition/data.js +28 -37
  68. package/libx/_runtime/common/composition/delete.js +107 -58
  69. package/libx/_runtime/common/composition/index.js +2 -1
  70. package/libx/_runtime/common/composition/insert.js +13 -13
  71. package/libx/_runtime/common/composition/update.js +39 -34
  72. package/libx/_runtime/common/error/frontend.js +17 -2
  73. package/libx/_runtime/common/generic/auth.js +20 -85
  74. package/libx/_runtime/common/generic/crud.js +22 -1
  75. package/libx/_runtime/common/i18n/messages.properties +2 -1
  76. package/libx/_runtime/common/utils/cqn.js +2 -6
  77. package/libx/_runtime/common/utils/cqn2cqn4sql.js +95 -122
  78. package/libx/_runtime/common/utils/csn.js +14 -3
  79. package/libx/_runtime/common/utils/foreignKeyPropagations.js +18 -1
  80. package/libx/_runtime/common/utils/keys.js +2 -1
  81. package/libx/_runtime/common/utils/path.js +1 -1
  82. package/libx/_runtime/common/utils/resolveView.js +12 -4
  83. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  84. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  85. package/libx/_runtime/common/utils/vcap.js +27 -10
  86. package/libx/_runtime/db/data-conversion/post-processing.js +20 -13
  87. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  88. package/libx/_runtime/db/expand/expandCQNToJoin.js +8 -6
  89. package/libx/_runtime/db/expand/index.js +3 -0
  90. package/libx/_runtime/db/generic/create.js +0 -10
  91. package/libx/_runtime/db/generic/index.js +3 -0
  92. package/libx/_runtime/db/generic/read.js +2 -24
  93. package/libx/_runtime/db/generic/rewrite.js +1 -3
  94. package/libx/_runtime/db/generic/update.js +1 -1
  95. package/libx/_runtime/db/query/delete.js +10 -4
  96. package/libx/_runtime/db/query/insert.js +3 -3
  97. package/libx/_runtime/db/query/read.js +4 -1
  98. package/libx/_runtime/db/query/update.js +5 -5
  99. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  100. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  101. package/libx/_runtime/db/sql-builder/index.js +3 -0
  102. package/libx/_runtime/db/utils/columns.js +5 -2
  103. package/libx/_runtime/db/utils/deep.js +6 -8
  104. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  105. package/libx/_runtime/fiori/generic/before.js +73 -49
  106. package/libx/_runtime/fiori/generic/edit.js +14 -18
  107. package/libx/_runtime/fiori/generic/patch.js +8 -11
  108. package/libx/_runtime/fiori/generic/read.js +19 -16
  109. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  110. package/libx/_runtime/hana/Service.js +1 -1
  111. package/libx/_runtime/hana/conversion.js +10 -0
  112. package/libx/_runtime/hana/execute.js +33 -16
  113. package/libx/_runtime/hana/search.js +3 -3
  114. package/libx/_runtime/hana/search2cqn4sql.js +22 -21
  115. package/libx/_runtime/hana/searchToContains.js +1 -1
  116. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  117. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  118. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  119. package/libx/_runtime/messaging/file-based.js +3 -1
  120. package/libx/_runtime/messaging/service.js +4 -1
  121. package/libx/_runtime/remote/utils/client.js +33 -20
  122. package/libx/_runtime/remote/utils/data.js +52 -11
  123. package/libx/_runtime/sqlite/Service.js +1 -1
  124. package/libx/_runtime/sqlite/conversion.js +10 -0
  125. package/libx/_runtime/types/api.js +2 -2
  126. package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
  127. package/libx/odata/afterburner.js +29 -6
  128. package/libx/odata/cqn2odata.js +9 -0
  129. package/libx/odata/grammar.pegjs +49 -21
  130. package/libx/odata/index.js +2 -2
  131. package/libx/odata/parser.js +1 -1
  132. package/libx/odata/utils.js +2 -2
  133. package/libx/rest/RestAdapter.js +29 -1
  134. package/libx/rest/middleware/auth.js +1 -3
  135. package/libx/rest/middleware/parse.js +1 -0
  136. package/package.json +1 -1
  137. package/server.js +1 -1
  138. package/bin/deploy/to-hana/logger.js +0 -27
  139. package/bin/deploy/to-hana/runCommand.js +0 -113
  140. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
  141. package/libx/_runtime/common/utils/auth.js +0 -16
package/CHANGELOG.md CHANGED
@@ -4,6 +4,78 @@
4
4
  - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## Version 5.8.0 - 2022-01-27
8
+
9
+ ### Added
10
+
11
+ - Custom `server.js` don't have to export `cds.server` anymore -> we use that by default now.
12
+ - In `cds.requires`: Support to replace primitive values with objects
13
+ - Support filter functions on renamed properties from external service
14
+ - Results of database queries use `big.js` for values of type `cds.Decimal` and `cds.Integer64` if enabled via `cds.env.features.bigjs`
15
+ - Support lambda in `$filter` in `$expand`
16
+ - Support for `GET` requests on service root in REST adapter (old and new)
17
+ - Support for `HEAD` requests in REST adapter (old and new)
18
+ - New hook `req.before('commit')`
19
+ - Draft (Access control for bound actions): Only the user that is the owner of the draft can execute its bound actions.
20
+ - Check that all keys are provided in REST adapter
21
+ - Restrict access to all services via `cds.env.requires.auth.restrict_all_services = true`
22
+ + That is, all unrestricted services (i.e., w/o own `@requires`) are treated as having `@requires: 'authenticated-user'`
23
+ - Threshold for automatically sending GET requests as `$batch` (beta, cf. @sap/cds@5.6.0) can be configured per remote service via `cds.env.requires.<srv>.max_get_url_length` (if not configured on service, the global config applies)
24
+ - Alpha out-of-the-box support for DwC
25
+ + Authentication based on headers set by Jupiter router via `cds.env.requires.auth.kind = 'dwc-auth'`
26
+ + All DwC headers are forwarded to remote service via `cds.env.requires.<srv>.forward_dwc_headers = true`
27
+ - Limited support for binary data in OData
28
+ + In payloads, the binary data must be a base64 encoded string
29
+ + In URLs, the binary data must have the following format: `binary'<url-safe base64 encoded>'`, e.g., `$filter=ID eq binary'Q0FQIE5vZGUuanM='`
30
+ + The use of binary data in some advanced constructs like `$apply` and `/any()` may be limited
31
+ + On SQLite, the base64 encoded string is stored to the database
32
+ + It is strongly discouraged to use binary data as keys. See "Primary Keys — Best Practices" in documentation.
33
+ - Support for OData annotation `@Core.ContentDisposition.Type` with `attachment` as the default value
34
+ - Support for returning custom stream objects in custom handlers (beta):
35
+ + Example:
36
+ ```js
37
+ return {
38
+ value: instanceof Readable || null,
39
+ $mediaContentType = 'image/jpeg',
40
+ $mediaContentDispositionFilename = 'foo.bar', // > optional
41
+ $mediaContentDispositionType = 'inline' // > optional
42
+ }
43
+ ```
44
+
45
+ ### Changed
46
+
47
+ - `cds deploy --to hana` now uses `cf curl` instead of `cf` command natively
48
+ - Event Mesh: In multitenancy mode, messaging artifacts are also deployed for provider accounts (unless the service option `deployForProvider` is set to `false`)
49
+ - Status code in case of multiple errors (rules apply in order):
50
+ + If all errors have the same status code, that status code is used
51
+ + If there is at least one 5xx status code, the resulting status code is 500
52
+ + If there is at least one 4xx status code, the resulting status code is 400
53
+ + If none of the rules apply, the resulting status code is 500
54
+ - Ignore the `If-Match` HTTP request header for `UPDATE`/`DELETE` requests whose target entities are not annotated with the `@odata.etag` annotation.
55
+ - I18n template strings now are replaced in EDMX documents such that they can occur multiple times. For example, the `{i18n>key1} - {i18n>key2}` template results in `value1 - value2`, while previously only the first string was replaced, leading to `value1 - {i18n>key2}`. This is helpful for the [`Template` strings of `UI.ConnectedFields`](https://github.com/SAP/odata-vocabularies/blob/ac9fe832df9b8c8d35517c637dba7c0ac2753b0f/vocabularies/UI.xml#L168).
56
+
57
+ ### Fixed
58
+
59
+ - At Node.js runtime, the `development` configuration profile is no longer active if `CDS_ENV` is set to `production` and `NODE_ENV` is undefined
60
+ - Enterprise Messaging: The user is now privileged for AMQP
61
+ - `cds.spawn` also works with synchronous functions
62
+ - Foreign keys in parent are set to `null` when deleting composition of one
63
+ - `cds version` now always prints the version of `@sap/cds-dk`, especially if `cds version` was called from within an npm script, i.e. not from `cds-dk`'s CLI.
64
+ - Better error message in case destination of Remote Service is not found
65
+ - Differentiate between draft already exists and entity locked
66
+ - OData adapter: rollback transaction before rethrowing standard error in case of atomicty group
67
+ - Results of actions/functions do not ignore custom data when using `$expand` query option
68
+ - `req.data` is available in custom error handler in case of deserialization error thrown by legacy odata server
69
+ - Joining entities with renamed foreign keys (limited to single-level projections)
70
+ - Requests with draft and `$expand=*` caused problems in some cases
71
+ - `cds serve` during development longer redirects URLs with similar path segments like `/browse/123/browse/` to e.g. `/browse/`
72
+ - Post processing for renamed column in expand
73
+ - Deploy to HANA: passing of options to `hdi-deploy` via `HDI_DEPLOY_OPTIONS` now possible
74
+ - Keys as path segments in beta OData to CQN parser
75
+ - OData V2 Remote Service (`"kind": "odata-v2"`):
76
+ + Request data properties of types `cds.Date`, `cds.DateTime` and `cds.Timestamp` are converted accordingly to OData V2 specification
77
+ + Response data properties of types `cds.Decimal`, `cds.DecimalFloat` (deprecated) and `cds.Integer64` are handled properly when using `Accept` header with `IEEE754Compatible=true/false` and `ExponentialDecimals=true/false` format parameters
78
+
7
79
  ## Version 5.7.5 - 2022-01-14
8
80
 
9
81
  ### Fixed
@@ -25,7 +25,7 @@ cds.on ('bootstrap', app => {
25
25
  if (v2Index >= 0) // --> /browse/webapp[/prefix]/v2/browse/ -> /v2/browse
26
26
  redirectUrl = originalUrl.substring(v2Index)
27
27
  else // --> /browse/webapp[/prefix]/browse/ -> /browse
28
- redirectUrl = originalUrl.substring(originalUrl.lastIndexOf(srv.path))
28
+ redirectUrl = originalUrl.substring(originalUrl.lastIndexOf(srv.path+'/'))
29
29
  if (originalUrl !== redirectUrl) {// safeguard to prevent running in loops
30
30
  // console.log ('>>', req.originalUrl, '->', redirectUrl)
31
31
  return res.redirect (308, redirectUrl)
@@ -1,179 +1,292 @@
1
+ const cp = require('child_process');
1
2
 
2
- const runCommand = require('./runCommand');
3
- const { nullLogger } = require('./logger');
3
+ const cds = require('../../../lib');
4
+ const LOG = cds.log ? cds.log('deploy') : console;
5
+ const DEBUG = cds.debug('deploy');
4
6
 
5
- const POLL_TIMEOUT = 180 * 1000; //ms
6
- const POLL_RETRY_TIMEOUT = 1000; //ms, since polling takes 1-2sec, repoll almost immediately
7
+ const { bold } = require('../../utils/term');
8
+
9
+ const CF_COMMAND = 'cf';
10
+
11
+ const POLL_COUNTER = 40;
12
+ const POLL_DELAY = 2500; //ms
7
13
 
8
- const SPINNER_CHARS = ['-', '\\', '|', '/'];
9
14
 
10
15
  class CfUtil {
11
- /**
12
- *
13
- * @param {*} serviceName
14
- * @param {*} logger
15
- * @returns service, or undefined
16
- */
17
- static async getService(serviceName, logger) {
18
- try {
19
- const serviceCmd = await runCommand('cf', ['service', serviceName], logger);
20
-
21
- let serviceInfo;
22
- if (serviceCmd.code === 0) {
23
- serviceInfo = {
24
- name: serviceName,
25
- status: /^\s*status\s*:\s*(.*)$/mi.exec(serviceCmd.stdout)[1],
26
- service: /^\s*service\s*:\s*(.*)$/mi.exec(serviceCmd.stdout)[1]
27
- }
28
- }
29
16
 
30
- return serviceInfo;
31
- } catch (err) {
32
- throw this._getError(err);
33
- }
17
+ _clear() {
18
+ this.spaceInfo = null;
34
19
  }
35
20
 
36
- static async _pollService(serviceName, startTime, logger = nullLogger) {
37
- return new Promise((resolve, reject) => {
38
- this._pollServiceTimer(serviceName, startTime, resolve, reject, logger);
21
+ async _sleep(ms) {
22
+ return new Promise((resolve) => {
23
+ setTimeout(resolve, ms);
39
24
  });
40
25
  }
41
26
 
42
- static _pollServiceTimer(serviceName, startTime, resolve, reject, logger) {
43
- setTimeout(async () => {
44
- try {
45
- if (Date.now() - startTime >= POLL_TIMEOUT) {
46
- logger.log();
47
- reject(new Error(`[cds.deploy] - Aborting service creation after ${POLL_TIMEOUT / 1000}sec.`));
48
- }
27
+ async _cfRun(...args) {
28
+ const cmdLine = `${CF_COMMAND} ${args.join(' ')}`;
29
+ DEBUG && console.time(cmdLine);
49
30
 
50
- const serviceCmd = await runCommand('cf', ['service', serviceName]);
51
- if (serviceCmd.code !== 0) {
52
- reject(new Error(`[cds.deploy] - Getting service info failed with code ${serviceCmd.code}.`));
53
- }
31
+ try {
32
+ return await new Promise((resolve, reject) => {
33
+ const child = cp.spawn(CF_COMMAND, args);
54
34
 
55
- if (serviceCmd.stdout.includes('failed')) {
56
- logger.log();
57
- logger.log(serviceCmd.stdout);
58
- reject(new Error(`[cds.deploy] - Service creation failed.`));
59
- }
35
+ let stdout = '';
36
+ child.stdout.on('data', (data) => {
37
+ stdout += data;
38
+ });
60
39
 
61
- if (serviceCmd.stdout.includes('succeeded')) {
62
- logger.log();
63
- logger.log(serviceCmd.stdout);
64
- return resolve();
65
- }
40
+ let stderr = '';
41
+ child.stderr.on('data', (data) => {
42
+ stderr += data;
43
+ });
44
+
45
+ child.on('error', (err) => {
46
+ if (err.code === 'ENOENT') {
47
+ reject(new Error(`Command ${bold(CF_COMMAND)} not found. Make sure to install the Cloud Foundry Command Line Interface.`));
48
+ } else {
49
+ reject(err);
50
+ }
51
+ });
52
+
53
+ child.on('close', (code) => {
54
+ if (!code) {
55
+ resolve(stdout.trim());
56
+ } else {
57
+ const errorMessage = `${stdout}\n${stderr}`;
58
+ reject(new Error(errorMessage.trim()));
59
+ }
60
+ });
61
+ });
62
+ } finally {
63
+ DEBUG && console.timeEnd(cmdLine);
64
+ }
65
+ }
66
+
67
+ async _cfRequest(urlPath, queryObj, bodyObj) {
68
+ if (queryObj) {
69
+ const entries = Object.entries(queryObj);
70
+ const queryStr = entries.map((entry) => `${entry[0]}=${encodeURIComponent(entry[1])}`).join('&');
71
+ urlPath = urlPath + `?${queryStr}`;
72
+ }
73
+
74
+ // cf curl PATH [-iv] [-X METHOD] [-H HEADER]... [-d DATA] [--output FILE]
75
+ const args = ['curl', urlPath];
76
+ if (bodyObj) {
77
+ args.push('-d');
78
+ args.push(JSON.stringify(bodyObj)); // cfRun uses spawn so no special handling for quotes on cli required
79
+ }
80
+
81
+ const response = await this._cfRun(...args);
82
+ const result = response ? JSON.parse(response) : {};
83
+ if (result.errors) {
84
+ const errorMessage = result.errors.map((entry) => `${entry.title}: ${entry.detail} (${entry.code})`).join('\n');
85
+ throw new Error(errorMessage);
86
+ }
87
+ return result;
88
+ }
66
89
 
67
- this._pollServiceTimer(serviceName, startTime, resolve, reject, logger);
68
- } catch (err) {
69
- reject(err);
90
+ _extract(string, pattern, defaultValue) {
91
+ const array = string.split(/[\n\r]+/);
92
+ for (let line of array) {
93
+ const match = line.match(pattern);
94
+ if (match) {
95
+ return match[1];
70
96
  }
71
- }, POLL_RETRY_TIMEOUT);
97
+ }
98
+
99
+ if (typeof defaultValue === 'undefined') {
100
+ throw Error(`Pattern not found: ${pattern}`);
101
+ } else {
102
+ return defaultValue;
103
+ }
72
104
  }
73
105
 
74
- /**
75
- * Triggers service creation and resolves once service is created or rejects in case of errors
76
- *
77
- * @param {*} service
78
- * @param {*} plan
79
- * @param {*} serviceName
80
- * @param {*} options
81
- * @param {*} logger
82
- */
83
- static async createService(service, plan, serviceName, options, logger) {
84
- // cf create-service SERVICE PLAN SERVICE_INSTANCE
85
- // eslint-disable-next-line no-async-promise-executor
86
- let spinnerId;
87
- try {
88
- const cfArgs = ['create-service', service, plan, serviceName];
89
- if (options) {
90
- cfArgs.push('-c');
91
- cfArgs.push(JSON.stringify(options));
106
+ async getCfTargetFromCli() {
107
+ const result = await this._cfRun('target');
108
+
109
+ return {
110
+ apiEndpoint: this._extract(result, /api endpoint:\s+(.*)$/i),
111
+ user: this._extract(result, /user:\s+(.*)$/i),
112
+ org: this._extract(result, /org:\s+(.*)$/i),
113
+ space: this._extract(result, /space:\s+(.*)$/i),
114
+ };
115
+ }
116
+
117
+ async getCfSpaceInfo() {
118
+ if (!this.spaceInfo) {
119
+ LOG.debug('getting space info');
120
+
121
+ const target = await this.getCfTargetFromCli();
122
+
123
+ const { org, space } = target;
124
+ const orgs = await this._cfRequest(`/v3/organizations`, { names: org });
125
+ if (!orgs.resources || orgs.resources.length !== 1) {
126
+ throw new Error(`CF org ${bold(org)} not found!`);
92
127
  }
93
- const createServiceCmd = await runCommand('cf', cfArgs, logger);
94
- if (createServiceCmd) {
95
- if (createServiceCmd.code !== 0) {
96
- const err = new Error('[cds.deploy] - Create request failed')
97
- err.command = createServiceCmd
98
- throw err;
99
- }
100
128
 
101
- if (createServiceCmd.stdout.includes('already exists')) {
102
- return;
103
- }
129
+ const orgGuid = orgs.resources[0].guid;
130
+ const spaces = await this._cfRequest(`/v3/spaces`, { names: space, organization_guids: orgGuid });
131
+ if (!spaces.resources || spaces.resources.length !== 1) {
132
+ throw new Error(`CF space ${bold(space)} not found in org ${bold(org)}!`);
104
133
  }
105
134
 
106
- spinnerId = this._startSpinner(logger);
107
- return await CfUtil._pollService(serviceName, Date.now(), logger);
108
- } catch (err) {
109
- throw this._getError(err);
110
- } finally {
111
- this._stopSpinner(spinnerId);
135
+ const spaceGuid = spaces.resources[0].guid;
136
+
137
+ this.spaceInfo = Object.assign({}, target, { orgGuid, spaceGuid });
112
138
  }
113
- }
114
139
 
115
- static _startSpinner(logger) {
116
- let idx = 0;
117
- return setInterval(() => {
118
- logger.write(SPINNER_CHARS[idx], true);
119
- idx = (idx + 1) % SPINNER_CHARS.length;
120
- }, 400);
140
+ return this.spaceInfo;
121
141
  }
122
142
 
123
- static _stopSpinner(id) {
124
- if (id) {
125
- clearInterval(id);
143
+ async getService(serviceName, showMessage = true) {
144
+ showMessage && LOG.log(`Getting service ${bold(serviceName)}`);
145
+ const spaceInfo = await this.getCfSpaceInfo();
146
+
147
+ let counter = POLL_COUNTER;
148
+ while (counter > 0) {
149
+ counter--;
150
+ const serviceInstances = await this._cfRequest('/v3/service_instances', {
151
+ names: serviceName,
152
+ space_guids: spaceInfo.spaceGuid,
153
+ organization_guids: spaceInfo.orgGuid
154
+ });
155
+ if (!serviceInstances || !serviceInstances.resources || serviceInstances.resources.length < 1) {
156
+ return null;
157
+ }
158
+
159
+ const serviceInstance = serviceInstances.resources[0];
160
+ if (serviceInstance && serviceInstance.last_operation && serviceInstance.last_operation.state !== 'in progress') {
161
+ return serviceInstance;
162
+ }
163
+
164
+ await this._sleep(POLL_DELAY);
126
165
  }
166
+
167
+ throw new Error(`Timeout occurred while getting service ${bold(serviceName)}`);
127
168
  }
128
169
 
129
- /**
130
- * Creates the service key
131
- *
132
- * @param {*} serviceInstance
133
- * @param {*} serviceKeyName
134
- * @param {*} logger
135
- * @returns service key
136
- */
137
- static async createServiceKey(serviceInstance, serviceKeyName, logger) {
138
- try {
139
- const createServiceKeyCmd = await runCommand('cf', ['create-service-key', serviceInstance, serviceKeyName, '-c', `{"permissions":"development"}`], logger);
140
- if (createServiceKeyCmd.code === 0) {
141
- return await this.getServiceKey(serviceInstance, serviceKeyName);
170
+
171
+ async getOrCreateService(serviceOfferingName, planName, serviceName, options) {
172
+
173
+ const probeService = await this.getService(serviceName, false);
174
+ if (probeService) {
175
+ LOG.log(`Getting service ${bold(serviceName)}`);
176
+ return probeService;
177
+ }
178
+
179
+ LOG.log(`Creating service ${bold(serviceName)} - please be patient...`);
180
+
181
+ const spaceInfo = await this.getCfSpaceInfo();
182
+
183
+ const servicePlan = await this._cfRequest(`/v3/service_plans`, {
184
+ names: planName,
185
+ space_guids: spaceInfo.spaceGuid,
186
+ organization_guids: spaceInfo.orgGuid,
187
+ service_offering_names: serviceOfferingName
188
+ });
189
+
190
+ if (!servicePlan.resources || servicePlan.resources.length === 0) {
191
+ throw new Error(`No service plans found`);
192
+ }
193
+
194
+ const body = {
195
+ type: 'managed',
196
+ name: serviceName,
197
+ tags: [serviceOfferingName],
198
+ relationships: {
199
+ space: {
200
+ data: {
201
+ guid: spaceInfo.spaceGuid
202
+ }
203
+ },
204
+ service_plan: {
205
+ data: {
206
+ guid: servicePlan.resources[0].guid
207
+ }
208
+ }
142
209
  }
210
+ }
211
+
212
+ if (options) {
213
+ body.parameters = { ...options };
214
+ }
215
+
216
+ const postResult = await this._cfRequest('/v3/service_instances', undefined, body);
217
+ if (postResult.errors) {
218
+ throw new Error(postResult.errors[0].detail);
219
+ }
143
220
 
144
- return null;
145
- } catch (err) {
146
- throw this._getError(err);
221
+ const newService = await this.getService(serviceName, false);
222
+ if (newService) {
223
+ return newService;
147
224
  }
225
+
226
+ throw new Error(`Could not create service ${bold(serviceName)}`);
148
227
  }
149
228
 
150
- /**
151
- * Get the service key
152
- *
153
- * @param {*} serviceInstance
154
- * @param {*} serviceKeyName
155
- * @returns service key or null
156
- */
157
- static async getServiceKey(serviceInstance, serviceKeyName) {
158
- try {
159
- // use null logger to avoid disclosing the key to console
160
- const getServiceKeyCmd = await runCommand('cf', ['service-key', serviceInstance, serviceKeyName], nullLogger);
161
- const keyJsonStr = getServiceKeyCmd.stdout.match(/\{([\s\S]*)\}/);
162
- return JSON.parse(keyJsonStr[0]);
163
- } catch (err) {
164
- return null;
229
+
230
+ async getServiceKey(serviceInstance, serviceKeyName, showMessage = true) {
231
+ showMessage && LOG.log(`Getting service key ${bold(serviceKeyName)}`);
232
+
233
+ let counter = POLL_COUNTER;
234
+ while (counter > 0) {
235
+ counter--;
236
+ const bindings = await this._cfRequest(`/v3/service_credential_bindings`, { names: serviceKeyName, service_instance_guids: serviceInstance.guid });
237
+ if (!bindings || !bindings.resources || bindings.resources.length < 1) {
238
+ return null;
239
+ }
240
+ const binding = bindings.resources[0];
241
+ if (binding && binding.last_operation && binding.last_operation.state !== 'in progress') {
242
+ const keyDetails = await this._cfRequest(`/v3/service_credential_bindings/${encodeURIComponent(binding.guid)}/details`);
243
+ return keyDetails.credentials;
244
+ }
245
+
246
+ await this._sleep(POLL_DELAY);
165
247
  }
248
+
249
+ throw new Error(`Timeout occurred while getting service key ${bold(serviceKeyName)}`);
166
250
  }
167
251
 
168
- static _getError(err) {
169
- if (err.code === 'ENOENT') {
170
- return new Error(`Command 'cf' not found. Make sure you have the Cloud Foundry command line tool installed.`);
171
- } else {
172
- return err;
252
+
253
+ async getOrCreateServiceKey(serviceInstance, serviceKeyName) {
254
+
255
+ const serviceKey = await this.getServiceKey(serviceInstance, serviceKeyName, false);
256
+ if (serviceKey) {
257
+ LOG.log(`Getting service key ${bold(serviceKeyName)}`);
258
+ return serviceKey;
259
+ }
260
+
261
+ LOG.log(`Creating service key ${bold(serviceKeyName)} - please be patient...`);
262
+
263
+ const body = {
264
+ type: 'key',
265
+ name: serviceKeyName,
266
+ relationships: {
267
+ service_instance: {
268
+ data: {
269
+ guid: serviceInstance.guid
270
+ }
271
+ }
272
+ },
273
+ parameters: {
274
+ permissions: 'development'
275
+ }
276
+ }
277
+
278
+ const postResult = await this._cfRequest('/v3/service_credential_bindings', undefined, body);
279
+ if (postResult.errors) {
280
+ throw new Error(postResult.errors[0].detail);
281
+ }
282
+
283
+ const newServiceKey = await this.getServiceKey(serviceInstance, serviceKeyName, false);
284
+ if (newServiceKey) {
285
+ return newServiceKey;
173
286
  }
287
+
288
+ throw new Error(`Could not create service key ${bold(serviceKeyName)}`);
174
289
  }
175
290
  }
176
291
 
177
- module.exports = CfUtil;
178
-
179
- /* eslint no-console: off */
292
+ module.exports = new CfUtil();
@@ -0,0 +1,55 @@
1
+ const cp = require('child_process');
2
+ const path = require('path');
3
+ const { fse } = require('@sap/cds-foss');
4
+ const util = require('util');
5
+
6
+ const cds = require('../../../lib');
7
+ const LOG = cds.log ? cds.log('deploy') : console;
8
+
9
+ const GIT_IGNORE_FILE = '.gitignore';
10
+
11
+ class GitUtil {
12
+
13
+ constructor() {
14
+ this.execAsync = util.promisify(cp.exec);
15
+ }
16
+
17
+ async ensureFileIsGitignored(file, currentFolder = process.cwd()) {
18
+ const gitIgnorePath = path.join(currentFolder, GIT_IGNORE_FILE);
19
+
20
+ try {
21
+ await this.execAsync(`git check-ignore -n -v ${file}`, {
22
+ cwd: currentFolder
23
+ });
24
+ return; // file is git ignored
25
+ } catch (err) {
26
+ if (err.code === 'ENOENT') { // git command not found
27
+ LOG.debug('git command not found');
28
+ return;
29
+ }
30
+
31
+ if (!err.stdout.match(/::/)) { // included or not a git repo
32
+ LOG.debug(err.message);
33
+ return;
34
+ }
35
+ }
36
+
37
+ LOG.log(`adding entry '${file}' to ${GIT_IGNORE_FILE}.`);
38
+ let gitignore = await this.readFileSafely(gitIgnorePath);
39
+ gitignore = gitignore + `
40
+ # added by cds deploy
41
+ ${file}
42
+ `;
43
+ await fse.outputFile(gitIgnorePath, gitignore, 'utf8');
44
+ }
45
+
46
+ async readFileSafely(file) {
47
+ try {
48
+ return await fse.readFile(file, 'utf8');
49
+ } catch (err) {
50
+ return '';
51
+ }
52
+ }
53
+ }
54
+
55
+ module.exports = new GitUtil();