@sap/cds 6.3.0 → 6.3.1

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,15 @@
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.3.1 - 2022-11-04
8
+
9
+ ### Fixed
10
+ - `cds build` no longer reports false positive validation errors for built-in MTX models like `@sap/cds/srv/mtx` or `@sap/cds-mtxs/srv/bootstrap`
11
+ - `cds deploy` handles empty result from `cf` call correctly
12
+ - `$search` fails on columns composed by a CQL expression that uses the SAP HANA `coalesce` predicate
13
+ - Draft ownership was erroneously checked for bound actions on active instances
14
+ - `cds watch/run/serve --with-mocks` no longer start randomly with missing mocked services. This could happen if previous runs crashed with errors and left bad state in the local service registry.
15
+
7
16
  ## Version 6.3.0 - 2022-10-28
8
17
 
9
18
  ### Added
@@ -2,8 +2,8 @@ const fs = require('fs')
2
2
  const path = require('path')
3
3
  const _cds = require('./cds'), { log } = _cds.exec
4
4
  const { sortMessagesSeverityAware, deduplicateMessages, CompilationError } = require('@sap/cds-compiler')
5
- const { relativePaths, BuildError, BuildMessage, resolveRequiredSapModels } = require('./util')
6
- const { OUTPUT_MODE_DEFAULT, SEVERITIES, LOG_LEVELS, LOG_MODULE_NAMES } = require('./constants')
5
+ const { relativePaths, BuildError, BuildMessage, resolveRequiredSapModels, hasJavaNature } = require('./util')
6
+ const { OUTPUT_MODE_DEFAULT, SEVERITIES, LOG_LEVELS, LOG_MODULE_NAMES, CDS_MODEL_EXCLUDE_LIST } = require('./constants')
7
7
  const BuildTaskProviderFactory = require('./buildTaskProviderFactory')
8
8
  const BuildTaskHandlerInternal = require('./provider/buildTaskHandlerInternal')
9
9
 
