@sap/cds 7.8.2 → 7.9.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.
- package/CHANGELOG.md +37 -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/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/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
|
@@ -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
|
|
|
@@ -3,7 +3,7 @@ const { Readable } = require('node:stream')
|
|
|
3
3
|
const getError = require('../../_runtime/common/error')
|
|
4
4
|
const { getTransition } = require('../../_runtime/common/utils/resolveView')
|
|
5
5
|
const LOG = cds.log('odata')
|
|
6
|
-
const { getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
|
|
6
|
+
const { getKeysAndParamsFromPath, handleSapMessages, validateIfNoneMatch } = require('../utils')
|
|
7
7
|
|
|
8
8
|
const _resolveContentProperty = (target, annotName, resolvedProp) => {
|
|
9
9
|
if (target.elements[resolvedProp]) {
|
|
@@ -20,8 +20,10 @@ const _resolveContentProperty = (target, annotName, resolvedProp) => {
|
|
|
20
20
|
|
|
21
21
|
const isStream = query => {
|
|
22
22
|
const { _propertyAccess, target } = query
|
|
23
|
+
if (!_propertyAccess) return
|
|
24
|
+
|
|
23
25
|
const element = target.elements[_propertyAccess]
|
|
24
|
-
return
|
|
26
|
+
return element._type === 'cds.LargeBinary' && element['@Core.MediaType']
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
const isStreamByDollarValue = (query, previous, last) => {
|
|
@@ -47,7 +49,7 @@ const _addMetadataProperty = (query, property, annotName, odataName) => {
|
|
|
47
49
|
const addStreamMetadata = query => {
|
|
48
50
|
// new odata parser sets streaming property in SELECT.from
|
|
49
51
|
const ref = query.SELECT.columns?.[0].ref || query.SELECT.from.ref
|
|
50
|
-
const propertyName = ref
|
|
52
|
+
const propertyName = ref.at(-1)
|
|
51
53
|
let mediaTypeProperty
|
|
52
54
|
for (let key in query.target.elements) {
|
|
53
55
|
const val = query.target.elements[key]
|
|
@@ -76,19 +78,16 @@ const addStreamMetadata = query => {
|
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
const validateStream = (req,
|
|
81
|
+
const validateStream = (req, result) => {
|
|
80
82
|
// REVISIT: compat, should actually be treated as object
|
|
81
83
|
if (!Array.isArray(result)) result = [result]
|
|
82
84
|
|
|
83
85
|
// Reading one entity or a property of it should yield only a result length of one.
|
|
84
|
-
if (result.length === 0 || result[0] === undefined)
|
|
85
|
-
if (req.headers['if-none-match']) return
|
|
86
|
-
throw getError(404)
|
|
87
|
-
}
|
|
86
|
+
if (result.length === 0 || result[0] === undefined) throw getError(404)
|
|
88
87
|
|
|
89
88
|
if (result.length > 1) throw getError(400)
|
|
90
89
|
|
|
91
|
-
if (result[0] === null) return
|
|
90
|
+
if (result[0] === null) return
|
|
92
91
|
|
|
93
92
|
result = result[0]
|
|
94
93
|
|
|
@@ -102,9 +101,8 @@ const validateStream = (req, res, result) => {
|
|
|
102
101
|
!headers.accept.includes(contentType) &&
|
|
103
102
|
!headers.accept.includes(contentType.split('/')[0] + '/*')
|
|
104
103
|
) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
})
|
|
104
|
+
const msg = `Content type "${contentType}" is not listed in accept header "${headers.accept}"`
|
|
105
|
+
throw Object.assign(new Error(msg), { statusCode: 406 })
|
|
108
106
|
}
|
|
109
107
|
}
|
|
110
108
|
|
|
@@ -173,7 +171,7 @@ const stream = srv =>
|
|
|
173
171
|
const { _query: query } = req
|
|
174
172
|
|
|
175
173
|
// $apply with concat -> multiple queries with special handling -> read only, no stream?
|
|
176
|
-
if (Array.isArray(query)) return next(
|
|
174
|
+
if (Array.isArray(query)) return next()
|
|
177
175
|
|
|
178
176
|
const [previous, lastPathElement] = req.path.split('/').slice(-2)
|
|
179
177
|
const _isStreamByDollarValue = isStreamByDollarValue(query, previous, lastPathElement)
|
|
@@ -188,16 +186,16 @@ const stream = srv =>
|
|
|
188
186
|
}
|
|
189
187
|
}
|
|
190
188
|
|
|
191
|
-
|
|
189
|
+
const pdfMimeType = !!req.headers.accept?.match(/application\/pdf/)
|
|
190
|
+
const isMimeTypeStreamedByDefault = !!(!query.SELECT.one && pdfMimeType)
|
|
191
|
+
const _isStream = isStream(query) || _isStreamByDollarValue || isMimeTypeStreamedByDefault
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
if (!_isStream) return next()
|
|
194
194
|
|
|
195
|
-
if (!
|
|
196
|
-
|
|
195
|
+
if (!query.target['@cds.persistence.skip'] && !isMimeTypeStreamedByDefault) {
|
|
196
|
+
addStreamMetadata(query)
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
if (!query.target['@cds.persistence.skip']) addStreamMetadata(query)
|
|
200
|
-
|
|
201
199
|
// 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
|
|
202
200
|
const cdsReq = new cds.Request({ query, req, res })
|
|
203
201
|
Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
|
|
@@ -214,18 +212,22 @@ const stream = srv =>
|
|
|
214
212
|
.tx(() => {
|
|
215
213
|
return srv.dispatch(cdsReq).then(async result => {
|
|
216
214
|
handleSapMessages(cdsReq, req, res)
|
|
217
|
-
validateStream(req,
|
|
215
|
+
validateStream(req, result)
|
|
216
|
+
|
|
217
|
+
if (validateIfNoneMatch(cdsReq, req, result)) {
|
|
218
|
+
return res.status(304).end()
|
|
219
|
+
}
|
|
218
220
|
|
|
219
221
|
const stream = normalizeStream(result, query._propertyAccess, lastPathElement, query.target)
|
|
220
222
|
if (stream === null) {
|
|
221
|
-
if (req.headers['if-none-match']) {
|
|
222
|
-
res.status(304)
|
|
223
|
-
return
|
|
224
|
-
}
|
|
225
223
|
res.status(204)
|
|
226
224
|
return
|
|
227
225
|
}
|
|
228
226
|
|
|
227
|
+
if (pdfMimeType) {
|
|
228
|
+
if (!result.$mediaContentType) result.$mediaContentType = 'application/pdf'
|
|
229
|
+
}
|
|
230
|
+
|
|
229
231
|
setStreamingHeaders(result, res)
|
|
230
232
|
|
|
231
233
|
return new Promise((resolve, reject) => {
|