@sap/cds 6.3.0 → 6.3.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,26 @@
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.2 - 2022-11-21
8
+
9
+ ### Fixed
10
+
11
+ - `cds deploy` reports errors correctly
12
+ - Reference resolution in QL API
13
+ - `cds.parse.path` to correctly handle special characters
14
+ - `cds build` issues on Windows: build with large number of files and build on Git Bash.
15
+ - `odata` as default protocol for enabled middlewares feature
16
+
17
+ ## Version 6.3.1 - 2022-11-04
18
+
19
+ ### Fixed
20
+
21
+ - `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`
22
+ - `cds deploy` handles empty result from `cf` call correctly
23
+ - `$search` fails on columns composed by a CQL expression that uses the SAP HANA `coalesce` predicate
24
+ - Draft ownership was erroneously checked for bound actions on active instances
25
+ - `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.
26
+
7
27
  ## Version 6.3.0 - 2022-10-28
8
28
 
9
29
  ### 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,20 @@ 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) {
108
- const errorMessage = result.errors.map((entry) => `${entry.title}: ${entry.detail} (${entry.code})`).join('\n');
90
+ if (response.errors) {
91
+ const errorMessage = response.errors.map((entry) => `${entry.title || ''}: ${entry.detail || ''} (${entry.code || ''})`).join('\n');
109
92
  throw new Error(errorMessage);
110
93
  }
111
- return result;
94
+
95
+ return response;
112
96
  }
113
97
 
114
98
  _extract(string, pattern, errorMsg) {
@@ -138,23 +122,24 @@ class CfUtil {
138
122
 
139
123
  async getCfTargetFromCli() {
140
124
  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
- };
125
+ if (result?.stdout) {
126
+ return {
127
+ apiEndpoint: this._extract(result.stdout, /api endpoint\s*:\s*([^\s]+)/i, `CF API endpoint is missing. Use 'cf login' to login.`),
128
+ user: this._extract(result.stdout, /user\s*:\s*(.+)/i, `CF user is missing. Use 'cf login' to login.`),
129
+ org: this._extract(result.stdout, /org\s*:\s*(.+)/i, `CF org is missing. Use 'cf target -o <ORG> to specify.`),
130
+ space: this._extract(result.stdout, /space\s*:\s*(.+)/i, `CF space is missing. Use 'cf target -s <SPACE>' to specify.`),
131
+ };
132
+ }
147
133
  }
148
134
 
149
135
  async getCfTarget() {
150
- // check if token is valid or expired / missing
151
- await this._cfRun('oauth-token');
136
+ await this._cfRun('oauth-token'); // check if token is valid or expired / missing
152
137
  return await this.getCfTargetFromConfigFile() || await this.getCfTargetFromCli();
153
138
  }
154
139
 
