@sap/cds 7.4.2 → 7.5.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 (115) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/apis/cds.d.ts +1 -38
  3. package/apis/core.d.ts +21 -101
  4. package/apis/cqn.d.ts +18 -76
  5. package/apis/csn.d.ts +18 -114
  6. package/apis/events.d.ts +16 -123
  7. package/apis/internal/inference.d.ts +18 -32
  8. package/apis/linked.d.ts +18 -97
  9. package/apis/log.d.ts +19 -164
  10. package/apis/models.d.ts +18 -180
  11. package/apis/ql.d.ts +16 -323
  12. package/apis/reflect.d.ts +32 -0
  13. package/apis/server.d.ts +18 -135
  14. package/apis/services.d.ts +18 -380
  15. package/bin/cds-serve.js +5 -2
  16. package/bin/serve.js +7 -16
  17. package/lib/auth/basic-auth.js +3 -1
  18. package/lib/auth/ias-auth.js +62 -48
  19. package/lib/auth/ias-claims.js +34 -0
  20. package/lib/auth/index.js +55 -33
  21. package/lib/auth/jwt-auth.js +55 -52
  22. package/lib/compile/cdsc.js +2 -2
  23. package/lib/compile/to/edm.js +4 -4
  24. package/lib/compile/to/hdbtabledata.js +5 -8
  25. package/lib/compile/to/srvinfo.js +2 -2
  26. package/lib/env/cds-env.js +3 -9
  27. package/lib/env/cds-requires.js +16 -17
  28. package/lib/env/compat.js +0 -9
  29. package/lib/env/defaults.js +17 -6
  30. package/lib/i18n/localize.js +46 -42
  31. package/lib/index.js +6 -8
  32. package/lib/linked/classes.js +7 -118
  33. package/lib/linked/entities.js +1 -1
  34. package/lib/log/cds-log.js +15 -10
  35. package/lib/log/format/aspects/als.js +41 -0
  36. package/lib/log/format/aspects/cf.js +36 -0
  37. package/lib/log/format/json.js +96 -0
  38. package/lib/plugins.js +7 -3
  39. package/lib/req/context.js +4 -2
  40. package/lib/srv/cds-connect.js +3 -5
  41. package/lib/srv/cds-serve.js +13 -26
  42. package/lib/srv/factory.js +3 -3
  43. package/lib/srv/middlewares/index.js +0 -2
  44. package/lib/srv/middlewares/trace.js +2 -3
  45. package/lib/srv/protocols/_legacy.js +27 -30
  46. package/lib/srv/protocols/index.js +173 -58
  47. package/lib/srv/protocols/odata-v4.js +29 -16
  48. package/lib/srv/srv-api.js +8 -13
  49. package/lib/srv/srv-handlers.js +14 -14
  50. package/lib/utils/cds-utils.js +15 -0
  51. package/libx/_runtime/auth/index.js +4 -5
  52. package/libx/_runtime/auth/strategies/basic.js +2 -2
  53. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +23 -13
  54. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +6 -15
  55. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +10 -3
  56. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +5 -2
  57. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +2 -1
  58. package/libx/_runtime/cds-services/services/utils/columns.js +3 -9
  59. package/libx/_runtime/cds.js +13 -0
  60. package/libx/_runtime/common/composition/data.js +3 -0
  61. package/libx/_runtime/common/composition/delete.js +1 -1
  62. package/libx/_runtime/common/error/frontend.js +2 -2
  63. package/libx/_runtime/common/generic/auth/readOnly.js +1 -1
  64. package/libx/_runtime/common/generic/auth/restrictions.js +1 -1
  65. package/libx/_runtime/common/generic/sorting.js +4 -5
  66. package/libx/_runtime/common/utils/csn.js +23 -18
  67. package/libx/_runtime/common/utils/restrictions.js +6 -15
  68. package/libx/_runtime/db/generic/input.js +3 -2
  69. package/libx/_runtime/fiori/generic/readOverDraft.js +2 -5
  70. package/libx/_runtime/fiori/lean-draft.js +69 -5
  71. package/libx/_runtime/hana/Service.js +1 -1
  72. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  73. package/libx/_runtime/messaging/Outbox.js +3 -8
  74. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -0
  75. package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
  76. package/libx/_runtime/messaging/file-based.js +1 -1
  77. package/libx/_runtime/messaging/service.js +7 -10
  78. package/libx/_runtime/remote/Service.js +15 -45
  79. package/libx/_runtime/remote/utils/client.js +20 -33
  80. package/libx/_runtime/remote/utils/cloudSdkProvider.js +30 -0
  81. package/libx/_runtime/sqlite/Service.js +2 -2
  82. package/libx/odata/afterburner.js +29 -21
  83. package/libx/odata/cqn2odata.js +1 -1
  84. package/libx/odata/error.js +7 -0
  85. package/libx/odata/grammar.peggy +16 -20
  86. package/libx/odata/metadata.js +73 -78
  87. package/libx/odata/parser.js +1 -1
  88. package/libx/odata/read.js +94 -0
  89. package/libx/odata/result.js +91 -0
  90. package/libx/odata/service-document.js +31 -37
  91. package/libx/odata/utils.js +2 -1
  92. package/libx/outbox/index.js +9 -4
  93. package/libx/rest/RestAdapter.js +68 -67
  94. package/libx/rest/middleware/create.js +20 -26
  95. package/libx/rest/middleware/delete.js +5 -3
  96. package/libx/rest/middleware/error.js +2 -3
  97. package/libx/rest/middleware/input.js +5 -5
  98. package/libx/rest/middleware/operation.js +96 -41
  99. package/libx/rest/middleware/parse.js +4 -6
  100. package/libx/rest/middleware/payload.js +5 -5
  101. package/libx/rest/middleware/read.js +11 -17
  102. package/libx/rest/middleware/update.js +20 -25
  103. package/package.json +2 -1
  104. package/server.js +7 -4
  105. package/srv/outbox.cds +9 -10
  106. package/apis/env.d.ts +0 -25
  107. package/apis/test.d.ts +0 -81
  108. package/apis/utils.d.ts +0 -15
  109. package/lib/auth/passport-basic.js +0 -14
  110. package/lib/auth/passport-digest.js +0 -16
  111. package/lib/env/presets.js +0 -35
  112. package/lib/log/format/cf.js +0 -16
  113. package/lib/log/format/kibana.js +0 -92
  114. package/lib/srv/middlewares/ctx-auth.js +0 -11
  115. package/libx/_runtime/cds-services/adapter/rest/utils/validation-checks.js +0 -119
