@sap/cds 8.3.0 → 8.4.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 (59) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/bin/serve.js +9 -2
  3. package/lib/auth/ias-auth.js +4 -1
  4. package/lib/auth/jwt-auth.js +4 -1
  5. package/lib/compile/cdsc.js +1 -1
  6. package/lib/compile/etc/_localized.js +1 -0
  7. package/lib/compile/extend.js +23 -23
  8. package/lib/compile/for/lean_drafts.js +5 -0
  9. package/lib/compile/to/srvinfo.js +3 -1
  10. package/lib/{linked → core}/classes.js +8 -6
  11. package/lib/{linked/models.js → core/linked-csn.js} +4 -0
  12. package/lib/env/defaults.js +4 -1
  13. package/lib/i18n/localize.js +2 -2
  14. package/lib/index.js +43 -59
  15. package/lib/log/cds-error.js +21 -21
  16. package/lib/ql/cds-ql.js +5 -5
  17. package/lib/req/cds-context.js +5 -0
  18. package/lib/req/context.js +2 -2
  19. package/lib/req/locale.js +25 -21
  20. package/lib/{linked → req}/validate.js +11 -9
  21. package/lib/srv/cds-serve.js +1 -1
  22. package/lib/srv/middlewares/cds-context.js +1 -1
  23. package/lib/srv/middlewares/errors.js +20 -7
  24. package/lib/srv/protocols/hcql.js +106 -43
  25. package/lib/srv/protocols/http.js +2 -2
  26. package/lib/srv/protocols/index.js +14 -10
  27. package/lib/srv/protocols/odata-v4.js +2 -26
  28. package/lib/srv/protocols/okra.js +24 -0
  29. package/lib/srv/srv-models.js +6 -8
  30. package/lib/{utils → test}/cds-test.js +5 -5
  31. package/lib/utils/check-version.js +8 -15
  32. package/lib/utils/extend.js +20 -0
  33. package/lib/utils/lazify.js +33 -0
  34. package/lib/utils/tar.js +39 -1
  35. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +0 -1
  36. package/libx/_runtime/common/error/frontend.js +18 -4
  37. package/libx/_runtime/common/generic/auth/restrict.js +1 -3
  38. package/libx/_runtime/common/generic/sorting.js +1 -1
  39. package/libx/_runtime/common/utils/compareJson.js +139 -53
  40. package/libx/_runtime/common/utils/resolveView.js +19 -23
  41. package/libx/_runtime/fiori/lean-draft.js +2 -2
  42. package/libx/_runtime/messaging/kafka.js +7 -1
  43. package/libx/_runtime/remote/utils/data.js +30 -24
  44. package/libx/odata/ODataAdapter.js +12 -7
  45. package/libx/odata/middleware/batch.js +3 -0
  46. package/libx/odata/middleware/error.js +6 -0
  47. package/libx/odata/parse/afterburner.js +5 -6
  48. package/libx/odata/parse/multipartToJson.js +12 -8
  49. package/libx/odata/utils/index.js +3 -2
  50. package/libx/odata/utils/metadata.js +31 -1
  51. package/libx/outbox/index.js +5 -1
  52. package/package.json +3 -4
  53. package/server.js +18 -0
  54. package/lib/lazy.js +0 -51
  55. package/lib/test/index.js +0 -2
  56. /package/lib/{linked → core}/entities.js +0 -0
  57. /package/lib/{linked → core}/types.js +0 -0
  58. /package/lib/{utils → test}/axios.js +0 -0
  59. /package/lib/{utils → test}/data.js +0 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@
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.4.0 - 2024-10-29
8
+
9
+ ### Added
10
+
11
+ - Set the maximum allowed size of HTTP headers in bytes for `$batch` subrequests via flag `cds.env.odata.max_batch_header_size` (default: 64 KiB)
12
+ - New OData flag `cds.env.odata.context_with_columns` that adds selected and expanded columns to `@odata.context`. Default is `false`
13
+ - New experimental option `--workers` to `cds watch/run/serve` that allows running a `cds.server` cluster
14
+ (process env variable `WORKERS` or `CDS_WORKERS` can be used alternatively)
15
+
16
+ ### Changed
17
+
18
+ - For remote service calls to OData v2 services, less conversions are performed on the returned data
19
+ - Internal API `srv.endpoints` now always is an array of endpoint objects, an empty one if the service is not served to any protocol
20
+
21
+ ### Fixed
22
+
23
+ - Commands like `cds deploy` now fail with a clear error message if called with an invalid value for `cds.features.assert_integrity` (like `true`)
24
+ - Authentication validation errors (e.g., expired token, wrong audience) are logged as warning
25
+ - Requests using `$apply` will always apply implicit sorting on best effort mechanism
26
+ - Properly handle empty content-type in new OData adapter
27
+ - Error/crash with `cds.features.odata_new_parser` for requests containing `$expand=*` and `$select`, which selects individual columns and star, e.g. `$select=ID,*`
28
+ - Referencing new entities in `$batch` with new OData adapter did not work properly when using non integer content IDs in multipart/mixed
29
+ - `cds.compile.to.serviceinfo` to ignore unknown protocols
30
+ - New OData adapter and `cds.spawn` did not crash on programming errors (for example TypeError)
31
+
32
+ ## Version 8.3.1 - 2024-10-08
33
+
34
+ ### Fixed
35
+
36
+ - Erroneous caching in `cds.validate`
37
+ - Precedence of request headers for `cds.context.id`
38
+ - For `quoted` names, overwrite `@cds.persistence.name` for drafts and localized views properly
39
+ - Do not use hana error code as http status code
40
+
7
41
  ## Version 8.3.0 - 2024-09-30