@@ -28,6 +28,7 @@ class BuildTaskEngine {
28
28
 
29
29
  async processTasks(tasks, buildOptions, clean = true) {
30
30
  const startTime = Date.now()
31
+ const messages = []
31
32
 
32
33
  if (buildOptions) {
33
34
  // clone as data may be stored as part of the buildOptions object
@@ -48,14 +49,16 @@ class BuildTaskEngine {
48
49
  buildOptions.target = path.resolve(buildOptions.root, this.env.build.target)
49
50
  }
50
51
 
51
- // validate required @sap namespace models - log only
52
- const { unresolved, missing } = this._resolveRequiredSapServices(tasks)
53
- const messages = []
54
- if (unresolved.length > 0) {
55
- messages.push(new BuildMessage(`Required CDS service models [${unresolved.join(', ')}] cannot be resolved. Make sure to install the missing npm modules.`))
56
- }
57
- if (missing.length > 0) {
58
- messages.push(new BuildMessage(`Required CDS service models [${missing.join(', ')}] are missing in custom build tasks. Make sure to add the missing models.`))
52
+ // Java projects don't have node modules installed on project root level
53
+ if (!hasJavaNature([buildOptions.root])) {
54
+ // validate required @sap namespace models - log only
55
+ const { unresolved, missing } = this._resolveRequiredSapServices(tasks)
56
+ if (unresolved.length > 0) {
57
+ messages.push(new BuildMessage(`Required CDS service models [${unresolved.join(', ')}] cannot be resolved. Make sure to install the missing npm modules.`))
58
+ }
59
+ if (missing.length > 0) {
60
+ messages.push(new BuildMessage(`Required CDS service models [${missing.join(', ')}] are missing in custom build tasks. Make sure to add the missing models.`))
61
+ }
59
62
  }
60
63
 
61
64
  // create build task handlers
@@ -85,7 +88,7 @@ class BuildTaskEngine {
85
88
  return buildResult
86
89
  } catch (error) {
87
90
  this._logBuildOutput(handlers, buildOptions)
88
-
91
+
89
92
  // cds CLI layer logs in case of an exception if invoked from CLI
90
93
  if (!buildOptions.cli) {
91
94
  this._logMessages(buildOptions, [...BuildTaskEngine._getErrorMessages([error]), ...messages])
@@ -336,7 +339,7 @@ class BuildTaskEngine {
336
339
 
337
340
  const unresolved = resolveRequiredSapModels(this.cds, [...taskModelPaths])
338
341
  // are the required service models contained in the task's options.model
339
- const missing = srvModelPaths.filter(m => m.startsWith('@sap/') && !taskModelPaths.has(m) && !unresolved.find(u => u === m))
342
+ const missing = srvModelPaths.filter(m => m.startsWith('@sap/') && !CDS_MODEL_EXCLUDE_LIST.includes(m) && !taskModelPaths.has(m) && !unresolved.find(u => u === m))
340
343
 
341
344
  return { unresolved, missing }
342
345
  }
@@ -45,6 +45,8 @@ exports.FOLDER_GEN = "gen"
45
45
  exports.FILE_EXT_CDS = ".cds"
46
46
  exports.MTX_SIDECAR_FOLDER = "mtx/sidecar" // default name of the mtx sidecar folder
47
47
  exports.DEFAULT_CSN_FILE_NAME = "csn.json"
48
+ // REVISIT: the models are not required if a custom server.js file is used for MTX bootstrap
49
+ exports.CDS_MODEL_EXCLUDE_LIST = ['@sap/cds/srv/mtx', '@sap/cds-mtxs/srv/bootstrap']
48
50
 
49
51
  exports.CDS_CONFIG_PATH_SEP = "/"
50
52
  exports.SKIP_ASSERT_COMPILER_V2 = "skip-assert-compiler-v2"
package/bin/build/util.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
- const { SEVERITY_ERROR } = require('./constants')
3
+ const { SEVERITY_ERROR, CDS_MODEL_EXCLUDE_LIST } = require('./constants')
4
4
 
5
5
  function getProperty(src, segments) {
6
6
  segments = Array.isArray(segments) ? segments : segments.split('.')
@@ -142,7 +142,7 @@ function isStreamlinedMtx(cds) {
142
142
  */
143
143
  function resolveRequiredSapModels(cds, modelPaths) {
144
144
  return modelPaths.filter(p => {
145
- if (p.startsWith('@sap/')) {
145
+ if (p.startsWith('@sap/') && !CDS_MODEL_EXCLUDE_LIST.includes(p)) {
146
146
  const files = cds.resolve(p)
147
147
  return !files || files.length === 0
148
148
  }
@@ -2,6 +2,10 @@ const cp = require('child_process');
2
2
  const fsp = require('fs').promises;
3
3
  const os = require('os');
4
4
  const path = require('path');
5
+ const util = require('util');
6
+
7
+ const IS_WIN = os.platform() === 'win32';
8
+ const execAsync = util.promisify(cp.exec);
5
9
 
6
10
  const cds = require('../../../lib');
7
11
  const LOG = cds.log ? cds.log('deploy') : console;
@@ -33,46 +37,23 @@ class CfUtil {
33
37
  }
34
38
 
35
39
  async _cfRun(...args) {
36
- const cmdLine = `${CF_COMMAND} ${args.join(' ')}`;
37
- if (DEBUG) {
38
- // eslint-disable-next-line no-console
39
- console.time(cmdLine);
40
- LOG.debug('>>> ' + cmdLine);
41
- }
40
+ args = args.map(arg => arg.replace(/"/g, '\\"'));
41
+ const cmdLine = `${CF_COMMAND} "${args.join('" "')}"`;
42
+ DEBUG && console.time(cmdLine);
43
+ LOG.debug('>>>', cmdLine);
42
44
 
43
45
  try {
44
- return await new Promise((resolve, reject) => {
45
- const child = cp.spawn(CF_COMMAND, args);
46
-
47
- let stdout = '';
48
- child.stdout.on('data', (data) => {
49
- stdout += data;
50
- });
51
-
52
- let stderr = '';
53
- child.stderr.on('data', (data) => {
54
- stderr += data;
55
- });
56
-
57
- child.on('error', (err) => {
58
- DEBUG && LOG.debug(`${stdout}\n${stderr}`);
59
- if (err.code === 'ENOENT') {
60
- reject(new Error(`Command ${bold(CF_COMMAND)} not found. Make sure to install the Cloud Foundry Command Line Interface.`));
61
- } else {
62
- reject(err);
63
- }
64
- });
65
-
66
- child.on('close', (code) => {
67
- DEBUG && LOG.debug(`${stdout}\n${stderr}`);
68
- if (!code) {
69
- resolve(stdout.trim());
70
- } else {
71
- const errorMessage = `${stdout}\n${stderr}`;
72
- reject(new Error(errorMessage.trim()));
73
- }
74
- });
46
+ const result = await execAsync(cmdLine, {
47
+ shell: IS_WIN,
48
+ stdio: ['inherit', 'pipe', 'inherit']
75
49
  });
50
+ result.stdout = result.stdout?.trim();
51
+ result.stderr = result.stderr?.trim();
52
+ return result;
53
+ } catch (err) {
54
+ err.stdout = err.stdout?.trim();
55
+ err.stderr = err.stderr?.trim();
56
+ throw err;
76
57
  } finally {
77
58
  // eslint-disable-next-line no-console
78
59
  DEBUG && console.timeEnd(cmdLine);
@@ -98,17 +79,19 @@ class CfUtil {
98
79
  args.push(JSON.stringify(bodyObj)); // cfRun uses spawn so no special handling for quotes on cli required
99
80
  }
100
81
 
101
- const response = await this._cfRun(...args);
102
- if (!response) {
103
- throw new Error(`The response from the server was empty. Maybe your token is expired. Run the command again and re-log on in case the problem persists.`);
82
+ const result = await this._cfRun(...args);
83
+ let response = {};
84
+ if (result.stdout) {
85
+ response = JSON.parse(result.stdout);
86
+ } else if (result.stderr) {
87
+ response = { errors: [{ title: result.stderr }] };
104
88
  }
105
89
 
106
- const result = JSON.parse(response);
107
- if (result.errors) {
90
+ if (response.errors) {
108
91
  const errorMessage = result.errors.map((entry) => `${entry.title}: ${entry.detail} (${entry.code})`).join('\n');
109
92
  throw new Error(errorMessage);
110
93
  }
111
- return result;
94
+ return response;
112
95
  }
113
96
 
114
97
  _extract(string, pattern, errorMsg) {
@@ -138,23 +121,24 @@ class CfUtil {
138
121
 
139
122
  async getCfTargetFromCli() {
140
123
  const result = await this._cfRun('target');
141
- return {
142
- apiEndpoint: this._extract(result, /api endpoint\s*:\s*([^\s]+)/i, `CF API endpoint is missing. Use 'cf login' to login.`),
143
- user: this._extract(result, /user\s*:\s*(.+)/i, `CF user is missing. Use 'cf login' to login.`),
144
- org: this._extract(result, /org\s*:\s*(.+)/i, `CF org is missing. Use 'cf target -o <ORG> to specify.`),
145
- space: this._extract(result, /space\s*:\s*(.+)/i, `CF space is missing. Use 'cf target -s <SPACE>' to specify.`),
146
- };
124
+ if (result?.stdout) {
125
+ return {
126
+ apiEndpoint: this._extract(result.stdout, /api endpoint\s*:\s*([^\s]+)/i, `CF API endpoint is missing. Use 'cf login' to login.`),
127
+ user: this._extract(result.stdout, /user\s*:\s*(.+)/i, `CF user is missing. Use 'cf login' to login.`),
128
+ org: this._extract(result.stdout, /org\s*:\s*(.+)/i, `CF org is missing. Use 'cf target -o <ORG> to specify.`),
129
+ space: this._extract(result.stdout, /space\s*:\s*(.+)/i, `CF space is missing. Use 'cf target -s <SPACE>' to specify.`),
130
+ };
131
+ }
147
132
  }
148
133
 
149
134
  async getCfTarget() {
150
- // check if token is valid or expired / missing
151
- await this._cfRun('oauth-token');
135
+ await this._cfRun('oauth-token'); // check if token is valid or expired / missing
152
136
  return await this.getCfTargetFromConfigFile() || await this.getCfTargetFromCli();
153
137
  }
154
138
 
155
139
  async getCfSpaceInfo() {
156
140
  if (!this.spaceInfo) {
157
- DEBUG && LOG.debug('getting space info');
141
+ LOG.debug('getting space info');
158
142
 
159
143
  const target = await this.getCfTarget();
160
144
 
@@ -179,7 +163,7 @@ class CfUtil {
179
163
  }
180
164
 
181
165
  async getService(serviceName, showMessage = true) {
182
- showMessage && LOG.log(`Getting service ${bold(serviceName)}`);
166
+ showMessage && console.log(`Getting service ${bold(serviceName)}`);
183
167
  const spaceInfo = await this.getCfSpaceInfo();
184
168
 
185
169
  let counter = POLL_COUNTER;
@@ -208,7 +192,7 @@ class CfUtil {
208
192
  throw new Error(`The returned service reported state '${OPERATION_STATE_FAILED}'.\n${JSON.stringify(serviceInstance, null, 4)}`);
209
193
 
210
194
  default:
211
- LOG.log(`Unsupported server response state '${serviceInstance?.last_operation?.state}'. Waiting for next response.`);
195
+ console.error(`Unsupported server response state '${serviceInstance?.last_operation?.state}'. Waiting for next response.`);
212
196
  break;
213
197
  }
214
198
  }
@@ -221,11 +205,11 @@ class CfUtil {
221
205
 
222
206
  const probeService = await this.getService(serviceName, false);
223
207
  if (probeService) {
224
- LOG.log(`Getting service ${bold(serviceName)}`);
208
+ console.log(`Getting service ${bold(serviceName)}`);
225
209
  return probeService;
226
210
  }
227
211
 
228
- LOG.log(`Creating service ${bold(serviceName)} - please be patient...`);
212
+ console.log(`Creating service ${bold(serviceName)} - please be patient...`);
229
213
 
230
214
  const spaceInfo = await this.getCfSpaceInfo();
231
215
 
@@ -263,7 +247,7 @@ class CfUtil {
263
247
  }
264
248
 
265
249
  const postResult = await this._cfRequest('/v3/service_instances', undefined, body);
266
- if (postResult.errors) {
250
+ if (postResult?.errors) {
267
251
  throw new Error(postResult.errors[0].detail);
268
252
  }
269
253
 
@@ -277,7 +261,7 @@ class CfUtil {
277
261
 
278
262
 
279
263
  async getServiceKey(serviceInstance, serviceKeyName, showMessage = true) {
280
- showMessage && LOG.log(`Getting service key ${bold(serviceKeyName)}`);
264
+ showMessage && console.log(`Getting service key ${bold(serviceKeyName)}`);
281
265
 
282
266
  let counter = POLL_COUNTER;
283
267
  while (counter > 0) {
@@ -303,7 +287,7 @@ class CfUtil {
303
287
  throw new Error(`The returned binding reported state '${OPERATION_STATE_FAILED}'.\n${JSON.stringify(binding, null, 4)}`);
304
288
 
305
289
  default:
306
- LOG.log(`Unsupported server response state '${binding?.last_operation?.state}'. Waiting for next response.`);
290
+ console.error(`Unsupported server response state '${binding?.last_operation?.state}'. Waiting for next response.`);
307
291
  break;
308
292
  }
309
293
  }
@@ -316,11 +300,11 @@ class CfUtil {
316
300
 
317
301
  const serviceKey = await this.getServiceKey(serviceInstance, serviceKeyName, false);
318
302
  if (serviceKey) {
319
- LOG.log(`Getting service key ${bold(serviceKeyName)}`);
303
+ console.log(`Getting service key ${bold(serviceKeyName)}`);
320
304
  return serviceKey;
321
305
  }
322
306
 
323
- LOG.log(`Creating service key ${bold(serviceKeyName)} - please be patient...`);
307
+ console.log(`Creating service key ${bold(serviceKeyName)} - please be patient...`);
324
308
 
325
309
  const body = {
326
310
  type: 'key',
@@ -338,7 +322,7 @@ class CfUtil {
338
322
  }
339
323
 
340
324
  const postResult = await this._cfRequest('/v3/service_credential_bindings', undefined, body);
341
- if (postResult.errors) {
325
+ if (postResult?.errors) {
342
326
  throw new Error(postResult.errors[0].detail);
343
327
  }
344
328
 
@@ -69,6 +69,7 @@ module.exports = class Bindings {
69
69
  }
70
70
  }
71
71
  cds.on ('shutdown', ()=>this.purge())
72
+ process.on ('exit', ()=>this.purge()) // last resort e.g. in case of errors
72
73
  return this.store()
73
74
  }
74
75
 
@@ -77,7 +77,7 @@ const _getRoot = req => {
77
77
  return root
78
78
  }
79
79
 
80
- const _getDraftDataFromExistingDraft = async (req, root) => {
80
+ const _getDraftDataFromExistingDraft = async (req, root, isBoundAction) => {
81
81
  if (!root) return []
82
82
  if (root?.IsActiveEntity === false) {
83
83
  const query = _getSelectDraftDataCqn(root.entityName, root.where)
@@ -85,6 +85,9 @@ const _getDraftDataFromExistingDraft = async (req, root) => {
85
85
  return result
86
86
  }
87
87
 
88
+ // do not expect validate draft ownership for action call on active instances
89
+ if (isBoundAction) return []
90
+
88
91
  const rootWhere = getKeysCondition(req)
89
92
  const query = _getSelectDraftDataCqn(ensureNoDraftsSuffix(req.target.name), rootWhere)
90
93
  const result = await cds.tx(req).run(query)
@@ -147,8 +150,8 @@ const _deleteCancel = async function (req) {
147
150
  }
148
151
 
149
152
  const _validateDraftBoundAction = async function (req) {
150
- const result = await _getDraftDataFromExistingDraft(req, _getRoot(req))
151
153
  const isBoundAction = true
154
+ const result = await _getDraftDataFromExistingDraft(req, _getRoot(req), isBoundAction)
152
155
  if (result && result.length > 0) _validateDraft(req, result, isBoundAction)
153
156
  }
154
157
 
@@ -95,7 +95,9 @@ const isContainsPredicateSupported = (query, entity, columns2Search) => {
95
95
  const _isColumnFunc = (columns2Search, columnsDefs) =>
96
96
  columns2Search.some(column2Search => {
97
97
  if (column2Search.func) return true
98
- return columnsDefs?.some(columnDef => columnDef.func && columnDef.as === column2Search.ref[0])
98
+ return columnsDefs?.some(
99
+ columnDef => (columnDef.func && columnDef.as === column2Search.ref[0]) || columnDef?.xpr?.some(xpr => xpr.func)
100
+ )
99
101
  })
100
102
 
101
103
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "6.3.0",
3
+ "version": "6.3.1",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [