@sap/cds 7.7.3 → 7.8.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 (75) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/lib/auth/ias-auth.js +5 -3
  3. package/lib/auth/jwt-auth.js +4 -2
  4. package/lib/compile/cdsc.js +0 -10
  5. package/lib/compile/for/java.js +9 -5
  6. package/lib/compile/for/lean_drafts.js +1 -1
  7. package/lib/compile/to/edm.js +2 -1
  8. package/lib/compile/to/sql.js +0 -21
  9. package/lib/compile/to/srvinfo.js +13 -4
  10. package/lib/dbs/cds-deploy.js +7 -7
  11. package/lib/env/cds-requires.js +6 -0
  12. package/lib/index.js +4 -3
  13. package/lib/linked/classes.js +151 -88
  14. package/lib/linked/entities.js +27 -23
  15. package/lib/linked/models.js +57 -36
  16. package/lib/linked/types.js +42 -104
  17. package/lib/ql/Whereable.js +3 -3
  18. package/lib/req/context.js +8 -4
  19. package/lib/srv/protocols/hcql.js +2 -1
  20. package/lib/srv/protocols/http.js +7 -7
  21. package/lib/srv/protocols/index.js +31 -13
  22. package/lib/srv/protocols/odata-v4.js +79 -58
  23. package/lib/srv/srv-api.js +7 -6
  24. package/lib/srv/srv-dispatch.js +1 -12
  25. package/lib/srv/srv-tx.js +9 -13
  26. package/lib/utils/cds-utils.js +6 -5
  27. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +11 -8
  28. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +21 -12
  29. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +5 -3
  30. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +3 -7
  31. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +5 -0
  32. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +2 -1
  33. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -7
  34. package/libx/_runtime/cds-services/services/utils/columns.js +6 -3
  35. package/libx/_runtime/cds.js +0 -13
  36. package/libx/_runtime/common/generic/input.js +3 -0
  37. package/libx/_runtime/common/generic/sorting.js +8 -6
  38. package/libx/_runtime/common/i18n/messages.properties +1 -0
  39. package/libx/_runtime/common/utils/cqn.js +5 -0
  40. package/libx/_runtime/common/utils/foreignKeyPropagations.js +7 -1
  41. package/libx/_runtime/common/utils/keys.js +2 -2
  42. package/libx/_runtime/common/utils/resolveView.js +2 -1
  43. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
  44. package/libx/_runtime/common/utils/stream.js +0 -10
  45. package/libx/_runtime/common/utils/template.js +20 -35
  46. package/libx/_runtime/db/Service.js +5 -1
  47. package/libx/_runtime/db/utils/columns.js +1 -1
  48. package/libx/_runtime/fiori/lean-draft.js +14 -2
  49. package/libx/_runtime/messaging/Outbox.js +7 -5
  50. package/libx/_runtime/messaging/kafka.js +266 -0
  51. package/libx/_runtime/messaging/service.js +7 -5
  52. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +1 -0
  53. package/libx/common/assert/validation.js +1 -1
  54. package/libx/odata/index.js +8 -2
  55. package/libx/odata/middleware/batch.js +340 -0
  56. package/libx/odata/middleware/create.js +43 -46
  57. package/libx/odata/middleware/delete.js +27 -15
  58. package/libx/odata/middleware/error.js +6 -5
  59. package/libx/odata/middleware/metadata.js +16 -15
  60. package/libx/odata/middleware/operation.js +107 -59
  61. package/libx/odata/middleware/parse.js +15 -7
  62. package/libx/odata/middleware/read.js +150 -24
  63. package/libx/odata/middleware/service-document.js +17 -6
  64. package/libx/odata/middleware/stream.js +34 -17
  65. package/libx/odata/middleware/update.js +123 -87
  66. package/libx/odata/parse/afterburner.js +131 -28
  67. package/libx/odata/parse/cqn2odata.js +1 -1
  68. package/libx/odata/parse/grammar.peggy +4 -5
  69. package/libx/odata/parse/multipartToJson.js +163 -0
  70. package/libx/odata/parse/parser.js +1 -1
  71. package/libx/odata/utils/index.js +29 -47
  72. package/libx/odata/utils/path.js +72 -0
  73. package/libx/odata/utils/result.js +123 -20
  74. package/package.json +1 -1
  75. package/server.js +4 -0
