@sap/cds 5.7.3 → 5.8.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 (151) hide show
  1. package/CHANGELOG.md +111 -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 -38
  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 +13 -7
  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 +1 -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 +17 -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 +2 -2
  54. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ErrorJsonSerializer.js +2 -0
  55. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  56. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  57. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  58. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +18 -5
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  60. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  61. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +80 -21
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +47 -10
  63. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  64. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  65. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  66. package/libx/_runtime/cds-services/services/Service.js +1 -1
  67. package/libx/_runtime/cds-services/services/utils/columns.js +5 -1
  68. package/libx/_runtime/cds-services/services/utils/compareJson.js +15 -16
  69. package/libx/_runtime/cds-services/services/utils/differ.js +2 -8
  70. package/libx/_runtime/common/aspects/Association.js +16 -0
  71. package/libx/_runtime/common/composition/data.js +28 -37
  72. package/libx/_runtime/common/composition/delete.js +107 -58
  73. package/libx/_runtime/common/composition/index.js +3 -3
  74. package/libx/_runtime/common/composition/insert.js +14 -27
  75. package/libx/_runtime/common/composition/update.js +39 -34
  76. package/libx/_runtime/common/error/frontend.js +19 -5
  77. package/libx/_runtime/common/generic/auth.js +20 -85
  78. package/libx/_runtime/common/generic/crud.js +22 -1
  79. package/libx/_runtime/common/i18n/messages.properties +2 -1
  80. package/libx/_runtime/common/utils/cqn.js +2 -6
  81. package/libx/_runtime/common/utils/cqn2cqn4sql.js +95 -122
  82. package/libx/_runtime/common/utils/csn.js +15 -4
  83. package/libx/_runtime/common/utils/foreignKeyPropagations.js +18 -1
  84. package/libx/_runtime/common/utils/keys.js +2 -1
  85. package/libx/_runtime/common/utils/path.js +1 -1
  86. package/libx/_runtime/common/utils/resolveView.js +12 -4
  87. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  88. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  89. package/libx/_runtime/common/utils/structured.js +11 -5
  90. package/libx/_runtime/common/utils/vcap.js +27 -10
  91. package/libx/_runtime/db/data-conversion/post-processing.js +42 -35
  92. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  93. package/libx/_runtime/db/expand/expandCQNToJoin.js +57 -29
  94. package/libx/_runtime/db/expand/index.js +3 -0
  95. package/libx/_runtime/db/generic/create.js +0 -10
  96. package/libx/_runtime/db/generic/index.js +3 -0
  97. package/libx/_runtime/db/generic/read.js +2 -24
  98. package/libx/_runtime/db/generic/rewrite.js +1 -3
  99. package/libx/_runtime/db/generic/update.js +1 -1
  100. package/libx/_runtime/db/query/delete.js +10 -4
  101. package/libx/_runtime/db/query/insert.js +3 -4
  102. package/libx/_runtime/db/query/read.js +4 -1
  103. package/libx/_runtime/db/query/update.js +5 -5
  104. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  105. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  106. package/libx/_runtime/db/sql-builder/SelectBuilder.js +7 -3
  107. package/libx/_runtime/db/sql-builder/index.js +3 -0
  108. package/libx/_runtime/db/utils/columns.js +5 -2
  109. package/libx/_runtime/db/utils/deep.js +6 -8
  110. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  111. package/libx/_runtime/fiori/generic/before.js +73 -49
  112. package/libx/_runtime/fiori/generic/edit.js +14 -18
  113. package/libx/_runtime/fiori/generic/patch.js +8 -11
  114. package/libx/_runtime/fiori/generic/read.js +22 -20
  115. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  116. package/libx/_runtime/fiori/utils/handler.js +1 -11
  117. package/libx/_runtime/hana/Service.js +1 -1
  118. package/libx/_runtime/hana/conversion.js +12 -1
  119. package/libx/_runtime/hana/execute.js +31 -16
  120. package/libx/_runtime/hana/localized.js +1 -1
  121. package/libx/_runtime/hana/search.js +3 -3
  122. package/libx/_runtime/hana/search2cqn4sql.js +23 -25
  123. package/libx/_runtime/hana/searchToContains.js +1 -1
  124. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +4 -2
  125. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  126. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  127. package/libx/_runtime/messaging/file-based.js +3 -1
  128. package/libx/_runtime/messaging/service.js +16 -7
  129. package/libx/_runtime/remote/utils/client.js +37 -20
  130. package/libx/_runtime/remote/utils/data.js +53 -12
  131. package/libx/_runtime/sqlite/Service.js +1 -1
  132. package/libx/_runtime/sqlite/conversion.js +10 -0
  133. package/libx/_runtime/sqlite/localized.js +1 -1
  134. package/libx/_runtime/types/api.js +2 -2
  135. package/libx/gql/resolvers/crud/update.js +8 -5
  136. package/libx/gql/resolvers/parse/ast/enrich.js +1 -0
  137. package/libx/odata/afterburner.js +29 -6
  138. package/libx/odata/cqn2odata.js +9 -0
  139. package/libx/odata/grammar.pegjs +50 -22
  140. package/libx/odata/index.js +2 -2
  141. package/libx/odata/parser.js +1 -1
  142. package/libx/odata/utils.js +2 -2
  143. package/libx/rest/RestAdapter.js +29 -1
  144. package/libx/rest/middleware/auth.js +1 -3
  145. package/libx/rest/middleware/parse.js +1 -0
  146. package/package.json +1 -1
  147. package/server.js +1 -1
  148. package/bin/deploy/to-hana/logger.js +0 -27
  149. package/bin/deploy/to-hana/runCommand.js +0 -113
  150. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/selectHelper.js +0 -37
  151. package/libx/_runtime/common/utils/auth.js +0 -16
