@sap/cds 5.7.5 → 5.8.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.
Files changed (151) hide show
  1. package/CHANGELOG.md +97 -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/log/format/kibana.js +3 -3
  21. package/lib/ql/SELECT.js +2 -2
  22. package/lib/req/cds-context.js +1 -1
  23. package/lib/req/context.js +1 -1
  24. package/lib/serve/Transaction.js +9 -5
  25. package/lib/serve/index.js +13 -21
  26. package/lib/utils/tests.js +90 -66
  27. package/libx/_runtime/audit/generic/personal/modification.js +0 -8
  28. package/libx/_runtime/auth/index.js +7 -6
  29. package/libx/_runtime/auth/strategies/dwc.js +43 -0
  30. package/libx/_runtime/auth/utils.js +24 -0
  31. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -3
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -32
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -38
  37. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  38. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
  39. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +1 -2
  40. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +0 -1
  41. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -2
  42. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +9 -0
  43. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +17 -30
  44. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +12 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +2 -1
  46. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +23 -2
  47. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
  48. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +2 -5
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
  51. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
  52. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
  53. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
  54. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +2 -2
  55. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ErrorJsonSerializer.js +2 -0
  56. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  57. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  58. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -4
  60. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  61. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +60 -18
  63. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +47 -10
  64. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  65. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  66. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  67. package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
  68. package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
  69. package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
  70. package/libx/_runtime/common/aspects/Association.js +16 -0
  71. package/libx/_runtime/common/composition/data.js +28 -37
  72. package/libx/_runtime/common/composition/delete.js +107 -58
  73. package/libx/_runtime/common/composition/index.js +3 -3
  74. package/libx/_runtime/common/composition/insert.js +14 -27
  75. package/libx/_runtime/common/composition/tree.js +1 -1
  76. package/libx/_runtime/common/composition/update.js +39 -34
  77. package/libx/_runtime/common/error/frontend.js +19 -5
  78. package/libx/_runtime/common/generic/auth.js +20 -85
  79. package/libx/_runtime/common/generic/crud.js +22 -1
  80. package/libx/_runtime/common/i18n/messages.properties +2 -1
  81. package/libx/_runtime/common/utils/cqn.js +2 -6
  82. package/libx/_runtime/common/utils/cqn2cqn4sql.js +95 -122
  83. package/libx/_runtime/common/utils/csn.js +29 -6
  84. package/libx/_runtime/common/utils/foreignKeyPropagations.js +21 -1
  85. package/libx/_runtime/common/utils/keys.js +2 -1
  86. package/libx/_runtime/common/utils/path.js +1 -1
  87. package/libx/_runtime/common/utils/resolveView.js +12 -4
  88. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  89. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  90. package/libx/_runtime/common/utils/structured.js +10 -4
  91. package/libx/_runtime/common/utils/vcap.js +27 -10
  92. package/libx/_runtime/db/data-conversion/post-processing.js +20 -13
  93. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  94. package/libx/_runtime/db/expand/expandCQNToJoin.js +67 -26
  95. package/libx/_runtime/db/expand/index.js +3 -0
  96. package/libx/_runtime/db/generic/create.js +0 -10
  97. package/libx/_runtime/db/generic/index.js +3 -0
  98. package/libx/_runtime/db/generic/read.js +2 -24
  99. package/libx/_runtime/db/generic/rewrite.js +1 -3
  100. package/libx/_runtime/db/generic/update.js +1 -1
  101. package/libx/_runtime/db/query/delete.js +10 -4
  102. package/libx/_runtime/db/query/insert.js +3 -4
  103. package/libx/_runtime/db/query/read.js +4 -1
  104. package/libx/_runtime/db/query/update.js +5 -5
  105. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  106. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  107. package/libx/_runtime/db/sql-builder/SelectBuilder.js +7 -3
  108. package/libx/_runtime/db/sql-builder/index.js +3 -0
  109. package/libx/_runtime/db/utils/columns.js +5 -2
  110. package/libx/_runtime/db/utils/deep.js +16 -14
  111. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  112. package/libx/_runtime/fiori/generic/before.js +73 -49
  113. package/libx/_runtime/fiori/generic/edit.js +14 -18
  114. package/libx/_runtime/fiori/generic/patch.js +8 -11
  115. package/libx/_runtime/fiori/generic/read.js +20 -19
  116. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  117. package/libx/_runtime/fiori/utils/handler.js +1 -11
  118. package/libx/_runtime/hana/Service.js +1 -1
  119. package/libx/_runtime/hana/conversion.js +12 -1
  120. package/libx/_runtime/hana/dynatrace.js +11 -5
  121. package/libx/_runtime/hana/execute.js +132 -19
  122. package/libx/_runtime/hana/search.js +3 -3
  123. package/libx/_runtime/hana/search2cqn4sql.js +23 -25
  124. package/libx/_runtime/hana/searchToContains.js +1 -1
  125. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  126. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  127. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  128. package/libx/_runtime/messaging/file-based.js +3 -1
  129. package/libx/_runtime/messaging/service.js +4 -1
  130. package/libx/_runtime/remote/utils/client.js +41 -24
  131. package/libx/_runtime/remote/utils/data.js +54 -12
  132. package/libx/_runtime/sqlite/Service.js +1 -1
  133. package/libx/_runtime/sqlite/conversion.js +10 -0
  134. package/libx/_runtime/types/api.js +2 -2
  135. package/libx/gql/resolvers/crud/update.js +8 -5
  136. package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
  137. package/libx/odata/afterburner.js +29 -6
  138. package/libx/odata/cqn2odata.js +9 -0
  139. package/libx/odata/grammar.pegjs +49 -21
  140. package/libx/odata/index.js +2 -2
  141. package/libx/odata/parser.js +1 -1
  142. package/libx/odata/utils.js +2 -2
  143. package/libx/rest/RestAdapter.js +29 -1
  144. package/libx/rest/middleware/auth.js +1 -3
  145. package/libx/rest/middleware/parse.js +1 -0
  146. package/package.json +1 -1
  147. package/server.js +1 -1
  148. package/bin/deploy/to-hana/logger.js +0 -27
  149. package/bin/deploy/to-hana/runCommand.js +0 -113
  150. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
  151. package/libx/_runtime/common/utils/auth.js +0 -16
