@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +54 -3
  2. package/_i18n/i18n.properties +4 -7
  3. package/eslint.config.mjs +1 -1
  4. package/lib/compile/etc/properties.js +2 -2
  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/sql.js +15 -8
  13. package/lib/core/linked-csn.js +10 -4
  14. package/lib/dbs/cds-deploy.js +2 -2
  15. package/lib/env/cds-env.js +76 -66
  16. package/lib/env/defaults.js +1 -0
  17. package/lib/i18n/bundles.js +2 -1
  18. package/lib/i18n/localize.js +2 -2
  19. package/lib/index.js +24 -18
  20. package/lib/ql/CREATE.js +11 -6
  21. package/lib/ql/DELETE.js +12 -9
  22. package/lib/ql/DROP.js +15 -8
  23. package/lib/ql/INSERT.js +19 -14
  24. package/lib/ql/SELECT.js +95 -168
  25. package/lib/ql/UPDATE.js +23 -14
  26. package/lib/ql/UPSERT.js +15 -2
  27. package/lib/ql/Whereable.js +44 -118
  28. package/lib/ql/cds-ql.js +222 -28
  29. package/lib/ql/{Query.js → cds.ql-Query.js} +52 -41
  30. package/lib/ql/cds.ql-predicates.js +133 -0
  31. package/lib/ql/cds.ql-projections.js +111 -0
  32. package/lib/ql/cqn.d.ts +146 -0
  33. package/lib/srv/cds-connect.js +3 -3
  34. package/lib/srv/cds-serve.js +2 -2
  35. package/lib/srv/cds.Service.js +132 -0
  36. package/lib/srv/{srv-api.js → cds.ServiceClient.js} +16 -71
  37. package/lib/srv/cds.ServiceProvider.js +20 -0
  38. package/lib/srv/factory.js +20 -8
  39. package/lib/srv/protocols/hcql.js +2 -3
  40. package/lib/srv/protocols/index.js +3 -3
  41. package/lib/srv/srv-dispatch.js +7 -6
  42. package/lib/srv/srv-handlers.js +103 -113
  43. package/lib/srv/srv-methods.js +14 -14
  44. package/lib/srv/srv-tx.js +5 -3
  45. package/lib/utils/cds-utils.js +2 -2
  46. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +3 -3
  47. package/libx/_runtime/cds.js +2 -1
  48. package/libx/_runtime/common/aspects/service.js +25 -0
  49. package/libx/_runtime/common/generic/auth/index.js +5 -0
  50. package/libx/_runtime/common/generic/auth/restrict.js +36 -14
  51. package/libx/_runtime/common/generic/auth/service.js +24 -0
  52. package/libx/_runtime/common/generic/auth/utils.js +14 -6
  53. package/libx/_runtime/common/generic/etag.js +1 -1
  54. package/libx/_runtime/common/utils/cqn.js +1 -2
  55. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  56. package/libx/_runtime/common/utils/generateOnCond.js +7 -3
  57. package/libx/_runtime/common/utils/postProcess.js +4 -1
  58. package/libx/_runtime/common/utils/restrictions.js +1 -0
  59. package/libx/_runtime/fiori/lean-draft.js +53 -42
  60. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -1
  61. package/libx/_runtime/remote/Service.js +2 -0
  62. package/libx/_runtime/remote/utils/client.js +12 -0
  63. package/libx/odata/ODataAdapter.js +2 -1
  64. package/libx/odata/index.js +5 -3
  65. package/libx/odata/middleware/batch.js +4 -0
  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 +64 -30
  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 +6 -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,6 +1,6 @@
1
1
  const cds = require('../../cds')
2
2
  const { SELECT, INSERT, DELETE, UPDATE } = cds.ql
3
- const Query = require('../../../../lib/ql/Query')
3
+ const Query = require('../../../../lib/ql/cds.ql-Query')
4
4
  const { resolveView } = require('./resolveView')
5
5
  const { ensureNoDraftsSuffix, getDraftColumnsCQNForDraft, ensureDraftsSuffix } = require('./draft')
6
6
  const { flattenStructuredSelect, OPERATIONS_MAP } = require('./structured')
@@ -29,7 +29,7 @@ const _adaptRefs = (onCond, path, { select, join }) => {
29
29
  return onCond.map(_adaptEl)
30
30
  }
