@sap/cds 7.8.2 → 7.9.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 (138) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/_i18n/i18n_ar.properties +3 -0
  3. package/_i18n/i18n_cs.properties +3 -0
  4. package/_i18n/i18n_da.properties +3 -0
  5. package/_i18n/i18n_es_MX.properties +3 -0
  6. package/_i18n/i18n_fi.properties +3 -0
  7. package/_i18n/i18n_hu.properties +6 -0
  8. package/_i18n/i18n_ko.properties +3 -0
  9. package/_i18n/i18n_ms.properties +3 -0
  10. package/_i18n/i18n_nl.properties +3 -0
  11. package/_i18n/i18n_no.properties +3 -0
  12. package/_i18n/i18n_ro.properties +3 -0
  13. package/_i18n/i18n_sv.properties +3 -0
  14. package/_i18n/i18n_th.properties +3 -0
  15. package/_i18n/i18n_tr.properties +6 -0
  16. package/_i18n/i18n_zh_TW.properties +3 -0
  17. package/bin/serve.js +5 -5
  18. package/lib/auth/basic-auth.js +1 -1
  19. package/lib/compile/cdsc.js +33 -6
  20. package/lib/compile/etc/_localized.js +14 -7
  21. package/lib/compile/for/lean_drafts.js +9 -0
  22. package/lib/compile/to/edm-files.js +116 -0
  23. package/lib/compile/to/edm.js +8 -1
  24. package/lib/compile/to/hdbtabledata.js +3 -3
  25. package/lib/compile/to/sql.js +4 -2
  26. package/lib/compile/to/srvinfo.js +6 -5
  27. package/lib/compile/to/yaml.js +22 -21
  28. package/lib/dbs/cds-deploy.js +5 -6
  29. package/lib/env/cds-env.js +7 -0
  30. package/lib/env/cds-requires.js +20 -1
  31. package/lib/env/defaults.js +21 -5
  32. package/lib/env/schemas/cds-package.js +1 -1
  33. package/lib/env/schemas/cds-rc.js +85 -4
  34. package/lib/index.js +1 -1
  35. package/lib/linked/entities.js +10 -0
  36. package/lib/linked/models.js +1 -1
  37. package/lib/plugins.js +1 -1
  38. package/lib/ql/INSERT.js +17 -3
  39. package/lib/ql/Query.js +4 -0
  40. package/lib/ql/infer.js +1 -1
  41. package/lib/req/request.js +1 -1
  42. package/lib/srv/cds-serve.js +1 -0
  43. package/lib/srv/middlewares/cds-context.js +1 -1
  44. package/lib/srv/protocols/odata-v4.js +5 -6
  45. package/lib/srv/srv-models.js +9 -2
  46. package/lib/utils/cds-test.js +2 -0
  47. package/lib/utils/cds-utils.js +9 -4
  48. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
  49. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  50. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
  52. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
  53. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
  54. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
  55. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
  56. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  57. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
  58. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +38 -1
  60. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
  63. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
  64. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
  65. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
  66. package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
  67. package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
  68. package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
  69. package/libx/_runtime/common/generic/auth/index.js +2 -0
  70. package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
  71. package/libx/_runtime/common/generic/auth/restrict.js +6 -5
  72. package/libx/_runtime/common/generic/auth/utils.js +1 -1
  73. package/libx/_runtime/common/generic/crud.js +5 -8
  74. package/libx/_runtime/common/generic/etag.js +8 -6
  75. package/libx/_runtime/common/generic/sorting.js +2 -2
  76. package/libx/_runtime/common/i18n/messages.properties +1 -0
  77. package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
  78. package/libx/_runtime/common/utils/compareJson.js +274 -0
  79. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  80. package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
  81. package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
  82. package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
  83. package/libx/_runtime/common/utils/resolveView.js +0 -16
  84. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
  85. package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
  86. package/libx/_runtime/common/utils/streamProp.js +9 -2
  87. package/libx/_runtime/common/utils/ucsn.js +1 -1
  88. package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
  89. package/libx/_runtime/db/generic/rewrite.js +7 -13
  90. package/libx/_runtime/fiori/generic/activate.js +1 -1
  91. package/libx/_runtime/fiori/generic/edit.js +1 -1
  92. package/libx/_runtime/fiori/generic/prepare.js +1 -1
  93. package/libx/_runtime/fiori/lean-draft.js +151 -46
  94. package/libx/_runtime/fiori/utils/handler.js +1 -1
  95. package/libx/_runtime/hana/execute.js +6 -2
  96. package/libx/_runtime/hana/pool.js +3 -0
  97. package/libx/_runtime/hana/search2cqn4sql.js +1 -1
  98. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
  99. package/libx/_runtime/messaging/event-broker.js +212 -0
  100. package/libx/_runtime/remote/Service.js +9 -32
  101. package/libx/_runtime/remote/utils/client.js +13 -21
  102. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
  103. package/libx/_runtime/sqlite/execute.js +8 -3
  104. package/libx/_runtime/ucl/Service.js +259 -0
  105. package/libx/common/assert/index.js +5 -11
  106. package/libx/common/assert/validation.js +6 -1
  107. package/libx/odata/index.js +47 -25
  108. package/libx/odata/middleware/batch.js +8 -7
  109. package/libx/odata/middleware/create.js +42 -16
  110. package/libx/odata/middleware/delete.js +18 -11
  111. package/libx/odata/middleware/metadata.js +15 -14
  112. package/libx/odata/middleware/operation.js +30 -40
  113. package/libx/odata/middleware/parse.js +2 -3
  114. package/libx/odata/middleware/read.js +59 -52
  115. package/libx/odata/middleware/service-document.js +7 -7
  116. package/libx/odata/middleware/stream.js +26 -24
  117. package/libx/odata/middleware/update.js +53 -92
  118. package/libx/odata/parse/afterburner.js +45 -47
  119. package/libx/odata/parse/grammar.peggy +3 -3
  120. package/libx/odata/parse/multipartToJson.js +10 -22
  121. package/libx/odata/parse/parser.js +1 -1
  122. package/libx/odata/utils/etag.js +13 -0
  123. package/libx/odata/utils/handler.js +120 -0
  124. package/libx/odata/utils/index.js +15 -2
  125. package/libx/odata/utils/metaInfo.js +410 -0
  126. package/libx/odata/utils/path.js +5 -2
  127. package/libx/odata/utils/readAfterWrite.js +23 -0
  128. package/libx/odata/utils/result.js +4 -5
  129. package/libx/rest/RestAdapter.js +4 -13
  130. package/libx/rest/middleware/parse.js +40 -7
  131. package/package.json +1 -1
  132. package/server.js +1 -0
  133. package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
  134. package/libx/_runtime/common/utils/thenable.js +0 -51
  135. package/libx/_runtime/rest/service.js +0 -2
  136. package/libx/odata/parse/parseToCqn.js +0 -39
  137. package/libx/rest/middleware/input.js +0 -54
  138. package/libx/rest/middleware/payload.js +0 -13
