@sap/cds 7.4.2 → 7.5.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.
- package/CHANGELOG.md +100 -0
- package/apis/cds.d.ts +1 -38
- package/apis/core.d.ts +21 -101
- package/apis/cqn.d.ts +18 -76
- package/apis/csn.d.ts +18 -114
- package/apis/events.d.ts +16 -123
- package/apis/internal/inference.d.ts +18 -32
- package/apis/linked.d.ts +18 -97
- package/apis/log.d.ts +19 -164
- package/apis/models.d.ts +18 -180
- package/apis/ql.d.ts +16 -323
- package/apis/reflect.d.ts +32 -0
- package/apis/server.d.ts +18 -135
- package/apis/services.d.ts +18 -380
- package/bin/cds-serve.js +5 -2
- package/bin/serve.js +7 -16
- package/lib/auth/basic-auth.js +3 -1
- package/lib/auth/ias-auth.js +62 -48
- package/lib/auth/ias-claims.js +34 -0
- package/lib/auth/index.js +55 -33
- package/lib/auth/jwt-auth.js +55 -52
- package/lib/compile/cdsc.js +2 -2
- package/lib/compile/to/edm.js +4 -4
- package/lib/compile/to/hdbtabledata.js +5 -8
- package/lib/compile/to/srvinfo.js +2 -2
- package/lib/env/cds-env.js +3 -9
- package/lib/env/cds-requires.js +16 -17
- package/lib/env/compat.js +0 -9
- package/lib/env/defaults.js +17 -6
- package/lib/i18n/localize.js +46 -42
- package/lib/index.js +6 -8
- package/lib/linked/classes.js +7 -118
- package/lib/linked/entities.js +1 -1
- package/lib/log/cds-log.js +15 -10
- package/lib/log/format/aspects/als.js +41 -0
- package/lib/log/format/aspects/cf.js +36 -0
- package/lib/log/format/json.js +96 -0
- package/lib/plugins.js +7 -3
- package/lib/req/context.js +4 -2
- package/lib/srv/cds-connect.js +3 -5
- package/lib/srv/cds-serve.js +13 -26
- package/lib/srv/factory.js +3 -3
- package/lib/srv/middlewares/index.js +0 -2
- package/lib/srv/middlewares/trace.js +2 -3
- package/lib/srv/protocols/_legacy.js +27 -30
- package/lib/srv/protocols/index.js +173 -58
- package/lib/srv/protocols/odata-v4.js +29 -16
- package/lib/srv/srv-api.js +8 -13
- package/lib/srv/srv-handlers.js +14 -14
- package/lib/utils/cds-utils.js +15 -0
- package/libx/_runtime/auth/index.js +4 -5
- package/libx/_runtime/auth/strategies/basic.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +23 -13
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +6 -15
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +10 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +5 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +2 -1
- package/libx/_runtime/cds-services/services/utils/columns.js +3 -9
- package/libx/_runtime/cds.js +13 -0
- package/libx/_runtime/common/composition/data.js +3 -0
- package/libx/_runtime/common/composition/delete.js +1 -1
- package/libx/_runtime/common/error/frontend.js +2 -2
- package/libx/_runtime/common/generic/auth/readOnly.js +1 -1
- package/libx/_runtime/common/generic/auth/restrictions.js +1 -1
- package/libx/_runtime/common/generic/sorting.js +4 -5
- package/libx/_runtime/common/utils/csn.js +23 -18
- package/libx/_runtime/common/utils/restrictions.js +6 -15
- package/libx/_runtime/db/generic/input.js +3 -2
- package/libx/_runtime/fiori/generic/readOverDraft.js +2 -5
- package/libx/_runtime/fiori/lean-draft.js +69 -5
- package/libx/_runtime/hana/Service.js +1 -1
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +1 -1
- package/libx/_runtime/messaging/Outbox.js +3 -8
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -0
- package/libx/_runtime/messaging/enterprise-messaging.js +1 -1
- package/libx/_runtime/messaging/file-based.js +1 -1
- package/libx/_runtime/messaging/service.js +7 -10
- package/libx/_runtime/remote/Service.js +15 -45
- package/libx/_runtime/remote/utils/client.js +20 -33
- package/libx/_runtime/remote/utils/cloudSdkProvider.js +30 -0
- package/libx/_runtime/sqlite/Service.js +2 -2
- package/libx/odata/afterburner.js +29 -21
- package/libx/odata/cqn2odata.js +1 -1
- package/libx/odata/error.js +7 -0
- package/libx/odata/grammar.peggy +16 -20
- package/libx/odata/metadata.js +73 -78
- package/libx/odata/parser.js +1 -1
- package/libx/odata/read.js +94 -0
- package/libx/odata/result.js +91 -0
- package/libx/odata/service-document.js +31 -37
- package/libx/odata/utils.js +2 -1
- package/libx/outbox/index.js +9 -4
- package/libx/rest/RestAdapter.js +68 -67
- package/libx/rest/middleware/create.js +20 -26
- package/libx/rest/middleware/delete.js +5 -3
- package/libx/rest/middleware/error.js +2 -3
- package/libx/rest/middleware/input.js +5 -5
- package/libx/rest/middleware/operation.js +96 -41
- package/libx/rest/middleware/parse.js +4 -6
- package/libx/rest/middleware/payload.js +5 -5
- package/libx/rest/middleware/read.js +11 -17
- package/libx/rest/middleware/update.js +20 -25
- package/package.json +2 -1
- package/server.js +7 -4
- package/srv/outbox.cds +9 -10
- package/apis/env.d.ts +0 -25
- package/apis/test.d.ts +0 -81
- package/apis/utils.d.ts +0 -15
- package/lib/auth/passport-basic.js +0 -14
- package/lib/auth/passport-digest.js +0 -16
- package/lib/env/presets.js +0 -35
- package/lib/log/format/cf.js +0 -16
- package/lib/log/format/kibana.js +0 -92
- package/lib/srv/middlewares/ctx-auth.js +0 -11
- 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.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
}
|
package/libx/odata/utils.js
CHANGED
|
@@ -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
|
package/libx/outbox/index.js
CHANGED
|
@@ -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
|
|
271
|
+
const originalSrv = srv[$unboxed] || srv
|
|
267
272
|
const outboxedSrv = Object.create(originalSrv)
|
|
268
|
-
outboxedSrv
|
|
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 }
|
package/libx/rest/RestAdapter.js
CHANGED
|
@@ -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
|
|
7
|
-
const
|
|
6
|
+
const parse_factory = require('./middleware/parse')
|
|
7
|
+
const input_factory = require('./middleware/input')
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
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
|
|
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
|
-
|
|
23
|
+
const parse = parse_factory(srv)
|
|
24
|
+
const input = input_factory(srv)
|
|
25
|
+
const payload = payload_factory(srv)
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
alias2ref(srv)
|
|
26
28
|
|
|
27
29
|
const router = express.Router()
|
|
28
30
|
|
|
29
|
-
//
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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')
|
|
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(
|
|
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
|
|
12
|
-
const {
|
|
11
|
+
module.exports = srv => async _req => {
|
|
12
|
+
const { _query: query, _target, _data, _params } = _req
|
|
13
13
|
|
|
14
14
|
let result, location
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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 &&
|
|
38
|
+
if (!(_data && srv && definition)) return next()
|
|
39
39
|
|
|
40
40
|
const errors = []
|
|
41
41
|
|
|
42
|
-
const template = getTemplate(_cache(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) {
|