@sap/cds 8.5.1 → 8.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +56 -3
  2. package/_i18n/i18n.properties +4 -7
  3. package/eslint.config.mjs +1 -1
  4. package/lib/compile/etc/properties.js +12 -9
  5. package/lib/compile/for/java.js +15 -3
  6. package/lib/compile/for/lean_drafts.js +44 -34
  7. package/lib/compile/for/nodejs.js +19 -10
  8. package/lib/compile/minify.js +2 -4
  9. package/lib/compile/parse.js +106 -72
  10. package/lib/compile/to/edm.js +19 -9
  11. package/lib/compile/to/hana.js +25 -21
  12. package/lib/compile/to/json.js +2 -2
  13. package/lib/compile/to/sql.js +15 -8
  14. package/lib/core/linked-csn.js +10 -4
  15. package/lib/dbs/cds-deploy.js +1 -1
  16. package/lib/env/cds-env.js +76 -66
  17. package/lib/env/defaults.js +1 -0
  18. package/lib/i18n/bundles.js +2 -1
  19. package/lib/i18n/files.js +3 -3
  20. package/lib/i18n/localize.js +2 -2
  21. package/lib/index.js +24 -18
  22. package/lib/ql/CREATE.js +11 -6
  23. package/lib/ql/DELETE.js +12 -9
  24. package/lib/ql/DROP.js +15 -8
  25. package/lib/ql/INSERT.js +19 -14
  26. package/lib/ql/SELECT.js +95 -168
  27. package/lib/ql/UPDATE.js +23 -14
  28. package/lib/ql/UPSERT.js +15 -2
  29. package/lib/ql/Whereable.js +44 -118
  30. package/lib/ql/cds-ql.js +222 -28
  31. package/lib/ql/{Query.js → cds.ql-Query.js} +52 -41
  32. package/lib/ql/cds.ql-predicates.js +133 -0
  33. package/lib/ql/cds.ql-projections.js +111 -0
  34. package/lib/ql/cqn.d.ts +146 -0
  35. package/lib/srv/cds-connect.js +3 -3
  36. package/lib/srv/cds-serve.js +2 -2
  37. package/lib/srv/cds.Service.js +132 -0
  38. package/lib/srv/{srv-api.js → cds.ServiceClient.js} +16 -71
  39. package/lib/srv/cds.ServiceProvider.js +20 -0
  40. package/lib/srv/factory.js +20 -8
  41. package/lib/srv/protocols/hcql.js +2 -3
  42. package/lib/srv/protocols/index.js +3 -3
  43. package/lib/srv/srv-dispatch.js +7 -6
  44. package/lib/srv/srv-handlers.js +103 -113
  45. package/lib/srv/srv-methods.js +14 -14
  46. package/lib/srv/srv-tx.js +5 -3
  47. package/lib/utils/cds-utils.js +2 -2
  48. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +3 -3
  49. package/libx/_runtime/cds.js +2 -1
  50. package/libx/_runtime/common/aspects/service.js +25 -0
  51. package/libx/_runtime/common/generic/auth/index.js +5 -0
  52. package/libx/_runtime/common/generic/auth/restrict.js +36 -14
  53. package/libx/_runtime/common/generic/auth/service.js +24 -0
  54. package/libx/_runtime/common/generic/auth/utils.js +14 -6
  55. package/libx/_runtime/common/generic/etag.js +1 -1
  56. package/libx/_runtime/common/utils/cqn.js +1 -2
  57. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  58. package/libx/_runtime/common/utils/generateOnCond.js +7 -3
  59. package/libx/_runtime/common/utils/postProcess.js +4 -1
  60. package/libx/_runtime/common/utils/restrictions.js +1 -0
  61. package/libx/_runtime/fiori/lean-draft.js +54 -43
  62. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -1
  63. package/libx/_runtime/remote/Service.js +2 -0
  64. package/libx/_runtime/remote/utils/client.js +12 -0
  65. package/libx/odata/index.js +5 -3
  66. package/libx/odata/middleware/create.js +2 -2
  67. package/libx/odata/middleware/delete.js +2 -2
  68. package/libx/odata/middleware/operation.js +2 -2
  69. package/libx/odata/middleware/read.js +14 -12
  70. package/libx/odata/middleware/service-document.js +16 -8
  71. package/libx/odata/middleware/update.js +2 -2
  72. package/libx/odata/parse/afterburner.js +63 -29
  73. package/libx/odata/parse/grammar.peggy +95 -0
  74. package/libx/odata/parse/parser.js +1 -1
  75. package/libx/odata/utils/index.js +5 -1
  76. package/libx/odata/utils/metadata.js +69 -75
  77. package/libx/odata/utils/postProcess.js +24 -3
  78. package/package.json +1 -1
  79. package/server.js +1 -1
  80. package/lib/ql/parse.js +0 -36
  81. /package/lib/ql/{infer.js → cds.ql-infer.js} +0 -0