@@ -2,13 +2,18 @@ const cds = require('../../../')
2
2
  const { INSERT } = cds.ql
3
3
 
4
4
  const { toODataResult, postProcess } = require('../utils/result')
5
- const { calculateLocationHeader, getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
5
+ const {
6
+ calculateLocationHeader,
7
+ getKeysAndParamsFromPath,
8
+ handleSapMessages,
9
+ getPreferReturnHeader
10
+ } = require('../utils')
11
+ const { getDeepSelect, getSimpleSelectCQN } = require('../utils/handler')
6
12
 
7
13
  const { deepCopy } = require('../../_runtime/common/utils/copy')
8
14
 
9
- // REVISIT: move to or rewrite in libx/odata
10
- const { readAfterWrite } = require('../../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
11
- const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
15
+ const readAfterWrite = require('../utils/readAfterWrite')
16
+ const metaInfo = require('../utils/metaInfo')
12
17
 
13
18
  module.exports = srv =>
14
19
  function create(req, res, next) {
@@ -17,12 +22,12 @@ module.exports = srv =>
17
22
  target
18
23
  } = req._query
19
24
 
20
- if (one) {
21
- // REVISIT: don't use "SINGLETON" or "ENTITY" as that are okra terms
22
- throw Object.assign(
23
- new Error(`Method ${req.method} not allowed for ${target._isSingleton ? 'SINGLETON' : 'ENTITY'}`),
24
- { statusCode: 405 }
25
- )
25
+ // req.__proto__.method is set in case of upsert
26
+ const isUpsert = req.__proto__.method in { PUT: 1, PATCH: 1 }
27
+
28
+ if (one && !isUpsert) {
29
+ const msg = 'Method POST is not allowed for singletons and individual entities'
30
+ throw Object.assign(new Error(msg), { statusCode: 405 })
26
31
  }
27
32
 
28
33
  // payload & params
@@ -42,10 +47,17 @@ module.exports = srv =>
42
47
  // query
43
48
  const query = INSERT.into(from).entries(data)
44
49
 
50
+ // cdsReq.headers should contain merged headers of envelope and subreq
51
+ const headers = { ...cds.context.http.req.headers, ...req.headers }
52
+
45
53
  // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
46
- const cdsReq = new cds.Request({ query, params, req, res })
54
+ const cdsReq = new cds.Request({ query, params, headers, req, res })
47
55
  Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
48
56
 
57
+ // API for subrequests of $batch (or incoming request)
58
+ cdsReq.req = req
59
+ cdsReq.res = res
60
+
49
61
  // rewrite event for draft-enabled entities
50
62
  if (target._isDraftEnabled) cdsReq.event = 'NEW'
51
63
 
@@ -57,8 +69,12 @@ module.exports = srv =>
57
69
  return srv.dispatch(cdsReq).then(result => {
58
70
  handleSapMessages(cdsReq, req, res)
59
71
 
72
+ // generic handlers indicate that read after write is required
60
73
  if (cdsReq._.readAfterWrite) {
61
- return readAfterWrite(cdsReq, srv, { operation: { result } })
74
+ // const keys = cdsReq.target.keys?.filter(k => !k.isAssociation)?.reduce((prev, k) => { prev[k] = result[k]; return prev}, {} )
75
+ // const query = SELECT.one(cdsReq.query.INSERT.into, keys)
76
+ const query = cdsReq.event === 'NEW' ? getSimpleSelectCQN(cdsReq.target, result) : getDeepSelect(cdsReq)
77
+ return readAfterWrite(cdsReq, srv, query)
62
78
  }
63
79
 
64
80
  return result
@@ -66,15 +82,25 @@ module.exports = srv =>
66
82
  })
67
83
  .then(result => {
68
84
  // we use an extra then block, after getting the result, so the transaction is commited, before sending the response
85
+
86
+ // determine calculation based on result with req.data as fallback
87
+ if (!target._isSingleton)
88
+ res.set('location', calculateLocationHeader(cdsReq.target, srv, result || cdsReq.data))
89
+
69
90
  if (result == null) return res.sendStatus(204)
70
- const isMinimal = req._preferReturn === 'minimal'
91
+ const isMinimal = getPreferReturnHeader(req) === 'minimal'
71
92
  postProcess(cdsReq.target, srv, result, isMinimal)
93
+
72
94
  if (result['$etag']) res.set('etag', result['$etag'])
73
- res.set('location', calculateLocationHeader(cdsReq.target, srv, result))
74
95
  if (isMinimal) return res.sendStatus(204)
96
+
75
97
  const info = metaInfo(query, 'CREATE', srv, result, req)
76
98
  result = toODataResult(result, info)
77
- res.status(201).set('Content-Type', 'application/json;IEEE754Compatible=true').send(result)
99
+ res.set('content-type', 'application/json;IEEE754Compatible=true')
100
+ res.status(201).send(result)
101
+ })
102
+ .catch(err => {
103
+ handleSapMessages(cdsReq, req, res)
104
+ next(err)
78
105
  })
79
- .catch(next) // should be outside, so tx can be rolled back in case of errors
80
106
  }
@@ -1,14 +1,13 @@
1
1
  const cds = require('../../../')
2
2
  const { UPDATE, DELETE } = cds.ql
3
3
 
4
- const { getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
4
+ const { getKeysAndParamsFromPath, handleSapMessages, getPreferReturnHeader } = require('../utils')
5
5
 
6
6
  module.exports = srv =>
7
7
  function deleete(req, res, next) {
8
- if (req._preferReturn) {
9
- throw Object.assign(new Error(`The 'return' preference is not allowed in ${req.method} requests`), {
10
- statusCode: 400
11
- })
8
+ if (getPreferReturnHeader(req)) {
9
+ const msg = "The 'return' preference is not allowed in DELETE requests"
10
+ throw Object.assign(new Error(msg), { statusCode: 400 })
12
11
  }
13
12
 
14
13
  // REVISIT: better solution for query._propertyAccess
@@ -19,8 +18,7 @@ module.exports = srv =>
19
18
  } = req._query
20
19
 
21
20
  if (!one) {
22
- // REVISIT: don't use "ENTITY.COLLECTION" as that's an okra term
23
- throw Object.assign(new Error('Method DELETE not allowed for ENTITY.COLLECTION'), { statusCode: 405 })
21
+ throw Object.assign(new Error('Method DELETE is not allowed for entity collections'), { statusCode: 405 })
24
22
  }
25
23
 
26
24
  // payload & params
@@ -31,20 +29,29 @@ module.exports = srv =>
31
29
  // query
32
30
  const query = _propertyAccess ? UPDATE(from).set({ [_propertyAccess]: null }) : DELETE.from(from)
33
31
 
32
+ // cdsReq.headers should contain merged headers of envelope and subreq
33
+ const headers = { ...cds.context.http.req.headers, ...req.headers }
34
+
34
35
  // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
35
- const cdsReq = new cds.Request({ query, data, params, req, res })
36
+ const cdsReq = new cds.Request({ query, data, headers, params, req, res })
36
37
  Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
37
38
 
39
+ // API for subrequests of $batch (or incoming request)
40
+ cdsReq.req = req
41
+ cdsReq.res = res
42
+
38
43
  // rewrite event for draft-enabled entities
39
44
  if (target._isDraftEnabled && cdsReq.data.IsActiveEntity === false) cdsReq.event = 'CANCEL'
40
45
 
41
46
  return srv
42
47
  .dispatch(cdsReq)
43
- .then(result => {
48
+ .then(() => {
44
49
  handleSapMessages(cdsReq, req, res)
45
50
 
46
- if (result === 0) throw Object.assign(new Error('Not found'), { statusCode: 404 })
47
51
  res.sendStatus(204)
48
52
  })
49
- .catch(next)
53
+ .catch(err => {
54
+ handleSapMessages(cdsReq, req, res)
55
+ next(err)
56
+ })
50
57
  }
@@ -51,8 +51,10 @@ const mpSupportsEmptyLocale = () => {
51
51
 
52
52
  module.exports = srv =>
53
53
  async function metadata(req, res, _next) {
54
- if (req.method !== 'GET')
55
- throw Object.assign(new Error(`Method ${req.method} not allowed for $metadata.`), { statusCode: 405 })
54
+ if (req.method !== 'GET') {
55
+ const msg = `Method ${req.method} is not allowed for calls to the metadata endpoint`
56
+ throw Object.assign(new Error(msg), { statusCode: 405 })
57
+ }
56
58
 
57
59
  const tenant = cds.context.tenant
58
60
  const locale = cds.context.locale
@@ -60,7 +62,7 @@ module.exports = srv =>
60
62
 
61
63
  // REVISIT: edm(x) and etag cache is only evicted with model
62
64
  const csnService = (cds.context.model || cds.model).definitions[srv.name]
63
- const metadataCache = (csnService.metadataCache = csnService.metadataCache || { jsonEtag: {}, xmlEtag: {} })
65
+ const metadataCache = (csnService.metadataCache = csnService.metadataCache || { jsonEtag: {}, xmlEtag: {} }) // REVISIT: yet another uncontrolled cache?
64
66
 
65
67
  const etag = format === 'json' ? metadataCache.jsonEtag?.[locale] : metadataCache.xmlEtag?.[locale]
66
68
 
@@ -75,7 +77,7 @@ module.exports = srv =>
75
77
  if (etag) {
76
78
  const unchanged = validate_etag(req.headers['if-none-match'], etag)
77
79
  if (unchanged) {
78
- res.set('Etag', etag)
80
+ res.set('etag', etag)
79
81
  return res.status(304).end()
80
82
  }
81
83
  }
@@ -83,11 +85,11 @@ module.exports = srv =>
83
85
 
84
86
  const { 'cds.xt.ModelProviderService': mps } = cds.services
85
87
  if (mps) {
86
- if (format === 'json')
87
- throw Object.assign(new Error('JSON metadata is not supported if cds.requires.extensibilty: true.'), {
88
- statusCode: 400,
89
- code: 'UNSUPPORTED_METADATA_TYPE'
90
- })
88
+ if (format === 'json') {
89
+ LOG._warn && LOG.warn('JSON metadata is not supported in case of cds.requires.extensibilty: true')
90
+ const msg = 'JSON metadata is not supported for this service'
91
+ throw Object.assign(new Error(msg), { statusCode: 501 })
92
+ }
91
93
 
92
94
  try {
93
95
  let edmx
@@ -105,7 +107,7 @@ module.exports = srv =>
105
107
  edmx = await mps.getEdmx({ tenant, model: srv.model, service: srv.definition.name, locale })
106
108
  }
107
109
  metadataCache.xmlEtag[locale] = generateEtag(edmx)
108
- res.set('Content-Type', 'application/xml')
110
+ res.set('content-type', 'application/xml')
109
111
  res.send(edmx)
110
112
  return
111
113
  } catch (e) {
@@ -113,8 +115,7 @@ module.exports = srv =>
113
115
  e.message = 'Unable to get EDMX for tenant ' + tenant + ' due to error: ' + e.message
114
116
  LOG.error(e)
115
117
  }
116
-
117
- throw Object.assign(new Error('Service Unavailable'), { statusCode: 503 })
118
+ throw Object.assign(new Error('503'), { statusCode: 503 })
118
119
  }
119
120
  }
120
121
 
@@ -132,7 +133,7 @@ module.exports = srv =>
132
133
  (metadataCache.edmx = cds.compile.to.edmx(srv.model, { service: srv.definition.name }))
133
134
  const localized = cds.localize(srv.model, locale, edmx)
134
135
  metadataCache.xmlEtag[locale] = generateEtag(localized)
135
- res.set('Etag', metadataCache.xmlEtag[locale])
136
- res.set('Content-Type', 'application/xml')
136
+ res.set('etag', metadataCache.xmlEtag[locale])
137
+ res.set('content-type', 'application/xml')
137
138
  return res.send(localized)
138
139
  }
@@ -1,15 +1,11 @@
1
1
  const cds = require('../../../')
2
2
 
3
3
  const { toODataResult, postProcess } = require('../utils/result')
4
- const { cds2edm, calculateLocationHeader, getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
4
+ const { cds2edm, getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
5
5
 
6
6
  const { deepCopy } = require('../../_runtime/common/utils/copy')
7
7
 
8
- // REVISIT: move to or rewrite in libx/odata
9
- const { readAfterWrite } = require('../../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
10
- const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
11
-
12
- const DRAFT_EVENTS = { draftActivate: 1, EDIT: 1, draftPrepare: 1 }
8
+ const metaInfo = require('../utils/metaInfo')
13
9
 
14
10
  module.exports = srv =>
15
11
  function operation(req, res, next) {
@@ -46,16 +42,21 @@ module.exports = srv =>
46
42
 
47
43
  // event
48
44
  // REVISIT: when is operation.name actually prefixed with the service name?
49
- let event = operation.name.replace(`${srv.name}.`, '')
50
- // REVISIT: rewrite draft event -> do centrally in draft impl
51
- if (event === 'draftEdit') event = 'EDIT'
45
+ const event = operation.name.replace(`${srv.name}.`, '')
52
46
 
53
47
  const query = entity ? req._query : undefined
54
48
 
49
+ // cdsReq.headers should contain merged headers of envelope and subreq
50
+ const headers = { ...cds.context.http.req.headers, ...req.headers }
51
+
55
52
  // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
56
- const cdsReq = new cds.Request({ query, event, data, params, target: query?.target, req, res })
53
+ const cdsReq = new cds.Request({ query, event, data, params, headers, target: query?.target, req, res })
57
54
  Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
58
55
 
56
+ // API for subrequests of $batch (or incoming request)
57
+ cdsReq.req = req
58
+ cdsReq.res = res
59
+
59
60
  // REVISIT: only via srv.run in combination with srv.dispatch inside
60
61
  // we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
61
62
  // or the auto-managed tx opened for the respective atomicity group, if exists
@@ -63,41 +64,37 @@ module.exports = srv =>
63
64
  .run(() => {
64
65
  return srv.dispatch(cdsReq).then(result => {
65
66
  handleSapMessages(cdsReq, req, res)
66
-
67
- // FIXME: should be handled in draft impl
68
- if (event in /* { draftActivate: 1, EDIT: 1 } */ DRAFT_EVENTS /* && cdsReq._.readAfterWrite */) {
69
- let columns
70
- const queryOptions = req.url.split('?')[1]
71
- if (queryOptions) columns = cds.odata.parse(`/X?${queryOptions}`).SELECT.columns
72
- return readAfterWrite(cdsReq, srv, { operation: { result, returnType: operation.returns }, columns })
73
- }
74
-
75
67
  return result
76
68
  })
77
69
  })
78
70
  .then(result => {
79
71
  // we use an extra then block, after getting the result, so the transaction is commited, before sending the response
72
+
80
73
  if (!operation.returns || result == null) return res.status(204).end()
81
74
 
82
75
  if (operation.returns._type?.match?.(/^cds\./)) {
76
+ res.set('content-type', 'application/json;IEEE754Compatible=true')
83
77
  // TODO: check result type
84
- return res.set('Content-Type', 'application/json;IEEE754Compatible=true').send({
85
- '@odata.context': `${entity ? '../' : ''}$metadata#${cds2edm[operation.returns._type]}`,
78
+ return res.send({
79
+ '@odata.context': `${'../'.repeat(query?.SELECT?.from?.ref?.length)}$metadata#${cds2edm[operation.returns._type]}`,
86
80
  value: result
87
81
  })
88
82
  }
89
83
 
90
84
  const info = metaInfo(req._query, event, srv, result, req)
91
85
 
92
- // FIXME: info.metadata.isCollection and contextUrl are incorrect for draft events
93
- if (event in /* { draftActivate: 1, EDIT: 1 } */ DRAFT_EVENTS) {
94
- info.metadata.isCollection = false
95
- info.metadata.contextUrl += '/$entity'
96
- }
97
-
98
86
  // FIXME: info.metadata.isCollection is incorrect
99
87
  if (!operation.returns.items) info.metadata.isCollection = false
100
88
 
89
+ // REVISIT impl of context url generation
90
+ if (
91
+ !info.metadata.isCollection &&
92
+ info.metadata.isServiceEntity &&
93
+ !info.metadata.contextUrl.endsWith('$entity')
94
+ ) {
95
+ info.metadata.contextUrl += '/$entity'
96
+ }
97
+
101
98
  if (info.metadata.returnType) {
102
99
  postProcess(info.metadata.returnType, srv, result)
103
100
  if (result['$etag']) res.set('etag', result['$etag'])
@@ -105,22 +102,15 @@ module.exports = srv =>
105
102
 
106
103
  result = toODataResult(result, info)
107
104
 
108
- // FIXME: draftActivate needs location header -> move to draft impl
109
- // FIXME: draftActivate needs HasActiveEntity and HasDraftEntity -> move to draft impl
110
- if (event in /* { draftActivate: 1, EDIT: 1 } */ DRAFT_EVENTS) {
111
- res.set('location', '../' + calculateLocationHeader(cdsReq.target, srv, result))
112
- result.HasDraftEntity = false
113
- if (event === 'draftActivate' || event === 'draftPrepare') result.HasActiveEntity = false
114
- }
115
-
116
- // // FIXME: draftEdit needs HasDraftEntity -> move to draft impl
117
- // if (event === 'EDIT') result.HasDraftEntity = false
118
-
119
105
  // FIXME: toODataResult() doesn't seem to handle this case
120
106
  if (entity && !result['@odata.context'].match(/^\.\.\//))
121
107
  result['@odata.context'] = '../' + result['@odata.context']
122
108
 
123
- res.set('Content-Type', 'application/json;IEEE754Compatible=true').send(result)
109
+ res.set('content-type', 'application/json;IEEE754Compatible=true')
110
+ res.send(result)
111
+ })
112
+ .catch(err => {
113
+ handleSapMessages(cdsReq, req, res)
114
+ next(err)
124
115
  })
125
- .catch(next)
126
116
  }
@@ -5,6 +5,8 @@ module.exports = srv =>
5
5
  // REVISIT: can't we register the batch handler before the parse handler to avoid this?
6
6
  if (req.path.startsWith('/$batch')) return next()
7
7
 
8
+ if (req._query) return next() //> already parsed (e.g., upsert)
9
+
8
10
  // if not a GET, use req.path instead of req.url to ignore query parameters
9
11
  req._query = cds.odata.parse(req.method === 'GET' ? req.url : req.path, {
10
12
  service: srv,
@@ -12,8 +14,5 @@ module.exports = srv =>
12
14
  strict: true
13
15
  })
14
16
 
15
- const preferReturn = req.headers.prefer?.match(/\W?return=(\w+)/i)
16
- if (preferReturn) req._preferReturn = preferReturn[1]
17
-
18
17
  next()
19
18
  }
@@ -1,11 +1,10 @@
1
1
  const cds = require('../../../')
2
2
  const { toODataResult, postProcess } = require('../utils/result')
3
3
  const querystring = require('node:querystring')
4
- const { getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
4
+ const { getKeysAndParamsFromPath, handleSapMessages, validateIfNoneMatch, getPreferReturnHeader } = require('../utils')
5
5
  const { handleStreamProperties } = require('../../_runtime/common/utils/streamProp')
6
6
 
7
- // REVISIT: move to or rewrite in libx/odata
8
- const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
7
+ const metaInfo = require('../utils/metaInfo')
9
8
  const { getPageSize } = require('../../_runtime/common/generic/paging')
10
9
 
11
10
  const _getCount = result =>
@@ -18,10 +17,10 @@ const _getCount = result =>
18
17
  const _calculateNextLink = (req, result) => {
19
18
  const $skiptoken = result.$nextLink ?? _calculateSkiptoken(req, result)
20
19
  if ($skiptoken) {
21
- const queryParamsWithSkipToken = { ...req.http.req.query, $skiptoken }
20
+ const queryParamsWithSkipToken = { ...req.req.query, $skiptoken }
22
21
  // REVISIT: slice replaces leading '/'. Always starts with '/'?
23
22
  result.$nextLink =
24
- req.http.req.path.slice(1) +
23
+ req.req.path.slice(1) +
25
24
  '?' +
26
25
  querystring.stringify(queryParamsWithSkipToken, '&', '=', { encodeURIComponent: e => e })
27
26
  }
@@ -29,9 +28,9 @@ const _calculateNextLink = (req, result) => {
29
28
 
30
29
  const _calculateSkiptoken = (req, result) => {
31
30
  const limit = Array.isArray(req.query) ? getPageSize(req.query[0]._target).max : req.query.SELECT.limit?.rows?.val
32
- const top = parseInt(req.http.req.query.$top)
31
+ const top = parseInt(req.req.query.$top)
33
32
  if (limit === result.length && limit !== top) {
34
- const token = req.http.req.query.$skiptoken
33
+ const token = req.req.query.$skiptoken
35
34
  if (cds.env.query.limit.reliablePaging && _reliablePagingPossible(req)) {
36
35
  const decoded = token && JSON.parse(Buffer.from(token, 'base64').toString())
37
36
  const skipToken = {
@@ -100,6 +99,14 @@ const _isNullableSingleton = query => query._target._isSingleton && query._targe
100
99
  const _isToOneAssoc = query =>
101
100
  query.SELECT.from.ref.length > 1 && typeof query.SELECT.from.ref.slice(-1)[0] === 'string'
102
101
 
102
+ const _count = result => {
103
+ return Array.isArray(result)
104
+ ? result.reduce((acc, val) => {
105
+ return acc + (val?.$count ?? val?._counted_ ?? (Array.isArray(val) && _count(val))) || 0
106
+ }, 0)
107
+ : result.$count ?? result._counted_ ?? 0
108
+ }
109
+
103
110
  // basically stolen from old read handler without understanding it ^^
104
111
  const _handleArrayOfQueries = (srv, req, res, next) => {
105
112
  const info = metaInfo(req._query, 'READ', srv, {}, req, false)
@@ -109,45 +116,27 @@ const _handleArrayOfQueries = (srv, req, res, next) => {
109
116
  .then(result => {
110
117
  handleSapMessages(cdsReq, req, res)
111
118
 
112
- if (req.url.match(/\/\$count/)) {
113
- const count = Array.isArray(result)
114
- ? result.reduce((acc, val) => {
115
- return (
116
- acc + ((val && (val.$count || val._counted_)) || (val[0] && (val[0].$count || val[0]._counted_))) || 0
117
- )
118
- }, 0)
119
- : result.$count || result._counted_ || 0
120
- return res.set('Content-Type', 'text/plain').send(count.toString())
121
- }
119
+ if (req.url.match(/\/\$count/)) return res.set('content-type', 'text/plain').send(_count(result).toString())
122
120
 
123
- const adjustedResult = []
124
- if (cdsReq.query[0].SELECT.count) adjustedResult.$count = 0
125
- adjustedResult.push(...result[0])
126
- adjustedResult.$count += result[0].$count ? result[0].$count : 0
127
- for (let i = 1; i < result.length; i++) {
128
- adjustedResult.push(...result[i])
129
- adjustedResult.$count += result[i].$count ? result[i].$count : 0
121
+ for (let i = 0; i < result.length; i++) {
130
122
  // Add OData context, if it deviates from main context
131
- if (info.metadata.contextUrl !== info.metadata.additionalContextUrl[i - 1])
123
+ if (i !== 0 && info.metadata.contextUrl !== info.metadata.additionalContextUrl[i - 1])
132
124
  result[i].forEach(entry => (entry['@odata.context'] = info.metadata.additionalContextUrl[i - 1]))
133
125
  }
134
- result.splice(0, result.length, ...adjustedResult)
135
- if (cdsReq.query[0].SELECT.count) result.$count = adjustedResult.$count || 0
136
- result = toODataResult(result, info)
137
- res.set('Content-Type', 'application/json;IEEE754Compatible=true').send(result)
126
+
127
+ res.set('content-type', 'application/json;IEEE754Compatible=true')
128
+ const flatRes = result.flat(Infinity)
129
+ if (cdsReq.query[0].SELECT.count) flatRes.$count = flatRes.length
130
+ res.send(toODataResult(flatRes, info))
138
131
  })
139
132
  .catch(next)
140
133
  }
141
134
 
142
135
  module.exports = srv =>
143
136
  function read(req, res, next) {
144
- // disable express etag checks
145
- req.headers['cache-control'] = 'no-cache'
146
-
147
- if (req._preferReturn) {
148
- throw Object.assign(new Error(`The 'return' preference is not allowed in ${req.method} requests`), {
149
- statusCode: 400
150
- })
137
+ if (getPreferReturnHeader(req)) {
138
+ const msg = `The 'return' preference is not allowed in ${req.method} requests`
139
+ throw Object.assign(new Error(msg), { statusCode: 400 })
151
140
  }
152
141
 
153
142
  // $apply with concat -> multiple queries with special handling
@@ -165,10 +154,17 @@ module.exports = srv =>
165
154
  const { keys, params } = getKeysAndParamsFromPath(from, srv)
166
155
  const data = keys //> for read and delete, we provide keys in req.data
167
156
 
157
+ // cdsReq.headers should contain merged headers of envelope and subreq
158
+ const headers = { ...cds.context.http.req.headers, ...req.headers }
159
+
168
160
  // we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
169
- const cdsReq = new cds.Request({ query, data, params, req, res })
161
+ const cdsReq = new cds.Request({ query, data, params, headers, req, res })
170
162
  Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
171
163
 
164
+ // API for subrequests of $batch (or incoming request)
165
+ cdsReq.req = req
166
+ cdsReq.res = res
167
+
172
168
  // REVISIT: what is this for? some tests fail without it... we should find a better solution!
173
169
  Object.defineProperty(query.SELECT, '_4odata', { value: true })
174
170
 
@@ -190,8 +186,6 @@ module.exports = srv =>
190
186
 
191
187
  const lastPathElement = req.path.split('/').slice(-1)[0]
192
188
 
193
- query.SELECT.columns ??= ['*']
194
-
195
189
  if (cds.env.effective.odata.proxies && cds.env.effective.odata.xrefs) {
196
190
  // REVISIT check above is still not perfect solution
197
191
  resolveProxyExpands(query, srv)
@@ -207,24 +201,35 @@ module.exports = srv =>
207
201
  .then(result => {
208
202
  handleSapMessages(cdsReq, req, res)
209
203
 
210
- if (cdsReq.target._etag && result == null && cdsReq.headers['if-none-match']) {
204
+ if (result == null) {
205
+ if (!query.SELECT.one) {
206
+ result = []
207
+ if (req.query.$count) result.$count = 0
208
+ } else if (_isNullableSingleton(query) || _isToOneAssoc(query)) {
209
+ return res.sendStatus(204)
210
+ } else {
211
+ throw Object.assign(new Error('404'), { statusCode: 404 })
212
+ }
213
+ }
214
+
215
+ if (validateIfNoneMatch(cdsReq, req, result)) {
211
216
  return res.status(304).end()
212
217
  }
213
218
 
214
- if (result == null) {
215
- if (_isNullableSingleton(query) || _isToOneAssoc(query)) return res.sendStatus(204)
216
- throw Object.assign(new Error(`Not Found`), {
217
- statusCode: 404
218
- })
219
+ // express always handles if-none-match header (see req.fresh)
220
+ if (!cdsReq.target._etag && req.headers['if-none-match']) {
221
+ delete req.headers['if-none-match']
219
222
  }
220
223
 
221
224
  if (_propertyAccess && result[_propertyAccess] === null) return res.sendStatus(204)
222
225
 
223
226
  if (lastPathElement === '$count') {
224
227
  result = _getCount(result)
225
- return res.set('Content-Type', 'text/plain').send(result.toString())
226
- } else if (lastPathElement === '$value' && _propertyAccess) {
227
- return res.set('Content-Type', 'text/plain').send(result[_propertyAccess].toString())
228
+ return res.set('content-type', 'text/plain').send(result.toString())
229
+ }
230
+
231
+ if (lastPathElement === '$value' && _propertyAccess) {
232
+ return res.set('content-type', 'text/plain').send(result[_propertyAccess].toString())
228
233
  }
229
234
 
230
235
  if (info.metadata.isCollection) _calculateNextLink(cdsReq, result)
@@ -234,9 +239,11 @@ module.exports = srv =>
234
239
 
235
240
  // Express interprets numbers as HTTP status codes
236
241
  const isNumber = typeof result === 'number'
237
- res
238
- .set('Content-Type', isNumber ? 'text/plain' : 'application/json;IEEE754Compatible=true')
239
- .send(isNumber ? result.toString() : result)
242
+ res.set('content-type', isNumber ? 'text/plain' : 'application/json;IEEE754Compatible=true')
243
+ res.send(isNumber ? result.toString() : result)
244
+ })
245
+ .catch(err => {
246
+ handleSapMessages(cdsReq, req, res)
247
+ next(err)
240
248
  })
241
- .catch(next)
242
249
  }
@@ -1,7 +1,7 @@
1
1
  const cds = require('../../../')
2
2
 
3
3
  const crypto = require('crypto')
4
- const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
4
+ const metaInfo = require('../utils/metaInfo')
5
5
 
6
6
  const normalize_header = value => {
7
7
  return value.split(',').map(str => str.trim())
@@ -19,10 +19,10 @@ const generateEtag = s => {
19
19
  module.exports = srv =>
20
20
  function service_document(req, res) {
21
21
  if (req.method === 'HEAD') return res.end()
22
- if (req.method !== 'GET')
23
- throw Object.assign(new Error(`Method ${req.method} not allowed for service document.`), {
24
- statusCode: 405
25
- })
22
+ if (req.method !== 'GET') {
23
+ const msg = `Method ${req.method} is not allowed for calls to the service endpoint`
24
+ throw Object.assign(new Error(msg), { statusCode: 405 })
25
+ }
26
26
 
27
27
  const m = cds.context.model || cds.model
28
28
  const csnService = (cds.context.model || cds.model).definitions[srv.name]
@@ -38,7 +38,7 @@ module.exports = srv =>
38
38
  if (csnService.srvDocEtag) {
39
39
  const unchanged = validate_etag(req.headers['if-none-match'], csnService.srvDocEtag)
40
40
  if (unchanged) {
41
- res.set('Etag', csnService.srvDocEtag)
41
+ res.set('etag', csnService.srvDocEtag)
42
42
  return res.status(304).end()
43
43
  }
44
44
  }
@@ -51,7 +51,7 @@ module.exports = srv =>
51
51
  )
52
52
 
53
53
  csnService.srvDocEtag = generateEtag(JSON.stringify(exposedEntities))
54
- res.set('Etag', csnService.srvDocEtag)
54
+ res.set('etag', csnService.srvDocEtag)
55
55
 
56
56
  const info = metaInfo({ SELECT: { from: { ref: [srv.name] } } }, 'READ', srv, {}, req, false)
57
57