package/CHANGELOG.md CHANGED
@@ -4,6 +4,103 @@
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.2 - 2022-02-22
8
+
9
+ ### Fixed
10
+
11
+ - Crash if error does not have a stack in kibana logging
12
+ - Allow short names for bound operations in odata-server
13
+ - Performance issue during deep operations
14
+ - Resolving views with parameters
15
+ - Expanding association-to-many within draft union scenario
16
+ - Erroneous invalidation of deep `INSERT|UPDATE|DELETE` operations if root entity has managed to-one association to non-writable view
17
+ - Handling of falsy results when sending requests to remote services
18
+ - Resolving foreign key propagations for views with union
19
+
20
+ ## Version 5.8.1 - 2022-02-11
21
+
22
+ ### Fixed
23
+
24
+ - Use single transaction for update mutations in GraphQL adapter
25
+ - ODATA to CQN parser returned not selected keys in `@odata.context`
26
+ - Draft: `$expand` with special draft columns in `$orderBy` for active entities
27
+ - Reading distinct values of draft enabled entity
28
+ - Handling of LOB data on HANA
29
+ - Fix streaming draft by navigation
30
+ - Empty to-many arrays are not removed from req.data for inserts
31
+ - `$filter` query option in structured mode (OData flavors `w4` and `x4`)
32
+ + Using JSON-stringified objects no longer occasionally crashes an application
33
+ + Filtering on a structured element with `ne null` condition also selects data having some `null` properties within
34
+
35
+ ## Version 5.8.0 - 2022-01-27
36
+
37
+ ### Added
38
+
39
+ - Custom `server.js` don't have to export `cds.server` anymore -> we use that by default now.
40
+ - In `cds.requires`: Support to replace primitive values with objects
41
+ - Support filter functions on renamed properties from external service
42
+ - Results of database queries use `big.js` for values of type `cds.Decimal` and `cds.Integer64` if enabled via `cds.env.features.bigjs`
43
+ - Support lambda in `$filter` in `$expand`
44
+ - Support for `GET` requests on service root in REST adapter (old and new)
45
+ - Support for `HEAD` requests in REST adapter (old and new)
46
+ - New hook `req.before('commit')`
47
+ - Draft (Access control for bound actions): Only the user that is the owner of the draft can execute its bound actions.
48
+ - Check that all keys are provided in REST adapter
49
+ - Restrict access to all services via `cds.env.requires.auth.restrict_all_services = true`
50
+ + That is, all unrestricted services (i.e., w/o own `@requires`) are treated as having `@requires: 'authenticated-user'`
51
+ - 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)
52
+ - Limited support for binary data in OData
53
+ + In payloads, the binary data must be a base64 encoded string
54
+ + In URLs, the binary data must have the following format: `binary'<url-safe base64 encoded>'`, e.g., `$filter=ID eq binary'Q0FQIE5vZGUuanM='`
55
+ + The use of binary data in some advanced constructs like `$apply` and `/any()` may be limited
56
+ + On SQLite, the base64 encoded string is stored to the database
57
+ + It is strongly discouraged to use binary data as keys. See "Primary Keys — Best Practices" in documentation.
58
+ - Support for OData annotation `@Core.ContentDisposition.Type` with `attachment` as the default value
59
+ - Support for returning custom stream objects in custom handlers (beta):
60
+ + Example:
61
+ ```js
62
+ return {
63
+ value: instanceof Readable || null,
64
+ $mediaContentType = 'image/jpeg',
65
+ $mediaContentDispositionFilename = 'foo.bar', // > optional
66
+ $mediaContentDispositionType = 'inline' // > optional
67
+ }
68
+ ```
69
+
70
+ ### Changed
71
+
72
+ - `cds deploy --to hana` now uses `cf curl` instead of `cf` command natively
73
+ - Event Mesh: In multitenancy mode, messaging artifacts are also deployed for provider accounts (unless the service option `deployForProvider` is set to `false`)
74
+ - Status code in case of multiple errors (rules apply in order):
75
+ + If all errors have the same status code, that status code is used
76
+ + If there is at least one 5xx status code, the resulting status code is 500
77
+ + If there is at least one 4xx status code, the resulting status code is 400
78
+ + If none of the rules apply, the resulting status code is 500
79
+ - Ignore the `If-Match` HTTP request header for `UPDATE`/`DELETE` requests whose target entities are not annotated with the `@odata.etag` annotation.
80
+ - 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).
81
+
82
+ ### Fixed
83
+
84
+ - At Node.js runtime, the `development` configuration profile is no longer active if `CDS_ENV` is set to `production` and `NODE_ENV` is undefined
85
+ - Enterprise Messaging: The user is now privileged for AMQP
86
+ - `cds.spawn` also works with synchronous functions
87
+ - Foreign keys in parent are set to `null` when deleting composition of one
88
+ - `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.
89
+ - Better error message in case destination of Remote Service is not found
90
+ - Differentiate between draft already exists and entity locked
91
+ - OData adapter: rollback transaction before rethrowing standard error in case of atomicty group
92
+ - Results of actions/functions do not ignore custom data when using `$expand` query option
93
+ - `req.data` is available in custom error handler in case of deserialization error thrown by legacy odata server
94
+ - Joining entities with renamed foreign keys (limited to single-level projections)
95
+ - Requests with draft and `$expand=*` caused problems in some cases
96
+ - `cds serve` during development longer redirects URLs with similar path segments like `/browse/123/browse/` to e.g. `/browse/`
97
+ - Post processing for renamed column in expand
98
+ - Deploy to HANA: passing of options to `hdi-deploy` via `HDI_DEPLOY_OPTIONS` now possible
99
+ - Keys as path segments in beta OData to CQN parser
100
+ - OData V2 Remote Service (`"kind": "odata-v2"`):
101
+ + Request data properties of types `cds.Date`, `cds.DateTime` and `cds.Timestamp` are converted accordingly to OData V2 specification
102
+ + 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
103
+
7
104
  ## Version 5.7.5 - 2022-01-14
8
105
 
9
106
  ### 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();