@sap/cds 7.4.2 → 7.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/apis/cds.d.ts +1 -38
  3. package/apis/core.d.ts +21 -101
  4. package/apis/cqn.d.ts +18 -76
  5. package/apis/csn.d.ts +18 -114
  6. package/apis/events.d.ts +16 -123
  7. package/apis/internal/inference.d.ts +18 -32
  8. package/apis/linked.d.ts +18 -97
  9. package/apis/log.d.ts +19 -164
  10. package/apis/models.d.ts +18 -180
  11. package/apis/ql.d.ts +16 -323
  12. package/apis/reflect.d.ts +32 -0
  13. package/apis/server.d.ts +18 -135
  14. package/apis/services.d.ts +18 -380
  15. package/bin/cds-serve.js +5 -2
  16. package/bin/serve.js +7 -16
  17. package/lib/auth/basic-auth.js +3 -1
  18. package/lib/auth/ias-auth.js +62 -48
  19. package/lib/auth/ias-claims.js +34 -0
  20. package/lib/auth/index.js +54 -33
  21. package/lib/auth/jwt-auth.js +55 -52
  22. package/lib/compile/cdsc.js +2 -2
  23. package/lib/compile/to/edm.js +4 -4
  24. package/lib/compile/to/hdbtabledata.js +5 -8
  25. package/lib/compile/to/srvinfo.js +2 -2
  26. package/lib/env/cds-env.js +3 -9
  27. package/lib/env/cds-requires.js +16 -17
  28. package/lib/env/compat.js +0 -9
  29. package/lib/env/defaults.js +17 -6
  30. package/lib/i18n/localize.js +46 -42
  31. package/lib/index.js +6 -8
  32. package/lib/linked/classes.js +7 -118
  33. package/lib/linked/entities.js +1 -1
  34. package/lib/log/cds-log.js +15 -10
  35. package/lib/log/format/aspects/als.js +41 -0
  36. package/lib/log/format/aspects/cf.js +36 -0
  37. package/lib/log/format/json.js +96 -0
  38. package/lib/plugins.js +7 -3
  39. package/lib/req/context.js +4 -2
  40. package/lib/srv/cds-connect.js +3 -5
  41. package/lib/srv/cds-serve.js +13 -26
  42. package/lib/srv/factory.js +3 -3
  43. package/lib/srv/middlewares/index.js +0 -2
  44. package/lib/srv/middlewares/trace.js +2 -3
  45. package/lib/srv/protocols/_legacy.js +27 -30
  46. package/lib/srv/protocols/index.js +173 -58
  47. package/lib/srv/protocols/odata-v4.js +29 -16
  48. package/lib/srv/srv-api.js +8 -13
  49. package/lib/srv/srv-handlers.js +14 -14
  50. package/lib/utils/cds-utils.js +15 -0
  51. package/libx/_runtime/auth/index.js +4 -5
  52. package/libx/_runtime/auth/strategies/basic.js +2 -2
  53. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +23 -13
  54. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +6 -15
  55. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +10 -3
  56. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +5 -2
  57. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +2 -1
  58. package/libx/_runtime/cds-services/services/utils/columns.js +3 -9
  59. package/libx/_runtime/cds.js +13 -0
  60. package/libx/_runtime/common/composition/data.js +3 -0
  61. package/libx/_runtime/common/composition/delete.js +1 -1
  62. package/libx/_runtime/common/error/frontend.js +2 -2
  63. package/libx/_runtime/common/generic/auth/readOnly.js +1 -1
  64. package/libx/_runtime/common/generic/auth/restrictions.js +1 -1
  65. package/libx/_runtime/common/generic/sorting.js +4 -5
  66. package/libx/_runtime/common/utils/csn.js +23 -18
  67. package/libx/_runtime/common/utils/restrictions.js +6 -15
  68. package/libx/_runtime/db/generic/input.js +3 -2
  69. package/libx/_runtime/fiori/generic/readOverDraft.js +2 -5
  70. package/libx/_runtime/fiori/lean-draft.js +69 -5
  71. package/libx/_runtime/hana/Service.js +1 -1
  72. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
  73. package/libx/_runtime/messaging/Outbox.js +3 -8
  74. package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
  75. package/libx/_runtime/messaging/file-based.js +1 -1
  76. package/libx/_runtime/messaging/service.js +7 -10
  77. package/libx/_runtime/remote/Service.js +15 -45
  78. package/libx/_runtime/remote/utils/client.js +20 -33
  79. package/libx/_runtime/remote/utils/cloudSdkProvider.js +30 -0
  80. package/libx/_runtime/sqlite/Service.js +2 -2
  81. package/libx/odata/afterburner.js +29 -21
  82. package/libx/odata/cqn2odata.js +1 -1
  83. package/libx/odata/error.js +7 -0
  84. package/libx/odata/grammar.peggy +16 -20
  85. package/libx/odata/metadata.js +73 -78
  86. package/libx/odata/parser.js +1 -1
  87. package/libx/odata/read.js +94 -0
  88. package/libx/odata/result.js +91 -0
  89. package/libx/odata/service-document.js +31 -37
  90. package/libx/odata/utils.js +2 -1
  91. package/libx/outbox/index.js +9 -4
  92. package/libx/rest/RestAdapter.js +68 -67
  93. package/libx/rest/middleware/create.js +20 -26
  94. package/libx/rest/middleware/delete.js +5 -3
  95. package/libx/rest/middleware/error.js +2 -3
  96. package/libx/rest/middleware/input.js +5 -5
  97. package/libx/rest/middleware/operation.js +96 -41
  98. package/libx/rest/middleware/parse.js +4 -6
  99. package/libx/rest/middleware/payload.js +5 -5
  100. package/libx/rest/middleware/read.js +11 -17
  101. package/libx/rest/middleware/update.js +20 -25
  102. package/package.json +2 -1
  103. package/server.js +7 -4
  104. package/srv/outbox.cds +9 -10
  105. package/apis/env.d.ts +0 -25
  106. package/apis/test.d.ts +0 -81
  107. package/apis/utils.d.ts +0 -15
  108. package/lib/auth/passport-basic.js +0 -14
  109. package/lib/auth/passport-digest.js +0 -16
  110. package/lib/env/presets.js +0 -35
  111. package/lib/log/format/cf.js +0 -16
  112. package/lib/log/format/kibana.js +0 -92
  113. package/lib/srv/middlewares/ctx-auth.js +0 -11
  114. package/libx/_runtime/cds-services/adapter/rest/utils/validation-checks.js +0 -119