@@ -6,7 +6,7 @@ const DEBUG = cds.debug('i18n')
6
6
  const _node_modules = cds.env.cdsc.moduleLookupDirectories.map(d => sep+d.slice(0, -1))
7
7
 
8
8
  module.exports = Object.assign (localize, {
9
- localize, lookup, bundles4, folders4, folder4, bundle4
9
+ localize, lookup, bundles4, folders4, folder4, bundle4, files4, allLocales4
10
10
  })
11
11
 
12
12
  /**
@@ -61,11 +61,12 @@ function localize (aString, locale, model = cds.context?.model || cds.model, ext
61
61
  function bundles4 (model, locales = cds.env.i18n.languages) {
62
62
 
63
63
  const folders = folders4 (model); if (folders.length === 0) return
64
+ const {i18n} = cds.env
64
65
 
65
66
  if (locales.split) locales = locales.split(',')
66
67
  if (locales[0] === '*' || locales[0] === 'all') {
67
68
  locales = allLocales4 (folders); if (!locales) return {}
68
- if (!locales.includes(cds.env.i18n.fallback_bundle)) locales.push (cds.env.i18n.fallback_bundle)
69
+ if (!locales.includes(i18n.fallback_bundle)) locales.push (i18n.fallback_bundle)
69
70
  }
70
71
 
71
72
  DEBUG?.('Languages:', locales)
@@ -79,23 +80,29 @@ function bundles4 (model, locales = cds.env.i18n.languages) {
79
80
  })()
80
81
  }
81
82
 
82
- /**
83
- * Return locales for all bundles found in given folders derived from .json, .properties or .csv files.
84
- *
85
- * TODO - .csv file handling seems to be questionable - do we need to check all .csv files additionally for locales ???
86
- */
87
- function allLocales4 (folders) {
88
- // find all languages in all folders
83
+
84
+ function files4 (folders_or_model) {
85
+ const {i18n} = cds.env
86
+ const folders = folders_or_model.map ? folders_or_model : folders4(folders_or_model)
89
87
  const files = folders.map (folder => readdirSync(folder)
90
- .filter (e => e.startsWith(cds.env.i18n.file))
88
+ .filter (e => e.startsWith(i18n.file))
91
89
  .map(i18nFile => join (folder, i18nFile))
92
- ).reduce ((files, file) => files.concat(file)) // flatten
93
-
90
+ ).flat()
94
91
  if (files.length === 0) {
95
92
  DEBUG?.('No languages for folders:', folders)
96
93
  return null
97
94
  }
95
+ return files
96
+ }
98
97
 
