@sap/cds 7.5.3 → 7.6.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 (101) hide show
  1. package/CHANGELOG.md +79 -21
  2. package/app/index.js +6 -17
  3. package/lib/auth/index.js +3 -0
  4. package/lib/compile/extend.js +9 -4
  5. package/lib/compile/for/lean_drafts.js +3 -4
  6. package/lib/compile/load.js +11 -15
  7. package/lib/compile/minify.js +2 -4
  8. package/lib/compile/to/sql.js +6 -4
  9. package/lib/compile/to/yaml.js +1 -1
  10. package/lib/dbs/cds-deploy.js +7 -13
  11. package/lib/env/defaults.js +1 -10
  12. package/lib/env/schemas/cds-package.js +27 -0
  13. package/lib/env/schemas/cds-rc.js +693 -0
  14. package/lib/env/schemas/index.js +6 -4
  15. package/lib/index.js +40 -47
  16. package/lib/log/cds-error.js +6 -0
  17. package/lib/log/format/aspects/als.js +1 -0
  18. package/lib/log/format/json.js +5 -1
  19. package/lib/ql/Query.js +2 -1
  20. package/lib/ql/cds-ql.js +1 -2
  21. package/lib/ql/infer.js +0 -2
  22. package/lib/req/cds-context.js +1 -1
  23. package/lib/req/request.js +3 -6
  24. package/lib/srv/middlewares/trace.js +2 -2
  25. package/lib/srv/protocols/hcql.js +44 -30
  26. package/lib/srv/protocols/http.js +60 -0
  27. package/lib/srv/protocols/index.js +0 -7
  28. package/lib/srv/protocols/odata-v4.js +8 -2
  29. package/lib/srv/srv-api.js +129 -62
  30. package/lib/srv/srv-handlers.js +0 -1
  31. package/lib/srv/srv-models.js +1 -0
  32. package/lib/utils/cds-utils.js +26 -0
  33. package/lib/utils/check-version.js +10 -13
  34. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +22 -6
  35. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -4
  36. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +89 -21
  37. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/boundToCQN.js +4 -2
  38. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +1 -24
  39. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/updateToCQN.js +1 -7
  40. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ApplyParser.js +3 -3
  41. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +1 -1
  42. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/http/HttpHeaderReader.js +1 -1
  43. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +6 -0
  44. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +0 -5
  45. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -0
  46. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +17 -1
  47. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +22 -2
  48. package/libx/_runtime/cds-services/services/utils/columns.js +1 -2
  49. package/libx/_runtime/common/aspects/Association.js +17 -9
  50. package/libx/_runtime/common/generic/crud.js +13 -22
  51. package/libx/_runtime/common/generic/etag.js +1 -1
  52. package/libx/_runtime/common/generic/input.js +9 -1
  53. package/libx/_runtime/common/generic/paging.js +3 -3
  54. package/libx/_runtime/common/generic/sorting.js +25 -15
  55. package/libx/_runtime/common/generic/stream.js +2 -16
  56. package/libx/_runtime/common/i18n/messages.properties +3 -0
  57. package/libx/_runtime/common/utils/copy.js +5 -0
  58. package/libx/_runtime/common/utils/cqn.js +1 -1
  59. package/libx/_runtime/common/utils/cqn2cqn4sql.js +4 -3
  60. package/libx/_runtime/common/utils/csn.js +0 -49
  61. package/libx/_runtime/common/utils/foreignKeyPropagations.js +5 -5
  62. package/libx/_runtime/common/utils/generateOnCond.js +50 -25
  63. package/libx/_runtime/common/utils/resolveView.js +5 -35
  64. package/libx/_runtime/common/utils/rewriteAsterisks.js +17 -4
  65. package/libx/_runtime/common/utils/stream.js +16 -15
  66. package/libx/_runtime/common/utils/streamProp.js +25 -22
  67. package/libx/_runtime/db/Service.js +27 -8
  68. package/libx/_runtime/db/generic/input.js +6 -1
  69. package/libx/_runtime/db/generic/rewrite.js +3 -2
  70. package/libx/_runtime/db/query/read.js +15 -5
  71. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -11
  72. package/libx/_runtime/db/utils/columns.js +1 -0
  73. package/libx/_runtime/db/utils/stream.js +41 -0
  74. package/libx/_runtime/fiori/generic/read.js +2 -1
  75. package/libx/_runtime/fiori/generic/readOverDraft.js +1 -1
  76. package/libx/_runtime/fiori/lean-draft.js +209 -55
  77. package/libx/_runtime/hana/Service.js +1 -1
  78. package/libx/_runtime/hana/execute.js +53 -14
  79. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +2 -1
  80. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +34 -15
  81. package/libx/_runtime/messaging/file-based.js +4 -3
  82. package/libx/_runtime/messaging/redis-messaging.js +2 -1
  83. package/libx/_runtime/remote/Service.js +2 -1
  84. package/libx/_runtime/remote/utils/client.js +1 -1
  85. package/libx/_runtime/sqlite/Service.js +1 -1
  86. package/libx/_runtime/sqlite/execute.js +17 -5
  87. package/libx/odata/afterburner.js +58 -19
  88. package/libx/odata/cqn2odata.js +6 -8
  89. package/libx/odata/create.js +44 -0
  90. package/libx/odata/delete.js +25 -0
  91. package/libx/odata/error.js +8 -3
  92. package/libx/odata/metadata.js +6 -8
  93. package/libx/odata/service-document.js +1 -1
  94. package/libx/odata/update.js +110 -0
  95. package/libx/odata/utils.js +9 -6
  96. package/libx/outbox/index.js +74 -89
  97. package/libx/rest/RestAdapter.js +0 -3
  98. package/package.json +1 -1
  99. package/lib/env/schemas/cds-package.json +0 -17
  100. package/lib/env/schemas/cds-rc.json +0 -740
  101. package/lib/ql/STREAM.js +0 -90