@@ -0,0 +1,94 @@
1
+ const metaInfo = require('../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
2
+ const cds = require('../../')
3
+ const { toODataResult } = require('./result')
4
+ const querystring = require('node:querystring')
5
+ const { getPageSize } = require('../_runtime/common/generic/paging')
6
+
7
+ const _getCount = result =>
8
+ Array.isArray(result)
9
+ ? result.reduce((acc, val) => {
10
+ return acc + ((val && (val.$count || val._counted_)) || (val[0] && (val[0].$count || val[0]._counted_))) || 0
11
+ }, 0)
12
+ : result.$count || result._counted_ || 0
13
+
14
+ const _calculateNextLink = (req, result) => {
15
+ const $skiptoken = _calculateSkiptoken(req, result)
16
+ if ($skiptoken) {
17
+ const queryParamsWithSkipToken = { ...req.http.req.query, $skiptoken }
18
+ // REVISIT: slice replaces leading '/'. Always starts with '/'?
19
+ result.$nextLink = (
20
+ req.http.req.path.slice(1) + '?' + querystring.stringify(queryParamsWithSkipToken, '&', '=', { encodeURIComponent: e => e })
21
+ )
22
+ }
23
+
24
+ }
25
+
26
+ const _calculateSkiptoken = (req, result) => {
27
+ const limit = Array.isArray(req.query)
28
+ ? getPageSize(req.query[0]._target).max
29
+ : req.query.SELECT.limit?.rows?.val
30
+ const top = parseInt(req.http.req.query['$top'])
31
+ if (limit === result.length && limit !== top) {
32
+ const token = req.http.req.query['$skiptoken']
33
+ if (cds.env.query.limit.reliablePaging && _reliablePagingPossible(req)) {
34
+ const decoded = token && JSON.parse(Buffer.from(token, 'base64').toString())
35
+ const skipToken = {
36
+ r: (decoded?.r || 0) + limit,
37
+ c: req.query.SELECT.orderBy.map(o => ({
38
+ a: o.sort ? o.sort === 'asc' : true,
39
+ k: o.ref[0],
40
+ v: result[result.length - 1][o.ref[0]]
41
+ }))
42
+ }
43
+
44
+ if (limit + (decoded?.r || 0) !== top) {
45
+ return Buffer.from(JSON.stringify(skipToken)).toString('base64')
46
+ }
47
+ } else {
48
+ return (token ? parseInt(token) : 0) + limit
49
+ }
50
+ }
51
+ }
52
+
53
+ const _reliablePagingPossible = req => {
54
+ if (req.target._isDraftEnabled) return false
55
+ if (cds.context?.http.req.query.$apply) return false
56
+ if (req.query.SELECT.limit.offset?.val ?? req.query.SELECT.limit.offset > 0) return false
57
+ if (req.query.SELECT.orderBy?.some(o => !o.ref)) return false
58
+ return (
59
+ !req.query.SELECT.columns ||
60
+ req.query.SELECT.columns.some(c => c === '*' || c.ref?.[0] === '*') ||
61
+ req.query.SELECT.orderBy?.every(o => req.query.SELECT.columns?.some(c => o.ref[0] === c.ref?.[0]))
62
+ )
63
+ }
64
+
65
+ module.exports = srv =>
66
+ function read(req, res, next) {
67
+ const query = cds.odata.parse(req.url, { service: srv, baseUrl: req.baseUrl })
68
+
69
+ // we need the cds request, so we can access the modified query, which is cloned due to lean-draft, so we need to use dispatch here and pass a cds req
70
+ const cdsReq = new cds.Request({query})
71
+ return srv
72
+ .dispatch(cdsReq)
73
+ .then(result => {
74
+ if (!result.$nextLink) {
75
+ _calculateNextLink(cdsReq, result)
76
+ }
77
+
78
+ const lastPathElement = req.path.split('/').slice(-1)[0]
79
+ if (lastPathElement === '$count') {
80
+ result = _getCount(result)
81
+ return res.send(result.toString())
82
+ } else if (lastPathElement === '$value' && query._propertyAccess) {
83
+ return res.send(result[query._propertyAccess].toString())
84
+ }
85
+
86
+ // mainly for @odata.context
87
+ const info = metaInfo(query, 'READ', srv, {}, req, false)
88
+ result = toODataResult(result, info)
89
+
90
+ // Express interprets numbers as HTTP status codes
91
+ return res.send(typeof result === 'number' ? result.toString() : result)
92
+ })
93
+ .catch(next)
94
+ }
@@ -0,0 +1,91 @@
1
+ const METADATA = {
2
+ $context: '@odata.context',
3
+ $count: '@odata.count',
4
+ $etag: '@odata.etag',
5
+ $metadataEtag: '@odata.metadataEtag',
6
+ $bind: '@odata.bind',
7
+ $id: '@odata.id',
8
+ $delta: '@odata.delta',
9
+ $removed: '@odata.removed',
10
+ $type: '@odata.type',
11
+ $nextLink: '@odata.nextLink',
12
+ $deltaLink: '@odata.deltaLink',
13
+ $editLink: '@odata.editLink',
14
+ $readLink: '@odata.readLink',
15
+ $navigationLink: '@odata.navigationLink',
16
+ $associationLink: '@odata.associationLink',
17
+ $mediaEditLink: '@odata.mediaEditLink',
18
+ $mediaReadLink: '@odata.mediaReadLink',
19
+ $mediaContentType: '@odata.mediaContentType',
20
+ $mediaContentDispositionFilename: '@odata.mediaContentDispositionFilename', // > not supported by okra
21
+ $mediaContentDispositionType: '@odata.mediaContentDispositionType', // > not supported by okra
22
+ $mediaEtag: '@odata.mediaEtag'
23
+ }
24
+
25
+ const _cleanupMetadata = (odataResult, result) => {
26
+ if (typeof result !== 'object') return odataResult
27
+
28
+ const keysToCleanup = {
29
+ // do not set "@odata.context" as it may be inherited of remote service
30
+ $context: true,
31
+ // REVISIT: okra doesn't support content disposition
32
+ $mediaContentDispositionFilename: true,
33
+ $mediaContentDispositionType: true
34
+ }
35
+
36
+ for (const key in METADATA) {
37
+ if (!(key in result)) continue
38
+ if (!keysToCleanup[key]) {
39
+ odataResult[METADATA[key]] = result[key]
40
+ }
41
+ delete result[key]
42
+ }
43
+
44
+ return odataResult
45
+ }
46
+
47
+ const _setContext = (odataResult, info, isCollection) => {
48
+ if (info && info.metadata) {
49
+ const result = isCollection || info.metadata.propertyName ? odataResult : odataResult.value
50
+
51
+ if (result != null) Object.assign(result, { [METADATA.$context]: info.metadata.contextUrl })
52
+ }
53
+ return odataResult
54
+ }
55
+
56
+ /**
57
+ * Convert any result to the result object structure, which is expected of odata-v4.
58
+ *
59
+ * @param {*} result
60
+ * @param {*} [info]
61
+ * @returns {string | object}
62
+ */
63
+ const toODataResult = (result, info) => {
64
+ if (result == null) return ''
65
+
66
+ let propertyName, isCollection
67
+ if (info) {
68
+ propertyName = info.metadata.propertyName
69
+ isCollection = info.metadata.isCollection
70
+ }
71
+
72
+ if (isCollection && !Array.isArray(result)) result = [result]
73
+ else if (!isCollection && Array.isArray(result)) result = result[0]
74
+
75
+ let value = result
76
+ if (typeof result === 'object') {
77
+ if (propertyName) value = result[propertyName]
78
+ }
79
+
80
+ const odataResult = _cleanupMetadata({ value }, result)
81
+
82
+ // REVISIT: Support exponential decimals header
83
+ // REVISIT: we always assume minimal metadata right now
84
+ _setContext(odataResult, info, isCollection)
85
+
86
+ if (!isCollection && !propertyName) return odataResult.value
87
+
88
+ return odataResult
89
+ }
90
+
91
+ module.exports = { toODataResult }
@@ -16,45 +16,39 @@ const generateEtag = s => {
16
16
 
17
17
  module.exports = srv =>
18
18
  function service_document(req, res, next) {
19
- if (req.path === '/') {
20
- if (req.method === 'HEAD') return res.end()
21
- if (req.method !== 'GET')
22
- return res
23
- .status(405)
24
- .json({
25
- error: { code: 'METHOD_NOT_ALLOWED', message: `Method ${req.method} not allowed for service document.` }
26
- })
27
-
28
- const m = cds.context.model || cds.model
29
- const csnService = (cds.context.model || cds.model).definitions[srv.name]
30
-
31
- if (req.headers['if-none-match']) {
32
- if (csnService.srvDocEtag) {
33
- const unchanged = validate_etag(req.headers['if-none-match'], csnService.srvDocEtag)
34
- if (unchanged) {
35
- res.set('Etag', csnService.srvDocEtag)
36
- return res.status(304).end()
37
- }
19
+ if (req.method === 'HEAD') return res.end()
20
+ if (req.method !== 'GET')
21
+ return res.status(405).json({
22
+ error: { code: 'METHOD_NOT_ALLOWED', message: `Method ${req.method} not allowed for service document.` }
23
+ })
24
+
25
+ const m = cds.context.model || cds.model
26
+ const csnService = (cds.context.model || cds.model).definitions[srv.name]
27
+
28
+ if (req.headers['if-none-match']) {
29
+ if (csnService.srvDocEtag) {
30
+ const unchanged = validate_etag(req.headers['if-none-match'], csnService.srvDocEtag)
31
+ if (unchanged) {
32
+ res.set('Etag', csnService.srvDocEtag)
33
+ return res.status(304).end()
38
34
  }
39
35
  }
40
-
41
- const srvEntities = m.childrenOf(srv.name)
42
- // REVISIT: How to identify the exposed entities? api.ignore, autoexposed, ...
43
- const exposedEntities = Object.keys(srvEntities).filter(
44
- e => !srvEntities[e]['@cds.api.ignore'] && e !== 'DraftAdministrativeData'
45
- )
46
-
47
- csnService.srvDocEtag = generateEtag(JSON.stringify(exposedEntities))
48
- res.set('Etag', csnService.srvDocEtag)
49
- return res.json({
50
- '@odata.context': `$metadata`,
51
- '@odata.metadataEtag': csnService.srvDocEtag,
52
- value: exposedEntities.map(e => {
53
- const e_ = e.replace(/\./g, '_')
54
- return { name: e_, url: e_ }
55
- })
56
- })
57
36
  }
58
37
 
59
- return next()
38
+ const srvEntities = m.childrenOf(srv.name)
39
+ // REVISIT: How to identify the exposed entities? api.ignore, autoexposed, ...
40
+ const exposedEntities = Object.keys(srvEntities).filter(
41
+ e => !srvEntities[e]['@cds.api.ignore'] && e !== 'DraftAdministrativeData'
42
+ )
43
+
44
+ csnService.srvDocEtag = generateEtag(JSON.stringify(exposedEntities))
45
+ res.set('Etag', csnService.srvDocEtag)
46
+ return res.json({
47
+ '@odata.context': `$metadata`,
48
+ '@odata.metadataEtag': csnService.srvDocEtag,
49
+ value: exposedEntities.map(e => {
50
+ const e_ = e.replace(/\./g, '_')
51
+ return { name: e_, url: e_ }
52
+ })
53
+ })
60
54
  }
@@ -153,9 +153,10 @@ const _v4 = (val, element) => {
153
153
  }
154
154
  }
155
155
 
156
- const formatVal = (val, elementName, csnTarget, kind, func) => {
156
+ const formatVal = (val, elementName, csnTarget, kind, func, literal) => {
157
157
  if (val === null || val === 'null') return 'null'
158
158
  if (typeof val === 'boolean') return val
159
+ if (typeof val === 'string' && literal === 'number' ) return `${val}`
159
160
  if (typeof val === 'string') {
160
161
  if (!csnTarget && UUID.test(val)) return kind === 'odata-v2' ? `guid'${val}'` : val
161
162
  if (func in MATH_FUNC) return val
@@ -12,6 +12,7 @@ const { isStandardError } = require('../_runtime/common/error/standardError')
12
12
  const cdsUser = 'cds.internal.user'
13
13
  const $messageProcessorRegistered = Symbol('message processor registered')
14
14
  const $outboxed = Symbol('outboxed')
15
+ const $unboxed = Symbol('unboxed')
15
16
 
16
17
  const _get100NanosecondTimestampISOString = () => {
17
18
  const [now, nanoseconds] = [new Date(), process.hrtime()[1]]
@@ -159,7 +160,7 @@ const processMessages = async (service, tenant, _opts = {}) => {
159
160
  }
160
161
  }
161
162
 
162
- const process = service.outbox?.process || this.process || processDefault
163
+ const process = service.options.outbox?.process || this.process || processDefault
163
164
  const toBeDeleted = []
164
165
  const toBeUpdated = []
165
166
  try {
@@ -256,6 +257,10 @@ const writeInOutbox = async (name, msg, context) => {
256
257
  // onProcess(msgs){ ... }
257
258
  // })
258
259
 
260
+ function unboxed(srv) {
261
+ return srv[$unboxed] || srv
262
+ }
263
+
259
264
  function outboxed(srv, customOpts) {
260
265
  // outbox max. once
261
266
  if (!new.target) {
@@ -263,9 +268,9 @@ function outboxed(srv, customOpts) {
263
268
  if (former) return former
264
269
  }
265
270
 
266
- const originalSrv = srv.immediate || srv
271
+ const originalSrv = srv[$unboxed] || srv
267
272
  const outboxedSrv = Object.create(originalSrv)
268
- outboxedSrv.immediate = originalSrv
273
+ outboxedSrv[$unboxed] = originalSrv
269
274
 
270
275
  if (!new.target) Object.defineProperty(srv, $outboxed, { value: outboxedSrv })
271
276
 
@@ -307,4 +312,4 @@ function outboxed(srv, customOpts) {
307
312
  return outboxedSrv
308
313
  }
309
314
 
310
- module.exports = outboxed
315
+ module.exports = { outboxed, unboxed }
@@ -3,67 +3,35 @@ const cds = require('../_runtime/cds')
3
3
  // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
4
4
  const express = require('express')
5
5
 
6
- const parse = require('./middleware/parse')
7
- const input = require('./middleware/input')
6
+ const parse_factory = require('./middleware/parse')
7
+ const input_factory = require('./middleware/input')
8
8
 
9
- const create = require('./middleware/create')
10
- const read = require('./middleware/read')
11
- const update = require('./middleware/update')
12
- const deleet = require('./middleware/delete')
13
- const operation = require('./middleware/operation')
14
- const payload = require('./middleware/payload')
9
+ const create_factory = require('./middleware/create')
10
+ const read_factory = require('./middleware/read')
11
+ const update_factory = require('./middleware/update')
12
+ const delete_factory = require('./middleware/delete')
13
+ const operation_factory = require('./middleware/operation')
14
+ const payload_factory = require('./middleware/payload')
15
15
 
16
- const error = require('./middleware/error')
17
- const { alias2ref } = require('../_runtime/common/utils/csn')
18
- const { bufferToBase64 } = require('../_runtime/common/utils/binary')
16
+ const error_factory = require('./middleware/error')
19
17
 
18
+ const { bufferToBase64 } = require('../_runtime/common/utils/binary')
19
+ const { alias2ref } = require('../_runtime/common/utils/csn')
20
20
  const { getAccessRestrictions } = require('../_runtime/common/utils/restrictions')
21
21
 
22
22
  const RestAdapter = function (srv) {
23
- alias2ref(srv) // REVISIT: that's an anti pattern in new prototocol adapter setups
23
+ const parse = parse_factory(srv)
24
+ const input = input_factory(srv)
25
+ const payload = payload_factory(srv)
24
26
 
25
- const accessRestrictions = getAccessRestrictions(srv)
27
+ alias2ref(srv)
26
28
 
27
29
  const router = express.Router()
28
30
 
29
- // pass srv-related stuff to middlewares via req
30
- router.use((req, res, next) => {
31
- req._srv = srv // FIXME: That's only because of how we organized our rest adapater code into fragmented files -> don't do that
32
-
33
- // cds/libx/rest/middleware/create.js:
34
- // 12: const { _srv: srv, _query: query, _target, _data } = _req
35
-
36
- // cds/libx/rest/middleware/delete.js:
37
- // 4: const { _srv: srv, _query: query, _target, _params } = _req
38
-
39
- // cds/libx/rest/middleware/error.js:
40
- // 40: const { _srv: srv } = req
41
-
42
- // cds/libx/rest/middleware/input.js:
43
- // 40: const { _srv, _query, _data, _operation } = req
44
- // 43: if (!(_data && _srv && definition)) return next()
45
- // 47: const template = getTemplate(_cache(req), _srv, definition, { pick: _picker(req) })
46
-
47
- // cds/libx/rest/middleware/operation.js:
48
- // 7: const { _srv: srv, _query: query, _operation: operation, _data: data } = _req
49
-
50
- // cds/libx/rest/middleware/parse.js:
51
- // 10: const { _srv: service } = req
52
-
53
- // cds/libx/rest/middleware/read.js:
54
- // 4: const { _srv: srv, _query: query, _target, _params } = _req
55
-
56
- // cds/libx/rest/middleware/update.js:
57
- // 11: let { _srv: srv, _query: query, _target, _data, _params } = _req
58
-
59
- next()
60
- })
61
-
62
- // req.user + req.tenant -> cds.context = { user, tenant }
63
- // NOT ALLOWED: cds.context.user = 'me'
64
- // req.requiresLogin() -> login -> redirect to referrer
65
-
31
+ // -----------------------------------------------------------------------------------------
66
32
  // check @requires as soon as possible (DoS)
33
+ //
34
+ const accessRestrictions = getAccessRestrictions(srv)
67
35
  router.use((req, res, next) => {
68
36
  // ensure there always is a user going forward (not always the case with old or custom auth)
69
37
  if (!req.user) req.user = new cds.User.default()
@@ -85,6 +53,7 @@ const RestAdapter = function (srv) {
85
53
 
86
54
  // -----------------------------------------------------------------------------------------
87
55
  // service root
56
+ //
88
57
  router.head('/', (_, res) => res.json({}))
89
58
  router.get('/', (_, res) =>
90
59
  res.json({
@@ -94,7 +63,7 @@ const RestAdapter = function (srv) {
94
63
 
95
64
  // -----------------------------------------------------------------------------------------
96
65
  // parse / validate
97
-
66
+ //
98
67
  // content-type check
99
68
  router.use((req, res, next) => {
100
69
  // REVISIT: move that into parse function
@@ -112,6 +81,8 @@ const RestAdapter = function (srv) {
112
81
  throw cds.error(`INVALID_${req.method}`, { statusCode: 400, code: '400' }) // FIXME: better i18n + use res.status
113
82
  }
114
83
 
84
+ // REVISIT: empty object length is not 0
85
+ // REVISIT: also check for POST?
115
86
  // check for empty payload body
116
87
  if (req.headers['content-length'] === '0') {
117
88
  res.status(400).json({ error: { message: 'Malformed patch document', statusCode: 400, code: '400' } })
@@ -129,6 +100,7 @@ const RestAdapter = function (srv) {
129
100
 
130
101
  // -----------------------------------------------------------------------------------------
131
102
  // begin tx
103
+ //
132
104
  router.use((req, res, next) => {
133
105
  // REVISIT: -> move to actual handler(s)
134
106
  const tenant = req.tenant || req.user?.tenant
@@ -140,23 +112,49 @@ const RestAdapter = function (srv) {
140
112
  // -----------------------------------------------------------------------------------------
141
113
  // Actual handlers for HEAD, GET, PUT, POST, PATCH, DELETE
142
114
  //
143
- router.head('/*', (req, res, next) => {
144
- read(req, res, next) // REVISIT: HEAD is doing a full read ?
145
- })
146
- router.post('/*', (req, res, next) => {
147
- if (req._operation) operation(req, res, next)
148
- else create(req, res, next)
149
- })
150
- router.get('/*', (req, res, next) => {
151
- if (req._operation) operation(req, res, next)
152
- else read(req, res, next)
115
+ const operation = operation_factory(srv)
116
+ const create = create_factory(srv)
117
+ const read = read_factory(srv)
118
+ const update = update_factory(srv)
119
+ const deleet = delete_factory(srv)
120
+ router.use(async (req, res, next) => {
121
+ try {
122
+ let result, status, location
123
+
124
+ if (req._operation) {
125
+ // actions and functions
126
+ ;({ result, status } = await operation(req, res))
127
+ } else {
128
+ // CRUD
129
+ switch (req.method) {
130
+ case 'POST':
131
+ ;({ result, status, location } = await create(req))
132
+ break
133
+ case 'HEAD':
134
+ case 'GET':
135
+ ;({ result, status } = await read(req))
136
+ break
137
+ case 'PUT':
138
+ case 'PATCH':
139
+ ;({ result, status } = await update(req))
140
+ break
141
+
142
+ case 'DELETE':
143
+ ;({ result, status } = await deleet(req))
144
+ break
145
+ }
146
+ }
147
+
148
+ req._result = { result, status, location }
149
+ return next()
150
+ } catch (e) {
151
+ next(e)
152
+ }
153
153
  })
154
- router.put('/*', update)
155
- router.patch('/*', update)
156
- router.delete('/*', (req, res, next) => deleet(req, res).then(next).catch(next))
157
154
 
158
155
  // -----------------------------------------------------------------------------------------
159
156
  // end tx (i.e., commit or rollback)
157
+ //
160
158
  router.use(async (req, res, next) => {
161
159
  const { result, status, location } = req._result // REVISIT: Ugly voodoo _req._result channel -> eliminate
162
160
 
@@ -170,10 +168,12 @@ const RestAdapter = function (srv) {
170
168
  // if authentication or something else within the processing of a cds.Request terminates the request, no need to continue
171
169
  if (res.headersSent) return
172
170
 
173
-
174
171
  // convert binaries
175
172
  let definition = req._operation || req._query.__target
176
- if (typeof definition === 'string') definition = srv.model.definitions[definition] || srv.model.definitions[definition.split(':$:')[0]].actions[definition.split(':$:')[1]]
173
+ if (typeof definition === 'string')
174
+ definition =
175
+ srv.model.definitions[definition] ||
176
+ srv.model.definitions[definition.split(':$:')[0]].actions[definition.split(':$:')[1]]
177
177
  if (result && srv && definition) bufferToBase64(result, srv, definition)
178
178
 
179
179
  // only set status if not yet modified
@@ -193,6 +193,7 @@ const RestAdapter = function (srv) {
193
193
 
194
194
  // -----------------------------------------------------------------------------------------
195
195
  // error handling
196
+ //
196
197
  router.use(async (err, req, res, next) => {
197
198
  // REVISIT: should not be neccessary!
198
199
  // request may fail during processing or during commit -> both caught here
@@ -204,7 +205,7 @@ const RestAdapter = function (srv) {
204
205
  })
205
206
 
206
207
  if (!cds.env.features.rest_error_handler) {
207
- router.use(error) // FIXME: nope -> call next()
208
+ router.use(error_factory(srv)) // FIXME: nope -> call next()
208
209
  }
209
210
 
210
211
  return router
@@ -8,35 +8,29 @@ const _error4 = rejected =>
8
8
  ? Object.assign(new Error('MULTIPLE_ERRORS'), { details: rejected.map(r => r.reason) })
9
9
  : rejected[0].reason
10
10
 
11
- module.exports = async (_req, _res, next) => {
12
- const { _srv: srv, _query: query, _target, _data, _params } = _req
11
+ module.exports = srv => async _req => {
12
+ const { _query: query, _target, _data, _params } = _req
13
13
 
14
14
  let result, location
15
15
 
16
- // unfortunately, express doesn't catch async errors -> try catch needed
17
- try {
18
- // add the data
19
- query.entries(_data)
20
- if (query.INSERT.entries.length > 1) {
21
- // > batch insert
22
- const reqs = query.INSERT.entries.map(
23
- entry => new RestRequest({ query: INSERT.into(query.INSERT.into).entries(entry), _target, params: _params })
24
- )
25
- const ress = await Promise.allSettled(reqs.map(req => srv.dispatch(req)))
26
- const rejected = ress.filter(r => r.status === 'rejected')
27
- if (rejected.length) throw _error4(rejected)
28
- result = ress.map(r => r.value)
29
- } else {
30
- // > single insert
31
- const req = new RestRequest({ query, _target, params: _params })
32
- result = await srv.dispatch(req)
33
- location = `../${req.entity.replace(srv.name + '.', '')}` // REVISIT: Is it guaranteed that the GET works? Why do we need relative urls?
34
- for (const k in req.target.keys) location += `/${result[k]}`
35
- }
36
- } catch (e) {
37
- return next(e)
16
+ // add the data
17
+ query.entries(_data)
18
+ if (query.INSERT.entries.length > 1) {
19
+ // > batch insert
20
+ const reqs = query.INSERT.entries.map(
21
+ entry => new RestRequest({ query: INSERT.into(query.INSERT.into).entries(entry), _target, params: _params })
22
+ )
23
+ const ress = await Promise.allSettled(reqs.map(req => srv.dispatch(req)))
24
+ const rejected = ress.filter(r => r.status === 'rejected')
25
+ if (rejected.length) throw _error4(rejected)
26
+ result = ress.map(r => r.value)
27
+ } else {
28
+ // > single insert
29
+ const req = new RestRequest({ query, _target, params: _params })
30
+ result = await srv.dispatch(req)
31
+ location = `../${req.entity.replace(srv.name + '.', '')}` // REVISIT: Is it guaranteed that the GET works? Why do we need relative urls?
32
+ for (const k in req.target.keys) location += `/${result[k]}`
38
33
  }
39
34
 
40
- _req._result = { result, status: 201, location } // REVISIT: Do a res.status(201).send(...) instead
41
- next()
35
+ return { result, status: 201, location }
42
36
  }
@@ -1,12 +1,14 @@
1
1
  const RestRequest = require('../RestRequest')
2
2
 
3
- module.exports = async _req => {
4
- const { _srv: srv, _query: query, _target, _data, _params } = _req
3
+ module.exports = srv => async _req => {
4
+ const { _query: query, _target, _data, _params } = _req
5
5
 
6
6
  const req = new RestRequest({ query, _target, params: _params, data: _data })
7
+
7
8
  // req.data is filled with keys during read and delete
8
9
  if (_params) req.data = Object.assign(_data, _params[_params.length - 1]) // REVISIT: We should avoid that!
9
10
 
10
11
  await srv.dispatch(req)
11
- _req._result = { result: null, status: 204 } // REVISIT: Ugly voodoo _re._result channel -> eliminate
12
+
13
+ return { result: null, status: 204 }
12
14
  }
@@ -12,6 +12,7 @@ const i18n = (...args) => {
12
12
  const { normalizeError, isClientError } = require('../../_runtime/common/error/frontend')
13
13
 
14
14
  const _log = err => {
15
+ // REVISIT: how does level behave compared to _log in (legacy) odata adapter?
15
16
  const level = isClientError(err) ? 'warn' : 'error'
16
17
  if ((level === 'warn' && !LOG._warn) || (level === 'error' && !LOG._error)) return
17
18
 
@@ -36,9 +37,7 @@ const _log = err => {
36
37
  }
37
38
 
38
39
  // eslint-disable-next-line no-unused-vars
39
- module.exports = (err, req, res, next) => {
40
- const { _srv: srv } = req
41
-
40
+ module.exports = srv => (err, req, res, next) => {
42
41
  // REVISIT: invoking service.on('error') handlers needs a cleanup with new protocol adapters!!!
43
42
  // invoke srv.on('error', function (err, req) { ... }) here in special situations
44
43
  let ctx = cds.context
@@ -30,16 +30,16 @@ const _processorFn = errors => {
30
30
 
31
31
  const _cache = req => `rest-input;skip-key-validation:${req.method !== 'POST'}`
32
32
 
33
- module.exports = (req, res, next) => {
34
- const { _srv, _query, _data, _operation } = req
33
+ module.exports = srv => (req, res, next) => {
34
+ const { _query, _data, _operation } = req
35
35
  let definition = _operation || _query.__target
36
- if (typeof definition === 'string') definition = _srv.model.definitions[definition] || _srv.model.definitions[definition.split(':$:')[0]].actions[definition.split(':$:')[1]]
36
+ if (typeof definition === 'string') definition = srv.model.definitions[definition] || srv.model.definitions[definition.split(':$:')[0]].actions[definition.split(':$:')[1]]
37
37
 
38
- if (!(_data && _srv && definition)) return next()
38
+ if (!(_data && srv && definition)) return next()
39
39
 
40
40
  const errors = []
41
41
 
42
- const template = getTemplate(_cache(req), _srv, definition, { pick: _picker(req) })
42
+ const template = getTemplate(_cache(req), srv, definition, { pick: _picker(req) })
43
43
  if (template && template.elements.size) {
44
44
  const rows = Array.isArray(_data) ? _data : [_data]
45
45
  for (const row of rows) {