155
140
  async getCfSpaceInfo() {
156
141
  if (!this.spaceInfo) {
157
- DEBUG && LOG.debug('getting space info');
142
+ LOG.debug('getting space info');
158
143
 
159
144
  const target = await this.getCfTarget();
160
145
 
@@ -179,7 +164,7 @@ class CfUtil {
179
164
  }
180
165
 
181
166
  async getService(serviceName, showMessage = true) {
182
- showMessage && LOG.log(`Getting service ${bold(serviceName)}`);
167
+ showMessage && console.log(`Getting service ${bold(serviceName)}`);
183
168
  const spaceInfo = await this.getCfSpaceInfo();
184
169
 
185
170
  let counter = POLL_COUNTER;
@@ -208,7 +193,7 @@ class CfUtil {
208
193
  throw new Error(`The returned service reported state '${OPERATION_STATE_FAILED}'.\n${JSON.stringify(serviceInstance, null, 4)}`);
209
194
 
210
195
  default:
211
- LOG.log(`Unsupported server response state '${serviceInstance?.last_operation?.state}'. Waiting for next response.`);
196
+ console.error(`Unsupported server response state '${serviceInstance?.last_operation?.state}'. Waiting for next response.`);
212
197
  break;
213
198
  }
214
199
  }
@@ -221,11 +206,11 @@ class CfUtil {
221
206
 
222
207
  const probeService = await this.getService(serviceName, false);
223
208
  if (probeService) {
224
- LOG.log(`Getting service ${bold(serviceName)}`);
209
+ console.log(`Getting service ${bold(serviceName)}`);
225
210
  return probeService;
226
211
  }
227
212
 
228
- LOG.log(`Creating service ${bold(serviceName)} - please be patient...`);
213
+ console.log(`Creating service ${bold(serviceName)} - please be patient...`);
229
214
 
230
215
  const spaceInfo = await this.getCfSpaceInfo();
231
216
 
@@ -263,7 +248,7 @@ class CfUtil {
263
248
  }
264
249
 
265
250
  const postResult = await this._cfRequest('/v3/service_instances', undefined, body);
266
- if (postResult.errors) {
251
+ if (postResult?.errors) {
267
252
  throw new Error(postResult.errors[0].detail);
268
253
  }
269
254
 
@@ -277,7 +262,7 @@ class CfUtil {
277
262
 
278
263
 
279
264
  async getServiceKey(serviceInstance, serviceKeyName, showMessage = true) {
280
- showMessage && LOG.log(`Getting service key ${bold(serviceKeyName)}`);
265
+ showMessage && console.log(`Getting service key ${bold(serviceKeyName)}`);
281
266
 
282
267
  let counter = POLL_COUNTER;
283
268
  while (counter > 0) {
@@ -303,7 +288,7 @@ class CfUtil {
303
288
  throw new Error(`The returned binding reported state '${OPERATION_STATE_FAILED}'.\n${JSON.stringify(binding, null, 4)}`);
304
289
 
305
290
  default:
306
- LOG.log(`Unsupported server response state '${binding?.last_operation?.state}'. Waiting for next response.`);
291
+ console.error(`Unsupported server response state '${binding?.last_operation?.state}'. Waiting for next response.`);
307
292
  break;
308
293
  }
309
294
  }
@@ -316,11 +301,11 @@ class CfUtil {
316
301
 
317
302
  const serviceKey = await this.getServiceKey(serviceInstance, serviceKeyName, false);
318
303
  if (serviceKey) {
319
- LOG.log(`Getting service key ${bold(serviceKeyName)}`);
304
+ console.log(`Getting service key ${bold(serviceKeyName)}`);
320
305
  return serviceKey;
321
306
  }
322
307
 
323
- LOG.log(`Creating service key ${bold(serviceKeyName)} - please be patient...`);
308
+ console.log(`Creating service key ${bold(serviceKeyName)} - please be patient...`);
324
309
 
325
310
  const body = {
326
311
  type: 'key',
@@ -338,7 +323,7 @@ class CfUtil {
338
323
  }
339
324
 
340
325
  const postResult = await this._cfRequest('/v3/service_credential_bindings', undefined, body);
341
- if (postResult.errors) {
326
+ if (postResult?.errors) {
342
327
  throw new Error(postResult.errors[0].detail);
343
328
  }
344
329
 
@@ -16,7 +16,8 @@ const parse = module.exports = Object.assign (cds_parse, {
16
16
  }},
17
17
  path: (x,...values) => {
18
18
  if (x && x.raw) return tagged (parse.path,x,...values)
19
- if (/^[A-Za-z_$][A-Za-z_0-9.$]*$/.test(x)) return {ref:[x]}
19
+ if (/^[A-Za-z_0-9.$]*$/.test(x)) return {ref:[x]}
20
+ if ((cds.context?.model || cds.model)?.definitions[x]) return {ref:[x]}
20
21
  let {SELECT} = parse.cql('SELECT from '+x)
21
22
  return SELECT.from
22
23
  },
@@ -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
 
@@ -10,7 +10,9 @@ class ProtocolAdapter {
10
10
  for (let [k,o] of Object.entries(protocols)) if (typeof o === 'string') protocols[k] = {path:o}
11
11
  if (!protocols.odata) protocols.odata = { impl: join(__dirname,'odata-v4') }
12
12
  if (!protocols.rest) protocols.rest = { impl: join(__dirname,'rest') }
13
- return this.protocols = protocols
13
+
14
+ // odata must always be first for fallback
15
+ return this.protocols = { odata: protocols.odata, ...protocols }
14
16
  }
15
17
 
16
18
  /**
package/lib/utils/tar.js CHANGED
@@ -5,7 +5,7 @@ const spawn = /\btar\b/.test(process.env.DEBUG) ? (cmd, args, options) => {
5
5
  return child_process.spawn(cmd, args, options)
6
6
  } : child_process.spawn
7
7
 
8
- const cds = require('../index'), { fs, path, mkdirp } = cds.utils
8
+ const cds = require('../index'), { fs, path, mkdirp, exists, rimraf } = cds.utils
9
9
  const _resolve = (...x) => path.resolve (cds.root,...x)
10
10
 
11
11
  // tar does not work properly on Windows (by npm/jest tests) w/o this change
@@ -15,6 +15,21 @@ const win = path => {
15
15
  if (Array.isArray(path)) return path.map(el => win(el))
16
16
  }
17
17
 
18
+ // Copy files to temp dir on Windows and pack temp dir.
19
+ // cli tar has a size limit on Windows.
20
+ const createTemp = async (root, files) => {
21
+ const temp = await fs.promises.mkdtemp(`${fs.realpathSync(require('os').tmpdir())}${path.sep}tar-`)
22
+ for (const file of files) {
23
+ const fname = path.relative(root, file)
24
+ const destination = path.join(temp, fname)
25
+ const dirname = path.dirname(destination)
26
+ if (!await exists(dirname)) await fs.promises.mkdir(dirname, { recursive: true })
27
+ await fs.promises.copyFile(file, destination)
28
+ }
29
+
30
+ return temp
31
+ }
32
+
18
33
  /**
19
34
  * Creates a tar archive, to an in-memory Buffer, or piped to write stream or file.
20
35
  * @example ```js
@@ -39,15 +54,31 @@ const win = path => {
39
54
  * - `.then()` collects the tar output into an in-memory `Buffer`
40
55
  * - `.to()` is a convenient shortcut to pipe the output into a write stream
41
56
  */
42
- exports.create = (dir='.', ...args) => {
57
+ exports.create = async (dir='.', ...args) => {
43
58
 
44
59
  if (typeof dir === 'string') dir = _resolve(dir)
45
60
  if (Array.isArray(dir)) [ dir, ...args ] = [ cds.root, dir, ...args ]
46
- if (Array.isArray(args[0])) args.push (...args.shift().map (f => path.isAbsolute(f) ? path.relative(dir,f) : f))
47
- else args.push('.')
48
- const c = process.platform === 'linux'
49
- ? spawn (`tar c -C ${dir}`, args, { shell:true })
50
- : spawn (`tar cf - -C ${win(dir)}`, args, { shell:true })
61
+
62
+ let c, temp
63
+ if (process.platform === 'win32') {
64
+ const spawnDir = (dir, args) => {
65
+ if (args.some(arg => arg === '-f')) return spawn ('tar', ['c', '-C', win(dir), ...win(args)])
66
+ else return spawn ('tar', ['cf', '-', '-C', win(dir), ...win(args)])
67
+ }
68
+ args.push('.')
69
+ if (Array.isArray(args[0])) {
70
+ temp = await createTemp(dir, args[0])
71
+ args.shift()
72
+ c = spawnDir(temp, args)
73
+ } else {
74
+ c = spawnDir(dir, args)
75
+ }
76
+ } else {
77
+ if (Array.isArray(args[0])) args.push (...args.shift().map (f => path.isAbsolute(f) ? path.relative(dir,f) : f))
78
+ else args.push('.')
79
+
80
+ c = spawn ('tar', ['c', '-C', dir, ...args])
81
+ }
51
82
 
52
83
  return {__proto__:c, // returning a thenable + fluent ChildProcess...
53
84
 
@@ -60,6 +91,10 @@ exports.create = (dir='.', ...args) => {
60
91
  const bb=[]; c.stdout.on('data', b => bb.push(b))
61
92
  c.on('close', ()=>r(Buffer.concat(bb)))
62
93
  c.on('error', e)
94
+ if (process.platform === 'win32') {
95
+ c.on('close', async () => temp && exists(temp) && await rimraf(temp))
96
+ c.on('error', async () => temp && exists(temp) && await rimraf(temp))
97
+ }
63
98
  },
64
99
 
65
100
  /**
@@ -107,7 +142,7 @@ exports.extract = (archive, ...args) => ({
107
142
  to (...dest) {
108
143
  if (typeof dest === 'string') dest = _resolve(...dest)
109
144
  const input = typeof archive !== 'string' || archive == '-' ? '-' : _resolve(archive)
110
- const x = spawn(`tar xf ${win(input)} -C ${win(dest)}`, args, { shell:true })
145
+ const x = spawn('tar', ['xf', win(input), '-C', win(dest), ...args])
111
146
  if (archive === '-') return x.stdin
112
147
  if (Buffer.isBuffer(archive)) archive = require('stream').Readable.from (archive)
113
148
  if (typeof archive !== 'string') archive.pipe (x.stdin)
@@ -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.2",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [