@sap/cds 5.7.5 → 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 (141) hide show
  1. package/CHANGELOG.md +72 -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/resolve.js +1 -1
  13. package/lib/compile/to/srvinfo.js +1 -1
  14. package/lib/core/classes.js +21 -1
  15. package/lib/env/index.js +3 -2
  16. package/lib/env/requires.js +4 -0
  17. package/lib/i18n/localize.js +5 -8
  18. package/lib/index.js +1 -0
  19. package/lib/log/errors.js +1 -1
  20. package/lib/ql/SELECT.js +2 -2
  21. package/lib/req/cds-context.js +1 -1
  22. package/lib/req/context.js +1 -1
  23. package/lib/serve/Transaction.js +9 -5
  24. package/lib/serve/index.js +13 -21
  25. package/lib/utils/tests.js +90 -66
  26. package/libx/_runtime/audit/generic/personal/modification.js +0 -8
  27. package/libx/_runtime/auth/index.js +7 -6
  28. package/libx/_runtime/auth/strategies/dwc.js +43 -0
  29. package/libx/_runtime/auth/utils.js +24 -0
  30. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -32
  31. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -38
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +1 -2
  38. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/deleteToCQN.js +0 -1
  39. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -2
  40. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +9 -0
  41. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +17 -30
  42. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +12 -1
  43. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +2 -1
  44. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
  45. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +2 -5
  46. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
  47. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
  48. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +1 -1
  52. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  53. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  54. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  55. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -4
  56. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  57. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  58. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +60 -18
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +7 -5
  60. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  61. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  62. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  63. package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
  64. package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
  65. package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
  66. package/libx/_runtime/common/aspects/Association.js +16 -0
  67. package/libx/_runtime/common/composition/data.js +28 -37
  68. package/libx/_runtime/common/composition/delete.js +107 -58
  69. package/libx/_runtime/common/composition/index.js +2 -1
  70. package/libx/_runtime/common/composition/insert.js +13 -13
  71. package/libx/_runtime/common/composition/update.js +39 -34
  72. package/libx/_runtime/common/error/frontend.js +17 -2
  73. package/libx/_runtime/common/generic/auth.js +20 -85
  74. package/libx/_runtime/common/generic/crud.js +22 -1
  75. package/libx/_runtime/common/i18n/messages.properties +2 -1
  76. package/libx/_runtime/common/utils/cqn.js +2 -6
  77. package/libx/_runtime/common/utils/cqn2cqn4sql.js +95 -122
  78. package/libx/_runtime/common/utils/csn.js +14 -3
  79. package/libx/_runtime/common/utils/foreignKeyPropagations.js +18 -1
  80. package/libx/_runtime/common/utils/keys.js +2 -1
  81. package/libx/_runtime/common/utils/path.js +1 -1
  82. package/libx/_runtime/common/utils/resolveView.js +12 -4
  83. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  84. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  85. package/libx/_runtime/common/utils/vcap.js +27 -10
  86. package/libx/_runtime/db/data-conversion/post-processing.js +20 -13
  87. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  88. package/libx/_runtime/db/expand/expandCQNToJoin.js +8 -6
  89. package/libx/_runtime/db/expand/index.js +3 -0
  90. package/libx/_runtime/db/generic/create.js +0 -10
  91. package/libx/_runtime/db/generic/index.js +3 -0
  92. package/libx/_runtime/db/generic/read.js +2 -24
  93. package/libx/_runtime/db/generic/rewrite.js +1 -3
  94. package/libx/_runtime/db/generic/update.js +1 -1
  95. package/libx/_runtime/db/query/delete.js +10 -4
  96. package/libx/_runtime/db/query/insert.js +3 -3
  97. package/libx/_runtime/db/query/read.js +4 -1
  98. package/libx/_runtime/db/query/update.js +5 -5
  99. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  100. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  101. package/libx/_runtime/db/sql-builder/index.js +3 -0
  102. package/libx/_runtime/db/utils/columns.js +5 -2
  103. package/libx/_runtime/db/utils/deep.js +6 -8
  104. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  105. package/libx/_runtime/fiori/generic/before.js +73 -49
  106. package/libx/_runtime/fiori/generic/edit.js +14 -18
  107. package/libx/_runtime/fiori/generic/patch.js +8 -11
  108. package/libx/_runtime/fiori/generic/read.js +19 -16
  109. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  110. package/libx/_runtime/hana/Service.js +1 -1
  111. package/libx/_runtime/hana/conversion.js +10 -0
  112. package/libx/_runtime/hana/execute.js +33 -16
  113. package/libx/_runtime/hana/search.js +3 -3
  114. package/libx/_runtime/hana/search2cqn4sql.js +22 -21
  115. package/libx/_runtime/hana/searchToContains.js +1 -1
  116. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  117. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  118. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  119. package/libx/_runtime/messaging/file-based.js +3 -1
  120. package/libx/_runtime/messaging/service.js +4 -1
  121. package/libx/_runtime/remote/utils/client.js +33 -20
  122. package/libx/_runtime/remote/utils/data.js +52 -11
  123. package/libx/_runtime/sqlite/Service.js +1 -1
  124. package/libx/_runtime/sqlite/conversion.js +10 -0
  125. package/libx/_runtime/types/api.js +2 -2
  126. package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
  127. package/libx/odata/afterburner.js +29 -6
  128. package/libx/odata/cqn2odata.js +9 -0
  129. package/libx/odata/grammar.pegjs +49 -21
  130. package/libx/odata/index.js +2 -2
  131. package/libx/odata/parser.js +1 -1
  132. package/libx/odata/utils.js +2 -2
  133. package/libx/rest/RestAdapter.js +29 -1
  134. package/libx/rest/middleware/auth.js +1 -3
  135. package/libx/rest/middleware/parse.js +1 -0
  136. package/package.json +1 -1
  137. package/server.js +1 -1
  138. package/bin/deploy/to-hana/logger.js +0 -27
  139. package/bin/deploy/to-hana/runCommand.js +0 -113
  140. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
  141. package/libx/_runtime/common/utils/auth.js +0 -16
@@ -67,6 +67,16 @@ const MULTI_LINE_STRING_VALIDATION = new RegExp('^' + SRID + 'MultiLineString\\(
67
67
  const MULTI_POLYGON_VALIDATION = new RegExp('^' + SRID + 'MultiPolygon\\((' + MULTI_POLYGON + ')\\)$', 'i')
68
68
  const COLLECTION_VALIDATION = new RegExp('^' + SRID + 'Collection\\((' + MULTI_GEO_LITERAL + ')\\)$', 'i')
69
69
 
70
+ const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
71
+
72
+ function _getBase64(val) {
73
+ // convert url-safe to standard base64 (with padding, if necessary)
74
+ val = val.replace(/_/g, '/').replace(/-/g, '+')
75
+ val = val.padEnd(val.length + val.length % 4, '=')
76
+ if (!val.match(BASE64)) return
77
+ return val
78
+ }
79
+
70
80
  /**
71
81
  * The primitive-value decoder decodes primitive values, using OData V4 primitive types.
72
82
  * The following mapping of V2 and V4 primitive types is assumed:
@@ -148,7 +158,9 @@ class PrimitiveValueDecoder {
148
158
  const type = propertyOrReturnType.getType()
149
159
 
150
160
  if (type === EdmPrimitiveTypeKind.Binary) {
151
- return this._decodeBinary(jsonValue, propertyOrReturnType.getMaxLength())
161
+ const val = _getBase64(jsonValue)
162
+ if (!val) throw new IllegalArgumentError('The value for Edm.Binary is not valid base64 content.')
163
+ return val
152
164
  }
153
165
 
154
166
  let value = jsonValue
@@ -284,12 +296,9 @@ class PrimitiveValueDecoder {
284
296
  }
285
297
 
286
298
  if (type === EdmPrimitiveTypeKind.Binary) {
287
- const maxLength =
288
- propertyOrReturnType.getMaxLength() ||
289
- (propertyOrReturnType.getType().getKind() === EdmTypeKind.DEFINITION &&
290
- propertyOrReturnType.getType().getMaxLength()) ||
291
- null
292
- return this._decodeBinary(value, maxLength)
299
+ const val = _getBase64(value)
300
+ if (!val) throw new IllegalArgumentError('The value for Edm.Binary is not valid base64 content.')
301
+ return val
293
302
  }
294
303
 
295
304
  if (type === EdmPrimitiveTypeKind.Int64 || type === EdmPrimitiveTypeKind.Decimal) {
@@ -362,14 +371,9 @@ class PrimitiveValueDecoder {
362
371
  if (type.getKind() === EdmTypeKind.DEFINITION) type = type.getUnderlyingType()
363
372
 
364
373
  if (type === EdmPrimitiveTypeKind.Binary) {
365
- const valueBuffer = Buffer.from(valueString)
366
- const maxLength =
367
- propertyOrReturnType.getMaxLength() ||
368
- (propertyOrReturnType.getType().getKind() === EdmTypeKind.DEFINITION &&
369
- propertyOrReturnType.getType().getMaxLength()) ||
370
- null
371
- this._validator.validateBinary(valueBuffer, maxLength)
372
- return valueBuffer
374
+ const val = _getBase64(valueString)
375
+ if (!val) throw new IllegalArgumentError('The value for Edm.Binary is not valid base64 content.')
376
+ return val
373
377
  }
374
378
 
375
379
  let decoded
@@ -661,38 +665,6 @@ class PrimitiveValueDecoder {
661
665
  )
662
666
  }
663
667
  }
664
-
665
- /**
666
- * Decode an OData JSON representation of a binary value into its JavaScript value.
667
- * @param {string} value the JSON value
668
- * @param {?(number|string)} maxLength the value of the Maxlength facet
669
- * @returns {Buffer} the JavaScript value
670
- * @private
671
- */
672
- _decodeBinary (value, maxLength) {
673
- const valueBuffer = Buffer.from(value, 'base64')
674
- // The method Buffer.from(...) does not throw an error on invalid input;
675
- // it simply returns the result of the conversion of the content up to the first error.
676
- // So we check if the length is correct, taking padding characters into account (see RFC 4648).
677
- // Newline or other whitespace characters are not allowed according to the OData JSON format specification.
678
- let length = (value.length * 3) / 4 // Four base64 characters result in three octets.
679
- if (value.length % 4) {
680
- // The length is not a multiple of four as it should be.
681
- length =
682
- 3 * Math.floor(value.length / 4) +
683
- // The remainder (due to missing padding characters) will result in one or two octets.
684
- Math.ceil((value.length % 4) / 2)
685
- } else {
686
- // Padding characters reduce the amount of expected octets.
687
- if (value.endsWith('==')) length--
688
- if (value.endsWith('=')) length--
689
- }
690
- if (valueBuffer.length < length) {
691
- throw new IllegalArgumentError('The value for Edm.Binary is not valid base64 content.')
692
- }
693
- this._validator.validateBinary(valueBuffer, maxLength)
694
- return valueBuffer
695
- }
696
668
  }
697
669
 
698
670
  module.exports = PrimitiveValueDecoder
@@ -186,19 +186,12 @@ class ValueConverter {
186
186
 
187
187
  /**
188
188
  * Converts value to the value of Edm.Binary type.
189
- * @param {Buffer} value - value, which should be converted
190
- * @param {number} [maxLength] - value of MaxLength facet
189
+ * @param {Buffer|string} value - value, which should be converted
191
190
  * @returns {string} Base64 string
192
191
  */
193
- convertBinary (value, maxLength) {
194
- this._valueValidator.validateBinary(value, maxLength)
195
- return (
196
- value
197
- .toString('base64')
198
- // Convert the standard base64 encoding to the URL-safe variant.
199
- .replace(PLUS_REGEXP, '-')
200
- .replace(SLASH_REGEXP, '_')
201
- )
192
+ convertBinary (value) {
193
+ // convert the standard base64 encoding to the URL-safe variant
194
+ return (Buffer.isBuffer(value) ? value.toString('base64') : value).replace(/\+/g, '-').replace(/\//g, '_')
202
195
  }
203
196
 
204
197
  /**
@@ -43,11 +43,13 @@ class ResourceJsonDeserializer {
43
43
  * @throws {DeserializationError} if provided data can not be deserialized
44
44
  */
45
45
  deserializeEntity (edmType, value, expand, additionalInformation) {
46
+ let data
46
47
  try {
47
- let data = JSON.parse(value)
48
+ data = JSON.parse(value)
48
49
  this._deserializeStructuralType(edmType, data, null, false, expand, additionalInformation)
49
50
  return data
50
51
  } catch (e) {
52
+ e._data = data
51
53
  if (e instanceof DeserializationError) throw e
52
54
  throw new DeserializationError('An error occurred during deserialization of the entity.', e)
53
55
  }
@@ -82,6 +84,7 @@ class ResourceJsonDeserializer {
82
84
  }
83
85
  return tempData
84
86
  } catch (e) {
87
+ e._data = tempData
85
88
  if (e instanceof DeserializationError) throw e
86
89
  throw new DeserializationError('An error occurred during deserialization of the collection.', e)
87
90
  }
@@ -129,6 +132,7 @@ class ResourceJsonDeserializer {
129
132
  try {
130
133
  return this._deserializePrimitive(edmProperty, tempData)
131
134
  } catch (e) {
135
+ e._data = tempData
132
136
  if (e instanceof DeserializationError) throw e
133
137
  throw new DeserializationError('An error occurred during deserialization of the property.', e)
134
138
  }
@@ -155,6 +159,7 @@ class ResourceJsonDeserializer {
155
159
  try {
156
160
  return this._deserializePrimitive(edmProperty, tempData)
157
161
  } catch (e) {
162
+ e._data = tempData
158
163
  if (e instanceof DeserializationError) throw e
159
164
  throw new DeserializationError('An error occurred during deserialization of the property.', e)
160
165
  }
@@ -174,6 +179,7 @@ class ResourceJsonDeserializer {
174
179
  try {
175
180
  return this._deserializeReference(edmType, tempData)
176
181
  } catch (e) {
182
+ e._data = tempData
177
183
  if (e instanceof DeserializationError) throw e
178
184
  throw new DeserializationError('An error occurred during deserialization of the reference.', e)
179
185
  }
@@ -82,9 +82,6 @@ class CommandExecutor {
82
82
  }
83
83
  }, this._error)
84
84
  } catch (innerError) {
85
- if (innerError.__crashOnError) {
86
- throw innerError
87
- }
88
85
  if (this._runTimeMeasurement && description) this._runTimeMeasurement.getChild(description).stop()
89
86
  callback(innerError)
90
87
  }
@@ -4,7 +4,6 @@ const commons = require('../../odata-commons')
4
4
  const StatusCodes = commons.http.HttpStatusCode.StatusCodes
5
5
  const RepresentationKinds = commons.format.RepresentationKind.Kinds
6
6
  const Command = require('./Command')
7
- const InternalServerError = require('../errors/InternalServerError')
8
7
 
9
8
  /**
10
9
  * The `next` callback to be called upon finish execution.
@@ -352,7 +352,7 @@ class ContextURLFactory {
352
352
  // Define a local function because navigation properties must be searched recursively.
353
353
  const getExpandPaths = structuredType => {
354
354
  let structureResult = []
355
- for (const name of structuredType.getNavigationProperties().keys()) structureResult.push(name)
355
+ for (const name of structuredType.getNavigationProperties(true).keys()) structureResult.push(name)
356
356
  for (const [name, property] of structuredType.getProperties()) {
357
357
  if (property.getType().getKind() === EdmTypeKind.COMPLEX) {
358
358
  for (const innerPath of getExpandPaths(property.getType())) {
@@ -472,11 +472,8 @@ class TrustedResourceJsonSerializer {
472
472
  if (type === EdmPrimitiveTypeKind.Decimal || type === EdmPrimitiveTypeKind.Int64) {
473
473
  result = this._formatParams.getIEEE754Setting() ? String(propertyValue) : Number(propertyValue)
474
474
  } else if (type === EdmPrimitiveTypeKind.Binary) {
475
- result = propertyValue
476
- .toString('base64')
477
- // Convert the standard base64 encoding to the URL-safe variant.
478
- .replace(new RegExp('\\+', 'g'), '-')
479
- .replace(new RegExp('/', 'g'), '_')
475
+ // convert the standard base64 encoding to the URL-safe variant
476
+ result = (Buffer.isBuffer(propertyValue) ? propertyValue.toString('base64') : propertyValue).replace(/\+/g, '-').replace(/\//g, '_')
480
477
  }
481
478
  break
482
479
  case EdmTypeKind.COMPLEX:
@@ -5,7 +5,6 @@ const HttpMethods = commons.http.HttpMethod.Methods
5
5
  const ResourceKinds = commons.uri.UriResource.ResourceKind
6
6
  const PreconditionFailedError = require('../errors/PreconditionFailedError')
7
7
  const PreconditionRequiredError = require('../errors/PreconditionRequiredError')
8
- const ConflictError = require('../errors/ConflictError')
9
8
 
10
9
  /**
11
10
  * Class to validate conditional requests.
@@ -23,11 +22,12 @@ class ConditionalRequestValidator {
23
22
  preValidate (ifMatch, ifNoneMatch, method, isConcurrentResource) {
24
23
  if (isConcurrentResource) {
25
24
  if (method !== HttpMethods.GET && !ifMatch && !ifNoneMatch) throw new PreconditionRequiredError()
26
- } else if (ifMatch || ifNoneMatch) {
27
- // PATCH and PUT can carry If-Match or If-None-Match headers to force update or insert (upsert feature).
28
- // The only allowed value in these cases is '*'. Careless clients send this also for DELETE and POST,
29
- // other careless clients send the star in doublequotes.
30
- if ([HttpMethods.PATCH, HttpMethods.PUT, HttpMethods.DELETE, HttpMethods.POST].includes(method)) {
25
+ return
26
+ }
27
+
28
+ if (ifMatch || ifNoneMatch) {
29
+ // Careless clients send this also for DELETE and POST, other careless clients send the star in double-quotes.
30
+ if ([HttpMethods.POST].includes(method)) {
31
31
  if (
32
32
  (ifMatch && ifMatch.trim() !== '*' && ifMatch.trim() !== '"*"') ||
33
33
  (ifNoneMatch && ifNoneMatch.trim() !== '*' && ifNoneMatch.trim() !== '"*"')
@@ -6,7 +6,7 @@ const Dispatcher = require('./Dispatcher')
6
6
  const { alias2ref } = require('../../../common/utils/csn')
7
7
 
8
8
  const to = service => {
9
- const edm = cds.compile.to.edm(service.model, { service: service.definition.name })
9
+ const edm = service._edm || cds.compile.to.edm(service.model, { service: service.definition.name })
10
10
  alias2ref(service, edm)
11
11
 
12
12
  const odata = new OData(edm, service.model, service.options)
@@ -65,7 +65,6 @@ const _getParamData = parameters => {
65
65
 
66
66
  return paramData
67
67
  }
68
-
69
68
  const _flattenStructureKeys = structureData => {
70
69
  const result = {}
71
70
  for (const prop in structureData) {
@@ -80,7 +79,6 @@ const _flattenStructureKeys = structureData => {
80
79
  }
81
80
  return result
82
81
  }
83
-
84
82
  // works only for custom on condition working on keys with '=' operator
85
83
  // and combination of multiple conditions connected with 'and'
86
84
  const _addKeysToData = (navSourceKeyValues, onCondition, data) => {
@@ -167,8 +165,7 @@ const _getCopiedData = (odataReq, streaming, lastSegment) => {
167
165
  return data
168
166
  }
169
167
 
170
- data = Array.isArray(data) ? deepCopyArray(data) : deepCopyObject(data)
171
- return data
168
+ return Array.isArray(data) ? deepCopyArray(data) : deepCopyObject(data)
172
169
  }
173
170
 
174
171
  /**
@@ -4,6 +4,8 @@ const { isCustomOperation } = require('./request')
4
4
  const expandToCQN = require('../odata-to-cqn/expandToCQN')
5
5
  const QueryOptions = require('../okra/odata-server').QueryOptions
6
6
  const { COMPLEX_PROPERTY, PRIMITIVE_PROPERTY } = require('../okra/odata-server').uri.UriResource.ResourceKind
7
+ const { mergeJson } = require('../../../services/utils/compareJson')
8
+ const { getColumns } = require('../../../services/utils/columns')
7
9
 
8
10
  const _selectForFunction = (selectColumns, result, opReturnType) => {
9
11
  if (!Array.isArray(result)) result = [result]
@@ -33,31 +35,51 @@ const _expand = (model, uriInfo, options) => {
33
35
  return expandToCQN(model, expand, uriInfo.getFinalEdmType(), options)
34
36
  }
35
37
 
38
+ const _compareKeys = (first, second) => key => {
39
+ const val1 = first[key]
40
+ const val2 = second[key]
41
+ if (Array.isArray(val1) || Buffer.isBuffer(val1)) return val1.every((_, i) => _compareKeys(val1, val2)(i))
42
+ if (val1 && typeof val1 === 'object') return Object.keys(val1).every(_compareKeys(val1, val2))
43
+ return val1 === val2
44
+ }
45
+
36
46
  const _expandForFunction = async (uriInfo, result, req, srv, opReturnType) => {
37
47
  const results = Array.isArray(result) ? result : [result]
38
-
39
- const opReturnTypeName = typeof opReturnType === 'string' ? opReturnType : opReturnType.name
40
- const isDraft = srv.model.definitions[opReturnTypeName] && srv.model.definitions[opReturnTypeName]._isDraftEnabled
41
-
48
+ const isDraft = opReturnType._isDraftEnabled
42
49
  const isDraftActivate = isDraftActivateAction(req)
43
50
 
44
51
  // REVISIT: what happens here exactly?
52
+ const selectQuery = SELECT.from(
53
+ isDraft && !isDraftActivate ? ensureDraftsSuffix(opReturnType.name) : opReturnType.name
54
+ )
55
+ const keys = getColumns(opReturnType, {
56
+ onlyNames: true,
57
+ removeIgnore: true,
58
+ filterDraft: !isDraft || isDraftActivate,
59
+ filterVirtual: true,
60
+ keysOnly: true
61
+ })
62
+ const expandCqn = _expand(srv.model, uriInfo, { rewriteAsterisks: true })
63
+ selectQuery.columns(expandCqn)
64
+ selectQuery.columns(keys)
45
65
  for (const row of results) {
46
- const selectQuery = SELECT.from(isDraft && !isDraftActivate ? ensureDraftsSuffix(opReturnType.name) : opReturnType)
47
-
48
- for (const key in opReturnType.keys) {
49
- if ((!isDraft || isDraftActivate) && key === 'IsActiveEntity') {
50
- continue
51
- }
52
- selectQuery.where(key, '=', row[key])
66
+ const where = ['(']
67
+ for (const key of keys) {
68
+ where.push({ ref: [key] }, '=', { val: row[key] }, 'and')
53
69
  }
70
+ where.pop() // last 'and'
71
+ where.push(')')
72
+ if (!selectQuery.SELECT.where) selectQuery.where(where)
73
+ else selectQuery.or(where)
74
+ }
75
+ const expandedResults = await cds.tx(req).run(selectQuery)
54
76
 
55
- const expandCqn = _expand(srv.model, uriInfo, { rewriteAsterisks: true })
56
- selectQuery.columns(expandCqn)
57
-
58
- const res = await cds.tx(req).run(selectQuery)
59
- if (res) Object.assign(row, res[0])
77
+ for (let i = 0; i < results.length; i++) {
78
+ const result = results[i]
79
+ const res = expandedResults.find(r => keys.every(_compareKeys(result, r)))
80
+ if (res) results[i] = mergeJson(res, result, opReturnType)
60
81
  }
82
+ return Array.isArray(result) ? results : results[0]
61
83
  }
62
84
 
63
85
  const _cleanupResult = (result, opReturnType) => {
@@ -89,14 +111,16 @@ const getActionOrFunctionReturnType = (pathSegments, definitions) => {
89
111
  const actionAndFunctionQueries = async (req, odataReq, result, srv, opReturnType) => {
90
112
  _cleanupResult(result, opReturnType)
91
113
 
114
+ // REVISIT consider $expand columns as inline content for $select
92
115
  if (odataReq.getQueryOptions().$select) {
93
116
  _selectForFunction(odataReq.getQueryOptions().$select.split(','), result, opReturnType)
94
117
  }
95
118
 
96
119
  // REVISIT: we need to read directly from db for this, which might not be there!
97
120
  if (odataReq.getQueryOptions().$expand && cds.db) {
98
- await _expandForFunction(odataReq.getUriInfo(), result, req, srv, opReturnType)
121
+ result = await _expandForFunction(odataReq.getUriInfo(), result, req, srv, opReturnType)
99
122
  }
123
+ return result
100
124
  }
101
125
 
102
126
  const resolveStructuredName = (pathSegments, index, nameArr = []) => {
@@ -1,5 +1,4 @@
1
1
  const {
2
- QueryOptions,
3
2
  uri: {
4
3
  UriResource: {
5
4
  ResourceKind: { ENTITY, ENTITY_COLLECTION }
@@ -60,22 +59,7 @@ const validateResourcePath = (odataReq, service) => {
60
59
  }
61
60
  }
62
61
 
63
- /**
64
- * Used for pagination, where the start of the collection is defined via skip token.
65
- *
66
- * @param {object} uriInfo
67
- * @returns {number}
68
- * @private
69
- */
70
- const skipToken = uriInfo => {
71
- const token = uriInfo.getQueryOption(QueryOptions.SKIPTOKEN)
72
-
73
- // If given, the token is a string but needed as numeric value.
74
- return token ? parseInt(token) : 0
75
- }
76
-
77
62
  module.exports = {
78
63
  isCustomOperation,
79
- validateResourcePath,
80
- skipToken
64
+ validateResourcePath
81
65
  }
@@ -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]
@@ -208,12 +246,16 @@ const _pick = options => (element, target, parent) => {
208
246
  const categories = []
209
247
 
210
248
  if (element['@odata.etag']) categories.push('@odata.etag')
249
+
211
250
  if (element.type === 'cds.Decimal') categories.push('@cds.Decimal')
251
+
212
252
  categories.push(..._assocs(element, target))
253
+
213
254
  if (options.omitValuesPreference) categories.push('@odata.omitValues')
214
255
  if (options.event === 'draftActivate' && options.locale && options.locale !== 'en' && element.localized === true) {
215
256
  categories.push('localizeAfterDraftActivate')
216
257
  }
258
+
217
259
  if (categories.length) return { categories }
218
260
  }
219
261
 
@@ -247,7 +289,7 @@ const _getOptions = ({ headers, locale, event }) => {
247
289
  const _generateCacheKey = (headers, options) => {
248
290
  let key = 'postProcess' // default template cache key for post processing
249
291
  if (headers.prefer) key += `:${headers.prefer}`
250
- if (options.decimals) key += `:exponentialDecimals=true`
292
+ if (options.decimals) key += `:ExponentialDecimals=true`
251
293
  if (options.event === 'draftActivate' && options.locale) key += `:locale=${options.locale}`
252
294
  return key
253
295
  }
@@ -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
  },