@sap/cds 7.2.0 → 7.3.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 +174 -126
- package/README.md +1 -1
- package/apis/connect.d.ts +1 -1
- package/apis/core.d.ts +6 -4
- package/apis/serve.d.ts +1 -1
- package/apis/services.d.ts +51 -31
- package/apis/test.d.ts +24 -10
- package/bin/serve.js +4 -3
- package/common.cds +4 -4
- package/lib/auth/ias-auth.js +7 -8
- package/lib/compile/cdsc.js +5 -7
- package/lib/compile/etc/csv.js +22 -11
- package/lib/dbs/cds-deploy.js +1 -2
- package/lib/env/cds-env.js +26 -20
- package/lib/env/defaults.js +4 -3
- package/lib/env/schema.js +9 -0
- package/lib/i18n/localize.js +83 -77
- package/lib/index.js +6 -2
- package/lib/linked/classes.js +13 -13
- package/lib/plugins.js +41 -45
- package/lib/req/user.js +2 -2
- package/lib/srv/protocols/_legacy.js +0 -1
- package/lib/srv/protocols/odata-v4.js +4 -0
- package/lib/utils/axios.js +7 -1
- package/lib/utils/cds-test.js +140 -133
- package/lib/utils/cds-utils.js +1 -1
- package/lib/utils/check-version.js +6 -0
- package/lib/utils/data.js +19 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +20 -19
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +10 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +2 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +0 -14
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/core/OdataRequest.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/BatchRequestListBuilder.js +5 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/MetadataHandler.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/ServiceHandler.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/DispatcherCommand.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +1 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -1
- package/libx/_runtime/common/composition/update.js +18 -2
- package/libx/_runtime/common/error/frontend.js +46 -34
- package/libx/_runtime/common/generic/auth/capabilities.js +33 -14
- package/libx/_runtime/common/generic/input.js +1 -1
- package/libx/_runtime/common/generic/paging.js +1 -0
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -3
- package/libx/_runtime/db/query/update.js +48 -30
- package/libx/_runtime/fiori/lean-draft.js +23 -24
- package/libx/_runtime/hana/conversion.js +3 -2
- package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -1
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/remote/Service.js +11 -26
- package/libx/_runtime/remote/utils/client.js +3 -2
- package/libx/_runtime/remote/utils/data.js +5 -7
- package/libx/odata/{grammar.pegjs → grammar.peggy} +1 -1
- package/libx/odata/metadata.js +121 -0
- package/libx/odata/parser.js +1 -1
- package/libx/odata/service-document.js +61 -0
- package/libx/odata/utils.js +102 -48
- package/libx/rest/RestAdapter.js +2 -2
- package/libx/rest/middleware/error.js +1 -1
- package/package.json +1 -1
|
@@ -34,6 +34,7 @@ const _setHeaders = (defaultHeaders, req) => {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const _setCorrectValue = (el, data, params, kind) => {
|
|
37
|
+
if (data[el] === undefined) return "'undefined'"
|
|
37
38
|
return typeof data[el] === 'object' && kind !== 'odata-v2'
|
|
38
39
|
? JSON.stringify(data[el])
|
|
39
40
|
: formatVal(data[el], el, { elements: params }, kind)
|
|
@@ -109,8 +110,9 @@ const _handleUnboundActionFunction = (srv, def, req, event) => {
|
|
|
109
110
|
def &&
|
|
110
111
|
def.returns &&
|
|
111
112
|
(def.returns.type === 'cds.LargeBinary' || def.returns.type === 'cds.Binary')
|
|
113
|
+
const { headers, data } = req
|
|
112
114
|
|
|
113
|
-
return srv.send({ method: 'POST', path: `/${event}`,
|
|
115
|
+
return srv.send({ method: 'POST', path: `/${event}`, headers, data, _binary: isBinary })
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
const url =
|
|
@@ -118,16 +120,17 @@ const _handleUnboundActionFunction = (srv, def, req, event) => {
|
|
|
118
120
|
return srv.get(url)
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
const _sendV2RequestActionFunction = (srv, def, url) => {
|
|
123
|
+
const _sendV2RequestActionFunction = (srv, def, req, url) => {
|
|
124
|
+
const { headers } = req
|
|
122
125
|
return def.kind === 'function'
|
|
123
|
-
? srv.send({ method: 'GET', path: url, _returnType: def.returns })
|
|
124
|
-
: srv.send({ method: 'POST', path: url, data: {}, _returnType: def.returns })
|
|
126
|
+
? srv.send({ method: 'GET', path: url, headers, _returnType: def.returns })
|
|
127
|
+
: srv.send({ method: 'POST', path: url, headers, data: {}, _returnType: def.returns })
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
const _handleV2ActionFunction = (srv, def, req, event, kind) => {
|
|
128
131
|
const url =
|
|
129
132
|
Object.keys(req.data).length > 0 ? _buildPartialUrlFunctions(`/${event}`, req.data, def.params, kind) : `/${event}`
|
|
130
|
-
return _sendV2RequestActionFunction(srv, def, url)
|
|
133
|
+
return _sendV2RequestActionFunction(srv, def, req, url)
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
const _handleV2BoundActionFunction = (srv, def, req, event, kind) => {
|
|
@@ -147,7 +150,7 @@ const _handleV2BoundActionFunction = (srv, def, req, event, kind) => {
|
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
const url = `${`/${event}`}?${params.join('&')}`
|
|
150
|
-
return _sendV2RequestActionFunction(srv, def, url)
|
|
153
|
+
return _sendV2RequestActionFunction(srv, def, req, url)
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
const _addHandlerActionFunction = (srv, def, target) => {
|
|
@@ -178,7 +181,6 @@ const resolvedTargetOfQuery = q => {
|
|
|
178
181
|
}
|
|
179
182
|
|
|
180
183
|
let logged
|
|
181
|
-
let sdkLoggerDisabled
|
|
182
184
|
|
|
183
185
|
const _resolveSelectionStrategy = options => {
|
|
184
186
|
if (typeof options?.selectionStrategy !== 'string') return
|
|
@@ -226,22 +228,6 @@ class RemoteService extends cds.Service {
|
|
|
226
228
|
'Configuration option "cds.env.features.fetch_csrf" is deprecated.\n Please use "csrf"/"csrfInBatch" as described in https://cap.cloud.sap/docs/node.js/remote-services'
|
|
227
229
|
)
|
|
228
230
|
}
|
|
229
|
-
|
|
230
|
-
// REVISIT: use cds.log's logger in cloud sdk
|
|
231
|
-
|
|
232
|
-
// disable sdk logger if not in debug mode
|
|
233
|
-
if (!LOG._debug && !sdkLoggerDisabled) {
|
|
234
|
-
try {
|
|
235
|
-
// eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
|
|
236
|
-
const sdkUtils = require('@sap-cloud-sdk/util')
|
|
237
|
-
sdkUtils.setGlobalLogLevel('error')
|
|
238
|
-
|
|
239
|
-
// disable sdk logger once
|
|
240
|
-
sdkLoggerDisabled = true
|
|
241
|
-
} catch (err) {
|
|
242
|
-
/* might fail in cds repl due to winston's exception handler, see cap/issues#10134 */
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
231
|
} else if ([...this.entities].length || [...this.operations].length) {
|
|
246
232
|
throw new Error(`No credentials configured for "${this.name}".`)
|
|
247
233
|
}
|
|
@@ -262,6 +248,8 @@ class RemoteService extends cds.Service {
|
|
|
262
248
|
}
|
|
263
249
|
|
|
264
250
|
this.on('*', async (req, next) => {
|
|
251
|
+
const { query } = req
|
|
252
|
+
if (!query && !(typeof req.path === 'string')) return next()
|
|
265
253
|
// early validation on first request for use case without remote API
|
|
266
254
|
// ideally, that's done on bootstrap of the remote service
|
|
267
255
|
if (typeof this.destination === 'object' && !this.destination.url)
|
|
@@ -269,9 +257,6 @@ class RemoteService extends cds.Service {
|
|
|
269
257
|
if (this._resilienceMiddlewares && !this._resilienceMiddlewares.timeout)
|
|
270
258
|
this._resilienceMiddlewares.timeout = cloudSdkResilience().timeout(this.requestTimeout)
|
|
271
259
|
|
|
272
|
-
const { query } = req
|
|
273
|
-
if (!query && !(typeof req.path === 'string')) return next()
|
|
274
|
-
|
|
275
260
|
const resolvedTarget = resolvedTargetOfQuery(query) || getTransition(req.target, this).target
|
|
276
261
|
const reqOptions = getReqOptions(req, query, this)
|
|
277
262
|
reqOptions.headers = _setHeaders(reqOptions.headers, req)
|
|
@@ -427,12 +427,13 @@ const _stringToReqOptions = (query, data, target) => {
|
|
|
427
427
|
return reqOptions
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
-
const _pathToReqOptions = (method, path, data, target) => {
|
|
430
|
+
const _pathToReqOptions = (method, path, data, target, srvName) => {
|
|
431
431
|
let url = path
|
|
432
432
|
if (!url.startsWith('/')) {
|
|
433
433
|
// extract entity name and instance identifier (either in "()" or after "/") from fully qualified path
|
|
434
434
|
const parts = path.match(/([\w.]*)([\W.]*)(.*)/)
|
|
435
435
|
if (!parts) url = '/' + path.match(/\w*$/)[0]
|
|
436
|
+
else if (url.startsWith(srvName)) url = '/' + parts[1].replace(srvName + '.', '') + parts[2] + parts[3]
|
|
436
437
|
else url = '/' + parts[1].match(/\w*$/)[0] + parts[2] + parts[3]
|
|
437
438
|
|
|
438
439
|
// normalize in case parts[2] already starts with /
|
|
@@ -459,7 +460,7 @@ const getReqOptions = (req, query, service) => {
|
|
|
459
460
|
? _cqnToReqOptions(query, service, req)
|
|
460
461
|
: typeof query === 'string'
|
|
461
462
|
? _stringToReqOptions(query, req.data, req.target)
|
|
462
|
-
: _pathToReqOptions(req.method, req.path, req.data, req.target)
|
|
463
|
+
: _pathToReqOptions(req.method, req.path, req.data, req.target, service.name)
|
|
463
464
|
|
|
464
465
|
if (service.kind === 'odata-v2' && req.event === 'READ' && reqOptions.url?.match(/(\/any\()|(\/all\()/)) {
|
|
465
466
|
req.reject(501, 'Lambda expressions are not supported in OData v2')
|
|
@@ -66,12 +66,9 @@ const _convertActionFuncResponse = (returnType, convertValueFn) => data => {
|
|
|
66
66
|
|
|
67
67
|
// eslint-disable-next-line complexity
|
|
68
68
|
const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, element) => {
|
|
69
|
-
if (value == null)
|
|
70
|
-
return value
|
|
71
|
-
}
|
|
69
|
+
if (value == null) return value
|
|
72
70
|
|
|
73
71
|
const type = _elementType(element)
|
|
74
|
-
|
|
75
72
|
if (type === 'cds.Boolean') {
|
|
76
73
|
if (value === 'true') {
|
|
77
74
|
value = true
|
|
@@ -119,18 +116,19 @@ const _convertValue = (ieee754Compatible, exponentialDecimals) => (value, elemen
|
|
|
119
116
|
|
|
120
117
|
return value
|
|
121
118
|
}
|
|
119
|
+
|
|
122
120
|
const _PT = ([hh, mm, ss]) => `PT${hh}H${mm}M${ss}S`
|
|
123
121
|
|
|
124
122
|
const _convertPayloadValue = (value, element) => {
|
|
125
|
-
|
|
123
|
+
if (value == null) return value
|
|
126
124
|
|
|
127
125
|
// see https://www.odata.org/documentation/odata-version-2-0/json-format/
|
|
128
|
-
|
|
126
|
+
const type = _elementType(element)
|
|
129
127
|
switch (type) {
|
|
130
128
|
case 'cds.Time':
|
|
131
129
|
return value.match(/^(PT)([H,M,S,0-9])*$/) ? value : _PT(value.split(':'))
|
|
132
130
|
case 'cds.Decimal':
|
|
133
|
-
return typeof value === 'string' ? value :
|
|
131
|
+
return typeof value === 'string' ? value : `${value}`
|
|
134
132
|
case 'cds.Date':
|
|
135
133
|
case 'cds.DateTime':
|
|
136
134
|
return `/Date(${new Date(value).getTime()})/`
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
const stack = []
|
|
34
34
|
let SELECT, count
|
|
35
35
|
const TECHNICAL_OPTS = ['$value'] // odata parts to be handled somewhere else
|
|
36
|
-
// we keep that here to allow for usage in https://
|
|
36
|
+
// we keep that here to allow for usage in https://peggyjs.org/online
|
|
37
37
|
const safeNumber =
|
|
38
38
|
options.safeNumber ||
|
|
39
39
|
function (str) {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const cds = require('../../lib')
|
|
2
|
+
const LOG = cds.log('odata')
|
|
3
|
+
const crypto = require('crypto')
|
|
4
|
+
|
|
5
|
+
const _requestedFormat = (queryOption, header) => {
|
|
6
|
+
if (queryOption) return queryOption.match(/json/i) ? 'json' : 'xml'
|
|
7
|
+
if (header) {
|
|
8
|
+
const jsonIndex = header.indexOf('application/json')
|
|
9
|
+
if (jsonIndex === -1) return 'xml'
|
|
10
|
+
const xmlIndex = header.indexOf('application/xml')
|
|
11
|
+
if (xmlIndex === -1) return 'json'
|
|
12
|
+
return jsonIndex < xmlIndex ? 'json' : 'xml'
|
|
13
|
+
}
|
|
14
|
+
return 'xml'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const _metadataFromFile = async srv => {
|
|
18
|
+
const fs = require('fs')
|
|
19
|
+
const filePath = cds.root + `/srv/odata/v4/${srv.name}.xml`
|
|
20
|
+
let exists
|
|
21
|
+
try {
|
|
22
|
+
exists = !(await fs.promises.access(filePath, fs.constants.F_OK))
|
|
23
|
+
} catch (e) {
|
|
24
|
+
LOG._debug && LOG.debug(`No metadata file found for service ${srv.name} at ${filePath}`)
|
|
25
|
+
}
|
|
26
|
+
if (exists) {
|
|
27
|
+
const file = await fs.promises.readFile(filePath)
|
|
28
|
+
return file.toString()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const normalize_header = value => {
|
|
33
|
+
return value.split(',').map(str => str.trim())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const validate_etag = (ifNoneMatch, etag) => {
|
|
37
|
+
const ifNoneMatchEtags = normalize_header(ifNoneMatch)
|
|
38
|
+
return ifNoneMatchEtags.includes(etag) || ifNoneMatchEtags.includes('*')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const generateEtag = s => {
|
|
42
|
+
return `W/"${crypto.createHash('sha256').update(s).digest('base64')}"`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const odata_error = (code, message) => ({ error: { code, message } })
|
|
46
|
+
|
|
47
|
+
module.exports = srv =>
|
|
48
|
+
async function metadata(req, res, next) {
|
|
49
|
+
if (req.path === '/$metadata') {
|
|
50
|
+
if (req.method !== 'GET')
|
|
51
|
+
return res
|
|
52
|
+
.status(405)
|
|
53
|
+
.json(odata_error('METHOD_NOT_ALLOWED', `Method ${req.method} not allowed for $metadata.`))
|
|
54
|
+
|
|
55
|
+
const tenant = cds.context.tenant
|
|
56
|
+
const locale = cds.context.locale
|
|
57
|
+
const format = _requestedFormat(req.query['$format'], req.headers['accept'])
|
|
58
|
+
|
|
59
|
+
// REVISIT: edm(x) and etag cache is only evicted with model
|
|
60
|
+
const csnService = (cds.context.model || cds.model).definitions[srv.name]
|
|
61
|
+
const metadataCache = (csnService.metadataCache = csnService.metadataCache || { jsonEtag: {}, xmlEtag: {} })
|
|
62
|
+
|
|
63
|
+
const etag = format === 'json' ? metadataCache.jsonEtag?.[locale] : metadataCache.xmlEtag?.[locale]
|
|
64
|
+
|
|
65
|
+
if (req.headers['if-none-match']) {
|
|
66
|
+
if (etag) {
|
|
67
|
+
const unchanged = validate_etag(req.headers['if-none-match'], etag)
|
|
68
|
+
if (unchanged) {
|
|
69
|
+
res.set('Etag', etag)
|
|
70
|
+
return res.status(304).end()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { 'cds.xt.ModelProviderService': mps } = cds.services
|
|
76
|
+
if (mps) {
|
|
77
|
+
if (format === 'json')
|
|
78
|
+
res
|
|
79
|
+
.status(400)
|
|
80
|
+
.json(
|
|
81
|
+
odata_error(
|
|
82
|
+
'UNSUPPORTED_METADATA_TYPE',
|
|
83
|
+
'JSON metadata is not supported if cds.requires.extensibilty: true.'
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const edmx = await mps.getEdmx({ tenant, model: srv.model, service: srv.definition.name, locale })
|
|
89
|
+
metadataCache.xmlEtag[locale] = generateEtag(edmx)
|
|
90
|
+
res.set('Content-Type', 'application/xml')
|
|
91
|
+
res.send(edmx)
|
|
92
|
+
} catch (e) {
|
|
93
|
+
if (LOG._error) {
|
|
94
|
+
e.message = 'Unable to get EDMX for tenant ' + tenant + ' due to error: ' + e.message
|
|
95
|
+
LOG.error(e)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return res.status(503).json(odata_error('SERVICE_UNAVAILABLE', 'Service unavailable'))
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (format === 'json') {
|
|
103
|
+
const edm =
|
|
104
|
+
metadataCache.edm || (metadataCache.edm = cds.compile.to.edm(srv.model, { service: srv.definition.name }))
|
|
105
|
+
const localized = cds.localize(srv.model, locale, edm)
|
|
106
|
+
metadataCache.jsonEtag[locale] = generateEtag(localized)
|
|
107
|
+
return res.json(JSON.parse(localized))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const edmx =
|
|
111
|
+
metadataCache.edmx ||
|
|
112
|
+
(await _metadataFromFile(srv)) ||
|
|
113
|
+
(metadataCache.edmx = cds.compile.to.edmx(srv.model, { service: srv.definition.name }))
|
|
114
|
+
const localized = cds.localize(srv.model, locale, edmx)
|
|
115
|
+
metadataCache.xmlEtag[locale] = generateEtag(localized)
|
|
116
|
+
res.set('Etag', metadataCache.xmlEtag[locale])
|
|
117
|
+
res.set('Content-Type', 'application/xml')
|
|
118
|
+
return res.send(localized)
|
|
119
|
+
}
|
|
120
|
+
return next()
|
|
121
|
+
}
|