@sap/cds 7.3.1 → 7.4.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 (110) hide show
  1. package/CHANGELOG.md +69 -3
  2. package/_i18n/i18n_es_MX.properties +110 -0
  3. package/apis/cds.d.ts +13 -12
  4. package/apis/core.d.ts +27 -108
  5. package/apis/cqn.d.ts +15 -18
  6. package/apis/csn.d.ts +95 -60
  7. package/apis/env.d.ts +25 -0
  8. package/apis/events.d.ts +125 -0
  9. package/apis/{reflect.d.ts → linked.d.ts} +29 -38
  10. package/apis/models.d.ts +60 -45
  11. package/apis/ql.d.ts +19 -5
  12. package/apis/{serve.d.ts → server.d.ts} +59 -33
  13. package/apis/services.d.ts +76 -147
  14. package/apis/test.d.ts +1 -1
  15. package/bin/serve.js +3 -0
  16. package/lib/compile/cds-compile.js +2 -2
  17. package/lib/compile/etc/csv.js +2 -1
  18. package/lib/compile/to/edm.js +8 -3
  19. package/lib/compile/to/gql.js +4 -0
  20. package/lib/dbs/cds-deploy.js +52 -4
  21. package/lib/env/cds-requires.js +27 -15
  22. package/lib/env/defaults.js +1 -0
  23. package/lib/env/schemas/index.js +10 -0
  24. package/lib/index.js +7 -4
  25. package/lib/linked/models.js +8 -5
  26. package/lib/ql/CREATE.js +2 -0
  27. package/lib/ql/DELETE.js +1 -0
  28. package/lib/ql/DROP.js +2 -0
  29. package/lib/ql/INSERT.js +2 -22
  30. package/lib/ql/Query.js +59 -22
  31. package/lib/ql/SELECT.js +5 -0
  32. package/lib/ql/STREAM.js +2 -0
  33. package/lib/ql/UPDATE.js +2 -0
  34. package/lib/ql/UPSERT.js +3 -1
  35. package/lib/ql/cds-ql.js +21 -5
  36. package/lib/ql/infer.js +129 -0
  37. package/lib/req/cds-context.js +8 -5
  38. package/lib/srv/cds-connect.js +3 -1
  39. package/lib/utils/axios.js +4 -2
  40. package/lib/utils/data.js +3 -0
  41. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +12 -0
  42. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +27 -9
  43. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +1 -1
  44. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +8 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +11 -8
  46. package/libx/_runtime/common/code-ext/worker.js +5 -16
  47. package/libx/_runtime/common/generic/auth/capabilities.js +11 -2
  48. package/libx/_runtime/common/i18n/messages.properties +1 -0
  49. package/libx/_runtime/common/utils/postProcessing.js +1 -1
  50. package/libx/_runtime/common/utils/resolveView.js +28 -9
  51. package/libx/{common → _runtime/common}/utils/ucsn.js +19 -11
  52. package/libx/_runtime/db/expand/expandCQNToJoin.js +6 -6
  53. package/libx/_runtime/db/expand/rawToExpanded.js +4 -4
  54. package/libx/_runtime/db/sql-builder/InsertBuilder.js +6 -1
  55. package/libx/_runtime/db/sql-builder/UpdateBuilder.js +6 -1
  56. package/libx/_runtime/db/sql-builder/dollar.js +7 -7
  57. package/libx/_runtime/fiori/generic/activate.js +2 -2
  58. package/libx/_runtime/fiori/generic/edit.js +25 -45
  59. package/libx/_runtime/fiori/generic/read.js +3 -5
  60. package/libx/_runtime/fiori/lean-draft.js +171 -84
  61. package/libx/_runtime/fiori/utils/delete.js +7 -1
  62. package/libx/_runtime/fiori/utils/handler.js +4 -6
  63. package/libx/_runtime/fiori/utils/lockInfo.js +27 -0
  64. package/libx/_runtime/fiori/utils/where.js +20 -1
  65. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -2
  66. package/libx/_runtime/messaging/Outbox.js +12 -47
  67. package/libx/_runtime/messaging/common-utils/AMQPClient.js +1 -3
  68. package/libx/_runtime/messaging/common-utils/authorizedRequest.js +3 -0
  69. package/libx/_runtime/messaging/common-utils/connections.js +1 -1
  70. package/libx/_runtime/messaging/enterprise-messaging.js +12 -13
  71. package/libx/_runtime/messaging/file-based.js +7 -5
  72. package/libx/_runtime/messaging/redis-messaging.js +10 -11
  73. package/libx/_runtime/messaging/service.js +12 -26
  74. package/libx/_runtime/remote/Service.js +52 -36
  75. package/libx/_runtime/remote/utils/client.js +24 -125
  76. package/libx/odata/afterburner.js +16 -6
  77. package/libx/odata/grammar.peggy +26 -7
  78. package/libx/odata/metadata.js +18 -1
  79. package/libx/odata/parser.js +1 -1
  80. package/libx/odata/service-document.js +0 -1
  81. package/libx/odata/utils.js +19 -3
  82. package/libx/{_runtime/messaging/outbox/utils.js → outbox/index.js} +94 -24
  83. package/libx/rest/middleware/parse.js +1 -1
  84. package/package.json +2 -2
  85. package/apis/connect.d.ts +0 -39
  86. package/bin/utils/modules.js +0 -7
  87. package/bin/utils/term.js +0 -56
  88. package/lib/env/schema.js +0 -9
  89. package/lib/linked/queries.js +0 -41
  90. package/lib/srv/protocols/odata-v2-proxy.js +0 -3699
  91. package/libx/common/asserts.js +0 -0
  92. package/libx/common/crud.js +0 -0
  93. package/libx/common/etag.js +0 -0
  94. package/libx/common/localized.js +0 -0
  95. package/libx/common/managed.js +0 -0
  96. package/libx/common/paging.js +0 -0
  97. package/libx/common/readme.md +0 -4
  98. package/libx/common/sorting.js +0 -0
  99. package/libx/common/temporal.js +0 -0
  100. package/libx/connect/auth.js +0 -0
  101. package/libx/connect/perf.js +0 -0
  102. package/libx/connect/readme.md +0 -3
  103. package/libx/fiori/draft/readme.md +0 -1
  104. package/libx/fiori/readme.md +0 -1
  105. package/libx/hana/readme.md +0 -1
  106. package/libx/msg/readme.md +0 -3
  107. package/libx/readme.md +0 -1
  108. package/libx/sqlite/readme.md +0 -1
  109. /package/libx/_runtime/{messaging/common-utils → common/utils}/waitingTime.js +0 -0
  110. /package/libx/{_runtime/messaging/outbox → outbox}/OutboxRunner.js +0 -0
