@sap/cds 8.5.0 → 8.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -3
- package/_i18n/i18n.properties +4 -7
- package/eslint.config.mjs +1 -1
- package/lib/compile/etc/properties.js +2 -2
- package/lib/compile/for/java.js +15 -3
- package/lib/compile/for/lean_drafts.js +44 -34
- package/lib/compile/for/nodejs.js +19 -10
- package/lib/compile/minify.js +2 -4
- package/lib/compile/parse.js +106 -72
- package/lib/compile/to/edm.js +19 -9
- package/lib/compile/to/hana.js +25 -21
- package/lib/compile/to/sql.js +15 -8
- package/lib/core/linked-csn.js +10 -4
- package/lib/dbs/cds-deploy.js +2 -2
- package/lib/env/cds-env.js +76 -66
- package/lib/env/defaults.js +1 -0
- package/lib/i18n/bundles.js +2 -1
- package/lib/i18n/localize.js +2 -2
- package/lib/index.js +24 -18
- package/lib/ql/CREATE.js +11 -6
- package/lib/ql/DELETE.js +12 -9
- package/lib/ql/DROP.js +15 -8
- package/lib/ql/INSERT.js +19 -14
- package/lib/ql/SELECT.js +95 -168
- package/lib/ql/UPDATE.js +23 -14
- package/lib/ql/UPSERT.js +15 -2
- package/lib/ql/Whereable.js +44 -118
- package/lib/ql/cds-ql.js +222 -28
- package/lib/ql/{Query.js → cds.ql-Query.js} +52 -41
- package/lib/ql/cds.ql-predicates.js +133 -0
- package/lib/ql/cds.ql-projections.js +111 -0
- package/lib/ql/cqn.d.ts +146 -0
- package/lib/srv/cds-connect.js +3 -3
- package/lib/srv/cds-serve.js +2 -2
- package/lib/srv/cds.Service.js +132 -0
- package/lib/srv/{srv-api.js → cds.ServiceClient.js} +16 -71
- package/lib/srv/cds.ServiceProvider.js +20 -0
- package/lib/srv/factory.js +20 -8
- package/lib/srv/protocols/hcql.js +2 -3
- package/lib/srv/protocols/index.js +3 -3
- package/lib/srv/srv-dispatch.js +7 -6
- package/lib/srv/srv-handlers.js +103 -113
- package/lib/srv/srv-methods.js +14 -14
- package/lib/srv/srv-tx.js +5 -3
- package/lib/utils/cds-utils.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +3 -3
- package/libx/_runtime/cds.js +2 -1
- package/libx/_runtime/common/aspects/service.js +25 -0
- package/libx/_runtime/common/generic/auth/index.js +5 -0
- package/libx/_runtime/common/generic/auth/restrict.js +36 -14
- package/libx/_runtime/common/generic/auth/service.js +24 -0
- package/libx/_runtime/common/generic/auth/utils.js +14 -6
- package/libx/_runtime/common/generic/etag.js +1 -1
- package/libx/_runtime/common/utils/cqn.js +1 -2
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/generateOnCond.js +7 -3
- package/libx/_runtime/common/utils/postProcess.js +4 -1
- package/libx/_runtime/common/utils/restrictions.js +1 -0
- package/libx/_runtime/fiori/lean-draft.js +53 -42
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -1
- package/libx/_runtime/remote/Service.js +2 -0
- package/libx/_runtime/remote/utils/client.js +12 -0
- package/libx/odata/ODataAdapter.js +2 -1
- package/libx/odata/index.js +5 -3
- package/libx/odata/middleware/batch.js +4 -0
- package/libx/odata/middleware/create.js +2 -2
- package/libx/odata/middleware/delete.js +2 -2
- package/libx/odata/middleware/operation.js +2 -2
- package/libx/odata/middleware/read.js +14 -12
- package/libx/odata/middleware/service-document.js +16 -8
- package/libx/odata/middleware/update.js +2 -2
- package/libx/odata/parse/afterburner.js +64 -30
- package/libx/odata/parse/grammar.peggy +95 -0
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +6 -1
- package/libx/odata/utils/metadata.js +69 -75
- package/libx/odata/utils/postProcess.js +24 -3
- package/package.json +1 -1
- package/server.js +1 -1
- package/lib/ql/parse.js +0 -36
- /package/lib/ql/{infer.js → cds.ql-infer.js} +0 -0
package/lib/srv/srv-handlers.js
CHANGED
|
@@ -1,136 +1,131 @@
|
|
|
1
|
+
/** @typedef {import('./cds.Service')} Service } */
|
|
2
|
+
|
|
1
3
|
const cds = require('..'), {expected} = cds.error
|
|
2
4
|
const LOG = cds.log()
|
|
3
5
|
|
|
4
6
|
class EventHandlers {
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/** @type {EventHandler[]} */ after:[],
|
|
12
|
-
/** @type {EventHandler[]} */ _error:[]
|
|
13
|
-
}
|
|
14
|
-
this.name = name
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
before (...args) { return _register (this, 'before', ...args) }
|
|
18
|
-
on (...args) { return _register (this, 'on', ...args) }
|
|
19
|
-
after (...args) { return _register (this, 'after', ...args) }
|
|
20
|
-
reject (e, path) { return _register (this, '_initial', e, path,
|
|
21
|
-
(r) => r.reject (405, `Event "${r.event}" not allowed for entity "${r.path}".`)
|
|
22
|
-
)}
|
|
8
|
+
/** @type {EventHandler[]} */ _initial = []
|
|
9
|
+
/** @type {EventHandler[]} */ before = []
|
|
10
|
+
/** @type {EventHandler[]} */ on = []
|
|
11
|
+
/** @type {EventHandler[]} */ after = []
|
|
12
|
+
/** @type {EventHandler[]} */ _error = []
|
|
23
13
|
|
|
14
|
+
/** @this {Service} */
|
|
24
15
|
prepend (fn) {
|
|
25
|
-
const {
|
|
16
|
+
const {handlers} = this, _new = this.handlers = new EventHandlers
|
|
26
17
|
const x = fn.call (this,this) // NOTE: we need the doubled await to compensate usages of srv.prepend() with missing awaits !!!
|
|
27
18
|
if (x?.then) throw cds.error `srv.prepend() doesn't accept asynchronous functions anymore`
|
|
28
|
-
for (let each in _new) if (_new[each].length)
|
|
29
|
-
this.
|
|
19
|
+
for (let each in _new) if (_new[each].length) handlers[each].unshift(..._new[each])
|
|
20
|
+
this.handlers = handlers
|
|
30
21
|
return this
|
|
31
22
|
}
|
|
32
23
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
24
|
+
//--------------------------------------------------------------------------
|
|
25
|
+
/** Registers event handlers. This is the central method to register handlers,
|
|
26
|
+
* used by all respective public API methods, i.e. .on/before/after/reject.
|
|
27
|
+
* @param {Service} srv
|
|
28
|
+
* @param {'on'|'before'|'after'} phase
|
|
29
|
+
* @param {string|string[]} event
|
|
30
|
+
* @param {string|string[]} path
|
|
31
|
+
* @param {(req)=>{}} handler
|
|
32
|
+
*/
|
|
33
|
+
register (srv, phase, event, path, handler) {
|
|
34
|
+
|
|
35
|
+
if (!handler) [ handler, path ] = [ path, '*' ] // argument path is optional
|
|
36
|
+
else if (path === undefined) expected `${{path}} to be a string or csn definition`
|
|
37
|
+
if (typeof handler !== 'function') expected `${{handler}} to be a function`
|
|
38
|
+
if (handler._is_stub) {
|
|
39
|
+
LOG.warn (`\n
|
|
40
|
+
WARNING: You are trying to register a frameworks-generated stub method for
|
|
41
|
+
custom action/function '${event}' in implementation of service '${srv.name}'.
|
|
42
|
+
We're ignoring that as we already registered the according handler.
|
|
43
|
+
Please fix your implementation, i.e., just don't register that handler.
|
|
44
|
+
`)
|
|
45
|
+
return srv
|
|
46
|
+
}
|
|
43
47
|
|
|
48
|
+
// Canonicalize event argument
|
|
49
|
+
if (!event || event === '*') event = undefined
|
|
50
|
+
else if (is_array(event)) {
|
|
51
|
+
for (let each of event) this.register (srv, phase, each, path, handler)
|
|
52
|
+
return srv
|
|
53
|
+
}
|
|
54
|
+
else if (event === 'SAVE' || event === 'WRITE') {
|
|
55
|
+
for (let each of ['CREATE','UPSERT','UPDATE']) this.register (srv, phase, each, path, handler)
|
|
56
|
+
return srv
|
|
57
|
+
}
|
|
58
|
+
else if (phase === 'after' && ( event === 'each' //> srv.after ('each', Book, b => ...) // event 'each' => READ each
|
|
59
|
+
|| event === 'READ' && path?.is_singular //> srv.after ('READ', Book, b => ...) // Book is a singular def from cds-typer
|
|
60
|
+
|| event === 'READ' && /^\(?each\b/.test(handler) //> srv.after ('READ', Book, each => ...) // handler's first param is named 'each'
|
|
61
|
+
)) {
|
|
62
|
+
event = 'READ' // override event='each' to 'READ'
|
|
63
|
+
const h=handler; handler = (rows,req) => is_array(rows) ? rows.forEach (r => h(r,req)) : rows && h(rows,req)
|
|
64
|
+
}
|
|
65
|
+
else if (typeof event === 'object') {
|
|
66
|
+
// extract action name from an action definition's fqn
|
|
67
|
+
event = event.name && /[^.]+$/.exec(event.name)[0] || expected `${{event}} to be a string or an action's CSN definition`
|
|
68
|
+
}
|
|
69
|
+
else event = AlternativeEvents[event] || event
|
|
44
70
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (handler._is_stub) {
|
|
58
|
-
LOG.warn (`\n
|
|
59
|
-
WARNING: You are trying to register a frameworks-generated stub method for
|
|
60
|
-
custom action/function '${event}' in implementation of service '${srv.name}'.
|
|
61
|
-
We're ignoring that as we already registered the according handler.
|
|
62
|
-
Please fix your implementation, i.e., just don't register that handler.
|
|
63
|
-
`)
|
|
64
|
-
return srv
|
|
65
|
-
}
|
|
71
|
+
// Canonicalize path argument
|
|
72
|
+
if (!path || path === '*') path = undefined
|
|
73
|
+
else if (is_array(path)) {
|
|
74
|
+
for (let each of path) this.register (srv, phase, event, each, handler)
|
|
75
|
+
return srv
|
|
76
|
+
}
|
|
77
|
+
else if (typeof path === 'object') {
|
|
78
|
+
path = path.name || expected `${{path}} to be a string or an entity's CSN definition`
|
|
79
|
+
}
|
|
80
|
+
else if (typeof path === 'string') {
|
|
81
|
+
if (!path.startsWith(srv.name+'.')) path = `${srv.name}.${path}`
|
|
82
|
+
}
|
|
66
83
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|| event === 'READ' && /^\(?each\b/.test(handler) //> srv.after ('READ', Book, each => ...) // handler's first param is named 'each'
|
|
80
|
-
)) {
|
|
81
|
-
event = 'READ' // override event='each' to 'READ'
|
|
82
|
-
const h=handler; handler = (rows,req) => is_array(rows) ? rows.forEach (r => h(r,req)) : rows && h(rows,req)
|
|
83
|
-
}
|
|
84
|
-
else if (typeof event === 'object') {
|
|
85
|
-
// extract action name from an action definition's fqn
|
|
86
|
-
event = event.name && /[^.]+$/.exec(event.name)[0] || expected `${{event}} to be a string or an action's CSN definition`
|
|
87
|
-
}
|
|
88
|
-
else event = AlternativeEvents[event] || event
|
|
84
|
+
if (cds.env.fiori.draft_compat) {
|
|
85
|
+
const entity = path && srv.model?.definitions[path.name || path]
|
|
86
|
+
if (['PATCH', 'CANCEL', 'NEW'].includes(event)) {
|
|
87
|
+
// delegate to drafts
|
|
88
|
+
path = typeof path === 'string' && path !== '*' && !path.endsWith('.drafts') ? path + '.drafts' : typeof path === 'object' && path.drafts || path
|
|
89
|
+
if (event === 'PATCH') event = 'UPDATE'
|
|
90
|
+
}
|
|
91
|
+
else if (entity && (event === 'READ' || entity.actions?.[event]) && (entity.drafts && !entity.name.endsWith('.drafts'))) {
|
|
92
|
+
// additionally add drafts for READ and bound actions/functions
|
|
93
|
+
this.register (srv, phase, event, entity.name + '.drafts', handler)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
89
96
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
for (let each of path) _register (srv, phase, event, each, handler)
|
|
94
|
-
return this
|
|
95
|
-
}
|
|
96
|
-
else if (typeof path === 'object') {
|
|
97
|
-
path = path.name || expected `${{path}} to be a string or an entity's CSN definition`
|
|
98
|
-
}
|
|
99
|
-
else if (typeof path === 'string') {
|
|
100
|
-
if (!path.startsWith(srv.name+'.')) path = `${srv.name}.${path}`
|
|
101
|
-
}
|
|
97
|
+
// Finally register with a filter function to match requests to be handled
|
|
98
|
+
const handlers = event === 'error' ? this._error : handler._initial ? this._initial : this[phase] // REVISIT: remove _initial handlers
|
|
99
|
+
handlers.push (new EventHandler (phase, event, path, handler))
|
|
102
100
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (['PATCH', 'CANCEL', 'NEW'].includes(event)) {
|
|
106
|
-
// delegate to drafts
|
|
107
|
-
path = typeof path === 'string' && path !== '*' && !path.endsWith('.drafts') ? path + '.drafts' : typeof path === 'object' && path.drafts || path
|
|
108
|
-
if (event === 'PATCH') event = 'UPDATE'
|
|
109
|
-
}
|
|
110
|
-
else if (entity && (event === 'READ' || entity.actions?.[event]) && (entity.drafts && !entity.name.endsWith('.drafts'))) {
|
|
111
|
-
// additionally add drafts for READ and bound actions/functions
|
|
112
|
-
_register(srv, phase, event, entity.name + '.drafts', handler)
|
|
113
|
-
}
|
|
101
|
+
if (phase === 'on') cds.emit('subscribe',srv,event) //> inform messaging service
|
|
102
|
+
return srv
|
|
114
103
|
}
|
|
115
104
|
|
|
116
|
-
// Finally register with a filter function to match requests to be handled
|
|
117
|
-
const _handlers = srv._handlers [event === 'error' ? '_error' : (handler._initial ? '_initial' : phase)] // REVISIT: remove _initial handlers
|
|
118
|
-
_handlers.push (new EventHandler (phase, event, path, handler))
|
|
119
105
|
|
|
120
|
-
|
|
121
|
-
|
|
106
|
+
//--------------------------------------------------------------------------
|
|
107
|
+
// EXPERIMENTAL: It is not decided yet, whether we should keep the stuff below
|
|
108
|
+
// => Please do not use anywhere!
|
|
109
|
+
onSucceeded (...args) { return _req_on (this, 'succeeded', ...args) }
|
|
110
|
+
onFailed (...args) { return _req_on (this, 'failed', ...args) }
|
|
111
|
+
for (event, path) { //> fetch all handlers for the given event and path
|
|
112
|
+
const filtered = {}
|
|
113
|
+
for (let each in this) {
|
|
114
|
+
filtered[each] = this[each].filter (h => h.for({event,path}))
|
|
115
|
+
}
|
|
116
|
+
return filtered
|
|
117
|
+
}
|
|
122
118
|
}
|
|
119
|
+
module.exports = EventHandlers
|
|
123
120
|
|
|
124
121
|
|
|
125
122
|
class EventHandler {
|
|
126
123
|
constructor (phase, event, path, handler) {
|
|
127
|
-
|
|
128
|
-
if (path)
|
|
129
|
-
|
|
130
|
-
Object.
|
|
131
|
-
|
|
132
|
-
for: { value: this.for(event,path) }
|
|
133
|
-
})
|
|
124
|
+
const h = { [phase]: event || '*' }
|
|
125
|
+
if (path) h.path = path
|
|
126
|
+
h.handler = Object.defineProperty (handler, '_initial', {enumerable:false})
|
|
127
|
+
Object.defineProperty (h, 'for', {value: this.for(event,path) })
|
|
128
|
+
return h
|
|
134
129
|
}
|
|
135
130
|
/** Factory for the actual filter method this.for, assigned above */
|
|
136
131
|
for (event, path) {
|
|
@@ -152,11 +147,6 @@ const AlternativeEvents = {
|
|
|
152
147
|
}
|
|
153
148
|
|
|
154
149
|
|
|
155
|
-
//--------------------------------------------------------------------------
|
|
156
|
-
// EXPERIMENTAL: It is not decided yet, whether we should keep the stuff below
|
|
157
|
-
// => Please do not use anywhere!
|
|
158
|
-
EventHandlers.prototype.onSucceeded = function (...args) { return _req_on (this, 'succeeded', ...args) }
|
|
159
|
-
EventHandlers.prototype.onFailed = function (...args) { return _req_on (this, 'failed', ...args) }
|
|
160
150
|
const _req_on = (srv, succeeded_or_failed, event, path, handler) => {
|
|
161
151
|
if (!handler) [path,handler] = [undefined,path]
|
|
162
152
|
return srv.before (event,path, req => req.on(succeeded_or_failed,handler))
|
package/lib/srv/srv-methods.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
+
/** @typedef {import('./cds.ServiceClient')} Service } */
|
|
2
|
+
|
|
1
3
|
const cds = require('..')
|
|
2
4
|
const LOG = cds.log('cds.serve',{label:'cds'})
|
|
3
5
|
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
for (const a in each.actions) {
|
|
16
|
-
add_handler_for (srv, each.actions[a])
|
|
17
|
-
}
|
|
7
|
+
/** @param {Service} srv */
|
|
8
|
+
module.exports = function (srv=this) {
|
|
9
|
+
if (!srv.definition) return //> can only add shortcuts for actions declared in service models
|
|
10
|
+
if (!srv.isAppService && !srv.isExternal && !srv._add_stub_methods) return
|
|
11
|
+
for (const each of srv.actions) {
|
|
12
|
+
add_handler_for (srv, each)
|
|
13
|
+
}
|
|
14
|
+
for (const each of srv.entities) {
|
|
15
|
+
for (const a in each.actions) {
|
|
16
|
+
add_handler_for (srv, each.actions[a])
|
|
18
17
|
}
|
|
19
18
|
}
|
|
20
|
-
return srv
|
|
21
19
|
}
|
|
22
20
|
|
|
21
|
+
|
|
22
|
+
/** @param {Service} srv */
|
|
23
23
|
const add_handler_for = (srv, def) => {
|
|
24
24
|
const event = def.name.match(/\w*$/)[0]
|
|
25
25
|
|
package/lib/srv/srv-tx.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** @typedef {import('./cds.Service')} Service } */
|
|
2
|
+
|
|
1
3
|
const cds = require('../index')
|
|
2
4
|
const EventContext = require('../req/context')
|
|
3
5
|
class RootContext extends EventContext {
|
|
@@ -17,7 +19,7 @@ class NestedContext extends EventContext {
|
|
|
17
19
|
/**
|
|
18
20
|
* This is the implementation of the `srv.tx(req)` method. It constructs
|
|
19
21
|
* a new Transaction as a derivate of the `srv` (i.e. {__proto__:srv})
|
|
20
|
-
* @returns { Promise<Transaction &
|
|
22
|
+
* @returns { Promise<Transaction & Service> }
|
|
21
23
|
* @param { EventContext } ctx
|
|
22
24
|
*/
|
|
23
25
|
module.exports = exports = function srv_tx (ctx,fn) { const srv = this
|
|
@@ -58,7 +60,7 @@ class Transaction {
|
|
|
58
60
|
return tx
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
/** @param {
|
|
63
|
+
/** @param {Service} srv */
|
|
62
64
|
constructor (srv, ctx) {
|
|
63
65
|
const tx = { __proto__:srv, _kind: new.target.name, context: ctx }
|
|
64
66
|
const proto = new.target.prototype
|
|
@@ -97,7 +99,7 @@ class Transaction {
|
|
|
97
99
|
* err is undefined if nested tx (cf. "root.before ('failed', ()=> this.rollback())")
|
|
98
100
|
*/
|
|
99
101
|
// FIXME: with noa, this.context === cds.context and not the individual cds.Request
|
|
100
|
-
if (err) for (const each of this.
|
|
102
|
+
if (err) for (const each of this.handlers._error) each.handler.call(this, err, this.context)
|
|
101
103
|
|
|
102
104
|
if (this.ready) { //> nothing to do if no transaction started at all
|
|
103
105
|
// don't actually roll back if already committed (e.g., error thrown in on succeeded or on done)
|
package/lib/utils/cds-utils.js
CHANGED
|
@@ -246,7 +246,7 @@ exports.deprecated = (fn, { kind = 'Method', old = fn.name+'()', use } = {}) =>
|
|
|
246
246
|
const reset = '\x1b[0m'
|
|
247
247
|
// use cds.log in production for custom logger
|
|
248
248
|
const {warn} = cds.env.production ? cds.log() : console
|
|
249
|
-
if(typeof fn !== 'function') {
|
|
249
|
+
if (typeof fn !== 'function') {
|
|
250
250
|
if (cds.env.features.deprecated === 'off') return
|
|
251
251
|
[kind,old,use] = [fn.kind || 'Configuration',fn.old,fn.use]
|
|
252
252
|
warn (
|
|
@@ -267,7 +267,7 @@ exports.deprecated = (fn, { kind = 'Method', old = fn.name+'()', use } = {}) =>
|
|
|
267
267
|
'\nDEPRECATED:', old, '\n',
|
|
268
268
|
'\n ', (kind ? `${kind} ${old}` : old), 'is deprecated and will be removed in upcoming releases!',
|
|
269
269
|
use ? `\n => Please use ${use} instead.` : '', '\n',
|
|
270
|
-
o.stack.replace(/^Error
|
|
270
|
+
o.stack.replace(/^Error:?\s*at.*\n/m,'\n'), '\n',
|
|
271
271
|
'\n------------------------------------------------------------------------------\n',
|
|
272
272
|
reset
|
|
273
273
|
)
|
|
@@ -116,7 +116,7 @@ const getErrorHandler = (crashOnError = true, srv) => {
|
|
|
116
116
|
// REVISIT: invoking service.on('error') handlers needs a cleanup with new protocol adapters!!!
|
|
117
117
|
// invoke srv.on('error', function (err, req) { ... }) here in special situations
|
|
118
118
|
// REVISIT: if for compat reasons, remove once cds^5.1
|
|
119
|
-
if (srv.
|
|
119
|
+
if (srv.handlers._error.length) {
|
|
120
120
|
// REVISIT: move to error middleware
|
|
121
121
|
let ctx = cds.context
|
|
122
122
|
if (!ctx) {
|
|
@@ -125,11 +125,11 @@ const getErrorHandler = (crashOnError = true, srv) => {
|
|
|
125
125
|
// but then the ctx in the else branch below isn't the ODataRequest anymore
|
|
126
126
|
// > error before req was dispatched
|
|
127
127
|
const creq = new cds.Request({ req, res: req.res, user: cds.context.user })
|
|
128
|
-
for (const each of srv.
|
|
128
|
+
for (const each of srv.handlers._error) each.handler.call(srv, err, creq)
|
|
129
129
|
} else if (ctx._tx?._done !== 'rolled back') {
|
|
130
130
|
// > error after req was dispatched, e.g., serialization error in okra
|
|
131
131
|
const creq = /* odataReq.req || */ new cds.Request({ req, res: req.res, user: ctx.user, tenant: ctx.tenant })
|
|
132
|
-
for (const each of srv.
|
|
132
|
+
for (const each of srv.handlers._error) each.handler.call(srv, err, creq)
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
package/libx/_runtime/cds.js
CHANGED
|
@@ -4,10 +4,11 @@ module.exports = cds
|
|
|
4
4
|
/*
|
|
5
5
|
* csn aspects
|
|
6
6
|
*/
|
|
7
|
-
const { any, entity, Association } = cds.builtin.classes
|
|
7
|
+
const { any, entity, Association, service } = cds.builtin.classes
|
|
8
8
|
cds.extend(any).with(require('./common/aspects/any'))
|
|
9
9
|
cds.extend(Association).with(require('./common/aspects/Association'))
|
|
10
10
|
cds.extend(entity).with(require('./common/aspects/entity'))
|
|
11
|
+
cds.extend(service).with(require('./common/aspects/service'))
|
|
11
12
|
|
|
12
13
|
/*
|
|
13
14
|
* Determines whether a request requires resolving of the target entity.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
|
|
3
|
+
module.exports = cds.env.effective.odata.containment
|
|
4
|
+
? class {
|
|
5
|
+
get _containedEntities() {
|
|
6
|
+
return this.own('__containedEntities', () => {
|
|
7
|
+
const containees = new Set()
|
|
8
|
+
|
|
9
|
+
for (const e in this.entities) {
|
|
10
|
+
const entity = this.entities[e]
|
|
11
|
+
if (entity.compositions) {
|
|
12
|
+
for (const c in entity.compositions) {
|
|
13
|
+
const comp = entity.compositions[c]
|
|
14
|
+
if (comp.parent.name !== comp.target) {
|
|
15
|
+
containees.add(comp.target)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return containees
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
: class {}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const cds = require('../../../cds')
|
|
2
2
|
|
|
3
|
+
const serviceHandler = require('./service')
|
|
3
4
|
const requiresHandler = require('./requires')
|
|
4
5
|
const readOnlyHandler = require('./readOnly')
|
|
5
6
|
const insertOnlyHandler = require('./insertOnly')
|
|
@@ -57,6 +58,10 @@ module.exports = cds.service.impl(function authorization() {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// service-level restrictions (not all requests are dispatched by protocol adapter with its early access check)
|
|
62
|
+
// REVISIT: make default in cds^9 -> remove feature flag completely or at least make it an opt-out
|
|
63
|
+
if (cds.env.features.service_level_restrictions) this.before('*', serviceHandler)
|
|
64
|
+
|
|
60
65
|
/*
|
|
61
66
|
* @requires
|
|
62
67
|
*/
|
|
@@ -5,6 +5,10 @@ const { reject, getRejectReason, resolveUserAttrs, getAuthRelevantEntity } = req
|
|
|
5
5
|
const { DRAFT_EVENTS, MOD_EVENTS } = require('./constants')
|
|
6
6
|
const { getNormalizedPlainRestrictions } = require('./restrictions')
|
|
7
7
|
|
|
8
|
+
const _hasRef = xpr => {
|
|
9
|
+
for (const each of xpr) if (each.ref || (each.xpr && _hasRef(each.xpr))) return true
|
|
10
|
+
}
|
|
11
|
+
|
|
8
12
|
const _getResolvedApplicables = (applicables, req) => {
|
|
9
13
|
const resolvedApplicables = []
|
|
10
14
|
|
|
@@ -26,7 +30,8 @@ const _getResolvedApplicables = (applicables, req) => {
|
|
|
26
30
|
target: restrict.target,
|
|
27
31
|
where: restrict.where,
|
|
28
32
|
// replace $user.x with respective values
|
|
29
|
-
_xpr: resolveUserAttrs(xpr, req)
|
|
33
|
+
_xpr: resolveUserAttrs(xpr, req),
|
|
34
|
+
_hasRef: _hasRef(xpr)
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
|
|
@@ -38,7 +43,11 @@ const _getResolvedApplicables = (applicables, req) => {
|
|
|
38
43
|
|
|
39
44
|
const _getStaticAuthRestrictions = resolvedApplicables => {
|
|
40
45
|
return resolvedApplicables.filter(
|
|
41
|
-
resolved =>
|
|
46
|
+
resolved =>
|
|
47
|
+
resolved &&
|
|
48
|
+
!resolved._hasRef &&
|
|
49
|
+
resolved._xpr.length === 3 &&
|
|
50
|
+
resolved._xpr.every(ele => typeof ele !== 'object' || ele.val)
|
|
42
51
|
)
|
|
43
52
|
}
|
|
44
53
|
|
|
@@ -71,6 +80,7 @@ const _handleStaticAuthRestrictions = (resolvedApplicables, req) => {
|
|
|
71
80
|
const vals = restriction._xpr.filter(ele => typeof ele === 'object' && ele.val).map(ele => ele.val)
|
|
72
81
|
return _evalStatic(op, vals)
|
|
73
82
|
})
|
|
83
|
+
|
|
74
84
|
// static clause grants access => done
|
|
75
85
|
if (isAllowed) return
|
|
76
86
|
|
|
@@ -155,14 +165,19 @@ const _addRestrictionsToRead = async (req, model, resolvedApplicables) => {
|
|
|
155
165
|
|
|
156
166
|
const _getUnrestrictedCount = async req => {
|
|
157
167
|
const dbtx = cds.tx(req)
|
|
168
|
+
|
|
158
169
|
const target =
|
|
159
170
|
(req.query.UPDATE && req.query.UPDATE.entity) ||
|
|
160
171
|
(req.query.DELETE && req.query.DELETE.from) ||
|
|
161
172
|
(req.query.SELECT && req.query.SELECT.from)
|
|
162
173
|
const selectUnrestricted = SELECT.one(['count(*) as n']).from(target)
|
|
163
|
-
const whereUnrestricted = (req.query.UPDATE && req.query.UPDATE.where) || (req.query.DELETE && req.query.DELETE.where)
|
|
164
174
|
|
|
165
|
-
|
|
175
|
+
// REVISIT: remove with cds^9
|
|
176
|
+
if (cds.env.features.compat_restrict_where) {
|
|
177
|
+
const whereUnrestricted =
|
|
178
|
+
(req.query.UPDATE && req.query.UPDATE.where) || (req.query.DELETE && req.query.DELETE.where)
|
|
179
|
+
if (whereUnrestricted) selectUnrestricted.where(whereUnrestricted)
|
|
180
|
+
}
|
|
166
181
|
|
|
167
182
|
// Because of side effects, the statements have to be fired sequentially.
|
|
168
183
|
const { n } = await dbtx.run(selectUnrestricted)
|
|
@@ -171,14 +186,18 @@ const _getUnrestrictedCount = async req => {
|
|
|
171
186
|
|
|
172
187
|
const _getRestrictedCount = async (req, model, resolvedApplicables) => {
|
|
173
188
|
const dbtx = cds.tx(req)
|
|
189
|
+
|
|
174
190
|
const target =
|
|
175
191
|
(req.query.UPDATE && req.query.UPDATE.entity) ||
|
|
176
192
|
(req.query.DELETE && req.query.DELETE.from) ||
|
|
177
193
|
(req.query.SELECT && req.query.SELECT.from)
|
|
178
194
|
const selectRestricted = SELECT.one(['count(*) as n']).from(target)
|
|
179
|
-
const whereRestricted = (req.query.UPDATE && req.query.UPDATE.where) || (req.query.DELETE && req.query.DELETE.where)
|
|
180
195
|
|
|
181
|
-
|
|
196
|
+
// REVISIT: remove with cds^9
|
|
197
|
+
if (cds.env.features.compat_restrict_where) {
|
|
198
|
+
const whereRestricted = (req.query.UPDATE && req.query.UPDATE.where) || (req.query.DELETE && req.query.DELETE.where)
|
|
199
|
+
if (whereRestricted) selectRestricted.where(whereRestricted)
|
|
200
|
+
}
|
|
182
201
|
|
|
183
202
|
if (typeof selectRestricted.SELECT === 'object') {
|
|
184
203
|
selectRestricted.SELECT.from.ref = _addWheresToRef(selectRestricted.SELECT.from.ref, model, resolvedApplicables)
|
|
@@ -233,10 +252,14 @@ async function check_roles(req) {
|
|
|
233
252
|
|
|
234
253
|
const resolvedApplicables = _getResolvedApplicables(restrictions, req)
|
|
235
254
|
|
|
236
|
-
// REVISIT
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
255
|
+
// REVISIT with cds^9
|
|
256
|
+
// - remove compat_static_auth
|
|
257
|
+
// - make check on CREATE/ NEW and unbound a compat opt-in
|
|
258
|
+
if (cds.env.features.compat_static_auth || req.event in { CREATE: 1, NEW: 1 } || this.operations[req.event]) {
|
|
259
|
+
const staticAuthRestriction = _getStaticAuthRestrictions(resolvedApplicables)
|
|
260
|
+
if (staticAuthRestriction.length > 0) {
|
|
261
|
+
return _handleStaticAuthRestrictions(staticAuthRestriction, req)
|
|
262
|
+
}
|
|
240
263
|
}
|
|
241
264
|
|
|
242
265
|
if (req.event === 'READ') {
|
|
@@ -244,7 +267,7 @@ async function check_roles(req) {
|
|
|
244
267
|
return
|
|
245
268
|
}
|
|
246
269
|
|
|
247
|
-
//Instance based authorization for bound actions /functions
|
|
270
|
+
// Instance based authorization for bound actions /functions
|
|
248
271
|
await restrictBoundActionFunctions(req, resolvedApplicables, definition, this)
|
|
249
272
|
|
|
250
273
|
// no modification -> nothing more to do
|
|
@@ -272,15 +295,14 @@ const isBoundToCollection = action =>
|
|
|
272
295
|
|
|
273
296
|
const restrictBoundActionFunctions = async (req, resolvedApplicables, definition, srv) => {
|
|
274
297
|
if (req.target?.actions?.[req.event] && !isBoundToCollection(req.target.actions[req.event])) {
|
|
275
|
-
//Clone to avoid target modification, which would cause a different query
|
|
298
|
+
// Clone to avoid target modification, which would cause a different query
|
|
276
299
|
const query = cds.ql.clone(req.query) ?? SELECT.from(req.subject)
|
|
277
300
|
_addRestrictionsToRead({ query: query, target: req.target }, cds.model, resolvedApplicables)
|
|
278
|
-
const result = await srv.run(query)
|
|
301
|
+
const result = await (cds.env.features.compat_restrict_bound ? srv : cds.tx(req)).run(query)
|
|
279
302
|
if (!result || result.length === 0) {
|
|
280
303
|
// If we got a result, we don't need to check for the existence, hence only in this special case we must determine if `404` or `403`.
|
|
281
304
|
const unrestrictedCount = await _getUnrestrictedCount(req)
|
|
282
305
|
if (unrestrictedCount === 0) req.reject(404)
|
|
283
|
-
|
|
284
306
|
if (LOG._debug) LOG.debug(`Restricted access on action ${req.event}`)
|
|
285
307
|
reject(req, getRejectReason(req, '@restrict', definition))
|
|
286
308
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const { reject, getRejectReason } = require('./utils')
|
|
2
|
+
|
|
3
|
+
const _getRequiresAsArray = definition =>
|
|
4
|
+
definition['@requires']
|
|
5
|
+
? Array.isArray(definition['@requires'])
|
|
6
|
+
? definition['@requires']
|
|
7
|
+
: [definition['@requires']]
|
|
8
|
+
: false
|
|
9
|
+
|
|
10
|
+
function handler(req) {
|
|
11
|
+
if (req.user._is_privileged) {
|
|
12
|
+
// > skip checks
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const requires = _getRequiresAsArray(this.definition)
|
|
17
|
+
|
|
18
|
+
if (!requires || requires.some(role => req.user.is(role))) return
|
|
19
|
+
reject(req, getRejectReason(req, '@requires', this.definition))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
handler._initial = true
|
|
23
|
+
|
|
24
|
+
module.exports = handler
|
|
@@ -105,8 +105,10 @@ const _handleArray = (arr, where, index) => {
|
|
|
105
105
|
} else _arrayComparison(arr, where, index)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
const $nonexistent = Symbol('nonexistent')
|
|
109
|
+
const operators = new Set(['=', '!=', '<>', '<', '<=', '>', '>=', 'in'])
|
|
110
|
+
|
|
108
111
|
const resolveUserAttrs = (where, req) => {
|
|
109
|
-
let non_existing
|
|
110
112
|
for (let i = 0; i < where.length; i++) {
|
|
111
113
|
const r = where[i]
|
|
112
114
|
if (r.xpr) r.xpr = resolveUserAttrs(r.xpr, req)
|
|
@@ -118,11 +120,17 @@ const resolveUserAttrs = (where, req) => {
|
|
|
118
120
|
for (let j = 1; j < r.ref.length; j++) {
|
|
119
121
|
const attr = r.ref[j]
|
|
120
122
|
if (!Object.prototype.hasOwnProperty.call(val, attr)) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
val = $nonexistent
|
|
124
|
+
if (where[i - 2] && operators.has(where[i - 1])) {
|
|
125
|
+
where.splice(i - 2, 3, { val: '1' }, '=', { val: '2' })
|
|
126
|
+
} else if (where[i + 2] && operators.has(where[i + 1])) {
|
|
127
|
+
where.splice(i, 3, { val: '1' }, '=', { val: '2' })
|
|
128
|
+
} else if (where[i + 1] === 'is') {
|
|
129
|
+
where.splice(i, where[i + 2] === 'not' ? 4 : 3, { val: '1' }, '=', { val: '2' })
|
|
130
|
+
}
|
|
123
131
|
} else val = val?.[attr]
|
|
124
132
|
}
|
|
125
|
-
if (
|
|
133
|
+
if (val === $nonexistent) continue
|
|
126
134
|
if (val === undefined) val = null
|
|
127
135
|
if (val === null && where[i - 1] === 'in') where[i] = { list: [{ val: '__dummy__' }] }
|
|
128
136
|
else if (Array.isArray(val)) _handleArray(val, where, i)
|
|
@@ -135,11 +143,11 @@ const resolveUserAttrs = (where, req) => {
|
|
|
135
143
|
})
|
|
136
144
|
} else if (r.func) {
|
|
137
145
|
r.args = resolveUserAttrs(r.args, req)
|
|
146
|
+
} else if (r.val) {
|
|
147
|
+
if (typeof r.val === 'number') r.param = false
|
|
138
148
|
}
|
|
139
149
|
}
|
|
140
150
|
|
|
141
|
-
if (non_existing) return [{ val: '1' }, '=', { val: '2' }]
|
|
142
|
-
|
|
143
151
|
_processNullAttr(where)
|
|
144
152
|
|
|
145
153
|
return where
|
|
@@ -118,7 +118,7 @@ const commonGenericValidateETag = async function (req) {
|
|
|
118
118
|
// add where clause for validation
|
|
119
119
|
const validationClause = ['exists', validationStmt]
|
|
120
120
|
req.query.where(validationClause)
|
|
121
|
-
// HACK for current draft impl
|
|
121
|
+
// HACK for current draft impl // REVISIT: which is really bad
|
|
122
122
|
req._etagValidationClause = validationClause
|
|
123
123
|
req._etagValidationType = ifMatchEtags ? 'if-match' : 'if-none-match'
|
|
124
124
|
}
|
|
@@ -82,8 +82,7 @@ function targetFromPath(from, model) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
const resolveFromSelect = query => {
|
|
85
|
-
|
|
86
|
-
if (!(query instanceof __protoSelect.constructor)) Object.setPrototypeOf(query, __protoSelect)
|
|
85
|
+
if (!(query instanceof SELECT.class)) Object.setPrototypeOf(query, SELECT.class.prototype)
|
|
87
86
|
const { from } = query.SELECT
|
|
88
87
|
return from.SELECT ? resolveFromSelect(from) : from
|
|
89
88
|
}
|