@sap/cds 8.2.3 → 8.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +44 -3
  2. package/bin/test.js +1 -1
  3. package/lib/compile/etc/_localized.js +1 -0
  4. package/lib/compile/etc/csv.js +1 -1
  5. package/lib/compile/for/lean_drafts.js +5 -0
  6. package/lib/dbs/cds-deploy.js +8 -5
  7. package/lib/env/cds-requires.js +0 -13
  8. package/lib/linked/validate.js +11 -9
  9. package/lib/log/cds-error.js +10 -7
  10. package/lib/plugins.js +8 -3
  11. package/lib/srv/middlewares/cds-context.js +1 -1
  12. package/lib/srv/middlewares/errors.js +5 -3
  13. package/lib/srv/protocols/index.js +4 -4
  14. package/lib/srv/srv-methods.js +1 -0
  15. package/lib/utils/cds-test.js +2 -1
  16. package/lib/utils/cds-utils.js +14 -1
  17. package/lib/utils/colors.js +45 -44
  18. package/libx/_runtime/common/composition/data.js +4 -2
  19. package/libx/_runtime/common/composition/index.js +1 -2
  20. package/libx/_runtime/common/composition/tree.js +1 -24
  21. package/libx/_runtime/common/error/frontend.js +18 -4
  22. package/libx/_runtime/common/generic/auth/restrict.js +29 -4
  23. package/libx/_runtime/common/generic/auth/restrictions.js +29 -36
  24. package/libx/_runtime/common/i18n/messages.properties +1 -1
  25. package/libx/_runtime/common/utils/cqn.js +0 -26
  26. package/libx/_runtime/common/utils/csn.js +0 -14
  27. package/libx/_runtime/common/utils/differ.js +1 -0
  28. package/libx/_runtime/common/utils/resolveView.js +28 -9
  29. package/libx/_runtime/common/utils/templateProcessor.js +3 -0
  30. package/libx/_runtime/fiori/lean-draft.js +30 -12
  31. package/libx/_runtime/types/api.js +1 -1
  32. package/libx/_runtime/ucl/Service.js +2 -2
  33. package/libx/common/utils/path.js +1 -4
  34. package/libx/odata/ODataAdapter.js +6 -0
  35. package/libx/odata/middleware/batch.js +7 -9
  36. package/libx/odata/middleware/create.js +4 -2
  37. package/libx/odata/middleware/delete.js +3 -1
  38. package/libx/odata/middleware/operation.js +7 -5
  39. package/libx/odata/middleware/read.js +14 -10
  40. package/libx/odata/middleware/service-document.js +1 -1
  41. package/libx/odata/middleware/stream.js +1 -0
  42. package/libx/odata/middleware/update.js +5 -3
  43. package/libx/odata/parse/afterburner.js +37 -49
  44. package/libx/odata/utils/index.js +3 -2
  45. package/libx/odata/utils/postProcess.js +3 -8
  46. package/package.json +1 -1
  47. package/libx/_runtime/cds-services/services/utils/compareJson.js +0 -2
  48. package/libx/_runtime/messaging/event-broker.js +0 -317
package/CHANGELOG.md CHANGED
@@ -4,6 +4,47 @@
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.3.1 - 2024-10-08
8
+
9
+ ### Fixed
10
+
11
+ - Erroneous caching in `cds.validate`
12
+ - Precedence of request headers for `cds.context.id`
13
+ - For `quoted` names, overwrite `@cds.persistence.name` for drafts and localized views properly
14
+ - Do not use hana error code as http status code
15
+
16
+ ## Version 8.3.0 - 2024-09-30
17
+
18
+ ### Added
19
+
20
+ - `cds.deploy` can now also write its DDL statements to a separate log
21
+ - Symlinks are followed in `cds test`
22
+
23
+ ### Changed
24
+
25
+ - Unknown protocols in `@protocol` annotations formerly prevented server starts; they are merely ignored now with a warning in the logs.
26
+ - Deprecated configuration flag `cds.env.features.keys_in_data_compat` because of incompatibility with data validation in new OData adapter.
27
+ - `@cds.api.ignore` doesn't suppress an association, the annotation is propagated to the (generated) foreign keys.
28
+ - Where clauses of restrictions for bound actions and functions defined by `@restrict` are now enforced and no longer ignored.
29
+ - `@cap-js/telemetry` is now loaded before other plugins to allow better instrumentation.
30
+
31
+ ### Fixed
32
+
33
+ - When modifying active children of of draft-enabled entities directly (`bypass_draft`), the error message was misleading.
34
+ - Cleaning up drafts calls `CANCEL` handlers
35
+ - Allow to call `CANCEL` on draft entities programmatically
36
+ - Encoding of `@odata.nextLink` path
37
+ - Computed fields are ignored in projections
38
+ - Consider `id` in a `ref` step for mapping of service elements to their name on the db.
39
+ - Feature toggles with new OData adapter.
40
+ - Target entity was incorrectly calculated for some actions in new OData adapter.
41
+ - `req.diff()` does not manipulate existing queries anymore.
42
+ - New OData adapter: normalize on commit error in `/$batch`
43
+
44
+ ### Removed
45
+
46
+ - Alpha support for SAP Event Broker-based messaging (kind `event-broker`). Use CDS plugin `@cap-js/event-broker` instead.
47
+
7
48
  ## Version 8.2.3 - 2024-09-20
8
49
 
9
50
  ### Changed
@@ -54,7 +95,9 @@
54
95
 
55
96
  ### Changed
56
97
 
57
- - 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
98
+ - 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.
99
+ - When parsing CSV files, `cds.deploy` no longer doubles a literal `\` character (backslash) with a second backslash (`\\`), but retains it as-is. This caused unwanted data changes.
100
+ - 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`.
58
101
 
59
102
  ### Fixed
60
103
 
@@ -76,7 +119,6 @@
76
119
  [...linked.definitions].map(d => d.name)
77
120
  ```
78
121
 
79
-
80
122
  ## Version 8.1.1 - 2024-08-08
81
123
 
82
124
  ### Fixed
@@ -406,7 +448,6 @@
406
448
 
407
449
  ### Changed
408
450
 
409
- - 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`.
410
451
  - The index page now lists all service endpoints, which is important for services that are exposed through multiple protocols.
411
452
  - `cds.deploy` improves error diagnostics with deeper `Query` object inspection.
412
453
  - Slightly changed the default export for ESM compatibility. This fixed failing ESM imports in Vitest tests.
package/bin/test.js CHANGED
@@ -67,7 +67,7 @@ async function find (argv,o,recent) {
67
67
  if (files.length && !roots.length && !includes.length) return files //> all files resolved
68
68
 
69
69
  // Prepare UNIX find command to fetch matching files
70
- let find = `find ${roots.join(' ')||'.'} -type f`
70
+ let find = `find -L ${roots.join(' ')||'.'} -type f`
71
71
  if (patterns.length) find += ` \\( ${ patterns.map (p=>`-name "${p.replace(/^([^*])/,'*$1')}"`).join(' -o ') } \\)`
72
72
  if (includes.length) find += ` \\( ${ includes.map (p=>`-regex .*${p.replace(/\./g,'\\\\.')}.*`).join(' -o ') } \\)`
73
73
  if (excludes.length) find += ` \\( ${ excludes.map (x=>`! -regex .*${x.replace(/\./g,'\\\\.')}.*`).join(' ') } \\)`
@@ -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
  }
@@ -44,7 +44,7 @@ function parse (csv) {
44
44
  }
45
45
  else { // normal char
46
46
  if (val === undefined) val = ''
47
- val += c === '\\' ? '\\\\' : c
47
+ val += c
48
48
  }
49
49
  }
50
50
 
@@ -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 })
@@ -60,6 +60,9 @@ const deploy = module.exports = function cds_deploy (model, options, csvs) {
60
60
  deploy.schema = async function (db, csn = db.model, o) {
61
61
 
62
62
  if (!o.to || o.to === db.options.kind) o = { ...db.options, ...o }
63
+ let schema_log
64
+ if (Array.isArray(o.schema_log)) schema_log = { log: (...args) => args.length ? o.schema_log.push(...args) : o.schema_log.push('') }
65
+ else if (o.dry) schema_log = console
63
66
 
64
67
  let drops, creas
65
68
  let schevo = (o.kind === 'postgres' && o.schema_evolution !== false)
@@ -82,7 +85,7 @@ deploy.schema = async function (db, csn = db.model, o) {
82
85
  }
83
86
  o.schema_evolution = 'auto' // for INSERT_from4 below
84
87
  // cds deploy --model-only > fills in table cds_model above
85
- if (o['model-only']) return o.dry && console.log(after)
88
+ if (o['model-only']) return o.dry && schema_log.log(after)
86
89
  // cds deploy -- with auto schema evolution > upgrade by applying delta to former model
87
90
  creas = createsAndAlters
88
91
  drops = d
@@ -108,11 +111,11 @@ deploy.schema = async function (db, csn = db.model, o) {
108
111
 
109
112
  if (!drops.length && !creas.length) return !o.dry
110
113
 
111
- if (o.dry) {
112
- console.log(); for (let each of drops) console.log(each)
113
- console.log(); for (let each of creas) console.log(each, '\n')
114
- return
114
+ if (schema_log) {
115
+ schema_log.log(); for (let each of drops) schema_log.log(each)
116
+ schema_log.log(); for (let each of creas) schema_log.log(each, '\n')
115
117
  }
118
+ if (o.dry) return
116
119
 
117
120
  await db.run(drops)
118
121
  await db.run(creas)
@@ -231,19 +231,6 @@ const _messaging = {
231
231
  vcap: { label: "enterprise-messaging" },
232
232
  outbox: true
233
233
  },
234
- "event-broker": {
235
- impl: `${_runtime}/messaging/event-broker.js`,
236
- format: 'cloudevents',
237
- vcap: {
238
- label: "event-broker"
239
- }
240
- },
241
- "event-broker-internal": {
242
- kind: "event-broker",
243
- vcap: {
244
- label: "eventmesh-sap2sap-internal"
245
- }
246
- },
247
234
  'message-queuing': {
248
235
  impl: `${_runtime}/messaging/message-queuing.js`,
249
236
  outbox: true
@@ -241,14 +241,16 @@ class Association extends struct {
241
241
  /** Compositions are like nested entities, validating deep input against their target entity definitions. */
242
242
  class Composition extends entity {
243
243
  validate (data, path, ctx) { if (!data) return
244
- const elements = this._target.elements
245
- const uplinks = {} // statically determine the uplinks for this composition
246
- if (this.on) for (let {ref} of this.on) if (ref?.[0] === this.name) {
247
- const fk = ref[1], fk_ = fk+'_'; uplinks[fk] = true
248
- for (let e in elements) if (e.startsWith(fk_)) uplinks[e] = true
249
- }
250
- this.validate = (data, path, ctx) => super.validate (data, path, ctx, elements, uplinks)
251
- this.validate (data, path, ctx) // call first time
244
+ const _validate = this.own('_validate', () => {
245
+ const elements = this._target.elements
246
+ const uplinks = {} // statically determine the uplinks for this composition
247
+ if (this.on) for (let {ref} of this.on) if (ref?.[0] === this.name) {
248
+ const fk = ref[1], fk_ = fk+'_'; uplinks[fk] = true
249
+ for (let e in elements) if (e.startsWith(fk_)) uplinks[e] = true
250
+ }
251
+ return (data, path, ctx) => super.validate (data, path, ctx, elements, uplinks)
252
+ })
253
+ _validate (data, path, ctx)
252
254
  }
253
255
  }
254
256
 
@@ -312,4 +314,4 @@ $.LargeBinary.prototype .type_check = v => Buffer.isBuffer(v) || typeof v === 's
312
314
  $.LargeString.prototype .type_check = v => Buffer.isBuffer(v) || typeof v === 'string' || v instanceof Readable
313
315
 
314
316
  // Mixin above class extensions to cds.linked.classes
315
- $.mixin ( Decimal, string, $any, action, array, struct, entity, Association, Composition )
317
+ $.mixin ( Decimal, string, $any, action, array, struct, entity, Association, Composition )
@@ -1,17 +1,17 @@
1
- const { format } = require('util'), _formatted = v => format(v)
1
+ const { format, inspect } = require('../utils/cds-utils')
2
2
 
3
3
 
4
4
  /**
5
- * This is the implementation of cds.error().
5
+ * Constructs and optionally throws an Error object.
6
6
  * Usage variants:
7
7
  *
8
8
  * cds.error `Message with formatted: ${{foo:'bar'}}`
9
9
  * cds.error ({ message, code, ... })
10
10
  * cds.error (message, { code, ... })
11
- * let e = new cds.error(...) //> will not throw
11
+ * cds.error (status, message, { code, ... })
12
12
  *
13
13
  * Calling `cds.error()` with `new` returns the newly created Error,
14
- * while calling it without `new` it throws immediately. The latter is
14
+ * while calling it without `new` throws immediately. The latter is
15
15
  * useful for usages like that:
16
16
  *
17
17
  * let x = y || cds.error `Argument 'y' must not be null`
@@ -19,6 +19,7 @@ const { format } = require('util'), _formatted = v => format(v)
19
19
  const error = exports = module.exports = function cds_error ( message, details, caller ) {
20
20
  let e
21
21
  if (message.raw) [ message, details, caller ] = [ error.message(...arguments) ]
22
+ if (typeof message === 'number') [ message, details ] = [ details, {status:message} ]
22
23
  if (typeof message === 'string') {
23
24
  e = new Error(message)
24
25
  } else {
@@ -41,7 +42,9 @@ const error = exports = module.exports = function cds_error ( message, details,
41
42
  * //> x = A sample message with a string and [object Object], and 1,2,3
42
43
  * //> y = with a string, { an: 'object' }, and [ 1, 2, 3 ]
43
44
  */
44
- exports.message = (strings,...values) => String.raw(strings,...values.map(_formatted))
45
+ exports.message = (strings,...values) => {
46
+ return String.raw(strings,...values.map(v => format(v)))
47
+ }
45
48
 
46
49
 
47
50
  /**
@@ -53,9 +56,9 @@ exports.message = (strings,...values) => String.raw(strings,...values.map(_forma
53
56
  * typeof x === 'string' || cds.error.expected `${{x}} to be a string`
54
57
  * //> Error: Expected argument 'x' to be a string, but got: { foo: 'bar' }
55
58
  */
56
- const expected = exports.expected = ([,type], arg) => {
59
+ exports.expected = ([,type], arg) => {
57
60
  const [ name, value ] = Object.entries(arg)[0]
58
- return error (`Expected argument '${name}'${type}, but got: ${require('util').inspect(value,{depth:11})}`, undefined, expected)
61
+ return error (`Expected argument '${name}'${type}, but got: ${inspect(value)}`, undefined, error.expected)
59
62
  }
60
63
 
61
64
 
package/lib/plugins.js CHANGED
@@ -1,5 +1,7 @@
1
-
2
1
  const cds = require('.')
2
+ const prio_plugins = {
3
+ '@cap-js/telemetry': true // to allow better instrumentation.
4
+ }
3
5
 
4
6
  exports.require = require
5
7
 
@@ -31,7 +33,7 @@ exports.activate = async function () {
31
33
  const DEBUG = cds.debug ('plugins', {label:'cds'})
32
34
  DEBUG?.time ('[cds] - loaded plugins in')
33
35
  const { plugins } = cds.env, { local } = cds.utils
34
- await Promise.all (Object.entries(plugins) .map (async ([ plugin, conf ]) => {
36
+ const loadPlugin = async ([plugin, conf]) => {
35
37
  DEBUG?.(`loading plugin ${plugin}:`, { impl: local(conf.impl) })
36
38
  // TODO: support ESM plugins. But see cap/cds/pull/1838#issuecomment-1177200 !
37
39
  const p = require (conf.impl)
@@ -43,7 +45,10 @@ exports.activate = async function () {
43
45
  await p.activate(conf)
44
46
  }
45
47
  return p
46
- }))
48
+ }
49
+ const all = Object.entries(plugins)
50
+ await Promise.all (all .filter(([name]) => prio_plugins[name]) .map (loadPlugin))
51
+ await Promise.all (all .filter(([name]) => !prio_plugins[name]) .map (loadPlugin))
47
52
  DEBUG?.timeEnd ('[cds] - loaded plugins in')
48
53
  return plugins
49
54
  }
@@ -9,7 +9,7 @@ const { EventContext } = cds
9
9
  module.exports = () => {
10
10
  /** @type { import('express').Handler } */
11
11
  return function cds_context (req, res, next) {
12
- const id = req.headers[corr_id] ??= req.headers[req_id] || req.headers[vr_id] || req.headers[crippled_corr_id] || uuid()
12
+ const id = req.headers[corr_id] ??= req.headers[crippled_corr_id] || req.headers[req_id] || req.headers[vr_id] || uuid()
13
13
  const ctx = EventContext.for ({ id, http: { req, res } })
14
14
  res.set ('X-Correlation-ID', id) // Note: we use capitalized style here as that's common standard in HTTP world
15
15
  cds._context.run (ctx, next)
@@ -1,5 +1,7 @@
1
1
  const production = process.env.NODE_ENV === 'production'
2
- const cds = require ('../..'), LOG = cds.log('error')
2
+ const cds = require ('../..')
3
+ const LOG = cds.log('error')
4
+ const { inspect } = cds.utils
3
5
 
4
6
  module.exports = () => {
5
7
  return function http_error (error, req, res, _next) { // eslint-disable-line no-unused-vars
@@ -15,9 +17,9 @@ module.exports = () => {
15
17
  if (!production && error.stack) error.stack = error.stack.replace(/\n {4}at .*(?:node_modules\/express|node:internal).*/g,'')
16
18
 
17
19
  if (400 <= status && status < 500) {
18
- LOG.warn (status, '>', error)
20
+ LOG.warn (status, '>', inspect(error))
19
21
  } else {
20
- LOG.error (status, '>', error)
22
+ LOG.error (status, '>', inspect(error))
21
23
  }
22
24
 
23
25
  // Expose as little information as possible in production, and as much as possible in development
@@ -56,7 +56,7 @@ class Protocols {
56
56
  // app.disable('x-powered-by')
57
57
  }
58
58
 
59
- for (let { kind, path } of endpoints) {
59
+ if (endpoints) for (let { kind, path } of endpoints) {
60
60
 
61
61
  // construct adapter instance from resolved implementation
62
62
  let adapter = cached[kind]; if (!adapter) {
@@ -117,11 +117,11 @@ class Protocols {
117
117
  // canonicalize to { kind, path } objects
118
118
  const endpoints = annos.map (each => {
119
119
  let { kind = each['='] || each, path } = each
120
- let { path: prefix } = this[kind] || cds.error `Unknown protocol: ${kind}`
120
+ if (!(kind in this)) return cds.log('adapters').warn ('ignoring unknown protocol:', kind)
121
121
  if (typeof path !== 'string') path = o?.at || o?.path || def['@path'] || _slugified(srv.name)
122
- if (path[0] !== '/') path = prefix+'/'+path
122
+ if (path[0] !== '/') path = this[kind].path + '/' + path // prefix with protocol path
123
123
  return { kind, path }
124
- })
124
+ }) .filter (e => e) //> skipping unknown protocols
125
125
 
126
126
  return endpoints.length && endpoints
127
127
  }
@@ -71,6 +71,7 @@ const add_handler_for = (srv, def) => {
71
71
 
72
72
  // ensure legacy compat, keys in req.data
73
73
  if(cds.env.features.keys_in_data_compat && target) {
74
+ cds.utils.deprecated({ old: 'flag cds.env.features.keys_in_data_compat'})
74
75
  // named/positional variant of keys
75
76
  const named = req.params.length === 1 && typeof req.params[0] === 'object'
76
77
 
@@ -221,7 +221,8 @@ let _expect = undefined
221
221
  global.beforeEach = beforeEach
222
222
  global.afterEach = afterEach
223
223
  global.expect = _expect = require('../test/expect')
224
- suite ('<next>', ()=>{}) //> to signal the start of a test file
224
+ // suite was introduced in Node 22
225
+ suite?.('<next>', ()=>{}) //> to signal the start of a test file
225
226
 
226
227
  }
227
228
 
@@ -2,15 +2,27 @@ const cwd = process.env._original_cwd || process.cwd()
2
2
  const cds = require('../index')
3
3
 
4
4
  module.exports = exports = new class {
5
+ get colors() { return super.colors = require('./colors') }
5
6
  get inflect() { return super.inflect = require('./inflect') }
6
- get inspect() { return super.inspect = require('util').inspect }
7
+ get inspect() {
8
+ const options = { depth: 11, colors: this.colors.enabled }
9
+ const {inspect} = require('node:util')
10
+ return super.inspect = v => inspect(v,options)
11
+ }
12
+ get format() {
13
+ const {format} = require('node:util')
14
+ return super.format = format
15
+ }
7
16
  get uuid() { return super.uuid = require('crypto').randomUUID }
8
17
  get yaml() { return super.yaml = require('@sap/cds-foss').yaml }
9
18
  get pool() { return super.pool = require('@sap/cds-foss').pool }
10
19
  get tar() { return super.tar = require('./tar') }
11
20
  }
12
21
 
22
+ /** @type {import('node:path')} */
13
23
  const path = exports.path = require('path'), { dirname, extname, join, resolve, relative } = path
24
+
25
+ /** @type {import('node:fs')} */
14
26
  const fs = exports.fs = Object.assign (exports,require('fs')) //> for compatibility
15
27
 
16
28
 
@@ -232,6 +244,7 @@ exports.find = function find (base, patterns='*', filter=()=>true) {
232
244
  return files
233
245
  }
234
246
 
247
+
235
248
  exports.deprecated = (fn, { kind = 'Method', old = fn.name+'()', use } = {}) => {
236
249
  const yellow = '\x1b[33m'
237
250
  const reset = '\x1b[0m'
@@ -1,49 +1,50 @@
1
- const none = process.stdout.isTTY && !process.env.NO_COLOR || process.env.FORCE_COLOR ? null : ''
1
+ const enabled = process.stdout.isTTY && !process.env.NO_COLOR || process.env.FORCE_COLOR
2
2
  const colors = {
3
- RESET: none ?? '\x1b[0m',
4
- BOLD: none ?? '\x1b[1m',
5
- BRIGHT: none ?? '\x1b[1m',
6
- DIMMED: none ?? '\x1b[2m',
7
- ITALIC: none ?? '\x1b[3m',
8
- UNDER: none ?? '\x1b[4m',
9
- BLINK: none ?? '\x1b[5m',
10
- FLASH: none ?? '\x1b[6m',
11
- INVERT: none ?? '\x1b[7m',
12
- BLACK: none ?? '\x1b[30m',
13
- RED: none ?? '\x1b[31m',
14
- GREEN: none ?? '\x1b[32m',
15
- YELLOW: none ?? '\x1b[33m',
16
- BLUE: none ?? '\x1b[34m',
17
- PINK: none ?? '\x1b[35m',
18
- CYAN: none ?? '\x1b[36m',
19
- LIGHT_GRAY: none ?? '\x1b[37m',
20
- DEFAULT: none ?? '\x1b[39m',
21
- GRAY: none ?? '\x1b[90m',
22
- LIGHT_RED: none ?? '\x1b[91m',
23
- LIGHT_GREEN: none ?? '\x1b[92m',
24
- LIGHT_YELLOW: none ?? '\x1b[93m',
25
- LIGHT_BLUE: none ?? '\x1b[94m',
26
- LIGHT_PINK: none ?? '\x1b[95m',
27
- LIGHT_CYAN: none ?? '\x1b[96m',
28
- WHITE: none ?? '\x1b[97m',
3
+ enabled,
4
+ RESET: enabled ? '\x1b[0m' : '',
5
+ BOLD: enabled ? '\x1b[1m' : '',
6
+ BRIGHT: enabled ? '\x1b[1m' : '',
7
+ DIMMED: enabled ? '\x1b[2m' : '',
8
+ ITALIC: enabled ? '\x1b[3m' : '',
9
+ UNDER: enabled ? '\x1b[4m' : '',
10
+ BLINK: enabled ? '\x1b[5m' : '',
11
+ FLASH: enabled ? '\x1b[6m' : '',
12
+ INVERT: enabled ? '\x1b[7m' : '',
13
+ BLACK: enabled ? '\x1b[30m' : '',
14
+ RED: enabled ? '\x1b[31m' : '',
15
+ GREEN: enabled ? '\x1b[32m' : '',
16
+ YELLOW: enabled ? '\x1b[33m' : '',
17
+ BLUE: enabled ? '\x1b[34m' : '',
18
+ PINK: enabled ? '\x1b[35m' : '',
19
+ CYAN: enabled ? '\x1b[36m' : '',
20
+ LIGHT_GRAY: enabled ? '\x1b[37m' : '',
21
+ DEFAULT: enabled ? '\x1b[39m' : '',
22
+ GRAY: enabled ? '\x1b[90m' : '',
23
+ LIGHT_RED: enabled ? '\x1b[91m' : '',
24
+ LIGHT_GREEN: enabled ? '\x1b[92m' : '',
25
+ LIGHT_YELLOW: enabled ? '\x1b[93m' : '',
26
+ LIGHT_BLUE: enabled ? '\x1b[94m' : '',
27
+ LIGHT_PINK: enabled ? '\x1b[95m' : '',
28
+ LIGHT_CYAN: enabled ? '\x1b[96m' : '',
29
+ WHITE: enabled ? '\x1b[97m' : '',
29
30
  bg: {
30
- BLACK: none ?? '\x1b[40m',
31
- RED: none ?? '\x1b[41m',
32
- GREEN: none ?? '\x1b[42m',
33
- YELLOW: none ?? '\x1b[43m',
34
- BLUE: none ?? '\x1b[44m',
35
- PINK: none ?? '\x1b[45m',
36
- CYAN: none ?? '\x1b[46m',
37
- WHITE: none ?? '\x1b[47m',
38
- DEFAULT: none ?? '\x1b[49m',
39
- LIGHT_GRAY: none ?? '\x1b[100m',
40
- LIGHT_RED: none ?? '\x1b[101m',
41
- LIGHT_GREEN: none ?? '\x1b[102m',
42
- LIGHT_YELLOW: none ?? '\x1b[103m',
43
- LIGHT_BLUE: none ?? '\x1b[104m',
44
- LIGHT_PINK: none ?? '\x1b[105m',
45
- LIGHT_CYAN: none ?? '\x1b[106m',
46
- LIGHT_WHITE: none ?? '\x1b[107m',
31
+ BLACK: enabled ? '\x1b[40m' : '',
32
+ RED: enabled ? '\x1b[41m' : '',
33
+ GREEN: enabled ? '\x1b[42m' : '',
34
+ YELLOW: enabled ? '\x1b[43m' : '',
35
+ BLUE: enabled ? '\x1b[44m' : '',
36
+ PINK: enabled ? '\x1b[45m' : '',
37
+ CYAN: enabled ? '\x1b[46m' : '',
38
+ WHITE: enabled ? '\x1b[47m' : '',
39
+ DEFAULT: enabled ? '\x1b[49m' : '',
40
+ LIGHT_GRAY: enabled ? '\x1b[100m' : '',
41
+ LIGHT_RED: enabled ? '\x1b[101m' : '',
42
+ LIGHT_GREEN: enabled ? '\x1b[102m' : '',
43
+ LIGHT_YELLOW: enabled ? '\x1b[103m' : '',
44
+ LIGHT_BLUE: enabled ? '\x1b[104m' : '',
45
+ LIGHT_PINK: enabled ? '\x1b[105m' : '',
46
+ LIGHT_CYAN: enabled ? '\x1b[106m' : '',
47
+ LIGHT_WHITE: enabled ? '\x1b[107m' : '',
47
48
  },
48
49
  }
49
50
  module.exports = colors
@@ -326,6 +326,7 @@ const selectDeepUpdateData = (service, model, req, selectAllColumns = false) =>
326
326
  const query = req.query
327
327
 
328
328
  // REVISIT this should be done somewhere before, so it is not done twice for deep updates
329
+ // REVISIT: this is done (better) in the new db-services
329
330
  const sqlQuery = cqn2cqn4sql(query, model)
330
331
 
331
332
  if (req && _isSameEntity(sqlQuery, req)) {
@@ -334,10 +335,11 @@ const selectDeepUpdateData = (service, model, req, selectAllColumns = false) =>
334
335
 
335
336
  const from = getEntityNameFromUpdateCQN(sqlQuery)
336
337
  const alias = sqlQuery.UPDATE.entity.as
337
- const where = sqlQuery.UPDATE.where || []
338
+ // if parts of (another) query are re-used make sure to get your own copy
339
+ const where = cds.clone(sqlQuery.UPDATE.where) || []
338
340
  const entityName = ensureNoDraftsSuffix(from)
339
341
  const draft = entityName !== from
340
- const orderBy = req?.target?.query?.SELECT?.orderBy
342
+ const orderBy = req?.target?.query?.SELECT?.orderBy ? cds.clone(req?.target?.query?.SELECT?.orderBy) : null
341
343
  _resolveOrderBy(orderBy, sqlQuery.UPDATE._transitions)
342
344
  const data = Object.assign({}, sqlQuery.UPDATE.data || {}, query.UPDATE.with || {})
343
345
  const compositionTree = getCompositionTree({
@@ -1,4 +1,4 @@
1
- const { getCompositionTree, getCompositionRoot } = require('./tree')
1
+ const { getCompositionTree } = require('./tree')
2
2
  const { hasDeepInsert, getDeepInsertCQNs } = require('./insert')
3
3
  const { hasDeepUpdate, getDeepUpdateCQNs } = require('./update')
4
4
  const { hasDeepDelete, getDeepDeleteCQNs, getSetNullParentForeignKeyCQNs } = require('./delete')
@@ -7,7 +7,6 @@ const { selectDeepUpdateData } = require('./data')
7
7
  module.exports = {
8
8
  // tree
9
9
  getCompositionTree,
10
- getCompositionRoot,
11
10
  // insert
12
11
  hasDeepInsert,
13
12
  getDeepInsertCQNs,
@@ -1,6 +1,5 @@
1
1
  const cds = require('../../cds')
2
2
 
3
- const { ensureNoDraftsSuffix } = require('../utils/draft')
4
3
  const { getTransition, getDBTable } = require('../utils/resolveView')
5
4
 
6
5
  const { prefixForStruct } = require('../../common/utils/csn')
@@ -259,27 +258,6 @@ const _memoizeGetCompositionTree = fn => {
259
258
  * exports
260
259
  */
261
260
 
262
- const getCompositionRoot = (definitions, entity) => {
263
- const associationElements = Object.keys(entity.elements)
264
- .map(key => entity.elements[key])
265
- .filter(element => element._isAssociationStrict)
266
-
267
- for (const { target } of associationElements) {
268
- const parentEntity = definitions[target]
269
- for (const parentElementName in parentEntity.elements) {
270
- const parentElement = parentEntity.elements[parentElementName]
271
- if (
272
- parentElement.isComposition &&
273
- parentElement.target === entity.name &&
274
- parentElement.target !== ensureNoDraftsSuffix(parentElement.parent.name)
275
- ) {
276
- return getCompositionRoot(definitions, parentEntity)
277
- }
278
- }
279
- }
280
- return entity
281
- }
282
-
283
261
  /**
284
262
  * Provides tree of all compositions. (Cached)
285
263
  *
@@ -291,6 +269,5 @@ const getCompositionRoot = (definitions, entity) => {
291
269
  const getCompositionTree = _memoizeGetCompositionTree(_getCompositionTree)
292
270
 
293
271
  module.exports = {
294
- getCompositionTree,
295
- getCompositionRoot
272
+ getCompositionTree
296
273
  }
@@ -51,7 +51,7 @@ const _rewriteError = error => {
51
51
  (code.startsWith('SQLITE_CONSTRAINT') && (message.match(/COMMIT/) || message.match(/FOREIGN KEY/))) ||
52
52
  (code === '155' && message.match(/fk constraint violation/))
53
53
  ) {
54
- // > foreign key constaint violation no sqlite/ hana
54
+ // > foreign key constaint violation on sqlite/ hana
55
55
  error.code = '400'
56
56
  error.message = 'FK_CONSTRAINT_VIOLATION'
57
57
  return
@@ -63,17 +63,33 @@ const _rewriteError = error => {
63
63
  }
64
64
  }
65
65
 
66
+ const _isInHttpResponseCodeRange = errorCode => errorCode >= 300 && errorCode <= 599
67
+
68
+ const BAD_REQUEST_ERRORS = new Set(['ENTITY_ALREADY_EXISTS', 'FK_CONSTRAINT_VIOLATION', 'UNIQUE_CONSTRAINT_VIOLATION'])
69
+
66
70
  const _normalize = (err, locale, formatterFn = _getFiltered) => {
67
71
  // REVISIT: code and message rewriting
68
72
  _rewriteError(err)
69
73
 
74
+ const { message: originalMessage } = err
75
+
70
76
  // message (i18n)
71
77
  err.message = getErrorMessage(err, locale)
72
78
 
73
79
  // ensure code is set and a string
74
80
  err.code = String(err.code || 'null')
75
81
 
76
- let statusCode = err.status || err.statusCode || (_isAllowedError(err.code) && err.code)
82
+ // determine status code from error
83
+ let statusCode = err.status || err.statusCode //> REVISIT: why prefer status over statusCode?
84
+ // well-defined bad request errors
85
+ if (!statusCode && BAD_REQUEST_ERRORS.has(originalMessage)) statusCode = 400
86
+ if (!statusCode && _isInHttpResponseCodeRange(err.code)) {
87
+ if ('sqlState' in err) {
88
+ // HANA/ database error -> don't use code as status code
89
+ } else {
90
+ statusCode = err.code
91
+ }
92
+ }
77
93
 
78
94
  // details
79
95
  if (err.details) {
@@ -95,8 +111,6 @@ const _normalize = (err, locale, formatterFn = _getFiltered) => {
95
111
  return { error, statusCode }
96
112
  }
97
113
 
98
- const _isAllowedError = errorCode => errorCode >= 300 && errorCode < 505
99
-
100
114
  // - for one unique value, we use it
101
115
  // - if at least one 5xx exists, we use 500
102
116
  // - else if at least one 4xx exists, we use 400