@@ -39,7 +39,7 @@ module.exports = class SQLiteDatabase extends DatabaseService {
39
39
 
40
40
  // REVISIT: official db api
41
41
  this._insert = this._queries.insert(execute.insert)
42
- this._read = this._queries.read(execute.select, execute.stream)
42
+ this._read = this._queries.read(execute.select, execute.stream, execute.convert)
43
43
  this._update = this._queries.update(execute.update, execute.select)
44
44
  this._delete = this._queries.delete(execute.delete, execute.update)
45
45
  this._run = this._queries.run(this._insert, this._read, this._update, this._delete, execute.cqn, execute.sql)
@@ -9,6 +9,7 @@ const cds = require('../cds')
9
9
  const LOG = cds.log('sqlite|db|sql')
10
10
  // && {_debug:true, debug(sql){ cds._debug && console.log(sql+';\n') }} //> please keep that for debugging stakeholder tests
11
11
  const coloredTxCommands = require('../db/utils/coloredTxCommands')
12
+ const { convertStream } = require('../db/utils/stream')
12
13
  const { inspect } = require('util')
13
14
 
14
15
  const SANITIZE_VALUES = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
@@ -234,10 +235,12 @@ function executeInsertSQL(dbc, sql, values, query) {
234
235
  })
235
236
  }
236
237
 