8
42
 
9
43
  ### Added
@@ -119,7 +153,7 @@
119
153
  - Expand to `DraftAdministrativeData` for active instances of draft-enabled entities over drafts
120
154
  - Deduplication of columns for certain on conditions for the legacy database driver
121
155
  - For legacy-sqlite/-hana: Add keys to expands with only non-key elements to ensure not returning null for expand.
122
- - New parser was to restrictive regarding an empty line at the end of batch body.
156
+ - New parser was too restrictive regarding an empty line at the end of batch body.
123
157
  - Error target for operations with complex parameters
124
158
  - Remote services: JWT gets found in authorization header
125
159
  - Search with invalid characters
package/bin/serve.js CHANGED
@@ -2,7 +2,7 @@
2
2
  module.exports = exports = Object.assign ( serve, {
3
3
  options: [
4
4
  '--service', '--from', '--to', '--at', '--with',
5
- '--port',
5
+ '--port', '--workers',
6
6
  ],
7
7
  flags: [
8
8
  '--project', '--projects',
@@ -10,7 +10,7 @@ module.exports = exports = Object.assign ( serve, {
10
10
  '--mocked', '--with-mocks', '--with-bindings', '--resolve-bindings',
11
11
  '--watch',
12
12
  ],
13
- shortcuts: [ '-s', undefined, '-2', '-a', '-w', undefined, '-p' ],
13
+ shortcuts: [ '-s', undefined, '-2', '-a', '-w', undefined, undefined, '-p' ],
14
14
  help: `
15
15
  # SYNOPSIS
16
16
 
@@ -75,6 +75,13 @@ module.exports = exports = Object.assign ( serve, {
75
75
  You can use *cds watch* as shortcut, which is equivalent to:
76
76
  *cds serve --with-mocks --in-memory? --watch --project ...*
77
77
 
78
+ *--workers* <number> | true
79
+
80
+ Spawns <number> of worker processes in a cluster to handle incoming requests.
81
+ If set to 'true', the number of workers is determined by the number of CPUs.
82
+ If omitted, the server runs in non-cluster mode as a single process.
83
+
84
+
78
85
  *--mocked*
79
86
 
80
87
  Use this option to launch a _single service_ in a mock server, for
@@ -49,7 +49,10 @@ module.exports = function ias_auth(config) {
49
49
 
50
50
  const token = req.headers.authorization?.split(/^bearer /i)[1]
51
51
  xssec.createSecurityContext(token, credentials, 'IAS', function (err, securityContext, tokenInfo) {
52
- if (err) LOG.error('User could not be authenticated due to error:', err)
52
+ if (err) {
53
+ const level = (err.statusCode >= 400 && err.statusCode < 500) ? 'warn' : 'error'
54
+ LOG[level]('User could not be authenticated due to error:', err)
55
+ }
53
56
 
54
57
  if (!securityContext) return next(new cds.error('Unauthorized', { code: 401 }))
55
58
 
@@ -51,7 +51,10 @@ module.exports = function jwt_auth(config) {
51
51
 
52
52
  const token = req.headers.authorization.split(/^bearer /i)[1]
53
53
  xssec.createSecurityContext(token, credentials, function (err, securityContext, tokenInfo) {
54
- if (err) LOG.error('User could not be authenticated due to error:', err)
54
+ if (err) {
55
+ const level = (err.statusCode >= 400 && err.statusCode < 500) ? 'warn' : 'error'
56
+ LOG[level]('User could not be authenticated due to error:', err)
57
+ }
55
58
 
56
59
  if (!securityContext) return next(new cds.error('Unauthorized', { code: 401, statusCode: 401 }))
57
60
 
@@ -91,7 +91,7 @@ const _options = {for: Object.assign (_options4, {
91
91
 
92
92
  const { assert_integrity } = cds.env.features
93
93
  if (assert_integrity)
94
- o.assertIntegrityType = assert_integrity.toUpperCase()
94
+ o.assertIntegrityType = assert_integrity.toString().toUpperCase()
95
95
 
96
96
  return o
97
97
  },
@@ -83,6 +83,7 @@ function unfold_csn (m) { // NOSONAR
83
83
  function _add_proxy4 (d, name, callback) {
84
84
  if (name in m.definitions) return DEBUG && DEBUG ('NOT overriding existing:', name)
85
85
  const x = {__proto__:d, name }; DEBUG && DEBUG ('adding proxy:', x)
86
+ if (d['@cds.persistence.name']) x['@cds.persistence.name'] = `localized.${d['@cds.persistence.name']}`
86
87
  Object.defineProperty (m.definitions, name, {value:x,writable:true,configurable:true})
87
88
  if (callback) callback(x)
88
89
  }
@@ -1,5 +1,5 @@
1
1
  const cds = new class { get compile(){ return super.compile = require ('./cds-compile') }}
2
- const { extend } = require ('../lazy')
2
+ const extend = require ('../utils/extend')
3
3
 
4
4
  /** @type <T> (target:T) => ({
5
5
  with <X,Y,Z> (x:X, y:Y, z:Z): ( T & X & Y & Z )
@@ -8,32 +8,32 @@ const { extend } = require ('../lazy')
8
8
  }) */
9
9
  module.exports = o => o.definitions ? { with(...csns) {
10
10
 
11
- // merge all extension csns
12
- const csn=o, merged = { definitions: {}, extensions: [] }
13
- for (const { definitions, extensions } of csns) {
14
- if (definitions) Object.assign(merged.definitions, definitions)
15
- if (extensions) merged.extensions.push(...extensions)
16
- }
11
+ // merge all extension csns
12
+ const csn=o, merged = { definitions: {}, extensions: [] }
13
+ for (const { definitions, extensions } of csns) {
14
+ if (definitions) Object.assign(merged.definitions, definitions)
15
+ if (extensions) merged.extensions.push(...extensions)
16
+ }
17
17
 
18
- // extend given base csn with merged extensions
19
- const extended = cds.compile({
20
- 'base.csn': cds.compile.to.json(csn),
21
- 'ext.csn': cds.compile.to.json(merged)
22
- })
18
+ // extend given base csn with merged extensions
19
+ const extended = cds.compile({
20
+ 'base.csn': cds.compile.to.json(csn),
21
+ 'ext.csn': cds.compile.to.json(merged)
22
+ })
23
23
 
24
- // handle localized extension elements
25
- for (let ext of merged.extensions) {
26
- for (let name in ext.elements) {
27
- const e = ext.elements[name]
28
- if (e.localized) {
29
- // add localized element also to respective .texts entity
30
- const texts = extended.definitions[ext.extend+'.texts']
31
- texts.elements[name] ??= { ...e, localized:null }
24
+ // handle localized extension elements
25
+ for (let ext of merged.extensions) {
26
+ for (let name in ext.elements) {
27
+ const e = ext.elements[name]
28
+ if (e.localized) {
29
+ // add localized element also to respective .texts entity
30
+ const texts = extended.definitions[ext.extend+'.texts']
31
+ texts.elements[name] ??= { ...e, localized:null }
32
+ }
32
33
  }
33
34
  }
34
- }
35
35
 
36
- extended.$sources = csn.$sources // required to load resources like i18n later on
37
- return extended
36
+ extended.$sources = csn.$sources // required to load resources like i18n later on
37
+ return extended
38
38
 
39
39
  }} : extend(o)
@@ -64,6 +64,11 @@ module.exports = function cds_compile_for_lean_drafts(csn) {
64
64
  elements: { ...active.elements, ...Draft.elements },
65
65
  query: undefined
66
66
  }
67
+
68
+ // for quoted names, we need to overwrite the cds.persistence.name of the derived, draft entity
69
+ if (active['@cds.persistence.name'])
70
+ draft['@cds.persistence.name'] = active['@cds.persistence.name'] + '_drafts'
71
+
67
72
  Object.defineProperty(model.definitions, _draftEntity, { value: draft })
68
73
  Object.defineProperty(active, 'drafts', { value: draft })
69
74
  Object.defineProperty(active, 'actives', { value: active })
@@ -45,7 +45,9 @@ module.exports = (model, options={}) => {
45
45
  function _makeNode(service) {
46
46
  // make a fake runtime object for the service, adding a `definition` property
47
47
  if (!service.definition) Object.defineProperty(service, 'definition', { value: service, enumerable: false })
48
- const endpoints = cds.service.protocols.endpoints4(service).map(e => Object.assign({}, e, { path: _url4(e.path) }))
48
+ const endpoints = cds.service.protocols.endpoints4(service).map?.(e => Object.assign({}, e, { path: _url4(e.path) }))
49
+ if (!endpoints || !endpoints.length) return
50
+
49
51
  return {
50
52
  name: service.name,
51
53
  urlPath: endpoints[0].path, // legacy
@@ -1,4 +1,3 @@
1
- const { extend } = require('../lazy')
2
1
  const _proxy = Symbol('_proxy')
3
2
 
4
3
  class any { is (kind) { return this.kind === kind || kind === 'any' }
@@ -211,9 +210,12 @@ module.exports = {
211
210
  * class entity { bar(){} }
212
211
  * )
213
212
  */
214
- mixin(...classes) { for (let each of classes) {
215
- const clazz = this[each.name]
216
- if (!clazz) throw new Error(`unknown class '${each.name}'`)
217
- extend(clazz).with(each)
218
- }},
213
+ mixin(...classes) {
214
+ const extend = require('../utils/extend')
215
+ for (let each of classes) {
216
+ const clazz = this[each.name]
217
+ if (!clazz) throw new Error(`unknown class '${each.name}'`)
218
+ extend(clazz).with(each)
219
+ }
220
+ },
219
221
  }
@@ -62,6 +62,7 @@ class LinkedCSN {
62
62
  let i=0, n=ref.length, t=defs[ref[0]]
63
63
  for (;;) {
64
64
  if (++i === n) return t
65
+ if (t.items) t = t.items
65
66
  if (t.target) t = defs[t.target]
66
67
  if (t.elements) t = t.elements[ref[i]]
67
68
  if (!t) return
@@ -114,6 +115,9 @@ class LinkedCSN {
114
115
  for (let s of srvs) Object.defineProperty (srvs, s.name, {value:s})
115
116
  return this.set ('services', srvs)
116
117
  }
118
+
119
+ /** A common cache for all kinds of things -> keeps the models clean */
120
+ get _cached() { return this.set ('_cached', {}) }
117
121
  }
118
122
 
119
123
 
@@ -50,6 +50,7 @@ const defaults = module.exports = {
50
50
  protocols: {
51
51
  'odata-v4' : { path: '/odata/v4' },
52
52
  'odata-v2' : { path: '/odata/v2' },
53
+ 'okra' : { path: '/okra' },
53
54
  'rest' : { path: '/rest' },
54
55
  'hcql' : { path: '/hcql' },
55
56
  },
@@ -186,6 +187,8 @@ const defaults = module.exports = {
186
187
  refs: undefined,
187
188
  proxies: undefined,
188
189
  containment: undefined,
190
+ context_with_columns: false,
191
+ max_batch_header_size: 64 * 1024, // default to 64KiB (instead of node's 16KiB)
189
192
  },
190
193
 
191
194
  sql: {
@@ -248,4 +251,4 @@ const defaults = module.exports = {
248
251
 
249
252
  }
250
253
 
251
- require('./plugins')(defaults)
254
+ require('./plugins')(defaults)
@@ -28,7 +28,7 @@ function localize (aString, locale, model = cds.context?.model || cds.model, ext
28
28
  if (typeof aString === 'object') [ aString, model ] = [ model, aString ]
29
29
 
30
30
  // in case of multiple locales, return a generator
31
- if (Array.isArray(locale)) return (function*(){
31
+ if (Array.isArray(locale) || locale == 'all' || locale == '*') return (function*(){
32
32
  let localized, bundles = bundles4 (model, locale)
33
33
  if (bundles?.[Symbol.iterator]) for (let [lang,each] of bundles) {
34
34
  localized = _localize_with (each)
@@ -64,7 +64,7 @@ function bundles4 (model, locales = cds.env.i18n.languages) {
64
64
  const {i18n} = cds.env
65
65
 
66
66
  if (locales.split) locales = locales.split(',')
67
- if (locales[0] === '*' || locales[0] === 'all') {
67
+ if (locales == 'all' || locales == '*') { // NOTE: using == to match 'all' as well as ['all']
68
68
  locales = allLocales4 (folders); if (!locales) return {}
69
69
  if (!locales.includes(i18n.fallback_bundle)) locales.push (i18n.fallback_bundle)
70
70
  }
package/lib/index.js CHANGED
@@ -1,22 +1,19 @@
1
- if (process.env.CDS_STRICT_NODE_VERSION !== 'false') require ('./utils/check-version')
2
- !(global.__cds_loaded_from ??= new Set).add(__filename) // track from where we loaded cds
3
-
4
- /** @typedef { import('./srv/srv-api') } Service */
1
+ if (process.env.CDS_STRICT_NODE_VERSION !== 'false') require ('./utils/check-version.js')
2
+ ! (global.__cds_loaded_from ??= new Set).add(__filename) // track from where we loaded cds
5
3
 
6
4
  const { EventEmitter } = require('node:events')
7
- const { extend, lazify } = require('./lazy')
8
-
9
5
  const cds = module.exports = global.cds = new class cds extends EventEmitter {
10
6
 
11
- async emit (eve, ...args) {
12
- if (eve === 'served') for (let l of this.listeners(eve)) await l.call(this,...args)
13
- else return super.emit (eve, ...args)
14
- }
15
-
7
+ /** @typedef {import('./srv/srv-api.js')} Service */
8
+ /** @type {import('./core/linked-csn.js')} */ model = undefined
9
+ /** @type Service */ db = undefined
16
10
  /** CLI args */ cli = { command:'', options:{}, argv:[] }
17
11
  /** Working dir */ root = process.cwd()
18
- /** @type Linked */ model = undefined
19
- /** @type Service */ db = undefined
12
+
13
+ async emit (eve, ...args) {
14
+ if (eve === 'served') for (let each of this.listeners(eve)) await each.call(this,...args)
15
+ else return super.emit (eve, ...args)
16
+ }
20
17
 
21
18
  // Configuration & Information
22
19
  get requires() { return super.requires = this.env.requires._resolved() }
@@ -41,10 +38,8 @@ const cds = module.exports = global.cds = new class cds extends EventEmitter {
41
38
  // Model Reflection, Builtin types and classes
42
39
  get entities() { return this.db?.entities || this.model?.entities }
43
40
  get reflect() { return super.reflect = this.linked }
44
- get linked() { return super.linked = require('./linked/models') }
45
- get infer() { return super.infer = require('./ql/infer') }
46
- get builtin() { return super.builtin = require('./linked/types') }
47
- get validate() { return super.validate = require('./linked/validate') }
41
+ get linked() { return super.linked = require('./core/linked-csn.js') }
42
+ get builtin() { return super.builtin = require('./core/types.js') }
48
43
  get Association() { return super.Association = this.builtin.classes.Association }
49
44
  get Composition() { return super.Composition = this.builtin.classes.Composition }
50
45
  get entity() { return super.entity = this.builtin.classes.entity }
@@ -52,17 +47,17 @@ const cds = module.exports = global.cds = new class cds extends EventEmitter {
52
47
  get type() { return super.type = this.builtin.classes.type }
53
48
  get array() { return super.array = this.builtin.classes.array }
54
49
  get struct() { return super.struct = this.builtin.classes.struct }
55
- get service() { return super.service = extend (this.builtin.classes.service) .with ({
50
+ get service() { return super.service = Object.assign (this.builtin.classes.service, {
56
51
  /** @param {( this:Service, srv:Service )} fn */ impl: fn => fn,
57
52
  /** @type Service[] */ providers: []
58
53
  })}
59
54
 
60
55
  // Providing and Consuming Services
61
- /** @type { Record<string,Service> } */ services = Object.defineProperties (
62
- new class { *[Symbol.iterator](){ for (let e in this) yield this[e] } },
63
- { _pending: {value:{}} }
64
- )
65
- get server() { return super.server = require('../server') }
56
+ /** @type { Record<string,Service> } */ services = new class {
57
+ *[Symbol.iterator](){ for (let e in this) yield this[e] }
58
+ get _pending(){ return this.#pending ??= {} } #pending
59
+ }
60
+ get server() { return super.server = require('../server.js') }
66
61
  get serve() { return super.serve = require('./srv/cds-serve') }
67
62
  get connect() { return super.connect = require('./srv/cds-connect') }
68
63
  get outboxed() { return super.outboxed = require('../libx/outbox').outboxed }
@@ -70,6 +65,7 @@ const cds = module.exports = global.cds = new class cds extends EventEmitter {
70
65
  get middlewares() { return super.middlewares = require('./srv/middlewares') }
71
66
  get odata() { return super.odata = require('../libx/odata') }
72
67
  get auth() { return super.auth = require('./auth') }
68
+ shutdown() { this.app?.server && process.exit() } // is overridden in bin/serve.js
73
69
 
74
70
  // Core Services API
75
71
  get Service() { return super.Service = require('./srv/srv-api') }
@@ -77,6 +73,7 @@ const cds = module.exports = global.cds = new class cds extends EventEmitter {
77
73
  get Request() { return super.Request = require('./req/request') }
78
74
  get Event() { return super.Event = require('./req/event') }
79
75
  get User() { return super.User = require('./req/user') }
76
+ get validate() { return super.validate = require('./req/validate.js') }
80
77
 
81
78
  // Services, Protocols and Periphery
82
79
  get ApplicationService() { return super.ApplicationService = require('../libx/_runtime/common/Service.js') }
@@ -94,15 +91,13 @@ const cds = module.exports = global.cds = new class cds extends EventEmitter {
94
91
  get utils() { return super.utils = require('./utils/cds-utils') }
95
92
  get error() { return super.error = require('./log/cds-error') }
96
93
  get exec() { return super.exec = require('../bin/serve').exec }
97
- get test() { return super.test = require('./utils/cds-test') }
94
+ get test() { return super.test = require('./test/cds-test.js') }
98
95
  get log() { return super.log = require('./log/cds-log') }
99
96
  get debug() { return super.debug = this.log.debug }
100
- get lazify() { return lazify }
101
- get lazified() { return lazify }
102
97
  clone(x) { return structuredClone(x) }
103
- exit(code){ return cds.shutdown ? cds.shutdown() : process.exit(code) }
104
98
 
105
99
  // Querying and Databases
100
+ get infer(){ return super.infer = require('./ql/infer') }
106
101
  get txs() { return super.txs = new this.Service('cds.tx') }
107
102
  get ql() { return super.ql = require('./ql/cds-ql') }
108
103
  tx (..._) { return (this.db || this.txs).tx(..._) }
@@ -116,38 +111,27 @@ const cds = module.exports = global.cds = new class cds extends EventEmitter {
116
111
  delete (..._) { return (this.db || this.error._no_primary_db).delete(..._) }
117
112
  disconnect (..._) { return (this.db || this.error._no_primary_db).disconnect(..._) }
118
113
 
119
- // legacy and to be moved stuff -> hidden for tools in cds.__proto__
120
- /** @deprecated */ transaction (..._) { return (this.db||this.error._no_primary_db).transaction(..._) }
114
+ // Deprecated stuff to be removed in upcomming releases...
115
+ /** @deprecated */ get lazified() { return this.lazify }
116
+ /** @deprecated */ get lazify() { return super.lazify = require('./utils/lazify.js') }
121
117
  /** @deprecated */ get in() { return super.in = cwd => !cwd ? this : {__proto__:this, cwd, env: this.env.for('cds',cwd) } }
118
+ /** @deprecated */ exit(code){ return this.app?.server ? this.shutdown() : process.exit(code) }
119
+ /** @deprecated */ transaction (..._) { return (this.db||this.error._no_primary_db).transaction(..._) }
122
120
  }
123
121
 
124
- // add global cds.ql commands
125
- extend (global) .with (class {
126
- static get SELECT() { return cds.ql.SELECT }
127
- static get INSERT() { return cds.ql.INSERT }
128
- static get UPSERT() { return cds.ql.UPSERT }
129
- static get UPDATE() { return cds.ql.UPDATE }
130
- static get DELETE() { return cds.ql.DELETE }
131
- static get CREATE() { return cds.ql.CREATE }
132
- static get DROP() { return cds.ql.DROP }
133
- static get CDL() { return cds.parse.CDL }
134
- static get CQL() { return cds.parse.CQL }
135
- static get CXL() { return cds.parse.CXL }
136
- })
137
-
138
- // ensure cds.test is loaded for running tests w/ node --test
139
- 'describe' in global || ['describe','before','beforeAll','afterAll'].forEach (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] }
143
- })
144
- })
145
-
146
- // Allow for import cds from '@sap/cds' without esModuleInterop
147
- // FIXME: remove this flag in the next release. Only serves as fallback switch if people report issues with value:cds
148
- // Setting it to module.exports lead to issues with vitest while setting it to cds apparently works fine.
149
- if (process.env.CDS_ESM_INTEROP_DEFAULT) {
150
- Object.defineProperties(module.exports, { default: {value:module.exports}, __esModule: {value:true} })
151
- } else {
152
- Object.defineProperties(module.exports, { default: {value:cds}, __esModule: {value:true} })
153
- }
122
+ // Add global convenience shortcuts for cds.ql and cds.parse commands
123
+ cds.extend (global) .with ( class globals {
124
+ get SELECT() { return cds.ql.SELECT }
125
+ get INSERT() { return cds.ql.INSERT }
126
+ get UPSERT() { return cds.ql.UPSERT }
127
+ get UPDATE() { return cds.ql.UPDATE }
128
+ get DELETE() { return cds.ql.DELETE }
129
+ get CREATE() { return cds.ql.CREATE }
130
+ get DROP() { return cds.ql.DROP }
131
+ get CDL() { return cds.parse.CDL }
132
+ get CQL() { return cds.parse.CQL }
133
+ get CXL() { return cds.parse.CXL }
134
+ } .prototype )
135
+
136
+ // Allow for `import cds from '@sap/cds'` without `esModuleInterop` in tsconfig.json
137
+ Object.defineProperties (module.exports, { default: {value:cds}, __esModule: {value:true} })
@@ -5,30 +5,30 @@ const { format, inspect } = require('../utils/cds-utils')
5
5
  * Constructs and optionally throws an Error object.
6
6
  * Usage variants:
7
7
  *
8
- * cds.error `Message with formatted: ${{foo:'bar'}}`
9
- * cds.error ({ message, code, ... })
10
- * cds.error (message, { code, ... })
11
- * cds.error (status, message, { code, ... })
8
+ * cds.error (404, 'Not Found', { code, ... })
9
+ * cds.error ('Not Found', { code, ... })
10
+ * cds.error ({ code, message, ... })
11
+ * cds.error `template string usage variant`
12
12
  *
13
- * Calling `cds.error()` with `new` returns the newly created Error,
14
- * while calling it without `new` throws immediately. The latter is
15
- * useful for usages like that:
13
+ * When called with `new` the newly created Error is returned.
14
+ * When called without `new` the error is thrown immediately.
15
+ * The latter is useful for usages like that:
16
16
  *
17
- * let x = y || cds.error `Argument 'y' must not be null`
17
+ * let x = y || cds.error `'y' must be truthy, got: ${y}`
18
+ *
19
+ * @param {number} [status] - HTTP status code
20
+ * @param {string} [message] - Error message
21
+ * @param {object} [details] - Additional error details
22
+ * @param {Function} [caller] - The function calling this
18
23
  */
19
- const error = exports = module.exports = function cds_error ( message, details, caller ) {
20
- let e
21
- if (message.raw) [ message, details, caller ] = [ error.message(...arguments) ]
22
- if (typeof message === 'number') [ message, details ] = [ details, {status:message} ]
23
- if (typeof message === 'string') {
24
- e = new Error(message)
25
- } else {
26
- e = message.stack ? message : Object.assign(new Error,message)
27
- ;[ caller, details ] = [ details ]
28
- }
29
- Error.captureStackTrace (e,caller||error)
30
- if (details) Object.assign (e,details)
31
- if (new.target) return e; else throw e
24
+ const error = exports = module.exports = function cds_error ( status, message, details, caller ) {
25
+ if (typeof status !== 'number') [ status, message, details, caller, status ] = status.raw ? [ undefined, error.message(...arguments) ] : [ undefined, status, message, details ]
26
+ if (typeof message === 'object') [ message, details, caller ] = [ undefined, message, details ]
27
+ const err = details?.stack ? details : Object.assign (new Error (message, details), details)
28
+ if (caller) Error.captureStackTrace (err, caller); else Error.captureStackTrace (err, error)
29
+ if (status) err.status = status
30
+ if (new.target) return err
31
+ else throw err
32
32
  }
33
33
 
34
34
 
package/lib/ql/cds-ql.js CHANGED
@@ -14,7 +14,7 @@ require = path => { // eslint-disable-line no-global-assign
14
14
  }
15
15
 
16
16
  module.exports = exports = {
17
- Query,
17
+ Query,
18
18
  SELECT: require('./SELECT'),
19
19
  INSERT: require('./INSERT'),
20
20
  UPSERT: require('./UPSERT'),
@@ -30,10 +30,10 @@ exports.clone = function (q,_) {
30
30
  }
31
31
 
32
32
  exports.query = function (q) {
33
- if (q instanceof Query) return q
34
- let kind = Object.keys(q)[0]
35
- let clazz = exports[kind]
36
- return !clazz ? q : new clazz (q[kind])
33
+ if (!q || q instanceof Query) return q
34
+ let kind = Object.keys(q)[0]; if (!kind) return
35
+ let clazz = exports[kind]; if (!clazz) return q
36
+ return new clazz (q[kind])
37
37
  },
38
38
 
39
39
  exports._reset = ()=>{ // for strange tests only
@@ -1,5 +1,6 @@
1
1
  const { AsyncLocalStorage } = require ('async_hooks')
2
2
  const { EventEmitter } = require('events')
3
+ const { isStandardError } = require('../../libx/_runtime/common/error/standardError')
3
4
  const EventContext = require('./context'), ec4 = v => {
4
5
  if (v instanceof EventContext || typeof v !== 'object') return v
5
6
  if (v.context) return v.context
@@ -38,6 +39,10 @@ module.exports = new class extends AsyncLocalStorage {
38
39
  const em = new EventEmitter
39
40
  if (o?.every) {
40
41
  em.timer = setInterval(fx, o.every)
42
+ em.on('failed', e => { if (isStandardError(e) && cds.env.server.shutdown_on_uncaught_errors) {
43
+ cds.log().error('❗️Uncaught', e)
44
+ return cds.shutdown(e)}
45
+ })
41
46
  cds.on('shutdown', () => clearInterval(em.timer))
42
47
  } else {
43
48
  em.timer = (o?.after ? setTimeout(fx, o.after) : setImmediate(fx)).unref()
@@ -99,10 +99,10 @@ class EventContext {
99
99
  if (l) super.locale = super._locale = l
100
100
  }
101
101
  get locale() {
102
- return super.locale = this._propagated.locale || req_locale(this._.req)
102
+ return super.locale = this._propagated.locale || req_locale.from(this._.req)
103
103
  }
104
104
  get _locale() {
105
- return super._locale = this._propagated._locale || req_locale.from_req(this._.req)
105
+ return super._locale = this._propagated._locale || req_locale.raw(this._.req)
106
106
  || this.hasOwnProperty('locale') && this.locale // eslint-disable-line no-prototype-builtins
107
107
  }
108
108
 
package/lib/req/locale.js CHANGED
@@ -1,12 +1,19 @@
1
+ const {i18n} = require('../index').env
1
2
 
2
- const {i18n} = require('..').env
3
- const INCLUDE_LIST = i18n.preserved_locales.reduce((p,n)=>{
4
- p[n]=n; p[n.toUpperCase()]=n; return p
5
- },{
6
- en_US_x_saptrc: 'en_US_saptrc',
7
- en_US_x_sappsd: 'en_US_sappsd',
8
- en_US_x_saprigi: 'en_US_saprigi'
9
- })
3
+ function normalized_locale_from (req) {
4
+ const header = original_locale_from (req); if (!header) return i18n.default_language
5
+ const locale = header .match (/^[^,; ]*/)[0] .replace (/-/g,'_')
6
+ return SPECIAL[locale] //> keeps language and region for those in i18n.preserved_locales
7
+ || locale.match(/[A-Za-z]+/)?.[0].toLowerCase() //> keeps language only
8
+ || i18n.default_language
9
+ }
10
+
11
+ function original_locale_from (req) {
12
+ return !req ? undefined :
13
+ req.query['sap-locale'] || SAP_LANGUAGES[req.query['sap-language']] ||
14
+ req.headers['x-sap-request-language'] ||
15
+ req.headers['accept-language']
16
+ }
10
17
 
11
18
  const SAP_LANGUAGES = {
12
19
  '1Q': 'en_US_x_saptrc',
@@ -14,18 +21,15 @@ const SAP_LANGUAGES = {
14
21
  '3Q': 'en_US_x_saprigi'
15
22
  }
16
23
 
17
- // normalizes to BCP47
18
- const from_req = req => req && (
19
- req.query && (req.query['sap-locale'] || SAP_LANGUAGES[req.query['sap-language']]) ||
20
- req.headers && (req.headers['x-sap-request-language'] || req.headers['accept-language'])
21
- )
22
-
23
- function req_locale (req) {
24
- const locale = from_req(req); if (!locale) return i18n.default_language
25
- const loc = locale.replace(/-/g,'_').match(/^[^,; ]*/)[0]
26
- return INCLUDE_LIST[loc]
27
- || /^([a-z]+)/i.test(loc) && RegExp.$1.toLowerCase()
28
- || i18n.default_language
24
+ const SPECIAL = {
25
+ ...Object.fromEntries (i18n.preserved_locales.map(l=>[l.toUpperCase(),l])), // REVISIT: why uppercase?
26
+ ...Object.fromEntries (i18n.preserved_locales.map(l=>[l,l])),
27
+ en_US_x_saptrc: 'en_US_saptrc',
28
+ en_US_x_sappsd: 'en_US_sappsd',
29
+ en_US_x_saprigi: 'en_US_saprigi',
29
30
  }
30
31
 
31
- module.exports = Object.assign(req_locale, { from_req })
32
+ module.exports = Object.assign (normalized_locale_from, {
33
+ from: normalized_locale_from,
34
+ raw: original_locale_from
35
+ })