@sap/cds 8.1.1 → 8.2.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 (51) hide show
  1. package/CHANGELOG.md +56 -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/linked/classes.js +0 -14
  13. package/lib/linked/types.js +12 -0
  14. package/lib/linked/validate.js +13 -8
  15. package/lib/log/cds-log.js +3 -3
  16. package/lib/log/format/aspects/als.js +23 -29
  17. package/lib/log/format/aspects/cls.js +9 -0
  18. package/lib/log/format/json.js +42 -6
  19. package/lib/ql/Whereable.js +5 -1
  20. package/lib/srv/cds-connect.js +33 -32
  21. package/lib/srv/cds-serve.js +2 -1
  22. package/lib/srv/middlewares/cds-context.js +2 -1
  23. package/lib/utils/cds-utils.js +4 -2
  24. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +1 -1
  25. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -5
  26. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +2 -31
  27. package/libx/_runtime/common/generic/auth/utils.js +2 -0
  28. package/libx/_runtime/common/generic/input.js +2 -11
  29. package/libx/_runtime/common/generic/put.js +1 -10
  30. package/libx/_runtime/common/utils/binary.js +1 -7
  31. package/libx/_runtime/common/utils/resolveView.js +2 -2
  32. package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
  33. package/libx/_runtime/common/utils/streamProp.js +19 -6
  34. package/libx/_runtime/common/utils/template.js +26 -16
  35. package/libx/_runtime/common/utils/templateProcessor.js +8 -7
  36. package/libx/_runtime/common/utils/ucsn.js +2 -5
  37. package/libx/_runtime/db/expand/expandCQNToJoin.js +10 -0
  38. package/libx/_runtime/db/generic/input.js +1 -5
  39. package/libx/_runtime/fiori/lean-draft.js +272 -90
  40. package/libx/_runtime/messaging/event-broker.js +105 -40
  41. package/libx/_runtime/remote/utils/client.js +12 -4
  42. package/libx/_runtime/ucl/Service.js +16 -6
  43. package/libx/odata/middleware/batch.js +2 -2
  44. package/libx/odata/middleware/read.js +6 -10
  45. package/libx/odata/middleware/stream.js +4 -5
  46. package/libx/odata/parse/afterburner.js +3 -2
  47. package/libx/odata/parse/multipartToJson.js +3 -1
  48. package/libx/odata/utils/index.js +3 -3
  49. package/libx/odata/utils/postProcess.js +3 -25
  50. package/libx/rest/middleware/parse.js +1 -6
  51. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,58 @@
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.1 - 2024-09-04
8
+
9
+ ### Fixed
10
+
11
+ - Date validation of legacy OData protocol adapter
12
+ - Content-Length headers in multipart batch request body
13
+ - Streaming requests with virtual properties
14
+ - Bring back support for `x-correlationid`
15
+ - Validation of inlined elements
16
+ - multipart `$batch` parsing with _--_ as part of payload
17
+
18
+ ## Version 8.2.0 - 2024-08-30
19
+
20
+ ### Added
21
+
22
+ - Allow `cds.connect.to (SomeService)` where `SomeService` is a class
23
+ - Lean draft: support CDS orderBy in `list status: all`
24
+ - Support where not in as object in `cds.ql` expressions like: `where({ID:{not:{in:[...]}}})`
25
+ - Unbound CDS functions now show up in the server's index page along with an exemplary call signature
26
+ - `cds.log`'s JSON formatter:
27
+ + Field `w3c_traceparent` is filled based on request header `traceparent` (cf. W3C Trace Context) for improved correlation
28
+ + Custom fields `cds.env.log.cls_custom_fields` are filled if bound to an instance of SAP Cloud Logging
29
+ + Default `cds.env.log.als_custom_fields` enhanced by `{ reason: 3 }` (project config takes precedence)
30
+ - Support for `cds.hana` types like `cds.hana.ST_POINT` in `cds.builtin`
31
+ - 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')`
32
+ - New config flag `cds.server.shutdown_on_uncaught_errors` allows to control whether the server should shut down on uncaught errors. Default is `true`
33
+
34
+ ### Changed
35
+
36
+ - 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
37
+
38
+ ### Fixed
39
+
40
+ - Resolving views with path expression renamings
41
+ - Set content-type-header in batch for actions with 204 No Content
42
+ - URI encoding of `@odata.nextLink` in OData response
43
+ - Requests reading media data streams did not provide `req.params`
44
+ - `cds.compile.to.hana` for legacy hana service with `@cap-js/sqlite` as dev dependency
45
+ - Better redaction of debug output
46
+ - Instance-based authorization using functions
47
+ - Fixed flaws in `cds.connect.to()` that lead to deadlocks in case of errors due to invalid service configurations or initializations.
48
+ - Navigation with backlink as key can now omit backlink keys for new OData adapter
49
+
50
+ ### Removed
51
+
52
+ - 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:
53
+
54
+ ```js
55
+ [...linked.definitions].map(d => d.name)
56
+ ```
57
+
58
+
7
59
  ## Version 8.1.1 - 2024-08-08
