@sap/cds 6.6.0 → 6.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/bin/deploy/to-hana/hana.js +1 -1
- package/bin/deploy/to-hana/hdiDeployUtil.js +24 -12
- package/lib/env/schemas/cds-rc.json +4 -0
- package/lib/srv/srv-models.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +0 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +7 -9
- package/libx/_runtime/common/generic/etag.js +21 -11
- package/libx/_runtime/common/utils/structured.js +1 -0
- package/libx/_runtime/fiori/generic/edit.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@
|
|
|
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 6.6.2 - 2023-03-17
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Exception during `cds deploy` without mtx
|
|
12
|
+
- Service name specified with `cds deploy --to hana:serviceName` takes precedence over environment variables.
|
|
13
|
+
|
|
14
|
+
## Version 6.6.1 - 2023-03-09
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `cds.xt.TENANT_UPDATED` event is emitted once a tenant was extended
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `TypeError` when using the query API with an unknown target in x4 flavor
|
|
23
|
+
- The setting for `cds.requires['cds.xt.DeploymentService'].lazyT0` is now recognized in the VS Code schema validation.
|
|
24
|
+
- The HDI deployment `stdout` logs are now only visible for `DEBUG` level if triggered via `cds-mtxs`. They are also streamed to `logs/<tenant>.log` in case you need the full deployment output, even without `DEBUG` enabled.
|
|
25
|
+
- `.forUpdate` when used for etags
|
|
26
|
+
- Prevent `TypeError` if an existing draft does not have admin data
|
|
27
|
+
- Outbound-streaming error handling
|
|
28
|
+
|
|
7
29
|
## Version 6.6.0 - 2023-02-27
|
|
8
30
|
|
|
9
31
|
### Added
|
|
@@ -72,7 +72,7 @@ class HanaDeployer {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
const hasVCAPEnv = Object.keys(vcapEnv).length > 0;
|
|
75
|
-
if (hasVCAPEnv) {
|
|
75
|
+
if (!serviceName && hasVCAPEnv) {
|
|
76
76
|
await fs.mkdir(currentModelFolder, { recursive: true });
|
|
77
77
|
} else {
|
|
78
78
|
const { cfServiceInstanceName, cfServiceInstanceKeyName, serviceKey } =
|
|
@@ -5,8 +5,9 @@ const util = require('util');
|
|
|
5
5
|
const execAsync = util.promisify(cp.exec);
|
|
6
6
|
|
|
7
7
|
const cds = require('../../../lib');
|
|
8
|
-
const { SILENT } = cds.log.levels;
|
|
9
8
|
const LOG = cds.log ? cds.log('deploy') : console;
|
|
9
|
+
const DEBUG = cds.debug('deploy');
|
|
10
|
+
const { SILENT } = cds.log.levels;
|
|
10
11
|
|
|
11
12
|
class HdiDeployUtil {
|
|
12
13
|
|
|
@@ -17,7 +18,7 @@ class HdiDeployUtil {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
async deployTenant(dbDir, env) {
|
|
20
|
-
await this._executeDeploy(dbDir, env);
|
|
21
|
+
await this._executeDeploy(dbDir, env, true);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
async deploy(dbDir, vcapEnv, options) {
|
|
@@ -89,7 +90,7 @@ class HdiDeployUtil {
|
|
|
89
90
|
Add it either as a devDependency using 'npm install -D ${this.deployerName}' or install it globally using 'npm install -g ${this.deployerName}'.`);
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
LOG.info(`Using HDI deployer from ${libPath}`)
|
|
93
94
|
|
|
94
95
|
// let any error go through and abort deploy
|
|
95
96
|
return require(libPath);
|
|
@@ -109,29 +110,40 @@ Add it either as a devDependency using 'npm install -D ${this.deployerName}' or
|
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
|
|
112
|
-
async _executeDeploy(dbDir, env) {
|
|
113
|
+
async _executeDeploy(dbDir, env, fromMtx) {
|
|
113
114
|
const hdiDeployLib = await this._getHdiDeployLib(dbDir);
|
|
115
|
+
let writeStream
|
|
116
|
+
if (fromMtx) {
|
|
117
|
+
await cds.utils.mkdirp('logs')
|
|
118
|
+
writeStream = require('fs').createWriteStream(path.join(cds.root, 'logs', `${cds.context.tenant}.log`))
|
|
119
|
+
}
|
|
114
120
|
return new Promise((resolve, reject) => {
|
|
115
121
|
const callbacks = {
|
|
116
|
-
stderrCB:
|
|
122
|
+
stderrCB: buffer => {
|
|
123
|
+
LOG.error(buffer.toString())
|
|
124
|
+
writeStream?.write(buffer)
|
|
125
|
+
},
|
|
117
126
|
}
|
|
118
|
-
if (
|
|
119
|
-
callbacks.stdoutCB =
|
|
127
|
+
if (fromMtx) {
|
|
128
|
+
callbacks.stdoutCB = buffer => {
|
|
129
|
+
DEBUG?.(buffer.toString())
|
|
130
|
+
writeStream.write(buffer)
|
|
131
|
+
}
|
|
132
|
+
} else if (LOG.level !== SILENT) {
|
|
133
|
+
callbacks.stdoutCB = buffer => LOG.info(buffer.toString())
|
|
120
134
|
}
|
|
121
|
-
|
|
122
135
|
hdiDeployLib.deploy(dbDir, env, (error, response) => {
|
|
123
136
|
if (error) {
|
|
124
137
|
return reject(error);
|
|
125
138
|
}
|
|
126
|
-
if (response
|
|
139
|
+
if (response?.exitCode) {
|
|
127
140
|
let message = `HDI deployment failed with exit code ${response.exitCode}`
|
|
128
141
|
if (response.signal) message += `. ${response.signal}`
|
|
129
142
|
return reject(new Error(message));
|
|
130
143
|
}
|
|
131
144
|
return resolve();
|
|
132
|
-
}, callbacks
|
|
133
|
-
|
|
134
|
-
});
|
|
145
|
+
}, callbacks);
|
|
146
|
+
}).finally(() => writeStream?.end());
|
|
135
147
|
}
|
|
136
148
|
}
|
|
137
149
|
|
|
@@ -342,6 +342,10 @@
|
|
|
342
342
|
"description": "HDI deployment parameters as defined on https://www.npmjs.com/package/@sap/hdi-deploy#supported-features"
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
|
+
},
|
|
346
|
+
"lazyT0": {
|
|
347
|
+
"type": "boolean",
|
|
348
|
+
"description": "Onboard bookkeeping t0 container at the first subscription."
|
|
345
349
|
}
|
|
346
350
|
}
|
|
347
351
|
}
|
package/lib/srv/srv-models.js
CHANGED
|
@@ -115,6 +115,7 @@ class ExtendedModels {
|
|
|
115
115
|
})
|
|
116
116
|
if (has_new_extensions) { // new extensions arrived -> refresh model in cache
|
|
117
117
|
let [ tenant = undefined, toggles ] = key.split(':')
|
|
118
|
+
cds.emit('cds.xt.TENANT_UPDATED', { tenant })
|
|
118
119
|
return _get_model4 (tenant, toggles.split(','))
|
|
119
120
|
} else { // no new extensions...
|
|
120
121
|
_cached.touched = Date.now() // check again in 1 min or so
|
|
@@ -147,13 +147,6 @@ const update = service => {
|
|
|
147
147
|
previousResult = await readAfterWrite(req, service, { isBefore: true })
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
// in case of express errors in streaming do rollback
|
|
151
|
-
const segments = odataReq.getUriInfo().getPathSegments()
|
|
152
|
-
if (isStreaming(segments)) {
|
|
153
|
-
odataReq.getIncomingRequest().on('error', async err => {
|
|
154
|
-
await tx.rollback(err).catch(() => {})
|
|
155
|
-
})
|
|
156
|
-
}
|
|
157
150
|
// try UPDATE and, on 404 error, try CREATE
|
|
158
151
|
;[result, req] = await _updateThenCreate(req, odataReq, odataRes, tx)
|
|
159
152
|
|
|
@@ -127,16 +127,14 @@ class DeserializerFactory {
|
|
|
127
127
|
static createBinaryDeserializer () {
|
|
128
128
|
return (request, next) => {
|
|
129
129
|
let type = request.getUriInfo() && request.getUriInfo().getFinalEdmType()
|
|
130
|
-
if (type && type.getKind() === EdmTypeKind.DEFINITION) type = type.getUnderlyingType()
|
|
130
|
+
if (type && type.getKind() === EdmTypeKind.DEFINITION) type = type.getUnderlyingType()
|
|
131
131
|
if (type && type === EdmPrimitiveTypeKind.Stream) {
|
|
132
|
-
|
|
133
|
-
null,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
.on('error', next)
|
|
139
|
-
)
|
|
132
|
+
if (request.getIncomingRequest().complete) { // empty or NULL
|
|
133
|
+
next(null, request.getIncomingRequest())
|
|
134
|
+
} else {
|
|
135
|
+
const streamPipeline = stream.pipeline(request.getIncomingRequest(), new stream.PassThrough(), () => {})
|
|
136
|
+
next(null, streamPipeline)
|
|
137
|
+
}
|
|
140
138
|
} else {
|
|
141
139
|
request
|
|
142
140
|
.getIncomingRequest()
|
|
@@ -7,26 +7,27 @@ const { ensureDraftsSuffix } = require('../../fiori/utils/handler')
|
|
|
7
7
|
const { cqn2cqn4sql } = require('../../common/utils/cqn2cqn4sql')
|
|
8
8
|
const { isAsteriskColumn } = require('../../common/utils/rewriteAsterisks')
|
|
9
9
|
const ODataRequest = require('../../cds-services/adapter/odata-v4/ODataRequest')
|
|
10
|
+
const { resolveView, getTransition } = require('../utils/resolveView')
|
|
10
11
|
|
|
11
12
|
const C_U_ = {
|
|
12
13
|
CREATE: 1,
|
|
13
14
|
UPDATE: 1
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
const
|
|
17
|
-
// REVISIT DRAFT HANDLING: this function is a hack until we solve drafts properly
|
|
18
|
-
let requestTarget
|
|
17
|
+
const getRequestedTarget = query => {
|
|
19
18
|
if (query.SELECT) {
|
|
20
|
-
|
|
19
|
+
return query.SELECT.from
|
|
21
20
|
} else if (query.UPDATE) {
|
|
22
|
-
|
|
21
|
+
return query.UPDATE.entity
|
|
23
22
|
} else {
|
|
24
|
-
|
|
23
|
+
return query.DELETE.from
|
|
25
24
|
}
|
|
25
|
+
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
cqn.
|
|
27
|
+
const getSelectCQN = (query, target, model, isActive, etag) => {
|
|
28
|
+
const targetName = isActive ? target.name : ensureDraftsSuffix(target.name)
|
|
29
|
+
const cqn = cqn2cqn4sql(SELECT.from(getRequestedTarget(query)), model)
|
|
30
|
+
cqn.columns([etag || target._etag.name])
|
|
30
31
|
cqn.SELECT.from.ref[0] = targetName
|
|
31
32
|
|
|
32
33
|
return cqn
|
|
@@ -81,8 +82,17 @@ const commonGenericEtag = async function (req) {
|
|
|
81
82
|
|
|
82
83
|
// validate
|
|
83
84
|
if (req.isConditional && !req.query.INSERT) {
|
|
84
|
-
let cqn
|
|
85
|
-
|
|
85
|
+
let cqn
|
|
86
|
+
const isActive = isActiveEntityRequested(getRequestedTarget(req.query).ref[0].where)
|
|
87
|
+
if ((req.query.UPDATE || req.query.DELETE) && isActive) {
|
|
88
|
+
const query_ = resolveView(req.query, this.model, cds.db)
|
|
89
|
+
const transition = (query_.UPDATE && query_.UPDATE._transitions[0]) || query_.DELETE._transitions[0]
|
|
90
|
+
const etag = transition.mapping.get(req.target._etag.name).ref[0]
|
|
91
|
+
cqn = getSelectCQN(query_, transition.target, this.model, isActive, etag).forUpdate()
|
|
92
|
+
} else {
|
|
93
|
+
cqn = getSelectCQN(req.query, req.target, this.model, isActive)
|
|
94
|
+
}
|
|
95
|
+
|
|
86
96
|
const result = await cds.tx(req).run(cqn)
|
|
87
97
|
|
|
88
98
|
if (result.length === 1) {
|
|
@@ -276,6 +276,7 @@ const flattenStructuredSelect = ({ SELECT }, model) => {
|
|
|
276
276
|
|
|
277
277
|
for (const entityNameAndId of entityNamesAndIds) {
|
|
278
278
|
const entity = model.definitions[entityNameAndId.name]
|
|
279
|
+
if (!entity) return
|
|
279
280
|
const tableId = entityNameAndId.id
|
|
280
281
|
|
|
281
282
|
if (Array.isArray(SELECT.columns) && SELECT.columns.length > 0) {
|
|
@@ -132,6 +132,9 @@ const fioriGenericEdit = async function (req) {
|
|
|
132
132
|
SELECT.one('DRAFT.DraftAdministrativeData', ['InProcessByUser', 'LastChangeDateTime']).where(draftExists[0])
|
|
133
133
|
)
|
|
134
134
|
|
|
135
|
+
// temp check if draft admin data in not maintained - raise 500 error
|
|
136
|
+
if (!adminData) req.reject(500, 'Draft administrative data is not maintained')
|
|
137
|
+
|
|
135
138
|
// draft is locked (default cancellation timeout timer has not expired) OR
|
|
136
139
|
// draft is not locked but must be rejected for popup
|
|
137
140
|
if (draftIsLocked(adminData.LastChangeDateTime) || req.data.PreserveChanges) {
|