@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
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
|
-
const {
|
|
2
|
+
const { UPDATE } = cds.ql
|
|
3
3
|
|
|
4
4
|
const { toODataResult, postProcess } = require('../utils/result')
|
|
5
|
-
const { getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
|
|
5
|
+
const { getKeysAndParamsFromPath, handleSapMessages, getPreferReturnHeader } = require('../utils')
|
|
6
6
|
const { deepCopy } = require('../../_runtime/common/utils/copy')
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const metaInfo = require('../../_runtime/cds-services/adapter/odata-v4/utils/metaInfo')
|
|
8
|
+
const readAfterWrite = require('../utils/readAfterWrite')
|
|
9
|
+
const metaInfo = require('../utils/metaInfo')
|
|
11
10
|
|
|
12
11
|
const _isUpsertAllowed = ({ target, data, event }) => {
|
|
13
12
|
return (
|
|
@@ -42,7 +41,7 @@ const _isNavigationWithKeyInParent = (keys, data, pathExpression, model) => {
|
|
|
42
41
|
return parent && navElement && where
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
module.exports = srv =>
|
|
44
|
+
module.exports = (srv, router) =>
|
|
46
45
|
function update(req, res, next) {
|
|
47
46
|
// REVISIT: better solution for _propertyAccess
|
|
48
47
|
const {
|
|
@@ -53,16 +52,11 @@ module.exports = srv =>
|
|
|
53
52
|
|
|
54
53
|
// REVISIT: patch on collection is allowed in odata 4.01
|
|
55
54
|
if (!one) {
|
|
56
|
-
|
|
57
|
-
throw Object.assign(new Error(`Method ${req.method} not allowed for ENTITY.COLLECTION`), {
|
|
58
|
-
statusCode: 405
|
|
59
|
-
})
|
|
55
|
+
throw Object.assign(new Error(`Method ${req.method} is not allowed for entity collections`), { statusCode: 405 })
|
|
60
56
|
}
|
|
61
57
|
|
|
62
58
|
if (_propertyAccess && req.method === 'PATCH') {
|
|
63
|
-
throw Object.assign(new Error(`Method ${req.method} not allowed for
|
|
64
|
-
statusCode: 405
|
|
65
|
-
})
|
|
59
|
+
throw Object.assign(new Error(`Method ${req.method} is not allowed for properties`), { statusCode: 405 })
|
|
66
60
|
}
|
|
67
61
|
|
|
68
62
|
// payload & params
|
|
@@ -87,11 +81,16 @@ module.exports = srv =>
|
|
|
87
81
|
// query
|
|
88
82
|
let query = UPDATE.entity(from).with(data)
|
|
89
83
|
|
|
84
|
+
// cdsReq.headers should contain merged headers of envelope and subreq
|
|
85
|
+
const headers = { ...cds.context.http.req.headers, ...req.headers }
|
|
86
|
+
|
|
90
87
|
// we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
|
|
91
|
-
let cdsReq = new cds.Request({ query, params, req, res })
|
|
88
|
+
let cdsReq = new cds.Request({ query, params, headers, req, res })
|
|
92
89
|
Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
|
|
93
90
|
|
|
94
|
-
|
|
91
|
+
// API for subrequests of $batch (or incoming request)
|
|
92
|
+
cdsReq.req = req
|
|
93
|
+
cdsReq.res = res
|
|
95
94
|
|
|
96
95
|
// REVISIT: adjust in getter?
|
|
97
96
|
if (req.method === 'PUT') cdsReq.method = 'PUT'
|
|
@@ -104,98 +103,60 @@ module.exports = srv =>
|
|
|
104
103
|
// or the auto-managed tx opened for the respective atomicity group, if exists
|
|
105
104
|
return srv
|
|
106
105
|
.run(() => {
|
|
107
|
-
return srv
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
_propertyAccess ||
|
|
118
|
-
!((is404 || isForcedInsert) && _isUpsertAllowed({ target, data, event: req.method }))
|
|
119
|
-
) {
|
|
120
|
-
throw e
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// PUT / PATCH with if-match header means "only if already exists" -> no insert if it does not
|
|
124
|
-
if (req.headers['if-match']) throw Object.assign(new Error('412'), { statusCode: 412 })
|
|
125
|
-
|
|
126
|
-
// check only works with req.body and not with updateDate
|
|
127
|
-
if (_isNavigationWithKeyInParent(target.keys, req.body, from, srv.model)) {
|
|
128
|
-
// REVISIT: better error message
|
|
129
|
-
throw Object.assign(new Error('Unprocessable Content'), { statusCode: 422 })
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// REVISIT:
|
|
133
|
-
// can we somehow "replay" the request with POST?
|
|
134
|
-
// or should we call the create handler directly?
|
|
135
|
-
|
|
136
|
-
// payload & params
|
|
137
|
-
data = deepCopy(req.body)
|
|
138
|
-
// add keys from url into payload (overwriting if already present)
|
|
139
|
-
Object.assign(data, keys)
|
|
140
|
-
|
|
141
|
-
// assert payload
|
|
142
|
-
const assertOptions = { filter: true, http: { req }, mandatories: true }
|
|
143
|
-
const errs = cds.assert(data, target, assertOptions)
|
|
144
|
-
if (errs) {
|
|
145
|
-
if (errs.length === 1) throw errs[0]
|
|
146
|
-
throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
crudEvent = 'CREATE'
|
|
150
|
-
|
|
151
|
-
// query
|
|
152
|
-
// REVISIT: up_XX needs to be looked up -> composition of aspect
|
|
153
|
-
query = INSERT.into(from).entries(data)
|
|
154
|
-
|
|
155
|
-
// we need a cds.Request for multiple reasons, incl. params, headers, sap-messages, read after write, ...
|
|
156
|
-
cdsReq = new cds.Request({ query: query, params, req, res })
|
|
157
|
-
|
|
158
|
-
return srv.dispatch(cdsReq)
|
|
159
|
-
})
|
|
160
|
-
.then(result => {
|
|
161
|
-
// REVISIT: not great, but avoids try catch in catch callback above
|
|
162
|
-
if (result.constructor.name === 'ServerResponse') return
|
|
163
|
-
handleSapMessages(cdsReq, req, res)
|
|
164
|
-
|
|
165
|
-
// TODO: any other checks needed?
|
|
166
|
-
if (cdsReq._.readAfterWrite && !(_propertyAccess && !target._etag))
|
|
167
|
-
return readAfterWrite(cdsReq, srv, { operation: { result } })
|
|
168
|
-
|
|
169
|
-
return result
|
|
170
|
-
})
|
|
106
|
+
return srv.dispatch(cdsReq).then(result => {
|
|
107
|
+
handleSapMessages(cdsReq, req, res)
|
|
108
|
+
|
|
109
|
+
// TODO: any other checks needed?
|
|
110
|
+
if (cdsReq._.readAfterWrite && !(_propertyAccess && !target._etag)) {
|
|
111
|
+
return readAfterWrite(cdsReq, srv, SELECT.one(cdsReq.subject))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result
|
|
115
|
+
})
|
|
171
116
|
})
|
|
172
117
|
.then(result => {
|
|
173
118
|
// we use an extra then block, after getting the result, so the transaction is commited, before sending the response
|
|
174
119
|
|
|
120
|
+
if (result == null) return res.sendStatus(204)
|
|
121
|
+
|
|
175
122
|
// REVISIT: metaInfo needs original query in case of property access, but why?
|
|
176
|
-
const info = metaInfo(_propertyAccess ? req._query : query,
|
|
123
|
+
const info = metaInfo(_propertyAccess ? req._query : query, 'UPDATE', srv, result, req)
|
|
177
124
|
|
|
178
|
-
|
|
125
|
+
const isMinimal = getPreferReturnHeader(req) === 'minimal'
|
|
179
126
|
|
|
180
|
-
const isMinimal = req._preferReturn === 'minimal'
|
|
181
127
|
postProcess(cdsReq.target, srv, result, isMinimal)
|
|
182
|
-
if (result['$etag']) res.set('etag', result['$etag'])
|
|
183
128
|
|
|
184
|
-
if (
|
|
185
|
-
// UPSERT
|
|
186
|
-
return res
|
|
187
|
-
.set('Content-Type', 'application/json;IEEE754Compatible=true')
|
|
188
|
-
.status(201)
|
|
189
|
-
.send(toODataResult(result, info))
|
|
190
|
-
}
|
|
129
|
+
if (result['$etag']) res.set('etag', result['$etag'])
|
|
191
130
|
|
|
192
131
|
if (isMinimal || (query._propertyAccess && result[query._propertyAccess] == null) || info.metadata.isStream) {
|
|
193
132
|
return res.sendStatus(204)
|
|
194
133
|
}
|
|
195
134
|
|
|
196
135
|
result = toODataResult(result, info)
|
|
136
|
+
res.set('content-type', 'application/json;IEEE754Compatible=true')
|
|
137
|
+
res.send(result)
|
|
138
|
+
})
|
|
139
|
+
.catch(e => {
|
|
140
|
+
// if UPSERT is allowed, redirect to POST
|
|
141
|
+
const is404 = e.code === 404 || e.status === 404 || e.statusCode === 404
|
|
142
|
+
const isForcedInsert =
|
|
143
|
+
(e.code === 412 || e.status === 412 || e.statusCode === 412) && req.headers['if-none-match'] === '*'
|
|
144
|
+
if (!_propertyAccess && (is404 || isForcedInsert) && _isUpsertAllowed({ target, data, event: req.method })) {
|
|
145
|
+
// PUT / PATCH with if-match header means "only if already exists" -> no insert if it does not
|
|
146
|
+
if (req.headers['if-match']) {
|
|
147
|
+
return next(Object.assign(new Error('412'), { statusCode: 412 }))
|
|
148
|
+
}
|
|
149
|
+
// (check only works with req.body and not with data)
|
|
150
|
+
if (_isNavigationWithKeyInParent(target.keys, req.body, from, srv.model)) {
|
|
151
|
+
return next(Object.assign(new Error('422'), { statusCode: 422 }))
|
|
152
|
+
}
|
|
153
|
+
// -> redirect to POST
|
|
154
|
+
return router.handle(Object.assign(Object.create(req), { method: 'POST' }), res)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
handleSapMessages(cdsReq, req, res)
|
|
197
158
|
|
|
198
|
-
|
|
159
|
+
// continue with caught error
|
|
160
|
+
next(e)
|
|
199
161
|
})
|
|
200
|
-
.catch(next)
|
|
201
162
|
}
|
|
@@ -178,14 +178,20 @@ function _convertVal(value, element) {
|
|
|
178
178
|
case 'cds.Integer':
|
|
179
179
|
case 'cds.Int16':
|
|
180
180
|
case 'cds.Int32':
|
|
181
|
-
if (!/^-?\+?\d+$/.test(value))
|
|
182
|
-
|
|
181
|
+
if (!/^-?\+?\d+$/.test(value)) {
|
|
182
|
+
const msg = `Element "${element.name}" does not contain a valid Integer`
|
|
183
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
184
|
+
}
|
|
183
185
|
// eslint-disable-next-line no-case-declarations
|
|
184
186
|
const n = Number(value)
|
|
185
|
-
if (!Number.isSafeInteger(n))
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
if (!Number.isSafeInteger(n)) {
|
|
188
|
+
const msg = `Element "${element.name}" does not contain a valid Integer`
|
|
189
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
190
|
+
}
|
|
191
|
+
if (element._type === 'cds.UInt8' && n < 0) {
|
|
192
|
+
const msg = `Element "${element.name}" does not contain a valid positive Integer`
|
|
193
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
194
|
+
}
|
|
189
195
|
return n
|
|
190
196
|
case 'cds.Double':
|
|
191
197
|
return parseFloat(value)
|
|
@@ -260,7 +266,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
260
266
|
) {
|
|
261
267
|
incompleteKeys = false
|
|
262
268
|
} else {
|
|
263
|
-
const msg =
|
|
269
|
+
const msg = `${action.kind[0].toUpperCase() + action.kind.slice(1)} "${action.name}" must be called on a single instance of ${current.name}`
|
|
264
270
|
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
265
271
|
}
|
|
266
272
|
}
|
|
@@ -332,10 +338,8 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
332
338
|
ref[i].where = undefined
|
|
333
339
|
if (ref[i + 1] !== 'Set') {
|
|
334
340
|
// /Set is missing
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
statusCode: 400
|
|
338
|
-
})
|
|
341
|
+
const msg = `Invalid call to "${current.name}". You need to navigate to Set`
|
|
342
|
+
throw cds.error(msg, { code: '400', statusCode: 400 })
|
|
339
343
|
}
|
|
340
344
|
ref[++i] = null
|
|
341
345
|
} else if (current.kind === 'entity') {
|
|
@@ -356,18 +360,25 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
356
360
|
} else if ({ action: 1, function: 1 }[current.kind]) {
|
|
357
361
|
// > action or function
|
|
358
362
|
if (current.kind === 'action' && ref && ref[ref.length - 1]?.where?.length === 0) {
|
|
359
|
-
const msg = `
|
|
363
|
+
const msg = `Parentheses are not allowed for action calls.`
|
|
360
364
|
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
361
365
|
}
|
|
362
366
|
|
|
363
367
|
if (i !== ref.length - 1) {
|
|
364
|
-
const msg = `${i ? 'Unbound' : 'Bound'} ${current.kind} are only supported as the last path segment
|
|
368
|
+
const msg = `${i ? 'Unbound' : 'Bound'} ${current.kind}s are only supported as the last path segment`
|
|
365
369
|
throw Object.assign(new Error(msg), { statusCode: 501 })
|
|
366
370
|
}
|
|
367
371
|
ref[i] = { operation: current.name }
|
|
368
372
|
if (params) ref[i].args = params
|
|
369
373
|
if (current.returns && current.returns._type) one = true
|
|
370
374
|
} else if (current.isAssociation) {
|
|
375
|
+
if (!current._target._service) {
|
|
376
|
+
// not exposed target
|
|
377
|
+
cds.error(`Property '${current.name}' does not exist in type '${target.name.replace(namespace + '.', '')}'`, {
|
|
378
|
+
statusCode: 404
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
|
|
371
382
|
// > navigation
|
|
372
383
|
one = !!(current.is2one || ref[i].where)
|
|
373
384
|
incompleteKeys = one || i === ref.length - 1 ? false : true
|
|
@@ -425,14 +436,8 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
425
436
|
|
|
426
437
|
if (incompleteKeys) {
|
|
427
438
|
// > last segment not fully qualified
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
`Entity "${current.name}" has ${_keysOf(current).length} keys. Only ${keyCount} ${
|
|
431
|
-
keyCount === 1 ? 'was' : 'were'
|
|
432
|
-
} provided.`
|
|
433
|
-
),
|
|
434
|
-
{ statusCode: 400 }
|
|
435
|
-
)
|
|
439
|
+
const msg = `Entity "${current.name}" has ${_keysOf(current).length} keys. Only ${keyCount} ${keyCount === 1 ? 'was' : 'were'} provided.`
|
|
440
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
436
441
|
}
|
|
437
442
|
|
|
438
443
|
// remove all nulled refs
|
|
@@ -441,7 +446,8 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
441
446
|
return { one, current, target }
|
|
442
447
|
}
|
|
443
448
|
|
|
444
|
-
const
|
|
449
|
+
const AGGR_DFLT = '@Aggregation.default'
|
|
450
|
+
const CSTM_AGGR = '@Aggregation.CustomAggregate'
|
|
445
451
|
|
|
446
452
|
function _addKeys(columns, target) {
|
|
447
453
|
let hasAggregatedColumn = false,
|
|
@@ -506,7 +512,7 @@ function _processColumns(cqn, target, protocol) {
|
|
|
506
512
|
|
|
507
513
|
if (!Array.isArray(columns)) return
|
|
508
514
|
|
|
509
|
-
let aggrProp, aggrElem
|
|
515
|
+
let aggrProp, aggrElem, defaultAggregation
|
|
510
516
|
for (let i = 0; i < columns.length; i++) {
|
|
511
517
|
if (
|
|
512
518
|
columns[i].func === null &&
|
|
@@ -518,15 +524,14 @@ function _processColumns(cqn, target, protocol) {
|
|
|
518
524
|
// REVISIT: also support aggregate(Sales/Amount)?
|
|
519
525
|
aggrProp = columns[i].args[0].ref[0]
|
|
520
526
|
aggrElem = target.elements[aggrProp]
|
|
521
|
-
if (
|
|
522
|
-
aggrElem
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
aggrElem[AGGREGATION_DEFAULT]['#']
|
|
526
|
-
) {
|
|
527
|
-
columns[i].func = aggrElem[AGGREGATION_DEFAULT]['#'].toLowerCase()
|
|
527
|
+
if (aggrElem && target[`${CSTM_AGGR}#${aggrProp}`] && aggrElem[AGGR_DFLT] && aggrElem[AGGR_DFLT]['#']) {
|
|
528
|
+
defaultAggregation = aggrElem[AGGR_DFLT]['#'].toLowerCase()
|
|
529
|
+
if (defaultAggregation === 'count_distinct') defaultAggregation = 'countdistinct'
|
|
530
|
+
columns[i].func = defaultAggregation
|
|
528
531
|
columns[i].as = columns[i].as || aggrProp
|
|
529
|
-
} else
|
|
532
|
+
} else {
|
|
533
|
+
throw new Error(`Default aggregation for property "${aggrProp}" not found`)
|
|
534
|
+
}
|
|
530
535
|
}
|
|
531
536
|
}
|
|
532
537
|
}
|
|
@@ -555,26 +560,18 @@ const _checkAllKeysProvided = (params, entity) => {
|
|
|
555
560
|
continue
|
|
556
561
|
}
|
|
557
562
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
entity.name
|
|
562
|
-
}"`
|
|
563
|
-
),
|
|
564
|
-
{ statusCode: 400 }
|
|
565
|
-
)
|
|
563
|
+
// prettier-ignore
|
|
564
|
+
const msg = `${isView ? 'Parameter' : 'Key'} "${keyOfEntity}" is missing for ${isView ? 'view' : 'entity'} "${entity.name}"`
|
|
565
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
566
566
|
}
|
|
567
567
|
}
|
|
568
568
|
}
|
|
569
569
|
|
|
570
570
|
const _doesNotExistError = (isExpand, refName, targetName) => {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
} else {
|
|
576
|
-
throw Object.assign(new Error(`Property '${refName}' does not exist in '${targetName}'`), { statusCode: 400 })
|
|
577
|
-
}
|
|
571
|
+
const msg = isExpand
|
|
572
|
+
? `Navigation property "${refName}" is not defined in ${targetName}`
|
|
573
|
+
: `Property "${refName}" does not exist in ${targetName}`
|
|
574
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
578
575
|
}
|
|
579
576
|
|
|
580
577
|
function _validateXpr(xpr, ignoredColumns, target, isOne, model, aliases = []) {
|
|
@@ -706,8 +703,9 @@ function _4service(service) {
|
|
|
706
703
|
namespace
|
|
707
704
|
)
|
|
708
705
|
// REVISIT: 404 or 400?
|
|
709
|
-
if (!root)
|
|
706
|
+
if (!root) {
|
|
710
707
|
cds.error(`Invalid resource path "${namespace}.${ref[0].id || ref[0]}"`, { code: '404', statusCode: 404 })
|
|
708
|
+
}
|
|
711
709
|
if (ref[0].id) ref[0].id = root.name
|
|
712
710
|
else ref[0] = root.name
|
|
713
711
|
|
|
@@ -714,7 +714,7 @@
|
|
|
714
714
|
function
|
|
715
715
|
= func:functionName OPEN args:functionArgs CLOSE {
|
|
716
716
|
if (strict && !(func.toLowerCase() in strict.functions)) {
|
|
717
|
-
throw Object.assign(new Error(`Function
|
|
717
|
+
throw Object.assign(new Error(`Function "${func}" is not supported`), { statusCode: 501 })
|
|
718
718
|
}
|
|
719
719
|
return { func: func.toLowerCase(), args }
|
|
720
720
|
}
|
|
@@ -723,7 +723,7 @@
|
|
|
723
723
|
= args:(a:operand more:( COMMA o:operand { return o } )* { return [ a, ...more ] })* { return args.length ? args[0] : args }
|
|
724
724
|
|
|
725
725
|
boolish
|
|
726
|
-
= func:("contains"i/"endswith"i/"startswith"i) OPEN a:operand COMMA b:operand CLOSE
|
|
726
|
+
= func:("contains"i/"endswith"i/"startswith"i/"matchespattern"i) OPEN a:operand COMMA b:operand CLOSE
|
|
727
727
|
{ return { func: func.toLowerCase(), args:[a,b] }}
|
|
728
728
|
|
|
729
729
|
NOT = o "NOT"i _ {return 'not'}
|
|
@@ -871,7 +871,7 @@
|
|
|
871
871
|
( "Z" / (("+" / "-")[0-9][0-9]":"[0-9][0-9]) )? // timezone (Z or +-hh:mm)
|
|
872
872
|
)?) {
|
|
873
873
|
if (s.split('-')[0].length > 4)
|
|
874
|
-
throw Object.assign(new Error(`The type
|
|
874
|
+
throw Object.assign(new Error(`The type Edm.DateTimeOffset is not compatible with "${s}"`), { statusCode: 400 })
|
|
875
875
|
return s
|
|
876
876
|
}
|
|
877
877
|
|
|
@@ -10,6 +10,7 @@ const parseStream = async function* (body, boundary) {
|
|
|
10
10
|
|
|
11
11
|
try {
|
|
12
12
|
const boundaries = [boundary]
|
|
13
|
+
let content_id
|
|
13
14
|
let yielded = 0
|
|
14
15
|
const requests = []
|
|
15
16
|
let idCount = 0
|
|
@@ -26,6 +27,7 @@ const parseStream = async function* (body, boundary) {
|
|
|
26
27
|
}
|
|
27
28
|
const newBoundary = req.headers['content-type']?.match(/boundary=(.*)/i)?.[1]
|
|
28
29
|
if (newBoundary) boundaries.push(newBoundary)
|
|
30
|
+
content_id = req.headers['content-id']
|
|
29
31
|
return
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -54,6 +56,7 @@ const parseStream = async function* (body, boundary) {
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
if (boundaries.length > 1) request.atomicityGroup = boundaries.at(-1)
|
|
59
|
+
if (content_id) request.content_id = content_id
|
|
57
60
|
|
|
58
61
|
requests.push(request)
|
|
59
62
|
}
|
|
@@ -69,28 +72,13 @@ const parseStream = async function* (body, boundary) {
|
|
|
69
72
|
.replace(/ \$/g, ' /$')
|
|
70
73
|
|
|
71
74
|
// HACKS!!!
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
let x = changed.substr(m.index)
|
|
81
|
-
let y = x.indexOf(`${CRLF}HEAD`)
|
|
82
|
-
let z = changed.substr(m.index, y)
|
|
83
|
-
const lines = z.split(CRLF)
|
|
84
|
-
if (lines.some(l => l.match(/^content-length/i))) continue
|
|
85
|
-
const cl = lines.slice(-1)[0].length
|
|
86
|
-
if (cl) {
|
|
87
|
-
lines.splice(-2, 0, `content-length:${cl}`)
|
|
88
|
-
changed = changed.substr(0, m.index) + lines.join(CRLF) + x.substr(y)
|
|
89
|
-
}
|
|
90
|
-
} catch (e) {
|
|
91
|
-
console.error(e)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
75
|
+
// ensure URLs start with slashes
|
|
76
|
+
changed = changed.replaceAll(/\r\n(GET|PUT|POST|PATCH|DELETE) (\w)/g, `\r\n$1 /$2`)
|
|
77
|
+
// add content-length headers
|
|
78
|
+
changed = changed.replaceAll(
|
|
79
|
+
/\r\n(.+)\r\nHEAD/g,
|
|
80
|
+
(match, p1) => `content-length: ${Buffer.byteLength(p1)}${CRLF}${match}`
|
|
81
|
+
)
|
|
94
82
|
// remove strange "Group ID" appendix
|
|
95
83
|
changed = changed.split(`${CRLF}Group ID`)[0] + CRLF
|
|
96
84
|
|