@sap/cds 5.7.2 → 5.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 (148) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/app/fiori/routes.js +1 -1
  3. package/bin/deploy/to-hana/cfUtil.js +251 -138
  4. package/bin/deploy/to-hana/gitUtil.js +55 -0
  5. package/bin/deploy/to-hana/hana.js +92 -93
  6. package/bin/deploy/to-hana/hdiDeployUtil.js +42 -27
  7. package/bin/deploy/to-hana/index.js +14 -13
  8. package/bin/mtx/in-cds.js +1 -0
  9. package/bin/serve.js +1 -1
  10. package/bin/version.js +1 -0
  11. package/lib/compile/cdsc.js +0 -6
  12. package/lib/compile/minify.js +1 -1
  13. package/lib/compile/resolve.js +1 -1
  14. package/lib/compile/to/srvinfo.js +1 -1
  15. package/lib/core/classes.js +21 -1
  16. package/lib/env/index.js +3 -2
  17. package/lib/env/requires.js +4 -0
  18. package/lib/i18n/localize.js +5 -8
  19. package/lib/index.js +1 -0
  20. package/lib/log/errors.js +1 -1
  21. package/lib/ql/SELECT.js +2 -2
  22. package/lib/req/cds-context.js +1 -1
  23. package/lib/req/context.js +1 -1
  24. package/lib/serve/Transaction.js +9 -5
  25. package/lib/serve/index.js +13 -21
  26. package/lib/utils/tests.js +90 -66
  27. package/libx/_runtime/audit/generic/personal/modification.js +0 -8
  28. package/libx/_runtime/auth/index.js +7 -6
  29. package/libx/_runtime/auth/strategies/dwc.js +43 -0
  30. package/libx/_runtime/auth/utils.js +24 -0
  31. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -38
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -42
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  37. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
  38. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +18 -8
  39. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +1 -2
  40. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +0 -1
  41. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +7 -2
  42. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +9 -0
  43. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +21 -30
  44. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +12 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +2 -1
  46. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
  47. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +5 -8
  48. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
  52. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
  53. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +1 -1
  54. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  55. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  56. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  57. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +18 -5
  58. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  60. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +80 -21
  61. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +7 -5
  62. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  63. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  64. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  65. package/libx/_runtime/cds-services/services/Service.js +1 -1
  66. package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
  67. package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
  68. package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
  69. package/libx/_runtime/common/aspects/Association.js +16 -0
  70. package/libx/_runtime/common/composition/data.js +28 -37
  71. package/libx/_runtime/common/composition/delete.js +107 -58
  72. package/libx/_runtime/common/composition/index.js +2 -1
  73. package/libx/_runtime/common/composition/insert.js +13 -13
  74. package/libx/_runtime/common/composition/update.js +39 -34
  75. package/libx/_runtime/common/error/frontend.js +17 -2
  76. package/libx/_runtime/common/generic/auth.js +20 -85
  77. package/libx/_runtime/common/generic/crud.js +22 -1
  78. package/libx/_runtime/common/i18n/messages.properties +3 -0
  79. package/libx/_runtime/common/utils/cqn.js +2 -6
  80. package/libx/_runtime/common/utils/cqn2cqn4sql.js +97 -123
  81. package/libx/_runtime/common/utils/csn.js +14 -3
  82. package/libx/_runtime/common/utils/foreignKeyPropagations.js +18 -1
  83. package/libx/_runtime/common/utils/keys.js +2 -1
  84. package/libx/_runtime/common/utils/path.js +1 -1
  85. package/libx/_runtime/common/utils/resolveView.js +12 -4
  86. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  87. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  88. package/libx/_runtime/common/utils/structured.js +1 -1
  89. package/libx/_runtime/common/utils/vcap.js +27 -10
  90. package/libx/_runtime/db/data-conversion/post-processing.js +42 -35
  91. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  92. package/libx/_runtime/db/expand/expandCQNToJoin.js +27 -29
  93. package/libx/_runtime/db/expand/index.js +3 -0
  94. package/libx/_runtime/db/generic/create.js +0 -10
  95. package/libx/_runtime/db/generic/index.js +3 -0
  96. package/libx/_runtime/db/generic/read.js +2 -24
  97. package/libx/_runtime/db/generic/rewrite.js +1 -3
  98. package/libx/_runtime/db/generic/update.js +1 -1
  99. package/libx/_runtime/db/query/delete.js +10 -4
  100. package/libx/_runtime/db/query/insert.js +3 -3
  101. package/libx/_runtime/db/query/read.js +15 -8
  102. package/libx/_runtime/db/query/update.js +5 -5
  103. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  104. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  105. package/libx/_runtime/db/sql-builder/index.js +3 -0
  106. package/libx/_runtime/db/utils/columns.js +5 -2
  107. package/libx/_runtime/db/utils/deep.js +6 -8
  108. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  109. package/libx/_runtime/fiori/generic/before.js +73 -49
  110. package/libx/_runtime/fiori/generic/edit.js +14 -18
  111. package/libx/_runtime/fiori/generic/patch.js +8 -11
  112. package/libx/_runtime/fiori/generic/read.js +22 -17
  113. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  114. package/libx/_runtime/hana/Service.js +1 -1
  115. package/libx/_runtime/hana/conversion.js +10 -0
  116. package/libx/_runtime/hana/execute.js +33 -16
  117. package/libx/_runtime/hana/localized.js +1 -1
  118. package/libx/_runtime/hana/search.js +3 -3
  119. package/libx/_runtime/hana/search2cqn4sql.js +22 -21
  120. package/libx/_runtime/hana/searchToContains.js +1 -1
  121. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +4 -2
  122. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  123. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  124. package/libx/_runtime/messaging/file-based.js +3 -1
  125. package/libx/_runtime/messaging/message-queuing-utils/options-messaging.js +1 -0
  126. package/libx/_runtime/messaging/service.js +16 -7
  127. package/libx/_runtime/remote/utils/client.js +33 -20
  128. package/libx/_runtime/remote/utils/data.js +53 -12
  129. package/libx/_runtime/sqlite/Service.js +1 -1
  130. package/libx/_runtime/sqlite/conversion.js +10 -0
  131. package/libx/_runtime/sqlite/localized.js +1 -1
  132. package/libx/_runtime/types/api.js +2 -2
  133. package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
  134. package/libx/odata/afterburner.js +29 -6
  135. package/libx/odata/cqn2odata.js +9 -0
  136. package/libx/odata/grammar.pegjs +101 -45
  137. package/libx/odata/index.js +7 -1
  138. package/libx/odata/parser.js +1 -1
  139. package/libx/odata/utils.js +2 -2
  140. package/libx/rest/RestAdapter.js +29 -1
  141. package/libx/rest/middleware/auth.js +1 -3
  142. package/libx/rest/middleware/parse.js +1 -0
  143. package/package.json +1 -1
  144. package/server.js +1 -1
  145. package/bin/deploy/to-hana/logger.js +0 -27
  146. package/bin/deploy/to-hana/runCommand.js +0 -113
  147. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
  148. package/libx/_runtime/common/utils/auth.js +0 -16
