@sap/cds 5.7.5 → 5.8.2

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 +97 -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/log/format/kibana.js +3 -3
  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/ODataRequest.js +1 -3
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +11 -32
  33. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +12 -5
  34. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +7 -4
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +24 -3
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +43 -38
  37. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  38. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +11 -5
  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/ResourcePathParser.js +23 -2
  47. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriHelper.js +7 -6
  48. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriTokenizer.js +2 -5
  49. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +19 -47
  50. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +4 -11
  51. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +7 -1
  52. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/CommandExecutor.js +0 -3
  53. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/ConditionalRequestControlCommand.js +0 -1
  54. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +2 -2
  55. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ErrorJsonSerializer.js +2 -0
  56. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +2 -5
  57. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +6 -6
  58. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  59. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -4
  60. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +41 -17
  61. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +1 -17
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +60 -18
  63. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +47 -10
  64. package/libx/_runtime/cds-services/adapter/rest/Rest.js +22 -1
  65. package/libx/_runtime/cds-services/adapter/rest/handlers/read.js +8 -3
  66. package/libx/_runtime/cds-services/adapter/rest/utils/parse-url.js +3 -0
  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/tree.js +1 -1
  76. package/libx/_runtime/common/composition/update.js +39 -34
  77. package/libx/_runtime/common/error/frontend.js +19 -5
  78. package/libx/_runtime/common/generic/auth.js +20 -85
  79. package/libx/_runtime/common/generic/crud.js +22 -1
  80. package/libx/_runtime/common/i18n/messages.properties +2 -1
  81. package/libx/_runtime/common/utils/cqn.js +2 -6
  82. package/libx/_runtime/common/utils/cqn2cqn4sql.js +95 -122
  83. package/libx/_runtime/common/utils/csn.js +29 -6
  84. package/libx/_runtime/common/utils/foreignKeyPropagations.js +21 -1
  85. package/libx/_runtime/common/utils/keys.js +2 -1
  86. package/libx/_runtime/common/utils/path.js +1 -1
  87. package/libx/_runtime/common/utils/resolveView.js +12 -4
  88. package/libx/_runtime/common/utils/rewriteAsterisks.js +27 -13
  89. package/libx/_runtime/common/utils/search2cqn4sql.js +11 -6
  90. package/libx/_runtime/common/utils/structured.js +10 -4
  91. package/libx/_runtime/common/utils/vcap.js +27 -10
  92. package/libx/_runtime/db/data-conversion/post-processing.js +20 -13
  93. package/libx/_runtime/db/expand/expand-v2.js +21 -12
  94. package/libx/_runtime/db/expand/expandCQNToJoin.js +67 -26
  95. package/libx/_runtime/db/expand/index.js +3 -0
  96. package/libx/_runtime/db/generic/create.js +0 -10
  97. package/libx/_runtime/db/generic/index.js +3 -0
  98. package/libx/_runtime/db/generic/read.js +2 -24
  99. package/libx/_runtime/db/generic/rewrite.js +1 -3
  100. package/libx/_runtime/db/generic/update.js +1 -1
  101. package/libx/_runtime/db/query/delete.js +10 -4
  102. package/libx/_runtime/db/query/insert.js +3 -4
  103. package/libx/_runtime/db/query/read.js +4 -1
  104. package/libx/_runtime/db/query/update.js +5 -5
  105. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +9 -2
  106. package/libx/_runtime/db/sql-builder/FunctionBuilder.js +3 -0
  107. package/libx/_runtime/db/sql-builder/SelectBuilder.js +7 -3
  108. package/libx/_runtime/db/sql-builder/index.js +3 -0
  109. package/libx/_runtime/db/utils/columns.js +5 -2
  110. package/libx/_runtime/db/utils/deep.js +16 -14
  111. package/libx/_runtime/db/utils/generateAliases.js +56 -6
  112. package/libx/_runtime/fiori/generic/before.js +73 -49
  113. package/libx/_runtime/fiori/generic/edit.js +14 -18
  114. package/libx/_runtime/fiori/generic/patch.js +8 -11
  115. package/libx/_runtime/fiori/generic/read.js +20 -19
  116. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -4
  117. package/libx/_runtime/fiori/utils/handler.js +1 -11
  118. package/libx/_runtime/hana/Service.js +1 -1
  119. package/libx/_runtime/hana/conversion.js +12 -1
  120. package/libx/_runtime/hana/dynatrace.js +11 -5
  121. package/libx/_runtime/hana/execute.js +132 -19
  122. package/libx/_runtime/hana/search.js +3 -3
  123. package/libx/_runtime/hana/search2cqn4sql.js +23 -25
  124. package/libx/_runtime/hana/searchToContains.js +1 -1
  125. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  126. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +0 -1
  127. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  128. package/libx/_runtime/messaging/file-based.js +3 -1
  129. package/libx/_runtime/messaging/service.js +4 -1
  130. package/libx/_runtime/remote/utils/client.js +41 -24
  131. package/libx/_runtime/remote/utils/data.js +54 -12
  132. package/libx/_runtime/sqlite/Service.js +1 -1
  133. package/libx/_runtime/sqlite/conversion.js +10 -0
  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 +49 -21
  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