@@ -7,41 +7,34 @@ const { convertV2ResponseData, deepSanitize, convertV2PayloadData } = require('.
7
7
 
8
8
  let _cloudSdk
9
9
 
10
- const PPPD = {
11
- POST: 1,
12
- PUT: 1,
13
- PATCH: 1,
14
- DELETE: 1
15
- }
16
-
10
+ const PPPD = { POST: 1, PUT: 1, PATCH: 1, DELETE: 1 }
17
11
  const KINDS_SUPPORTING_BATCH = { odata: 1, 'odata-v2': 1, 'odata-v4': 1 }
18
12
 
19
13
  const _sanitizeHeaders = headers => {
20
- if (headers && headers.authorization) headers.authorization = headers.authorization.split(' ')[0] + ' ...'
21
-
14
+ // REVISIT: is this in-place modification intended?
15
+ if (headers?.authorization) headers.authorization = headers.authorization.split(' ')[0] + ' ***'
22
16
  return headers
23
17
  }
24
18
 
25
19
  const _executeHttpRequest = async ({ requestConfig, destination, destinationOptions, jwt, csrf, csrfInBatch }) => {
26
- const { executeHttpRequestWithOrigin } = cloudSdk()
27
- const destinationName = typeof destination === 'string' && destination
20
+ const { executeHttpRequestWithOrigin } = _getCloudSdk()
28
21
 
29
- if (destinationName) {
30
- destination = { destinationName, ...(resolveDestinationOptions(destinationOptions, jwt) ?? {}) }
22
+ if (typeof destination === 'string') {
23
+ destination = {
24
+ destinationName: destination,
25
+ ...destinationOptions,
26
+ ...{ jwt: destinationOptions?.jwt !== undefined ? destinationOptions.jwt : jwt }
27
+ }
28
+ if (destination.jwt !== undefined && !destination.jwt) delete destination.jwt // don't pass any value
31
29
  } else if (destination.forwardAuthToken) {
32
30
  destination = {
33
31
  ...destination,
34
32
  headers: destination.headers ? { ...destination.headers } : {},
35
33
  authentication: 'NoAuthentication'
36
34
  }
37
-
38
35
  delete destination.forwardAuthToken
39
-
40
- if (jwt) {
41
- destination.headers.authorization = `Bearer ${jwt}`
42
- } else {
43
- LOG._warn && LOG.warn('Missing JWT token for forwardAuthToken')
44
- }
36
+ if (jwt) destination.headers.authorization = `Bearer ${jwt}`
37
+ else LOG._warn && LOG.warn('Missing JWT token for forwardAuthToken')
45
38
  }
46
39
 
47
40
  let requestOptions
@@ -64,62 +57,20 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
64
57
  const maxBodyLength = cds.env?.remote?.max_body_length
65
58
  requestConfig = {
66
59
  ...requestConfig,
67
- headers: {
68
- custom: { ...requestConfig.headers }
69
- },
60
+ headers: { custom: { ...requestConfig.headers } },
70
61
  ...(maxBodyLength && { maxBodyLength })
71
62
  }
72
63
 
73
64
  return executeHttpRequestWithOrigin(destination, requestConfig, requestOptions)
74
65
  }
75
66
 
76
- const cloudSdk = () => {
67
+ const _getCloudSdk = () => {
77
68
  if (_cloudSdk) return _cloudSdk
78
69
  // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
79
70
  _cloudSdk = require('@sap-cloud-sdk/http-client')
80
71
  return _cloudSdk
81
72
  }
82
73
 
83
- const getDestination = (name, credentials) => {
84
- // Cloud SDK wants property "queryParameters" but we have documented "queries"
85
- if (credentials.queries && !credentials.queryParameters) {
86
- credentials.queryParameters = credentials.queries
87
- }
88
-
89
- return { name, ...credentials }
90
- }
91
-
92
- /**
93
- * @param {import('@sap-cloud-sdk/connectivity').DestinationFetchOptions} [options]
94
- * @param {string} [jwt]
95
- * @returns {import('@sap-cloud-sdk/connectivity').DestinationFetchOptions}
96
- */
97
- const resolveDestinationOptions = function (options, jwt) {
98
- if (!options && !jwt) return
99
-
100
- const resolvedOptions = Object.assign({}, options ?? {})
101
- resolvedOptions.jwt = jwt
102
-
103
- if (options?.selectionStrategy) {
104
- resolvedOptions.selectionStrategy = options.selectionStrategy
105
- }
106
-
107
- return resolvedOptions
108
- }
109
-
110
- const getKind = options => {
111
- const kind = (options.credentials && options.credentials.kind) || options.kind
112
- if (typeof kind === 'object') {
113
- const k = Object.keys(kind).find(
114
- key => key === 'odata' || key === 'odata-v4' || key === 'odata-v2' || key === 'rest'
115
- )
116
- // odata-v4 is equivalent of odata
117
- return k === 'odata-v4' ? 'odata' : k
118
- }
119
-
120
- return kind
121
- }
122
-
123
74
  /**
124
75
  * Rest Client
125
76
  */
@@ -278,21 +229,10 @@ const _getSanitizedError = (e, reqOptions, options = { suppressRemoteResponseBod
278
229
  }
279
230
 
280
231
  // eslint-disable-next-line complexity
281
- const run = async (
282
- requestConfig,
283
- {
284
- destination,
285
- jwt,
286
- kind,
287
- resolvedTarget,
288
- returnType,
289
- suppressRemoteResponseBody,
290
- destinationOptions,
291
- csrf,
292
- csrfInBatch
293
- }
294
- ) => {
232
+ const run = async (requestConfig, options) => {
295
233
  let response
234
+
235
+ const { destination, destinationOptions, jwt, csrf, csrfInBatch, suppressRemoteResponseBody } = options
296
236
  try {
297
237
  response = await _executeHttpRequest({
298
238
  requestConfig,
@@ -368,31 +308,19 @@ const run = async (
368
308
  }
369
309
  }
370
310
 
311
+ const { kind, resolvedTarget, returnType } = options
371
312
  if (kind === 'odata-v4') return _purgeODataV4(response.data)
372
313
  if (kind === 'odata-v2') return _purgeODataV2(response.data, resolvedTarget, returnType, requestConfig.headers)
373
314
  if (kind === 'odata') {
374
315
  if (typeof response.data !== 'object') return response.data
375
316
  // try to guess if we need to purge v2 or v4
376
- if (response.data.d) {
377
- return _purgeODataV2(response.data, resolvedTarget, returnType, requestConfig.headers)
378
- }
379
-
317
+ if (response.data.d) return _purgeODataV2(response.data, resolvedTarget, returnType, requestConfig.headers)
380
318
  return _purgeODataV4(response.data)
381
319
  }
382
320
 
383
321
  return response.data
384
322
  }
385
323
 
386
- const getJwt = req => {
387
- const headers = req?.context?.headers
388
- if (headers?.authorization) {
389
- const token = headers.authorization.match(/^bearer (.+)/i)
390
- if (token) return token[1]
391
- }
392
-
393
- return null
394
- }
395
-
396
324
  const _cqnToReqOptions = (query, service, req) => {
397
325
  const { kind, model } = service
398
326
  const method = req.method
@@ -459,8 +387,8 @@ const getReqOptions = (req, query, service) => {
459
387
  typeof query === 'object'
460
388
  ? _cqnToReqOptions(query, service, req)
461
389
  : typeof query === 'string'
462
- ? _stringToReqOptions(query, req.data, req.target)
463
- : _pathToReqOptions(req.method, req.path, req.data, req.target, service.name)
390
+ ? _stringToReqOptions(query, req.data, req.target)
391
+ : _pathToReqOptions(req.method, req.path, req.data, req.target, service.name)
464
392
 
465
393
  if (service.kind === 'odata-v2' && req.event === 'READ' && reqOptions.url?.match(/(\/any\()|(\/all\()/)) {
466
394
  req.reject(501, 'Lambda expressions are not supported in OData v2')
@@ -528,41 +456,12 @@ const getReqOptions = (req, query, service) => {
528
456
  if (service.path) reqOptions.url = `${encodeURI(service.path)}${reqOptions.url}`
529
457
 
530
458
  // set axios responseType to 'arraybuffer' if returning binary in rest
531
- if (req._binary) {
532
- reqOptions.responseType = 'arraybuffer'
533
- }
459
+ if (req._binary) reqOptions.responseType = 'arraybuffer'
534
460
 
535
461
  return reqOptions
536
462
  }
537
463
 
538
- const getAdditionalOptions = (
539
- req,
540
- destination,
541
- kind,
542
- resolvedTarget,
543
- returnType,
544
- destinationOptions,
545
- csrf,
546
- csrfInBatch
547
- ) => {
548
- const jwt = getJwt(req)
549
- const additionalOptions = {
550
- destination,
551
- kind,
552
- resolvedTarget,
553
- returnType,
554
- destinationOptions,
555
- csrf,
556
- csrfInBatch
557
- }
558
- if (jwt) additionalOptions.jwt = jwt
559
- return additionalOptions
560
- }
561
-
562
464
  module.exports = {
563
- getKind,
564
465
  run,
565
- getReqOptions,
566
- getDestination,
567
- getAdditionalOptions
466
+ getReqOptions
568
467
  }
@@ -125,12 +125,15 @@ function getResolvedElement(entity, { ref }) {
125
125
  return element
126
126
  }
127
127
 
128
+ const forbidden = { '(': 1, and: 1, or: 1, not: 1, ')': 1 }
129
+
128
130
  function _processWhere(where, entity) {
129
131
  for (let i = 0; i < where.length; i++) {
130
132
  const ref = where[i]
133
+ const operator = where[i + 1]
131
134
  const val = where[i + 2]
132
135
 
133
- if (ref === '(' || ref === ')' || ref === 'and' || ref === 'or' || ref === 'not' || val === 'not' || ref.func) {
136
+ if (ref in forbidden || val in forbidden || ref.func) {
134
137
  continue
135
138
  }
136
139
  if (ref.xpr) {
@@ -138,6 +141,11 @@ function _processWhere(where, entity) {
138
141
  continue
139
142
  }
140
143
 
144
+ if (operator in forbidden) {
145
+ // xpr check needs to be done first, else it could happen, that we ignore xpr OR xpr
146
+ continue;
147
+ }
148
+
141
149
  let valIndex = -1
142
150
  let refIndex = -1
143
151
  if (typeof val === 'object') {
@@ -165,19 +173,21 @@ function _processWhere(where, entity) {
165
173
  function _convertVal(element, value) {
166
174
  if (value === null) return value
167
175
  switch (element._type) {
168
- case 'cds.Integer':
169
176
  case 'cds.UInt8':
177
+ case 'cds.Integer':
170
178
  case 'cds.Int16':
171
179
  case 'cds.Int32':
180
+ if (!/^-?\d+$/.test(value)) throw new Error('Not a valid integer')
172
181
  // eslint-disable-next-line no-case-declarations
173
182
  const n = Number(value)
174
- if (Number.isSafeInteger(n)) return n
175
- throw new Error('Not a valid integer') // TODO
183
+ if (!Number.isSafeInteger(n)) throw new Error('Not a valid integer')
184
+ if (element._type === 'cds.UInt8' && n < 0) throw new Error('Not a positive integer')
185
+ return n
176
186
 
177
187
  case 'cds.String':
178
- case 'cds.LargeString':
188
+ case 'cds.LargeString':
179
189
  return String(value)
180
- case 'cds.Double':
190
+ case 'cds.Double':
181
191
  return parseFloat(value)
182
192
  case 'cds.Decimal':
183
193
  case 'cds.DecimalFloat':
@@ -36,9 +36,25 @@
36
36
  // we keep that here to allow for usage in https://peggyjs.org/online
37
37
  const safeNumber =
38
38
  options.safeNumber ||
39
- function (str) {
40
- const n = Number(str)
41
- return Number.isSafeInteger(n) ? n : str
39
+ function (inputString) {
40
+ if (typeof inputString !== 'string') return inputString
41
+ // Try to parse the input string as a floating-point number using parseFloat
42
+ const parsedFloat = parseFloat(inputString)
43
+
44
+ // Check if the parsed value is not NaN and is equal to the original input string
45
+ if (!isNaN(parsedFloat) && String(parsedFloat) === inputString) {
46
+ return parsedFloat
47
+ }
48
+
49
+ // Try to parse the input string as an integer using parseInt
50
+ const parsedInt = parseInt(inputString);
51
+ // special case like '3.00000000000001', the precision is not lost and string is returned
52
+ if (!isNaN(parsedInt) && String(parsedInt) === inputString.replace(/^-?\d+\.0+$/, inputString.split('.')[0])) {
53
+ return parsedInt
54
+ }
55
+
56
+ // If none of the above conditions are met, return the input string as is
57
+ return inputString
42
58
  }
43
59
  const skipToken = options.skipToken
44
60
  const standardBase64 =
@@ -380,7 +396,9 @@
380
396
  "$skip=" o val:skip { _setLimitOffset(val) } /
381
397
  "$search=" o s:search { if (s) SELECT.search = s } /
382
398
  "$count=" o count /
383
- "$apply=" o trafos:transformations { return trafos }
399
+ "$apply=" o trafos:transformations { return trafos } /
400
+ //Workaround to support empty expand even if not OData compliant old adapter supported it and did not crash
401
+ "$expand=" {return null}
384
402
 
385
403
  temporal = ("$at" / "$from" / "$toInclusive" / "$to") "=" date
386
404
 
@@ -600,7 +618,8 @@
600
618
  aliasedParamVal = val / jsonObject / jsonArray / "[" list:innerList "]" { return { list } }
601
619
 
602
620
  custom
603
- = [a-zA-Z0-9-_.~]+ "=" [^&]*
621
+ = [a-zA-Z0-9-_.~]+ ("=" [^&]*)?
622
+
604
623
  aliasedParam "an aliased parameter (@param)" = "@" i:identifier { return "@" + i }
605
624
  aliasedParamEqualsVal = alias:aliasedParam "=" !aliasedParam value:aliasedParamVal {
606
625
  _replaceAliased(SELECT, alias, value);
@@ -655,9 +674,9 @@
655
674
  }
656
675
 
657
676
  val
658
- = val:(bool / date) {return {val}}
659
- / val:time {return {val}}
677
+ = val:bool {return {val}}
660
678
  / val:date {return {val}}
679
+ / val:time {return {val}}
661
680
  / val:guid {return {val}}
662
681
  / val:number {return typeof val === 'number' ? {val} : { val, literal:'number' }}
663
682
  / val:string {return {val}}
@@ -1,6 +1,7 @@
1
1
  const cds = require('../../lib')
2
2
  const LOG = cds.log('odata')
3
3
  const crypto = require('crypto')
4
+ const { join } = require ('path')
4
5
 
5
6
  const _requestedFormat = (queryOption, header) => {
6
7
  if (queryOption) return queryOption.match(/json/i) ? 'json' : 'xml'
@@ -44,6 +45,12 @@ const generateEtag = s => {
44
45
 
45
46
  const odata_error = (code, message) => ({ error: { code, message } })
46
47
 
48
+ const mpSupportsEmptyLocale = () => {
49
+ const pkg = require(join('@sap/cds-mtxs', 'package.json'))
50
+ const version = pkg.version.match(/^(\d+\.)?(\d+\.)?(\*|\d+)$/).map(Number)
51
+ return version[1] > 1 || (version[1] === 1 && version[2] >= 12)
52
+ }
53
+
47
54
  module.exports = srv =>
48
55
  async function metadata(req, res, next) {
49
56
  if (req.path === '/$metadata') {
@@ -85,10 +92,20 @@ module.exports = srv =>
85
92
  )
86
93
 
87
94
  try {
88
- const edmx = await mps.getEdmx({ tenant, model: srv.model, service: srv.definition.name, locale })
95
+ let edmx
96
+ // REVISIT: remove check later
97
+ if (mpSupportsEmptyLocale()) {
98
+ edmx = metadataCache.edm || await mps.getEdmx({ tenant, model: srv.model, service: srv.definition.name })
99
+ metadataCache.edm = edmx
100
+ const extBundle = cds.env.requires.extensibility && await mps.getI18n({ tenant, locale })
101
+ edmx = cds.localize(srv.model, locale, edmx, extBundle)
102
+ } else {
103
+ edmx = await mps.getEdmx({ tenant, model: srv.model, service: srv.definition.name, locale })
104
+ }
89
105
  metadataCache.xmlEtag[locale] = generateEtag(edmx)
90
106
  res.set('Content-Type', 'application/xml')
91
107
  res.send(edmx)
108
+ return
92
109
  } catch (e) {
93
110
  if (LOG._error) {
94
111
  e.message = 'Unable to get EDMX for tenant ' + tenant + ' due to error: ' + e.message