@sap/cds 8.1.0 → 8.2.0

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 (61) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/app/index.css +3 -0
  3. package/app/index.js +50 -4
  4. package/bin/serve.js +1 -1
  5. package/lib/compile/cdsc.js +2 -2
  6. package/lib/compile/etc/_localized.js +1 -1
  7. package/lib/compile/for/lean_drafts.js +1 -0
  8. package/lib/compile/to/sql.js +2 -2
  9. package/lib/env/cds-requires.js +6 -0
  10. package/lib/env/defaults.js +14 -3
  11. package/lib/env/plugins.js +6 -22
  12. package/lib/index.js +3 -2
  13. package/lib/linked/classes.js +0 -14
  14. package/lib/linked/types.js +12 -0
  15. package/lib/linked/validate.js +3 -2
  16. package/lib/log/cds-log.js +3 -3
  17. package/lib/log/format/aspects/als.js +23 -29
  18. package/lib/log/format/aspects/cls.js +9 -0
  19. package/lib/log/format/json.js +42 -6
  20. package/lib/ql/Whereable.js +5 -1
  21. package/lib/req/context.js +1 -0
  22. package/lib/req/locale.js +1 -1
  23. package/lib/srv/cds-connect.js +33 -32
  24. package/lib/srv/cds-serve.js +2 -1
  25. package/lib/srv/srv-tx.js +1 -0
  26. package/lib/utils/cds-utils.js +4 -2
  27. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -5
  28. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +2 -31
  29. package/libx/_runtime/common/generic/auth/utils.js +2 -0
  30. package/libx/_runtime/common/generic/input.js +2 -11
  31. package/libx/_runtime/common/generic/put.js +1 -10
  32. package/libx/_runtime/common/utils/binary.js +1 -7
  33. package/libx/_runtime/common/utils/cqn2cqn4sql.js +10 -1
  34. package/libx/_runtime/common/utils/resolveView.js +2 -2
  35. package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
  36. package/libx/_runtime/common/utils/streamProp.js +12 -1
  37. package/libx/_runtime/common/utils/template.js +26 -16
  38. package/libx/_runtime/common/utils/templateProcessor.js +8 -7
  39. package/libx/_runtime/common/utils/ucsn.js +2 -5
  40. package/libx/_runtime/db/expand/expandCQNToJoin.js +43 -2
  41. package/libx/_runtime/db/generic/input.js +1 -5
  42. package/libx/_runtime/fiori/lean-draft.js +287 -96
  43. package/libx/_runtime/messaging/event-broker.js +105 -40
  44. package/libx/_runtime/remote/Service.js +3 -1
  45. package/libx/_runtime/remote/utils/client.js +12 -4
  46. package/libx/_runtime/ucl/Service.js +16 -6
  47. package/libx/odata/middleware/batch.js +2 -2
  48. package/libx/odata/middleware/create.js +5 -0
  49. package/libx/odata/middleware/delete.js +5 -0
  50. package/libx/odata/middleware/error.js +1 -0
  51. package/libx/odata/middleware/operation.js +6 -0
  52. package/libx/odata/middleware/read.js +16 -11
  53. package/libx/odata/middleware/stream.js +4 -5
  54. package/libx/odata/middleware/update.js +9 -4
  55. package/libx/odata/parse/afterburner.js +3 -2
  56. package/libx/odata/parse/multipartToJson.js +1 -1
  57. package/libx/odata/utils/index.js +3 -3
  58. package/libx/odata/utils/postProcess.js +3 -25
  59. package/libx/rest/middleware/error.js +1 -0
  60. package/libx/rest/middleware/parse.js +1 -6
  61. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,62 @@
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 8.2.0 - 2024-08-30
8
+
9
+ ### Added
10
+
11
+ - Allow `cds.connect.to (SomeService)` where `SomeService` is a class
12
+ - Lean draft: support CDS orderBy in `list status: all`
13
+ - Support where not in as object in `cds.ql` expressions like: `where({ID:{not:{in:[...]}}})`
14
+ - Unbound CDS functions now show up in the server's index page along with an exemplary call signature
15
+ - `cds.log`'s JSON formatter:
16
+ + Field `w3c_traceparent` is filled based on request header `traceparent` (cf. W3C Trace Context) for improved correlation
17
+ + Custom fields `cds.env.log.cls_custom_fields` are filled if bound to an instance of SAP Cloud Logging
18
+ + Default `cds.env.log.als_custom_fields` enhanced by `{ reason: 3 }` (project config takes precedence)
19
+ - Support for `cds.hana` types like `cds.hana.ST_POINT` in `cds.builtin`
20
+ - Internal `cds.debug()` API now always returns a logger instance, which allows switching on debugging subsequently, e.g. by the like of `cds.log('sql','debug')`
21
+ - New config flag `cds.server.shutdown_on_uncaught_errors` allows to control whether the server should shut down on uncaught errors. Default is `true`
22
+
23
+ ### Changed
24
+
25
+ - Revert workaround from 8.1.0 for server startup message `WARNING: Package '@sap/cds' was loaded from different installations`. This is now addressed in `@sap/cds-mtxs` 2.0.5
26
+
27
+ ### Fixed
28
+
29
+ - Resolving views with path expression renamings
30
+ - Set content-type-header in batch for actions with 204 No Content
31
+ - URI encoding of `@odata.nextLink` in OData response
32
+ - Requests reading media data streams did not provide `req.params`
33
+ - `cds.compile.to.hana` for legacy hana service with `@cap-js/sqlite` as dev dependency
34
+ - Better redaction of debug output
35
+ - Instance-based authorization using functions
36
+ - Fixed flaws in `cds.connect.to()` that lead to deadlocks in case of errors due to invalid service configurations or initializations.
37
+ - Navigation with backlink as key can now omit backlink keys for new OData adapter
38
+
39
+ ### Removed
40
+
41
+ - Array methods `forEach`, `filter`, `find`, `map`, `some`, `every` from [`LinkedDefinitions`](https://cap.cloud.sap/docs/node.js/cds-reflect#iterable). Convert linked definitions into arrays before using these methods, for example:
42
+
43
+ ```js
44
+ [...linked.definitions].map(d => d.name)
45
+ ```
46
+
47
+
48
+ ## Version 8.1.1 - 2024-08-08
49
+
50
+ ### Fixed
51
+
52
+ - For `accept-language`, ignore additional options
53
+ - Global `describe`, `before`, `beforeAll`, `afterAll` hooks are now writable again. They were accidentally made read-only in 8.0.0.
54
+ - Expand to `DraftAdministrativeData` for active instances of draft-enabled entities over drafts
55
+ - Deduplication of columns for certain on conditions for the legacy database driver
56
+ - For legacy-sqlite/-hana: Add keys to expands with only non-key elements to ensure not returning null for expand.
57
+ - New parser was to restrictive regarding an empty line at the end of batch body.
58
+ - Error target for operations with complex parameters
59
+ - Remote services: JWT gets found in authorization header
60
+ - Search with invalid characters
61
+ - Invoke `srv.on('error')` for each failing batch subrequest
62
+
7
63
  ## Version 8.1.0 - 2024-07-26
8
64
 
9
65
  ### Added
@@ -43,6 +99,9 @@
43
99
  ### Fixed
44
100
 
45
101
  - Empty feature set by switched off feature toggles
102
+ - Allow programmatic operations on draft-enabled entities (`NEW`, `CREATE`, `UPDATE`, `DELETE`)
103
+
104
+ ### Removed
46
105
  - Allow deviating response types for `$batch`, e. g. input `multipart` and output `json`
47
106
 
48
107
  ## Version 8.0.2 - 2024-07-09
@@ -315,6 +374,7 @@
315
374
 
316
375
  ### Changed
317
376
 
377
+ - Optimized handling of large binaries (BLOBs) in case of drafts. Unchanged BLOBs are not copied into the draft entity. If those BLOBs from draft entities are requested, the unchanged BLOBs will be fetched from the corresponding active entity. Note that this change may require adjustment of custom logic, if large binaries from draft entities are requested (for example, using `ql.SELECT` statement). To restore previous behavior use `cds.features.binary_draft_compat`.
318
378
  - The index page now lists all service endpoints, which is important for services that are exposed through multiple protocols.
319
379
  - `cds.deploy` improves error diagnostics with deeper `Query` object inspection.
320
380
  - Slightly changed the default export for ESM compatibility. This fixed failing ESM imports in Vitest tests.
package/app/index.css CHANGED
@@ -77,6 +77,9 @@ body #welcome li a, body #welcome li > span {
77
77
  color: #1d5985;
78
78
  transition: all 0.1s ease-in;
79
79
  }
