@sap/cds 7.8.2 → 7.9.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 +45 -0
- package/_i18n/i18n_ar.properties +3 -0
- package/_i18n/i18n_cs.properties +3 -0
- package/_i18n/i18n_da.properties +3 -0
- package/_i18n/i18n_es_MX.properties +3 -0
- package/_i18n/i18n_fi.properties +3 -0
- package/_i18n/i18n_hu.properties +6 -0
- package/_i18n/i18n_ko.properties +3 -0
- package/_i18n/i18n_ms.properties +3 -0
- package/_i18n/i18n_nl.properties +3 -0
- package/_i18n/i18n_no.properties +3 -0
- package/_i18n/i18n_ro.properties +3 -0
- package/_i18n/i18n_sv.properties +3 -0
- package/_i18n/i18n_th.properties +3 -0
- package/_i18n/i18n_tr.properties +6 -0
- package/_i18n/i18n_zh_TW.properties +3 -0
- package/bin/serve.js +5 -5
- package/lib/auth/basic-auth.js +1 -1
- package/lib/compile/cdsc.js +33 -6
- package/lib/compile/etc/_localized.js +14 -7
- package/lib/compile/for/lean_drafts.js +9 -0
- package/lib/compile/to/edm-files.js +116 -0
- package/lib/compile/to/edm.js +8 -1
- package/lib/compile/to/hdbtabledata.js +3 -3
- package/lib/compile/to/sql.js +4 -2
- package/lib/compile/to/srvinfo.js +6 -5
- package/lib/compile/to/yaml.js +22 -21
- package/lib/dbs/cds-deploy.js +5 -6
- package/lib/env/cds-env.js +7 -0
- package/lib/env/cds-requires.js +20 -1
- package/lib/env/defaults.js +21 -5
- package/lib/env/schemas/cds-package.js +1 -1
- package/lib/env/schemas/cds-rc.js +85 -4
- package/lib/index.js +1 -1
- package/lib/linked/entities.js +10 -0
- package/lib/linked/models.js +1 -1
- package/lib/plugins.js +1 -1
- package/lib/ql/INSERT.js +17 -3
- package/lib/ql/Query.js +4 -0
- package/lib/ql/infer.js +1 -1
- package/lib/req/request.js +1 -1
- package/lib/srv/cds-serve.js +1 -0
- package/lib/srv/middlewares/cds-context.js +1 -1
- package/lib/srv/protocols/odata-v4.js +5 -6
- package/lib/srv/srv-models.js +9 -2
- package/lib/utils/cds-test.js +2 -0
- package/lib/utils/cds-utils.js +9 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +38 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
- package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
- package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
- package/libx/_runtime/common/generic/auth/index.js +2 -0
- package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
- package/libx/_runtime/common/generic/auth/restrict.js +6 -5
- package/libx/_runtime/common/generic/auth/utils.js +1 -1
- package/libx/_runtime/common/generic/crud.js +5 -8
- package/libx/_runtime/common/generic/etag.js +8 -6
- package/libx/_runtime/common/generic/sorting.js +2 -2
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
- package/libx/_runtime/common/utils/compareJson.js +274 -0
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
- package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
- package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
- package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
- package/libx/_runtime/common/utils/resolveView.js +0 -16
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
- package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/streamProp.js +9 -2
- package/libx/_runtime/common/utils/ucsn.js +1 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
- package/libx/_runtime/db/generic/rewrite.js +7 -13
- package/libx/_runtime/fiori/generic/activate.js +1 -1
- package/libx/_runtime/fiori/generic/edit.js +1 -1
- package/libx/_runtime/fiori/generic/prepare.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +151 -46
- package/libx/_runtime/fiori/utils/handler.js +1 -1
- package/libx/_runtime/hana/execute.js +6 -2
- package/libx/_runtime/hana/pool.js +3 -0
- package/libx/_runtime/hana/search2cqn4sql.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
- package/libx/_runtime/messaging/event-broker.js +212 -0
- package/libx/_runtime/remote/Service.js +9 -32
- package/libx/_runtime/remote/utils/client.js +13 -21
- package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
- package/libx/_runtime/sqlite/execute.js +8 -3
- package/libx/_runtime/ucl/Service.js +259 -0
- package/libx/common/assert/index.js +5 -11
- package/libx/common/assert/validation.js +6 -1
- package/libx/odata/index.js +47 -25
- package/libx/odata/middleware/batch.js +8 -7
- package/libx/odata/middleware/create.js +42 -16
- package/libx/odata/middleware/delete.js +18 -11
- package/libx/odata/middleware/metadata.js +15 -14
- package/libx/odata/middleware/operation.js +30 -40
- package/libx/odata/middleware/parse.js +2 -3
- package/libx/odata/middleware/read.js +59 -52
- package/libx/odata/middleware/service-document.js +7 -7
- package/libx/odata/middleware/stream.js +26 -24
- package/libx/odata/middleware/update.js +53 -92
- package/libx/odata/parse/afterburner.js +45 -47
- package/libx/odata/parse/grammar.peggy +3 -3
- package/libx/odata/parse/multipartToJson.js +10 -22
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/etag.js +13 -0
- package/libx/odata/utils/handler.js +120 -0
- package/libx/odata/utils/index.js +15 -2
- package/libx/odata/utils/metaInfo.js +410 -0
- package/libx/odata/utils/path.js +5 -2
- package/libx/odata/utils/readAfterWrite.js +23 -0
- package/libx/odata/utils/result.js +4 -5
- package/libx/rest/RestAdapter.js +4 -13
- package/libx/rest/middleware/parse.js +40 -7
- package/package.json +1 -1
- package/server.js +1 -0
- package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
- package/libx/_runtime/common/utils/thenable.js +0 -51
- package/libx/_runtime/rest/service.js +0 -2
- package/libx/odata/parse/parseToCqn.js +0 -39
- package/libx/rest/middleware/input.js +0 -54
- package/libx/rest/middleware/payload.js +0 -13
|
@@ -2,13 +2,18 @@ const cds = require('../../../')
|
|
|
2
2
|
const { INSERT } = cds.ql
|
|
3
3
|
|
|
4
4
|
const { toODataResult, postProcess } = require('../utils/result')
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
calculateLocationHeader,
|
|
7
|
+
getKeysAndParamsFromPath,
|
|
8
|
+
handleSapMessages,
|
|
9
|
+
getPreferReturnHeader
|
|
10
|
+
} = require('../utils')
|
|
11
|
+
const { getDeepSelect, getSimpleSelectCQN } = require('../utils/handler')
|
|
6
12
|
|
|
7
13
|
const { deepCopy } = require('../../_runtime/common/utils/copy')
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
|
|
15
|
+
const readAfterWrite = require('../utils/readAfterWrite')
|
|
16
|
+
const metaInfo = require('../utils/metaInfo')
|
|
12
17
|
|
|
13
18
|
module.exports = srv =>
|
|
14
19
|
function create(req, res, next) {
|
|
@@ -17,12 +22,12 @@ module.exports = srv =>
|
|
|
17
22
|
target
|
|
18
23
|
} = req._query
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)
|
|
25
|
+
// req.__proto__.method is set in case of upsert
|
|
26
|
+
const isUpsert = req.__proto__.method in { PUT: 1, PATCH: 1 }
|
|
27
|
+
|
|
28
|
+
if (one && !isUpsert) {
|
|
29
|
+
const msg = 'Method POST is not allowed for singletons and individual entities'
|
|
30
|
+
throw Object.assign(new Error(msg), { statusCode: 405 })
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
// payload & params
|
|
@@ -42,10 +47,17 @@ module.exports = srv =>
|
|
|
42
47
|
// query
|
|
43
48
|
const query = INSERT.into(from).entries(data)
|
|
44
49
|
|
|
50
|
+
// cdsReq.headers should contain merged headers of envelope and subreq
|
|
51
|
+
const headers = { ...cds.context.http.req.headers, ...req.headers }
|
|
52
|
+
|
|
45
53
|
// we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
|
|
46
|
-
const cdsReq = new cds.Request({ query, params, req, res })
|
|
54
|
+
const cdsReq = new cds.Request({ query, params, headers, req, res })
|
|
47
55
|
Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
|
|
48
56
|
|
|
57
|
+
// API for subrequests of $batch (or incoming request)
|
|
58
|
+
cdsReq.req = req
|
|
59
|
+
cdsReq.res = res
|
|
60
|
+
|
|
49
61
|
// rewrite event for draft-enabled entities
|
|
50
62
|
if (target._isDraftEnabled) cdsReq.event = 'NEW'
|
|
51
63
|
|
|
@@ -57,8 +69,12 @@ module.exports = srv =>
|
|
|
57
69
|
return srv.dispatch(cdsReq).then(result => {
|
|
58
70
|
handleSapMessages(cdsReq, req, res)
|
|
59
71
|
|
|
72
|
+
// generic handlers indicate that read after write is required
|
|
60
73
|
if (cdsReq._.readAfterWrite) {
|
|
61
|
-
|
|
74
|
+
// const keys = cdsReq.target.keys?.filter(k => !k.isAssociation)?.reduce((prev, k) => { prev[k] = result[k]; return prev}, {} )
|
|
75
|
+
// const query = SELECT.one(cdsReq.query.INSERT.into, keys)
|
|
76
|
+
const query = cdsReq.event === 'NEW' ? getSimpleSelectCQN(cdsReq.target, result) : getDeepSelect(cdsReq)
|
|
77
|
+
return readAfterWrite(cdsReq, srv, query)
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
return result
|
|
@@ -66,15 +82,25 @@ module.exports = srv =>
|
|
|
66
82
|
})
|
|
67
83
|
.then(result => {
|
|
68
84
|
// we use an extra then block, after getting the result, so the transaction is commited, before sending the response
|
|
85
|
+
|
|
86
|
+
// determine calculation based on result with req.data as fallback
|
|
87
|
+
if (!target._isSingleton)
|
|
88
|
+
res.set('location', calculateLocationHeader(cdsReq.target, srv, result || cdsReq.data))
|
|
89
|
+
|
|
69
90
|
if (result == null) return res.sendStatus(204)
|
|
70
|
-
const isMinimal = req
|
|
91
|
+
const isMinimal = getPreferReturnHeader(req) === 'minimal'
|
|
71
92
|
postProcess(cdsReq.target, srv, result, isMinimal)
|
|
93
|
+
|
|
72
94
|
if (result['$etag']) res.set('etag', result['$etag'])
|
|
73
|
-
res.set('location', calculateLocationHeader(cdsReq.target, srv, result))
|
|
74
95
|
if (isMinimal) return res.sendStatus(204)
|
|
96
|
+
|
|
75
97
|
const info = metaInfo(query, 'CREATE', srv, result, req)
|
|
76
98
|
result = toODataResult(result, info)
|
|
77
|
-
res.
|
|
99
|
+
res.set('content-type', 'application/json;IEEE754Compatible=true')
|
|
100
|
+
res.status(201).send(result)
|
|
101
|
+
})
|
|
102
|
+
.catch(err => {
|
|
103
|
+
handleSapMessages(cdsReq, req, res)
|
|
104
|
+
next(err)
|
|
78
105
|
})
|
|
79
|
-
.catch(next) // should be outside, so tx can be rolled back in case of errors
|
|
80
106
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
2
|
const { UPDATE, DELETE } = cds.ql
|
|
3
3
|
|
|
4
|
-
const { getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
|
|
4
|
+
const { getKeysAndParamsFromPath, handleSapMessages, getPreferReturnHeader } = require('../utils')
|
|
5
5
|
|
|
6
6
|
module.exports = srv =>
|
|
7
7
|
function deleete(req, res, next) {
|
|
8
|
-
if (req
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
})
|
|
8
|
+
if (getPreferReturnHeader(req)) {
|
|
9
|
+
const msg = "The 'return' preference is not allowed in DELETE requests"
|
|
10
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
// REVISIT: better solution for query._propertyAccess
|
|
@@ -19,8 +18,7 @@ module.exports = srv =>
|
|
|
19
18
|
} = req._query
|
|
20
19
|
|
|
21
20
|
if (!one) {
|
|
22
|
-
|
|
23
|
-
throw Object.assign(new Error('Method DELETE not allowed for ENTITY.COLLECTION'), { statusCode: 405 })
|
|
21
|
+
throw Object.assign(new Error('Method DELETE is not allowed for entity collections'), { statusCode: 405 })
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
// payload & params
|
|
@@ -31,20 +29,29 @@ module.exports = srv =>
|
|
|
31
29
|
// query
|
|
32
30
|
const query = _propertyAccess ? UPDATE(from).set({ [_propertyAccess]: null }) : DELETE.from(from)
|
|
33
31
|
|
|
32
|
+
// cdsReq.headers should contain merged headers of envelope and subreq
|
|
33
|
+
const headers = { ...cds.context.http.req.headers, ...req.headers }
|
|
34
|
+
|
|
34
35
|
// we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
|
|
35
|
-
const cdsReq = new cds.Request({ query, data, params, req, res })
|
|
36
|
+
const cdsReq = new cds.Request({ query, data, headers, params, req, res })
|
|
36
37
|
Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
|
|
37
38
|
|
|
39
|
+
// API for subrequests of $batch (or incoming request)
|
|
40
|
+
cdsReq.req = req
|
|
41
|
+
cdsReq.res = res
|
|
42
|
+
|
|
38
43
|
// rewrite event for draft-enabled entities
|
|
39
44
|
if (target._isDraftEnabled && cdsReq.data.IsActiveEntity === false) cdsReq.event = 'CANCEL'
|
|
40
45
|
|
|
41
46
|
return srv
|
|
42
47
|
.dispatch(cdsReq)
|
|
43
|
-
.then(
|
|
48
|
+
.then(() => {
|
|
44
49
|
handleSapMessages(cdsReq, req, res)
|
|
45
50
|
|
|
46
|
-
if (result === 0) throw Object.assign(new Error('Not found'), { statusCode: 404 })
|
|
47
51
|
res.sendStatus(204)
|
|
48
52
|
})
|
|
49
|
-
.catch(
|
|
53
|
+
.catch(err => {
|
|
54
|
+
handleSapMessages(cdsReq, req, res)
|
|
55
|
+
next(err)
|
|
56
|
+
})
|
|
50
57
|
}
|
|
@@ -51,8 +51,10 @@ const mpSupportsEmptyLocale = () => {
|
|
|
51
51
|
|
|
52
52
|
module.exports = srv =>
|
|
53
53
|
async function metadata(req, res, _next) {
|
|
54
|
-
if (req.method !== 'GET')
|
|
55
|
-
|
|
54
|
+
if (req.method !== 'GET') {
|
|
55
|
+
const msg = `Method ${req.method} is not allowed for calls to the metadata endpoint`
|
|
56
|
+
throw Object.assign(new Error(msg), { statusCode: 405 })
|
|
57
|
+
}
|
|
56
58
|
|
|
57
59
|
const tenant = cds.context.tenant
|
|
58
60
|
const locale = cds.context.locale
|
|
@@ -60,7 +62,7 @@ module.exports = srv =>
|
|
|
60
62
|
|
|
61
63
|
// REVISIT: edm(x) and etag cache is only evicted with model
|
|
62
64
|
const csnService = (cds.context.model || cds.model).definitions[srv.name]
|
|
63
|
-
const metadataCache = (csnService.metadataCache = csnService.metadataCache || { jsonEtag: {}, xmlEtag: {} })
|
|
65
|
+
const metadataCache = (csnService.metadataCache = csnService.metadataCache || { jsonEtag: {}, xmlEtag: {} }) // REVISIT: yet another uncontrolled cache?
|
|
64
66
|
|
|
65
67
|
const etag = format === 'json' ? metadataCache.jsonEtag?.[locale] : metadataCache.xmlEtag?.[locale]
|
|
66
68
|
|
|
@@ -75,7 +77,7 @@ module.exports = srv =>
|
|
|
75
77
|
if (etag) {
|
|
76
78
|
const unchanged = validate_etag(req.headers['if-none-match'], etag)
|
|
77
79
|
if (unchanged) {
|
|
78
|
-
res.set('
|
|
80
|
+
res.set('etag', etag)
|
|
79
81
|
return res.status(304).end()
|
|
80
82
|
}
|
|
81
83
|
}
|
|
@@ -83,11 +85,11 @@ module.exports = srv =>
|
|
|
83
85
|
|
|
84
86
|
const { 'cds.xt.ModelProviderService': mps } = cds.services
|
|
85
87
|
if (mps) {
|
|
86
|
-
if (format === 'json')
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
if (format === 'json') {
|
|
89
|
+
LOG._warn && LOG.warn('JSON metadata is not supported in case of cds.requires.extensibilty: true')
|
|
90
|
+
const msg = 'JSON metadata is not supported for this service'
|
|
91
|
+
throw Object.assign(new Error(msg), { statusCode: 501 })
|
|
92
|
+
}
|
|
91
93
|
|
|
92
94
|
try {
|
|
93
95
|
let edmx
|
|
@@ -105,7 +107,7 @@ module.exports = srv =>
|
|
|
105
107
|
edmx = await mps.getEdmx({ tenant, model: srv.model, service: srv.definition.name, locale })
|
|
106
108
|
}
|
|
107
109
|
metadataCache.xmlEtag[locale] = generateEtag(edmx)
|
|
108
|
-
res.set('
|
|
110
|
+
res.set('content-type', 'application/xml')
|
|
109
111
|
res.send(edmx)
|
|
110
112
|
return
|
|
111
113
|
} catch (e) {
|
|
@@ -113,8 +115,7 @@ module.exports = srv =>
|
|
|
113
115
|
e.message = 'Unable to get EDMX for tenant ' + tenant + ' due to error: ' + e.message
|
|
114
116
|
LOG.error(e)
|
|
115
117
|
}
|
|
116
|
-
|
|
117
|
-
throw Object.assign(new Error('Service Unavailable'), { statusCode: 503 })
|
|
118
|
+
throw Object.assign(new Error('503'), { statusCode: 503 })
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
@@ -132,7 +133,7 @@ module.exports = srv =>
|
|
|
132
133
|
(metadataCache.edmx = cds.compile.to.edmx(srv.model, { service: srv.definition.name }))
|
|
133
134
|
const localized = cds.localize(srv.model, locale, edmx)
|
|
134
135
|
metadataCache.xmlEtag[locale] = generateEtag(localized)
|
|
135
|
-
res.set('
|
|
136
|
-
res.set('
|
|
136
|
+
res.set('etag', metadataCache.xmlEtag[locale])
|
|
137
|
+
res.set('content-type', 'application/xml')
|
|
137
138
|
return res.send(localized)
|
|
138
139
|
}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
2
|
|
|
3
3
|
const { toODataResult, postProcess } = require('../utils/result')
|
|
4
|
-
const { cds2edm,
|
|
4
|
+
const { cds2edm, getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
|
|
5
5
|
|
|
6
6
|
const { deepCopy } = require('../../_runtime/common/utils/copy')
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
const { readAfterWrite } = require('../../_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite')
|
|
10
|
-
const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
|
|
11
|
-
|
|
12
|
-
const DRAFT_EVENTS = { draftActivate: 1, EDIT: 1, draftPrepare: 1 }
|
|
8
|
+
const metaInfo = require('../utils/metaInfo')
|
|
13
9
|
|
|
14
10
|
module.exports = srv =>
|
|
15
11
|
function operation(req, res, next) {
|
|
@@ -46,16 +42,21 @@ module.exports = srv =>
|
|
|
46
42
|
|
|
47
43
|
// event
|
|
48
44
|
// REVISIT: when is operation.name actually prefixed with the service name?
|
|
49
|
-
|
|
50
|
-
// REVISIT: rewrite draft event -> do centrally in draft impl
|
|
51
|
-
if (event === 'draftEdit') event = 'EDIT'
|
|
45
|
+
const event = operation.name.replace(`${srv.name}.`, '')
|
|
52
46
|
|
|
53
47
|
const query = entity ? req._query : undefined
|
|
54
48
|
|
|
49
|
+
// cdsReq.headers should contain merged headers of envelope and subreq
|
|
50
|
+
const headers = { ...cds.context.http.req.headers, ...req.headers }
|
|
51
|
+
|
|
55
52
|
// we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
|
|
56
|
-
const cdsReq = new cds.Request({ query, event, data, params, target: query?.target, req, res })
|
|
53
|
+
const cdsReq = new cds.Request({ query, event, data, params, headers, target: query?.target, req, res })
|
|
57
54
|
Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
|
|
58
55
|
|
|
56
|
+
// API for subrequests of $batch (or incoming request)
|
|
57
|
+
cdsReq.req = req
|
|
58
|
+
cdsReq.res = res
|
|
59
|
+
|
|
59
60
|
// REVISIT: only via srv.run in combination with srv.dispatch inside
|
|
60
61
|
// we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
|
|
61
62
|
// or the auto-managed tx opened for the respective atomicity group, if exists
|
|
@@ -63,41 +64,37 @@ module.exports = srv =>
|
|
|
63
64
|
.run(() => {
|
|
64
65
|
return srv.dispatch(cdsReq).then(result => {
|
|
65
66
|
handleSapMessages(cdsReq, req, res)
|
|
66
|
-
|
|
67
|
-
// FIXME: should be handled in draft impl
|
|
68
|
-
if (event in /* { draftActivate: 1, EDIT: 1 } */ DRAFT_EVENTS /* && cdsReq._.readAfterWrite */) {
|
|
69
|
-
let columns
|
|
70
|
-
const queryOptions = req.url.split('?')[1]
|
|
71
|
-
if (queryOptions) columns = cds.odata.parse(`/X?${queryOptions}`).SELECT.columns
|
|
72
|
-
return readAfterWrite(cdsReq, srv, { operation: { result, returnType: operation.returns }, columns })
|
|
73
|
-
}
|
|
74
|
-
|
|
75
67
|
return result
|
|
76
68
|
})
|
|
77
69
|
})
|
|
78
70
|
.then(result => {
|
|
79
71
|
// we use an extra then block, after getting the result, so the transaction is commited, before sending the response
|
|
72
|
+
|
|
80
73
|
if (!operation.returns || result == null) return res.status(204).end()
|
|
81
74
|
|
|
82
75
|
if (operation.returns._type?.match?.(/^cds\./)) {
|
|
76
|
+
res.set('content-type', 'application/json;IEEE754Compatible=true')
|
|
83
77
|
// TODO: check result type
|
|
84
|
-
return res.
|
|
85
|
-
'@odata.context': `${
|
|
78
|
+
return res.send({
|
|
79
|
+
'@odata.context': `${'../'.repeat(query?.SELECT?.from?.ref?.length)}$metadata#${cds2edm[operation.returns._type]}`,
|
|
86
80
|
value: result
|
|
87
81
|
})
|
|
88
82
|
}
|
|
89
83
|
|
|
90
84
|
const info = metaInfo(req._query, event, srv, result, req)
|
|
91
85
|
|
|
92
|
-
// FIXME: info.metadata.isCollection and contextUrl are incorrect for draft events
|
|
93
|
-
if (event in /* { draftActivate: 1, EDIT: 1 } */ DRAFT_EVENTS) {
|
|
94
|
-
info.metadata.isCollection = false
|
|
95
|
-
info.metadata.contextUrl += '/$entity'
|
|
96
|
-
}
|
|
97
|
-
|
|
98
86
|
// FIXME: info.metadata.isCollection is incorrect
|
|
99
87
|
if (!operation.returns.items) info.metadata.isCollection = false
|
|
100
88
|
|
|
89
|
+
// REVISIT impl of context url generation
|
|
90
|
+
if (
|
|
91
|
+
!info.metadata.isCollection &&
|
|
92
|
+
info.metadata.isServiceEntity &&
|
|
93
|
+
!info.metadata.contextUrl.endsWith('$entity')
|
|
94
|
+
) {
|
|
95
|
+
info.metadata.contextUrl += '/$entity'
|
|
96
|
+
}
|
|
97
|
+
|
|
101
98
|
if (info.metadata.returnType) {
|
|
102
99
|
postProcess(info.metadata.returnType, srv, result)
|
|
103
100
|
if (result['$etag']) res.set('etag', result['$etag'])
|
|
@@ -105,22 +102,15 @@ module.exports = srv =>
|
|
|
105
102
|
|
|
106
103
|
result = toODataResult(result, info)
|
|
107
104
|
|
|
108
|
-
// FIXME: draftActivate needs location header -> move to draft impl
|
|
109
|
-
// FIXME: draftActivate needs HasActiveEntity and HasDraftEntity -> move to draft impl
|
|
110
|
-
if (event in /* { draftActivate: 1, EDIT: 1 } */ DRAFT_EVENTS) {
|
|
111
|
-
res.set('location', '../' + calculateLocationHeader(cdsReq.target, srv, result))
|
|
112
|
-
result.HasDraftEntity = false
|
|
113
|
-
if (event === 'draftActivate' || event === 'draftPrepare') result.HasActiveEntity = false
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// // FIXME: draftEdit needs HasDraftEntity -> move to draft impl
|
|
117
|
-
// if (event === 'EDIT') result.HasDraftEntity = false
|
|
118
|
-
|
|
119
105
|
// FIXME: toODataResult() doesn't seem to handle this case
|
|
120
106
|
if (entity && !result['@odata.context'].match(/^\.\.\//))
|
|
121
107
|
result['@odata.context'] = '../' + result['@odata.context']
|
|
122
108
|
|
|
123
|
-
res.set('
|
|
109
|
+
res.set('content-type', 'application/json;IEEE754Compatible=true')
|
|
110
|
+
res.send(result)
|
|
111
|
+
})
|
|
112
|
+
.catch(err => {
|
|
113
|
+
handleSapMessages(cdsReq, req, res)
|
|
114
|
+
next(err)
|
|
124
115
|
})
|
|
125
|
-
.catch(next)
|
|
126
116
|
}
|
|
@@ -5,6 +5,8 @@ module.exports = srv =>
|
|
|
5
5
|
// REVISIT: can't we register the batch handler before the parse handler to avoid this?
|
|
6
6
|
if (req.path.startsWith('/$batch')) return next()
|
|
7
7
|
|
|
8
|
+
if (req._query) return next() //> already parsed (e.g., upsert)
|
|
9
|
+
|
|
8
10
|
// if not a GET, use req.path instead of req.url to ignore query parameters
|
|
9
11
|
req._query = cds.odata.parse(req.method === 'GET' ? req.url : req.path, {
|
|
10
12
|
service: srv,
|
|
@@ -12,8 +14,5 @@ module.exports = srv =>
|
|
|
12
14
|
strict: true
|
|
13
15
|
})
|
|
14
16
|
|
|
15
|
-
const preferReturn = req.headers.prefer?.match(/\W?return=(\w+)/i)
|
|
16
|
-
if (preferReturn) req._preferReturn = preferReturn[1]
|
|
17
|
-
|
|
18
17
|
next()
|
|
19
18
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
2
|
const { toODataResult, postProcess } = require('../utils/result')
|
|
3
3
|
const querystring = require('node:querystring')
|
|
4
|
-
const { getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
|
|
4
|
+
const { getKeysAndParamsFromPath, handleSapMessages, validateIfNoneMatch, getPreferReturnHeader } = require('../utils')
|
|
5
5
|
const { handleStreamProperties } = require('../../_runtime/common/utils/streamProp')
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
|
|
7
|
+
const metaInfo = require('../utils/metaInfo')
|
|
9
8
|
const { getPageSize } = require('../../_runtime/common/generic/paging')
|
|
10
9
|
|
|
11
10
|
const _getCount = result =>
|
|
@@ -18,10 +17,10 @@ const _getCount = result =>
|
|
|
18
17
|
const _calculateNextLink = (req, result) => {
|
|
19
18
|
const $skiptoken = result.$nextLink ?? _calculateSkiptoken(req, result)
|
|
20
19
|
if ($skiptoken) {
|
|
21
|
-
const queryParamsWithSkipToken = { ...req.
|
|
20
|
+
const queryParamsWithSkipToken = { ...req.req.query, $skiptoken }
|
|
22
21
|
// REVISIT: slice replaces leading '/'. Always starts with '/'?
|
|
23
22
|
result.$nextLink =
|
|
24
|
-
req.
|
|
23
|
+
req.req.path.slice(1) +
|
|
25
24
|
'?' +
|
|
26
25
|
querystring.stringify(queryParamsWithSkipToken, '&', '=', { encodeURIComponent: e => e })
|
|
27
26
|
}
|
|
@@ -29,9 +28,9 @@ const _calculateNextLink = (req, result) => {
|
|
|
29
28
|
|
|
30
29
|
const _calculateSkiptoken = (req, result) => {
|
|
31
30
|
const limit = Array.isArray(req.query) ? getPageSize(req.query[0]._target).max : req.query.SELECT.limit?.rows?.val
|
|
32
|
-
const top = parseInt(req.
|
|
31
|
+
const top = parseInt(req.req.query.$top)
|
|
33
32
|
if (limit === result.length && limit !== top) {
|
|
34
|
-
const token = req.
|
|
33
|
+
const token = req.req.query.$skiptoken
|
|
35
34
|
if (cds.env.query.limit.reliablePaging && _reliablePagingPossible(req)) {
|
|
36
35
|
const decoded = token && JSON.parse(Buffer.from(token, 'base64').toString())
|
|
37
36
|
const skipToken = {
|
|
@@ -100,6 +99,14 @@ const _isNullableSingleton = query => query._target._isSingleton && query._targe
|
|
|
100
99
|
const _isToOneAssoc = query =>
|
|
101
100
|
query.SELECT.from.ref.length > 1 && typeof query.SELECT.from.ref.slice(-1)[0] === 'string'
|
|
102
101
|
|
|
102
|
+
const _count = result => {
|
|
103
|
+
return Array.isArray(result)
|
|
104
|
+
? result.reduce((acc, val) => {
|
|
105
|
+
return acc + (val?.$count ?? val?._counted_ ?? (Array.isArray(val) && _count(val))) || 0
|
|
106
|
+
}, 0)
|
|
107
|
+
: result.$count ?? result._counted_ ?? 0
|
|
108
|
+
}
|
|
109
|
+
|
|
103
110
|
// basically stolen from old read handler without understanding it ^^
|
|
104
111
|
const _handleArrayOfQueries = (srv, req, res, next) => {
|
|
105
112
|
const info = metaInfo(req._query, 'READ', srv, {}, req, false)
|
|
@@ -109,45 +116,27 @@ const _handleArrayOfQueries = (srv, req, res, next) => {
|
|
|
109
116
|
.then(result => {
|
|
110
117
|
handleSapMessages(cdsReq, req, res)
|
|
111
118
|
|
|
112
|
-
if (req.url.match(/\/\$count/))
|
|
113
|
-
const count = Array.isArray(result)
|
|
114
|
-
? result.reduce((acc, val) => {
|
|
115
|
-
return (
|
|
116
|
-
acc + ((val && (val.$count || val._counted_)) || (val[0] && (val[0].$count || val[0]._counted_))) || 0
|
|
117
|
-
)
|
|
118
|
-
}, 0)
|
|
119
|
-
: result.$count || result._counted_ || 0
|
|
120
|
-
return res.set('Content-Type', 'text/plain').send(count.toString())
|
|
121
|
-
}
|
|
119
|
+
if (req.url.match(/\/\$count/)) return res.set('content-type', 'text/plain').send(_count(result).toString())
|
|
122
120
|
|
|
123
|
-
|
|
124
|
-
if (cdsReq.query[0].SELECT.count) adjustedResult.$count = 0
|
|
125
|
-
adjustedResult.push(...result[0])
|
|
126
|
-
adjustedResult.$count += result[0].$count ? result[0].$count : 0
|
|
127
|
-
for (let i = 1; i < result.length; i++) {
|
|
128
|
-
adjustedResult.push(...result[i])
|
|
129
|
-
adjustedResult.$count += result[i].$count ? result[i].$count : 0
|
|
121
|
+
for (let i = 0; i < result.length; i++) {
|
|
130
122
|
// Add OData context, if it deviates from main context
|
|
131
|
-
if (info.metadata.contextUrl !== info.metadata.additionalContextUrl[i - 1])
|
|
123
|
+
if (i !== 0 && info.metadata.contextUrl !== info.metadata.additionalContextUrl[i - 1])
|
|
132
124
|
result[i].forEach(entry => (entry['@odata.context'] = info.metadata.additionalContextUrl[i - 1]))
|
|
133
125
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
126
|
+
|
|
127
|
+
res.set('content-type', 'application/json;IEEE754Compatible=true')
|
|
128
|
+
const flatRes = result.flat(Infinity)
|
|
129
|
+
if (cdsReq.query[0].SELECT.count) flatRes.$count = flatRes.length
|
|
130
|
+
res.send(toODataResult(flatRes, info))
|
|
138
131
|
})
|
|
139
132
|
.catch(next)
|
|
140
133
|
}
|
|
141
134
|
|
|
142
135
|
module.exports = srv =>
|
|
143
136
|
function read(req, res, next) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (req._preferReturn) {
|
|
148
|
-
throw Object.assign(new Error(`The 'return' preference is not allowed in ${req.method} requests`), {
|
|
149
|
-
statusCode: 400
|
|
150
|
-
})
|
|
137
|
+
if (getPreferReturnHeader(req)) {
|
|
138
|
+
const msg = `The 'return' preference is not allowed in ${req.method} requests`
|
|
139
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
151
140
|
}
|
|
152
141
|
|
|
153
142
|
// $apply with concat -> multiple queries with special handling
|
|
@@ -165,10 +154,17 @@ module.exports = srv =>
|
|
|
165
154
|
const { keys, params } = getKeysAndParamsFromPath(from, srv)
|
|
166
155
|
const data = keys //> for read and delete, we provide keys in req.data
|
|
167
156
|
|
|
157
|
+
// cdsReq.headers should contain merged headers of envelope and subreq
|
|
158
|
+
const headers = { ...cds.context.http.req.headers, ...req.headers }
|
|
159
|
+
|
|
168
160
|
// we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
|
|
169
|
-
const cdsReq = new cds.Request({ query, data, params, req, res })
|
|
161
|
+
const cdsReq = new cds.Request({ query, data, params, headers, req, res })
|
|
170
162
|
Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
|
|
171
163
|
|
|
164
|
+
// API for subrequests of $batch (or incoming request)
|
|
165
|
+
cdsReq.req = req
|
|
166
|
+
cdsReq.res = res
|
|
167
|
+
|
|
172
168
|
// REVISIT: what is this for? some tests fail without it... we should find a better solution!
|
|
173
169
|
Object.defineProperty(query.SELECT, '_4odata', { value: true })
|
|
174
170
|
|
|
@@ -190,8 +186,6 @@ module.exports = srv =>
|
|
|
190
186
|
|
|
191
187
|
const lastPathElement = req.path.split('/').slice(-1)[0]
|
|
192
188
|
|
|
193
|
-
query.SELECT.columns ??= ['*']
|
|
194
|
-
|
|
195
189
|
if (cds.env.effective.odata.proxies && cds.env.effective.odata.xrefs) {
|
|
196
190
|
// REVISIT check above is still not perfect solution
|
|
197
191
|
resolveProxyExpands(query, srv)
|
|
@@ -207,24 +201,35 @@ module.exports = srv =>
|
|
|
207
201
|
.then(result => {
|
|
208
202
|
handleSapMessages(cdsReq, req, res)
|
|
209
203
|
|
|
210
|
-
if (
|
|
204
|
+
if (result == null) {
|
|
205
|
+
if (!query.SELECT.one) {
|
|
206
|
+
result = []
|
|
207
|
+
if (req.query.$count) result.$count = 0
|
|
208
|
+
} else if (_isNullableSingleton(query) || _isToOneAssoc(query)) {
|
|
209
|
+
return res.sendStatus(204)
|
|
210
|
+
} else {
|
|
211
|
+
throw Object.assign(new Error('404'), { statusCode: 404 })
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (validateIfNoneMatch(cdsReq, req, result)) {
|
|
211
216
|
return res.status(304).end()
|
|
212
217
|
}
|
|
213
218
|
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
statusCode: 404
|
|
218
|
-
})
|
|
219
|
+
// express always handles if-none-match header (see req.fresh)
|
|
220
|
+
if (!cdsReq.target._etag && req.headers['if-none-match']) {
|
|
221
|
+
delete req.headers['if-none-match']
|
|
219
222
|
}
|
|
220
223
|
|
|
221
224
|
if (_propertyAccess && result[_propertyAccess] === null) return res.sendStatus(204)
|
|
222
225
|
|
|
223
226
|
if (lastPathElement === '$count') {
|
|
224
227
|
result = _getCount(result)
|
|
225
|
-
return res.set('
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
+
return res.set('content-type', 'text/plain').send(result.toString())
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (lastPathElement === '$value' && _propertyAccess) {
|
|
232
|
+
return res.set('content-type', 'text/plain').send(result[_propertyAccess].toString())
|
|
228
233
|
}
|
|
229
234
|
|
|
230
235
|
if (info.metadata.isCollection) _calculateNextLink(cdsReq, result)
|
|
@@ -234,9 +239,11 @@ module.exports = srv =>
|
|
|
234
239
|
|
|
235
240
|
// Express interprets numbers as HTTP status codes
|
|
236
241
|
const isNumber = typeof result === 'number'
|
|
237
|
-
res
|
|
238
|
-
|
|
239
|
-
|
|
242
|
+
res.set('content-type', isNumber ? 'text/plain' : 'application/json;IEEE754Compatible=true')
|
|
243
|
+
res.send(isNumber ? result.toString() : result)
|
|
244
|
+
})
|
|
245
|
+
.catch(err => {
|
|
246
|
+
handleSapMessages(cdsReq, req, res)
|
|
247
|
+
next(err)
|
|
240
248
|
})
|
|
241
|
-
.catch(next)
|
|
242
249
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto')
|
|
4
|
-
const metaInfo = require('
|
|
4
|
+
const metaInfo = require('../utils/metaInfo')
|
|
5
5
|
|
|
6
6
|
const normalize_header = value => {
|
|
7
7
|
return value.split(',').map(str => str.trim())
|
|
@@ -19,10 +19,10 @@ const generateEtag = s => {
|
|
|
19
19
|
module.exports = srv =>
|
|
20
20
|
function service_document(req, res) {
|
|
21
21
|
if (req.method === 'HEAD') return res.end()
|
|
22
|
-
if (req.method !== 'GET')
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
if (req.method !== 'GET') {
|
|
23
|
+
const msg = `Method ${req.method} is not allowed for calls to the service endpoint`
|
|
24
|
+
throw Object.assign(new Error(msg), { statusCode: 405 })
|
|
25
|
+
}
|
|
26
26
|
|
|
27
27
|
const m = cds.context.model || cds.model
|
|
28
28
|
const csnService = (cds.context.model || cds.model).definitions[srv.name]
|
|
@@ -38,7 +38,7 @@ module.exports = srv =>
|
|
|
38
38
|
if (csnService.srvDocEtag) {
|
|
39
39
|
const unchanged = validate_etag(req.headers['if-none-match'], csnService.srvDocEtag)
|
|
40
40
|
if (unchanged) {
|
|
41
|
-
res.set('
|
|
41
|
+
res.set('etag', csnService.srvDocEtag)
|
|
42
42
|
return res.status(304).end()
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -51,7 +51,7 @@ module.exports = srv =>
|
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
csnService.srvDocEtag = generateEtag(JSON.stringify(exposedEntities))
|
|
54
|
-
res.set('
|
|
54
|
+
res.set('etag', csnService.srvDocEtag)
|
|
55
55
|
|
|
56
56
|
const info = metaInfo({ SELECT: { from: { ref: [srv.name] } } }, 'READ', srv, {}, req, false)
|
|
57
57
|
|