98
+ /**
99
+ * Return locales for all bundles found in given folders derived from .json, .properties or .csv files.
100
+ *
101
+ * TODO - .csv file handling seems to be questionable - do we need to check all .csv files additionally for locales ???
102
+ */
103
+ function allLocales4 (folders_or_model) {
104
+ const files = files4(folders_or_model)
105
+ if (!files) return null
99
106
  if (files[0].endsWith('.csv')) {
100
107
  return cds.load.csv (files[0])[0].slice(1)
101
108
  } else {
@@ -129,35 +136,29 @@ function bundle4 (model, locale) {
129
136
  const bundles = model.texts || Object.defineProperty (model,'texts',{value:{}}).texts
130
137
  if (locale in bundles) return bundles[locale]
131
138
 
132
- const bundle = {}
133
139
  const folders = folders4(model)
134
- try {
135
- if (!folders.length) return bundle
136
-
137
- add (cds.env.i18n.fallback_bundle) // e.g. i18n.properties
138
- if (locale === cds.env.i18n.fallback_bundle) return bundle
139
-
140
- add (cds.env.i18n.default_language) // e.g. i18n_en.properties
141
- if (locale === cds.env.i18n.default_language) return bundle
142
-
143
- add (locale) // e.g. i18n_de.properties
144
- return bundle
145
-
146
- } finally { bundles[locale] = bundle }
147
-
148
- function add (lang) {
149
- for (let each of folders) {
150
- const suffix = lang === '' ? '' : '_' + lang
151
- const file = join (each, cds.env.i18n.file), key = file+suffix
152
- const next = bundle4[key] || (bundle4[key] = (
153
- loadFromJSON (file, lang) ||
140
+ if (!folders.length) return bundles[locale] = {}
141
+
142
+ const {i18n} = cds.env
143
+ let bundle = null, locales = (
144
+ locale === i18n.fallback_bundle ? [ i18n.fallback_bundle ] :
145
+ locale === i18n.default_language ? [ i18n.fallback_bundle, i18n.default_language ] :
146
+ [ i18n.fallback_bundle, i18n.default_language, locale ]
147
+ )
148
+ for (let each of locales) {
149
+ const b = bundle = Object.create(bundle)
150
+ for (let folder of folders) {
151
+ const file = join (folder, i18n.file), suffix = each ? '_' + each : ''
152
+ const next = bundle4[file + suffix] ??= (
153
+ loadFromJSON (file, each) ||
154
154
  cds.load.properties (file + suffix.replace('-','_')) || // e.g. en-UK --> en_UK
155
155
  cds.load.properties (file + suffix.match(/\w+/)) || // e.g. en_UK --> en
156
- loadFromCSV (file, lang)
157
- ))
158
- Object.assign (bundle, next)
156
+ loadFromCSV (file, each)
157
+ )
158
+ Object.assign (b, next)
159
159
  }
160
160
  }
161
+ return bundles[locale] = bundle
161
162
  }
162
163
 
163
164
  /**
@@ -190,8 +191,9 @@ function folder4 (loc) {
190
191
  // already cached from a former lookup?
191
192
  if (loc in folder4) return folder4[loc]
192
193
  // check whether a <loc>/_i18n exists
193
- for (let i18n of cds.env.i18n.folders) {
194
- const f = join (loc, i18n)
194
+ const {i18n} = cds.env
195
+ for (let each of i18n.folders) {
196
+ const f = join (loc, each)
195
197
  if (existsSync(f)) return folder4[loc] = f
196
198
  }
197
199
  //> no --> search up the folder hierarchy up to cds.root, cds.home, or some .../node_modules/<package>
@@ -202,7 +204,7 @@ function folder4 (loc) {
202
204
  if (!(
203
205
  next.startsWith(cds.root) ||
204
206
  next.startsWith(cds.home) ||
205
- cds.env.i18n.root && next.startsWith(cds.env.i18n.root)
207
+ i18n.root && next.startsWith(i18n.root)
206
208
  )) return folder4[loc] = null
207
209
  }
208
210
  if (!next || next === loc) return folder4[loc] = null
@@ -212,12 +214,14 @@ function folder4 (loc) {
212
214
 
213
215
 
214
216
  function loadFromJSON (res, lang=cds.env.i18n.default_language) {
215
- try {
216
- const bundles = require (resolve (cds.root,res+'.json'))
217
- return bundles[lang] || bundles [(lang.match(/\w+/)||[])[0]]
217
+ let cached = loadFromJSON[res]
218
+ if (!cached) try {
219
+ cached = loadFromJSON[res] = require (resolve (cds.root,res+'.json'))
218
220
  } catch(e) {
219
221
  if (e.code !== 'MODULE_NOT_FOUND') throw e
222
+ else cached = loadFromJSON[res] = {}
220
223
  }
224
+ return cached[lang] || cached[lang.match(/\w+/)?.[0]]
221
225
  }
222
226
 
223
227
  function loadFromCSV (res, lang=cds.env.i18n.default_language) {
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  if (process.env.CDS_STRICT_NODE_VERSION !== 'false') require('./utils/check-version')
2
2
 
3
- const { extend, lazify, lazified } = require('./lazy')
4
3
  const { EventEmitter } = require('node:events')
4
+ const { extend, lazify } = require('./lazy')
5
5
 
6
6
  const cds = module.exports = new class cds extends EventEmitter {
7
7
 
@@ -41,13 +41,10 @@ const cds = module.exports = new class cds extends EventEmitter {
41
41
  get type() { return super.type = this.builtin.classes.type }
42
42
  get array() { return super.array = this.builtin.classes.array }
43
43
  get struct() { return super.struct = this.builtin.classes.struct }
44
- get service() { return super.service = extend (this.builtin.classes.service) .with (lazified({
45
- impl: (/** @type {(this:Service, srv:Service, ...etc) => any} */ srv) => srv,
46
- protocols: lazy => require('./srv/protocols'),
47
- bindings: lazy => require('./srv/bindings'),
48
- factory: lazy => require('./srv/factory'),
44
+ get service() { return super.service = extend (this.builtin.classes.service) .with ({
45
+ /** @param {( this:Service, srv:Service )} fn */ impl: fn => fn,
49
46
  /** @type Service[] */ providers: []
50
- }))}
47
+ })}
51
48
 