80
+ body #welcome li.operation span {
81
+ font-style: italic;
82
+ }
80
83
 
81
84
  body #welcome li a span:hover {
82
85
  color: #f0faff;
package/app/index.js CHANGED
@@ -14,7 +14,7 @@ module.exports = { get html(){
14
14
  .replace (/{{app}}/g, cds.env.folders.app.replace(/*trailing slash*/ /\/$/, ''))
15
15
  .replace ('{{style}}', css)
16
16
  .replace ('{{apps}}', _app_links().map(
17
- html => `\n<li><a href="${html}"><span>/${html.replace(/^\//,'').replace('/index.html','')}</span></a></li>`
17
+ html => `<li><a href="${html}"><span>/${html.replace(/^\//,'').replace('/index.html','')}</span></a></li>`
18
18
  ).join('\n') || '— none —'
19
19
  )
20
20
  .replace ('{{services}}', cds.service.providers
@@ -25,14 +25,20 @@ module.exports = { get html(){
25
25
  <h3 class="header">
26
26
  <a href="${endpoint.path}"><span>${endpoint.path}</span></a>${metadata(endpoint)} ${_moreLinks(srv, endpoint, undefined, false)}
27
27
  </h3>
28
- <ul>${_entities_in(srv).map (e => {
29
- return `
28
+ <ul>${_entities_in(srv).map (e => `
30
29
  <li id="${asHtmlId(srv.name)}-${endpoint.kind}-${asHtmlId(e)}">
31
30
  <div>
32
31
  <a href="${endpoint.path}/${e.replace(/\./g, '_')}"><span>${e}</span></a>
33
32
  </div>
34
33
  ${_moreLinks(srv, endpoint, e)}
35
- </li>`}).join('')}
34
+ </li>`).join('')}
35
+ </ul>
36
+ <ul>${_operations_in(srv).map (e => `
37
+ <li id="${asHtmlId(srv.name)}-${endpoint.kind}-${asHtmlId(e.name)}" class="operation">
38
+ <div>
39
+ <a href="${endpoint.path}/${e.name}${e.params}" title="${endpoint.path}/${e.name}${e.params}"><span>${e.name}()</span></a>
40
+ </div>
41
+ </li>`).join('')}
36
42
  </ul>
37
43
  </div>
38
44
  `).join(''))
@@ -63,6 +69,46 @@ function _entities_in (service) {
63
69
  return exposed
64
70
  }
65
71
 
72
+ function _operations_in (service) {
73
+ const exposed=[], {operations} = service
74
+ for (let name in operations) {
75
+ const op = cds.model.definitions[service.name + '.' + name]
76
+ if (op?.kind === 'function') {
77
+ const params = '('+ Object.values(op.params||[]).map(p => {
78
+ let val = _sampleValue(p)
79
+ if (typeof val === 'string') val = encodeURIComponent(`'${val}'`)
80
+ else if (typeof val === 'object') val = encodeURIComponent(`'${JSON.stringify(val)}'`)
81
+ return `${p.name}=${val}`
82
+ }).join(',') + ')'
83
+ exposed.push ({ name, params })
84
+ }
85
+ }
86
+ return exposed
87
+ }
88
+
89
+ function _sampleValue (param) {
90
+ if (param.items) // many
91
+ return [ _sampleValue(param.items) ]
92
+ if (param.elements) { // structured
93
+ return Object.entries(param.elements).reduce((all,[n,p]) => {
94
+ all[n] = _sampleValue(p)
95
+ return all
96
+ },{})
97
+ }
98
+ // scalar
99
+ const type = param._type || param.type
100
+ if (type === 'cds.String') return 'hello'
101
+ if (type === 'cds.Boolean') return true
102
+ if (type === 'cds.Decimal'||type === 'cds.Double') return '4.2'
103
+ if (type === 'cds.Date') return '2021-12-31'
104
+ if (type === 'cds.Time') return '23:42:42'
105
+ if (type === 'cds.DateTime') return '2021-12-31T23:42:42Z'
106
+ if (type === 'cds.Timestamp') return '2021-12-31T23:42:42.123Z'
107
+ if (type === 'cds.UUID') return cds.utils.uuid()
108
+ if (type?.match(/cds\..*Int.*/i)) return 42
109
+ return type // fallback
110
+ }
111
+
66
112
  function _moreLinks (srv, endpoint, entity, div=true) {
67
113
  return (srv.$linkProviders || [])
68
114
  .map (linkProv => linkProv(entity, endpoint))
package/bin/serve.js CHANGED
@@ -212,7 +212,7 @@ async function serve (all=[], o={}) {
212
212
 
213
213
  const LOG = cds.log('cli|server')
214
214
  cds.shutdown = _shutdown //> for programmatic invocation
215
- if (!cds.repl) {
215
+ if (cds.env.server.shutdown_on_uncaught_errors && !cds.repl) {
216
216
  process.on('unhandledRejection', e => _shutdown (e, cds.log().error('❗️Uncaught',e))) //> using std logger to have it labelled with [cds] - instead of [cli] -
217
217
  process.on('uncaughtException', e => _shutdown (e, cds.log().error('❗️Uncaught',e))) //> using std logger to have it labelled with [cds] - instead of [cli] -
218
218
  }
@@ -77,8 +77,8 @@ const _options = {for: Object.assign (_options4, {
77
77
  if (dialect) o.sqlDialect = dialect
78
78
  }
79
79
 
80
- const legacy_sqlite = '@sap/cds/libx/_runtime/sqlite/Service.js'
81
- if (_conf.impl === legacy_sqlite) {
80
+ const legacy = '@sap/cds/libx/_runtime/' // includes legacy sqlite and hana
81
+ if (_conf.impl.includes(legacy)) {
82
82
  o.betterSqliteSessionVariables = false
83
83
  o.fewerLocalizedViews = false
84
84
  }
@@ -16,7 +16,7 @@ const _been_here = Symbol('is _localized')
16
16
  function unfold_ddl (ddl, csn, o={}) { // NOSONAR
17
17
  const db = env.requires.db || env.requires.kinds.sql
18
18
  const locales = db.impl === _legacy_sqlite && _locales_4sql[o.dialect]; if (!locales) return ddl
19
- const localized_views = ddl.filter (each => each.startsWith('CREATE VIEW localized_') || each.startsWith('DROP VIEW localized_'))
19
+ const localized_views = ddl.filter (each => /^(?:-- .+\n)*(?:CREATE|DROP) VIEW localized_/g.test(each))
20
20
  for (const localized_view of localized_views) {
21
21
  for (const locale of locales) ddl.push (localized_view
22
22
  .replace (/localized_/g, `localized_${locale}_`)
@@ -66,6 +66,7 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
66
66
  }
67
67
  Object.defineProperty(model.definitions, _draftEntity, { value: draft })
68
68
  Object.defineProperty(active, 'drafts', { value: draft })
69
+ Object.defineProperty(active, 'actives', { value: active })
69
70
  Object.defineProperty(draft, 'actives', { value: active })
70
71
  Object.defineProperty(draft, 'isDraft', { value: true })
71
72
 
@@ -31,8 +31,8 @@ function cds_compile_to_deltaSql (csn, o, beforeCsn) {
31
31
  const { afterImage, drops, createsAndAlters } = cdsc.to.deltaSql (csn, options, beforeCsn || {definitions: {}, $version: '2.0'} ); // FIXME: As default value in compiler API?
32
32
  return {
33
33
  afterImage,
34
- drops: unfold_ddl(drops.map (each => each.replace(/^-- .+\n/,'')), csn, options),
35
- createsAndAlters: unfold_ddl(createsAndAlters.map (each => each.replace(/^-- .+\n/,'')), csn, options)
34
+ drops: unfold_ddl(drops, csn, options),
35
+ createsAndAlters: unfold_ddl(createsAndAlters, csn, options)
36
36
  };
37
37
  }
38
38
 
@@ -234,6 +234,12 @@ const _messaging = {
234
234
  "event-broker": {
235
235
  impl: `${_runtime}/messaging/event-broker.js`,
236
236
  format: 'cloudevents',
237
+ vcap: {
238
+ label: "event-broker"
239
+ }
240
+ },
241
+ "event-broker-internal": {
242
+ kind: "event-broker",
237
243
  vcap: {
238
244
  label: "eventmesh-sap2sap-internal"
239
245
  }
@@ -39,6 +39,7 @@ const defaults = module.exports = {
39
39
  requires: require('./cds-requires'),
40
40
 
41
41
  server: {
42
+ shutdown_on_uncaught_errors: true,
42
43
  force_exit_timeout: 1111,
43
44
  body_parser: undefined, // Allows to configure all body parser options, e.g. limit
44
45
  cors: !production, // CORS middleware is off in production
@@ -102,15 +103,25 @@ const defaults = module.exports = {
102
103
  // the rest is only applicable for the json formatter
103
104
  user: false,
104
105
  mask_headers: ['/authorization/i', '/cookie/i', '/cert/i', '/ssl/i'],
105
- aspects: ['./aspects/cf', './aspects/als'], //> EXPERIMENTAL!!!
106
+ aspects: ['./aspects/cf', './aspects/als', './aspects/cls'], //> EXPERIMENTAL!!!
106
107
  // adds custom fields in kibana's error rendering (unknown fields are ignored); key: index
107
108
  // note: custom fields are a feature of Application Logging Service (ALS) and not Kibana per se
108
109
  als_custom_fields: {
109
110
  // sql
110
111
  query: 0,
111
112
  // generic validations
112
- target: 1, details: 2
113
- }
113
+ target: 1, details: 2,
114
+ // errors
115
+ reason: 3
116
+ },
117
+ cls_custom_fields: [
118
+ // sql
119
+ 'query',
120
+ // generic validations
121
+ 'target', 'details',
122
+ // errors
123
+ 'reason'
124
+ ]
114
125
  },
115
126
 
116
127
  folders: { // IMPORTANT: order is significant for cds.load('*')
@@ -1,34 +1,18 @@
1
1
  // REVISIT: we should have a real modular plugin technique for cds.env
2
2
  module.exports = function add_mtx_env (env) {
3
3
 
4
- if (cds_load_mismatch()) return env
5
-
6
- const mtx_env = _require('@sap/cds-mtxs/env')
4
+ const mtx_env = require('@sap/cds-mtxs/env')
7
5
  if (mtx_env) {
8
6
  const {requires} = env, {kinds} = requires
9
7
  Object.assign (env, mtx_env, {requires})
10
8
  Object.assign (requires, mtx_env.requires, {kinds})
11
9
  Object.assign (kinds, mtx_env.requires?.kinds)
12
10
  }
13
- return env
14
- }
15
11
 
16
- function _require (id) {
17
- try { return module.require(id) }
18
- catch(e) { if (e.code !== 'MODULE_NOT_FOUND') throw e }
19
- }
12
+ function require (id) {
13
+ try { return module.require(id) }
14
+ catch(e) { if (e.code !== 'MODULE_NOT_FOUND') throw e }
15
+ }
20
16
 
21
- // Anticipates if loading mtxs would load a _different_ @sap/cds than this one, which we must avoid.
22
- // May happen in PNPM setups that add all global installs to NODE_PATH. Seen in BAS.
23
- function cds_load_mismatch() {
24
- const cds = require('..')
25
- try {
26
- const mtxs = require.resolve('@sap/cds-mtxs')
27
- const cds2 = require.resolve('@sap/cds/package.json', { paths:[mtxs] })
28
- const csd2Home = cds.utils.path.resolve(cds2, '..')
29
- if (csd2Home !== cds.home) {
30
- return true
31
- }
32
- // console.log('home', cds.home, 'other', otherCdsHome)
33
- } catch(e) { if (e.code !== 'MODULE_NOT_FOUND') throw e }
17
+ return env
34
18
  }
package/lib/index.js CHANGED
@@ -137,8 +137,9 @@ extend (global) .with (class {
137
137
 
138
138
  // ensure cds.test is loaded for running tests w/ node --test
139
139
  'describe' in global || ['describe','before','beforeAll','afterAll'].forEach (p => {
140
- Object.defineProperty (global,p, { configurable:1, set(v){Object.defineProperty(global,p,{value:v})},
141
- get(){ return cds.test, global[p] }
140
+ Object.defineProperty (global,p, { configurable:1,
141
+ set(v){ Object.defineProperty (global,p,{ value:v, writable:true }) },
142
+ get(){ cds.test; return global[p] }
142
143
  })
143
144
  })
144
145
 
@@ -184,22 +184,8 @@ class event extends aspect {}
184
184
 
185
185
  class LinkedDefinitions {
186
186
  *[Symbol.iterator](){ for (let e in this) yield this[e] }
187
- forEach(f){ let i=0; for (let k in this) f(this[k],i++,this) }
188
- filter(f){ let i=0, r=[]; for (let k in this) f(this[k],i++,this) && r.push(this[k]); return r }
189
- find(f){ for (let k in this) if (f(this[k])) return this[k] }
190
- map(f){ let i=0, r=[]; for (let k in this) r.push(f(this[k],i++,this)); return r }
191
- some(f){ for (let k in this) if (f(this[k])) return true }
192
- every(f){ for (let k in this) if (!f(this[k])) return false }
193
187
  }
194
188
 
195
- // Protect LinkedDefinitions methods from erroneously being used as linked definitions/elements
196
- Object.entries(Object.getOwnPropertyDescriptors(LinkedDefinitions.prototype)).forEach(([p,pd]) => p === 'constructor' || Object.defineProperties(pd.value, {
197
- own: {get(){ throw new Error(`'${p}' is not a linked definition but a method inherited from LinkedDefinitions`) }},
198
- name: {get(){ throw new Error(`'${p}' is not a linked definition but a method inherited from LinkedDefinitions`) }},
199
- kind: {get(){ throw new Error(`'${p}' is not a linked definition but a method inherited from LinkedDefinitions`) }},
200
- type: {get(){ throw new Error(`'${p}' is not a linked definition but a method inherited from LinkedDefinitions`) }},
201
- _type: {get(){ throw new Error(`'${p}' is not a linked definition but a method inherited from LinkedDefinitions`) }},
202
- }))
203
189
 
204
190
  module.exports = {
205
191
 
@@ -20,6 +20,18 @@ Object.assign (protos, types.deprecated = {
20
20
  'cds.Integer64': new classes.Int64,
21
21
  })
22
22
 
23
+ Object.assign (protos, types.hana = {
24
+ 'cds.hana.SMALLDECIMAL': new classes.Decimal,
25
+ 'cds.hana.SMALLINT': new classes.Int16,
26
+ 'cds.hana.TINYINT': new classes.UInt8,
27
+ 'cds.hana.REAL': new classes.Double,
28
+ 'cds.hana.CHAR': new classes.String,
29
+ 'cds.hana.CLOB': new classes.LargeString,
30
+ 'cds.hana.NCHAR': new classes.String,
31
+ 'cds.hana.BINARY': new classes.String,
32
+ 'cds.hana.ST_POINT': new classes.type,
33
+ 'cds.hana.ST_GEOMETRY': new classes.type,
34
+ })
23
35
 
24
36
  protos.service.set('is_service',true)
25
37
  protos.struct.set('is_struct',true)
@@ -26,15 +26,16 @@ class Validation {
26
26
  const err = (this.errors ??= new ValidationErrors).add (code)
27
27
  if (this.options.path) path = [ this.options.path, ...path ] // e.g. used to prefic 'in/' for actions
28
28
  if (path) err.target = (!leaf ? path : path.concat(leaf)).reduce?.((p,n)=> (
29
- n?.row ? p + this.filter4(n) : //> some/entity(ID=1)...
29
+ n?.row ? p + this.filter4(n) : //> some/entity(ID=1)...
30
30
  typeof n === 'number' ? p + `[${n}]` : //> some/array[1]...
31
- p && n ? p+'/'+n : n //> some/element/...
31
+ p && n ? p+'/'+n : n //> some/element...
32
32
  ),'')
33
33
  if (val) err.args = [ val, ...args ]
34
34
  return err
35
35
  }
36
36
 
37
37
  filter4 ({ def, row, index }) {
38
+ if (this.target.kind in { 'action': 1, 'function': 1 }) return '' //> no filter for operations
38
39
  const entity = def._target || def, filter=[]
39
40
  for (let k in entity.keys) {
40
41
  let v = row[k]
@@ -71,9 +71,9 @@ function cds_log (module, options) { // NOSONAR
71
71
  */
72
72
  exports.debug = function cds_debug (id, options) {
73
73
  const L = cds_log (id, options)
74
- if (L._debug) return Object.assign(L.debug, {
75
- time: label => console.time (`[${id}] - ${label}`),
76
- timeEnd: label => console.timeEnd (`[${id}] - ${label}`),
74
+ return Object.assign((..._) => L._debug && L.debug (..._), {
75
+ time: label => L._debug && console.time (`[${id}] - ${label}`),
76
+ timeEnd: label => L._debug && console.timeEnd (`[${id}] - ${label}`),
77
77
  })
78
78
  }
79
79
 
@@ -1,42 +1,36 @@
1
1
  const cds = require('../../..')
2
2
 
3
- const $remove = Symbol('remove')
4
-
5
- const _is_custom_fields = (arg, custom_fields) => {
6
- if (!Object.keys(arg).length) return false
7
- for (const k in arg) if (!(k in custom_fields)) return false
8
- return true
9
- }
10
-
11
- const _is_categories = arg => arg.categories && Array.isArray(arg.categories) && Object.keys(arg).length === 1
12
-
13
- function als_aspect(module, level, args, toLog) {
14
- // REVISIT: kibana_custom_fields for backward compatibility. remove in cds^8.
3
+ let _kibana_custom_fields_deprecation_logged = false
4
+ const _get_als_custom_fields = () => {
15
5
  const { als_custom_fields, kibana_custom_fields } = cds.env.log
16
- this._CUSTOM_FIELDS ??= kibana_custom_fields ? { ...kibana_custom_fields } : { ...als_custom_fields }
17
- this._HAS_CUSTOM_FIELDS ??= Object.keys(this._CUSTOM_FIELDS).length > 0
18
-
19
- // extract custom fields and categories from remaining args (while avoiding as many loops/ iterations as possible)
20
- if (args.length) {
21
- let filter4removed = false
22
- for (let i = 0; i < args.length; i++) {
23
- const arg = args[i]
24
- if (typeof arg !== 'object') continue
25
- if ((this._HAS_CUSTOM_FIELDS && _is_custom_fields(arg, this._CUSTOM_FIELDS)) || _is_categories(arg)) {
26
- Object.assign(toLog, arg)
27
- args[i] = $remove
28
- filter4removed = true
29
- }
6
+ if (kibana_custom_fields) {
7
+ if (!_kibana_custom_fields_deprecation_logged) {
8
+ _kibana_custom_fields_deprecation_logged = true
9
+ cds.utils.deprecated({ old: 'cds.env.log.kibana_custom_fields', use: 'cds.env.log.als_custom_fields' })
30
10
  }
31
- if (filter4removed) args.sort((a, b) => (b === $remove) * -1).splice(args.lastIndexOf($remove))
11
+ return kibana_custom_fields
32
12
  }
13
+ return als_custom_fields
14
+ }
15
+
16
+ function als_aspect(module, level, args, toLog) {
17
+ this._ALS_CUSTOM_FIELDS ??= { ..._get_als_custom_fields() }
18
+ this._ALS_HAS_CUSTOM_FIELDS ??= Object.keys(this._ALS_CUSTOM_FIELDS).length > 0
33
19
 
34
20
  // ALS custom fields
35
- if (this._HAS_CUSTOM_FIELDS) {
21
+ if (this._ALS_HAS_CUSTOM_FIELDS) {
36
22
  const cf = []
37
- for (const k in this._CUSTOM_FIELDS) if (toLog[k]) cf.push({ k, v: toLog[k], i: this._CUSTOM_FIELDS[k] })
23
+ for (const k in this._ALS_CUSTOM_FIELDS) {
24
+ if (toLog[k]) {
25
+ const i = cf.findIndex(e => e.i === this._ALS_CUSTOM_FIELDS[k])
26
+ if (i > -1) cf[i] = { k, v: toLog[k], i: this._ALS_CUSTOM_FIELDS[k] }
27
+ else cf.push({ k, v: toLog[k], i: this._ALS_CUSTOM_FIELDS[k] })
28
+ }
29
+ }
38
30
  if (cf.length) toLog['#cf'] = { string: cf }
39
31
  }
40
32
  }
41
33
 
34
+ als_aspect.cf = () => Object.keys({ ..._get_als_custom_fields() })
35
+
42
36
  module.exports = process.env.VCAP_SERVICES?.match(/"label":\s*"application-logs"/) ? als_aspect : () => {}
@@ -0,0 +1,9 @@
1
+ const cds = require('../../..')
2
+
3
+ function cls_aspect(/* module, level, args, toLog */) {
4
+ // actually nothing to do
5
+ }
6
+
7
+ cls_aspect.cf = () => [...cds.env.log.cls_custom_fields]
8
+
9
+ module.exports = process.env.VCAP_SERVICES?.match(/"label":\s*"cloud-logging"/) ? cls_aspect : () => {}
@@ -5,7 +5,8 @@ const util = require('util')
5
5
  const L2L = { 1: 'error', 2: 'warn', 3: 'info', 4: 'debug', 5: 'trace' }
6
6
  const HEADER_MAPPINGS = {
7
7
  x_vcap_request_id: 'request_id',
8
- content_length: 'request_size_b'
8
+ content_length: 'request_size_b',
9
+ traceparent: 'w3c_traceparent'
9
10
  }
10
11
 
11
12
  const _is4xx = ele =>
@@ -13,6 +14,32 @@ const _is4xx = ele =>
13
14
  (ele.status >= 400 && ele.status < 500) ||
14
15
  (ele.statusCode >= 400 && ele.statusCode < 500)
15
16
 
17
+ const $remove = Symbol('remove')
18
+
19
+ const _is_custom_fields = (arg, custom_fields) => {
20
+ if (!Object.keys(arg).length) return false
21
+ for (const k in arg) if (!custom_fields.has(k)) return false
22
+ return true
23
+ }
24
+
25
+ const _is_categories = arg => arg.categories && Array.isArray(arg.categories) && Object.keys(arg).length === 1
26
+
27
+ const _extract_custom_fields_and_categories = (args, toLog, custom_fields) => {
28
+ if (args.length) {
29
+ let filter4removed = false
30
+ for (let i = 0; i < args.length; i++) {
31
+ const arg = args[i]
32
+ if (typeof arg !== 'object') continue
33
+ if ((custom_fields.size && _is_custom_fields(arg, custom_fields)) || _is_categories(arg)) {
34
+ Object.assign(toLog, arg)
35
+ args[i] = $remove
36
+ filter4removed = true
37
+ }
38
+ }
39
+ if (filter4removed) args.sort((a, b) => (b === $remove) * -1).splice(args.lastIndexOf($remove))
40
+ }
41
+ }
42
+
16
43
  const _getCircularReplacer = () => {
17
44
  const seen = new WeakSet()
18
45
  return (key, value) => {
@@ -51,13 +78,13 @@ module.exports = function format(module, level, ...args) {
51
78
  // log user id, if configured (data privacy)
52
79
  if (user && log_user) toLog.remote_user = user.id
53
80
  // if available, add headers (normalized to lowercase and with _ instead of -) with masking as configured and mappings applied
54
- const req = cds.context._ && cds.context._.req
55
- if (req && req.headers) {
56
- for (const k in req.headers) {
81
+ const headers = cds.context.http?.req?.headers
82
+ if (headers) {
83
+ for (const k in headers) {
57
84
  const h = k.replace(/-/g, '_').toLowerCase()
58
85
  toLog[h] = (() => {
59
86
  if (this._MASK_HEADERS.some(m => k.match(m))) return '***'
60
- return req.headers[k]
87
+ return headers[k]
61
88
  })()
62
89
  if (h in HEADER_MAPPINGS) toLog[HEADER_MAPPINGS[h]] = toLog[h]
63
90
  }
@@ -84,7 +111,16 @@ module.exports = function format(module, level, ...args) {
84
111
  Object.assign(toLog, err, { level: toLog.level })
85
112
  }
86
113
 
87
- // apply aspects
114
+ /*
115
+ * apply aspects:
116
+ * 1. extract custom fields (provided by the aspects) and categories from remaining args
117
+ * 2. actually apply the aspects
118
+ */
119
+ if (!this._custom_fields) {
120
+ this._custom_fields = new Set()
121
+ for (const each of this._ASPECTS) if (each.cf) each.cf().forEach(v => this._custom_fields.add(v))
122
+ }
123
+ _extract_custom_fields_and_categories(args, toLog, this._custom_fields)
88
124
  for (const each of this._ASPECTS) each.call(this, module, level, args, toLog)
89
125
 
90
126
  // append remaining args via util.format()
@@ -90,6 +90,10 @@ const _object_predicate = ([arg], _clause) => { // e.g. .where ({ID:4711, stock:
90
90
  pred.push('and', 'not', 'exists', typeof x === 'object' ? x : { ref: x.split('.') })
91
91
  continue
92
92
  }
93
+ if (k === 'in') {
94
+ pred.push('in', val(x))
95
+ continue
96
+ }
93
97
  else pred.push('and', parse.expr(k))
94
98
  if (!x || x==='*') pred.push('=', {val:x})
95
99
  else if (x.SELECT || x.list) pred.push('in', x)
@@ -98,7 +102,7 @@ const _object_predicate = ([arg], _clause) => { // e.g. .where ({ID:4711, stock:
98
102
  else if (x instanceof Buffer) pred.push('=', {val:x})
99
103
  else if (x instanceof RegExp) pred.push('like', {val:x})
100
104
  else if (x instanceof Date) pred.push('=', {val:x})
101
- else if (typeof x === 'object') for (let op in x) pred.push(op, val(x[op]))
105
+ else if (typeof x === 'object') for (let op in x) x[op]?.in ? pred.push(op, ...predicate4([x[op]],_clause)) : pred.push(op, val(x[op])) // REVIST: Should always be proper recursion
102
106
  else if (_clause === 'on' && typeof x === 'string') pred.push('=', { ref: x.split('.') })
103
107
  else pred.push('=', {val:x})
104
108
  }
@@ -132,6 +132,7 @@ class EventContext {
132
132
  if (h) super.headers = h
133
133
  }
134
134
  get headers() {
135
+ // REVISIT: isn't "this._.req?.headers" deprecated? shouldn't it be "this.http?.req?.headers"?
135
136
  let headers = this._.req?.headers
136
137
  if (!headers) { headers={}
137
138
  const outer = this._propagated.headers
package/lib/req/locale.js CHANGED
@@ -22,7 +22,7 @@ const from_req = req => req && (
22
22
 
23
23
  function req_locale (req) {
24
24
  const locale = from_req(req); if (!locale) return i18n.default_language
25
- const loc = locale.replace(/-/g,'_')
25
+ const loc = locale.replace(/-/g,'_').match(/^[^,; ]*/)[0]
26
26
  return INCLUDE_LIST[loc]
27
27
  || /^([a-z]+)/i.test(loc) && RegExp.$1.toLowerCase()
28
28
  || i18n.default_language