@@ -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
- constructor (name) {
7
- this._handlers = {
8
- /** @type {EventHandler[]} */ _initial:[],
9
- /** @type {EventHandler[]} */ before:[],
10
- /** @type {EventHandler[]} */ on:[],
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 {_handlers} = this, _new = this._handlers = { on:[], before:[], after:[], _initial:[], _error:[] }
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) _handlers[each].unshift(..._new[each])
29
- this._handlers = _handlers
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
- async init() {}
34
- async _init() {
35
- const { impl } = this.options
36
- if (typeof impl === 'function' && !/^class\b/.test(impl))
37
- await impl.call(this,this)
38
- await this.init()
39
- return this
40
- }
41
- }
42
- module.exports = EventHandlers
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
- /** Registers event handlers. This is the central method to register handlers,
47
- * used by all respective public API methods, i.e. .on/before/after/reject.
48
- * @param {'on'|'before'|'after'} phase
49
- * @param {string|string[]} event
50
- * @param {string|string[]} path
51
- * @param {(req)=>{}} handler
52
- */
53
- const _register = function (srv, phase, event, path, handler) { //NOSONAR
54
-
55
- if (!handler) [ handler, path ] = [ path ] // argument path is optional
56
- if (typeof handler !== 'function') expected `${{handler}} to be a function`
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
- // Canonicalize event argument
68
- if (!event || event === '*') event = undefined
69
- else if (is_array(event)) {
70
- for (let each of event) _register (srv, phase, each, path, handler)
71
- return this
72
- }
73
- else if (event === 'SAVE') {
74
- for (let each of ['CREATE','UPDATE']) _register (srv, phase, each, path, handler)
75
- return this
76
- }
77
- else if (phase === 'after' && ( event === 'each' //> srv.after ('each', Book, b => ...) // event 'each' => READ each
78
- || event === 'READ' && path?.is_singular //> srv.after ('READ', Book, b => ...) // Book is a singular def from cds-typer
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
- // Canonicalize path argument
91
- if (!path || path === '*') path = undefined
92
- else if (is_array(path)) {
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
- if (cds.env.fiori.draft_compat) {
104
- const entity = path && srv.model?.definitions[path.name || path]
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
- if (phase === 'on') cds.emit('subscribe',srv,event) //> inform messaging service
121
- return srv
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
- this[phase] = event || '*'
128
- if (path) this.path = path
129
- this.handler = handler
130
- Object.defineProperties (this, { // non-enumerable properties to improve debugging
131
- _initial: { value: handler._initial },
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))
@@ -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
- module.exports = (srv) => {
6
- if (srv.model && ( //> we only support that for app services
7
- srv.isAppService ||
8
- srv.isExternal ||
9
- srv._add_stub_methods
10
- )) {
11
- for (const each of srv.operations) {
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])
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 & import('./srv-api')> }
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 {import('./srv-api')} srv */
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._handlers._error) each.handler.call(this, err, this.context)
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)
@@ -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:\s*at.*\n/,'\n'), '\n',
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._handlers._error.length) {
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._handlers._error) each.handler.call(srv, err, creq)
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._handlers._error) each.handler.call(srv, err, creq)
132
+ for (const each of srv.handlers._error) each.handler.call(srv, err, creq)
133
133
  }
134
134
  }
135
135
 
@@ -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 => resolved && resolved._xpr.length === 3 && resolved._xpr.every(ele => typeof ele !== 'object' || ele.val)
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
- if (whereUnrestricted) selectUnrestricted.where(whereUnrestricted)
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
- if (whereRestricted) selectRestricted.where(whereRestricted)
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: support more complex statics
237
- const staticAuthRestriction = _getStaticAuthRestrictions(resolvedApplicables)
238
- if (staticAuthRestriction.length > 0) {
239
- return _handleStaticAuthRestrictions(staticAuthRestriction, req)
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
- non_existing = true
122
- break
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 (non_existing) break
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
- const __protoSelect = Object.getPrototypeOf(SELECT())
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
  }