@sap/cds 9.3.1 → 9.4.2
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.
- package/CHANGELOG.md +54 -3
- package/_i18n/i18n_vi.properties +113 -0
- package/_i18n/messages.properties +106 -17
- package/_i18n/messages_ar.properties +194 -0
- package/_i18n/messages_bg.properties +194 -0
- package/_i18n/messages_cs.properties +194 -0
- package/_i18n/messages_da.properties +194 -0
- package/_i18n/messages_de.properties +194 -0
- package/_i18n/messages_el.properties +194 -0
- package/_i18n/messages_en.properties +194 -0
- package/_i18n/messages_en_US_saptrc.properties +194 -0
- package/_i18n/messages_es.properties +194 -0
- package/_i18n/messages_es_MX.properties +194 -0
- package/_i18n/messages_fi.properties +194 -0
- package/_i18n/messages_fr.properties +194 -0
- package/_i18n/messages_he.properties +194 -0
- package/_i18n/messages_hr.properties +194 -0
- package/_i18n/messages_hu.properties +194 -0
- package/_i18n/messages_it.properties +194 -0
- package/_i18n/messages_ja.properties +194 -0
- package/_i18n/messages_kk.properties +194 -0
- package/_i18n/messages_ko.properties +194 -0
- package/_i18n/messages_ms.properties +194 -0
- package/_i18n/messages_nl.properties +194 -0
- package/_i18n/messages_no.properties +194 -0
- package/_i18n/messages_pl.properties +194 -0
- package/_i18n/messages_pt.properties +194 -0
- package/_i18n/messages_ro.properties +194 -0
- package/_i18n/messages_ru.properties +194 -0
- package/_i18n/messages_sh.properties +194 -0
- package/_i18n/messages_sk.properties +194 -0
- package/_i18n/messages_sl.properties +194 -0
- package/_i18n/messages_sv.properties +194 -0
- package/_i18n/messages_th.properties +194 -0
- package/_i18n/messages_tr.properties +194 -0
- package/_i18n/messages_uk.properties +194 -0
- package/_i18n/messages_vi.properties +194 -0
- package/_i18n/messages_zh_CN.properties +194 -0
- package/_i18n/messages_zh_TW.properties +194 -0
- package/bin/serve.js +9 -1
- package/common.cds +9 -1
- package/lib/compile/cds-compile.js +1 -0
- package/lib/compile/etc/properties.js +1 -0
- package/lib/compile/for/flows.js +70 -4
- package/lib/compile/for/nodejs.js +1 -1
- package/lib/compile/minify.js +84 -56
- package/lib/compile/to/csn.js +2 -0
- package/lib/compile/to/yaml.js +1 -1
- package/lib/env/cds-requires.js +3 -0
- package/lib/i18n/bundles.js +8 -1
- package/lib/i18n/files.js +5 -1
- package/lib/i18n/index.js +1 -5
- package/lib/i18n/localize.js +4 -2
- package/lib/index.js +1 -1
- package/lib/ql/SELECT.js +16 -19
- package/lib/req/validate.js +10 -5
- package/lib/srv/bindings.js +1 -1
- package/lib/srv/cds-serve.js +1 -1
- package/lib/srv/middlewares/auth/ias-auth.js +3 -2
- package/lib/srv/middlewares/auth/jwt-auth.js +3 -2
- package/lib/srv/protocols/hcql.js +8 -6
- package/lib/srv/srv-dispatch.js +4 -8
- package/lib/srv/srv-handlers.js +28 -1
- package/lib/utils/colors.js +54 -49
- package/libx/_runtime/common/generic/flows.js +79 -12
- package/libx/_runtime/fiori/lean-draft.js +10 -2
- package/libx/_runtime/messaging/common-utils/connections.js +31 -18
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -2
- package/libx/_runtime/messaging/redis-messaging.js +1 -1
- package/libx/_runtime/ucl/Service.js +5 -5
- package/libx/http/body-parser.js +10 -1
- package/libx/odata/ODataAdapter.js +10 -7
- package/libx/odata/middleware/error.js +3 -0
- package/libx/odata/parse/afterburner.js +13 -16
- package/libx/odata/parse/multipartToJson.js +3 -1
- package/libx/rest/middleware/parse.js +1 -1
- package/package.json +1 -1
- package/server.js +1 -1
package/lib/compile/minify.js
CHANGED
|
@@ -1,64 +1,92 @@
|
|
|
1
|
-
|
|
2
|
-
const DEBUG = cds.debug('minify')
|
|
1
|
+
module.exports = exports = (m,o) => m.meta?.minified ? m : (new Minifier) .minify (m,o)
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
for (let
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
for (let e in
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
package/lib/compile/to/csn.js
CHANGED
|
@@ -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) {
|
package/lib/compile/to/yaml.js
CHANGED
|
@@ -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 || /^[
|
|
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
|
package/lib/env/cds-requires.js
CHANGED
package/lib/i18n/bundles.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
*/
|
package/lib/i18n/localize.js
CHANGED
|
@@ -73,17 +73,19 @@ exports.edmx = edmx => {
|
|
|
73
73
|
'<' : '<',
|
|
74
74
|
'>' : '>',
|
|
75
75
|
'&' : '&', // if not followed by amp; quot; lt; gt; apos; or #
|
|
76
|
+
'\t' : '	',
|
|
76
77
|
'\n' : '
',
|
|
78
|
+
'\f' : '',
|
|
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
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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) {
|
package/lib/req/validate.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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
|
}
|
package/lib/srv/bindings.js
CHANGED
|
@@ -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
|
|
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 = {
|
package/lib/srv/cds-serve.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
51
|
+
LOG.error('Error while authenticating user:', e)
|
|
51
52
|
return next(500)
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -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 (
|
|
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 (
|
|
114
|
-
|
|
115
|
+
if (typeof results === 'number') results = String (results)
|
|
116
|
+
res.set('Content-Type','application/json').send (results)
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
/**
|
package/lib/srv/srv-dispatch.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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))
|
package/lib/srv/srv-handlers.js
CHANGED
|
@@ -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'
|
|
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',
|
package/lib/utils/colors.js
CHANGED
|
@@ -1,51 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
module.exports = Object.assign (_colors4 (process.stdout), {
|
|
2
|
+
for: _colors4,
|
|
3
|
+
})
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
enabled
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
5
|
+
function _colors4 (stdout_or_stderr = process.stdout) {
|
|
6
|
+
const enabled = stdout_or_stderr.isTTY && !process.env.NO_COLOR || process.env.FORCE_COLOR
|
|
7
|
+
const color = enabled ? ttl => ttl[0] : ()=>''
|
|
8
|
+
return {
|
|
9
|
+
enabled,
|
|
10
|
+
RESET: color `\x1b[0m`,
|
|
11
|
+
BOLD: color `\x1b[1m`,
|
|
12
|
+
BRIGHT: color `\x1b[1m`,
|
|
13
|
+
DIMMED: color `\x1b[2m`,
|
|
14
|
+
ITALIC: color `\x1b[3m`,
|
|
15
|
+
UNDER: color `\x1b[4m`,
|
|
16
|
+
BLINK: color `\x1b[5m`,
|
|
17
|
+
FLASH: color `\x1b[6m`,
|
|
18
|
+
INVERT: color `\x1b[7m`,
|
|
19
|
+
BLACK: color `\x1b[30m`,
|
|
20
|
+
RED: color `\x1b[31m`,
|
|
21
|
+
GREEN: color `\x1b[32m`,
|
|
22
|
+
YELLOW: color `\x1b[33m`,
|
|
23
|
+
BLUE: color `\x1b[34m`,
|
|
24
|
+
PINK: color `\x1b[35m`,
|
|
25
|
+
CYAN: color `\x1b[36m`,
|
|
26
|
+
LIGHT_GRAY: color `\x1b[37m`,
|
|
27
|
+
DEFAULT: color `\x1b[39m`,
|
|
28
|
+
GRAY: color `\x1b[90m`,
|
|
29
|
+
LIGHT_RED: color `\x1b[91m`,
|
|
30
|
+
LIGHT_GREEN: color `\x1b[92m`,
|
|
31
|
+
LIGHT_YELLOW: color `\x1b[93m`,
|
|
32
|
+
LIGHT_BLUE: color `\x1b[94m`,
|
|
33
|
+
LIGHT_PINK: color `\x1b[95m`,
|
|
34
|
+
LIGHT_CYAN: color `\x1b[96m`,
|
|
35
|
+
WHITE: color `\x1b[97m`,
|
|
36
|
+
bg: {
|
|
37
|
+
BLACK: color `\x1b[40m`,
|
|
38
|
+
RED: color `\x1b[41m`,
|
|
39
|
+
GREEN: color `\x1b[42m`,
|
|
40
|
+
YELLOW: color `\x1b[43m`,
|
|
41
|
+
BLUE: color `\x1b[44m`,
|
|
42
|
+
PINK: color `\x1b[45m`,
|
|
43
|
+
CYAN: color `\x1b[46m`,
|
|
44
|
+
WHITE: color `\x1b[47m`,
|
|
45
|
+
DEFAULT: color `\x1b[49m`,
|
|
46
|
+
LIGHT_GRAY: color `\x1b[100m`,
|
|
47
|
+
LIGHT_RED: color `\x1b[101m`,
|
|
48
|
+
LIGHT_GREEN: color `\x1b[102m`,
|
|
49
|
+
LIGHT_YELLOW: color `\x1b[103m`,
|
|
50
|
+
LIGHT_BLUE: color `\x1b[104m`,
|
|
51
|
+
LIGHT_PINK: color `\x1b[105m`,
|
|
52
|
+
LIGHT_CYAN: color `\x1b[106m`,
|
|
53
|
+
LIGHT_WHITE: color `\x1b[107m`,
|
|
54
|
+
},
|
|
55
|
+
}
|
|
51
56
|
}
|