@@ -5,7 +5,7 @@ const SANITIZE_VALUES = process.env.NODE_ENV === 'production' && cds.env.log.san
5
5
 
6
6
  const cdsLocale = require('../../../../lib/req/locale')
7
7
 
8
- const { convertV2ResponseData, deepSanitize } = require('./data')
8
+ const { convertV2ResponseData, deepSanitize, convertV2PayloadData } = require('./data')
9
9
 
10
10
  let _cloudSdkCore
11
11
 
@@ -30,6 +30,7 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
30
30
  const destinationName = typeof destination === 'string' && destination
31
31
  if (destinationName) {
32
32
  destination = await getDestination(destinationName, resolveDestinationOptions(destinationOptions, jwt))
33
+ if (!destination) throw new Error(`Cannot resolve destination "${destinationName}"`)
33
34
  } else if (destination.forwardAuthToken) {
34
35
  destination = {
35
36
  ...destination,
@@ -166,10 +167,10 @@ const _purgeODataV2 = (data, target, reqHeaders) => {
166
167
  if (typeof data !== 'object' || !data.d) return data
167
168
 
168
169
  data = data.d
169
- const contentType = reqHeaders['content-type']
170
- const ieee754Compatible = contentType && contentType.includes('IEEE754Compatible=true')
170
+ const ieee754Compatible = reqHeaders.accept && reqHeaders.accept.includes('IEEE754Compatible=true')
171
+ const exponentialDecimals = ieee754Compatible && reqHeaders.accept.includes('ExponentialDecimals=true')
171
172
  const purgedResponse = data.results || data
172
- const convertedResponse = convertV2ResponseData(purgedResponse, target, ieee754Compatible)
173
+ const convertedResponse = convertV2ResponseData(purgedResponse, target, ieee754Compatible, exponentialDecimals)
173
174
  return _normalizeMetadata(/^__/, data, convertedResponse)
174
175
  }
175
176
 
@@ -245,6 +246,7 @@ const run = async (
245
246
 
246
247
  LOG._warn && LOG.warn(sanitizedError)
247
248
 
249
+ // REVISIT: switch from innererror to reason in cds^6
248
250
  throw Object.assign(new Error(e.message), { statusCode: 502, innererror: sanitizedError })
249
251
  }
250
252
 
@@ -266,6 +268,7 @@ const run = async (
266
268
 
267
269
  LOG._warn && LOG.warn(sanitizedError)
268
270
 
271
+ // REVISIT: switch from innererror to reason in cds^6
269
272
  throw Object.assign(new Error(`Error during request to remote service: ${e.message}`), {
270
273
  statusCode: 502,
271
274
  innererror: sanitizedError
@@ -293,6 +296,8 @@ const run = async (
293
296
  : 'Request to remote service failed.'
294
297
  const sanitizedError = _getSanitizedError(contentJSON, requestConfig)
295
298
  LOG._warn && LOG.warn(sanitizedError)
299
+
300
+ // REVISIT: switch from innererror to reason in cds^6
296
301
  throw Object.assign(new Error(contentJSON.message), { statusCode: 502, innererror: sanitizedError })
297
302
  }
298
303
  }
@@ -321,32 +326,37 @@ const getJwt = req => {
321
326
  return null
322
327
  }
323
328
 
324
- const _cqnToReqOptions = (query, kind, model) => {
329
+ const _cqnToReqOptions = (query, kind, model, target) => {
325
330
  const queryObject = cds.odata.urlify(query, { kind, model })
326
- return {
331
+ const reqOptions = {
327
332
  method: queryObject.method,
328
333
  url: encodeURI(
329
334
  queryObject.path
330
335
  // ugly workaround for Okra not allowing spaces in ( x eq 1 )
331
336
  .replace(/\( /g, '(')
332
337
  .replace(/ \)/g, ')')
333
- ),
334
- data: queryObject.body
338
+ )
335
339
  }
340
+ if (queryObject.method !== 'GET' && queryObject.method !== 'HEAD') {
341
+ reqOptions.data = kind === 'odata-v2' ? convertV2PayloadData(queryObject.body, target) : queryObject.body
342
+ }
343
+ return reqOptions
336
344
  }
337
345
 
338
- const _stringToReqOptions = (query, data) => {
346
+ const _stringToReqOptions = (query, data, target) => {
339
347
  const cleanQuery = query.trim()
340
348
  const blankIndex = cleanQuery.substring(0, 8).indexOf(' ')
341
349
  const reqOptions = {
342
350
  method: cleanQuery.substring(0, blankIndex).toUpperCase(),
343
351
  url: encodeURI(formatPath(cleanQuery.substring(blankIndex, cleanQuery.length).trim()))
344
352
  }
345
- if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') reqOptions.data = data
353
+ if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
354
+ reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
355
+ }
346
356
  return reqOptions
347
357
  }
348
358
 
349
- const _pathToReqOptions = (method, path, data) => {
359
+ const _pathToReqOptions = (method, path, data, target) => {
350
360
  let url = path
351
361
  if (!url.startsWith('/')) {
352
362
  // extract entity name and instance identifier (either in "()" or after "/") from fully qualified path
@@ -358,7 +368,9 @@ const _pathToReqOptions = (method, path, data) => {
358
368
  url = url.replace(/^\/\//, '/')
359
369
  }
360
370
  const reqOptions = { method, url }
361
- if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') reqOptions.data = data
371
+ if (data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
372
+ reqOptions.data = this.kind === 'odata-v2' ? Object.assign({}, convertV2PayloadData(data, target)) : data
373
+ }
362
374
  return reqOptions
363
375
  }
364
376
 
@@ -367,13 +379,14 @@ const _hasHeader = (headers, header) =>
367
379
  .map(k => k.toLowerCase())
368
380
  .includes(header)
369
381
 
382
+ // eslint-disable-next-line complexity
370
383
  const getReqOptions = (req, query, service) => {
371
384
  const reqOptions =
372
385
  typeof query === 'object'
373
- ? _cqnToReqOptions(query, service.kind, service.model)
386
+ ? _cqnToReqOptions(query, service.kind, service.model, req.target)
374
387
  : typeof query === 'string'
375
- ? _stringToReqOptions(query, req.data)
376
- : _pathToReqOptions(req.method, req.path, req.data)
388
+ ? _stringToReqOptions(query, req.data, req.target)
389
+ : _pathToReqOptions(req.method, req.path, req.data, req.target)
377
390
 
378
391
  reqOptions.headers = { accept: 'application/json,text/plain' }
379
392
  reqOptions.timeout = service.requestTimeout
@@ -387,6 +400,12 @@ const getReqOptions = (req, query, service) => {
387
400
  if (locale) reqOptions.headers['accept-language'] = locale
388
401
  }
389
402
 
403
+ // forward all dwc-* headers
404
+ if (service.options.forward_dwc_headers) {
405
+ const originalHeaders = (req.context && req.context._ && req.context._.req && req.context._.req.headers) || {}
406
+ for (const k in originalHeaders) if (k.match(/^dwc-/)) reqOptions.headers[k] = originalHeaders[k]
407
+ }
408
+
390
409
  if (reqOptions.data && reqOptions.method !== 'GET' && reqOptions.method !== 'HEAD') {
391
410
  reqOptions.headers['content-type'] = 'application/json'
392
411
  reqOptions.headers['content-length'] = Buffer.byteLength(JSON.stringify(reqOptions.data))
@@ -394,11 +413,9 @@ const getReqOptions = (req, query, service) => {
394
413
  reqOptions.url = formatPath(reqOptions.url)
395
414
 
396
415
  // batch envelope if needed
397
- if (
398
- KINDS_SUPPORTING_BATCH[service.kind] &&
399
- reqOptions.method === 'GET' &&
400
- reqOptions.url.length > ((cds.env.remote && cds.env.remote.max_get_url_length) || 1028)
401
- ) {
416
+ const maxGetUrlLength =
417
+ service.options.max_get_url_length || (cds.env.remote && cds.env.remote.max_get_url_length) || 1028
418
+ if (KINDS_SUPPORTING_BATCH[service.kind] && reqOptions.method === 'GET' && reqOptions.url.length > maxGetUrlLength) {
402
419
  reqOptions._autoBatch = true
403
420
  reqOptions.data = [
404
421
  '--batch1',
@@ -1,3 +1,5 @@
1
+ const { big } = require('@sap/cds-foss')
2
+
1
3
  // Code adopted from @sap/cds-odata-v2-adapter-proxy
2
4
  // https://www.w3.org/TR/xmlschema11-2/#nt-duDTFrag
3
5
  const DurationRegex = /^P(?:(\d)Y)?(?:(\d{1,2})M)?(?:(\d{1,2})D)?T(?:(\d{1,2})H)?(?:(\d{2})M)?(?:(\d{2}(?:\.\d+)?)S)?$/i
@@ -20,15 +22,16 @@ const DataTypeOData = {
20
22
  Time: 'cds.TimeOfDay'
21
23
  }
22
24
 
23
- const _convertData = (data, target, ieee754Compatible) => {
25
+ const _convertData = (data, target, convertValueFn) => {
26
+ const _convertRecordFn = _getConvertRecordFn(target, convertValueFn)
24
27
  if (Array.isArray(data)) {
25
- return data.map(record => _getConvertRecordFn(target, ieee754Compatible)(record))
28
+ return data.map(_convertRecordFn)
26
29
  }
27
30
 
28
- return _getConvertRecordFn(target, ieee754Compatible)(data)
31
+ return _convertRecordFn(data)
29
32
  }
30
33
 
31
- const _getConvertRecordFn = (target, ieee754Compatible) => record => {
34
+ const _getConvertRecordFn = (target, convertValueFn) => record => {
32
35
  for (const key in record) {
33
36
  if (key === '__metadata') continue
34
37
 
@@ -36,24 +39,26 @@ const _getConvertRecordFn = (target, ieee754Compatible) => record => {
36
39
  if (!element) continue
37
40
 
38
41
  const recordValue = record[key]
39
- const type = _elementType(element)
40
42
  const value = (recordValue && recordValue.results) || recordValue
41
43
 
42
44
  if (value && (element.isAssociation || Array.isArray(value))) {
43
- record[key] = _convertData(value, element._target, ieee754Compatible)
45
+ record[key] = _convertData(value, element._target, convertValueFn)
44
46
  } else {
45
- record[key] = _convertValue(value, type, ieee754Compatible)
47
+ record[key] = convertValueFn(value, element)
46
48
  }
47
49
  }
48
50
 
49
51
  return record
50
52
  }
51
53
 
52
- const _convertValue = (value, type, ieee754Compatible) => {
54
+ // eslint-disable-next-line complexity
55
+ const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, element) => {
53
56
  if (value == null) {
54
57
  return value
55
58
  }
56
59
 
60
+ const type = _elementType(element)
61
+
57
62
  if (['cds.Boolean'].includes(type)) {
58
63
  if (value === 'true') {
59
64
  value = true
@@ -63,7 +68,14 @@ const _convertValue = (value, type, ieee754Compatible) => {
63
68
  } else if (['cds.Integer'].includes(type)) {
64
69
  value = parseInt(value, 10)
65
70
  } else if (['cds.Decimal', 'cds.Integer64', 'cds.DecimalFloat'].includes(type)) {
66
- value = ieee754Compatible ? `${value}` : parseFloat(value)
71
+ const bigValue = big(value)
72
+ if (ieee754Compatible) {
73
+ // TODO test with arrayed => element.items.scale?
74
+ value = exponentialDecimals ? bigValue.toExponential(element.scale) : bigValue.toFixed(element.scale)
75
+ } else {
76
+ // OData V2 does not even mention ieee754Compatible, but V4 requires JSON number if ieee754Compatible=false
77
+ value = bigValue.toNumber()
78
+ }
67
79
  } else if (['cds.Double'].includes(type)) {
68
80
  value = parseFloat(value)
69
81
  } else if (['cds.Time'].includes(type)) {
@@ -90,6 +102,29 @@ const _convertValue = (value, type, ieee754Compatible) => {
90
102
  return value
91
103
  }
92
104
 
105
+ const _convertPayloadValue = (value, element) => {
106
+ const type = _elementType(element)
107
+
108
+ // see https://www.odata.org/documentation/odata-version-2-0/json-format/
109
+ if (value == null) return value
110
+ switch (type) {
111
+ case 'cds.Date':
112
+ case 'cds.DateTime':
113
+ return `/Date(${new Date(value).getTime()})/`
114
+ case 'cds.Binary':
115
+ case 'cds.LargeBinary':
116
+ return Buffer.isBuffer(value) ? value.toString('base64') : value
117
+ case 'cds.Timestamp':
118
+ // According to OData V2 spec, and as cds.DateTime => (V2) Edm.DateTimeOffset => cds.Timestamp,
119
+ // cds.Timestamp should be converted into Edm.DateTimeOffset literal form `datetimeoffset'${new Date(value).toISOString()}'`
120
+ // However, odata-v2-proxy forwards it literaly as `datetimeoffset'...'` which is rejected by okra.
121
+ // Note that OData V2 spec example also does not contain 'datetimeoffset' predicate.
122
+ return new Date(value).toISOString()
123
+ default:
124
+ return value
125
+ }
126
+ }
127
+
93
128
  const _calculateTicksOffsetSum = text => {
94
129
  return (text.replace(/\s/g, '').match(/[+-]?([0-9]+)/g) || []).reduce((sum, value, index) => {
95
130
  return sum + parseFloat(value) * (index === 0 ? 1 : 60 * 1000) // ticks are milliseconds (0), offset are minutes (1)
@@ -115,14 +150,19 @@ const _elementType = element => {
115
150
  return type
116
151
  }
117
152
 
118
- const convertV2ResponseData = (data, target, ieee754Compatible) => {
153
+ const convertV2ResponseData = (data, target, ieee754Compatible, exponentialDecimals) => {
154
+ if (!target || !target.elements) return data
155
+ return _convertData(data, target, _convertValue(ieee754Compatible, exponentialDecimals))
156
+ }
157
+
158
+ const convertV2PayloadData = (data, target) => {
119
159
  if (!target || !target.elements) return data
120
- return _convertData(data, target, ieee754Compatible)
160
+ return _convertData(data, target, _convertPayloadValue)
121
161
  }
122
162
 
123
163
  const deepSanitize = arg => {
124
164
  if (Array.isArray(arg)) return arg.map(deepSanitize)
125
- if (typeof arg === 'object')
165
+ if (typeof arg === 'object' && arg !== null)
126
166
  return Object.keys(arg).reduce((acc, cur) => {
127
167
  acc[cur] = deepSanitize(arg[cur])
128
168
  return acc
@@ -132,5 +172,6 @@ const deepSanitize = arg => {
132
172
 
133
173
  module.exports = {
134
174
  convertV2ResponseData,
175
+ convertV2PayloadData,
135
176
  deepSanitize
136
177
  }
@@ -39,7 +39,7 @@ module.exports = class SQLiteDatabase extends DatabaseService {
39
39
  this._insert = this._queries.insert(execute.insert)
40
40
  this._read = this._queries.read(execute.select, execute.stream)
41
41
  this._update = this._queries.update(execute.update, execute.select)
42
- this._delete = this._queries.delete(execute.delete)
42
+ this._delete = this._queries.delete(execute.delete, execute.update)
43
43
  this._run = this._queries.run(this._insert, this._read, this._update, this._delete, execute.cqn, execute.sql)
44
44
 
45
45
  this.dbcs = new Map()
@@ -1,3 +1,5 @@
1
+ const cds = require('../cds')
2
+
1
3
  const convertToBoolean = boolean => {
2
4
  if (boolean === null) return null
3
5
 
@@ -41,4 +43,12 @@ const SQLITE_TYPE_CONVERSION_MAP = new Map([
41
43
  ['cds.Timestamp', convertToISOTime]
42
44
  ])
43
45
 
46
+ if (cds.env.features.bigjs) {
47
+ const Big = require('big.js')
48
+ const convertToBig = value => new Big(value)
49
+
50
+ SQLITE_TYPE_CONVERSION_MAP.set('cds.Integer64', convertToBig)
51
+ SQLITE_TYPE_CONVERSION_MAP.set('cds.Decimal', convertToBig)
52
+ }
53
+
44
54
  module.exports = { SQLITE_TYPE_CONVERSION_MAP }
@@ -41,7 +41,7 @@ const _handler = function (req) {
41
41
 
42
42
  // suppress localization in "select for update" n/a for sqlite
43
43
 
44
- redirect(query.SELECT, getLocalize(req.locale, this.model))
44
+ redirect(query, getLocalize(req.locale, this.model))
45
45
  }
46
46
 
47
47
  _handler._initial = true
@@ -94,14 +94,14 @@
94
94
 
95
95
  /**
96
96
  * @typedef {object} search2cqnOptions
97
- * @property {ColumnRefs} [columns] The columns to
98
- * be searched
97
+ * @property {ColumnRefs} [columns] The columns to be searched
99
98
  * @property {string} locale The user locale
100
99
  */
101
100
 
102
101
  /**
103
102
  * @typedef {object} cqn2cqn4sqlOptions
104
103
  * @property {boolean} [suppressSearch=false] Indicates whether the search handler is called.
104
+ * @property {boolean} [_4fiori]
105
105
  * @property {import('../db/Service')} [service]
106
106
  */
107
107
 
@@ -5,16 +5,14 @@ const { entriesStructureToEntityStructure } = require('./utils')
5
5
  module.exports = async (service, entityFQN, selection) => {
6
6
  const filter = getArgumentByName(selection.arguments, ARGUMENT.FILTER)
7
7
 
8
- let queryBeforeUpdate = service.read(entityFQN)
8
+ const queryBeforeUpdate = service.read(entityFQN)
9
9
  queryBeforeUpdate.columns(astToColumns(selection.selectionSet.selections))
10
10
 
11
11
  if (filter) {
12
12
  queryBeforeUpdate.where(astToWhere(filter))
13
13
  }
14
14
 
15
- const resultBeforeUpdate = await service.tx(tx => tx.run(queryBeforeUpdate))
16
-
17
- let query = service.update(entityFQN)
15
+ const query = service.update(entityFQN)
18
16
 
19
17
  if (filter) {
20
18
  query.where(astToWhere(filter))
@@ -24,7 +22,12 @@ module.exports = async (service, entityFQN, selection) => {
24
22
  const entries = entriesStructureToEntityStructure(service, entityFQN, astToEntries(input))
25
23
  query.with(entries)
26
24
 
27
- const result = await service.tx(tx => tx.run(query))
25
+ let resultBeforeUpdate
26
+ const result = await service.tx(async tx => {
27
+ // read needs to be done before the update, otherwise the where clause might become invalid (case that properties in where clause are updated by the mutation)
28
+ resultBeforeUpdate = await service.tx(tx => tx.run(queryBeforeUpdate))
29
+ return tx.run(query)
30
+ })
28
31
 
29
32
  // Merge selected fields with updated data
30
33
  return resultBeforeUpdate.map(original => ({ ...original, ...result }))
@@ -45,6 +45,7 @@ const traverseField = (info, field) => {
45
45
  const traverseFieldNodes = (info, fieldNodes) => fieldNodes.map(fieldNode => traverseField(info, fieldNode))
46
46
 
47
47
  module.exports = info => {
48
+ // REVISIT: JSON.parse(JSON.stringify(obj)) breaks buffers
48
49
  const deepClonedFieldNodes = JSON.parse(JSON.stringify(info.fieldNodes))
49
50
  traverseFieldNodes(info, deepClonedFieldNodes)
50
51
  return deepClonedFieldNodes
@@ -3,12 +3,23 @@ const cds = require('../_runtime/cds')
3
3
  const { where2obj } = require('../_runtime/common/utils/cqn')
4
4
  const { findCsnTargetFor } = require('../_runtime/common/utils/csn')
5
5
 
6
+ const _addKeysDeep = (keys, keysCollector) => {
7
+ for (const keyName in keys) {
8
+ const key = keys[keyName]
9
+ if (key.type === 'cds.Association' || key['@odata.foreignKey4'] === 'up_') continue
10
+ if ('elements' in key) {
11
+ _addKeysDeep(key.elements, keysCollector)
12
+ continue
13
+ }
14
+ keysCollector.push(keyName)
15
+ }
16
+ }
17
+
6
18
  function _keysOf(entity) {
7
- return entity && entity.keys
8
- ? Object.keys(entity.keys).filter(
9
- k => entity.elements[k].type !== 'cds.Association' && entity.elements[k]['@odata.foreignKey4'] !== 'up_'
10
- )
11
- : []
19
+ if (!entity || !entity.keys) return
20
+ const keysCollector = []
21
+ _addKeysDeep(entity.keys, keysCollector)
22
+ return keysCollector
12
23
  }
13
24
 
14
25
  function _getDefinition(definition, name, namespace) {
@@ -49,7 +60,7 @@ function _processSegments(cqn, model, namespace) {
49
60
  let one
50
61
  for (let i = 0; i < ref.length; i++) {
51
62
  const seg = ref[i].id || ref[i]
52
- const params = ref[i].where && where2obj(ref[i].where)
63
+ let params = ref[i].where && where2obj(ref[i].where)
53
64
 
54
65
  if (incompleteKeys) {
55
66
  // > key
@@ -91,6 +102,9 @@ function _processSegments(cqn, model, namespace) {
91
102
  if (ref[i].where) {
92
103
  keyCount += addRefToWhereIfNecessary(ref[i].where, current)
93
104
  _resolveAliasInWhere(ref[i].where, current)
105
+ // in case of Foo(1), params will be {} (before addRefToWhereIfNecessary was called)
106
+ if (!Object.keys(params).length) params = where2obj(ref[i].where)
107
+ _checkAllKeysProvided(params, current)
94
108
  }
95
109
  } else if ({ action: 1, function: 1 }[current.kind]) {
96
110
  // > action or function
@@ -146,6 +160,15 @@ function _processSegments(cqn, model, namespace) {
146
160
 
147
161
  const _resolveFrom = from => (from.SELECT ? _resolveFrom(from.SELECT.from) : from)
148
162
 
163
+ const _checkAllKeysProvided = (params, entity) => {
164
+ const keysOfEntity = _keysOf(entity)
165
+ if (!keysOfEntity) return
166
+ for (const keyOfEntity of keysOfEntity) {
167
+ if (!(keyOfEntity in params))
168
+ throw Object.assign(new Error(`Key "${keyOfEntity}" is missing for entity "${entity.name}"`), { status: 400 })
169
+ }
170
+ }
171
+
149
172
  function _4service(service) {
150
173
  const { namespace, model } = service
151
174
 
@@ -357,6 +357,15 @@ function $orderBy(orderBy) {
357
357
  if (hasValidProps(cur, 'ref')) {
358
358
  res.push(cur.ref.join('/'))
359
359
  }
360
+
361
+ if (hasValidProps(cur, 'func', 'sort')) {
362
+ res.push(`${cur.func}(${_args(cur.args)})` + ' ' + cur.sort)
363
+ continue
364
+ }
365
+
366
+ if (hasValidProps(cur, 'func')) {
367
+ res.push(`${cur.func}(${_args(cur.args)})`)
368
+ }
360
369
  }
361
370
 
362
371
  return '$orderby=' + res.join(',')
@@ -38,6 +38,11 @@
38
38
  const n = Number(str)
39
39
  return Number.isSafeInteger(n) ? n : str
40
40
  }
41
+ const standardBase64 = options.standardBase64 || function (str) {
42
+ // convert url-safe to standard base64 (with padding, if necessary)
43
+ str = str.replace(/_/g, '/').replace(/-/g, '+')
44
+ return str.padEnd(str.length + str.length % 4, '=')
45
+ }
41
46
 
42
47
  const _compareRefs = col => exp => col === exp ||
43
48
  (col.as && exp.as && col.as === exp.as) ||
@@ -178,18 +183,29 @@
178
183
  = "$count" {count = true}
179
184
  / rv:$("$ref"/"$value") {return !TECHNICAL_OPTS.includes(rv) && {from: {ref: [rv]}}}
180
185
  / head:(
181
- (identifier filter:(OPEN CLOSE/OPEN args CLOSE)? !segment) / val:(s:(!string val){return s[1]} / segment){return [val]}
186
+ (identifier filter:(OPEN CLOSE/OPEN args CLOSE)? !segment) / val:segment{return [val]}
182
187
  ) tail:( '/' p:path {return p} )? {
183
188
  const [id, filter] = head
184
189
  // minimal: val also as path segment
185
- const ref = [
186
- filter
187
- ? filter.length > 2
188
- ? { id, where: filter[1].map(f => f.val && f.val.match && f.val.match(/^"(.*)"$/) ? { val: f.val.match(/^"(.*)"$/)[1] } : f) }
189
- : { id, where: [] }
190
- // hasOwnProperty in case of '{val:0}' and so on
191
- : ( minimal ? `${typeof id === 'object' && Object.prototype.hasOwnProperty.call(id, 'val') ? id.val : id}` : id )
192
- ]
190
+ const ref = []
191
+ if (filter) {
192
+ if (filter.length > 2) {
193
+ ref.push({ id, where: filter[1].map(f => f.val && f.val.match && f.val.match(/^"(.*)"$/) ? { val: f.val.match(/^"(.*)"$/)[1] } : f) })
194
+ } else {
195
+ ref.push({ id, where: [] })
196
+ }
197
+ } else {
198
+ if (minimal) {
199
+ ref.push(`${typeof id === 'object' && 'val' in id ? id.val : id}`)
200
+ } else {
201
+ // REVISIT: keep 123 as number?
202
+ if (typeof id === 'object' && typeof id.val === 'string' && id.val.match(/^[1-9]\d*$|^0$/)) {
203
+ ref.push({ val: safeNumber(id.val) })
204
+ } else {
205
+ ref.push(id)
206
+ }
207
+ }
208
+ }
193
209
  if (tail && tail.from) {
194
210
  const more = tail.from.ref
195
211
  if (Object.prototype.hasOwnProperty.call(more[0], 'val')) ref[ref.length-1] = { id:ref[ref.length-1], where:[more.shift()] }
@@ -314,7 +330,13 @@
314
330
  where_clause = p:( n:NOT? {return n?[n]:[]} )(
315
331
  OPEN xpr:where_clause CLOSE {p.push({xpr})}
316
332
  / comp:comparison {p.push(...comp)}
317
- / lambda:lambda {p.push(...lambda)}
333
+ / lambda:lambda {
334
+ if (p[p.length - 1] === 'not' && lambda[0] === 'not') {
335
+ p.push('(', ...lambda, ')')
336
+ } else {
337
+ p.push(...lambda)
338
+ }
339
+ }
318
340
  / func:boolish {p.push(func)}
319
341
  / val:bool {p.push({val})}
320
342
  )( ao:(AND/OR) more:where_clause {p.push(ao,...more)} )*
@@ -369,7 +391,8 @@
369
391
  all = "all" OPEN p:lambda_clause CLOSE { return p }
370
392
 
371
393
  orderby
372
- = ref:(function/ref) sort:( _ s:$("asc"/"desc") {return s})? {
394
+ = ref:(lambda{const err = new Error("ORDERBY_LAMBDA_UNSUPPORTED");err.statusCode=501;throw err;}/function/ref) sort:( _ s:$("asc"/"desc") {return s})? {
395
+ // TODO: Lambda support
373
396
  const appendObj = $(ref, sort && {sort});
374
397
  SELECT.orderBy = SELECT.orderBy ?
375
398
  [...SELECT.orderBy, appendObj] :
@@ -398,7 +421,7 @@
398
421
  = operand (_ ("add" / "sub" / "mul" / "div" / "mod") _ operand)*
399
422
 
400
423
  operand "an operand"
401
- = navigationCount / function / ref / val / jsonObject / jsonArray / list
424
+ = navigationCount / function / val / ref / jsonObject / jsonArray / list
402
425
 
403
426
  navigationCount "navigation with $count"
404
427
  = navigationPath:(head:identifier key:(OPEN keyArgs:args CLOSE {return keyArgs;})? '/' {
@@ -425,6 +448,7 @@
425
448
  / val:guid {return {val}}
426
449
  / val:number {return typeof val === 'number' ? {val} : { val, literal:'number' }}
427
450
  / val:string {return {val}}
451
+ / val:binary {return {val}}
428
452
 
429
453
  jsonObject = val:$("{" (jsonObject / [^}])* "}") {return {val}}
430
454
 
@@ -561,6 +585,7 @@
561
585
  concatTrafo = OPEN o apply (o COMMA o apply)+ o CLOSE
562
586
 
563
587
  computeTrafo = OPEN o computeExpr (o COMMA o computeExpr)* o CLOSE
588
+
564
589
  computeExpr = where_clause asAlias
565
590
 
566
591
  commonFuncTrafo = OPEN o first:operand o COMMA o second:operand o CLOSE { return [first, second] }
@@ -582,34 +607,37 @@
582
607
  {return s.replace(/\\\\/g,"\\").replace(/\\"/g,'"')}
583
608
 
584
609
  word
585
- = s:$([a-zA-Z0-9.+-]+)
610
+ = $([^ \t\n()"&;]+)
586
611
 
587
612
  date
588
613
  = s:$( [0-9]+"-"[0-9][0-9]"-"[0-9][0-9] // date
589
- ("T"[0-9][0-9]":"[0-9][0-9](":"[0-9][0-9]("."[0-9]+)?)? // time
614
+ ( "T"[0-9][0-9]":"[0-9][0-9](":"[0-9][0-9]("."[0-9]+)?)? // time
590
615
  ( "Z" / (("+" / "-")[0-9][0-9]":"[0-9][0-9]) )? // timezone (Z or +-hh:mm)
591
616
  )?)
592
617
 
593
618
  number
594
- = s:$( [+-]? [0-9]+ ("."[0-9]+)? ("e"[0-9]+)? ) {return safeNumber(s)}
619
+ = s:$( [+-]? [0-9]+ ("."[0-9]+)? ("e"[0-9]+)? ) { return safeNumber(s) }
595
620
 
596
621
  integer
597
- = s:$( [+-]? [0-9]+ ) {return parseInt(s)}
622
+ = s:$( [+-]? [0-9]+ ) { return parseInt(s) }
598
623
 
599
624
  identifier
600
- = !bool !guid s:$([_a-zA-Z][_a-zA-Z0-9"."]*) {return s}
625
+ = !bool !guid s:$([_a-zA-Z][_a-zA-Z0-9"."]*) { return s }
601
626
 
602
627
  guid
603
- = $(hex16 hex16 "-"? hex16 "-"? hex16 "-"? hex16 "-"? hex16 hex16 hex16)
628
+ = $( hex16 hex16 "-"? hex16 "-"? hex16 "-"? hex16 "-"? hex16 hex16 hex16 )
604
629
 
605
630
  hex16
606
- = $([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])
631
+ = $( [0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F] )
607
632
 
608
- segment
609
- = val:$([a-zA-Z0-9-"."_~!$&'()*+,;=:@]+){return {val}}
633
+ segment // > everything except / and ?
634
+ = val:$( [^/?]+ ) { return { val } }
610
635
 
611
636
  skiptokenChars
612
- = $([a-zA-Z0-9-"."_~!$'()*+,;=:@"/""?"]+)
637
+ = $( [a-zA-Z0-9-"."_~!$'()*+,;=:@"/""?"]+ )
638
+
639
+ binary // > url-safe base64
640
+ = "binary'" s:$([a-zA-Z0-9-_]+ ("=="/"=")?) "'" { return standardBase64(s) }
613
641
 
614
642
  //
615
643
  // ---------- Punctuation ----------
@@ -63,7 +63,7 @@ module.exports = {
63
63
  try {
64
64
  cqn = odata2cqn(url, options)
65
65
  } catch (err) {
66
- if (err.message === 'EXPAND_COUNT_UNSUPPORTED') {
66
+ if (err.message === 'EXPAND_COUNT_UNSUPPORTED' || err.message === 'ORDERBY_LAMBDA_UNSUPPORTED') {
67
67
  throw getError(err.statusCode || 400, err.message)
68
68
  }
69
69
 
@@ -79,7 +79,7 @@ module.exports = {
79
79
 
80
80
  // REVISIT: _target vs __target, i.e., pseudo csn vs actual csn
81
81
  // DO NOT USE __target outside of libx/rest!!!
82
- query.__target = cqn.__target
82
+ if (cqn.__target) query.__target = cqn.__target
83
83
 
84
84
  return query
85
85
  },