52
49
  // Providing and Consuming Services
53
50
  /** @type {{ [name:string]: Service }} */
@@ -57,7 +54,8 @@ const cds = module.exports = new class cds extends EventEmitter {
57
54
  get server() { return super.server = require('../server') }
58
55
  get serve() { return super.serve = require('./srv/cds-serve') }
59
56
  get connect() { return super.connect = require('./srv/cds-connect') }
60
- get outboxed() { return super.outboxed = require('../libx/outbox') }
57
+ get outboxed() { return super.outboxed = require('../libx/outbox').outboxed }
58
+ get unboxed() { return super.unboxed = require('../libx/outbox').unboxed }
61
59
  get middlewares() { return super.middlewares = require('./srv/middlewares') }
62
60
  get odata() { return super.odata = require('../libx/odata') }
63
61
  get auth() { return super.auth = require('./auth') }
@@ -30,124 +30,13 @@ class action extends any {}
30
30
  class context extends any {}
31
31
 
32
32
  class service extends context {
33
-
34
- static _protocols = []
35
- get endpoints() { return super.endpoints = service.endpoints4(this) }
36
- get path() { return super.path = service.path4(this) }
37
-
38
- /**
39
- * Return the first service endpoint path to mount it to
40
- */
41
- static path4 (srv, options = {}) {
42
- return this.endpoints4(srv, options)[0]?.path
43
- }
44
-
45
- /**
46
- * Return an array of protocol names to be served by the given service.
47
- * Ensure that 'odata' shortcuts are expanded to 'odata-v4'.
48
- */
49
- static protocols4 (def, options = {}) {
50
- const ov4 = p => p === 'odata' ? 'odata-v4' : p
51
-
52
- if (options.to) return [ov4(options.to)]
53
-
54
- const protocols = this._protocols
55
- if (def) {
56
- // Check @protocol annotation
57
- let atProtocol = def['@protocol']
58
- if (atProtocol === 'none') return []
59
- if (atProtocol) {
60
- const result = (Array.isArray(atProtocol) ? atProtocol : [atProtocol]).map(p => ov4(typeof p === 'string' ? p : p.kind))
61
- return [...new Set(result)]
62
- }
63
-
64
- // Check @odata, @rest, ... shortcuts
65
- const shortcut = Object.keys(protocols).find(p => def['@'+p])
66
- if (shortcut) return [ov4(shortcut)]
67
- }
68
-
69
- // No protocol annotation found -> serve odata
70
- return ['odata-v4']
71
- }
72
-
73
- static endpoints4 (srv, options = {}) {
74
- const def = srv?.definition || srv
75
- if (!def) return []
76
-
77
- // Return a sluggified variant of the service's name
78
- const pathFromServiceName4 = (srv) => {
79
- return (
80
- /[^.]+$/.exec(srv.name)[0] //> my.very.CatalogService --> CatalogService
81
- .replace(/Service$/,'') //> CatalogService --> Catalog
82
- .replace(/_/g,'-') //> foo_bar_baz --> foo-bar-baz
83
- .replace(/([a-z0-9])([A-Z])/g, (_,c,C) => c+'-'+C) //> ODataFooBarX9 --> OData-Foo-Bar-X9
84
- .toLowerCase() //> FOO --> foo
85
- )
86
- }
87
-
88
- // Return an array of service paths for a given service and protocol.
89
- // The results are not prefixed with the protocol's path.
90
- // Can return multiple results to support serving the same protocol on multiple paths:
91
- // `@protocol: [{kind: 'odata', path: '/a'}, {kind: 'odata', path: '/b'}]`
92
- const unprefixedPaths4 = (protocol, srv, options) => {
93
- const def = srv?.definition || srv
94
- if (options.at) return [options.at]
95
- if (options.path) return [options.path]
96
-
97
- const atProtocol = def['@protocol']
98
- if (atProtocol) {
99
- let paths = []
100
- let protocols = Array.isArray(atProtocol) ? atProtocol : [atProtocol]
101
- for (let p of protocols) {
102
- let pName = (p.kind || p) === 'odata' ? 'odata-v4' : p.kind || p
103
- if (pName === protocol) {
104
- if (p.path) paths.push(p.path)
105
- else paths.push(def['@path'] || pathFromServiceName4(srv))
106
- }
107
- }
108
-
109
- // in case protocol is set via options.at but not contained in @protocol
110
- if (paths.length === 0) paths.push(def['@path'] || pathFromServiceName4(srv))
111
-
112
- return paths
113
- }
114
-
115
- if (def['@path']) return [def['@path']]
116
- return [pathFromServiceName4(srv)]
117
- }
118
-
119
- const protocols = this.protocols4(def, options)
120
-
121
- if (protocols.length > 1 && def['@path']?.[0] === '/') {
122
- throw new Error(`Absolute @path (starting with '/') cannot be set for more than one protocol`)
123
- }
124
-
125
- const endpoints = [], env_protocols = global.cds?.env.protocols
126
- const serve_on_root = global.cds?.env.features.serve_on_root
127
-
128
- for (let each of protocols) {
129
- let p = this._protocols[each] || env_protocols[each]
130
- if (!p) throw new Error(`Protocol "${each}" is unknown`)
131
-
132
- const middlewares = global.cds?.requires.middlewares
133
- const kind = each
134
- let prefix = middlewares ? p.path || '/' : '/' // > no middlewares, no prefix
135
- if (!prefix.endsWith('/')) prefix += '/'
136
- const unprefixedPaths = unprefixedPaths4(each, srv, options)
137
- for (let path of unprefixedPaths) {
138
- if (path.startsWith('/')) endpoints.push({ kind, path})
139
- else {
140
- // Extra serve in root if serve_on_root=true, but not(!) in case of ...
141
- // - multiple protocols
142
- // - empty prefix, which is the case for legacy protocol adapter
143
- // - middlewares=false (check only needed for handling of custom protocols)
144
- if (middlewares && serve_on_root && protocols.length === 1 && prefix !== '/') endpoints.push({ kind, path: '/' + path })
145
- endpoints.push({ kind, path: prefix + path })
146
- }
147
- }
148
- }
149
- return endpoints
150
- }
33
+ /** @type <T> (p,v:T) => T */ static _lazy (p,v) { Reflect.defineProperty (this,p,{value:v}); return v }
34
+ static get protocols() { return this._lazy ('protocols', require('../srv/protocols')) }
35
+ static get bindings() { return this._lazy ('bindings', require('../srv/bindings')) }
36
+ static get factory() { return this._lazy ('factory', require('../srv/factory')) }
37
+ static endpoints4(..._) { return this.protocols.endpoints4(..._) }
38
+ static path4(..._) { return this.protocols.path4(..._) }
39
+ get _serves_odata() { return super._serves_odata = service.protocols._serves_odata(this) }
151
40
  }
152
41
 
153
42
  class array extends type { is(kind) { return kind === 'array' || super.is(kind) }}
@@ -56,7 +56,7 @@ class Association extends struct {
56
56
 
57
57
  set keys(k) { super.keys = k }
58
58
  get keys() {
59
- if (this.on || this.is2many || !this._target) return this._keys = undefined
59
+ if (this.on || this.is2many || !this._target) return this.set('_keys', undefined)
60
60
  const keys=[], tks = this._target.keys
61
61
  for (let k in tks) keys.push({ ref: [tks[k].name] })
62
62
  return this.keys = keys
@@ -1,12 +1,14 @@
1
- const cds = require ('../index'), conf = cds.env.log
1
+ const cds = require('../index'), conf = cds.env.log
2
2
  const log = module.exports = exports = cds_log
3
3
  const path = require('path')
4
4
 
5
+
5
6
  /**
6
7
  * Cache used for all constructed loggers.
7
8
  */
8
9
  exports.loggers = {}
9
10
 
11
+
10
12
  /**
11
13
  * Returns a trace logger for the given module if trace is switched on for it,
12
14
  * otherwise returns null. All cds runtime packages use this method for their
@@ -130,20 +132,25 @@ exports.winstonLogger = (options) => (label, level) => {
130
132
  })
131
133
  }
132
134
 
135
+
133
136
  /**
134
137
  * Built-in formatters
135
138
  */
136
- const { simple } = exports.formatters = {
137
- simple: (label, level, ...args) => [ `[${label}] -`, ...args ],
138
- mt: (label, level, ...args) => {
139
+ const { _simple, _mt } = exports.formatters = {
140
+ _simple: (label, level, ...args) => [ `[${label}] -`, ...args ],
141
+ _mt: (label, level, ...args) => {
139
142
  const t = cds.context?.tenant; if (t) label += '|'+t
140
- return simple (label, level, ...args)
143
+ return _simple (label, level, ...args)
144
+ },
145
+ get plain() {
146
+ return this._plain || (this._plain = cds.requires.multitenancy ? _mt : _simple)
141
147
  },
142
148
  get json() {
143
- return this._json || (this._json = require('./format/cf'))
149
+ return this._json || (this._json = require('./format/json'))
144
150
  }
145
151
  }
146
152
 
153
+
147
154
  /**
148
155
  * Formats log outputs by returning an array of arguments which are passed to
149
156
  * console.log() et al.
@@ -155,10 +162,7 @@ exports.winstonLogger = (options) => (label, level) => {
155
162
  * @param {number} level the log level to enable -> 0=off, 1=error, 2=warn, 3=info, 4=debug, 5=trace
156
163
  * @param {any[]} args the arguments passed to Logger.debug|log|info|warn|error()
157
164
  */
158
- exports.format = (
159
- process.env.NODE_ENV === 'production' && cds.env.features.kibana_formatter ? log.formatters.json :
160
- cds.requires.multitenancy ? log.formatters.mt : simple
161
- )
165
+ exports.format = log.formatters[cds.env.log.format || 'plain']
162
166
 
163
167
 
164
168
  const DEBUG_matches = (m) => process.env.DEBUG?.match(RegExp(`\\b(y|all|${m||'any'})\\b`))
@@ -166,6 +170,7 @@ const { ERROR, WARN, INFO, DEBUG, TRACE } = exports.levels = {
166
170
  SILENT:0, ERROR:1, WARN:2, INFO:3, DEBUG:4, TRACE:5, SILLY:5, VERBOSE:5
167
171
  }
168
172
 
173
+
169
174
  ;(function _init() {
170
175
  const conf = cds.env.log
171
176
  if (conf.Logger) {
@@ -0,0 +1,41 @@
1
+ const cds = require('../../..')
2
+
3
+ const $remove = Symbol('remove')
4
+
5
+ const _is_custom_fields = (arg, custom_fields) => {
6
+ for (const k in arg) if (!(k in custom_fields)) return false
7
+ return true
8
+ }
9
+
10
+ const _is_categories = arg => arg.categories && Array.isArray(arg.categories) && Object.keys(arg).length === 1
11
+
12
+ function als_aspect(module, level, args, toLog) {
13
+ // REVISIT: kibana_custom_fields for backward compatibility. remove in cds^8.
14
+ const { als_custom_fields, kibana_custom_fields } = cds.env.log
15
+ this._CUSTOM_FIELDS ??= kibana_custom_fields ? { ...kibana_custom_fields } : { ...als_custom_fields }
16
+ this._HAS_CUSTOM_FIELDS ??= Object.keys(this._CUSTOM_FIELDS).length > 0
17
+
18
+ // extract custom fields and categories from remaining args (while avoiding as many loops/ iterations as possible)
19
+ if (args.length) {
20
+ let filter4removed = false
21
+ for (let i = 0; i < args.length; i++) {
22
+ const arg = args[i]
23
+ if (typeof arg !== 'object') continue
24
+ if ((this._HAS_CUSTOM_FIELDS && _is_custom_fields(arg, this._CUSTOM_FIELDS)) || _is_categories(arg)) {
25
+ Object.assign(toLog, arg)
26
+ args[i] = $remove
27
+ filter4removed = true
28
+ }
29
+ }
30
+ if (filter4removed) args.sort((a, b) => (b === $remove) * -1).splice(args.lastIndexOf($remove))
31
+ }
32
+
33
+ // ALS custom fields
34
+ if (this._HAS_CUSTOM_FIELDS) {
35
+ const cf = []
36
+ for (const k in this._CUSTOM_FIELDS) if (toLog[k]) cf.push({ k, v: toLog[k], i: this._CUSTOM_FIELDS[k] })
37
+ if (cf.length) toLog['#cf'] = { string: cf }
38
+ }
39
+ }
40
+
41
+ module.exports = process.env.VCAP_SERVICES?.match(/"label":\s*"application-logs"/) ? als_aspect : () => {}
@@ -0,0 +1,36 @@
1
+ const cds = require('../../..')
2
+
3
+ const _get_cf_fields = () => {
4
+ const cf_fields = {
5
+ layer: 'cds',
6
+ component_type: 'application',
7
+ container_id: process.env.CF_INSTANCE_IP
8
+ }
9
+ const VCAP_APPLICATION = process.env.VCAP_APPLICATION && JSON.parse(process.env.VCAP_APPLICATION)
10
+ if (VCAP_APPLICATION) {
11
+ Object.assign(cf_fields, {
12
+ component_id: VCAP_APPLICATION.application_id,
13
+ component_name: VCAP_APPLICATION.application_name,
14
+ component_instance: VCAP_APPLICATION.instance_index,
15
+ source_instance: VCAP_APPLICATION.instance_index,
16
+ organization_name: VCAP_APPLICATION.organization_name,
17
+ organization_id: VCAP_APPLICATION.organization_id,
18
+ space_name: VCAP_APPLICATION.space_name,
19
+ space_id: VCAP_APPLICATION.space_id
20
+ })
21
+ }
22
+ return cf_fields
23
+ }
24
+
25
+ function cf_aspect(module, level, args, toLog) {
26
+ this._CF_FIELDS ??= _get_cf_fields()
27
+
28
+ // add static fields from environment
29
+ Object.assign(toLog, this._CF_FIELDS)
30
+
31
+ // add subdomain, if available (use cds.context._ instead of cds.context.http because of messaging)
32
+ const tenant_subdomain = cds.context?._?.req?.authInfo?.getSubdomain?.()
33
+ if (tenant_subdomain) toLog.tenant_subdomain = tenant_subdomain
34
+ }
35
+
36
+ module.exports = process.env.CF_INSTANCE_GUID ? cf_aspect : () => {}
@@ -0,0 +1,96 @@
1
+ const cds = require('../../')
2
+
3
+ const util = require('util')
4
+
5
+ const L2L = { 1: 'error', 2: 'warn', 3: 'info', 4: 'debug', 5: 'trace' }
6
+ const HEADER_MAPPINGS = {
7
+ x_vcap_request_id: 'request_id',
8
+ content_length: 'request_size_b'
9
+ }
10
+
11
+ const _is4xx = ele =>
12
+ (ele.code >= 400 && ele.code < 500) ||
13
+ (ele.status >= 400 && ele.status < 500) ||
14
+ (ele.statusCode >= 400 && ele.statusCode < 500)
15
+
16
+ const _getCircularReplacer = () => {
17
+ const seen = new WeakSet()
18
+ return (key, value) => {
19
+ if (typeof value === 'object' && value !== null) {
20
+ if (seen.has(value)) return 'cyclic'
21
+ seen.add(value)
22
+ }
23
+ return value
24
+ }
25
+ }
26
+
27
+ /*
28
+ * JSON-based log formatter for use in production
29
+ */
30
+ module.exports = function format(module, level, ...args) {
31
+ // config
32
+ const { user: log_user, mask_headers, aspects } = cds.env.log
33
+ this._MASK_HEADERS ??= (mask_headers || []).map(s => {
34
+ const parts = s.match(/\/(.+)\/(\w*)/)
35
+ if (parts) return new RegExp(parts[1], parts[2])
36
+ return new RegExp(s)
37
+ })
38
+ this._ASPECTS ??= (aspects || []).map(require)
39
+
40
+ // the object to log
41
+ const toLog = {
42
+ level: L2L[level] || 'info',
43
+ logger: module
44
+ }
45
+
46
+ // add correlation
47
+ if (cds.context) {
48
+ const { id, tenant, user } = cds.context
49
+ toLog.correlation_id = id
50
+ if (tenant) toLog.tenant_id = tenant
51
+ // log user id, if configured (data privacy)
52
+ if (user && log_user) toLog.remote_user = user.id
53
+ // 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) {
57
+ const h = k.replace(/-/g, '_').toLowerCase()
58
+ toLog[h] = (() => {
59
+ if (this._MASK_HEADERS.some(m => k.match(m))) return '***'
60
+ return req.headers[k]
61
+ })()
62
+ if (h in HEADER_MAPPINGS) toLog[HEADER_MAPPINGS[h]] = toLog[h]
63
+ }
64
+ }
65
+ }
66
+ toLog.timestamp = new Date()
67
+
68
+ // merge toLog with passed Error (or error-like object)
69
+ if (args.length && typeof args[0] === 'object' && args[0].message) {
70
+ const err = args.shift()
71
+ toLog.msg = err.message
72
+ if (typeof err.stack === 'string' && !_is4xx(err)) toLog.stacktrace = err.stack.split(/\s*\r?\n\s*/)
73
+ if (Array.isArray(err.details))
74
+ for (const d of err.details)
75
+ if (typeof d.stack === 'string' && !_is4xx(d)) d.stacktrace = d.stack.split(/\s*\r?\n\s*/)
76
+ Object.assign(toLog, err, { level: toLog.level })
77
+ }
78
+
79
+ // apply aspects
80
+ for (const each of this._ASPECTS) each.call(this, module, level, args, toLog)
81
+
82
+ // append remaining args via util.format()
83
+ if (args.length) toLog.msg = toLog.msg ? util.format(toLog.msg, ...args) : util.format(...args)
84
+
85
+ // REVISIT: should not be necessary with new protocol adapters
86
+ // 4xx: lower to warning (if error)
87
+ if (toLog.level && toLog.level.match(/error/i) && _is4xx(toLog)) toLog.level = 'warn'
88
+
89
+ // return array with the stringified toLog (to avoid multiple log lines) as the sole element
90
+ try {
91
+ return [JSON.stringify(toLog)]
92
+ } catch (e) {
93
+ // try again with removed circular references
94
+ return [JSON.stringify(toLog, _getCircularReplacer())]
95
+ }
96
+ }
package/lib/plugins.js CHANGED
@@ -1,5 +1,8 @@
1
+
1
2
  const cds = require('.')
2
3
 
4
+ exports.require = require
5
+
3
6
  /**
4
7
  * Fetch cds-plugins from project's package dependencies.
5
8
  * Used in and made available through cds.env.plugins.
@@ -9,11 +12,12 @@ exports.fetch = function (DEV = process.env.NODE_ENV !== 'production') {
9
12
  fetch_plugins_in (cds.home, false)
10
13
  fetch_plugins_in (cds.root, DEV)
11
14
  function fetch_plugins_in (root, dev) {
12
- let pkg; try { pkg = require(root + '/package.json') } catch { return }
15
+ let pkg; try { pkg = exports.require(root + '/package.json') } catch { return }
13
16
  let deps = { ...pkg.dependencies, ...dev && pkg.devDependencies }
14
17
  for (let each in deps) try {
15
- let impl = require.resolve(each + '/cds-plugin', { paths: [root] })
16
- plugins[each] = { impl }
18
+ let impl = exports.require.resolve(each + '/cds-plugin', { paths: [root] })
19
+ const packageJson = exports.require.resolve(each + '/package.json', { paths: [root] })
20
+ plugins[each] = { impl, packageJson }
17
21
  } catch { /* no cds-plugin.js */ }
18
22
  }
19
23
  return plugins
@@ -16,8 +16,10 @@ class EventContext {
16
16
  const ctx = new this (_)
17
17
  const base = cds.context
18
18
  if (base) {
19
- ctx._set('_propagated', base) // we inherit from former cds.currents
20
- if (!_as_root) {
19
+ // we inherit from former cds.currents (except for the timestamp if new root ctx)
20
+ if (_as_root) ctx._set('_propagated', Object.create(base, { timestamp: { value: undefined } }))
21
+ else {
22
+ ctx._set('_propagated', base)
21
23
  if (!ctx.context) ctx._set('context', base.context) // all transaction handling works with root contexts
22
24
  if (!ctx.tx && base.tx) ctx.tx = base.tx
23
25
  }
@@ -43,11 +43,9 @@ connect.to = async (datasource, options) => {
43
43
  throw new Error (`No service definition found for '${required.service || datasource}'`)
44
44
  }
45
45
  // construct new service instance
46
- let srv = await new Service (datasource,m,o)
47
- await srv.prepend (srv.init, srv.options.impl)
46
+ let srv = await new Service (datasource,m,o); await srv._init()
47
+ if (o.outbox) srv = cds.outboxed(srv)
48
48
  if (datasource === 'db') cds.db = srv
49
- // outbox the service, if configured
50
- if (srv.options.outbox) srv = cds.outboxed(srv)
51
49
  _done (cds.services[datasource] = srv)
52
50
  if (!o.silent) cds.emit ('connect',srv)
53
51
  TRACE?.timeEnd(`cds.connect ${datasource} `)
@@ -55,7 +53,7 @@ connect.to = async (datasource, options) => {
55
53
  }
56
54
 
57
55
  function options4 (name, _o) {
58
- const [, kind=_o && _o.kind, url] = /^(\w+):(.*)/.exec(name) || []
56
+ const [, kind=_o?.kind, url ] = /^(\w+):(.*)/.exec(name) || []
59
57
  const conf = cds.requires[name] || cds.requires[kind] || cds.requires.kinds[name] || cds.requires.kinds[kind]
60
58
  const o = { kind, ...conf, ..._o }
61
59
  if (!o.kind && !o.impl && !o.silent) throw cds.error(