@@ -65,7 +65,6 @@ class EndpointRegistry {
65
65
  const other = authInfo
66
66
  ? {
67
67
  _: { req: { authInfo, headers: {}, query: {} } }, // for messaging to retrieve subdomain
68
- user: new cds.User.Privileged(),
69
68
  tenant: tenantId
70
69
  }
71
70
  : {}
@@ -20,6 +20,7 @@ const _checkAppURL = appURL => {
20
20
  throw new Error(
21
21
  'Enterprise Messaging: You need to provide an HTTPS endpoint to your application.\n\nHint: You can set the application URI in environment variable `VCAP_APPLICATION.application_uris[0]`. This is needed because incoming messages are delivered through HTTP via webhooks.\nExample: `{ ..., "VCAP_APPLICATION": { "application_uris": ["my-app.com"] } }`\nIn case you want to use Enterprise Messaging in shared (that means single-tenant) mode, you can use kind `enterprise-messaging-amqp`.'
22
22
  )
23
+
23
24
  if (appURL.startsWith('https://localhost'))
24
25
  throw new Error(
25
26
  'The endpoint of your application is local and cannot be reached from Enterprise Messaging.\n\nHint: For local development you can set up a tunnel to your local endpoint and enter its public https endpoint in `VCAP_APPLICATION.application_uris[0]`.\nIn case you want to use Enterprise Messaging in shared (that means single-tenant) mode, you can use kind `enterprise-messaging-amqp`.'
@@ -53,7 +53,9 @@ class FileBasedMessaging extends MessagingService {
53
53
  if (this.subscribedTopics.has(topic)) {
54
54
  const event = this.subscribedTopics.get(topic)
55
55
  if (!event) return
56
- super.emit({ event, ...json, inbound: true }).catch(e => LOG._debug && LOG.debug(e))
56
+ super
57
+ .emit({ event, ...json, inbound: true })
58
+ .catch(e => LOG.error('ERROR occured in asynchronous event processing:', e))
57
59
  } else other.push(each + '\n')
58
60
  }
59
61
  } catch (e) {
@@ -75,7 +75,8 @@ class MessagingService extends OutboxService {
75
75
  }
76
76
 
77
77
  emit(event, data, headers) {
78
- const msg = event instanceof cds.Event ? event : new cds.Event(this.message4(event, data, headers))
78
+ const _msg = typeof event === 'object' ? event : { event, data, headers }
79
+ const msg = _msg instanceof cds.Event ? _msg : new cds.Event(this.message4(_msg))
79
80
  return super.emit(msg)
80
81
  }
81
82
 
@@ -108,6 +109,8 @@ class MessagingService extends OutboxService {
108
109
 
109
110
  message4(msg) {
110
111
  const _msg = { ...msg }
112
+ if (msg.inbound && !cds.context)
113
+ _msg.user = msg.tenant ? new cds.User.Privileged({ tenant: msg.tenant }) : new cds.User.Privileged()
111
114
  _msg.event = _warnAndStripTopicPrefix(_msg.event)
112
115
  if (!_msg.headers) _msg.headers = {}
113
116
  if (!_msg.inbound) {
@@ -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,
@@ -141,8 +142,8 @@ function _defineProperty(obj, property, value) {
141
142
  }
142
143
 
143
144
  function _normalizeMetadata(prefix, data, results) {
144
- const target = results || data
145
- if (typeof target !== 'object') return target
145
+ const target = results !== undefined ? results : data
146
+ if (typeof target !== 'object' || target === null) return target
146
147
  const metadataKeys = Object.keys(data).filter(k => prefix.test(k))
147
148
  for (const k of metadataKeys) {
148
149
  const $ = k.replace(prefix, '$')
@@ -166,17 +167,17 @@ 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')
171
- const purgedResponse = data.results || data
172
- const convertedResponse = convertV2ResponseData(purgedResponse, target, ieee754Compatible)
170
+ const ieee754Compatible = reqHeaders.accept && reqHeaders.accept.includes('IEEE754Compatible=true')
171
+ const exponentialDecimals = ieee754Compatible && reqHeaders.accept.includes('ExponentialDecimals=true')
172
+ const purgedResponse = 'results' in data ? data.results : data
173
+ const convertedResponse = convertV2ResponseData(purgedResponse, target, ieee754Compatible, exponentialDecimals)
173
174
  return _normalizeMetadata(/^__/, data, convertedResponse)
174
175
  }
175
176
 
176
177
  const _purgeODataV4 = data => {
177
178
  if (typeof data !== 'object') return data
178
179
 
179
- const purgedResponse = data.value || data
180
+ const purgedResponse = 'value' in data ? data.value : data
180
181
  return _normalizeMetadata(/^@odata\./, data, purgedResponse)
181
182
  }
182
183
 
@@ -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,27 @@ const _getConvertRecordFn = (target, ieee754Compatible) => record => {
36
39
  if (!element) continue
37
40
 
38
41
  const recordValue = record[key]
39
- const type = _elementType(element)
40
- const value = (recordValue && recordValue.results) || recordValue
42
+ const value =
43
+ (recordValue && typeof recordValue === 'object' && 'results' in recordValue && recordValue.results) || recordValue
41
44
 
42
45
  if (value && (element.isAssociation || Array.isArray(value))) {
43
- record[key] = _convertData(value, element._target, ieee754Compatible)
46
+ record[key] = _convertData(value, element._target, convertValueFn)
44
47
  } else {
45
- record[key] = _convertValue(value, type, ieee754Compatible)
48
+ record[key] = convertValueFn(value, element)
46
49
  }
47
50
  }
48
51
 
49
52
  return record
50
53
  }
51
54
 
52
- const _convertValue = (value, type, ieee754Compatible) => {
55
+ // eslint-disable-next-line complexity
56
+ const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, element) => {
53
57
  if (value == null) {
54
58
  return value
55
59
  }
56
60
 
61
+ const type = _elementType(element)
62
+
57
63
  if (['cds.Boolean'].includes(type)) {
58
64
  if (value === 'true') {
59
65
  value = true
@@ -63,7 +69,14 @@ const _convertValue = (value, type, ieee754Compatible) => {
63
69
  } else if (['cds.Integer'].includes(type)) {
64
70
  value = parseInt(value, 10)
65
71
  } else if (['cds.Decimal', 'cds.Integer64', 'cds.DecimalFloat'].includes(type)) {
66
- value = ieee754Compatible ? `${value}` : parseFloat(value)
72
+ const bigValue = big(value)
73
+ if (ieee754Compatible) {
74
+ // TODO test with arrayed => element.items.scale?
75
+ value = exponentialDecimals ? bigValue.toExponential(element.scale) : bigValue.toFixed(element.scale)
76
+ } else {
77
+ // OData V2 does not even mention ieee754Compatible, but V4 requires JSON number if ieee754Compatible=false
78
+ value = bigValue.toNumber()
79
+ }
67
80
  } else if (['cds.Double'].includes(type)) {
68
81
  value = parseFloat(value)
69
82
  } else if (['cds.Time'].includes(type)) {
@@ -90,6 +103,29 @@ const _convertValue = (value, type, ieee754Compatible) => {
90
103
  return value
91
104
  }
92
105
 
106
+ const _convertPayloadValue = (value, element) => {
107
+ const type = _elementType(element)
108
+
109
+ // see https://www.odata.org/documentation/odata-version-2-0/json-format/
110
+ if (value == null) return value
111
+ switch (type) {
112
+ case 'cds.Date':
113
+ case 'cds.DateTime':
114
+ return `/Date(${new Date(value).getTime()})/`
115
+ case 'cds.Binary':
116
+ case 'cds.LargeBinary':
117
+ return Buffer.isBuffer(value) ? value.toString('base64') : value
118
+ case 'cds.Timestamp':
119
+ // According to OData V2 spec, and as cds.DateTime => (V2) Edm.DateTimeOffset => cds.Timestamp,
120
+ // cds.Timestamp should be converted into Edm.DateTimeOffset literal form `datetimeoffset'${new Date(value).toISOString()}'`
121
+ // However, odata-v2-proxy forwards it literaly as `datetimeoffset'...'` which is rejected by okra.
122
+ // Note that OData V2 spec example also does not contain 'datetimeoffset' predicate.
123
+ return new Date(value).toISOString()
124
+ default:
125
+ return value
126
+ }
127
+ }
128
+
93
129
  const _calculateTicksOffsetSum = text => {
94
130
  return (text.replace(/\s/g, '').match(/[+-]?([0-9]+)/g) || []).reduce((sum, value, index) => {
95
131
  return sum + parseFloat(value) * (index === 0 ? 1 : 60 * 1000) // ticks are milliseconds (0), offset are minutes (1)
@@ -115,9 +151,14 @@ const _elementType = element => {
115
151
  return type
116
152
  }
117
153
 
118
- const convertV2ResponseData = (data, target, ieee754Compatible) => {
154
+ const convertV2ResponseData = (data, target, ieee754Compatible, exponentialDecimals) => {
155
+ if (!target || !target.elements) return data
156
+ return _convertData(data, target, _convertValue(ieee754Compatible, exponentialDecimals))
157
+ }
158
+
159
+ const convertV2PayloadData = (data, target) => {
119
160
  if (!target || !target.elements) return data
120
- return _convertData(data, target, ieee754Compatible)
161
+ return _convertData(data, target, _convertPayloadValue)
121
162
  }
122
163
 
123
164
  const deepSanitize = arg => {
@@ -132,5 +173,6 @@ const deepSanitize = arg => {
132
173
 
133
174
  module.exports = {
134
175
  convertV2ResponseData,
176
+ convertV2PayloadData,
135
177
  deepSanitize
136
178
  }
@@ -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 }
@@ -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(',')