@@ -1,9 +1,10 @@
1
1
  // TODO: split into multiple files
2
2
 
3
3
  const cds = require('../../../')
4
+ const _path = require('./path')
4
5
 
5
6
  const { toBase64url } = require('../../_runtime/common/utils/binary')
6
- const { where2obj } = require('../../_runtime/common/utils/cqn')
7
+ const { getSapMessages } = require('../../_runtime/common/error/frontend')
7
8
 
8
9
  // copied from cds-compiler/lib/edm/edmUtils.js
9
10
  const cds2edm = {
@@ -39,8 +40,6 @@ const cds2edm = {
39
40
  // 'cds.hana.ST_GEOMETRY': 'Edm.Geometry',
40
41
  }
41
42
 
42
- const odataError = (code, message) => ({ error: { code, message } })
43
-
44
43
  const getSafeNumber = inputString => {
45
44
  if (typeof inputString !== 'string') return inputString
46
45
  // Try to parse the input string as a floating-point number using parseFloat
@@ -211,6 +210,8 @@ const formatVal = (val, elementName, csnTarget, kind, func, literal) => {
211
210
  }
212
211
  const element = _getElement(csnTarget, elementName)
213
212
  if (!element?.type) return typeof val === 'string' ? `'${val}'` : val
213
+ if ((element.type === 'cds.LargeString' || element.type === 'cds.String') && val.indexOf("'") >= 0)
214
+ val = val.replace(/'/g, "''")
214
215
  return kind === 'odata-v2' ? _v2(val, element) : _v4(val, element)
215
216
  }
216
217
 
@@ -269,49 +270,29 @@ const skipToken = (token, cqn) => {
269
270
  }
270
271
  }
271
272
 
272
- // REVISIT: do we already have something like this _without using okra api_?
273
- const getKeysFromPath = (from, srv) => {
274
- const keys = {}
275
- // own
276
- if (from.ref[from.ref.length - 1].where) {
277
- Object.assign(keys, where2obj(from.ref[from.ref.length - 1].where))
278
- }
279
- // previous path segments
280
- if (from.ref.length > 1) {
281
- const entities = []
282
- let cur = srv.model.definitions
283
- for (let i = 0; i < from.ref.length; i++) {
284
- const id = from.ref[i].id || from.ref[i]
285
- const t = cur[id]._target || cur[id]
286
- entities.push(t)
287
- cur = t.elements
288
- }
289
- for (let i = from.ref.length - 2; i >= 0; i--) {
290
- const ref = from.ref[i]
291
- if (ref.where) {
292
- const relation = entities[i]._relations[from.ref[i + 1].id || from.ref[i + 1]].join('target', 'source')
293
- const seg_keys = where2obj(ref.where)
294
- if (relation?.[0].xpr) {
295
- const join = [...relation[0].xpr]
296
- while (join.length >= 3) {
297
- const [left, _, right] = join.splice(0, 4)
298
- if (left.ref?.[0] === 'target') {
299
- if (left.ref[1] in keys) break // we already added the foreign key for the last segment
300
- keys[left.ref[1]] = 'val' in right ? right.val : seg_keys[right.ref[1]]
301
- } else if (right.ref?.[0] === 'target') {
302
- if (right.ref[1] in keys) break // we already added the foreign key for the last segment
303
- keys[right.ref[1]] = 'val' in left ? left.val : seg_keys[left.ref[1]]
304
- } else {
305
- // REVISIT: what to do here?
306
- }
307
- }
308
- } else {
309
- // REVISIT: what to do here?
310
- }
311
- }
312
- }
273
+ const calculateLocationHeader = (target, srv, result) => {
274
+ const targetName = target.name.replace(`${srv.name}.`, '')
275
+
276
+ const keyValuePairs = Object.keys(target.keys).reduce((acc, key) => {
277
+ acc[key] = result[key]
278
+ return acc
279
+ }, {})
280
+
281
+ let keys
282
+ const entries = Object.entries(keyValuePairs)
283
+ if (entries.length === 1) {
284
+ keys = entries[0][1]
285
+ } else {
286
+ keys = entries.map(([key, value]) => `${key}=${value}`).join(',')
313
287
  }
314
- return keys
288
+
289
+ return `${targetName}(${keys})`
290
+ }
291
+
292
+ const handleSapMessages = (cdsReq, req, res) => {
293
+ if (!cdsReq.messages || !cdsReq.messages.length) return
294
+ const msgs = getSapMessages(cdsReq.messages, req)
295
+ if (msgs) res.setHeader('sap-messages', msgs)
315
296
  }
316
297
 
317
298
  module.exports = {
@@ -319,6 +300,7 @@ module.exports = {
319
300
  getSafeNumber,
320
301
  formatVal,
321
302
  skipToken,
322
- odataError,
323
- getKeysFromPath
303
+ calculateLocationHeader,
304
+ handleSapMessages,
305
+ getKeysAndParamsFromPath: _path.getKeysAndParamsFromPath
324
306
  }
@@ -0,0 +1,72 @@
1
+ const { where2obj } = require('../../_runtime/common/utils/cqn')
2
+
3
+ const _handleXpr = (relation, keys, seg_keys) => {
4
+ const join = [...relation]
5
+ while (join.length >= 3) {
6
+ const [left, _, right] = join
7
+
8
+ if (left.xpr) {
9
+ // can be [ref = ref] or [xpr and ref = ref] and [xpr and xpr] so we will always catch xprs as left element, as it follows and/or or is first element
10
+ _handleXpr(left.xpr, keys, seg_keys)
11
+ join.splice(0, 2)
12
+ continue
13
+ }
14
+
15
+ if (left.ref?.[0] === 'target') {
16
+ if (left.ref[1] in keys) break // we already added the foreign key for the last segment
17
+ keys[left.ref[1]] = 'val' in right ? right.val : seg_keys[right.ref[1]]
18
+ join.splice(0, 4)
19
+ } else if (right.ref?.[0] === 'target') {
20
+ if (right.ref[1] in keys) break // we already added the foreign key for the last segment
21
+ keys[right.ref[1]] = 'val' in left ? left.val : seg_keys[left.ref[1]]
22
+ join.splice(0, 4)
23
+ }
24
+ }
25
+ }
26
+
27
+ // REVISIT: do we already have something like this _without using okra api_?
28
+ // REVISIT: should we still support process.env.CDS_FEATURES_PARAMS? probably nobody uses it...
29
+ const getKeysAndParamsFromPath = (from, srv) => {
30
+ if (!from.ref) return {}
31
+
32
+ const keys = {}
33
+ const params = []
34
+
35
+ // last path segment
36
+ if (from.ref[from.ref.length - 1].where) {
37
+ const seg_keys = where2obj(from.ref[from.ref.length - 1].where)
38
+ Object.assign(keys, seg_keys)
39
+ params.unshift(seg_keys.ID && Object.keys(seg_keys).length === 1 ? seg_keys.ID : seg_keys)
40
+ }
41
+
42
+ // previous path segments
43
+ if (from.ref.length > 1) {
44
+ const entities = []
45
+ let cur = srv.model.definitions
46
+ for (let i = 0; i < from.ref.length; i++) {
47
+ const id = from.ref[i].id || from.ref[i]
48
+ const t = cur[id]._target || cur[id]
49
+ entities.push(t)
50
+ cur = t.elements
51
+ }
52
+ for (let i = from.ref.length - 2; i >= 0; i--) {
53
+ const ref = from.ref[i]
54
+ if (ref.where) {
55
+ const relation = entities[i]._relations[from.ref[i + 1].id || from.ref[i + 1]].join('target', 'source')
56
+ const seg_keys = where2obj(ref.where)
57
+ if (relation?.[0].xpr) {
58
+ _handleXpr(relation[0].xpr, keys, seg_keys)
59
+ } else {
60
+ // REVISIT: what to do here?
61
+ }
62
+ params.unshift(seg_keys.ID && Object.keys(seg_keys).length === 1 ? seg_keys.ID : seg_keys)
63
+ }
64
+ }
65
+ }
66
+
67
+ return { keys, params }
68
+ }
69
+
70
+ module.exports = {
71
+ getKeysAndParamsFromPath
72
+ }
@@ -1,3 +1,6 @@
1
+ const getTemplate = require('../../_runtime/common/utils/template')
2
+ const templateProcessor = require('../../_runtime/common/utils/templateProcessor')
3
+
1
4
  const METADATA = {
2
5
  $context: '@odata.context',
3
6
  $count: '@odata.count',
@@ -22,25 +25,46 @@ const METADATA = {
22
25
  $mediaEtag: '@odata.mediaEtag'
23
26
  }
24
27
 
25
- const _cleanupMetadata = (odataResult, result) => {
26
- if (typeof result !== 'object') return odataResult
27
-
28
- const keysToCleanup = {
29
- // do not set "@odata.context" as it may be inherited of remote service
30
- $context: true,
31
- // REVISIT: okra doesn't support content disposition
32
- $mediaContentDispositionFilename: true,
33
- $mediaContentDispositionType: true
34
- }
28
+ const KEYSTOCLEANUP = {
29
+ // do not set "@odata.context" as it may be inherited of remote service
30
+ $context: true,
31
+ // REVISIT: okra doesn't support content disposition
32
+ $mediaContentDispositionFilename: true,
33
+ $mediaContentDispositionType: true
34
+ }
35
35
 
36
+ const _metadataRoot = (result, odataResult) => {
36
37
  for (const key in METADATA) {
37
38
  if (!(key in result)) continue
38
- if (!keysToCleanup[key]) {
39
- odataResult[METADATA[key]] = result[key]
39
+ if (!KEYSTOCLEANUP[key]) odataResult[METADATA[key]] = result[key]
40
+ }
41
+ }
42
+
43
+ const _metadata = (result, propertyName, odataResult) => {
44
+ for (const key in result) {
45
+ if (typeof result[key] === 'object') _metadata(result[key])
46
+ if (!(key in METADATA)) continue
47
+ if (!KEYSTOCLEANUP[key]) {
48
+ if (propertyName) odataResult[METADATA[key]] = result[key]
49
+ else result[METADATA[key]] = result[key]
40
50
  }
41
- delete result[key]
51
+ if (!propertyName) delete result[key]
52
+ }
53
+ }
54
+
55
+ const _cleanupMetadata = (propertyName, result) => {
56
+ if (typeof result !== 'object') return odataResult
57
+
58
+ const odataResult = {}
59
+ if (propertyName) {
60
+ odataResult.value = result[propertyName]
61
+ } else {
62
+ odataResult.value = result
42
63
  }
43
64
 
65
+ if (Array.isArray(result)) _metadataRoot(result, odataResult)
66
+ _metadata(result, propertyName, odataResult)
67
+
44
68
  return odataResult
45
69
  }
46
70
 
@@ -53,6 +77,90 @@ const _setContext = (odataResult, info, isCollection) => {
53
77
  return odataResult
54
78
  }
55
79
 
80
+ const _getParent = (model, name) => {
81
+ const target = model.definitions[name]
82
+
83
+ if (target && target.elements) {
84
+ for (const elementName in target.elements) {
85
+ const element = target.elements[elementName]
86
+ if (element._anchor && element._anchor._isContained) return element._anchor
87
+ }
88
+ }
89
+
90
+ return null
91
+ }
92
+
93
+ const addEtags = (row, key) => {
94
+ row['$etag'] = row[key].startsWith('W/') ? row[key] : `W/"${row[key]}"`
95
+ }
96
+
97
+ const _processCategory = (category, elementInfo) => {
98
+ const { row, key } = elementInfo
99
+
100
+ switch (category) {
101
+ case '@odata.etag':
102
+ addEtags(row, key)
103
+ break
104
+ case 'stringify':
105
+ // REVISIT: remove once DB always returns strings
106
+ if (row[key] == null) return
107
+ row[key] = `${row[key]}`
108
+ break
109
+ // no default
110
+ }
111
+ }
112
+
113
+ const _processorFn = () => elementInfo => {
114
+ const { row, key, plain } = elementInfo
115
+ if (typeof row !== 'object' || !Object.prototype.hasOwnProperty.call(row, key)) return
116
+ const categories = plain.categories
117
+
118
+ for (const category of categories) {
119
+ _processCategory(category, elementInfo)
120
+ }
121
+ }
122
+
123
+ const _pick = element => {
124
+ const categories = []
125
+ if (element['@odata.etag']) categories.push('@odata.etag')
126
+ if (element.type === 'cds.Decimal' || element.type === 'cds.Integer64') categories.push('stringify') // REVISIT: remove once DB always returns strings
127
+ if (categories.length) return { categories }
128
+ }
129
+
130
+ const postProcess = (target, service, result, isMinimal) => {
131
+ const { model } = service
132
+ if (!target || !result || !model || !model.definitions[target.name]) return
133
+
134
+ const cacheKey = isMinimal ? 'postProcessMinimal' : 'postProcess'
135
+ const parent = _getParent(model, target.name)
136
+ const template = getTemplate(
137
+ cacheKey,
138
+ service,
139
+ target,
140
+ { pick: _pick, ignore: isMinimal ? el => el.isAssociation : undefined },
141
+ parent
142
+ )
143
+
144
+ if (template.elements.size === 0) return
145
+
146
+ // normalize result to rows
147
+ result = result.value != null && Object.keys(result).filter(k => !k.match(/^\W/)).length === 1 ? result.value : result
148
+
149
+ if (typeof result === 'object' && result != null) {
150
+ const rows = Array.isArray(result) ? result : [result]
151
+
152
+ // process each row
153
+ const processFn = _processorFn()
154
+ for (const row of rows) {
155
+ templateProcessor({
156
+ processFn,
157
+ row,
158
+ template
159
+ })
160
+ }
161
+ }
162
+ }
163
+
56
164
  /**
57
165
  * Convert any result to the result object structure, which is expected of odata-v4.
58
166
  *
@@ -72,12 +180,7 @@ const toODataResult = (result, info) => {
72
180
  if (isCollection && !Array.isArray(result)) result = [result]
73
181
  else if (!isCollection && Array.isArray(result)) result = result[0]
74
182
 
75
- let value = result
76
- if (typeof result === 'object') {
77
- if (propertyName) value = result[propertyName]
78
- }
79
-
80
- const odataResult = _cleanupMetadata({ value }, result)
183
+ const odataResult = _cleanupMetadata(propertyName, result)
81
184
 
82
185
  // REVISIT: Support exponential decimals header
83
186
  // REVISIT: we always assume minimal metadata right now
@@ -88,4 +191,4 @@ const toODataResult = (result, info) => {
88
191
  return odataResult
89
192
  }
90
193
 
91
- module.exports = { toODataResult }
194
+ module.exports = { toODataResult, postProcess }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "7.7.3",
3
+ "version": "7.8.0",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
package/server.js CHANGED
@@ -22,6 +22,7 @@ module.exports = async function cds_server (options) {
22
22
 
23
23
  // mount static resources and cors middleware
24
24
  if (o.cors) app.use (o.cors) //> if not in prod
25
+ if (o.health) app.get ('/health', o.health)
25
26
  if (o.static) app.use (express.static (o.static)) //> defaults to ./app
26
27
  if (o.favicon) app.use ('/favicon.ico', o.favicon) //> if none in ./app
27
28
  if (o.index) app.get ('/',o.index) //> if none in ./app
@@ -76,6 +77,9 @@ const defaults = {
76
77
  }
77
78
  next()
78
79
  }
80
+ },
81
+ get health() {
82
+ return (_, res) => res.json({ status: 'UP' })
79
83
  }
80
84
  }
81
85