@sap/cds 6.2.3 → 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 +57 -0
- package/apis/connect.d.ts +1 -1
- package/apis/cqn.d.ts +1 -1
- package/apis/internal/inference.d.ts +14 -0
- package/apis/ql.d.ts +40 -36
- package/apis/services.d.ts +23 -6
- package/bin/build/buildTaskEngine.js +15 -12
- package/bin/build/buildTaskHandler.js +3 -3
- package/bin/build/constants.js +2 -0
- package/bin/build/provider/buildTaskHandlerEdmx.js +1 -1
- package/bin/build/provider/buildTaskHandlerFeatureToggles.js +4 -3
- package/bin/build/provider/buildTaskHandlerInternal.js +2 -2
- package/bin/build/provider/java/index.js +2 -1
- package/bin/build/provider/mtx/index.js +2 -1
- package/bin/build/provider/mtx/resourcesTarBuilder.js +3 -2
- package/bin/build/provider/mtx-extension/index.js +2 -1
- package/bin/build/provider/mtx-sidecar/index.js +3 -1
- package/bin/build/util.js +2 -2
- package/bin/deploy/to-hana/cfUtil.js +46 -62
- package/lib/auth/index.js +2 -1
- package/lib/auth/jwt-auth.js +64 -3
- package/lib/auth/xsuaa-auth.js +2 -3
- package/lib/compile/cdsc.js +1 -0
- package/lib/compile/etc/_localized.js +1 -0
- package/lib/dbs/cds-deploy.js +2 -1
- package/lib/env/cds-env.js +14 -49
- package/lib/env/cds-requires.js +13 -7
- package/lib/env/defaults.js +4 -0
- package/lib/i18n/localize.js +11 -8
- package/lib/index.js +1 -1
- package/lib/log/cds-log.js +2 -2
- package/lib/log/format/cf.js +16 -0
- package/lib/log/format/kibana.js +15 -2
- package/lib/ql/INSERT.js +12 -11
- package/lib/ql/Query.js +14 -7
- package/lib/ql/UPSERT.js +1 -0
- package/lib/ql/Whereable.js +6 -2
- package/lib/ql/cds-ql.js +2 -4
- package/lib/req/request.js +2 -0
- package/lib/srv/bindings.js +1 -0
- package/lib/srv/middlewares/cds-context.js +1 -1
- package/lib/srv/srv-dispatch.js +1 -0
- package/lib/srv/srv-tx.js +3 -3
- package/lib/utils/cds-utils.js +75 -30
- package/lib/utils/inflect.js +24 -0
- package/libx/_runtime/auth/strategies/ias-auth.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +9 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +23 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +27 -15
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +11 -10
- package/libx/_runtime/cds-services/services/utils/differ.js +6 -4
- package/libx/_runtime/common/composition/data.js +29 -40
- package/libx/_runtime/common/composition/update.js +6 -19
- package/libx/_runtime/common/generic/paging.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +7 -13
- package/libx/_runtime/db/utils/generateAliases.js +1 -0
- package/libx/_runtime/fiori/generic/before.js +5 -2
- package/libx/_runtime/fiori/generic/read.js +11 -4
- package/libx/_runtime/hana/execute.js +2 -2
- package/libx/_runtime/hana/search2Contains.js +3 -1
- package/libx/_runtime/hana/search2cqn4sql.js +1 -0
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +5 -2
- package/libx/_runtime/messaging/enterprise-messaging.js +7 -1
- package/libx/_runtime/messaging/file-based.js +1 -1
- package/libx/_runtime/messaging/message-queuing.js +5 -2
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/messaging/service.js +5 -3
- package/libx/odata/cqn2odata.js +4 -1
- package/libx/odata/utils.js +8 -7
- package/libx/rest/RestAdapter.js +1 -4
- package/package.json +1 -1
|
@@ -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/auth/index.js
CHANGED
|
@@ -25,7 +25,8 @@ module.exports = lazified (Object.assign (auth_factory, {
|
|
|
25
25
|
basic: require('./basic-auth'),
|
|
26
26
|
dummy: require('./dummy-auth'),
|
|
27
27
|
ias: require('./ias-auth'),
|
|
28
|
-
|
|
28
|
+
jwt: require('./jwt-auth'),
|
|
29
|
+
xsuaa: require('./jwt-auth'),
|
|
29
30
|
}))
|
|
30
31
|
|
|
31
32
|
require = _require // eslint-disable-line no-global-assign
|
package/lib/auth/jwt-auth.js
CHANGED
|
@@ -1,3 +1,64 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const cds = require('../')
|
|
2
|
+
const _require = require('../../libx/_runtime/common/utils/require')
|
|
3
|
+
// _require for better error message
|
|
4
|
+
const express = _require('express')
|
|
5
|
+
const passport = _require('passport')
|
|
6
|
+
const { JWTStrategy } = _require('@sap/xssec')
|
|
7
|
+
const LOG = cds.log('auth')
|
|
8
|
+
|
|
9
|
+
module.exports = function jwt_auth(config) {
|
|
10
|
+
// warn if no credentials
|
|
11
|
+
if (!config.credentials) {
|
|
12
|
+
LOG._warn &&
|
|
13
|
+
LOG.warn(`
|
|
14
|
+
No XSUAA instance bound to application, but "${config.kind}" configured.
|
|
15
|
+
This is NOT recommended in production!
|
|
16
|
+
`)
|
|
17
|
+
|
|
18
|
+
return (req,res,next) => next()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
passport.use(config.kind, new JWTStrategy(config.credentials))
|
|
22
|
+
return express
|
|
23
|
+
.Router()
|
|
24
|
+
.use(passport.authenticate(config.kind, { session: false }))
|
|
25
|
+
.use((req, res, next) => {
|
|
26
|
+
const payload = req.tokenInfo.getPayload()
|
|
27
|
+
|
|
28
|
+
let id = req.user.id
|
|
29
|
+
|
|
30
|
+
let roles = payload.scope.map(s => s.replace(new RegExp(`^(${config.credentials.xsappname + '.'})`), ''))
|
|
31
|
+
roles.push('identified-user')
|
|
32
|
+
if (payload.grant_type) {
|
|
33
|
+
// > not "weak"
|
|
34
|
+
roles.push('authenticated-user')
|
|
35
|
+
|
|
36
|
+
const CLIENT = { client_credentials: 1, client_x509: 1 }
|
|
37
|
+
if (payload.grant_type in CLIENT) {
|
|
38
|
+
id = 'system'
|
|
39
|
+
roles.push('system-user')
|
|
40
|
+
if (req.tokenInfo.getClientId() === config.credentials.clientid) roles.push('internal-user')
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let attr = req.authInfo.getAttributes() || {}
|
|
45
|
+
if (config.kind === 'xsuaa') {
|
|
46
|
+
attr.logonName = req.authInfo.getLogonName()
|
|
47
|
+
attr.givenName = req.authInfo.getGivenName()
|
|
48
|
+
attr.familyName = req.authInfo.getFamilyName()
|
|
49
|
+
attr.email = req.authInfo.getEmail()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
req.user = new cds.User({ id, roles, attr })
|
|
53
|
+
req.tenant = req.tokenInfo.getZoneId?.()
|
|
54
|
+
next()
|
|
55
|
+
})
|
|
56
|
+
.use((err, req, res, next) => {
|
|
57
|
+
if (req.tokenInfo) {
|
|
58
|
+
LOG?.debug('error during token validation', req.tokenInfo.getErrorObject())
|
|
59
|
+
}
|
|
60
|
+
// REVISIT: reject request immediately as our other auth strategies do
|
|
61
|
+
// should we call next(err)? -> I don't think so; it's not an error, is it?
|
|
62
|
+
res.status(401).json({ code: '401', message: 'Unauthorized' }) // REVISIT: this is OData style?
|
|
63
|
+
})
|
|
64
|
+
}
|
package/lib/auth/xsuaa-auth.js
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
module.exports = ()=> (req,res,next)=> next()
|
|
1
|
+
// REVISIT: "kind": "xsuaa-auth" does not work because cds.requires logic does not resolve it. Intentional?
|
|
2
|
+
module.exports = require('./jwt-auth')
|
package/lib/compile/cdsc.js
CHANGED
|
@@ -103,6 +103,7 @@ const _options = {for: Object.assign (_options4, {
|
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* Return a derivate of cdsc, with the most prominent
|
|
106
|
+
* @type { import('@sap/cds-compiler') }
|
|
106
107
|
*/
|
|
107
108
|
module.exports = exports = {__proto__:compile, _options,
|
|
108
109
|
for: {__proto__: compile.for,
|
package/lib/dbs/cds-deploy.js
CHANGED
|
@@ -143,7 +143,6 @@ exports.init = (db, csn=db.model, log=()=>{}) => db.run (async tx => {
|
|
|
143
143
|
if (db.kind === 'better-sqlite') _add_missing_pks2(q)
|
|
144
144
|
log (file,e)
|
|
145
145
|
inits.push (tx.run(q) .catch (e => {
|
|
146
|
-
Error.captureStackTrace(e)
|
|
147
146
|
throw Object.assign (e, { message: 'in cds.deploy(): ' + e.message +'\n'+ inspect(q) })
|
|
148
147
|
}))
|
|
149
148
|
}
|
|
@@ -232,11 +231,13 @@ const _entity4 = (file,csn) => {
|
|
|
232
231
|
const INSERT_from_csv = (entity, csv) => {
|
|
233
232
|
let [ cols, ...rows ] = cds.parse.csv (csv)
|
|
234
233
|
if (rows.length > 0) return INSERT.into (entity) .columns (cols) .rows (rows)
|
|
234
|
+
// if (rows.length > 0) return UPSERT.into (entity) .columns (cols) .rows (rows)
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
const INSERT_from_json = (entity, json) => {
|
|
238
238
|
let records = JSON.parse (json)
|
|
239
239
|
if (records.length > 0) return INSERT.into (entity) .entries (records)
|
|
240
|
+
// if (records.length > 0) return UPSERT.into (entity) .entries (records)
|
|
240
241
|
}
|
|
241
242
|
|
|
242
243
|
const _from_csv_or_json = { '.json': INSERT_from_json, '.csv': INSERT_from_csv, }
|
package/lib/env/cds-env.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { isfile, fs, path } = require('../utils/cds-utils')
|
|
1
|
+
const { isdir, isfile, fs, path } = require('../utils/cds-utils')
|
|
2
2
|
const DEFAULTS = require('./defaults'), defaults = require.resolve ('./defaults')
|
|
3
3
|
const compat = require('./compat')
|
|
4
4
|
const presets = require('./presets')
|
|
@@ -6,7 +6,7 @@ const serviceBindings = require('./serviceBindings');
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Both a config
|
|
9
|
+
* Both a config instance as well as factory for.
|
|
10
10
|
*/
|
|
11
11
|
class Config {
|
|
12
12
|
|
|
@@ -28,7 +28,7 @@ class Config {
|
|
|
28
28
|
constructor (_context, _home, _defaults=true) {
|
|
29
29
|
Object.assign (this, { _context, _home, _sources:[] })
|
|
30
30
|
|
|
31
|
-
// 0. determine profiles from NODE_ENV+ CDS_ENV
|
|
31
|
+
// 0. determine profiles from NODE_ENV + CDS_ENV
|
|
32
32
|
const { NODE_ENV, CDS_ENV } = process.env, profiles = []
|
|
33
33
|
if (NODE_ENV) profiles.push (NODE_ENV)
|
|
34
34
|
if (CDS_ENV) profiles.push (...CDS_ENV.split(/\s*,\s*/))
|
|
@@ -52,29 +52,29 @@ class Config {
|
|
|
52
52
|
const sources = Config.sources(_home, _context)
|
|
53
53
|
|
|
54
54
|
// 2. read config sources in defined order
|
|
55
|
-
for (const
|
|
56
|
-
this._load(
|
|
55
|
+
for (const { path, file, mapper } of sources) {
|
|
56
|
+
this._load(path, file, mapper, this._profiles, false)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// 3. read important (!) profiles from config sources in defined order
|
|
60
60
|
const important = new Set(this.profiles.map( profile => `${profile}!` ).filter( profile => this._profiles._defined.has( profile ) ));
|
|
61
61
|
if (important.size > 0) {
|
|
62
|
-
for (const
|
|
63
|
-
this._load(
|
|
62
|
+
for (const { path, file, mapper } of sources) {
|
|
63
|
+
this._load(path, file, mapper, important, true)
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// 4. link
|
|
67
|
+
// 4. link dependent services (through kind/use)
|
|
68
68
|
this._link_required_services()
|
|
69
69
|
|
|
70
70
|
// 5. add process env
|
|
71
71
|
this._add_process_env(_context, _home)
|
|
72
72
|
|
|
73
|
-
// 6. link
|
|
73
|
+
// 6. link dependent services again -> e.g. to allow things like CDS_requires_db=sql
|
|
74
74
|
this._link_required_services()
|
|
75
75
|
|
|
76
76
|
// 7. complete service configurations from cloud service bindings
|
|
77
|
-
this._add_cloud_service_bindings(
|
|
77
|
+
this._add_cloud_service_bindings(process.env)
|
|
78
78
|
|
|
79
79
|
// 8. Add compatibility for mtx
|
|
80
80
|
if (this.requires && this.requires.db) {
|
|
@@ -480,48 +480,13 @@ function _readJson (file) {
|
|
|
480
480
|
|
|
481
481
|
|
|
482
482
|
|
|
483
|
-
function _readFromDir (p
|
|
484
|
-
if (
|
|
485
|
-
try {
|
|
486
|
-
const entry = fs.statSync(p)
|
|
487
|
-
if (entry.isDirectory()) {
|
|
488
|
-
isDir = true
|
|
489
|
-
} else if (isFile(p, entry)) {
|
|
490
|
-
isDir = false
|
|
491
|
-
} else {
|
|
492
|
-
return undefined
|
|
493
|
-
}
|
|
494
|
-
} catch (e) {
|
|
495
|
-
return undefined
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
if (isDir) {
|
|
483
|
+
function _readFromDir (p) {
|
|
484
|
+
if (isdir(p)) {
|
|
499
485
|
const result = {}
|
|
500
|
-
const
|
|
501
|
-
for (let entry of entries) {
|
|
502
|
-
const entryPath = path.join(p, entry.name)
|
|
503
|
-
if (entry.isDirectory()) {
|
|
504
|
-
result[entry.name] = _readFromDir(entryPath, true)
|
|
505
|
-
} else if (isFile(entryPath, entry)) {
|
|
506
|
-
result[entry.name] = _readFromDir(entryPath, false)
|
|
507
|
-
}
|
|
508
|
-
}
|
|
486
|
+
for (const dirent of fs.readdirSync(p)) result[dirent] = _readFromDir(path.join(p, dirent))
|
|
509
487
|
return result
|
|
510
|
-
} else {
|
|
511
|
-
return _value4(fs.readFileSync(p, "utf-8"))
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function isFile(p, entry) {
|
|
515
|
-
if (entry.isFile()) return true
|
|
516
|
-
if (entry.isSymbolicLink()) {
|
|
517
|
-
// Kubernetes credentials use symlinks
|
|
518
|
-
const target = fs.realpathSync(p)
|
|
519
|
-
const targetStat = fs.statSync(target)
|
|
520
|
-
|
|
521
|
-
if (targetStat.isFile()) return true
|
|
522
|
-
}
|
|
523
|
-
return false
|
|
524
488
|
}
|
|
489
|
+
return _value4(fs.readFileSync(p, "utf-8"))
|
|
525
490
|
}
|
|
526
491
|
|
|
527
492
|
|
package/lib/env/cds-requires.js
CHANGED
|
@@ -38,6 +38,9 @@ exports = module.exports = {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
const admin = [ 'cds.Subscriber', 'admin' ]
|
|
42
|
+
const builder = [ 'cds.ExtensionDeveloper', 'cds.UIFlexDeveloper' ]
|
|
43
|
+
|
|
41
44
|
const _authentication_strategies = {
|
|
42
45
|
|
|
43
46
|
"basic-auth": {
|
|
@@ -49,12 +52,13 @@ const _authentication_strategies = {
|
|
|
49
52
|
"mocked-auth": {
|
|
50
53
|
kind: 'basic-auth',
|
|
51
54
|
users: {
|
|
52
|
-
alice: {
|
|
53
|
-
bob: { roles: [
|
|
54
|
-
carol: { tenant: 't1', roles: [
|
|
55
|
-
dave: { tenant: 't1', roles: [
|
|
56
|
-
erin: { tenant: 't2', roles: [
|
|
57
|
-
fred: { tenant: 't2',
|
|
55
|
+
alice: { tenant: 't1', roles: [ ...admin ] },
|
|
56
|
+
bob: { tenant: 't1', roles: [ ...builder ] },
|
|
57
|
+
carol: { tenant: 't1', roles: [ ...admin, ...builder ] },
|
|
58
|
+
dave: { tenant: 't1', roles: [ ...admin ], features: [] },
|
|
59
|
+
erin: { tenant: 't2', roles: [ ...admin, ...builder ] },
|
|
60
|
+
fred: { tenant: 't2', features: ['isbn'] },
|
|
61
|
+
me: { tenant: 't1', features: ['*'] },
|
|
58
62
|
yves: { roles: ['internal-user'] },
|
|
59
63
|
'*': true
|
|
60
64
|
},
|
|
@@ -65,14 +69,16 @@ const _authentication_strategies = {
|
|
|
65
69
|
},
|
|
66
70
|
"jwt-auth": {
|
|
67
71
|
strategy: 'JWT', // REVISIT: Can be removed when we switch to new auth middlewars
|
|
72
|
+
kind: 'jwt-auth',
|
|
68
73
|
vcap: { label: 'xsuaa' }
|
|
69
74
|
},
|
|
70
75
|
"ias-auth": {
|
|
71
76
|
kind: 'ias-auth',
|
|
72
77
|
vcap: { label: 'identity' }
|
|
73
78
|
},
|
|
74
|
-
"xsuaa": {
|
|
79
|
+
"xsuaa": { // CLARIFY why is this not -auth postfixed?
|
|
75
80
|
strategy: 'xsuaa', // REVISIT: Can be removed when we switch to new auth middlewars
|
|
81
|
+
kind: 'xsuaa',
|
|
76
82
|
vcap: { label: 'xsuaa' }
|
|
77
83
|
},
|
|
78
84
|
"dummy-auth": {
|
package/lib/env/defaults.js
CHANGED
|
@@ -12,6 +12,10 @@ module.exports = {
|
|
|
12
12
|
},
|
|
13
13
|
|
|
14
14
|
features: {
|
|
15
|
+
"[schevo]": {
|
|
16
|
+
schema_evolution: true,
|
|
17
|
+
},
|
|
18
|
+
schema_evolution: false,
|
|
15
19
|
folders: 'fts/*', // where to find feature toggles -> switch on by default when released
|
|
16
20
|
cls: major > 12 || major == 12 && minor >= 18,
|
|
17
21
|
live_reload: !production,
|
package/lib/i18n/localize.js
CHANGED
|
@@ -10,8 +10,8 @@ module.exports = Object.assign (localize, {
|
|
|
10
10
|
})
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
function localize (model, /*with:*/ locale, aString) {
|
|
14
|
-
const _localize = bundle => localizeString (aString, bundle)
|
|
13
|
+
function localize (model, /*with:*/ locale, aString, extBundle) {
|
|
14
|
+
const _localize = bundle => localizeString (aString, bundle, extBundle)
|
|
15
15
|
|
|
16
16
|
const bundle = bundles4 (model, locale)
|
|
17
17
|
if (Array.isArray(locale)) { // array of multiple locales
|
|
@@ -29,14 +29,14 @@ function localize (model, /*with:*/ locale, aString) {
|
|
|
29
29
|
|
|
30
30
|
const TEXT_KEY_MARKER = 'i18n>'
|
|
31
31
|
const TEXT_KEYS = /{b?i18n>([^"}]+)}/g
|
|
32
|
-
function localizeString (aString, bundle) {
|
|
32
|
+
function localizeString (aString, bundle, extBundle) {
|
|
33
33
|
if (!bundle || !aString) return aString
|
|
34
34
|
if (typeof aString === 'object') aString = JSON.stringify(aString, null, 2)
|
|
35
35
|
// quick check for presence of any text key, to avoid expensive operation below
|
|
36
36
|
if (!aString.includes(TEXT_KEY_MARKER)) return aString
|
|
37
37
|
const escape = aString.startsWith('<?xml') ? escapeXmlAttr : /^[{[]/.test(aString) ? escapeJson : v=>v
|
|
38
38
|
return aString.replace (TEXT_KEYS, (_, key) => {
|
|
39
|
-
const val = bundle[key]
|
|
39
|
+
const val = (extBundle && extBundle[key]) || bundle[key]
|
|
40
40
|
return val ? escape(val) : key
|
|
41
41
|
})
|
|
42
42
|
}
|
|
@@ -155,11 +155,14 @@ function folders4 (model) {
|
|
|
155
155
|
// foo/node_modules/reuse-level-1/model.cds
|
|
156
156
|
// foo/node_modules/reuse-level-2/model.cds
|
|
157
157
|
if (!model.$sources) return []
|
|
158
|
-
const
|
|
158
|
+
const srcFolders = new Set (model.$sources.map (dirname))
|
|
159
|
+
const folders = []
|
|
160
|
+
srcFolders.forEach(src => {
|
|
159
161
|
let folder = folder4 (src)
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
162
|
+
if (folder && !folders.includes(folder)) {
|
|
163
|
+
folders.push(folder) // use an array here to not screw up the folder order
|
|
164
|
+
}
|
|
165
|
+
})
|
|
163
166
|
|
|
164
167
|
Object.defineProperty (model, '_i18nfolders', {value:folders})
|
|
165
168
|
return folders.reverse()
|
package/lib/index.js
CHANGED
|
@@ -132,7 +132,7 @@ extend (cds.__proto__) .with (lazified ({
|
|
|
132
132
|
const odp = Object.defineProperty, _global = (_,...pp) => pp.forEach (p => odp(global,p,{
|
|
133
133
|
configurable:true, get:()=>{ let v=cds[_][p]; odp(this,p,{value:v}); return v }
|
|
134
134
|
}))
|
|
135
|
-
_global ('ql','SELECT','INSERT','UPDATE','DELETE','CREATE','DROP')
|
|
135
|
+
_global ('ql','SELECT','INSERT','UPSERT','UPDATE','DELETE','CREATE','DROP')
|
|
136
136
|
_global ('parse','CDL','CQL','CXL')
|
|
137
137
|
|
|
138
138
|
// Check Node.js version
|
package/lib/log/cds-log.js
CHANGED
|
@@ -136,7 +136,7 @@ exports.winstonLogger = (options) => (label, level) => {
|
|
|
136
136
|
return simple (label, level, ...args)
|
|
137
137
|
},
|
|
138
138
|
get json() {
|
|
139
|
-
return this._json || (this._json = require('./format/
|
|
139
|
+
return this._json || (this._json = require('./format/cf'))
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -149,7 +149,7 @@ exports.winstonLogger = (options) => (label, level) => {
|
|
|
149
149
|
*
|
|
150
150
|
* @param {string} label the label to prefix to log output
|
|
151
151
|
* @param {number} level the log level to enable -> 0=off, 1=error, 2=warn, 3=info, 4=debug, 5=trace
|
|
152
|
-
* @param {any[]} args the arguments passed to Logger.debug|log|info|
|
|
152
|
+
* @param {any[]} args the arguments passed to Logger.debug|log|info|warn|error()
|
|
153
153
|
*/
|
|
154
154
|
exports.format = (
|
|
155
155
|
process.env.NODE_ENV === 'production' && cds.env.features.kibana_formatter ? log.formatters.json :
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const cds = require ('../../')
|
|
2
|
+
const kibana = require('./kibana')
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* extension of log formatter for kibana that additionally logs Cloud Foundry specific data
|
|
6
|
+
*/
|
|
7
|
+
module.exports = (module, level, ...args) => {
|
|
8
|
+
const toLog = kibana.addFields(module, level, ...args)
|
|
9
|
+
|
|
10
|
+
toLog.layer = 'cds'
|
|
11
|
+
|
|
12
|
+
// cds.context._ instead of cds.context.http because of messaging
|
|
13
|
+
toLog.tenant_subdomain = cds.context?._?.req?.authInfo?.getSubdomain()
|
|
14
|
+
|
|
15
|
+
return kibana.format(toLog)
|
|
16
|
+
}
|
package/lib/log/format/kibana.js
CHANGED
|
@@ -6,7 +6,12 @@ const _l2l = { 1: 'error', 2: 'warn', 3: 'info', 4: 'debug', 5: 'trace' }
|
|
|
6
6
|
/*
|
|
7
7
|
* log formatter for kibana
|
|
8
8
|
*/
|
|
9
|
-
module.exports = (module, level, ...args) => {
|
|
9
|
+
module.exports = exports = (module, level, ...args) => {
|
|
10
|
+
const toLog = exports.addFields(module, level, ...args)
|
|
11
|
+
return exports.format(toLog)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
exports.addFields = (module, level, ...args) => {
|
|
10
15
|
// config
|
|
11
16
|
const { user: log_user , kibana_custom_fields } = cds.env.log
|
|
12
17
|
|
|
@@ -28,7 +33,11 @@ module.exports = (module, level, ...args) => {
|
|
|
28
33
|
const req = cds.context._ && cds.context._.req
|
|
29
34
|
if (req && req.headers)
|
|
30
35
|
for (const k in req.headers)
|
|
31
|
-
toLog[k.replace(/-/g, '_')] =
|
|
36
|
+
toLog[k.replace(/-/g, '_')] = (() => {
|
|
37
|
+
if (k.match(/authorization/i)) return `${req.headers[k].split(' ')[0]} ***`
|
|
38
|
+
if (k.match(/cookie/i)) return '***'
|
|
39
|
+
return req.headers[k]
|
|
40
|
+
})()
|
|
32
41
|
}
|
|
33
42
|
toLog.timestamp = new Date()
|
|
34
43
|
|
|
@@ -56,6 +65,10 @@ module.exports = (module, level, ...args) => {
|
|
|
56
65
|
if (cf.length) toLog['#cf'] = { string: cf }
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
return toLog
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
exports.format = toLog => {
|
|
59
72
|
const getCircularReplacer = () => {
|
|
60
73
|
const seen = new WeakSet()
|
|
61
74
|
return (key, value) => {
|