@sap/cds 5.7.2 → 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.
- package/CHANGELOG.md +108 -0
- package/app/fiori/routes.js +1 -1
- package/bin/deploy/to-hana/cfUtil.js +251 -138
- package/bin/deploy/to-hana/gitUtil.js +55 -0
- package/bin/deploy/to-hana/hana.js +92 -93
- package/bin/deploy/to-hana/hdiDeployUtil.js +42 -27
- package/bin/deploy/to-hana/index.js +14 -13
- package/bin/mtx/in-cds.js +1 -0
- package/bin/serve.js +1 -1
- package/bin/version.js +1 -0
- package/lib/compile/cdsc.js +0 -6
- package/lib/compile/minify.js +1 -1
- package/lib/compile/resolve.js +1 -1
- package/lib/compile/to/srvinfo.js +1 -1
- package/lib/core/classes.js +21 -1
- package/lib/env/index.js +3 -2
- package/lib/env/requires.js +4 -0
- package/lib/i18n/localize.js +5 -8
- package/lib/index.js +1 -0
- package/lib/log/errors.js +1 -1
- package/lib/ql/SELECT.js +2 -2
- package/lib/req/cds-context.js +1 -1
- package/lib/req/context.js +1 -1
- package/lib/serve/Transaction.js +9 -5
- package/lib/serve/index.js +13 -21
- package/lib/utils/tests.js +90 -66
- package/libx/_runtime/audit/generic/personal/modification.js +0 -8
- package/libx/_runtime/auth/index.js +7 -6
- package/libx/_runtime/auth/strategies/dwc.js +43 -0
- package/libx/_runtime/auth/utils.js +24 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -38
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -42
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +18 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +0 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +7 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +9 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +21 -30
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +12 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +5 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +18 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +80 -21
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +7 -5
- package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
- package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
- package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
- package/libx/_runtime/cds-services/services/Service.js +1 -1
- package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
- package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
- package/libx/_runtime/common/aspects/Association.js +16 -0
- package/libx/_runtime/common/composition/data.js +28 -37
- package/libx/_runtime/common/composition/delete.js +107 -58
- package/libx/_runtime/common/composition/index.js +2 -1
- package/libx/_runtime/common/composition/insert.js +13 -13
- package/libx/_runtime/common/composition/update.js +39 -34
- package/libx/_runtime/common/error/frontend.js +17 -2
- package/libx/_runtime/common/generic/auth.js +20 -85
- package/libx/_runtime/common/generic/crud.js +22 -1
- package/libx/_runtime/common/i18n/messages.properties +3 -0
- package/libx/_runtime/common/utils/cqn.js +2 -6
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +97 -123
- package/libx/_runtime/common/utils/csn.js +14 -3
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +18 -1
- package/libx/_runtime/common/utils/keys.js +2 -1
- package/libx/_runtime/common/utils/path.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +12 -4
- package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
- package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
- package/libx/_runtime/common/utils/structured.js +1 -1
- package/libx/_runtime/common/utils/vcap.js +27 -10
- package/libx/_runtime/db/data-conversion/post-processing.js +42 -35
- package/libx/_runtime/db/expand/expand-v2.js +21 -12
- package/libx/_runtime/db/expand/expandCQNToJoin.js +27 -29
- package/libx/_runtime/db/expand/index.js +3 -0
- package/libx/_runtime/db/generic/create.js +0 -10
- package/libx/_runtime/db/generic/index.js +3 -0
- package/libx/_runtime/db/generic/read.js +2 -24
- package/libx/_runtime/db/generic/rewrite.js +1 -3
- package/libx/_runtime/db/generic/update.js +1 -1
- package/libx/_runtime/db/query/delete.js +10 -4
- package/libx/_runtime/db/query/insert.js +3 -3
- package/libx/_runtime/db/query/read.js +15 -8
- package/libx/_runtime/db/query/update.js +5 -5
- package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
- package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
- package/libx/_runtime/db/sql-builder/index.js +3 -0
- package/libx/_runtime/db/utils/columns.js +5 -2
- package/libx/_runtime/db/utils/deep.js +6 -8
- package/libx/_runtime/db/utils/generateAliases.js +56 -6
- package/libx/_runtime/fiori/generic/before.js +73 -49
- package/libx/_runtime/fiori/generic/edit.js +14 -18
- package/libx/_runtime/fiori/generic/patch.js +8 -11
- package/libx/_runtime/fiori/generic/read.js +22 -17
- package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
- package/libx/_runtime/hana/Service.js +1 -1
- package/libx/_runtime/hana/conversion.js +10 -0
- package/libx/_runtime/hana/execute.js +33 -16
- package/libx/_runtime/hana/localized.js +1 -1
- package/libx/_runtime/hana/search.js +3 -3
- package/libx/_runtime/hana/search2cqn4sql.js +22 -21
- package/libx/_runtime/hana/searchToContains.js +1 -1
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +4 -2
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
- package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
- package/libx/_runtime/messaging/file-based.js +3 -1
- package/libx/_runtime/messaging/message-queuing-utils/options-messaging.js +1 -0
- package/libx/_runtime/messaging/service.js +16 -7
- package/libx/_runtime/remote/utils/client.js +33 -20
- package/libx/_runtime/remote/utils/data.js +53 -12
- package/libx/_runtime/sqlite/Service.js +1 -1
- package/libx/_runtime/sqlite/conversion.js +10 -0
- package/libx/_runtime/sqlite/localized.js +1 -1
- package/libx/_runtime/types/api.js +2 -2
- package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
- package/libx/odata/afterburner.js +29 -6
- package/libx/odata/cqn2odata.js +9 -0
- package/libx/odata/grammar.pegjs +101 -45
- package/libx/odata/index.js +7 -1
- package/libx/odata/parser.js +1 -1
- package/libx/odata/utils.js +2 -2
- package/libx/rest/RestAdapter.js +29 -1
- package/libx/rest/middleware/auth.js +1 -3
- package/libx/rest/middleware/parse.js +1 -0
- package/package.json +1 -1
- package/server.js +1 -1
- package/bin/deploy/to-hana/logger.js +0 -27
- package/bin/deploy/to-hana/runCommand.js +0 -113
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
- package/libx/_runtime/common/utils/auth.js +0 -16
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,114 @@
|
|
|
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
|
+
|
|
79
|
+
## Version 5.7.5 - 2022-01-14
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- Instance-based restriction for activation of draft-enabled entities using `or` in restriction
|
|
84
|
+
- Messaging: Duplicate handler execution if application service registered events twice
|
|
85
|
+
- Post of a deeply nested sub-entity with structured parent keys
|
|
86
|
+
- Negating lambda expressions in OData using the `not` operator
|
|
87
|
+
- Event Mesh: Redelivery count when using AMQP
|
|
88
|
+
- OData requests using lambda expressions on localized data
|
|
89
|
+
- `cds.db.exists` wrongly generated a `SELECT * FROM ...` for odata flavor x4
|
|
90
|
+
- Return localized texts on draft activate
|
|
91
|
+
- Unicode characters in unquoted search terms in beta OData to CQN parser
|
|
92
|
+
|
|
93
|
+
## Version 5.7.4 - 2021-12-22
|
|
94
|
+
|
|
95
|
+
### Fixed
|
|
96
|
+
|
|
97
|
+
- Complex `@restrict.where: 'exists [...] or (... or ...) or ...'` in draft union scenario no longer crashes the application
|
|
98
|
+
- Sanitization of null values for `cds.RemoteService`
|
|
99
|
+
- Handling of boolean values in draft union scenario with `$expand` query option
|
|
100
|
+
- `_4odata` flag in CQN stays non-enumerable when forwarding to another application service
|
|
101
|
+
- Handling of type references on properties of associations in `cds.minify`
|
|
102
|
+
|
|
103
|
+
## Version 5.7.3 - 2021-12-16
|
|
104
|
+
|
|
105
|
+
### Fixed
|
|
106
|
+
|
|
107
|
+
- Message Queuing now accepts `amqp` options
|
|
108
|
+
- OData requests using lambda expressions with `contains` function
|
|
109
|
+
- Result of OData query option `$count=true` when using `$apply`
|
|
110
|
+
- `$filter` with navigation to-one equals value crashes
|
|
111
|
+
- `$skiptoken` query option allows to use arbitrary symbols except of `&` with beta OData URL to CQN parser (`cds.env.features.odata_new_parser`). In this non-integer value case the value will not be parsed into CQN.
|
|
112
|
+
- Function names in `$filter` can now be case insensitive (as per OData 4.01)
|
|
113
|
+
- `$count` in `$expand` caused server to crash
|
|
114
|
+
|
|
7
115
|
## Version 5.7.2 - 2021-12-09
|
|
8
116
|
|
|
9
117
|
### Fixed
|
package/app/fiori/routes.js
CHANGED
|
@@ -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
|
|
3
|
-
const
|
|
3
|
+
const cds = require('../../../lib');
|
|
4
|
+
const LOG = cds.log ? cds.log('deploy') : console;
|
|
5
|
+
const DEBUG = cds.debug('deploy');
|
|
4
6
|
|
|
5
|
-
const
|
|
6
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
throw this._getError(err);
|
|
33
|
-
}
|
|
17
|
+
_clear() {
|
|
18
|
+
this.spaceInfo = null;
|
|
34
19
|
}
|
|
35
20
|
|
|
36
|
-
|
|
37
|
-
return new Promise((resolve
|
|
38
|
-
|
|
21
|
+
async _sleep(ms) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
setTimeout(resolve, ms);
|
|
39
24
|
});
|
|
40
25
|
}
|
|
41
26
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
31
|
+
try {
|
|
32
|
+
return await new Promise((resolve, reject) => {
|
|
33
|
+
const child = cp.spawn(CF_COMMAND, args);
|
|
54
34
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
35
|
+
let stdout = '';
|
|
36
|
+
child.stdout.on('data', (data) => {
|
|
37
|
+
stdout += data;
|
|
38
|
+
});
|
|
60
39
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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();
|