@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/apis/connect.d.ts +1 -1
  3. package/apis/cqn.d.ts +1 -1
  4. package/apis/internal/inference.d.ts +14 -0
  5. package/apis/ql.d.ts +40 -36
  6. package/apis/services.d.ts +23 -6
  7. package/bin/build/buildTaskEngine.js +15 -12
  8. package/bin/build/buildTaskHandler.js +3 -3
  9. package/bin/build/constants.js +2 -0
  10. package/bin/build/provider/buildTaskHandlerEdmx.js +1 -1
  11. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +4 -3
  12. package/bin/build/provider/buildTaskHandlerInternal.js +2 -2
  13. package/bin/build/provider/java/index.js +2 -1
  14. package/bin/build/provider/mtx/index.js +2 -1
  15. package/bin/build/provider/mtx/resourcesTarBuilder.js +3 -2
  16. package/bin/build/provider/mtx-extension/index.js +2 -1
  17. package/bin/build/provider/mtx-sidecar/index.js +3 -1
  18. package/bin/build/util.js +2 -2
  19. package/bin/deploy/to-hana/cfUtil.js +46 -62
  20. package/lib/auth/index.js +2 -1
  21. package/lib/auth/jwt-auth.js +64 -3
  22. package/lib/auth/xsuaa-auth.js +2 -3
  23. package/lib/compile/cdsc.js +1 -0
  24. package/lib/compile/etc/_localized.js +1 -0
  25. package/lib/dbs/cds-deploy.js +2 -1
  26. package/lib/env/cds-env.js +14 -49
  27. package/lib/env/cds-requires.js +13 -7
  28. package/lib/env/defaults.js +4 -0
  29. package/lib/i18n/localize.js +11 -8
  30. package/lib/index.js +1 -1
  31. package/lib/log/cds-log.js +2 -2
  32. package/lib/log/format/cf.js +16 -0
  33. package/lib/log/format/kibana.js +15 -2
  34. package/lib/ql/INSERT.js +12 -11
  35. package/lib/ql/Query.js +14 -7
  36. package/lib/ql/UPSERT.js +1 -0
  37. package/lib/ql/Whereable.js +6 -2
  38. package/lib/ql/cds-ql.js +2 -4
  39. package/lib/req/request.js +2 -0
  40. package/lib/srv/bindings.js +1 -0
  41. package/lib/srv/middlewares/cds-context.js +1 -1
  42. package/lib/srv/srv-dispatch.js +1 -0
  43. package/lib/srv/srv-tx.js +3 -3
  44. package/lib/utils/cds-utils.js +75 -30
  45. package/lib/utils/inflect.js +24 -0
  46. package/libx/_runtime/auth/strategies/ias-auth.js +1 -1
  47. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +9 -1
  48. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +23 -6
  49. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -0
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +27 -15
  51. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -1
  52. package/libx/_runtime/cds-services/services/utils/compareJson.js +11 -10
  53. package/libx/_runtime/cds-services/services/utils/differ.js +6 -4
  54. package/libx/_runtime/common/composition/data.js +29 -40
  55. package/libx/_runtime/common/composition/update.js +6 -19
  56. package/libx/_runtime/common/generic/paging.js +1 -1
  57. package/libx/_runtime/common/utils/resolveView.js +7 -13
  58. package/libx/_runtime/db/utils/generateAliases.js +1 -0
  59. package/libx/_runtime/fiori/generic/before.js +5 -2
  60. package/libx/_runtime/fiori/generic/read.js +11 -4
  61. package/libx/_runtime/hana/execute.js +2 -2
  62. package/libx/_runtime/hana/search2Contains.js +3 -1
  63. package/libx/_runtime/hana/search2cqn4sql.js +1 -0
  64. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  65. package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +5 -2
  66. package/libx/_runtime/messaging/enterprise-messaging.js +7 -1
  67. package/libx/_runtime/messaging/file-based.js +1 -1
  68. package/libx/_runtime/messaging/message-queuing.js +5 -2
  69. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  70. package/libx/_runtime/messaging/service.js +5 -3
  71. package/libx/odata/cqn2odata.js +4 -1
  72. package/libx/odata/utils.js +8 -7
  73. package/libx/rest/RestAdapter.js +1 -4
  74. 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
- 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
 
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
- xsuaa: require('./xsuaa-auth'),
28
+ jwt: require('./jwt-auth'),
29
+ xsuaa: require('./jwt-auth'),
29
30
  }))
30
31
 
31
32
  require = _require // eslint-disable-line no-global-assign
@@ -1,3 +1,64 @@
1
- // TODO...
2
- console.trace ('JWT auth is not yet implemented')
3
- module.exports = ()=> (req,res,next)=> next()
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
+ }
@@ -1,3 +1,2 @@
1
- // TODO...
2
- console.trace ('XSUAA auth is not yet implemented')
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')
@@ -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,
@@ -2,6 +2,7 @@ const cds = require('../..'), {env} = cds
2
2
  const DEBUG = cds.debug('alpha|_localized')
3
3
  const _locales_4sql = {
4
4
  sqlite : env.i18n.for_sqlite || env.i18n.for_sql || [],
5
+ h2 : env.i18n.for_sql || [],
5
6
  plain : env.i18n.for_sql || [],
6
7
  }
7
8
 
@@ -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, }
@@ -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 inctance as well as factory for.
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 source of sources) {
56
- this._load(source.path, source.file, source.mapper, this._profiles, false)
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 source of sources) {
63
- this._load(source.path, source.file, source.mapper, important, true)
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 dependant services (through kind/use)
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 dependant services again -> e.g. to allow things like CDS_requires_db=sql
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({ VCAP_SERVICES: process.env.VCAP_SERVICES, SERVICE_BINDING_ROOT: process.env.SERVICE_BINDING_ROOT })
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, isDir) {
484
- if (typeof isDir === "undefined") {
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 entries = fs.readdirSync(p, {withFileTypes: true})
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
 
@@ -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: { roles: ['admin', 'cds.Subscriber'] },
53
- bob: { roles: ['builder'] },
54
- carol: { tenant: 't1', roles: ['admin', 'cds.Subscriber', 'cds.ExtensionDeveloper', 'cds.UIFlexDeveloper'] },
55
- dave: { tenant: 't1', roles: ['cds.Subscriber'], features: [] }, // user-specific features
56
- erin: { tenant: 't2', roles: ['admin', 'cds.Subscriber', 'cds.ExtensionDeveloper'] },
57
- fred: { tenant: 't2', roles: [], features: ['isbn'] },
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": {
@@ -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,
@@ -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 folders=[]; for (let src of model.$sources) {
158
+ const srcFolders = new Set (model.$sources.map (dirname))
159
+ const folders = []
160
+ srcFolders.forEach(src => {
159
161
  let folder = folder4 (src)
160
- if (!folder || folders.includes(folder)) continue
161
- folders.push(folder) // use an array here to not screw up the folder order
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
@@ -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/kibana'))
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|wanr|error()
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
+ }
@@ -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, '_')] = k.match(/authorization/i) ? `${req.headers[k].split(' ')[0]} ***` : req.headers[k]
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) => {