@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 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
- console.log(`Using HDI deployer from ${libPath}`)
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: error => LOG.error(error.toString())
122
+ stderrCB: buffer => {
123
+ LOG.error(buffer.toString())
124
+ writeStream?.write(buffer)
125
+ },
117
126
  }
118
- if (LOG.level !== SILENT) {
119
- callbacks.stdoutCB = (data) => LOG.log(data.toString());
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 && response.exitCode && response.exitCode !== 0) {
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
  }
@@ -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
- next(
133
- null,
134
- request
135
- .getIncomingRequest()
136
- .on('error', next)
137
- .pipe(new stream.PassThrough())
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 getSelectCQN = (query, target, model) => {
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
- requestTarget = query.SELECT.from
19
+ return query.SELECT.from
21
20
  } else if (query.UPDATE) {
22
- requestTarget = query.UPDATE.entity
21
+ return query.UPDATE.entity
23
22
  } else {
24
- requestTarget = query.DELETE.from
23
+ return query.DELETE.from
25
24
  }
25
+ }
26
26
 
27
- const targetName = isActiveEntityRequested(requestTarget.ref[0].where) ? target.name : ensureDraftsSuffix(target.name)
28
- const cqn = cqn2cqn4sql(SELECT.from(requestTarget), model)
29
- cqn.columns([target._etag.name])
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 = getSelectCQN(req.query, req.target, this.model)
85
- if (req.query.UPDATE || req.query.DELETE) cqn = cqn.forUpdate()
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "6.6.0",
3
+ "version": "6.6.2",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [