@sap/cds 9.3.1 → 9.4.3

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 (80) hide show
  1. package/CHANGELOG.md +63 -3
  2. package/_i18n/i18n_vi.properties +113 -0
  3. package/_i18n/messages.properties +106 -17
  4. package/_i18n/messages_ar.properties +194 -0
  5. package/_i18n/messages_bg.properties +194 -0
  6. package/_i18n/messages_cs.properties +194 -0
  7. package/_i18n/messages_da.properties +194 -0
  8. package/_i18n/messages_de.properties +194 -0
  9. package/_i18n/messages_el.properties +194 -0
  10. package/_i18n/messages_en.properties +194 -0
  11. package/_i18n/messages_en_US_saptrc.properties +194 -0
  12. package/_i18n/messages_es.properties +194 -0
  13. package/_i18n/messages_es_MX.properties +194 -0
  14. package/_i18n/messages_fi.properties +194 -0
  15. package/_i18n/messages_fr.properties +194 -0
  16. package/_i18n/messages_he.properties +194 -0
  17. package/_i18n/messages_hr.properties +194 -0
  18. package/_i18n/messages_hu.properties +194 -0
  19. package/_i18n/messages_it.properties +194 -0
  20. package/_i18n/messages_ja.properties +194 -0
  21. package/_i18n/messages_kk.properties +194 -0
  22. package/_i18n/messages_ko.properties +194 -0
  23. package/_i18n/messages_ms.properties +194 -0
  24. package/_i18n/messages_nl.properties +194 -0
  25. package/_i18n/messages_no.properties +194 -0
  26. package/_i18n/messages_pl.properties +194 -0
  27. package/_i18n/messages_pt.properties +194 -0
  28. package/_i18n/messages_ro.properties +194 -0
  29. package/_i18n/messages_ru.properties +194 -0
  30. package/_i18n/messages_sh.properties +194 -0
  31. package/_i18n/messages_sk.properties +194 -0
  32. package/_i18n/messages_sl.properties +194 -0
  33. package/_i18n/messages_sv.properties +194 -0
  34. package/_i18n/messages_th.properties +194 -0
  35. package/_i18n/messages_tr.properties +194 -0
  36. package/_i18n/messages_uk.properties +194 -0
  37. package/_i18n/messages_vi.properties +194 -0
  38. package/_i18n/messages_zh_CN.properties +194 -0
  39. package/_i18n/messages_zh_TW.properties +194 -0
  40. package/bin/serve.js +9 -1
  41. package/common.cds +9 -1
  42. package/lib/compile/cds-compile.js +1 -0
  43. package/lib/compile/etc/properties.js +1 -0
  44. package/lib/compile/for/flows.js +70 -4
  45. package/lib/compile/for/nodejs.js +1 -1
  46. package/lib/compile/minify.js +84 -56
  47. package/lib/compile/to/csn.js +2 -0
  48. package/lib/compile/to/yaml.js +1 -1
  49. package/lib/env/cds-requires.js +3 -0
  50. package/lib/i18n/bundles.js +8 -1
  51. package/lib/i18n/files.js +5 -1
  52. package/lib/i18n/index.js +1 -5
  53. package/lib/i18n/localize.js +4 -2
  54. package/lib/index.js +1 -1
  55. package/lib/ql/SELECT.js +16 -19
  56. package/lib/req/validate.js +10 -5
  57. package/lib/srv/bindings.js +1 -1
  58. package/lib/srv/cds-serve.js +1 -1
  59. package/lib/srv/middlewares/auth/ias-auth.js +3 -2
  60. package/lib/srv/middlewares/auth/jwt-auth.js +3 -2
  61. package/lib/srv/middlewares/errors.js +2 -1
  62. package/lib/srv/protocols/hcql.js +8 -6
  63. package/lib/srv/srv-dispatch.js +4 -8
  64. package/lib/srv/srv-handlers.js +28 -1
  65. package/lib/utils/colors.js +54 -49
  66. package/libx/_runtime/common/generic/flows.js +79 -12
  67. package/libx/_runtime/common/generic/input.js +12 -2
  68. package/libx/_runtime/fiori/lean-draft.js +10 -2
  69. package/libx/_runtime/messaging/common-utils/connections.js +31 -18
  70. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
  71. package/libx/_runtime/messaging/redis-messaging.js +1 -1
  72. package/libx/_runtime/ucl/Service.js +5 -5
  73. package/libx/http/body-parser.js +10 -1
  74. package/libx/odata/ODataAdapter.js +10 -7
  75. package/libx/odata/middleware/error.js +3 -0
  76. package/libx/odata/parse/afterburner.js +13 -16
  77. package/libx/odata/parse/multipartToJson.js +3 -1
  78. package/libx/rest/middleware/parse.js +1 -1
  79. package/package.json +2 -2
  80. package/server.js +1 -1
@@ -1,64 +1,92 @@
1
- const cds = require('../index')
2
- const DEBUG = cds.debug('minify')
1
+ module.exports = exports = (m,o) => m.meta?.minified ? m : (new Minifier) .minify (m,o)
3
2
 
4
- module.exports = function cds_minify (csn, roots = cds.env.features.skip_unused) {
5
- if (roots === false) return csn
6
- if ((csn.meta??={}).minified) return csn
7
- const all = csn.definitions, reached = new Set
8
- if (roots === 'services') {
9
- for (let n in all) if (all[n].kind === 'service') _visit_service(n)
10
- } else if (typeof roots === 'string') {
11
- _visit_service(roots)
12
- } else for (let n in all) {
13
- let d = all[n]
14
- if (d.kind === 'service') _visit_service(n)
15
- else if (d.kind === 'entity') {
16
- if (d['@cds.persistence.skip'] === 'if-unused') continue
17
- if (n.endsWith('.texts')) {
18
- let e = all[n.slice(0,-6)]
19
- if (e && e['@cds.persistence.skip'] === 'if-unused') continue
3
+ class Minifier {
4
+
5
+ /**
6
+ * Minifies a model by removing all definitions not reachable from given roots.
7
+ * @param {object} [options] - Options for the minification as follows...
8
+ * @param {kinds} [options.keep] Controls which children of services to keep.
9
+ * @param {string[]} [options.cleanse] Names of properties or annotations to be removed.
10
+ * @typedef {{ entity, aspect, type, event, action, function }} kinds
11
+ * @returns {CSN.Model} the minified model with kept definitions only.
12
+ */
13
+ minify (csn, options, { skip_unused } = global.cds.env.features) {
14
+
15
+ const o = this.options = options || {}
16
+ if (skip_unused === false) return csn
17
+ if (skip_unused === 'services') o.services = 'all'
18
+ const all = new Map (Object.entries( this.defs ??= csn.definitions || {} ))
19
+ const children = (n,fn,pre=n+'.') => { for (let [n,d] of all) if (n.startsWith(pre)) fn (n,d) }
20
+ const events = { event:1, action:1, function:1 }
21
+ const keep = o.keep ??= { entity:1, type:1, ...events }
22
+ const kept = this.kept = csn.definitions = {}
23
+ const skipped = this.skipped = {}
24
+
25
+ if (o.services) {
26
+ // If o.services is specified, only keep matching services and their children
27
+ const rx = o.services == 'all' || o.services == '/all/i' ? {test:()=>true} : o.services
28
+ for (let [s,d] of all) if (d.kind === 'service' && rx.test(s)) {
29
+ this.keep (s,d); children (s, (c,d) => this.keep (c,d))
30
+ }
31
+ } else {
32
+ // Otherwise first mark all external services and their children as initially skipped
33
+ for (let [s,d] of all) if (d.kind === 'service' && _skip_service(s,d)) {
34
+ skipped[s] = 0; children (s, (c,d) => d.kind in events ? this.keep (c,d) : skipped[c] = s) // used later on in this.keep()
35
+ }
36
+ // Then keep all own services and their children
37
+ for (let [s,d] of all) if (d.kind === 'service' && !(s in skipped)) {
38
+ this.keep (s,d); children (s, (c,d) => d.kind in keep ? this.keep (c,d) : skipped[c] = 0)
39
+ }
40
+ // Also keep remaining non-service entities
41
+ for (let [e,d] of all) if (d.kind === 'entity') {
42
+ e in kept || e in skipped || _skip_entity(e,d) || this.keep (e,d)
20
43
  }
21
- _visit(d)
22
44
  }
45
+
46
+ ;(csn.meta ??= {}) .minified = true
47
+ return csn
23
48
  }
24
- function _visit_service (service) {
25
- reached.add (all[service])
26
- for (let e in all) if (e.startsWith(service+'.')) _visit(all[e])
27
- }
28
- function _visit_query (q) {
29
- if (q.SELECT) return _visit_query (q.SELECT)
30
- if (q.SET) return q.SET.args.forEach (_visit_query)
31
- if (q.mixin) for (let e in q.mixin) _visit (q.mixin[e])
32
- if (q.from) {
33
- if (q.from.join) return q.from.args.forEach (_visit)
34
- else return _visit (q.from)
49
+
50
+ cleanse (d, o = this.options, keep = this._keep ??= Object.keys (o.keep)) {
51
+ for (let p in o.cleanse) {
52
+ if (p[0] !== '@') delete d[p] // a single property
53
+ for (let a in d) if (a.startsWith(p) && !keep.some(k => a.startsWith(k))) delete d[a]
35
54
  }
36
55
  }
37
- function _visit (d) {
38
- if (typeof d === 'string') {
39
- d = all[d]
40
- if (!d) return // builtins like cds.String
41
- } else if (d.ref) return d.ref.reduce((p,n) => {
42
- let d = (p.elements || all[p.target || p.type].elements)[n.id || n] // > n.id -> view with parameters
43
- if (d) _visit(d)
44
- return d
45
- },{elements:all})
46
- if (reached.has(d)) return; else reached.add(d)
47
- if (d.includes) d.includes.forEach(i => _visit(all[i])) // Note: with delete d.includes, redirects in AFC broke
48
- if (d.projection) _visit_query (d.projection)
49
- if (d.query) _visit_query (d.query)
50
- if (d.type) _visit (d.type)
51
- if (d.target) _visit (d.target)
52
- if (d.targetAspect) _visit (d.targetAspect)
53
- if (d.items) _visit (d.items)
54
- if (d.returns) _visit (d.returns)
55
- for (let e in d.elements) _visit (d.elements[e])
56
- for (let a in d.actions) _visit (d.actions[a])
57
- for (let p in d.params) _visit (d.params[p])
56
+
57
+ walk (d) { this.cleanse(d)
58
+ if (d.target) this.keep (d.target) // has to go first w/o return for redirected targets
59
+ if (d.type in this.defs) return this.keep (d.type) // return to avoid endless recursion
60
+ if (d.type?.ref) return this.keep (d.type.ref[0]) // return to avoid endless recursion
61
+ if (d.projection) this.view (d.projection)
62
+ if (d.query) this.view (d.query)
63
+ if (d.items) this.walk (d.items)
64
+ if (d.returns) this.walk (d.returns)
65
+ for (let e in d.elements) this.walk (d.elements[e])
66
+ for (let a in d.actions) this.walk (d.actions[a])
67
+ for (let p in d.params) this.walk (d.params[p])
68
+ for (let i in d.includes) this.keep (d.includes[i])
69
+ // Note: this ^^^^^^^^^^^^ is required for cdsc.recompile; with delete d.includes, redirects in AFC broke
70
+ }
71
+
72
+ view (q) {
73
+ if (q.SELECT) q = q.SELECT // i.e. entity as select from ...
74
+ if (q.mixin) for (let e in q.mixin) this.walk (q.mixin[e])
75
+ if (q.from?.ref) return this.keep (_source(q.from.ref[0])) // keep sources of views
76
+ if (q.from?.join) return q.from.args.forEach (from => this.view ({from}))
77
+ if (q.SET) return q.SET.args.forEach (q => this.view (q.SELECT||q))
78
+ function _source (r) { return r.id || r }
79
+ }
80
+
81
+ keep (n,d) {
82
+ if (n in this.kept) return; else d ??= this.defs[n]
83
+ if (d) this.walk (this.kept[n] = d, n); else return
84
+ let texts = this.defs[n+'.texts']; if (texts) this.keep (n+'.texts', texts)
85
+ let parent = this.skipped[n]; if (parent) this.keep(parent) // keep initially skipped services
58
86
  }
59
- const minified = csn, less = minified.definitions = {}
60
- for (let n in all) if (reached.has(all[n])) less[n] = all[n]
61
- else DEBUG?.('skipping', all[n].kind, n)
62
- ;(minified.meta??={}).minified = true
63
- return minified
87
+
64
88
  }
89
+
90
+ const _skip_service = (s,d) => d['@cds.external'] >= 2
91
+ const _skip_entity = (e,d) => d['@cds.external'] >= 2 || d['@cds.persistence.skip'] === 'if-unused' || e.endsWith('.texts')
92
+ exports.Minifier = Minifier
@@ -26,6 +26,8 @@ function cds_compile_to_csn (model, options, _flavor) {
26
26
  else return _finalize (cdsc.compileSources(model,o)) //> compile CDL sources
27
27
 
28
28
  function _finalize (csn) {
29
+ // REVISIT: experimental implementation to automatically add aspect FlowHistory
30
+ if (cds.env.features.history_for_flows) cds.compile.for.flows(csn)
29
31
  if (o.min) csn = cds.minify(csn)
30
32
  // REVISIT: experimental implementation to detect external APIs
31
33
  for (let each in csn.definitions) {
@@ -29,7 +29,7 @@ module.exports = function _2yaml (object, {limit=111}={}) {
29
29
  if (typeof o === 'string') {
30
30
  if (o.indexOf('\n')>=0) return '|'+'\n'+indent+ o.replace(/\n/g,'\n'+indent)
31
31
  let s = o.trim()
32
- return !s || /^[\^@#:,=!<>*|]/.test(s) || /:\s/.test(o) ? '"'+ o.replace(/\\/g,'\\\\') +'"' : s
32
+ return !s || /^[\^@#:,=!<>*+-/|]/.test(s) || /:\s/.test(o) ? '"'+ o.replace(/\\/g,'\\\\') +'"' : s
33
33
  }
34
34
  if (typeof o === 'function') return
35
35
  else return o
@@ -276,3 +276,6 @@ exports.kinds = {
276
276
  ..._messaging,
277
277
  ..._platform_services,
278
278
  }
279
+
280
+ // This is to hide that method in the output of cds env
281
+ Object.defineProperty(exports,'_resolved',{value:exports._resolved,enumerable:false}) // hide it in outputs
@@ -31,7 +31,14 @@ class I18nBundle {
31
31
  at (key, locale, args) {
32
32
  if (typeof locale !== 'string') [ args, locale ] = [ locale, cds.context?.locale ?? i18n.default_language ]
33
33
  if (typeof key === 'object') key = this.key4(key)
34
- let t = this.texts4 (locale) [key]
34
+
35
+ let t
36
+ // fallback for legacy assert message
37
+ if (key === 'ASSERT_MANDATORY') {
38
+ t = this.texts4 (locale) ['ASSERT_NOT_NULL']
39
+ if (!t) t = this.texts4 (locale) [key]
40
+ } else t = this.texts4 (locale) [key]
41
+
35
42
  if (t && args) t = t.replace (/{(\w+)}/g, (_,k) => args[k])
36
43
  return t
37
44
  }
package/lib/i18n/files.js CHANGED
@@ -30,10 +30,14 @@ class I18nFiles {
30
30
  const _folders = I18nFiles.folders ??= {}
31
31
  const _entries = I18nFiles.entries ??= {}
32
32
 
33
+ // ensure we always load factory defaults for messages at very first
34
+ if (basename === 'messages') _add_entries4 (path.resolve (__dirname,'../../_i18n'))
35
+
33
36
  // fetch relatively specified i18n.folders in the neighborhood of sources...
34
37
  const relative_folders = folders.filter (f => f[0] !== '/')
35
38
  if (relative_folders.length) {
36
- const leafs = model?.$sources.map(path.dirname) ?? roots, visited = {}
39
+ const outbox_cds = cds.env.requires.queue?.model // ignore outbox.cds if present in model.$sources
40
+ const leafs = model?.$sources.filter(f => !f.startsWith(outbox_cds)).map(path.dirname) ?? roots, visited = {}
37
41
  ;[...new Set(leafs)].reverse() .forEach (function _visit (dir) {
38
42
  if (dir in visited) return; else visited[dir] = true
39
43
  LOG.debug ('fetching', basename, 'bundles in', dir, relative_folders)
package/lib/i18n/index.js CHANGED
@@ -16,13 +16,9 @@ class I18nFacade {
16
16
  * The default bundle for runtime messages.
17
17
  */
18
18
  get messages() {
19
- // ensure we always find our factory defaults as fallback
20
- const factory_defaults = cds.utils.path.resolve (__dirname,'../../_i18n')
21
- const folders = [ ...cds.env.i18n.folders, factory_defaults ]
22
- return super.messages = this.bundle4 ('messages', { folders })
19
+ return super.messages = this.bundle4 ('messages')
23
20
  }
24
21
 
25
-
26
22
  /**
27
23
  * The default bundle for UI labels and texts.
28
24
  */
@@ -73,17 +73,19 @@ exports.edmx = edmx => {
73
73
  '<' : '&lt;',
74
74
  '>' : '&gt;',
75
75
  '&' : '&amp;', // if not followed by amp; quot; lt; gt; apos; or #
76
+ '\t' : '&#x9;',
76
77
  '\n' : '&#xa;',
78
+ '\f' : '&#xc;',
77
79
  '\r' : '',
78
80
  }
79
- const _2b_escaped = /["<>\n\r]|&(?!quot;|amp;|lt;|gt;|apos;|#)/g
81
+ const _2b_escaped = /["<>\t\n\f\r]|&(?!quot;|amp;|lt;|gt;|apos;|#)/g
80
82
  const _xml_replacer = s => s?.replace (_2b_escaped, m => _xml_escapes[m])
81
83
  return localize (edmx) .using (_xml_replacer)
82
84
  }
83
85
 
84
86
  exports.json = json => {
85
87
  if (typeof json === 'object') json = JSON.stringify(json)
86
- const _json_replacer = s => s?.replace(/"/g, '\\"')
88
+ const _json_replacer = s => s && JSON.stringify(s).slice(1,-1)
87
89
  return localize(json) .using (_json_replacer)
88
90
  }
89
91
 
package/lib/index.js CHANGED
@@ -82,7 +82,7 @@ const cds = exports = module.exports = global.cds = new class cds extends EventE
82
82
  get unqueued() { return super.unqueued = require('../libx/queue/index.js').unqueued }
83
83
  get middlewares() { return super.middlewares = require('./srv/middlewares/index.js') }
84
84
  get odata() { return super.odata = require('../libx/odata/index.js') }
85
- get auth() { return super.auth = require('./auth.js') }
85
+ get auth() { return super.auth = require('./srv/middlewares/auth/index.js') }
86
86
  shutdown() { this.app?.server && process.exit() } // is overridden in bin/serve.js
87
87
 
88
88
  // Core Services API
package/lib/ql/SELECT.js CHANGED
@@ -171,27 +171,24 @@ class SELECT extends Whereable {
171
171
  }
172
172
  }
173
173
 
174
+ /**
175
+ * Convenience for streaming individual rows with async iteration. @example
176
+ * for await (let row of await query.stream(true)) ...
177
+ * for await (let row of query) ... // convenience by this method
178
+ */
174
179
  [Symbol.asyncIterator]() {
175
- return (async function* (self) {
176
- const srv = self._srv || cds.db || cds.error`Can't execute query as no primary database is connected.`
177
- const stream = await srv.send({
178
- iterator: true,
179
- objectMode: true,
180
- query: self,
181
- })
182
- for await (const row of stream) yield row
183
- })(this)
184
- }
185
-
186
- async pipeline(...args) {
180
+ const res = this.stream (true)
181
+ return async function* () { yield* await res }()
182
+ }
183
+
184
+ pipeline (...args_or_inserts) { if (!args_or_inserts.length) return this.stream() //> 4 compat only
185
+ const args = args_or_inserts.map (a => a instanceof Query ? s => a.entries(s) : a)
186
+ return this.stream() .then (stream => pipeline (stream, ...args))
187
+ }
188
+
189
+ stream (objectMode = false) {
187
190
  const srv = this._srv || cds.db || cds.error`Can't execute query as no primary database is connected.`
188
- const res = await srv.send({
189
- iterator: true,
190
- objectMode: false,
191
- query: this,
192
- })
193
- if (args.length) return pipeline(res, ...args.map(a => a instanceof Query ? stream => a.entries(stream) : a))
194
- return res
191
+ return srv.send ({ query:this, iterator:true, objectMode })
195
192
  }
196
193
 
197
194
  hints (...args) {
@@ -10,6 +10,8 @@ const conf = module.exports = exports = function validate (data, target, options
10
10
  return vc.errors
11
11
  }
12
12
 
13
+ // remove compat with cds^10
14
+ const ASSERT_MANDATORY = cds.env.features.compat_assert_not_null ? 'ASSERT_NOT_NULL' : 'ASSERT_MANDATORY'
13
15
 
14
16
  /** Instances represent single validations and are mainly used to record errors during validation. */
15
17
  class Validation {
@@ -102,11 +104,11 @@ const $any = class any {
102
104
  asserts.push ((v,p,ctx) => v == null || type_check(v) || ctx.error ('ASSERT_DATA_TYPE', p, this.name, null, v, this ))
103
105
  }
104
106
  if (this._is_mandatory()) {
105
- asserts.push ((v,p,ctx) => v != null && v.trim?.() !== '' || ctx.error ('ASSERT_NOT_NULL', p, this.name, this['@mandatory.message'] || this['@mandatory'], v)) // ASSERT_NOT_NULL is misleading -> should be ASSERT_REQUIRED
107
+ asserts.push ((v,p,ctx) => v != null && v.trim?.() !== '' || ctx.error (ASSERT_MANDATORY, p, this.name, this['@mandatory.message'] || this['@mandatory'], v))
106
108
  }
107
109
  if (this['@assert.format']) {
108
110
  const format = new RegExp(this['@assert.format'],'u')
109
- asserts.push ((v,p,ctx) => v == null || format.test(v) || ctx.error ('ASSERT_FORMAT', p, this.name, this['@assert.format.message'], v, format))
111
+ asserts.push ((v,p,ctx) => v == null || format.test(v) || ctx.error ('ASSERT_FORMAT', p, this.name, this['@assert.format.message'], v, this['@assert.format']))
110
112
  }
111
113
  if (this['@assert.range'] && !this.enum) {
112
114
  const [ min, max ] = this['@assert.range']
@@ -161,6 +163,10 @@ const $any = class any {
161
163
  })
162
164
  }
163
165
 
166
+ _is_immutable (d=this) {
167
+ return d['@insertonly'] || d['@Core.Immutable']
168
+ }
169
+
164
170
  /**
165
171
  * Checks if a nested row of a deep update is in turn to be inserted or updated.
166
172
  * This is the case if the row date does not contain all primary key elements of the target entity.
@@ -201,15 +207,14 @@ class struct extends $any {
201
207
  if (each.$struct in data) continue // got struct for flattened element/fk, e.g. {author:{ID:1}}
202
208
  if ((each.elements && each.kind !== 'param' ) || each.foreignKeys) continue // skip struct-likes as we check flat payloads above, and deep payloads via struct.validate(), parameters don't have flat elements
203
209
  if (each.isAssociation) continue // unmanaged associations are always ignored (no value like)
204
- else ctx.error ('ASSERT_NOT_NULL', path_, each.name) // ASSERT_NOT_NULL should be ASSERT_REQUIRED
210
+ else ctx.error (ASSERT_MANDATORY, path_, each.name)
205
211
  }
206
212
  // check values of given data
207
213
  for (let each in data) { // will work for structured payloads as well as flattened ones with universal CSN
208
214
  let /** @type {$any} */ d = Object.hasOwn(elements, each) && elements[each]
209
215
  if (!d || (d['@cds.api.ignore'] && ctx.rejectIgnore)) ctx.unknown (each, this, data)
210
216
  else if (ctx.cleanse && d._is_readonly() && !d.key) delete data[each]
211
- // @Core.Immutable processed only for root, children are handled when knowing db state
212
- else if (ctx.cleanse && d['@Core.Immutable'] && !ctx.insert && !path) delete data[each]
217
+ else if (ctx.cleanse && d._is_immutable() && !ctx.insert && !path) delete data[each] // @Core.Immutable processed only for root, children are handled when knowing db state
213
218
  else if (d['@cds.validate'] !== false) d.validate (data[each], path_, ctx)
214
219
  }
215
220
  }
@@ -22,7 +22,7 @@ class Bindings {
22
22
  let binding = this.provides [required?.service || service]
23
23
  if (binding?.endpoints) {
24
24
  const server = this.servers [binding.server]
25
- const kind = [ required.kind, 'hcql', 'rest', 'odata' ].find (k => k in binding.endpoints)
25
+ const kind = [ required?.kind, 'hcql', 'rest', 'odata' ].find (k => k in binding.endpoints)
26
26
  const path = binding.endpoints [kind]
27
27
  // in case of cds.requires.Foo = { ... }
28
28
  if (typeof required === 'object') required.credentials = {
@@ -51,7 +51,7 @@ async function _construct (services, o) {
51
51
  return [ await init (new services (services.name, cds.model, o)) ]
52
52
 
53
53
  // Resolve/load the model to use subsequently
54
- const csn = !o.from || o.from === '*' ? cds.model || await cds.load('*')
54
+ const csn = !o.from || o.from === '*' || o.from === 'all' ? cds.model || await cds.load('*')
55
55
  : is_csn(o.from) ? o.from : await cds.load (o.from, { silent: true })
56
56
  const m = cds.compile.for.nodejs (csn)
57
57
 
@@ -82,10 +82,11 @@ module.exports = function ias_auth(config) {
82
82
  })
83
83
  } catch (e) {
84
84
  if (e instanceof ValidationError) {
85
- LOG.warn('Unauthenticated request: ', e)
85
+ if (e.token?.payload) e.token_payload = e.token.payload
86
+ LOG.warn('Unauthenticated request:', e)
86
87
  return next(401)
87
88
  }
88
- LOG.error('Error while authenticating user: ', e)
89
+ LOG.error('Error while authenticating user:', e)
89
90
  return next(500)
90
91
  }
91
92
 
@@ -44,10 +44,11 @@ module.exports = function jwt_auth(config) {
44
44
  })
45
45
  } catch (e) {
46
46
  if (e instanceof ValidationError) {
47
- LOG.warn('Unauthenticated request: ', e)
47
+ if (e.token?.payload) e.token_payload = e.token.payload
48
+ LOG.warn('Unauthenticated request:', e)
48
49
  return next(401)
49
50
  }
50
- LOG.error('Error while authenticating user: ', e)
51
+ LOG.error('Error while authenticating user:', e)
51
52
  return next(500)
52
53
  }
53
54
 
@@ -2,13 +2,14 @@ const cds = require('../..'), {i18n} = cds
2
2
  const LOG = cds.log('error')
3
3
  const internals = /\n +at .*(?:node_modules\/express|node:).*/gm
4
4
  const is_test = typeof global.it === 'function'
5
+ const { STATUS_CODES } = require('http')
5
6
 
6
7
  module.exports = () => {
7
8
  /** @param {import('express').Response} res */
8
9
  return function http_error (err, req, res, next) { // eslint-disable-line no-unused-vars
9
10
 
10
11
  // In case of 401 require login if available by auth strategy
11
- if (typeof err === 'number') err = { code: err }
12
+ if (typeof err === 'number') err = { status: err, code: String(err), message: STATUS_CODES[err] }
12
13
  if (err.code == 401 && req._login) return req._login()
13
14
 
14
15
  // Shutdown on uncaught errors, which could be critical programming errors
@@ -27,7 +27,7 @@ class HCQLAdapter extends require('./http') {
27
27
 
28
28
  // Route for REST-style convenience shortcuts with queries in URL + body ...
29
29
  const $ = cb => (req,_,next) => { req.body = cb(req.params,req); next() }
30
- PROD || router.route('/:entity/:id?')
30
+ PROD || router.route(express.application.del ? '/:entity/:id?' : '/:entity{/:id}')
31
31
  .get ($(({entity,id,tail}, req) => {
32
32
  if (entity.includes(' ')) [,entity,tail] = /^(\w+)( .*)?/.exec(entity)
33
33
  if (id?.includes(' ')) [,id,tail] = /^(\w+)( .*)/.exec(id)
@@ -74,7 +74,7 @@ class HCQLAdapter extends require('./http') {
74
74
  * which is expected to be a plain CQN object or a CQL string.
75
75
  */
76
76
  query4 (/** @type express.Request */ req) {
77
- let q = req.body = cds.ql(req.body) || this.error (400, 'Invalid query', { query: req.body })
77
+ let q = req.body = cds.ql(req.body ?? {}) || this.error (400, 'Invalid query', { query: req.body })
78
78
  // handle request headers
79
79
  if (q.SELECT) {
80
80
  if (req.get('Accept-Language')) q.SELECT.localized = true
@@ -99,6 +99,7 @@ class HCQLAdapter extends require('./http') {
99
99
  valid (query) {
100
100
  if (!this.service.definition) return query
101
101
  let target = cds.infer.target (query, this.service)
102
+ if (!target) throw this.error (400, 'Cannot determine target entity of query.')
102
103
  if (target._unresolved) throw this.error (400, `${target.name} is not an entity served by '${this.service.name}'.`, { query })
103
104
  return query
104
105
  }
@@ -106,12 +107,13 @@ class HCQLAdapter extends require('./http') {
106
107
  /**
107
108
  * Serialize the results into response.
108
109
  */
109
- reply (results, /** @type express.Response */ res) {
110
- if (!results) return res.end()
110
+ reply (results, /** @type express.Response */ res, q = res.req.body) {
111
+ if (q.INSERT) res.statusCode = 201
112
+ if (results == null) return res.sendStatus(204)
111
113
  if (results.$count) res.set ('X-Total-Count', results.$count)
112
114
  if (typeof results === 'object') return res.json (results)
113
- if (res.req.method === 'DELETE') return res.sendStatus(204)
114
- else res.send (results)
115
+ if (typeof results === 'number') results = String (results)
116
+ res.set('Content-Type','application/json').send (results)
115
117
  }
116
118
 
117
119
  /**
@@ -40,17 +40,13 @@ exports.handle = async function handle (req) {
40
40
 
41
41
  // ._initial handlers run in sequence
42
42
  handlers = this.handlers._initial.filter (h => h.for(req))
43
- if (handlers.length) {
44
- for (const each of handlers) await each.handler.call (this,req)
45
- if (req.errors) throw req.reject()
46
- }
43
+ if (handlers.length) for (const each of handlers) await each.handler.call (this,req)
44
+ // no reject after _initial phase
47
45
 
48
46
  // .before handlers run in parallel
49
47
  handlers = this.handlers.before.filter (h => h.for(req))
50
- if (handlers.length) {
51
- await Promise.all (handlers.map (each => each.handler.call (this,req)))
52
- if (req.errors) throw req.reject()
53
- }
48
+ if (handlers.length) await Promise.all (handlers.map (each => each.handler.call (this,req)))
49
+ if (req.errors) throw req.reject()
54
50
 
55
51
  // .on handlers run in parallel for async events, and as interceptors stack for sync requests
56
52
  handlers = this.handlers.on.filter (h => h.for(req))
@@ -50,10 +50,36 @@ class EventHandlers {
50
50
  for (let each of event) this.register (srv, phase, each, path, handler)
51
51
  return srv
52
52
  }
53
- else if (event === 'SAVE' || event === 'WRITE') {
53
+ else if (event === 'SAVE') { //> special handling for SAVE
54
+ // REVISIT: remove compat with cds^10
55
+ if (cds.env.features?.compat_save_drafts) {
56
+ // no special handling for SAVE
57
+ } else {
58
+ if (is_array(path)) {
59
+ for (let each of path) this.register (srv, phase, event, each, handler)
60
+ return srv
61
+ }
62
+ if ((path.name || path).endsWith('.drafts')) {
63
+ const h = handler
64
+ path = typeof path === 'object' ? (path.actives || path) : path.slice(0, -7)
65
+ handler =
66
+ phase === 'before' ? function(req) { return is_activate(req) ? h.call(this,req) : null } :
67
+ phase === 'after' ? function(res,req) { return is_activate(req) ? h.call(this,res,req) : null } :
68
+ phase === 'on' ? function(req,next) { return is_activate(req) ? h.call(this,req,next) : next() } :
69
+ cds.error `SAVE not supported in ${phase} handlers`
70
+ }
71
+ }
54
72
  for (let each of ['CREATE','UPSERT','UPDATE']) this.register (srv, phase, each, path, handler)
55
73
  return srv
56
74
  }
75
+ else if (event === 'WRITE') {
76
+ for (let each of ['CREATE','UPSERT','UPDATE']) this.register (srv, phase, each, path, handler)
77
+ return srv
78
+ }
79
+ else if (event === 'DISCARD') { //> REVISIT: how solve in lean draft impl?
80
+ this.register (srv, phase, 'CANCEL', path, handler)
81
+ return srv
82
+ }
57
83
  else if (phase === 'after' && ( event === 'each' //> srv.after ('each', Book, b => ...) // event 'each' => READ each
58
84
  || event === 'READ' && path?.is_singular //> srv.after ('READ', Book, b => ...) // Book is a singular def from cds-typer
59
85
  || event === 'READ' && /^\(?each\b/.test(handler) //> srv.after ('READ', Book, each => ...) // handler's first param is named 'each'
@@ -109,6 +135,7 @@ class EventHandler {
109
135
 
110
136
 
111
137
  const is_array = Array.isArray
138
+ const is_activate = req => req._?.event === 'draftActivate'
112
139
  const events = {
113
140
  SELECT: 'READ',
114
141
  GET: 'READ',