@sap/cds 5.9.0 → 5.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/app/fiori/routes.js +15 -8
  3. package/lib/compile/cdsc.js +1 -19
  4. package/lib/compile/etc/_localized.js +5 -4
  5. package/lib/compile/for/drafts.js +1 -1
  6. package/lib/compile/for/java.js +1 -1
  7. package/lib/compile/for/nodejs.js +1 -1
  8. package/lib/compile/for/odata.js +1 -1
  9. package/lib/connect/bindings.js +1 -1
  10. package/lib/connect/index.js +2 -3
  11. package/lib/env/requires.js +1 -1
  12. package/lib/index.js +2 -1
  13. package/lib/serve/Service-methods.js +28 -1
  14. package/lib/serve/adapters.js +6 -6
  15. package/lib/serve/factory.js +14 -9
  16. package/lib/serve/index.js +4 -3
  17. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -1
  18. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/BatchRequestListBuilder.js +3 -1
  19. package/libx/_runtime/cds-services/adapter/odata-v4/utils/dispatcherUtils.js +13 -7
  20. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +1 -3
  21. package/libx/_runtime/cds-services/services/Service.js +1 -1
  22. package/libx/_runtime/common/composition/data.js +22 -13
  23. package/libx/_runtime/common/composition/delete.js +14 -12
  24. package/libx/_runtime/common/generic/auth/restrict.js +3 -1
  25. package/libx/_runtime/{cds-services/services/utils → common/generic/auth}/restrictions.js +8 -1
  26. package/libx/_runtime/common/generic/input.js +1 -0
  27. package/libx/_runtime/common/generic/put.js +1 -0
  28. package/libx/_runtime/common/utils/cqn.js +5 -10
  29. package/libx/_runtime/common/utils/cqn2cqn4sql.js +39 -65
  30. package/libx/_runtime/common/utils/foreignKeyPropagations.js +28 -8
  31. package/libx/_runtime/common/utils/path.js +3 -3
  32. package/libx/_runtime/common/utils/resolveView.js +3 -0
  33. package/libx/_runtime/common/utils/structured.js +6 -1
  34. package/libx/_runtime/db/Service.js +10 -0
  35. package/libx/_runtime/db/data-conversion/post-processing.js +5 -0
  36. package/libx/_runtime/db/expand/expand-v2.js +13 -5
  37. package/libx/_runtime/db/expand/expandCQNToJoin.js +56 -26
  38. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +8 -8
  39. package/libx/_runtime/db/utils/generateAliases.js +9 -0
  40. package/libx/_runtime/extensibility/uiflex/handler/transformWRITE.js +1 -0
  41. package/libx/_runtime/fiori/generic/read.js +83 -31
  42. package/libx/_runtime/fiori/generic/readOverDraft.js +31 -19
  43. package/libx/_runtime/fiori/utils/handler.js +3 -0
  44. package/libx/_runtime/fiori/utils/where.js +38 -25
  45. package/libx/_runtime/hana/execute.js +18 -1
  46. package/libx/_runtime/hana/search2cqn4sql.js +4 -1
  47. package/libx/_runtime/remote/utils/client.js +4 -6
  48. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +19 -12
  49. package/libx/odata/cqn2odata.js +24 -27
  50. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,41 @@
4
4
  - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## Version 5.9.3 - 2022-04-25
8
+
9
+ ### Fixed
10
+
11
+ - Since 5.8.2 `req.target` for requests like `srv.put('/MyService.entity')` is defined, but `req.query` undefined (before `req.target` was also undefined). This was leading to accessing undefined, which has been fixed.
12
+ - Custom actions with names conflicting with methods from service base classes, e.g. `run()`, could lead to hard-to-detect errors. This is now detected and avoided with a warning.
13
+ - Typed methods for custom actions were erroneously applied to `cds.db` service, which led to server crashes, e.g. when the action was named `deploy()`.
14
+ - Invalid batch requests were further processed after error response was already sent to client, leading to an InternalServerError
15
+ - Full support of `SELECT` queries with operator expressions (`xpr`)
16
+
17
+
18
+ ## Version 5.9.2 - 2022-04-07
19
+
20
+ ### Fixed
21
+
22
+ - i18n translation for errors did not work correctly in some cases
23
+ - Normalization in custom `getRestrictions`
24
+ - Throw exception by `INSERT` into HANA queries if number of provided rows deviates from number of affected rows returned by hdb to prevent data losses
25
+ - Handler detection for extended services
26
+ - Speed-up in localization handling
27
+ - Draft: navigation via an association to many from a non-draft enabled entity to a draft-enabled entity
28
+ - Limited support of `SELECT` queries with operator expressions (`xpr`)
29
+
30
+ ## Version 5.9.1 - 2022-03-31
31
+
32
+ ### Fixed
33
+
34
+ - Function arguments might be escaped too often
35
+ - URL encoding for remote services for CQN queries
36
+ - `cds serve` during development again redirects URLs with for UI apps in a folder with the same name as a service, so `/foo/webapp` would redirect to `/foo`. This got broken in 5.8.3.
37
+ - Endless loop in localization handling
38
+ - Ensure service impl while extending entity from the service
39
+ - Post-processing of custom draft queries
40
+ - No minifying of CSN artifacts for Java build
41
+
7
42
  ## Version 5.9.0 - 2022-03-25