31
31
 
32
- const replace$selfAndAliasOnCOnd = (xpr, csnElement, aliases, path) => {
32
+ const replace$selfAndAliasOnCond = (xpr, csnElement, aliases, path) => {
33
33
  const selfIndex = xpr.findIndex(({ ref }) => ref?.[0] === '$self')
34
34
  if (selfIndex != -1) {
35
35
  let backLinkIndex
@@ -50,7 +50,7 @@ const replace$selfAndAliasOnCOnd = (xpr, csnElement, aliases, path) => {
50
50
  for (let i = 0; i < xpr.length; i++) {
51
51
  const element = xpr[i]
52
52
  if (element.xpr) {
53
- replace$selfAndAliasOnCOnd(element.xpr, csnElement, aliases, path)
53
+ replace$selfAndAliasOnCond(element.xpr, csnElement, aliases, path)
54
54
  continue
55
55
  }
56
56
 
@@ -65,6 +65,10 @@ const replace$selfAndAliasOnCOnd = (xpr, csnElement, aliases, path) => {
65
65
  element.ref = _toRef(undefined, element.ref.slice(0)).ref
66
66
  continue
67
67
  }
68
+ //no alias for special $now variable
69
+ if (element.ref[0] === '$now') {
70
+ continue
71
+ }
68
72
 
69
73
  if (element.ref[0] === aliases.join || element.ref[0] === aliases.select) {
70
74
  // nothing todo here, as already right alias
@@ -83,7 +87,7 @@ const _args = (csnElement, path, aliases) => {
83
87
  if (!csnElement._isSelfManaged) return _adaptRefs(onCond, path, aliases)
84
88
 
85
89
  const onCondCopy = JSON.parse(JSON.stringify(onCond))
86
- replace$selfAndAliasOnCOnd(onCondCopy, csnElement, aliases, path)
90
+ replace$selfAndAliasOnCond(onCondCopy, csnElement, aliases, path)
87
91
 
88
92
  return onCondCopy
89
93
  }
@@ -73,7 +73,10 @@ const postProcess = (query, result, service, onlySelectAliases = false) => {
73
73
  if (query.DELETE) return result
74
74
 
75
75
  if (query.SELECT) {
76
- if (query.SELECT.columns?.find(col => col.func === 'count' && col.as === '$count')) return [{ $count: result }]
76
+ if (query.SELECT.columns?.find(col => col.func === 'count' && col.as === '$count')) {
77
+ if (result[0] && '$count' in result[0]) return result
78
+ return [{ $count: result }]
79
+ }
77
80
 
78
81
  handleAliasInResult(query.SELECT.columns, result)
79
82
 
@@ -3,6 +3,7 @@ const cds = require('../../cds')
3
3
 
4
4
  const $cache = Symbol('service contains any restrictions cache')
5
5
 
6
+ /** @param {import('../../../../lib/srv/cds.Service')} srv */
6
7
  const containsAnyRestrictions = srv => {
7
8
  let { model } = srv
8
9
  if (srv.isExtensible) model = cds.context?.model || srv.model // REVISIT: extensions are not allowed to add or change restrictions, are they?
@@ -197,13 +197,15 @@ const _lock = {
197
197
 
198
198
  const _redirectRefToDrafts = (ref, model) => {
199
199
  const [root, ...tail] = ref
200
- const draft = model.definitions[root.id || root].drafts
200
+ const target = model.definitions[root.id || root]
201
+ const draft = target.drafts || target
201
202
  return [root.id ? { ...root, id: draft.name } : draft.name, ...tail]
202
203
  }
203
204
 
204
205
  const _redirectRefToActives = (ref, model) => {
205
206
  const [root, ...tail] = ref
206
- const active = model.definitions[root.id || root].actives
207
+ const target = model.definitions[root.id || root]
208
+ const active = target.actives || target
207
209
  return [root.id ? { ...root, id: active.name } : active.name, ...tail]
208
210
  }
209
211
 
@@ -325,6 +327,11 @@ const h = cds.ApplicationService.prototype.handle
325
327
  cds.ApplicationService.prototype.handle = async function (req) {
326
328
  const handle = h.bind(this)
327
329
 
330
+ if (req.event === 'DISCARD') req.event = 'CANCEL'
331
+ if (req.event === 'SAVE') {
332
+ req.event = 'draftActivate'
333
+ req.query ??= SELECT.from(req.target, req.data) //> support simple srv.send('SAVE',entity,...)
334
+ }
328
335
  if (
329
336
  !req.query ||
330
337
  // REVISIT: Currently all requests in an Object Page to nested composition targets, e.g. Incidents(ID)/conversation are also Draft Requests which seems wrong overkill -> is that required?
@@ -492,14 +499,14 @@ cds.ApplicationService.prototype.handle = async function (req) {
492
499
 
493
500
  // It needs to be redirected to drafts
494
501
  if (req.event === 'NEW' || req.event === 'CANCEL' || req.event === 'draftPrepare') {
495
- req.target = req.target.drafts
502
+ if (!req.target.isDraft) req.target = req.target.drafts // COMPAT: also support these events for actives
496
503
 
497
- if (query.INSERT?.into) {
504
+ if (query.INSERT) {
498
505
  if (typeof query.INSERT.into === 'string') query.INSERT.into = req.target.name
499
506
  else if (query.INSERT.into.ref) query.INSERT.into.ref = _redirectRefToDrafts(query.INSERT.into.ref, this.model)
500
- } else if (query.DELETE?.from?.ref) {
507
+ } else if (query.DELETE) {
501
508
  query.DELETE.from.ref = _redirectRefToDrafts(query.DELETE.from.ref, this.model)
502
- } else if (query.SELECT?.from?.ref) {
509
+ } else if (query.SELECT) {
503
510
  query.SELECT.from.ref = _redirectRefToDrafts(query.SELECT.from.ref, this.model)
504
511
  }
505
512
 
@@ -564,8 +571,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
564
571
  req.reject({ code: 428, statusCode: 428 })
565
572
  }
566
573
 
567
- const targetDraft = req.target.drafts
568
- const cols = expandStarStar(targetDraft, true)
574
+ const cols = expandStarStar(req.target.drafts, true)
569
575
 
570
576
  // Use `run` (since also etags might need to be checked)
571
577
  // REVISIT: Find a better approach (`etag` as part of CQN?)
@@ -1470,7 +1476,7 @@ function expandStarStar(target, draftActivate, recursion = new Map()) {
1470
1476
  'NULL',
1471
1477
  'then',
1472
1478
  { val: null },
1473
- 'else',
1479
+ 'else',
1474
1480
  { val: '' },
1475
1481
  'end'
1476
1482
  ],
@@ -1511,6 +1517,7 @@ async function onNew(req) {
1511
1517
 
1512
1518
  if (req.target.actives['@Capabilities.InsertRestrictions.Insertable'] === false || req.target.actives['@readonly'])
1513
1519
  req.reject({ code: 405, statusCode: 405 })
1520
+ req.query ??= INSERT.into(req.subject).entries(req.data || {}) //> support simple srv.send('NEW',entity,...)
1514
1521
  const isDirectAccess = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
1515
1522
  // Only allowed for pseudo draft roots (entities with this action)
1516
1523
  if (isDirectAccess && !req.target.actives['@Common.DraftRoot.ActivationAction'])
@@ -1592,8 +1599,10 @@ async function onNew(req) {
1592
1599
  async function onEdit(req) {
1593
1600
  LOG.debug('edit active')
1594
1601
 
1602
+ req.query ??= SELECT.from(req.target, req.data).where({ IsActiveEntity: true }) //> support simple srv.send('EDIT',entity,...)
1603
+
1595
1604
  // use symbol for _draftParams
1596
- const draftParams = req.query[DRAFT_PARAMS]
1605
+ const draftParams = req.query[DRAFT_PARAMS] || { IsActiveEntity: true } // REVISIT: can draftParams in the edit caser ever be undefined or other than IsActiveEntity=true ?
1597
1606
  if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== true) {
1598
1607
  req.reject({
1599
1608
  code: 400,
@@ -1692,7 +1701,7 @@ async function onEdit(req) {
1692
1701
  }
1693
1702
 
1694
1703
  const cqns = [
1695
- cds.env.fiori.read_actives_from_db ? this._datasource.run(selectActiveCQN) : this.run(selectActiveCQN),
1704
+ cds.env.fiori.read_actives_from_db ? cds.db.run(selectActiveCQN) : this.run(selectActiveCQN),
1696
1705
  existingDraft
1697
1706
  ]
1698
1707
 
@@ -1713,7 +1722,7 @@ async function onEdit(req) {
1713
1722
 
1714
1723
  ;[res, draft] = await _promiseAll([
1715
1724
  // REVISIT: inofficial compat flag just in case it breaks something -> do not document
1716
- cds.env.fiori.read_actives_from_db ? this._datasource.run(selectActiveCQN) : this.run(selectActiveCQN),
1725
+ cds.env.fiori.read_actives_from_db ? cds.db.run(selectActiveCQN) : this.run(selectActiveCQN),
1717
1726
  // no user check must be done here...
1718
1727
  existingDraft
1719
1728
  ])
@@ -1747,13 +1756,12 @@ async function onEdit(req) {
1747
1756
  InProcessByUser: req.user.id
1748
1757
  })
1749
1758
 
1750
- const targetDraft = req.target.drafts
1751
1759
  // is set to `null` on srv layer
1752
1760
  res.DraftAdministrativeData_DraftUUID = DraftUUID
1753
1761
  res.HasActiveEntity = true
1754
1762
  delete res.DraftAdministrativeData
1755
1763
  // change to db run
1756
- await INSERT.into(targetDraft).entries(res)
1764
+ await INSERT.into(req.target.drafts).entries(res)
1757
1765
 
1758
1766
  // REVISIT: we need to use okra API here because it must be set in the batched request
1759
1767
  // status code must be set in handler to allow overriding for FE V2
@@ -1783,8 +1791,9 @@ async function onEdit(req) {
1783
1791
  async function onCancel(req) {
1784
1792
  LOG.debug('delete draft')
1785
1793
 
1794
+ req.query ??= DELETE(req.target, req.data) //> support simple srv.send('CANCEL',entity,...)
1786
1795
  const activeRef = _redirectRefToActives(req.query.DELETE.from.ref, this.model)
1787
- const draftParams = req.query[DRAFT_PARAMS]
1796
+ const draftParams = req.query[DRAFT_PARAMS] || { IsActiveEntity: false } // REVISIT: can draftParams in the cancel case ever be undefined or other than IsActiveEntity=false ?
1788
1797
 
1789
1798
  const draftQuery = SELECT.one
1790
1799
  .from({ ref: req.query.DELETE.from.ref })
@@ -1801,7 +1810,7 @@ async function onCancel(req) {
1801
1810
  if (!cds.context.user._is_privileged && processByUser && processByUser !== cds.context.user.id)
1802
1811
  req.reject({ code: 403, statusCode: 403, message: 'DRAFT_LOCKED_BY_ANOTHER_USER', args: [processByUser] })
1803
1812
  }
1804
- const draftDeleteQuery = DELETE.from({ ref: req.query.DELETE.from.ref })
1813
+ const draftDeleteQuery = DELETE.from({ ref: req.query.DELETE.from.ref }) // REVISIT: Isn't that == req.query ?
1805
1814
  const queries = !draft
1806
1815
  ? []
1807
1816
  : [draftParams.IsActiveEntity === false ? this.run(draftDeleteQuery, req.data) : draftDeleteQuery]
@@ -1900,38 +1909,40 @@ const _readAfterDraftAction = async function ({ req, payload, action }) {
1900
1909
  }
1901
1910
  }
1902
1911
 
1903
- module.exports = {
1904
- impl() {
1905
- if (!this.new)
1906
- this.new = function (entity, data) {
1907
- return this.send({ event: 'NEW', query: INSERT.into(entity).entries(data ?? {}) })
1908
- }
1909
- if (!this.cancel)
1910
- this.cancel = function (entity, data) {
1911
- return this.send({ event: 'CANCEL', query: DELETE(entity, data) })
1912
- }
1913
- if (!this.edit)
1914
- this.edit = function (entity, data) {
1915
- if ((typeof entity === 'string' && entity.endsWith('.drafts')) || entity.isDraft)
1916
- throw new Error('Action `edit` must be called on the active entity')
1917
- return this.send({ event: 'EDIT', query: SELECT.from(entity, data).where({ IsActiveEntity: true }) })
1918
- // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ makes sure draftParams are set
1919
- }
1920
- if (!this.save)
1921
- this.save = function (entity, data) {
1922
- // a bit fishy to demand registering it on drafts since `SAVE` is usually just a shortcut for `['CREATE', 'UPDATE']`
1923
- // and is typically registered for active entities. Hence, we allow to register it on both and redirect to drafts
1924
- const _entity =
1925
- typeof entity === 'string' ? (entity.endsWith('.drafts') ? entity : entity + '.drafts') : entity?.drafts
1926
- return this.send({ event: 'draftActivate', query: SELECT.from(_entity, data) })
1912
+ // REVISIT: Looking at the simplified impls, I wonder if there's really much added convenience.
1913
+ // Even more as these events are very rarely sent from programmatic clients, if at all, but rather from Fiori clients only.
1914
+ cds.extend(cds.ApplicationService).with(
1915
+ class {
1916
+ new(draft, key) {
1917
+ return {
1918
+ then: (r, e) => this.send('NEW', draft, key).then(r, e),
1919
+ for: key => this.send('EDIT', typeof draft === 'string' ? draft.replace(/\.drafts$/, '') : draft.actives, key)
1927
1920
  }
1921
+ }
1922
+ edit(active, key) {
1923
+ return this.send('EDIT', active, key)
1924
+ }
1925
+ save(draft, key) {
1926
+ return this.send('SAVE', draft, key)
1927
+ }
1928
+ cancel(draft, key) {
1929
+ return this.send('CANCEL', draft, key)
1930
+ }
1931
+ discard(draft, key) {
1932
+ return this.send('DISCARD', draft, key)
1933
+ }
1934
+ }
1935
+ )
1928
1936
 
1929
- if (!this._datasource) this._datasource = cds.db
1937
+ module.exports = {
1938
+ impl() {
1939
+ // REVISIT: don't pollute services... -> do we really need this?
1940
+ Object.defineProperty(this, '_datasource', { value: cds.db })
1930
1941
 
1931
1942
  function _wrapped(handler, isActiveEntity) {
1932
1943
  const fn = function (req, next) {
1933
1944
  if (!req.target?.drafts || (isActiveEntity && req.target.isDraft) || (!isActiveEntity && !req.target.isDraft))
1934
- return next.call(this)
1945
+ return next?.()
1935
1946
  return handler.call(this, req, next)
1936
1947
  }
1937
1948
  if (handler._initial) fn._initial = true
@@ -19,7 +19,7 @@ class EndpointRegistry {
19
19
  cds.app.use(basePath, cds.middlewares.context())
20
20
  cds.app.use(basePath, jwt_auth(cds.requires.auth))
21
21
  cds.app.use(basePath, (err, req, res, next) => {
22
- if (err === 401) res.send(401)
22
+ if (err === 401) res.sendStatus(401)
23
23
  else next(err)
24
24
  })
25
25
  }
@@ -275,6 +275,8 @@ class RemoteService extends cds.Service {
275
275
  result = typeof query === 'object' && query.SELECT?.one && Array.isArray(result) ? result[0] : result
276
276
  return result
277
277
  })
278
+
279
+ return super.init()
278
280
  }
279
281
 
280
282
  // Overload .handle in order to resolve projections up to a definition that is known by the remote service instance.
@@ -33,9 +33,21 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
33
33
  if (jwt) destination.headers.authorization = `Bearer ${jwt}`
34
34
  else LOG._warn && LOG.warn('Missing JWT token for forwardAuthToken')
35
35
  }
36
+
36
37
  // Cloud SDK throws error if useCache is activated and jwt is undefined
37
38
  if (destination.jwt === undefined) delete destination.useCache
38
39
 
40
+ // prevent token exchange if ias token for technical user (unofficial!!!)
41
+ if (cds.env.features.remote_prevent_token_exchange) {
42
+ try {
43
+ const jsonwebtoken = require('jsonwebtoken')
44
+ const decoded = jsonwebtoken.decode(jwt)
45
+ if (decoded.app_tid && decoded.azp && decoded.azp === decoded.sub) destination.iasToXsuaaTokenExchange = false
46
+ } catch (error) {
47
+ LOG._debug && LOG.debug('Preventing token exchange failed:', error)
48
+ }
49
+ }
50
+
39
51
  if (LOG._debug) {
40
52
  const req2log = { headers: _sanitizeHeaders({ ...requestConfig.headers }) }
41
53
  if (requestConfig.method !== 'GET' && requestConfig.method !== 'DELETE')
@@ -96,7 +96,8 @@ class ODataAdapter extends HttpAdapter {
96
96
  return jsonBodyParser(req, res, next)
97
97
  })
98
98
  // batch
99
- .post('/\\$batch', require('./middleware/batch')(this))
99
+ // .all is used deliberately instead of .use so that the matched path is not stripped from req properties
100
+ .all('/\\$batch', require('./middleware/batch')(this))
100
101
  // handle
101
102
  // REVISIT: with old adapter, we return 405 for HEAD requests -> check OData spec
102
103
  .head('*', (_, res) => res.sendStatus(405))
@@ -1,3 +1,5 @@
1
+ /** @typedef {import('../../lib/srv/cds.Service')} Service */
2
+
1
3
  const cds = require('../../')
2
4
  const { SELECT } = cds.ql
3
5
  const { decodeURIComponent } = cds.utils
@@ -79,10 +81,10 @@ const _2query = cqn => {
79
81
 
80
82
  const enhanceCqn = (cqn, options) => {
81
83
  if (options.afterburner) {
82
- const { service, protocol } = options
83
- let { model, definition: { name: namespace } } = service // prettier-ignore
84
+ /** @type Service */ const service = options.service
85
+ let { model, namespace } = service // prettier-ignore
84
86
  if (service.isExtensible) model = cds.context?.model || model
85
- cqn = options.afterburner(cqn, model, namespace, protocol)
87
+ cqn = options.afterburner(cqn, model, namespace, options.protocol)
86
88
  }
87
89
 
88
90
  const query = _2query(cqn)
@@ -561,6 +561,10 @@ module.exports = adapter => {
561
561
  })
562
562
 
563
563
  return function odata_batch(req, res, next) {
564
+ if (req.method !== 'POST') {
565
+ throw cds.error(`Method ${req.method} is not allowed for calls to $batch endpoint`, { code: 405 })
566
+ }
567
+
564
568
  if (req.headers['content-type'].includes('application/json')) {
565
569
  return _processBatch(service, router, req, res, next)
566
570
  }
@@ -90,8 +90,8 @@ module.exports = (adapter, isUpsert) => {
90
90
  handleSapMessages(cdsReq, req, res)
91
91
 
92
92
  // REVISIT: invoke service.on('error') for failed batch subrequests
93
- if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
94
- for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
93
+ if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
94
+ for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
95
95
  }
96
96
 
97
97
  next(err)
@@ -65,8 +65,8 @@ module.exports = adapter => {
65
65
  handleSapMessages(cdsReq, req, res)
66
66
 
67
67
  // REVISIT: invoke service.on('error') for failed batch subrequests
68
- if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
69
- for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
68
+ if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
69
+ for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
70
70
  }
71
71
 
72
72
  next(err)
@@ -146,8 +146,8 @@ module.exports = adapter => {
146
146
  handleSapMessages(cdsReq, req, res)
147
147
 
148
148
  // REVISIT: invoke service.on('error') for failed batch subrequests
149
- if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
150
- for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
149
+ if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
150
+ for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
151
151
  }
152
152
 
153
153
  next(err)
@@ -101,11 +101,11 @@ const _isToOneAssoc = query =>
101
101
  query.SELECT.from.ref.length > 1 && typeof query.SELECT.from.ref.slice(-1)[0] === 'string'
102
102
 
103
103
  const _count = result => {
104
- return Array.isArray(result)
105
- ? result.reduce((acc, val) => {
106
- return acc + (val?.$count ?? val?._counted_ ?? (Array.isArray(val) && _count(val))) || 0
107
- }, 0)
108
- : (result.$count ?? result._counted_ ?? 0)
104
+ if (Array.isArray(result))
105
+ return result.reduce((acc, val) => {
106
+ return acc + (val?.$count ?? val?._counted_ ?? (Array.isArray(val) && _count(val))) || 0
107
+ }, 0)
108
+ else return result.$count ?? result._counted_ ?? 0
109
109
  }
110
110
 
111
111
  // REVISIT: integrate with default handler
@@ -136,14 +136,16 @@ const _handleArrayOfQueriesFactory = adapter => {
136
136
  result: result[0],
137
137
  isCollection: !req._query[0].SELECT.one
138
138
  })
139
- for (let i = 0; i < result.length; i++) {
139
+ // Skip first query, as its context is represented in the main context
140
+ for (let i = 1; i < result.length; i++) {
140
141
  const { context: subOdataContext } = getODataMetadata(req._query[i], {
141
142
  result: result[i],
142
143
  isCollection: !req._query[i].SELECT.one
143
144
  })
144
145
  // Add OData context, if it deviates from main context
145
- if (i !== 0 && mainOdataContext !== subOdataContext) {
146
- result[i].forEach(entry => (entry['@odata.context'] = subOdataContext))
146
+ if (mainOdataContext !== subOdataContext) {
147
+ // OData spec: "If present, the context control information MUST be the first property in the JSON object."
148
+ result[i] = result[i].map(entry => ({ '@odata.context': subOdataContext, ...entry }))
147
149
  }
148
150
  }
149
151
 
@@ -161,8 +163,8 @@ const _handleArrayOfQueriesFactory = adapter => {
161
163
  handleSapMessages(cdsReq, req, res)
162
164
 
163
165
  // REVISIT: invoke service.on('error') for failed batch subrequests
164
- if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
165
- for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
166
+ if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
167
+ for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
166
168
  }
167
169
 
168
170
  next(err)
@@ -277,8 +279,8 @@ module.exports = adapter => {
277
279
  handleSapMessages(cdsReq, req, res)
278
280
 
279
281
  // REVISIT: invoke service.on('error') for failed batch subrequests
280
- if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
281
- for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
282
+ if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
283
+ for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
282
284
  }
283
285
 
284
286
  next(err)
@@ -44,10 +44,21 @@ module.exports = adapter => {
44
44
 
45
45
  const srvEntities = model.entities(service.definition.name)
46
46
 
47
- // REVISIT: How to identify the exposed entities? api.ignore, autoexposed, ...
48
- const exposedEntities = Object.keys(srvEntities).filter(
49
- entityName => !srvEntities[entityName]['@cds.api.ignore'] && entityName !== 'DraftAdministrativeData'
50
- )
47
+ const exposedEntities = []
48
+ for (const e in srvEntities) {
49
+ if (e === 'DraftAdministrativeData') continue
50
+
51
+ const entity = srvEntities[e]
52
+ if (entity['@cds.api.ignore']) continue
53
+ if (cds.env.effective.odata.containment && csnService._containedEntities.has(entity.name)) continue
54
+
55
+ const odataName = e.replace(/\./g, '_')
56
+ const obj = { name: odataName, url: odataName }
57
+
58
+ if (entity['@odata.singleton'] || entity['@odata.singleton.nullable']) obj.kind = 'Singleton'
59
+
60
+ exposedEntities.push(obj)
61
+ }
51
62
 
52
63
  csnService.srvDocEtag = generateEtag(JSON.stringify(exposedEntities))
53
64
  res.set('ETag', csnService.srvDocEtag)
@@ -60,10 +71,7 @@ module.exports = adapter => {
60
71
  return res.json({
61
72
  '@odata.context': odataContext,
62
73
  '@odata.metadataEtag': csnService.srvDocEtag,
63
- value: exposedEntities.map(e => {
64
- const e_ = e.replace(/\./g, '_')
65
- return { name: e_, url: e_ }
66
- })
74
+ value: exposedEntities
67
75
  })
68
76
  }
69
77
  }
@@ -153,8 +153,8 @@ module.exports = adapter => {
153
153
  }
154
154
 
155
155
  // REVISIT: invoke service.on('error') for failed batch subrequests
156
- if (cdsReq.http.req.path.startsWith('/$batch') && service._handlers._error.length) {
157
- for (const each of service._handlers._error) each.handler.call(service, err, cdsReq)
156
+ if (cdsReq.http.req.path.startsWith('/$batch') && service.handlers._error.length) {
157
+ for (const each of service.handlers._error) each.handler.call(service, err, cdsReq)
158
158
  }
159
159
 
160
160
  // continue with caught error