@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 +9 -0
- package/bin/build/buildTaskEngine.js +15 -12
- package/bin/build/constants.js +2 -0
- package/bin/build/util.js +2 -2
- package/bin/deploy/to-hana/cfUtil.js +46 -62
- package/lib/srv/bindings.js +1 -0
- package/libx/_runtime/fiori/generic/before.js +5 -2
- package/libx/_runtime/hana/search2Contains.js +3 -1
- package/package.json +1 -1
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
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
}
|
package/bin/build/constants.js
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
208
|
+
console.log(`Getting service ${bold(serviceName)}`);
|
|
225
209
|
return probeService;
|
|
226
210
|
}
|
|
227
211
|
|
|
228
|
-
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
303
|
+
console.log(`Getting service key ${bold(serviceKeyName)}`);
|
|
320
304
|
return serviceKey;
|
|
321
305
|
}
|
|
322
306
|
|
|
323
|
-
|
|
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
|
|
325
|
+
if (postResult?.errors) {
|
|
342
326
|
throw new Error(postResult.errors[0].detail);
|
|
343
327
|
}
|
|
344
328
|
|
package/lib/srv/bindings.js
CHANGED
|
@@ -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(
|
|
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 = {
|