8
60
 
9
61
  ### Fixed
@@ -58,6 +110,9 @@
58
110
  ### Fixed
59
111
 
60
112
  - Empty feature set by switched off feature toggles
113
+ - Allow programmatic operations on draft-enabled entities (`NEW`, `CREATE`, `UPDATE`, `DELETE`)
114
+
115
+ ### Removed
61
116
  - Allow deviating response types for `$batch`, e. g. input `multipart` and output `json`
62
117
 
63
118
  ## Version 8.0.2 - 2024-07-09
@@ -330,6 +385,7 @@
330
385
 
331
386
  ### Changed
332
387
 
388
+ - 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`.
333
389
  - The index page now lists all service endpoints, which is important for services that are exposed through multiple protocols.
334
390
  - `cds.deploy` improves error diagnostics with deeper `Query` object inspection.
335
391
  - 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
  }
@@ -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)
@@ -56,13 +56,15 @@ class Validation {
56
56
  class ValidationErrors extends Array {
57
57
  add (error) {
58
58
  const err = Object.create (ValidationErrors.proto)
59
- err.message = err.stack = error
59
+ err.message = error
60
60
  this.push (err)
61
61
  return err
62
62
  }
63
63
  static proto = Object.create (Error.prototype, {
64
64
  message: { writable:true, configurable:true },
65
- stack: { writable:true, configurable:true, value: '<none>' },
65
+ stack: { configurable:true, get() { return this.message },
66
+ set(v) { Object.defineProperty (this, 'stack', { value:v, writable:true, configurable:true }) },
67
+ },
66
68
  code: { value: '400', writable:true }, // REVISIT: should be 'ASSERT_'... (i.e. msg) but we need to adjust all tests, and have a code catalogue
67
69
  statusCode: { value: 400 }, // REVISIT: should go into mappings in adapter's error handlers -> requires a code catalogue // REVISIT: .statusCode vs .status?
68
70
  numericSeverity: { value: 4, enumerable: true }, // REVISIT: that is OData-specific
@@ -118,12 +120,15 @@ const $any = class any {
118
120
  }
119
121
 
120
122
  _is_mandatory (d=this) {
121
- return d.own('_mandatory', ()=> {
122
- if (d['@readonly']) return false // readonly -> not mandatory
123
- if (d['@mandatory']) return true
124
- if (d['@Common.FieldControl']?.['#'] === 'Mandatory') return true
125
- else return false
126
- })
123
+ return d.own('_mandatory', ()=> (
124
+ !d['@readonly'] // readonly -> not mandatory
125
+ && (d['@mandatory'] || d['@Common.FieldControl']?.['#'] === 'Mandatory')
126
+ && !d._is_flattened()
127
+ ))
128
+ }
129
+
130
+ _is_flattened (d=this) {
131
+ return d.parent?.query?.SELECT.columns?.some (c => c.ref?.length > 1 && d.name === (c.as || c.ref.at(-1)))
127
132
  }
128
133
 
129
134
  _is_readonly (d=this) {
@@ -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
  }