237
- function _convertStreamValues(values) {
238
+ // REVISIT: optimize - now sub-arrays are called sequentially
239
+ async function _convertStreamValues(values) {
238
240
  let any
239
- values.forEach((v, i) => {
240
- if (v && typeof v.pipe === 'function') {
241
+ for (let i = 0; i < values.length; i++) {
242
+ const v = values[i]
243
+ if (v instanceof Readable) {
241
244
  any = values[i] = new Promise(resolve => {
242
245
  const chunks = []
243
246
  v.on('data', chunk => chunks.push(chunk))
@@ -247,8 +250,10 @@ function _convertStreamValues(values) {
247
250
  v.push(null)
248
251
  })
249
252
  })
253
+ } else if (Array.isArray(v)) {
254
+ values[i] = await _convertStreamValues(v)
250
255
  }
251
- })
256
+ }
252
257
  return any ? Promise.all(values) : values
253
258
  }
254
259
 
@@ -295,13 +300,19 @@ function executeGenericCQN(model, dbc, cqn, user, locale, txTimestamp) {
295
300
  return executePlainSQL(dbc, sql, values)
296
301
  }
297
302
 
298
- async function executeSelectStreamCQN(model, dbc, query, user, locale, txTimestamp) {
303
+ // REVISIT: consider deleting this function after removing stream_compat
304
+ async function executeSelectStreamCQN({ model, dbc, query, user, locale, txTimestamp }) {
299
305
  const result = await executeSelectCQN(model, dbc, query, user, locale, txTimestamp)
300
306
 
301
307
  if (result == null || result.length === 0) {
302
308
  return
303
309
  }
304
310
 
311
+ if (!cds.env.features.stream_compat) {
312
+ convertStream(query.SELECT.columns, query.target, result, query.SELECT.one)
313
+ return result
314
+ }
315
+
305
316
  let val = Array.isArray(result) ? Object.values(result[0])[0] : Object.values(result)[0]
306
317
  if (val === null) {
307
318
  return null
@@ -323,6 +334,7 @@ module.exports = {
323
334
  update: executeUpdateCQN,
324
335
  select: executeSelectCQN,
325
336
  stream: executeSelectStreamCQN,
337
+ convert: convertStream,
326
338
  cqn: executeGenericCQN,
327
339
  sql: executePlainSQL
328
340
  }
@@ -4,6 +4,7 @@ const { where2obj, resolveFromSelect } = require('../_runtime/common/utils/cqn')
4
4
  const { findCsnTargetFor } = require('../_runtime/common/utils/csn')
5
5
  const normalizeTimestamp = require('../_runtime/common/utils/normalizeTimestamp')
6
6
  const { rewriteExpandAsterisk } = require('../_runtime/common/utils/rewriteAsterisks')
7
+ const resolveStructured = require('../_runtime/common/utils/resolveStructured')
7
8
 
8
9
  const _addKeysDeep = (keys, keysCollector, ignoreManagedBacklinks) => {
9
10
  for (const keyName in keys) {
@@ -44,21 +45,20 @@ function _getDefinition(definition, name, namespace) {
44
45
  )
45
46
  }
46
47
 
47
- function _resolveAliasInParams(params, entity) {
48
- if (!entity._alias2ref) return
49
- const paramKeys = Object.keys(params)
50
- for (const paramKey of paramKeys) {
51
- if (entity._alias2ref[paramKey]) {
52
- params[entity._alias2ref[paramKey].join('_')] = params[paramKey]
53
- params[paramKey] = undefined
54
- }
55
- }
56
- }
57
-
58
48
  function _resolveAliasesInRef(ref, target) {
59
49
  if (ref.length === 1) {
60
50
  if (target.keys[ref[0]]) return ref
61
- if (target._alias2ref && target._alias2ref[ref[0]]) return [...target._alias2ref[ref[0]]]
51
+ // resolve multi-part refs for innermost ref in url
52
+ if (target._flattenedKeys === undefined) {
53
+ const flattenedKeys = []
54
+ for (const key in target.keys) {
55
+ if (!target.keys[key].elements) continue
56
+ flattenedKeys.push(...resolveStructured({ element: target.keys[key], structProperties: [] }, false, true))
57
+ }
58
+ target._flattenedKeys = flattenedKeys.length ? flattenedKeys : null
59
+ }
60
+ const fk = target._flattenedKeys?.find(fk => fk.key === ref[0])
61
+ if (fk) return [...fk.resolved]
62
62
  }
63
63
  for (const seg of ref) {
64
64
  target = target.elements[seg.id || seg]
@@ -143,7 +143,7 @@ function _processWhere(where, entity) {
143
143
 
144
144
  if (operator in forbidden) {
145
145
  // xpr check needs to be done first, else it could happen, that we ignore xpr OR xpr
146
- continue;
146
+ continue
147
147
  }
148
148
 
149
149
  let valIndex = -1
@@ -216,7 +216,7 @@ function _processSegments(from, model, namespace, cqn) {
216
216
  let incompleteKeys = false
217
217
  let one
218
218
  let target
219
-
219
+
220
220
  function _handleCollectionBoundActions(i) {
221
221
  let action
222
222
  if (current.actions) {
@@ -224,9 +224,9 @@ function _processSegments(from, model, namespace, cqn) {
224
224
  const shortName = nextRef && nextRef.replace(namespace + '.', '')
225
225
  action = shortName && current.actions[shortName]
226
226
  }
227
-
227
+
228
228
  incompleteKeys = ref[i].where ? false : i === ref.length - 1 || one ? false : true
229
-
229
+
230
230
  if (incompleteKeys && action) {
231
231
  if (
232
232
  action['@cds.odata.bindingparameter.collection'] ||
@@ -239,7 +239,7 @@ function _processSegments(from, model, namespace, cqn) {
239
239
  }
240
240
  }
241
241
  }
242
-
242
+
243
243
  for (let i = 0; i < ref.length; i++) {
244
244
  const seg = ref[i].id || ref[i]
245
245
  const whereRef = ref[i].where
@@ -286,7 +286,6 @@ function _processSegments(from, model, namespace, cqn) {
286
286
  if (whereRef) {
287
287
  keyCount += addRefToWhereIfNecessary(ref[i].where, current)
288
288
  _resolveAliasesInXpr(ref[i].where, current)
289
- _resolveAliasInParams(params, current)
290
289
  _processWhere(ref[i].where, current)
291
290
  }
292
291
 
@@ -320,7 +319,6 @@ function _processSegments(from, model, namespace, cqn) {
320
319
  if (whereRef) {
321
320
  keyCount += addRefToWhereIfNecessary(whereRef, current)
322
321
  _resolveAliasesInXpr(whereRef, current)
323
- _resolveAliasInParams(params, current)
324
322
  // in case of Foo(1), params will be {} (before addRefToWhereIfNecessary was called)
325
323
  if (!Object.keys(params).length) params = where2obj(ref[i].where)
326
324
  _processWhere(ref[i].where, current)
@@ -513,6 +511,41 @@ const _checkAllKeysProvided = (params, entity) => {
513
511
  }
514
512
  }
515
513
 
514
+ function _cleanupIgnoredXpr(xpr, ignoredColumns, target, isOne) {
515
+ if (!xpr) return
516
+
517
+ for (const x of xpr) {
518
+ if (x.xpr) {
519
+ _cleanupIgnoredXpr(x.xpr, ignoredColumns, target, isOne)
520
+ continue
521
+ }
522
+
523
+ if (x.ref && ignoredColumns.includes(x.ref[0])) {
524
+ cds.error(
525
+ `Property '${x.ref}' does not exist in type '${
526
+ isOne ? target.name.replace(`${target._service.name}.`, '') : target.name
527
+ }'`,
528
+ {
529
+ code: 400
530
+ }
531
+ )
532
+ }
533
+
534
+ if (x.func) {
535
+ _cleanupIgnoredXpr(x.args, ignoredColumns, target, isOne)
536
+ continue
537
+ }
538
+ }
539
+ }
540
+
541
+ function _cleanupIgnored(SELECT, ignoredColumns, target, isOne) {
542
+ _cleanupIgnoredXpr(SELECT.columns, ignoredColumns, target, isOne)
543
+ _cleanupIgnoredXpr(SELECT.orderBy, ignoredColumns, target, isOne)
544
+ _cleanupIgnoredXpr(SELECT.where, ignoredColumns, target, isOne)
545
+ _cleanupIgnoredXpr(SELECT.groupBy, ignoredColumns, target, isOne)
546
+ _cleanupIgnoredXpr(SELECT.having, ignoredColumns, target, isOne)
547
+ }
548
+
516
549
  function _4service(service) {
517
550
  const { namespace, model } = service
518
551
  if (!model) return cqn => cqn
@@ -563,6 +596,12 @@ function _4service(service) {
563
596
  */
564
597
  _processColumns(cqn, current)
565
598
 
599
+ const ignoredColumns = Object.values(root.elements ?? {})
600
+ .filter(element => element['@cds.api.ignore'])
601
+ .map(element => element.name)
602
+
603
+ _cleanupIgnored(cqn.SELECT, ignoredColumns, root, one)
604
+
566
605
  return cqn
567
606
  }
568
607
  }
@@ -177,7 +177,12 @@ function _xpr(expr, target, kind, isLambda) {
177
177
  } else if (_isLambda(cur, expr[i + 1])) {
178
178
  const { where, id } = expr[i + 1].ref.slice(-1)[0]
179
179
  const nav = [...expr[i + 1].ref.slice(0, -1), id].join('/')
180
- if (!where) res.push(`${nav}/any()`)
180
+ // odata-v2 does not support lambda expressions but successfactors allows filter like for to-one assocs
181
+ if (kind === 'odata-v2') {
182
+ cds.log('remote').info(`OData V2 does not support lambda expressions. Using path expression as best effort.`)
183
+ isLambda = false
184
+ res.push(`${id}%2F${_xpr(where, target, kind)}`)
185
+ } else if (!where) res.push(`${nav}/any()`)
181
186
  else res.push(`${nav}/any(${LAMBDA_VARIABLE}:${_xpr(where, target, kind, true)})`)
182
187
  i++
183
188
  } else {
@@ -216,13 +221,6 @@ const _keysOfWhere = (where, kind, target) => {
216
221
  const res = []
217
222
  for (const cur of where) {
218
223
  if (hasValidProps(cur, 'ref')) {
219
- if (target && target._alias2ref) {
220
- const alias = target._alias2ref.__2alias[cur.ref.join('/')]
221
- if (alias) {
222
- res.push(_format({ ref: [alias] }))
223
- continue
224
- }
225
- }
226
224
  res.push(_format(cur))
227
225
  } else if (hasValidProps(cur, 'val')) {
228
226
  // find previous ref
@@ -0,0 +1,44 @@
1
+ const cds = require('../../')
2
+ const { odataError } = require('./utils')
3
+ const { INSERT } = require('../../lib/ql/cds-ql')
4
+ const { readAfterWrite } = require('../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
5
+ const metaInfo = require('../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
6
+ const { toODataResult } = require('./result')
7
+
8
+ module.exports = srv =>
9
+ function create(req, res, next) {
10
+ const query = cds.odata.parse(req.url, { service: srv, baseUrl: req.baseUrl })
11
+
12
+ const {
13
+ SELECT: { one }
14
+ } = query
15
+
16
+ if (one) {
17
+ const singleton = query.target._isSingleton
18
+ const error = odataError('405', `Method ${req.method} not allowed for ${singleton ? 'SINGLETON' : 'ENTITY'}`)
19
+ return res.status(405).json(error)
20
+ }
21
+
22
+ const queryPathXpr = query.SELECT?.from
23
+
24
+ const insertQuery = INSERT.into(queryPathXpr).entries(req.body)
25
+
26
+ const cdsReq = new cds.Request({ query: insertQuery })
27
+ return srv
28
+ .dispatch(cdsReq)
29
+ .then(async result => {
30
+ if (cdsReq._.readAfterWrite) {
31
+ // TODO see if in old odata impl for other checks that should happen
32
+ result = await readAfterWrite(cdsReq, srv, { operation: { result } })
33
+ }
34
+
35
+ if (result == null) {
36
+ res.status(204)
37
+ }
38
+
39
+ const info = metaInfo(insertQuery, 'CREATE', srv, req.body, req, false)
40
+
41
+ return res.status(201).send(toODataResult(result, info))
42
+ })
43
+ .catch(next)
44
+ }
@@ -0,0 +1,25 @@
1
+ const cds = require('../../')
2
+ const { odataError } = require('./utils')
3
+
4
+ module.exports = srv =>
5
+ function deleete(req, res, next) {
6
+ const query = cds.odata.parse(req.url, { service: srv, baseUrl: req.baseUrl })
7
+
8
+ let {
9
+ SELECT: { one }
10
+ } = query
11
+
12
+ if (!one) {
13
+ return res.status(405).json(odataError('405', `Method DELETE not allowed for ENTITY.COLLECTION`))
14
+ }
15
+
16
+ const _target = query.SELECT && query.SELECT.from
17
+ const deleteQuery = DELETE.from(_target)
18
+
19
+ return srv
20
+ .run(deleteQuery)
21
+ .then(() => {
22
+ return res.send(204)
23
+ })
24
+ .catch(next)
25
+ }
@@ -1,6 +1,11 @@
1
- module.exports = srv => (err, req, res, next) => {
2
- if (err.code >= 400 && err.code < 500) {
3
- return res.status(err.code).send(err.message)
1
+ const { normalizeError } = require('../_runtime/common/error/frontend')
2
+ const { odataError } = require('./utils')
3
+
4
+ module.exports = _srv => (err, req, res, _next) => {
5
+ const { error, statusCode } = normalizeError(err, req)
6
+
7
+ if (statusCode >= 400 && statusCode < 500) {
8
+ return res.status(statusCode).json(odataError(`${err.code}`, error.message))
4
9
  }
5
10
 
6
11
  return res.status(500).send('Internal Server Error')
@@ -1,7 +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
+ const { odataError } = require('./utils')
5
5
 
6
6
  const _requestedFormat = (queryOption, header) => {
7
7
  if (queryOption) return queryOption.match(/json/i) ? 'json' : 'xml'
@@ -43,18 +43,16 @@ const generateEtag = s => {
43
43
  return `W/"${crypto.createHash('sha256').update(s).digest('base64')}"`
44
44
  }
45
45
 
46
- const odata_error = (code, message) => ({ error: { code, message } })
47
-
48
46
  const mpSupportsEmptyLocale = () => {
49
47
  const pkg = require(require.resolve('@sap/cds-mtxs/package.json'))
50
48
  const [major, minor] = pkg.version.split('.').map(Number)
51
- return major > 1 || major === 1 && minor >= 12
49
+ return major > 1 || (major === 1 && minor >= 12)
52
50
  }
53
51
 
54
52
  module.exports = srv =>
55
- async function metadata(req, res, next) {
53
+ async function metadata(req, res, _next) {
56
54
  if (req.method !== 'GET')
57
- return res.status(405).json(odata_error('METHOD_NOT_ALLOWED', `Method ${req.method} not allowed for $metadata.`))
55
+ return res.status(405).json(odataError('METHOD_NOT_ALLOWED', `Method ${req.method} not allowed for $metadata.`))
58
56
 
59
57
  const tenant = cds.context.tenant
60
58
  const locale = cds.context.locale
@@ -82,7 +80,7 @@ module.exports = srv =>
82
80
  res
83
81
  .status(400)
84
82
  .json(
85
- odata_error(
83
+ odataError(
86
84
  'UNSUPPORTED_METADATA_TYPE',
87
85
  'JSON metadata is not supported if cds.requires.extensibilty: true.'
88
86
  )
@@ -113,7 +111,7 @@ module.exports = srv =>
113
111
  LOG.error(e)
114
112
  }
115
113
 
116
- return res.status(503).json(odata_error('SERVICE_UNAVAILABLE', 'Service unavailable'))
114
+ return res.status(503).json(odataError('SERVICE_UNAVAILABLE', 'Service unavailable'))
117
115
  }
118
116
  }
119
117
 
@@ -15,7 +15,7 @@ const generateEtag = s => {
15
15
  }
16
16
 
17
17
  module.exports = srv =>
18
- function service_document(req, res, next) {
18
+ function service_document(req, res) {
19
19
  if (req.method === 'HEAD') return res.end()
20
20
  if (req.method !== 'GET')
21
21
  return res.status(405).json({
@@ -0,0 +1,110 @@
1
+ const cds = require('../../')
2
+ const metaInfo = require('../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
3
+ const { readAfterWrite } = require('../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
4
+ const { where2obj } = require('../_runtime/common/utils/cqn')
5
+ const { toODataResult } = require('./result')
6
+ const { odataError } = require('./utils')
7
+
8
+ const _isUpsertAllowed = ({ target, data, event }) => {
9
+ return (
10
+ !(cds.env.runtime && cds.env.runtime.allow_upsert === false) &&
11
+ !(target && target._isDraftEnabled && (!cds.env.fiori.lean_draft || (!data.IsActiveEntity && event === 'PATCH')))
12
+ )
13
+ }
14
+
15
+ const _isNavigationWithKeyInParent = (keys, data, pathExpression, model) => {
16
+ // keys not in data
17
+ if (keys && Object.keys(keys).some(key => key in data)) {
18
+ return false
19
+ }
20
+
21
+ const nav = pathExpression.ref && pathExpression.ref.length !== 0 && pathExpression.ref[1]
22
+ const parent = pathExpression.ref && pathExpression.ref[0].id
23
+
24
+ // not a navigation
25
+ if (!parent || !nav) {
26
+ return false
27
+ }
28
+
29
+ const navID = typeof nav === 'string' ? nav : nav.id
30
+ const navElement = model.definitions[parent].elements[navID]
31
+
32
+ // not a containment
33
+ if (!navElement._isContained) {
34
+ return false
35
+ }
36
+
37
+ const where = pathExpression.ref[0].where
38
+ return parent && navElement && where
39
+ }
40
+
41
+ module.exports = srv =>
42
+ function update(req, res, next) {
43
+ const query = cds.odata.parse(req.url, { service: srv, baseUrl: req.baseUrl })
44
+
45
+ const {
46
+ SELECT: { one }
47
+ } = query
48
+
49
+ if (!one) {
50
+ return res.status(405).json(odataError('405', `Method ${req.method} not allowed for ENTITY.COLLECTION`))
51
+ }
52
+
53
+ const queryPathXpr = query.SELECT && query.SELECT.from
54
+
55
+ const isPrimitive = query._propertyAccess
56
+ const data = isPrimitive ? { [query._propertyAccess]: req.body.value } : req.body
57
+
58
+ const updateQuery = UPDATE.entity(queryPathXpr).with(data)
59
+
60
+ // we need the cds request, so we can access req._.readAfterWrite
61
+ const cdsReq = new cds.Request({ query: updateQuery })
62
+
63
+ const info = metaInfo(query, 'UPDATE', srv, data, req, false)
64
+
65
+ if (!isPrimitive && queryPathXpr.ref?.[queryPathXpr.ref.length - 1].where) {
66
+ Object.assign(data, where2obj(queryPathXpr.ref?.[queryPathXpr.ref.length - 1].where))
67
+ }
68
+
69
+ return srv
70
+ .dispatch(cdsReq)
71
+ .then(async result => {
72
+ if (!isPrimitive && cdsReq._.readAfterWrite) {
73
+ // TODO see if in old odata impl for other checks that should happen
74
+ result = await readAfterWrite(cdsReq, srv, { operation: { result } })
75
+ }
76
+
77
+ if (result == null) {
78
+ res.status(204)
79
+ }
80
+
81
+ return res.send(toODataResult(result, info))
82
+ })
83
+ .catch(async e => {
84
+ // UPSERT
85
+ const is404 = e.code === 404 || e.status === 404 || e.statusCode === 404
86
+ if (is404 && !isPrimitive && _isUpsertAllowed({ target: query.target, data: req.body, event: req.method })) {
87
+ // PUT / PATCH with if-match header means "only if already exists", i.e., no insert if not
88
+ if (req.headers['if-match']) throw Object.assign(new Error('412'), { statusCode: 412 })
89
+
90
+ if (_isNavigationWithKeyInParent(query.target.keys, data, queryPathXpr, srv.model)) {
91
+ // REVISIT better error message
92
+ return res.status(422).json(odataError('422', `Unprocessable Entity`))
93
+ }
94
+
95
+ // REVISIT: up_XX needs to be looked up -> composition of aspect
96
+ const insertQuery = INSERT.into(queryPathXpr).entries(data)
97
+ const cdsReq = new cds.Request({ query: insertQuery })
98
+ let result = await srv.dispatch(cdsReq)
99
+
100
+ if (cdsReq._.readAfterWrite) {
101
+ // TODO see if in old odata impl for other checks that should happen
102
+ result = await readAfterWrite(cdsReq, srv, { operation: { result } })
103
+ }
104
+
105
+ return res.status(201).send(toODataResult(result, info))
106
+ }
107
+ throw e
108
+ })
109
+ .catch(next)
110
+ }
@@ -3,18 +3,20 @@ const cds = require('../_runtime/cds')
3
3
 
4
4
  const MATH_FUNC = { round: 1, floor: 1, ceiling: 1 }
5
5
 
6
+ const odataError = (code, message) => ({ error: { code, message } })
7
+
6
8
  const getSafeNumber = inputString => {
7
9
  if (typeof inputString !== 'string') return inputString
8
10
  // Try to parse the input string as a floating-point number using parseFloat
9
11
  const parsedFloat = parseFloat(inputString)
10
-
12
+
11
13
  // Check if the parsed value is not NaN and is equal to the original input string
12
14
  if (!isNaN(parsedFloat) && String(parsedFloat) === inputString) {
13
15
  return parsedFloat
14
- }
15
-
16
+ }
17
+
16
18
  // Try to parse the input string as an integer using parseInt
17
- const parsedInt = parseInt(inputString);
19
+ const parsedInt = parseInt(inputString)
18
20
  // special case like '3.00000000000001', the precision is not lost and string is returned
19
21
  if (!isNaN(parsedInt) && String(parsedInt) === inputString.replace(/^-?\d+\.0+$/, inputString.split('.')[0])) {
20
22
  return parsedInt
@@ -156,7 +158,7 @@ const _v4 = (val, element) => {
156
158
  const formatVal = (val, elementName, csnTarget, kind, func, literal) => {
157
159
  if (val === null || val === 'null') return 'null'
158
160
  if (typeof val === 'boolean') return val
159
- if (typeof val === 'string' && literal === 'number' ) return `${val}`
161
+ if (typeof val === 'string' && literal === 'number') return `${val}`
160
162
  if (typeof val === 'string') {
161
163
  if (!csnTarget && UUID.test(val)) return kind === 'odata-v2' ? `guid'${val}'` : val
162
164
  if (func in MATH_FUNC) return val
@@ -231,5 +233,6 @@ const skipToken = (token, cqn) => {
231
233
  module.exports = {
232
234
  getSafeNumber,
233
235
  formatVal,
234
- skipToken
236
+ skipToken,
237
+ odataError
235
238
  }