8
43
 
9
44
  ### Added
@@ -1,4 +1,6 @@
1
1
  const cds = require('../../lib')
2
+ const DEBUG = cds.debug('fiori/routes')
3
+ const {dirname, relative, join} = require('path')
2
4
 
3
5
  // Only for local cds runs w/o approuter:
4
6
  // If there is a relative URL in UI5's manifest.json for the datasource,
@@ -11,16 +13,20 @@ cds.on ('bootstrap', app => {
11
13
  const v2Prefix = (env.odata.v2proxy && env.odata.v2proxy.urlpath) || '/v2'
12
14
  const serviceForUri = {}
13
15
 
14
- dataSourceURIs (env.folders.app).forEach(uri => {
15
- app.use('*/'+uri, ({originalUrl}, res, next)=> { // */browse/webapp[/prefix]/browse/
16
+ dataSourceURIs (env.folders.app).forEach(({appPath, dataSourceUri}) => {
17
+ const uiRoutes = [
18
+ join('/', appPath, dataSourceUri, '*'), // /uiApp/webapp/browse/*
19
+ join('/', appPath, '*', dataSourceUri, '*') // /uiApp/webapp/*/browse/*
20
+ ].map(r => r.replace(/\\/g, '/')) // handle Windows \
21
+ DEBUG && DEBUG ('Register routes', uiRoutes)
22
+
23
+ app.use(uiRoutes, ({originalUrl}, res, next)=> {
16
24
  // any of our special URLs ($fiori-, $api-docs) ? -> next
17
25
  if (originalUrl.startsWith('/$')) return next()
18
- // is there a service starting with the URL? -> next
19
- if (cds.service.providers.find (srv => originalUrl.startsWith(srv.path))) return next()
20
26
 
21
27
  // is there a service for '[prefix]/browse' ?
22
- const srv = serviceForUri[uri] || (serviceForUri[uri] =
23
- cds.service.providers.find (srv => ('/'+uri).lastIndexOf(srv.path) >=0))
28
+ const srv = serviceForUri[dataSourceUri] || (serviceForUri[dataSourceUri] =
29
+ cds.service.providers.find (srv => ('/'+dataSourceUri).lastIndexOf(srv.path) >=0))
24
30
  if (srv) {
25
31
  let redirectUrl
26
32
  // odata-proxy may be in the line with its /v2 prefix. Make sure we retain it.
@@ -30,7 +36,7 @@ cds.on ('bootstrap', app => {
30
36
  else // --> /browse/webapp[/prefix]/browse/ -> /browse
31
37
  redirectUrl = originalUrl.substring(originalUrl.lastIndexOf(srv.path+'/'))
32
38
  if (originalUrl !== redirectUrl) {// safeguard to prevent running in loops
33
- // console.log ('>>', req.originalUrl, '->', redirectUrl)
39
+ DEBUG && DEBUG ('Redirecting', {src: originalUrl}, '~>', {target: redirectUrl})
34
40
  return res.redirect (308, redirectUrl)
35
41
  }
36
42
  }
@@ -41,10 +47,11 @@ cds.on ('bootstrap', app => {
41
47
  function dataSourceURIs (dir) {
42
48
  const uris = new Set()
43
49
  find (dir, ['*/manifest.json', '*/*/manifest.json']).forEach(file => {
50
+ const appPath = relative(join(cds.root, dir), dirname(file))
44
51
  const {dataSources: ds} = JSON.parse(fs.readFileSync(file))['sap.app'] || {}
45
52
  Object.keys (ds||[])
46
53
  .filter (k => ds[k].uri && !ds[k].uri.startsWith('/')) // only consider relative URLs)
47
- .forEach(k => uris.add(ds[k].uri))
54
+ .forEach(k => uris.add({ appPath, dataSourceUri: ds[k].uri }))
48
55
  })
49
56
  return uris
50
57
  }
@@ -105,25 +105,7 @@ const _options = {for: Object.assign (_options4, {
105
105
  */
106
106
  module.exports = exports = {__proto__:compile, _options,
107
107
  for: {__proto__: compile.for,
108
- odata: (csn,o) => {
109
- if (features.ucsn) {
110
- const { cloneCsn } = require('@sap/cds-compiler/lib/model/csnUtils') // REVISIT: This should be done by the compiler
111
- if (compile.version() >= "2.12.1") {
112
- const generateDrafts = require('@sap/cds-compiler/lib/transform/draft/odata')
113
- const compiled = generateDrafts(cloneCsn(csn, {}), { messages: [] })
114
- compiled.meta._4odata = true
115
- return compiled
116
- } else {
117
- // not yet in compiler branch, can't add drafts
118
- const cloned = cloneCsn(csn, {})
119
- cloned.meta._4odata = true
120
- return cloned
121
- }
122
- }
123
- const compiled = compile.for.odata (csn, _options.for.odata(o))
124
- compiled.meta._4odata = true
125
- return compiled
126
- },
108
+ odata: (csn,o) => compile.for.odata (csn, _options.for.odata(o)),
127
109
  },
128
110
  to: {__proto__: compile.to,
129
111
  edmx: Object.assign ((csn,o) => compile.to.edmx (csn, _options.for.edm(o)), {
@@ -96,16 +96,17 @@ function unfold_csn (m) { // NOSONAR
96
96
  }
97
97
 
98
98
 
99
- const $localized = '$$localized', _is_localized = (d,_path) => {
100
- if (d.own($localized)) return true
101
- if (!d.elements || d.name.endsWith('.texts')) return false
99
+ const $localized = '$$localized', _is_localized = (d,_path={}) => {
100
+ if (typeof d.own($localized) === 'boolean') return d.own($localized)
101
+ if (!d.elements || d.name.endsWith('.texts')) return d.set($localized,false)
102
102
  // if (d.elements.texts && d.elements.texts.target === `${d.name}.texts`) return d.set($localized,true)
103
103
  for (let each in d.elements) {
104
104
  const e = d.elements [each]
105
- if (e.localized || e._target && !(_path && e._target.name in _path) && _is_localized(e._target,{..._path, [d.name]:1 })) {
105
+ if (e.localized || e._target && !(_path && e._target.name in _path) && _is_localized(e._target,Object.assign(_path, { [d.name]:1 }))) {
106
106
  return d.set($localized,true)
107
107
  }
108
108
  }
109
+ return d.set($localized,false)
109
110
  }
110
111
 
111
112
 
@@ -4,6 +4,6 @@ module.exports = function cds_compile_for_drafts (csn,o) {
4
4
  const unfold = cds_compile_for_drafts.unfold || (
5
5
  cds_compile_for_drafts.unfold = require ('@sap/cds-compiler/lib/transform/draft/odata')
6
6
  )
7
- // csn = JSON.parse (JSON.stringify (csn)) // REVISIT: workaround for bad test setup
7
+ csn = JSON.parse (JSON.stringify (csn)) // REVISIT: workaround for bad test setup
8
8
  return unfold (csn,o||{})
9
9
  }
@@ -4,7 +4,7 @@ const _cached = Symbol('for Java')
4
4
  module.exports = function cds_compile_for_java (csn,o) {
5
5
  if (!csn) return
6
6
  const cached = csn[_cached]; if (cached) return cached
7
- csn = cds.minify (csn)
7
+ // csn = cds.minify (csn)
8
8
  csn = cds.compile.for.drafts (csn,o)
9
9
  // Add a parsed _where clause for @restrict.{grant,where} annotations
10
10
  if (csn.definitions) for (let {'@restrict':rr} of Object.values(csn.definitions)) if (rr) {
@@ -4,7 +4,7 @@ const _cached = Symbol('for Node.js')
4
4
  module.exports = function cds_compile_for_nodejs (csn,o) {
5
5
  if (!csn) return
6
6
  const cached = csn[_cached]; if (cached) return cached
7
- csn = cds.minify (csn)
7
+ // csn = cds.minify (csn)
8
8
  csn = cds.compile.for.drafts (csn,o) //> creates a partial copy -> avoid any cds.linked() before
9
9
  csn = cds.compile._localized.unfold_csn (csn)
10
10
  csn = cds.linked (csn)
@@ -13,4 +13,4 @@ module.exports = function cds_compile_for_odata (csn,_o) {
13
13
  return _4odata
14
14
  }
15
15
 
16
- const _is_odata = csn => csn.meta && csn.meta._4odata
16
+ const _is_odata = csn => csn.meta && csn.meta.transformation === 'odata'
@@ -1,4 +1,4 @@
1
- const DEBUG = /\b(y|all|serve)\b/.test (process.env.DEBUG) && console.warn
1
+ const DEBUG = /\b(y|all|serve|bindings)\b/.test (process.env.DEBUG) && console.warn
2
2
  // || console.debug
3
3
 
4
4
  const cds = require ('..')
@@ -1,5 +1,4 @@
1
1
  const cds = require('..'), {one_model} = cds.env.features, LOG = cds.log('cds.connect')
2
- const factory = require('../serve/factory')
3
2
  const _pending = cds.services._pending || {} // used below to chain parallel connect.to(<same>)
4
3
 
5
4
  /**
@@ -22,7 +21,7 @@ const connect = module.exports = async function cds_connect (options) {
22
21
  * @returns { Promise<import('../serve/Service-api')> }
23
22
  */
24
23
  connect.to = async (datasource, options) => {
25
- let Service = factory, _done = x=>x
24
+ let Service = cds.service.factory, _done = x=>x
26
25
  if (typeof datasource === 'object') [options,datasource] = [datasource]
27
26
  else if (datasource) {
28
27
  if (datasource._is_service_class) [ Service, datasource ] = [ datasource, datasource.name ]
@@ -33,7 +32,7 @@ connect.to = async (datasource, options) => {
33
32
  // queue parallel requests to a single promise, to avoid creating multiple services
34
33
  _pending[datasource] = new Promise (r=>_done=r).finally(()=>{ delete _pending[datasource] })
35
34
  }
36
- const o = Service === factory ? options4 (datasource, options) : {}
35
+ const o = Service === cds.service.factory ? options4 (datasource, options) : {}
37
36
  const m = await model4 (o)
38
37
  // check if required service definition exists
39
38
  const required = cds.requires[datasource]
@@ -1,4 +1,4 @@
1
- const _runtime = '../../libx/_runtime'
1
+ const _runtime = '@sap/cds/libx/_runtime'
2
2
 
3
3
  exports = module.exports = {
4
4
 
package/lib/index.js CHANGED
@@ -12,6 +12,7 @@ if (global.cds) Object.assign(module,{exports:global.cds}) ; else {
12
12
  /** @type {{ [path:string] : Service }} */ paths: {},
13
13
  /** @type Service[] */ providers: [],
14
14
  factory: require ('./serve/factory'),
15
+ adapters: require ('./serve/adapters'),
15
16
  bindings: require ('./connect/bindings'),
16
17
  })}
17
18
  /** @type {import './req/context'} */
@@ -80,7 +81,7 @@ if (global.cds) Object.assign(module,{exports:global.cds}) ; else {
80
81
  ApplicationService: lazy => require('../libx/_runtime/cds-services/services/Service.js'),
81
82
  MessagingService: lazy => require('../libx/_runtime/messaging/service.js'),
82
83
  DatabaseService: lazy => require('../libx/_runtime/db/Service.js'),
83
- RemoteService: lazy => require('../libx/_runtime/rest/service.js'),
84
+ RemoteService: lazy => require('../libx/_runtime/remote/Service.js'),
84
85
  AuditLogService: lazy => require('../libx/_runtime/audit/Service.js'),
85
86
  odata: require('../libx/odata'),
86
87
 
@@ -1,5 +1,12 @@
1
+ const cds = require('..')
2
+ const LOG = cds.log('cds-app-service-methods')
3
+
1
4
 
2
5
  module.exports = (srv) => {
6
+ if (!( //> we only support that for app services
7
+ srv instanceof cds.ApplicationService ||
8
+ srv instanceof cds.RemoteService
9
+ )) return
3
10
  for (const each of srv.operations) {
4
11
  add_handler_for (srv, each)
5
12
  }
@@ -16,7 +23,23 @@ const add_handler_for = (srv, def) => {
16
23
  // Use existing methods as handler implementations
17
24
  const method = srv[event]
18
25
  if (method) {
19
- if (method._is_stub || method.name in srv.__proto__.__proto__) return
26
+ if (method._is_stub) return
27
+ const baseclass = (
28
+ srv.__proto__ === cds.ApplicationService.prototype ? srv.__proto__ :
29
+ srv.__proto__ === cds.RemoteService.prototype ? srv.__proto__ :
30
+ srv.__proto__.__proto__ // in case of class-based impls
31
+ )
32
+ if (method.name in baseclass) return LOG.warn(`WARNING: custom ${def.kind} '${event}()' conflicts with method in base class.
33
+
34
+ Cannot add typed method for custom ${def.kind} '${event}' to service impl of '${srv.name}',
35
+ as this would shadow equally named method in service base class '${baseclass.constructor.name}'.
36
+ Consider choosing a different name for your custom ${def.kind}.
37
+ Learn more at https://cap.cloud.sap/docs/guides/providing-services#actions-and-functions.
38
+ `)
39
+ LOG.debug (`
40
+ Using method ${event} from service class '${baseclass.constructor.name}'
41
+ as handler for ${def.kind} '${event}' in service '${srv.name}'
42
+ `)
20
43
  srv.on (event, function ({params,data}) {
21
44
  const args = []; if (def.parent) args.push (def.parent)
22
45
  for (let p in params) args.push(params[p])
@@ -26,6 +49,10 @@ const add_handler_for = (srv, def) => {
26
49
  }
27
50
 
28
51
  // Add stub methods to send request via typed API
52
+ LOG.debug (`
53
+ Adding typed method stub for calling custom ${def.kind} '${event}'
54
+ to service impl '${srv.name}'
55
+ `)
29
56
  const stub = srv[event] = function (...args) {
30
57
  const req = { event, data:{} }, $ = args[0]
31
58
  const target = this.entities [ $ && $.name ? $.name.match(/\w*$/)[0] : $ ]
@@ -1,11 +1,11 @@
1
1
  const lib = require('../../libx/_runtime')
2
2
  const registry = {
3
- rest: lib.to.old_rest,
4
- new_rest: lib.to.new_rest,
5
- odata: lib.to.odata_v4,
6
- odata_v2: lib.to.odata_v4,
7
- odata_v4: lib.to.odata_v4,
8
- fiori: lib.to.odata_v4,
3
+ get rest() { return lib.to.old_rest },
4
+ get new_rest() { return lib.to.new_rest },
5
+ get odata() { return lib.to.odata_v4 },
6
+ get odata_v2() { return lib.to.odata_v4 },
7
+ get odata_v4() { return lib.to.odata_v4 },
8
+ get fiori() { return lib.to.odata_v4 },
9
9
  }
10
10
 
11
11
 
@@ -1,5 +1,6 @@
1
- const cds = require('..')
2
- const { path, isfile } = cds.utils
1
+ const cds = require('..'), { path, isfile } = cds.utils
2
+ const paths = Array.from (new Set ([ cds.root, ...require.resolve.paths('x') ]))
3
+ const DEBUG = cds.debug('srv.factory'); DEBUG && DEBUG ({ 'cds.root':cds.root, paths })
3
4
 
4
5
  /** @typedef {import('./Service-api')} Service @type { (()=>Service) & (new()=>Service) } */
5
6
  const ServiceFactory = function (name, model, options) { //NOSONAR
@@ -8,6 +9,7 @@ const ServiceFactory = function (name, model, options) { //NOSONAR
8
9
  const serve = !cds.requires[name] || o.mocked
9
10
  const defs = !model ? {[name]:{}} : model.definitions || cds.error (`Invalid argument for 'model': ${model}`)
10
11
  const def = !name || name === 'db' ? {} : defs[name] || {}
12
+ DEBUG && DEBUG ({ name, definition:def, options:o })
11
13
 
12
14
  let it /* eslint-disable no-cond-assign */
13
15
  if (it = o.with) return _use (it) // from cds.serve (<options>)
@@ -27,21 +29,24 @@ const ServiceFactory = function (name, model, options) { //NOSONAR
27
29
 
28
30
  function _required() {
29
31
  const kind = o.kind = serve && def['@kind'] || o.kind || 'app-service'
30
- if (_required[kind]) return _required[kind]
32
+ if (_require[kind]) return _require[kind]
31
33
  const {impl} = cds.requires[kind] || cds.error (`No configuration found for 'cds.requires.${kind}'`)
32
- return _required[kind] = _require (impl || cds.error (`No 'impl' configured for 'cds.requires.${kind}'`))
34
+ DEBUG && DEBUG ('requires',{kind,impl})
35
+ return _require[kind] = _require (impl || cds.error (`No 'impl' configured for 'cds.requires.${kind}'`))
33
36
  }
34
37
  }
35
38
 
36
39
  const _require = (it,d) => {
37
- if (it.startsWith('@sap/cds/')) it = cds.home + it.slice(8) //> for local tests in @sap/cds dev
38
- else if (it.startsWith('./')) it = _relative (d, it.slice(2)) //> relative to <service>.cds
39
- else if (it.startsWith('//')) it = path.resolve (cds.root,it.slice(2)) //> relative to cds.root
40
- try { var resolved = require.resolve(it) } catch(e) {
40
+ DEBUG && d && DEBUG ('requires',{ service: d.name, source:_source(d), impl:it })
41
+ if (it.startsWith('@sap/cds/')) it = cds.home + it.slice(8) //> for local tests in @sap/cds dev
42
+ if (it.startsWith('./')) it = _relative (d,it.slice(2)) //> relative to <service>.cds
43
+ try { var resolved = require.resolve(it,{paths}) } catch(e) {
41
44
  try { resolved = require.resolve(it = path.resolve(cds.root,it)) } catch(e) { // for compatibility
45
+ DEBUG && DEBUG (`Failed loading service implementation from '${it}'`, { 'cds.root':cds.root, paths })
42
46
  throw cds.error(`Failed loading service implementation from '${it}'`)
43
47
  }
44
48
  }
49
+ DEBUG && DEBUG({resolved})
45
50
  return require(resolved)
46
51
  }
47
52
 
@@ -59,7 +64,7 @@ const sibling = (d) => {
59
64
  let found
60
65
  if (process.env.CDS_TYPESCRIPT === 'true') found = isfile(path.join(home, each, file + '.ts'))
61
66
  if (!found) found = isfile(path.join(home, each, file + '.js'))
62
- if (found) return found
67
+ if (found) return found //> equiv to '.'+found.slice(home.length)
63
68
  }
64
69
  }
65
70
 
@@ -1,6 +1,6 @@
1
- const { ProtocolAdapter } = require('./adapters')
2
- const { Service } = require('./factory')
3
1
  const cds = require ('..')
2
+ const { ProtocolAdapter } = cds.service.adapters
3
+ const { Service } = cds.service.factory
4
4
  const _ready = Symbol(), _pending = cds.services._pending || {}
5
5
 
6
6
  /** @param som - a service name or a model (name or csn) */
@@ -54,7 +54,8 @@ function cds_serve (som, _options) { // NOSONAR
54
54
  // Shortcut for directly passed service classes
55
55
  if (o.service && o.service._is_service_class) {
56
56
  const Service = o.service, d = { name: o.service.name }
57
- return all.push (_new (Service, d,csn,o))
57
+ const srv = _new (Service, d,csn,o)
58
+ return all.push (srv)
58
59
  }
59
60
 
60
61
  // Get relevant service definitions from model...
@@ -66,7 +66,12 @@ function _log(level, arg) {
66
66
 
67
67
  // reduce 4xx to warning
68
68
  if (isClientError(obj)) {
69
- if (!LOG._warn) return
69
+ if (!LOG._warn) {
70
+ // restore
71
+ obj.message = _message
72
+ if (_details) obj.details = _details
73
+ return
74
+ }
70
75
  level = 'warn'
71
76
  }
72
77
  }
@@ -89,7 +89,9 @@ class BatchRequestListBuilder {
89
89
  source
90
90
  .pipe(reader)
91
91
  .on('finish', () => {
92
- callback(null, this._requestInBatchList)
92
+ //Revisit: if statement needed in node v12 and v14, not in v16.
93
+ //Without it, finish callback reached in 12/14 after error handler was thrown
94
+ if (!source.res || !source.res.headersSent) callback(null, this._requestInBatchList)
93
95
  })
94
96
  .on('error', callback)
95
97
  }
@@ -1,5 +1,6 @@
1
1
  const cds = require('../../../../cds')
2
2
  const OData = require('../OData')
3
+ const DEBUG = cds.debug('extensibility')
3
4
 
4
5
  const { alias2ref } = require('../../../../common/utils/csn')
5
6
  const { BASE_TENANT } = require('../../../../common/utils/extensibilityUtils')
@@ -17,13 +18,18 @@ function createOdataService(service) {
17
18
  return odataService
18
19
  }
19
20
 
20
- async function createNewService(name, csn, defaultOptions) {
21
- const options = Object.assign({}, defaultOptions, { reflectedModel: csn })
22
-
23
- const service = new cds.ApplicationService(name, csn, options)
24
- await service.init()
25
- if (options.impl) service.impl(options.impl)
26
-
21
+ async function createNewService(name, model, options) {
22
+ const { constructor: Service, path } = cds.services[name]
23
+ const service = new Service(name, model, { ...options }) // cloning options to be safe
24
+ if (service.init) await service.prepend(service.init)
25
+ if (options.impl) await service.prepend(options.impl)
26
+ if (path) service.path = path
27
+ DEBUG &&
28
+ DEBUG('Created tenant-specific service:', service.name, '= new', Service.name, {
29
+ _handlers: {
30
+ on: service._handlers.on.map(h => ({ on: h.on, handler: () => {} }))
31
+ }
32
+ })
27
33
  return createOdataService(service)
28
34
  }
29
35
 
@@ -30,9 +30,7 @@ const _adaptSubSelectsDraft = select => {
30
30
  if (element.SELECT) {
31
31
  _adaptSubSelectsDraft(element)
32
32
  } else if (element.xpr) {
33
- for (const ele of element.xpr.filter(e => e.SELECT)) {
34
- _adaptSubSelectsDraft(ele)
35
- }
33
+ _adaptSubSelectsDraft({ SELECT: { from: {}, where: element.xpr } })
36
34
  }
37
35
  }
38
36
  }
@@ -8,7 +8,7 @@ const { postProcess } = require('../../common/utils/postProcessing')
8
8
  const _isSimpleCqnQuery = q => typeof q === 'object' && q !== null && !Array.isArray(q) && Object.keys(q).length > 0
9
9
 
10
10
  // for getRestrictions()
11
- const { getNormalizedRestrictions, getApplicableRestrictions } = require('./utils/restrictions')
11
+ const { getNormalizedRestrictions, getApplicableRestrictions } = require('../../common/generic/auth/restrictions.js')
12
12
  const WRITE_EVENTS = { CREATE: 1, NEW: 1, UPDATE: 1, PATCH: 1, DELETE: 1, CANCEL: 1, EDIT: 1 }
13
13
  const CRUD = Object.assign({ READ: 1 }, WRITE_EVENTS)
14
14
 
@@ -12,20 +12,13 @@ const { SELECT } = cds.ql
12
12
  * own utils
13
13
  */
14
14
 
15
- const _isSameEntity = (cqn, req) => {
16
- const where = cqn.UPDATE.where || []
17
- const persistentObj = Array.isArray(req._.partialPersistentState)
18
- ? req._.partialPersistentState[0]
19
- : req._.partialPersistentState
20
- if (!persistentObj) {
21
- // If no data was found we don't know if it is the same entity
22
- return false
23
- }
24
- const target = getDBTable(req.target)
25
- if (target.name !== (cqn.UPDATE.entity.ref && cqn.UPDATE.entity.ref[0]) && target.name !== cqn.UPDATE.entity) {
26
- return false
27
- }
15
+ const _isSameEntityInWhere = (where, target, persistentObj) => {
28
16
  for (let i = 0; i < where.length; i++) {
17
+ if (where[i].xpr) {
18
+ const res = _isSameEntityInWhere(where[i].xpr, target, persistentObj)
19
+ if (!res) return res
20
+ continue
21
+ }
29
22
  if (!where[i] || !where[i].ref || !target.elements[where[i].ref]) {
30
23
  continue
31
24
  }
@@ -40,6 +33,22 @@ const _isSameEntity = (cqn, req) => {
40
33
  return true
41
34
  }
42
35
 
36
+ const _isSameEntity = (cqn, req) => {
37
+ const where = cqn.UPDATE.where || []
38
+ const persistentObj = Array.isArray(req._.partialPersistentState)
39
+ ? req._.partialPersistentState[0]
40
+ : req._.partialPersistentState
41
+ if (!persistentObj) {
42
+ // If no data was found we don't know if it is the same entity
43
+ return false
44
+ }
45
+ const target = getDBTable(req.target)
46
+ if (target.name !== (cqn.UPDATE.entity.ref && cqn.UPDATE.entity.ref[0]) && target.name !== cqn.UPDATE.entity) {
47
+ return false
48
+ }
49
+ return _isSameEntityInWhere(where, target, persistentObj)
50
+ }
51
+
43
52
  const _getLinksOfCompTree = compositionTree => {
44
53
  const links = []
45
54
  for (const link of [...compositionTree.backLinks, ...compositionTree.customBackLinks]) {
@@ -216,6 +216,18 @@ const resolveNavigationTarget = (cqn, ref, model) => {
216
216
  return { target, elementName }
217
217
  }
218
218
 
219
+ const _getDataFromOncond = (onCond, parent) => {
220
+ return onCond.reduce((res, e) => {
221
+ if (e.xpr) {
222
+ return Object.assign(res, _getDataFromOncond(e.xpr, parent))
223
+ }
224
+ if (!e.ref || e.ref[0] !== '$$parent') return res
225
+ const fk = e.ref.slice(1).join('_')
226
+ if (!parent.keys[fk]) res[fk] = null
227
+ return res
228
+ }, {})
229
+ }
230
+
219
231
  // eslint-disable-next-line complexity
220
232
  const getSetNullParentForeignKeyCQNs = async (model, req, dbQuery) => {
221
233
  const cqns = []
@@ -229,12 +241,7 @@ const getSetNullParentForeignKeyCQNs = async (model, req, dbQuery) => {
229
241
  const element = parent.elements[elementName]
230
242
  if (element && element._isCompositionEffective && element.is2one) {
231
243
  const onCond = element && parent._relations[element.name].join('$$whatever', '$$parent')
232
- const data = onCond.reduce((res, e) => {
233
- if (!e.ref || e.ref[0] !== '$$parent') return res
234
- const fk = e.ref.slice(1).join('_')
235
- if (!parent.keys[fk]) res[fk] = null
236
- return res
237
- }, {})
244
+ const data = _getDataFromOncond(onCond, parent)
238
245
  cqn.data(data)
239
246
  cqn.__4delete = true
240
247
  if (Object.keys(data).length) cqns.push(cqn)
@@ -247,12 +254,7 @@ const getSetNullParentForeignKeyCQNs = async (model, req, dbQuery) => {
247
254
  for (const element of comp2oneParents) {
248
255
  const parent = element.parent
249
256
  const onCond = parent._relations[element.name].join('$$child', '$$parent')
250
- const data = onCond.reduce((res, e) => {
251
- if (!e.ref || e.ref[0] !== '$$parent') return res
252
- const fk = e.ref.slice(1).join('_')
253
- if (!parent.keys[fk]) res[fk] = null
254
- return res
255
- }, {})
257
+ const data = _getDataFromOncond(onCond, parent)
256
258
  const columns = getColumns(parent, { _4db: true, onlyKeys: true }).map(c => ({ ref: ['$$parent', c.name] }))
257
259
  const as = query.DELETE.from.as || (query.DELETE.from.ref && query.DELETE.from.ref[0]) || query.DELETE.from
258
260
  const selectCQN = SELECT.from(`${parent.name} as $$parent`)
@@ -2,6 +2,7 @@ const cds = require('../../../cds')
2
2
 
3
3
  const { reject, getRejectReason, resolveUserAttrs, getAuthRelevantEntity } = require('./utils')
4
4
  const { DRAFT_EVENTS, MOD_EVENTS } = require('./constants')
5
+ const { getNormalizedPlainRestrictions } = require('./restrictions')
5
6
 
6
7
  const { cqn2cqn4sql } = require('../../utils/cqn2cqn4sql')
7
8
 
@@ -250,7 +251,8 @@ async function handler(req) {
250
251
  // > no applicable restrictions -> 403
251
252
  reject(req, getRejectReason(req, '@restrict', definition))
252
253
  }
253
-
254
+ // normalize
255
+ restrictions = getNormalizedPlainRestrictions(restrictions, definition)
254
256
  // at least one if the user's roles grants unrestricted access => done
255
257
  if (restrictions.some(restrict => !restrict.where)) return
256
258
 
@@ -72,7 +72,14 @@ const getApplicableRestrictions = (restrictions, event, user) => {
72
72
  })
73
73
  }
74
74
 
75
+ const getNormalizedPlainRestrictions = (restrictions, definition) => {
76
+ const result = []
77
+ for (const restriction of restrictions) _addNormalizedRestrict(restriction, result, definition)
78
+ return result
79
+ }
80
+
75
81
  module.exports = {
76
82
  getNormalizedRestrictions,
77
- getApplicableRestrictions
83
+ getApplicableRestrictions,
84
+ getNormalizedPlainRestrictions
78
85
  }
@@ -184,6 +184,7 @@ const _getBoundActionBindingParameter = req => {
184
184
  }
185
185
 
186
186
  function _handler(req) {
187
+ if (!req.query) return // FIXME: the code below expects req.query to be defined
187
188
  if (!req.target) return
188
189
 
189
190
  const template = getTemplate('app-input', this, req.target, { pick: _pick })
@@ -57,6 +57,7 @@ const _pick = element => {
57
57
 
58
58
  function _handler(req) {
59
59
  if (req.method !== 'PUT') return
60
+ if (!req.query) return // FIXME: the code below expects req.query to be defined
60
61
  if (!req.target) return
61
62
 
62
63
  // not for payloads with stream properties