@@ -1,7 +1,10 @@
1
1
  const cds = require('../../../../cds')
2
+
3
+ const { Readable } = require('stream')
4
+ const { big } = require('@sap/cds-foss')
5
+
2
6
  const getTemplate = require('../../../../common/utils/template')
3
7
  const templateProcessor = require('../../../../common/utils/templateProcessor')
4
- const { big } = require('@sap/cds-foss')
5
8
  const { omitValue, applyOmitValuesPreference } = require('./omitValues')
6
9
 
7
10
  const METADATA = {
@@ -23,36 +26,59 @@ const METADATA = {
23
26
  $mediaEditLink: '*@odata.mediaEditLink',
24
27
  $mediaReadLink: '*@odata.mediaReadLink',
25
28
  $mediaContentType: '*@odata.mediaContentType',
29
+ $mediaContentDispositionFilename: '*@odata.mediaContentDispositionFilename', // > not supported by okra
30
+ $mediaContentDispositionType: '*@odata.mediaContentDispositionType', // > not supported by okra
26
31
  $mediaEtag: '*@odata.mediaEtag'
27
32
  }
28
33
 
34
+ const _getPropertyName = req => {
35
+ const segments = req._.odataReq.getUriInfo().getPathSegments()
36
+ if (segments[segments.length - 1].getKind().match(/\w*\.PROPERTY$/)) {
37
+ return segments[segments.length - 1].getProperty().getName()
38
+ }
39
+ if (
40
+ segments[segments.length - 1].getKind() === 'VALUE' &&
41
+ segments[segments.length - 2].getKind() === 'PRIMITIVE.PROPERTY'
42
+ ) {
43
+ return segments[segments.length - 2].getProperty().getName()
44
+ }
45
+ }
46
+
29
47
  /**
30
48
  * Convert any result to the result object structure, which is expected of odata-v4.
31
49
  *
32
50
  * @param {*} result
33
- * @param {*} [arg]
51
+ * @param {*} [req]
34
52
  * @returns {string | object}
35
53
  */
36
- const toODataResult = (result, arg) => {
37
- if (result === undefined || result === null) return ''
38
-
39
- if (arg) {
40
- if (typeof arg === 'object') {
41
- arg = arg._.odataReq.getUriInfo().getLastSegment().isCollection() ? 'Array' : ''
42
- }
43
-
44
- if (!Array.isArray(result) && arg === 'Array') {
45
- result = [result]
46
- } else if (Array.isArray(result) && arg !== 'Array') {
47
- result = result[0]
48
- }
54
+ // eslint-disable-next-line complexity
55
+ const toODataResult = (result, req) => {
56
+ if (result == null) return ''
57
+
58
+ let propertyName, isStream
59
+ if (req) {
60
+ propertyName = _getPropertyName(req)
61
+ isStream =
62
+ req._.odataReq.getUriInfo().getLastSegment().getProperty() &&
63
+ req._.odataReq.getUriInfo().getLastSegment().getProperty().getType().toString() === 'Edm.Stream'
64
+
65
+ const isCollection = !propertyName && req._.odataReq.getUriInfo().getLastSegment().isCollection()
66
+ if (isCollection && !Array.isArray(result)) result = [result]
67
+ else if (!isCollection && Array.isArray(result)) result = result[0]
49
68
  }
50
69
 
51
- const odataResult = {
52
- value: result
70
+ let value = result
71
+ if (typeof result === 'object') {
72
+ if ('value' in result && (result.value instanceof Readable || isStream)) value = result.value
73
+ else if (propertyName) value = result[propertyName]
53
74
  }
54
75
 
76
+ const odataResult = { value }
77
+
55
78
  if (typeof result === 'object') {
79
+ // backwards compatibility for content-type in stream
80
+ if (result['*@odata.mediaContentType']) result.$mediaContentType = result['*@odata.mediaContentType']
81
+
56
82
  for (const key in METADATA) {
57
83
  // do not set "@odata.context" as it may be inherited of remote service
58
84
  if (key === '$context') {
@@ -60,6 +86,18 @@ const toODataResult = (result, arg) => {
60
86
  continue
61
87
  }
62
88
 
89
+ // REVISIT: okra doesn't support content disposition
90
+ if (key === '$mediaContentDispositionFilename' && result[key] && req) {
91
+ const cdt = result.$mediaContentDispositionType || 'attachment'
92
+ req._.odataRes.setHeader('Content-Disposition', `${cdt}; filename="${encodeURIComponent(result[key])}"`)
93
+ delete result[key]
94
+ continue
95
+ }
96
+ if (key === '$mediaContentDispositionType') {
97
+ delete result[key]
98
+ continue
99
+ }
100
+
63
101
  if (key in result) {
64
102
  odataResult[METADATA[key]] = result[key]
65
103
  delete result[key]
@@ -112,6 +150,13 @@ const addAssociationToRow = (row, foreignKey, foreignKeyElement) => {
112
150
  delete row[foreignKey]
113
151
  }
114
152
 
153
+ const localizeAfterDraftActivate = (row, key, locale) => {
154
+ if (row.texts && Object.prototype.hasOwnProperty.call(row, key)) {
155
+ const texts = row.texts.filter(t => t.locale === locale)[0]
156
+ if (texts && texts[key]) row[key] = texts[key]
157
+ }
158
+ }
159
+
115
160
  const _processCategory = (category, processArgs, req, options, previousResult) => {
116
161
  const { row, key, element } = processArgs
117
162
 
@@ -136,6 +181,10 @@ const _processCategory = (category, processArgs, req, options, previousResult) =
136
181
  if (key !== 'DraftAdministrativeData_DraftUUID') delete row[key]
137
182
  break
138
183
 
184
+ case 'localizeAfterDraftActivate':
185
+ localizeAfterDraftActivate(row, key, req.locale)
186
+ break
187
+
139
188
  // no default
140
189
  }
141
190
  }
@@ -197,16 +246,25 @@ const _pick = options => (element, target, parent) => {
197
246
  const categories = []
198
247
 
199
248
  if (element['@odata.etag']) categories.push('@odata.etag')
249
+
200
250
  if (element.type === 'cds.Decimal') categories.push('@cds.Decimal')
251
+
201
252
  categories.push(..._assocs(element, target))
253
+
202
254
  if (options.omitValuesPreference) categories.push('@odata.omitValues')
255
+ if (options.event === 'draftActivate' && options.locale && options.locale !== 'en' && element.localized === true) {
256
+ categories.push('localizeAfterDraftActivate')
257
+ }
258
+
203
259
  if (categories.length) return { categories }
204
260
  }
205
261
 
206
- const _getOptions = headers => {
262
+ const _getOptions = ({ headers, locale, event }) => {
207
263
  const options = {
208
264
  decimals: null,
209
- omitValuesPreference: null
265
+ omitValuesPreference: null,
266
+ locale,
267
+ event
210
268
  }
211
269
 
212
270
  if (!headers) return options
@@ -231,7 +289,8 @@ const _getOptions = headers => {
231
289
  const _generateCacheKey = (headers, options) => {
232
290
  let key = 'postProcess' // default template cache key for post processing
233
291
  if (headers.prefer) key += `:${headers.prefer}`
234
- if (options.decimals) key += `:exponentialDecimals=true`
292
+ if (options.decimals) key += `:ExponentialDecimals=true`
293
+ if (options.event === 'draftActivate' && options.locale) key += `:locale=${options.locale}`
235
294
  return key
236
295
  }
237
296
 
@@ -241,7 +300,7 @@ const postProcess = (req, res, service, result, previousResult) => {
241
300
 
242
301
  if (!target || !result || !model || !model.definitions[target.name]) return
243
302
 
244
- const options = _getOptions(headers)
303
+ const options = _getOptions(req)
245
304
  const cacheKey = _generateCacheKey(headers, options)
246
305
  const parent = _getParent(model, target.name)
247
306
  const template = getTemplate(cacheKey, service, target, { pick: _pick(options) }, parent)
@@ -18,7 +18,7 @@ const isStreaming = segments => {
18
18
  const getStreamProperties = (req, model) => {
19
19
  const mediaTypeProperty = Object.values(req.target.elements).find(val => val['@Core.MediaType'])
20
20
 
21
- let contentType, contentDisposition
21
+ let contentType, contentDispositionFilename
22
22
  const columns = []
23
23
  if (typeof mediaTypeProperty['@Core.MediaType'] === 'object') {
24
24
  let contentTypeProperty = mediaTypeProperty['@Core.MediaType']['=']
@@ -46,12 +46,13 @@ const getStreamProperties = (req, model) => {
46
46
  `@Core.ContentDisposition.Filename in entity "${req.target.name}" points to property "${contentDispositionProperty}" which was renamed or is not part of the projection. You must update the annotation value.`
47
47
  )
48
48
  } else {
49
- columns.push({ ref: [contentDispositionProperty], as: 'contentDisposition' })
49
+ columns.push({ ref: [contentDispositionProperty], as: 'contentDispositionFilename' })
50
50
  }
51
51
  } else {
52
- contentDisposition = mediaTypeProperty['@Core.ContentDisposition.Filename']
52
+ contentDispositionFilename = mediaTypeProperty['@Core.ContentDisposition.Filename']
53
53
  }
54
54
  }
55
+ const contentDispositionType = mediaTypeProperty['@Core.ContentDisposition.Type']
55
56
 
56
57
  if (columns.length && cds.db && !req.target._hasPersistenceSkip) {
57
58
  // used cloned path
@@ -68,11 +69,12 @@ const getStreamProperties = (req, model) => {
68
69
  .run(select)
69
70
  .then(res => ({
70
71
  contentType: (res && res.contentType) || contentType,
71
- contentDisposition: (res && res.contentDisposition) || contentDisposition
72
+ contentDispositionFilename: (res && res.contentDispositionFilename) || contentDispositionFilename,
73
+ contentDispositionType
72
74
  }))
73
75
  }
74
76
 
75
- return Promise.resolve({ contentType, contentDisposition })
77
+ return Promise.resolve({ contentType, contentDispositionFilename, contentDispositionType })
76
78
  }
77
79
 
78
80
  module.exports = {
@@ -12,9 +12,10 @@ const { contentTypeCheck } = require('./utils/header-checks')
12
12
  const parse = require('./utils/parse-url')
13
13
  const { base64toBuffer } = require('./utils/binary')
14
14
 
15
- const { UNAUTHORIZED, FORBIDDEN, getRequiresAsArray } = require('../../../common/utils/auth')
15
+ const { UNAUTHORIZED, FORBIDDEN, getRequiresAsArray } = require('../../../auth/utils')
16
16
 
17
17
  const PPP = { POST: 1, PUT: 1, PATCH: 1 }
18
+ const GH = { GET: 1, HEAD: 1 }
18
19
 
19
20
  let _i18n
20
21
  const i18n = (...args) => {
@@ -93,6 +94,21 @@ class Rest {
93
94
  throw FORBIDDEN
94
95
  }
95
96
 
97
+ // service root
98
+ if (req.path === '/' && GH[req.method]) {
99
+ if (req.method === 'HEAD') return res.json({})
100
+ else
101
+ return res.json(
102
+ Object.keys(srv.entities).reduce(
103
+ (acc, cur) => {
104
+ acc.entities.push({ name: cur, url: cur })
105
+ return acc
106
+ },
107
+ { entities: [] }
108
+ )
109
+ )
110
+ }
111
+
96
112
  // content-type check, parse url, and base64 to buffer
97
113
  try {
98
114
  if (PPP[req.method]) contentTypeCheck(req)
@@ -115,6 +131,11 @@ class Rest {
115
131
  }
116
132
  })
117
133
 
134
+ // HEAD
135
+ this.router.head('/*', (req, res, next) => {
136
+ read(req, res, next)
137
+ })
138
+
118
139
  // GET
119
140
  this.router.get('/*', (req, res, next) => {
120
141
  // READ or custom operation?
@@ -28,9 +28,7 @@ module.exports = service => {
28
28
 
29
29
  result = await tx.dispatch(req)
30
30
 
31
- if (result == null) {
32
- throw getError(404, 'NO_MATCHING_RESOURCE')
33
- }
31
+ if (result == null) throw getError(404, 'NO_MATCHING_RESOURCE')
34
32
 
35
33
  bufferToBase64(result, segments[0])
36
34
 
@@ -41,6 +39,13 @@ module.exports = service => {
41
39
  await tx.rollback(e).catch(() => {})
42
40
  } finally {
43
41
  if (err) next(err)
42
+ else if (restReq.method === 'HEAD')
43
+ restRes
44
+ .set({
45
+ 'content-type': 'application/json; charset=utf-8',
46
+ 'content-length': JSON.stringify(result).length
47
+ })
48
+ .end()
44
49
  else restRes.send(toRestResult(result))
45
50
  }
46
51
  }
@@ -230,6 +230,9 @@ module.exports = {
230
230
  POST: (service, req) => {
231
231
  return parseCreateOrReadUrl('CREATE', service, req)
232
232
  },
233
+ HEAD: (service, req) => {
234
+ return parseCreateOrReadUrl('READ', service, req)
235
+ },
233
236
  GET: (service, req) => {
234
237
  return parseCreateOrReadUrl('READ', service, req)
235
238
  },
@@ -128,7 +128,7 @@ class ApplicationService extends cds.Service {
128
128
  // compat
129
129
  restoreLink(req)
130
130
  if (req.query.SELECT && req.query.SELECT._4odata) {
131
- q.SELECT._4odata = req.query.SELECT._4odata
131
+ Object.defineProperty(q.SELECT, '_4odata', { value: req.query.SELECT._4odata })
132
132
  }
133
133
 
134
134
  // REVISIT: We need to provide target explicitly because it's cached already within ensure_target
@@ -17,7 +17,10 @@ const { DRAFT_COLUMNS_UNION } = require('../../../common/constants/draft')
17
17
  * @param [options.filterVirtual=false]
18
18
  * @returns {Array<object>} - array of columns
19
19
  */
20
- const getColumns = (entity, { onlyNames = false, removeIgnore = false, filterDraft = true, filterVirtual = false }) => {
20
+ const getColumns = (
21
+ entity,
22
+ { onlyNames = false, removeIgnore = false, filterDraft = true, filterVirtual = false, keysOnly = false }
23
+ ) => {
21
24
  const skipDraft = filterDraft && entity._isDraftEnabled
22
25
  const columns = []
23
26
  const elements = entity.elements
@@ -28,6 +31,7 @@ const getColumns = (entity, { onlyNames = false, removeIgnore = false, filterDra
28
31
  if (filterVirtual && element.virtual) continue
29
32
  if (removeIgnore && element['@cds.api.ignore']) continue
30
33
  if (skipDraft && DRAFT_COLUMNS_UNION.includes(each)) continue
34
+ if (keysOnly && !element.key) continue
31
35
  columns.push(onlyNames ? each : element)
32
36
  }
33
37
 
@@ -285,30 +285,29 @@ const _mergeArrays = (entity, oldValue, newValue) => {
285
285
  return merged
286
286
  }
287
287
 
288
- const mergeJsonDeep = (entity, oldValue, newValue) => {
288
+ const mergeJsonDeep = (entity, oldValue, value) => {
289
+ // REVISIT readAfterWrite -> commit -> postProcessing
290
+ // Detach result from req.data
291
+ const newValue = value ? Object.assign({}, value) : oldValue // if newValue === undefined
289
292
  if (_isObject(oldValue) && _isObject(newValue)) {
290
- Object.keys(newValue).forEach(key => {
291
- if (_isObject(newValue[key])) {
292
- if (!(key in oldValue)) Object.assign(oldValue, { [key]: newValue[key] })
293
- else {
294
- const target = entity && entity.elements[key] && entity.elements[key]._target
295
- oldValue[key] = mergeJsonDeep(target, oldValue[key], newValue[key])
296
- }
297
- } else if (Array.isArray(newValue[key])) {
298
- if (!(key in oldValue)) Object.assign(oldValue, { [key]: newValue[key] })
299
- else {
300
- const target = entity && entity.elements[key] && entity.elements[key]._target
293
+ // append to newValue to keep an order of attributes as might be defined in custom handler
294
+ Object.keys(oldValue).forEach(key => {
295
+ if (!(key in newValue)) {
296
+ Object.assign(newValue, { [key]: oldValue[key] })
297
+ } else {
298
+ const target = entity && entity.elements[key] && entity.elements[key]._target
299
+ if (_isObject(newValue[key])) {
300
+ newValue[key] = mergeJsonDeep(target, oldValue[key], newValue[key])
301
+ } else if (Array.isArray(newValue[key])) {
301
302
  if (target) {
302
- oldValue[key] = _mergeArrays(target, oldValue[key], newValue[key])
303
+ newValue[key] = _mergeArrays(target, oldValue[key], newValue[key])
303
304
  }
304
305
  // Can't merge items without target
305
306
  }
306
- } else {
307
- Object.assign(oldValue, { [key]: newValue[key] })
308
307
  }
309
308
  })
310
309
  }
311
- return oldValue
310
+ return newValue
312
311
  }
313
312
 
314
313
  // Signature similar to Object.assign(oldValue, newValue)
@@ -45,14 +45,8 @@ module.exports = class {
45
45
  }
46
46
 
47
47
  async _addPartialPersistentState(req) {
48
- const deepUpdateData = await selectDeepUpdateData(
49
- this._srv.model.definitions,
50
- req.query,
51
- req,
52
- true,
53
- true,
54
- this._srv
55
- )
48
+ // REVISIT: cds.context.model?
49
+ const deepUpdateData = await selectDeepUpdateData(this._srv, this._srv.model, req, true)
56
50
  req._.partialPersistentState = deepUpdateData
57
51
  }
58
52
 
@@ -1,11 +1,24 @@
1
1
  // global.cds is used on purpose here!
2
2
  const cds = global.cds
3
3
 
4
+ // requesting logger without module on purpose!
5
+ const LOG = cds.log()
6
+
4
7
  const ODATA_CONTAINED = '@odata.contained'
5
8
 
6
9
  const { isSelfManaged, isBacklink, getAnchor, getBacklink } = require('./utils')
7
10
  const { foreignKeyPropagations } = require('../utils/foreignKeyPropagations')
8
11
 
12
+ const _logDeprecationForODataContained = () => {
13
+ if (!cds._deprecationWarningForODataContained) {
14
+ LOG._warn &&
15
+ LOG.warn(
16
+ 'Annotation "@odata.contained" is deprecated and will be removed in an upcoming release. Use compositions instead of associations.'
17
+ )
18
+ cds._deprecationWarningForODataContained = true
19
+ }
20
+ }
21
+
9
22
  module.exports = class {
10
23
  get _isAssociationStrict() {
11
24
  return (
@@ -15,6 +28,7 @@ module.exports = class {
15
28
  }
16
29
 
17
30
  get _isAssociationEffective() {
31
+ this[ODATA_CONTAINED] && _logDeprecationForODataContained()
18
32
  return (
19
33
  this.own('__isAssociationEffective') ||
20
34
  this.set(
@@ -25,6 +39,7 @@ module.exports = class {
25
39
  }
26
40
 
27
41
  get _isCompositionEffective() {
42
+ this[ODATA_CONTAINED] && _logDeprecationForODataContained()
28
43
  return (
29
44
  this.own('__isCompositionEffective') ||
30
45
  this.set(
@@ -36,6 +51,7 @@ module.exports = class {
36
51
  }
37
52
 
38
53
  get _isContained() {
54
+ this[ODATA_CONTAINED] && _logDeprecationForODataContained()
39
55
  return (
40
56
  this.own('__isContained') ||
41
57
  this.set(
@@ -120,26 +120,26 @@ const _subWhere = (result, element) => {
120
120
  if (links && links.length > 0) {
121
121
  where = []
122
122
  for (const row of result) {
123
- if (where.length > 0) {
124
- where.push('or')
125
- }
126
123
  const whereObj = links.reduce((res, currentLink) => {
127
- if (Object.prototype.hasOwnProperty.call(row, currentLink.targetKey))
124
+ if (Object.prototype.hasOwnProperty.call(row, currentLink.targetKey) && row[currentLink.targetKey] !== null)
128
125
  res[currentLink.entityKey] = row[currentLink.targetKey]
129
126
  return res
130
127
  }, {})
131
128
  const whereCQN = ctUtils.whereKey(whereObj)
132
- if (whereCQN.length) where.push('(', ...whereCQN, ')')
129
+ if (whereCQN.length) {
130
+ if (where.length > 0) where.push('or')
131
+ where.push('(', ...whereCQN, ')')
132
+ }
133
133
  }
134
134
  }
135
135
  return where
136
136
  }
137
137
 
138
- const _mergeResults = (result, selectData, root, definitions, compositionTree, entityName) => {
138
+ const _mergeResults = (result, selectData, root, model, compositionTree, entityName) => {
139
139
  if (root) {
140
140
  return [...selectData, ...result]
141
141
  } else {
142
- const parent = definitions[compositionTree.target] || definitions[entityName]
142
+ const parent = model.definitions[compositionTree.target] || model.definitions[entityName]
143
143
  const assoc = (parent && parent.elements[compositionTree.name]) || {}
144
144
  return selectData.map(selectEntry => {
145
145
  if (assoc.is2one) {
@@ -148,8 +148,9 @@ const _mergeResults = (result, selectData, root, definitions, compositionTree, e
148
148
  selectEntry[compositionTree.name] = selectEntry[compositionTree.name] || []
149
149
  }
150
150
  const newData = _findWhere(result, _parentKey(compositionTree, selectEntry))
151
- if (assoc.is2one && newData[0]) {
152
- selectEntry[compositionTree.name] = Object.assign(selectEntry[compositionTree.name], newData[0])
151
+ if (assoc.is2one) {
152
+ if (newData[0]) selectEntry[compositionTree.name] = Object.assign(selectEntry[compositionTree.name], newData[0])
153
+ else selectEntry[compositionTree.name] = null
153
154
  } else if (assoc.is2many) {
154
155
  selectEntry[compositionTree.name].push(...newData)
155
156
  }
@@ -158,14 +159,14 @@ const _mergeResults = (result, selectData, root, definitions, compositionTree, e
158
159
  }
159
160
  }
160
161
 
161
- const _columns = (entity, data, compositionTree, selectAll) => {
162
+ const _columns = (entity, data, compositionTree, selectAllColumns) => {
162
163
  const backLinkKeys = _getLinksOfCompTree(compositionTree)
163
164
  const columns = []
164
165
  for (const elementName in entity.elements) {
165
166
  const element = entity.elements[elementName]
166
167
  if (element.virtual || element.isAssociation) continue
167
168
  if (
168
- selectAll ||
169
+ selectAllColumns ||
169
170
  element.key ||
170
171
  backLinkKeys.includes(element.name) ||
171
172
  (Array.isArray(data) && data.find(entry => element.name in entry))
@@ -177,45 +178,42 @@ const _columns = (entity, data, compositionTree, selectAll) => {
177
178
  }
178
179
 
179
180
  const _select = ({
180
- definitions,
181
+ model,
181
182
  entityName,
182
183
  draft,
183
184
  alias,
184
185
  compositionTree,
185
186
  data,
186
- root,
187
- includeAllRootColumns,
188
- includeAllColumns,
187
+ selectAllColumns,
189
188
  where,
190
189
  parentKeys,
191
190
  orderBy,
192
191
  singleton
193
192
  }) => {
194
- const entity = definitions && definitions[entityName]
193
+ const entity = model.definitions[entityName]
195
194
  const from = ctUtils.addDraftSuffix(draft, entity.name)
196
195
  const selectCQN = SELECT.from(from)
197
196
  if (alias) selectCQN.SELECT.from.as = alias
198
- const selectAll = includeAllColumns || (includeAllRootColumns && root)
199
- selectCQN.SELECT.columns = _columns(entity, data, compositionTree, selectAll)
197
+ selectCQN.SELECT.columns = _columns(entity, data, compositionTree, selectAllColumns)
200
198
  if (where) selectCQN.SELECT.where = where
201
199
  else if (parentKeys) selectCQN.SELECT.where = _whereKeys(parentKeys)
202
200
  if (orderBy) selectCQN.SELECT.orderBy = orderBy
203
201
  if (singleton) selectCQN.SELECT.limit = { rows: { val: 1 } }
204
202
  // REVISIT: remove once SELECT builder does flattening!
205
- return cqn2cqn4sql(selectCQN, { definitions })
203
+ return cqn2cqn4sql(selectCQN, model)
206
204
  }
207
205
 
208
206
  const _selectDeepUpdateData = async args => {
209
- const { definitions, compositionTree, entityName, data, includeAllColumns, root, selectData, tx } = args
207
+ const { model, compositionTree, entityName, data, root, selectData, tx, selectAllColumns } = args
210
208
  const selectCQN = _select(args)
211
209
  const result = await tx.run(selectCQN)
212
210
  if (!result.length) return Promise.resolve(result)
213
211
 
214
- const keys = _keys(definitions[entityName], result)
212
+ const keys = _keys(model.definitions[entityName], result)
215
213
  await Promise.all(
216
214
  compositionTree.compositionElements.map(element => {
217
215
  if (element.skipPersistence) return Promise.resolve()
218
- if (data !== undefined && !data.find(entry => element.name in entry) && !(includeAllColumns && result.length))
216
+ if (data !== undefined && !data.find(entry => element.name in entry) && !(selectAllColumns && result.length))
219
217
  return Promise.resolve()
220
218
  const subs = {
221
219
  compositionTree: element,
@@ -235,23 +233,17 @@ const _selectDeepUpdateData = async args => {
235
233
  })
236
234
  )
237
235
 
238
- return _mergeResults(result, selectData || [], root, definitions, compositionTree, entityName)
236
+ return _mergeResults(result, selectData || [], root, model, compositionTree, entityName)
239
237
  }
240
238
 
241
239
  /*
242
240
  * exports
243
241
  */
244
242
 
245
- const selectDeepUpdateData = (
246
- definitions,
247
- cqn,
248
- req,
249
- includeAllRootColumns = false,
250
- includeAllColumns = false,
251
- service
252
- ) => {
243
+ const selectDeepUpdateData = (service, model, req, selectAllColumns = false) => {
244
+ const query = req.query
253
245
  // REVISIT this should be done somewhere before, so it is not done twice for deep updates
254
- const sqlQuery = cqn2cqn4sql(cqn, { definitions })
246
+ const sqlQuery = cqn2cqn4sql(query, model)
255
247
 
256
248
  if (req && _isSameEntity(sqlQuery, req)) {
257
249
  return Promise.resolve(req._.partialPersistentState)
@@ -263,9 +255,9 @@ const selectDeepUpdateData = (
263
255
  const entityName = ensureNoDraftsSuffix(from)
264
256
  const draft = entityName !== from
265
257
  const orderBy = req && req.target && req.target.query && req.target.query.SELECT && req.target.query.SELECT.orderBy
266
- const data = Object.assign({}, sqlQuery.UPDATE.data || {}, cqn.UPDATE.with || {})
258
+ const data = Object.assign({}, sqlQuery.UPDATE.data || {}, query.UPDATE.with || {})
267
259
  const compositionTree = getCompositionTree({
268
- definitions,
260
+ definitions: model.definitions,
269
261
  rootEntityName: entityName, // REVISIT: drafts are resolved too eagerly
270
262
  checkRoot: false,
271
263
  resolveViews: !draft,
@@ -274,17 +266,16 @@ const selectDeepUpdateData = (
274
266
 
275
267
  return _selectDeepUpdateData({
276
268
  tx: cds.tx(req),
277
- definitions,
269
+ model,
278
270
  compositionTree,
279
271
  entityName,
280
272
  data: [data],
281
273
  where,
282
274
  orderBy,
283
275
  draft,
284
- includeAllRootColumns,
285
276
  singleton: req && req.target && req.target._isSingleton,
286
277
  alias,
287
- includeAllColumns: cqn._selectAll || includeAllColumns,
278
+ selectAllColumns,
288
279
  root: true,
289
280
  service
290
281
  })