@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
@@ -0,0 +1,410 @@
1
+ const cds = require('../../_runtime/cds')
2
+ const LOG = cds.log('odata')
3
+
4
+ const { appURL } = require('../../_runtime/common/utils/vcap')
5
+ const { resolveFromSelect, targetFromPath } = require('../../_runtime/common/utils/cqn')
6
+ const { setEntityContained } = require('../../_runtime/common/utils/csn')
7
+ const { getNavigationIfStruct } = require('../../_runtime/common/utils/structured')
8
+ const getTemplate = require('../../_runtime/common/utils/template')
9
+ const templateProcessor = require('../../_runtime/common/utils/templateProcessor')
10
+
11
+ const _ignoreColumns = columns => {
12
+ if (!(Array.isArray(columns) && columns.some(c => c === '*' || c.as || c.ref))) return true
13
+ }
14
+
15
+ const _getNestedQueryOptions = (ref, expand, expandString) => {
16
+ const isNested = expandString.match(new RegExp(`${ref}(?=\\()`))
17
+ // if no "ref(" i.e. "ref" without open bracket => no nested options => shift and return
18
+ if (!(isNested && Array.isArray(expand) && expand.length)) return { _expand: expandString.replace(ref, '') }
19
+ // if "ref(" found, shift to the first position after "("
20
+ expandString = expandString.slice(isNested.index + ref.length + 1)
21
+ // i.e. we found 1 open bracket already
22
+ let openBrackets = 1
23
+ let head = ''
24
+ // if expandString is '$top=10;$expand=foo($select=*),bar($select=buz));$select=a,b)',
25
+ // then outterQueryOptions is '$top=10;$expand=foo,bar;$select=a,b'
26
+ let outterQueryOptions = ''
27
+ let nestedExpand = ''
28
+
29
+ // parse until the last even closing bracket
30
+ while (openBrackets) {
31
+ const bracketFound = expandString.match(/\(|\)/)
32
+ head = expandString.substring(0, bracketFound.index)
33
+ expandString = expandString.slice(bracketFound.index + 1)
34
+ nestedExpand = `${nestedExpand}${head}${bracketFound[0]}`
35
+ // every time we have only 1 opened bracket and find another one,
36
+ // everything to the left is related to outter query options
37
+ if (openBrackets === 1 && bracketFound[0] === '(') {
38
+ outterQueryOptions = `${outterQueryOptions}${head}`
39
+ }
40
+ openBrackets = bracketFound[0] === '(' ? openBrackets + 1 : openBrackets - 1
41
+ }
42
+ outterQueryOptions = `${outterQueryOptions}${head}`
43
+
44
+ // outterQueryOptions also contain $expand, but without nested options i.e. can safely be split by ";"
45
+ const $select = outterQueryOptions.split(';').find(s => s.startsWith('$select'))
46
+
47
+ const expandIndex = nestedExpand.indexOf('$expand=')
48
+ // last symbol is a pair to open bracket in "ref(" => slice(..., -1)
49
+ const $expand = expandIndex === -1 ? '' : nestedExpand.slice(expandIndex + '$expand='.length, -1)
50
+
51
+ return {
52
+ $select: $select && $select.replace('$select=', ''),
53
+ $expand,
54
+ _expand: expandString
55
+ }
56
+ }
57
+
58
+ const _columnsFromQuery = (columns, target, options) => {
59
+ // must use query.columns as it includes columns from $apply except of $apply=expand()
60
+ // must use query options to get nested $selects inside $expand() as they are mixed into query columns
61
+ // example: GET /Foo?$select=bar&$expand=bar => @odata.context: $metadata#Foo(bar,bar())
62
+ // REVISIT tbd if having expand column in $select could be integrated into query in grammar.peggy
63
+ // REVISIT support $apply=expand()
64
+ if (_ignoreColumns(columns, options)) return ''
65
+ const context = []
66
+ const _select = options.$select ? options.$select.split(',') : []
67
+ let _expand = options.$expand || ''
68
+
69
+ const hasAsterisk = _select.indexOf('*') > -1
70
+ if (hasAsterisk) context.push('*')
71
+
72
+ for (const c of columns) {
73
+ if (!c) continue
74
+ const ref = c.ref && c.ref.join('/')
75
+ if (!hasAsterisk && !c.expand) {
76
+ if (c.as) context.push(c.as)
77
+ else if (ref) context.push(ref)
78
+ } else if (c.expand) {
79
+ if (!hasAsterisk && _select.indexOf(ref) > -1) context.push(ref)
80
+
81
+ const nextTarget = getNavigationIfStruct(target, c.ref)
82
+ if (nextTarget && nextTarget._target && nextTarget._target.elements) {
83
+ const nestedOptions = _getNestedQueryOptions(ref, c.expand, _expand)
84
+ _expand = nestedOptions._expand
85
+ context.push(`${ref}(${_columnsFromQuery(c.expand, nextTarget._target, nestedOptions)})`)
86
+ }
87
+ }
88
+ }
89
+ if (context.length) return context.join(',')
90
+ else if (hasAsterisk) return '*'
91
+ return ''
92
+ }
93
+
94
+ const _processFn = columns => {
95
+ return ({ row, key, element, pathSegmentsInfo }) => {
96
+ if (!(key in row) || row[key] === null) return
97
+ let cur = columns
98
+ if (element.parent._isStructured) {
99
+ const prefix = pathSegmentsInfo.join('/')
100
+ key = `${prefix}/${key}`
101
+ } else {
102
+ for (let p of pathSegmentsInfo) {
103
+ if (!cur[p]) cur[p] = {}
104
+ cur = cur[p]
105
+ }
106
+ }
107
+ if (!cur[key]) cur[key] = {}
108
+ }
109
+ }
110
+
111
+ const _columnsFromData = (data, definition, service) => {
112
+ const columns = {}
113
+ const template = getTemplate('odata-context', service, definition, { pick: element => element.isAssociation })
114
+ if (!template || !template.elements.size) return ''
115
+ const arrayData = Array.isArray(data) ? data : data ? [data] : []
116
+ for (const row of arrayData) {
117
+ templateProcessor({ processFn: _processFn(columns), row, template, pathOptions: { pathSegmentsInfo: [] } })
118
+ }
119
+ return _stringifyColumnsFromData(columns)
120
+ }
121
+
122
+ const _stringifyColumnsFromData = columns =>
123
+ Object.keys(columns)
124
+ .map(key => `${key}(${_stringifyColumnsFromData(columns[key])})`)
125
+ .join(',')
126
+
127
+ const _listColumns = ({ columns, data, isUpsert, returnType, event, /* express */ _req, service, propertyName }) => {
128
+ if (columns.length === 1 && propertyName) return `/${propertyName}`
129
+ // query options ($select, $expand, etc) as strings
130
+ const queryOptions = _req.query
131
+ let columnsStr
132
+ if (!isUpsert && event in { CREATE: 1 }) {
133
+ columnsStr = _columnsFromData(data, returnType, service)
134
+ } else {
135
+ columnsStr = _columnsFromQuery(columns, returnType, queryOptions)
136
+ }
137
+ return columnsStr && `(${columnsStr})`
138
+ }
139
+
140
+ const _getContextUrlPrefix = ({ _req, path, target }) => {
141
+ if (cds.env.odata.contextAbsoluteUrl) {
142
+ try {
143
+ if (typeof cds.env.odata.contextAbsoluteUrl === 'string') {
144
+ const userDefinedURL = new URL(cds.env.odata.contextAbsoluteUrl, cds.env.odata.contextAbsoluteUrl).toString()
145
+ return (!userDefinedURL.endsWith('/') && `${userDefinedURL}/`) || userDefinedURL
146
+ }
147
+ } catch (e) {
148
+ e.message = `cds.odata.contextAbsoluteUrl could not be parsed as URL: ${cds.env.odata.contextAbsoluteUrl}`
149
+ LOG._warn && LOG.warn(e)
150
+ }
151
+ const reqURL = _req && _req.get && _req.get('host') && `${_req.protocol || 'https'}://${_req.get('host')}`
152
+ const baseAppURL = appURL || reqURL || ''
153
+ const serviceUrl = `${(_req && _req.baseUrl) || ''}/`
154
+ return baseAppURL && new URL(serviceUrl, baseAppURL).toString()
155
+ }
156
+ return target && target.params ? '../'.repeat(path.length) : '../'.repeat(path.length - 1)
157
+ }
158
+
159
+ const _findEdmNameFor = (definition, namespace, fullyQualified = false) => {
160
+ let name
161
+ if (!definition) return ''
162
+ if (definition._isStructured) {
163
+ const structured = [definition.name]
164
+ while (definition.parent) {
165
+ definition = definition.parent
166
+ structured.unshift(definition.name)
167
+ }
168
+ name = structured.join('_')
169
+ } else {
170
+ name = definition.name
171
+ }
172
+ if (!name.startsWith(`${namespace}.`)) return name
173
+ return fullyQualified ? name : name.replace(new RegExp(`^${namespace}\\.`), '')
174
+ }
175
+
176
+ const _opResultName = ({ service, returnType, operation, isServiceEntity }) => {
177
+ const { namespace } = service
178
+ if (returnType.name) {
179
+ const resultName = _findEdmNameFor(returnType, namespace)
180
+ if (returnType.name.startsWith(`${namespace}.`)) {
181
+ if (isServiceEntity) return resultName.replace(/\./g, '_')
182
+ return `${namespace}.${resultName.replace(/\./g, '_')}`
183
+ }
184
+ return resultName
185
+ }
186
+ // bound action / function returns inline structure
187
+ if (operation.parent) {
188
+ const boundEntityName = _findEdmNameFor(operation.parent, namespace, true).replace(/\./g, '_')
189
+ // REVISIT exactly this return type name is generated in edm by compiler
190
+ return `${namespace}.return_${boundEntityName}_${_findEdmNameFor(operation, namespace)}`
191
+ }
192
+ // unbound action / function returns inline structure
193
+ // REVISIT exactly this return type name is generated in edm by compiler
194
+ return `${namespace}.return_${_findEdmNameFor(operation, namespace, true).replace(/\./g, '_')}`
195
+ }
196
+
197
+ const _isNavToDraftAdmin = path => path.length > 1 && path[path.length - 1] === 'DraftAdministrativeData'
198
+
199
+ const _getCanonicalUrl = (path, target, model) => {
200
+ const toManySegment =
201
+ path.length > 1 && Array.isArray(path[path.length - 1].where) && path[path.length - 1].where.length && path.pop()
202
+ if (target.params) path.push('Set')
203
+ // construct path with only innermost refs for @odata.context
204
+ const _path = []
205
+ for (const seg of path) {
206
+ if (typeof seg === 'string') _path.push(seg)
207
+ else {
208
+ const _seg = { ...seg }
209
+ if (_seg.where) {
210
+ _seg.where = []
211
+ for (const ele of seg.where) {
212
+ if (ele.ref && ele.ref.length > 1) _seg.where.push({ ref: [ele.ref[ele.ref.length - 1]] })
213
+ else _seg.where.push(ele)
214
+ }
215
+ }
216
+ _path.push(_seg)
217
+ }
218
+ }
219
+ const odataUrl = cds.odata.urlify({ SELECT: { from: { ref: _path } } }, { model, kind: 'odata' })
220
+ let contextPath = odataUrl.path && odataUrl.path.match(/^([^?]*)\??/)[1]
221
+ if (toManySegment) {
222
+ contextPath += `/${toManySegment.id}`
223
+ path.push(toManySegment)
224
+ }
225
+ return contextPath
226
+ }
227
+
228
+ const _getReturnTypeUrl = options => {
229
+ const {
230
+ service,
231
+ isCollection,
232
+ returnType,
233
+ operation,
234
+ path,
235
+ target,
236
+ propertyName,
237
+ isServiceEntity,
238
+ isTargetComposition
239
+ } = options
240
+ const { namespace } = service
241
+ if (operation) {
242
+ const resultName = _opResultName(options)
243
+ if (isServiceEntity) return resultName
244
+ return isCollection ? `Collection(${resultName})` : resultName
245
+ }
246
+ if (isTargetComposition || propertyName || target.params || _isNavToDraftAdmin(path)) {
247
+ return _getCanonicalUrl(path, target, service.model)
248
+ }
249
+ if (isServiceEntity) return _findEdmNameFor(returnType, namespace).replace(/\./g, '_')
250
+ return isCollection ? `Collection(${returnType.name})` : returnType.name
251
+ }
252
+
253
+ const _isSingleEntity = options => {
254
+ const { isCollection, propertyName, returnType, isServiceEntity, isTargetComposition } = options
255
+ if (isCollection || (returnType && returnType._isSingleton) || propertyName) return false
256
+ return isServiceEntity || isTargetComposition
257
+ }
258
+
259
+ const _isStructuredProperty = ({ returnType }) => {
260
+ return returnType.elements && returnType.kind === 'element'
261
+ }
262
+
263
+ const _getContextUrl = options => {
264
+ if (!options.returnType) return ''
265
+ const contextUrlPrefix = _getContextUrlPrefix(options)
266
+
267
+ if (options.returnType.kind === 'service') {
268
+ return `${contextUrlPrefix}$metadata`
269
+ }
270
+
271
+ const returnTypeUrl = _getReturnTypeUrl(options)
272
+ const columnsStringified = options.propertyName || _isStructuredProperty(options) ? '' : _listColumns(options)
273
+ const $entity = _isSingleEntity(options) ? '/$entity' : ''
274
+ return `${contextUrlPrefix}$metadata#${returnTypeUrl}${columnsStringified}${$entity}`
275
+ }
276
+
277
+ const _getAdditionalContextUrl = (query, service, data, eventType, _req, isUpsert) => {
278
+ if (Array.isArray(query)) {
279
+ const additionalContextUrls = []
280
+ for (let i = 1; i < query.length; i++) {
281
+ additionalContextUrls.push(
282
+ _getContextUrl(
283
+ Object.assign(_getQueryInfo(query[i], service, data, eventType), { _req, service, data, isUpsert })
284
+ )
285
+ )
286
+ }
287
+ return additionalContextUrls
288
+ }
289
+ return []
290
+ }
291
+
292
+ const _partialCopyColumn = c => {
293
+ if (c.expand) {
294
+ const copy = { expand: Array.isArray(c.expand) ? c.expand.map(_partialCopyColumn) : c.expand }
295
+ if (c.ref) copy.ref = [...c.ref]
296
+ return copy
297
+ }
298
+ if (c.ref) return { ref: [...c.ref] }
299
+ if (c.as) return { as: c.as }
300
+ return c
301
+ }
302
+
303
+ const _partialCopyColumns = query => {
304
+ if (query.SELECT) {
305
+ // stop digging into subSelects as soon as columns found, as deeper columns will be shadowed by these
306
+ if (!query.SELECT.columns && query.SELECT.from.SELECT) return _partialCopyColumns(query.SELECT.from)
307
+ if (query.SELECT.columns) return query.SELECT.columns.map(_partialCopyColumn)
308
+ }
309
+ return []
310
+ }
311
+
312
+ // eslint-disable-next-line complexity
313
+ const _getPathInfo = (query, model) => {
314
+ const queryFrom =
315
+ (query.SELECT && resolveFromSelect(query)) ||
316
+ (query.INSERT && query.INSERT.into) ||
317
+ (query.UPDATE && query.UPDATE.entity) ||
318
+ (query.DELETE && query.DELETE.from)
319
+ const { last, target, path, isTargetComposition } = targetFromPath(queryFrom, model)
320
+ const operation = (last.kind === 'action' || last.kind === 'function') && last
321
+ let returnType, isCollection, propertyName, unbound
322
+ if (operation) {
323
+ // last segment is bound action/function => must not be in ref
324
+ queryFrom && queryFrom.ref && queryFrom.ref.pop()
325
+ unbound = !operation.parent
326
+ if (operation.returns) {
327
+ returnType = setEntityContained(operation.returns.items || operation.returns, model)
328
+ isCollection = !!operation.returns.items
329
+ }
330
+ // no propertyName as operations do not (yet?) support navigation
331
+ } else {
332
+ returnType = target
333
+ isCollection = Array.isArray(query) || (query.SELECT && !query.SELECT.one)
334
+ propertyName = query._propertyAccess && query.SELECT.columns[0].ref[query.SELECT.columns[0].ref.length - 1]
335
+ if (propertyName && path.slice(-1)[0] !== propertyName) {
336
+ path.push(propertyName)
337
+ returnType = query.SELECT.columns[0].ref.reduce((r, c) => r.elements[c], target)
338
+ }
339
+ }
340
+ const isStream = propertyName && target.elements[propertyName]?.['@Core.MediaType']
341
+ return {
342
+ path,
343
+ target,
344
+ last,
345
+ operation,
346
+ returnType,
347
+ isCollection,
348
+ propertyName,
349
+ isStream,
350
+ unbound,
351
+ isTargetComposition
352
+ }
353
+ }
354
+
355
+ const _getEvent = (eventType, namespace, data, { last, target }) => {
356
+ if (last && (last.kind === 'action' || last.kind === 'function')) {
357
+ // BOUND
358
+ if (last.parent) eventType = last.name
359
+ // UNBOUND
360
+ eventType = last.name.replace(`${namespace}.`, '')
361
+ }
362
+ // draft
363
+ if (target && target._isDraftEnabled) {
364
+ if (eventType === 'CREATE') return 'NEW'
365
+ else if (eventType === 'draftEdit') return 'EDIT'
366
+ else if (eventType === 'UPDATE') return 'PATCH'
367
+ else if (eventType === 'DELETE' && data.IsActiveEntity !== true) return 'CANCEL'
368
+ }
369
+ return eventType
370
+ }
371
+
372
+ const _getQueryInfo = (query, service, data, eventType) => {
373
+ const { namespace, model } = service
374
+ const _pathInfo = _getPathInfo(Array.isArray(query) ? query[0] : query, model)
375
+ const { returnType } = _pathInfo
376
+
377
+ // store original columns before they are polluted by drafts, db and so on
378
+ const columns = _partialCopyColumns(Array.isArray(query) ? query[0] : query)
379
+ const isServiceEntity = _findEdmNameFor(returnType, namespace) in service.entities && !returnType._isContained
380
+ const event = _getEvent(eventType, namespace, data, _pathInfo)
381
+ return Object.assign(_pathInfo, {
382
+ columns,
383
+ isServiceEntity,
384
+ event
385
+ })
386
+ }
387
+
388
+ module.exports = (query, eventType, service, data, /* express req */ _req, isUpsert) => {
389
+ const queryInfo = _getQueryInfo(query, service, data, eventType)
390
+
391
+ const { isCollection, isStream, propertyName, unbound, event, returnType, isServiceEntity } = queryInfo
392
+
393
+ const contextUrl = _getContextUrl(Object.assign(queryInfo, { _req, service, data, isUpsert }))
394
+
395
+ const additionalContextUrl = _getAdditionalContextUrl(query, service, data, eventType, _req, isUpsert)
396
+
397
+ return {
398
+ event,
399
+ unbound,
400
+ metadata: {
401
+ isCollection,
402
+ isStream,
403
+ propertyName,
404
+ contextUrl,
405
+ returnType,
406
+ isServiceEntity,
407
+ additionalContextUrl
408
+ }
409
+ }
410
+ }
@@ -3,6 +3,7 @@ const { where2obj } = require('../../_runtime/common/utils/cqn')
3
3
  const _handleXpr = (relation, keys, seg_keys) => {
4
4
  const join = [...relation]
5
5
  while (join.length >= 3) {
6
+ // eslint-disable-next-line no-unused-vars
6
7
  const [left, _, right] = join
7
8
 
8
9
  if (left.xpr) {
@@ -14,11 +15,13 @@ const _handleXpr = (relation, keys, seg_keys) => {
14
15
 
15
16
  if (left.ref?.[0] === 'target') {
16
17
  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
+ const keyValue = 'val' in right ? right.val : seg_keys[right.ref[1]]
19
+ if (keyValue !== undefined) keys[left.ref[1]] = keyValue
18
20
  join.splice(0, 4)
19
21
  } else if (right.ref?.[0] === 'target') {
20
22
  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]]
23
+ const keyValue = 'val' in left ? left.val : seg_keys[left.ref[1]]
24
+ if (keyValue !== undefined) keys[right.ref[1]] = keyValue
22
25
  join.splice(0, 4)
23
26
  }
24
27
  }
@@ -0,0 +1,23 @@
1
+ const cds = require('../../_runtime/cds')
2
+ const { Request } = cds
3
+
4
+ const readAfterWrite = async (req, srv, query) => {
5
+ // gracefully set location and no body if no read auth or not readable capability
6
+ let result
7
+ try {
8
+ const _req = new Request({ query, event: 'READ', _: req._, params: req.params })
9
+ result = await srv.dispatch(_req)
10
+ // NEW/PATCH must not include DraftAdministrativeData_DraftUUID for plain v4 usage, however required for odata-v2
11
+ if (result && req.target._isDraftEnabled && req.headers?.['x-cds-odata-version'] !== 'v2') {
12
+ delete result.DraftAdministrativeData_DraftUUID
13
+ }
14
+ } catch (e) {
15
+ // read was not possible because of access restrictions => ignore
16
+ if (!(Number(e.code) in { 401: 1, 403: 1, 404: 1, 405: 1 })) throw e
17
+ result = null
18
+ }
19
+
20
+ return result
21
+ }
22
+
23
+ module.exports = readAfterWrite
@@ -91,6 +91,7 @@ const _getParent = (model, name) => {
91
91
  }
92
92
 
93
93
  const addEtags = (row, key) => {
94
+ if (!row[key]) return
94
95
  row['$etag'] = row[key].startsWith('W/') ? row[key] : `W/"${row[key]}"`
95
96
  }
96
97
 
@@ -101,10 +102,8 @@ const _processCategory = (category, elementInfo) => {
101
102
  case '@odata.etag':
102
103
  addEtags(row, key)
103
104
  break
104
- case 'stringify':
105
- // REVISIT: remove once DB always returns strings
106
- if (row[key] == null) return
107
- row[key] = `${row[key]}`
105
+ case '@cds.api.ignore':
106
+ delete row[key]
108
107
  break
109
108
  // no default
110
109
  }
@@ -123,7 +122,7 @@ const _processorFn = () => elementInfo => {
123
122
  const _pick = element => {
124
123
  const categories = []
125
124
  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
125
+ if (element['@cds.api.ignore']) categories.push('@cds.api.ignore')
127
126
  if (categories.length) return { categories }
128
127
  }
129
128
 
@@ -4,14 +4,12 @@ const cds = require('../_runtime/cds')
4
4
  const express = require('express')
5
5
 
6
6
  const parse_factory = require('./middleware/parse')
7
- const input_factory = require('./middleware/input')
8
7
 
9
8
  const create_factory = require('./middleware/create')
10
9
  const read_factory = require('./middleware/read')
11
10
  const update_factory = require('./middleware/update')
12
11
  const delete_factory = require('./middleware/delete')
13
12
  const operation_factory = require('./middleware/operation')
14
- const payload_factory = require('./middleware/payload')
15
13
 
16
14
  const error_factory = require('./middleware/error')
17
15
 
@@ -19,10 +17,6 @@ const { bufferToBase64 } = require('../_runtime/common/utils/binary')
19
17
  const { getAccessRestrictions } = require('../_runtime/common/utils/restrictions')
20
18
 
21
19
  const RestAdapter = function (srv) {
22
- const parse = parse_factory(srv)
23
- const input = input_factory(srv)
24
- const payload = payload_factory(srv)
25
-
26
20
  const router = express.Router()
27
21
 
28
22
  // -----------------------------------------------------------------------------------------
@@ -38,8 +32,8 @@ const RestAdapter = function (srv) {
38
32
  // > unauthorized or forbidden?
39
33
  if (req.user._is_anonymous) {
40
34
  // NOTE: "return req._login()" would not invoke custom error handlers
41
- if (req._login) res.set('WWW-Authenticate', `Basic realm="Users"`)
42
- else if (req.user._challenges) res.set('WWW-Authenticate', req.user._challenges.join(';'))
35
+ if (req._login) res.set('www-authenticate', `Basic realm="Users"`)
36
+ else if (req.user._challenges) res.set('www-authenticate', req.user._challenges.join(';'))
43
37
  throw cds.error('Unauthorized', { statusCode: 401, code: '401' })
44
38
  }
45
39
  throw cds.error('Forbidden', { statusCode: 403, code: '403' })
@@ -89,11 +83,8 @@ const RestAdapter = function (srv) {
89
83
  }
90
84
  next()
91
85
  })
92
- router.use(express.json()) // REVISIT: -> belongs to the parses
93
- router.use(parse) // REVISIT: -> move to actual handler(s)
94
- router.use(payload) // REVISIT: -> move?
95
- // payload validation
96
- router.use(input) // REVISIT: This is protocol-independent, isn't it? -> move to service layer
86
+ router.use(express.json())
87
+ router.use(parse_factory(srv))
97
88
 
98
89
  // -----------------------------------------------------------------------------------------
99
90
  // begin tx
@@ -1,10 +1,34 @@
1
1
  const cds = require('../../_runtime/cds')
2
2
  const { INSERT, SELECT, UPDATE, DELETE } = cds.ql
3
3
 
4
+ const { base64ToBuffer } = require('../../_runtime/common/utils/binary')
5
+ const { deepCopy } = require('../../_runtime/common/utils/copy')
4
6
  const { where2obj } = require('../../_runtime/common/utils/cqn')
5
-
6
7
  const { convertStructured } = require('../../_runtime/common/utils/ucsn')
7
- const { deepCopy } = require('../../_runtime/common/utils/copy')
8
+
9
+ const getTemplate = require('../../_runtime/common/utils/template')
10
+ const templateProcessor = require('../../_runtime/common/utils/templateProcessor')
11
+ const { checkStaticElementByKey } = require('../../_runtime/cds-services/util/assert')
12
+
13
+ const _processorFn = errors => {
14
+ return ({ row, key, plain: categories, target }) => {
15
+ // REVISIT move validation to generic asserter => see PR 717
16
+ if (categories['static_validation'] && row[key] != null) {
17
+ const validations = checkStaticElementByKey(target, key, row[key])
18
+ errors.push(...validations)
19
+ }
20
+ }
21
+ }
22
+
23
+ const _picker = element => {
24
+ const categories = {}
25
+ if (Array.isArray(element)) return
26
+ if (element._isStructured || element.isAssociation || element.items) return
27
+ categories['static_validation'] = true
28
+ return categories
29
+ }
30
+
31
+ const _cache = req => `rest-input;skip-key-validation:${req.method !== 'POST'}`
8
32
 
9
33
  module.exports = srv => (req, res, next) => {
10
34
  // REVISIT: Once we don't display the error message location in terms of an offset, but instead a copy of the
@@ -107,20 +131,29 @@ module.exports = srv => (req, res, next) => {
107
131
  } else {
108
132
  // TODO: add keys from url into payload (overwriting if already present) -> document this behavior, also for OData
109
133
  const payload = deepCopy(args || req.body)
134
+ let errs
110
135
  if (cds.env.features.cds_assert) {
111
136
  const assertOptions = {
112
137
  filter: true,
113
138
  http: { req },
114
139
  mandatories: req.method === 'POST' || req.method === 'PUT' || undefined
115
140
  }
116
- const errs = cds.assert(payload, definition, assertOptions)
117
- if (errs) {
118
- if (errs.length === 1) throw errs[0]
119
- throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
120
- }
141
+ errs = cds.assert(payload, definition, assertOptions)
121
142
  } else {
122
143
  convertStructured(srv, operation || definition, payload, { cleanupStruct: cds.env.features.rest_struct_data })
144
+ const template = getTemplate(_cache(req), srv, definition, { pick: _picker })
145
+ if (template && template.elements.size) {
146
+ errs = []
147
+ for (const row of Array.isArray(payload) ? payload : [payload]) {
148
+ templateProcessor({ processFn: _processorFn(errs), row, template })
149
+ }
150
+ }
151
+ }
152
+ if (errs?.length) {
153
+ if (errs.length === 1) throw errs[0]
154
+ throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
123
155
  }
156
+ base64ToBuffer(payload, srv, definition)
124
157
  req._data = payload
125
158
  }
126
159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "7.8.2",
3
+ "version": "7.9.1",
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
@@ -29,6 +29,7 @@ module.exports = async function cds_server (options) {
29
29
 
30
30
  // load and prepare models
31
31
  const csn = await cds.load(o.from||'*',o) .then (cds.minify)
32
+ cds.edmxs = cds.compile.to.edmx.files (csn)
32
33
  cds.model = cds.compile.for.nodejs (csn)
33
34
 
34
35
  // connect to essential framework services