@sap/cds 7.7.3 → 7.8.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 +24 -1
- package/lib/auth/ias-auth.js +5 -3
- package/lib/auth/jwt-auth.js +4 -2
- package/lib/compile/cdsc.js +0 -10
- package/lib/compile/for/java.js +9 -5
- package/lib/compile/for/lean_drafts.js +1 -1
- package/lib/compile/to/edm.js +2 -1
- package/lib/compile/to/sql.js +0 -21
- package/lib/compile/to/srvinfo.js +13 -4
- package/lib/dbs/cds-deploy.js +7 -7
- package/lib/env/cds-requires.js +6 -0
- package/lib/index.js +4 -3
- package/lib/linked/classes.js +151 -88
- package/lib/linked/entities.js +27 -23
- package/lib/linked/models.js +57 -36
- package/lib/linked/types.js +42 -104
- package/lib/ql/Whereable.js +3 -3
- package/lib/req/context.js +8 -4
- package/lib/srv/protocols/hcql.js +2 -1
- package/lib/srv/protocols/http.js +7 -7
- package/lib/srv/protocols/index.js +31 -13
- package/lib/srv/protocols/odata-v4.js +79 -58
- package/lib/srv/srv-api.js +7 -6
- package/lib/srv/srv-dispatch.js +1 -12
- package/lib/srv/srv-tx.js +9 -13
- package/lib/utils/cds-utils.js +6 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +11 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +21 -12
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +5 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +3 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +5 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -7
- package/libx/_runtime/cds-services/services/utils/columns.js +6 -3
- package/libx/_runtime/cds.js +0 -13
- package/libx/_runtime/common/generic/input.js +3 -0
- package/libx/_runtime/common/generic/sorting.js +8 -6
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/common/utils/cqn.js +5 -0
- package/libx/_runtime/common/utils/foreignKeyPropagations.js +7 -1
- package/libx/_runtime/common/utils/keys.js +2 -2
- package/libx/_runtime/common/utils/resolveView.js +2 -1
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
- package/libx/_runtime/common/utils/stream.js +0 -10
- package/libx/_runtime/common/utils/template.js +20 -35
- package/libx/_runtime/db/Service.js +5 -1
- package/libx/_runtime/db/utils/columns.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +14 -2
- package/libx/_runtime/messaging/Outbox.js +7 -5
- package/libx/_runtime/messaging/kafka.js +266 -0
- package/libx/_runtime/messaging/service.js +7 -5
- package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +1 -0
- package/libx/common/assert/validation.js +1 -1
- package/libx/odata/index.js +8 -2
- package/libx/odata/middleware/batch.js +340 -0
- package/libx/odata/middleware/create.js +43 -46
- package/libx/odata/middleware/delete.js +27 -15
- package/libx/odata/middleware/error.js +6 -5
- package/libx/odata/middleware/metadata.js +16 -15
- package/libx/odata/middleware/operation.js +107 -59
- package/libx/odata/middleware/parse.js +15 -7
- package/libx/odata/middleware/read.js +150 -24
- package/libx/odata/middleware/service-document.js +17 -6
- package/libx/odata/middleware/stream.js +34 -17
- package/libx/odata/middleware/update.js +123 -87
- package/libx/odata/parse/afterburner.js +131 -28
- package/libx/odata/parse/cqn2odata.js +1 -1
- package/libx/odata/parse/grammar.peggy +4 -5
- package/libx/odata/parse/multipartToJson.js +163 -0
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +29 -47
- package/libx/odata/utils/path.js +72 -0
- package/libx/odata/utils/result.js +123 -20
- package/package.json +1 -1
- package/server.js +4 -0
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
2
|
const { INSERT, UPDATE } = cds.ql
|
|
3
3
|
|
|
4
|
-
const { toODataResult } = require('../utils/result')
|
|
5
|
-
const {
|
|
6
|
-
|
|
4
|
+
const { toODataResult, postProcess } = require('../utils/result')
|
|
5
|
+
const { getKeysAndParamsFromPath, handleSapMessages } = require('../utils')
|
|
7
6
|
const { deepCopy } = require('../../_runtime/common/utils/copy')
|
|
8
7
|
|
|
9
8
|
// REVISIT: move to or rewrite in libx/odata
|
|
@@ -43,33 +42,40 @@ const _isNavigationWithKeyInParent = (keys, data, pathExpression, model) => {
|
|
|
43
42
|
return parent && navElement && where
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
const _hasEtag = target => target._etag
|
|
47
|
-
|
|
48
45
|
module.exports = srv =>
|
|
49
46
|
function update(req, res, next) {
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
// REVISIT: better solution for _propertyAccess
|
|
52
48
|
const {
|
|
53
|
-
SELECT: { one, from }
|
|
54
|
-
|
|
49
|
+
SELECT: { one, from },
|
|
50
|
+
target,
|
|
51
|
+
_propertyAccess
|
|
52
|
+
} = req._query
|
|
55
53
|
|
|
56
54
|
// REVISIT: patch on collection is allowed in odata 4.01
|
|
57
55
|
if (!one) {
|
|
58
|
-
|
|
56
|
+
// REVISIT: don't use "ENTITY.COLLECTION" as that's an okra term
|
|
57
|
+
throw Object.assign(new Error(`Method ${req.method} not allowed for ENTITY.COLLECTION`), {
|
|
58
|
+
statusCode: 405
|
|
59
|
+
})
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
if (_propertyAccess && req.method === 'PATCH') {
|
|
63
|
+
throw Object.assign(new Error(`Method ${req.method} not allowed for PRIMITIVE.PROPERTY`), {
|
|
64
|
+
statusCode: 405
|
|
65
|
+
})
|
|
66
|
+
}
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
// payload & params
|
|
69
|
+
let data = _propertyAccess ? { [_propertyAccess]: req.body.value } : deepCopy(req.body)
|
|
70
|
+
const { keys, params } = getKeysAndParamsFromPath(from, srv)
|
|
71
|
+
// add keys from url into payload (overwriting if already present)
|
|
72
|
+
if (!_propertyAccess) Object.assign(data, keys)
|
|
69
73
|
|
|
74
|
+
// assert payload
|
|
75
|
+
if (!_propertyAccess) {
|
|
70
76
|
// assert complex
|
|
71
77
|
const assertOptions = { filter: true, http: { req }, mandatories: req.method === 'PUT' || undefined }
|
|
72
|
-
const errs = cds.assert(
|
|
78
|
+
const errs = cds.assert(data, target, assertOptions)
|
|
73
79
|
if (errs) {
|
|
74
80
|
if (errs.length === 1) throw errs[0]
|
|
75
81
|
throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
|
|
@@ -78,88 +84,118 @@ module.exports = srv =>
|
|
|
78
84
|
// TODO: assert primitive
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
// query
|
|
88
|
+
let query = UPDATE.entity(from).with(data)
|
|
89
|
+
|
|
90
|
+
// 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 })
|
|
92
|
+
Object.defineProperty(cdsReq, 'protocol', { value: 'odata-v4' })
|
|
82
93
|
|
|
83
|
-
|
|
84
|
-
const cdsReq = new cds.Request({ query: updateQuery })
|
|
94
|
+
let crudEvent = 'UPDATE'
|
|
85
95
|
|
|
86
96
|
// REVISIT: adjust in getter?
|
|
87
97
|
if (req.method === 'PUT') cdsReq.method = 'PUT'
|
|
88
98
|
|
|
89
99
|
// rewrite event for draft-enabled entities
|
|
90
|
-
if (
|
|
100
|
+
if (target._isDraftEnabled) cdsReq.event = 'PATCH'
|
|
91
101
|
|
|
102
|
+
// REVISIT: only via srv.run in combination with srv.dispatch inside
|
|
103
|
+
// we automatically either use a single auto-managed tx for the req (i.e., insert and read after write in same tx)
|
|
104
|
+
// or the auto-managed tx opened for the respective atomicity group, if exists
|
|
92
105
|
return srv
|
|
93
|
-
.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
.run(() => {
|
|
107
|
+
return srv
|
|
108
|
+
.dispatch(cdsReq)
|
|
109
|
+
.catch(async e => {
|
|
110
|
+
// if no UPSERT is allowed, continue with error
|
|
111
|
+
const is404 = e.code === 404 || e.status === 404 || e.statusCode === 404
|
|
112
|
+
|
|
113
|
+
const isForcedInsert =
|
|
114
|
+
(e.code === 412 || e.status === 412 || e.statusCode === 412) && req.headers['if-none-match'] === '*'
|
|
115
|
+
|
|
116
|
+
if (
|
|
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
|
+
})
|
|
171
|
+
})
|
|
172
|
+
.then(result => {
|
|
173
|
+
// we use an extra then block, after getting the result, so the transaction is commited, before sending the response
|
|
99
174
|
|
|
100
175
|
// REVISIT: metaInfo needs original query in case of property access, but why?
|
|
101
|
-
const info = metaInfo(
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
176
|
+
const info = metaInfo(_propertyAccess ? req._query : query, crudEvent, srv, result, req)
|
|
177
|
+
|
|
178
|
+
if (result == null) return res.sendStatus(204)
|
|
179
|
+
|
|
180
|
+
const isMinimal = req._preferReturn === 'minimal'
|
|
181
|
+
postProcess(cdsReq.target, srv, result, isMinimal)
|
|
182
|
+
if (result['$etag']) res.set('etag', result['$etag'])
|
|
183
|
+
|
|
184
|
+
if (crudEvent === 'CREATE') {
|
|
185
|
+
// UPSERT
|
|
186
|
+
return res
|
|
187
|
+
.set('Content-Type', 'application/json;IEEE754Compatible=true')
|
|
188
|
+
.status(201)
|
|
189
|
+
.send(toODataResult(result, info))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (isMinimal || (query._propertyAccess && result[query._propertyAccess] == null) || info.metadata.isStream) {
|
|
109
193
|
return res.sendStatus(204)
|
|
194
|
+
}
|
|
110
195
|
|
|
111
196
|
result = toODataResult(result, info)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.catch(async e => {
|
|
115
|
-
// UPSERT
|
|
116
|
-
const is404 = e.code === 404 || e.status === 404 || e.statusCode === 404
|
|
117
|
-
if (
|
|
118
|
-
is404 &&
|
|
119
|
-
!isPropertyAccess &&
|
|
120
|
-
_isUpsertAllowed({ target: query.target, data: updateData, event: req.method })
|
|
121
|
-
) {
|
|
122
|
-
// PUT / PATCH with if-match header means "only if already exists" -> no insert if it does not
|
|
123
|
-
if (req.headers['if-match']) throw Object.assign(new Error('412'), { statusCode: 412 })
|
|
124
|
-
|
|
125
|
-
// check only works with req.body and not with updateDate
|
|
126
|
-
if (_isNavigationWithKeyInParent(query.target.keys, req.body, from, srv.model)) {
|
|
127
|
-
// REVISIT: better error message
|
|
128
|
-
return res.status(422).json(odataError('422', `Unprocessable Entity`))
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// REVISIT:
|
|
132
|
-
// can we somehow "replay" the request with POST?
|
|
133
|
-
// or should we call the create handler directly?
|
|
134
|
-
|
|
135
|
-
const insertData = deepCopy(req.body)
|
|
136
|
-
|
|
137
|
-
// add keys from url into payload (overwriting if already present)
|
|
138
|
-
Object.assign(insertData, getKeysFromPath(from, srv))
|
|
139
|
-
|
|
140
|
-
// assert payload
|
|
141
|
-
const assertOptions = { filter: true, http: { req }, mandatories: true }
|
|
142
|
-
const errs = cds.assert(insertData, query.target, assertOptions)
|
|
143
|
-
if (errs) {
|
|
144
|
-
if (errs.length === 1) throw errs[0]
|
|
145
|
-
throw Object.assign(new Error('MULTIPLE_ERRORS'), { statusCode: 400, details: errs })
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// REVISIT: up_XX needs to be looked up -> composition of aspect
|
|
149
|
-
const insertQuery = INSERT.into(from).entries(insertData)
|
|
150
|
-
const cdsReq = new cds.Request({ query: insertQuery })
|
|
151
|
-
let result = await srv.dispatch(cdsReq)
|
|
152
|
-
|
|
153
|
-
if (cdsReq._.readAfterWrite) {
|
|
154
|
-
// TODO see if in old odata impl for other checks that should happen
|
|
155
|
-
result = await readAfterWrite(cdsReq, srv, { operation: { result } })
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const info = metaInfo(insertQuery, 'CREATE', srv, result, req)
|
|
159
|
-
result = toODataResult(result, info)
|
|
160
|
-
return res.status(201).send(result)
|
|
161
|
-
}
|
|
162
|
-
throw e
|
|
197
|
+
|
|
198
|
+
return res.set('Content-Type', 'application/json;IEEE754Compatible=true').send(result)
|
|
163
199
|
})
|
|
164
200
|
.catch(next)
|
|
165
201
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const cds = require('../../../')
|
|
2
2
|
|
|
3
|
-
const { where2obj, resolveFromSelect } = require('../../_runtime/common/utils/cqn')
|
|
3
|
+
const { where2obj, resolveFromSelect, targetFromPath } = require('../../_runtime/common/utils/cqn')
|
|
4
4
|
const { findCsnTargetFor } = require('../../_runtime/common/utils/csn')
|
|
5
5
|
const normalizeTimestamp = require('../../_runtime/common/utils/normalizeTimestamp')
|
|
6
6
|
const { rewriteExpandAsterisk } = require('../../_runtime/common/utils/rewriteAsterisks')
|
|
@@ -304,7 +304,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
304
304
|
// REVISIT: replace use case: <namespace>.<entity>_history is at <namespace>.<entity>.history
|
|
305
305
|
current = _getDefinition(current, seg, namespace) || _getDefinition(current, seg.replace(/_/g, '.'), namespace)
|
|
306
306
|
// REVISIT: 404 or 400?
|
|
307
|
-
if (!current) cds.error(`Invalid resource path "${path}"`, { code: 404 })
|
|
307
|
+
if (!current) cds.error(`Invalid resource path "${path}"`, { code: '404', statusCode: 404 })
|
|
308
308
|
|
|
309
309
|
if (current.params && current.kind === 'entity') {
|
|
310
310
|
// > View with params
|
|
@@ -333,8 +333,8 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
333
333
|
if (ref[i + 1] !== 'Set') {
|
|
334
334
|
// /Set is missing
|
|
335
335
|
throw cds.error(`Invalid call to "${current.name}". You need to navigate to Set`, {
|
|
336
|
-
|
|
337
|
-
|
|
336
|
+
code: '400',
|
|
337
|
+
statusCode: 400
|
|
338
338
|
})
|
|
339
339
|
}
|
|
340
340
|
ref[++i] = null
|
|
@@ -479,6 +479,13 @@ function _removeDuplicateAsterisk(columns) {
|
|
|
479
479
|
}
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
+
const _structProperty = (ref, target) => {
|
|
483
|
+
if (target.elements && target.kind === 'element') {
|
|
484
|
+
return _structProperty(ref.slice(1), target.elements[ref[0]])
|
|
485
|
+
}
|
|
486
|
+
return target
|
|
487
|
+
}
|
|
488
|
+
|
|
482
489
|
function _processColumns(cqn, target, protocol) {
|
|
483
490
|
if (cqn.SELECT.from.SELECT) _processColumns(cqn.SELECT.from, target)
|
|
484
491
|
|
|
@@ -530,7 +537,7 @@ const _checkAllKeysProvided = (params, entity) => {
|
|
|
530
537
|
if (isView) {
|
|
531
538
|
// view with params
|
|
532
539
|
if (params === undefined) {
|
|
533
|
-
throw cds.error(`Invalid call to "${entity.name}". You need to navigate to Set`, {
|
|
540
|
+
throw cds.error(`Invalid call to "${entity.name}". You need to navigate to Set`, { code: '400', statusCode: 400 })
|
|
534
541
|
} else if (Object.keys(params).length === 0) {
|
|
535
542
|
throw new Error('KEY_EXPECTED')
|
|
536
543
|
}
|
|
@@ -560,39 +567,120 @@ const _checkAllKeysProvided = (params, entity) => {
|
|
|
560
567
|
}
|
|
561
568
|
}
|
|
562
569
|
|
|
563
|
-
|
|
564
|
-
if (
|
|
570
|
+
const _doesNotExistError = (isExpand, refName, targetName) => {
|
|
571
|
+
if (isExpand) {
|
|
572
|
+
throw Object.assign(new Error(`Navigation property '${refName}' is not defined in '${targetName}'`), {
|
|
573
|
+
statusCode: 400
|
|
574
|
+
})
|
|
575
|
+
} else {
|
|
576
|
+
throw Object.assign(new Error(`Property '${refName}' does not exist in '${targetName}'`), { statusCode: 400 })
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function _validateXpr(xpr, ignoredColumns, target, isOne, model, aliases = []) {
|
|
581
|
+
if (!xpr) return []
|
|
582
|
+
|
|
583
|
+
const _aliases = []
|
|
565
584
|
|
|
566
585
|
for (const x of xpr) {
|
|
586
|
+
if (x.as) _aliases.push(x.as)
|
|
587
|
+
|
|
567
588
|
if (x.xpr) {
|
|
568
|
-
|
|
589
|
+
_validateXpr(x.xpr, ignoredColumns, target, isOne, model)
|
|
569
590
|
continue
|
|
570
591
|
}
|
|
571
592
|
|
|
572
|
-
if (x.ref
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
593
|
+
if (x.ref) {
|
|
594
|
+
const refName = x.ref[0].id ?? x.ref[0]
|
|
595
|
+
|
|
596
|
+
if (x.ref[0].where) {
|
|
597
|
+
const element = target.elements[refName]
|
|
598
|
+
|
|
599
|
+
if (!element) {
|
|
600
|
+
_doesNotExistError(true, refName, target.name)
|
|
579
601
|
}
|
|
580
|
-
|
|
602
|
+
_validateXpr(x.ref[0].where, ignoredColumns, element._target ?? element.items, isOne, model)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (ignoredColumns.includes(refName) || (!target.elements[refName] && !aliases.includes(refName))) {
|
|
606
|
+
_doesNotExistError(x.expand, refName, target.name)
|
|
607
|
+
} else if (x.ref.length > 1) {
|
|
608
|
+
const element = target.elements[refName]
|
|
609
|
+
|
|
610
|
+
if (element.isAssociation) {
|
|
611
|
+
// navigation
|
|
612
|
+
const _target = element._target
|
|
613
|
+
const _ignoredColumns = Object.values(_target.elements ?? {})
|
|
614
|
+
.filter(element => element['@cds.api.ignore'])
|
|
615
|
+
.map(element => element.name)
|
|
616
|
+
if (element.is2one) {
|
|
617
|
+
_validateXpr([{ ref: x.ref.slice(1) }], _ignoredColumns, _target, false, model)
|
|
618
|
+
} else {
|
|
619
|
+
_validateXpr([{ ref: x.ref.slice(1) }], _ignoredColumns, _target, false, model)
|
|
620
|
+
}
|
|
621
|
+
} else if (element.kind === 'element') {
|
|
622
|
+
// structured
|
|
623
|
+
_validateXpr([{ ref: x.ref.slice(1) }], ignoredColumns, element, isOne, model)
|
|
624
|
+
} else {
|
|
625
|
+
throw new Error('not yet validated')
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (x.expand) {
|
|
629
|
+
let element = target.elements[refName]
|
|
630
|
+
if (element.kind === 'element' && element.elements) {
|
|
631
|
+
// structured
|
|
632
|
+
_validateXpr([{ ref: x.ref.slice(1) }], ignoredColumns, element, isOne, model)
|
|
633
|
+
element = _structProperty(x.ref.slice(1), element)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const _ignoredColumns = Object.values(element._target.elements ?? {})
|
|
637
|
+
.filter(element => element['@cds.api.ignore'])
|
|
638
|
+
.map(element => element.name)
|
|
639
|
+
_validateXpr(x.expand, _ignoredColumns, element._target, false, model)
|
|
640
|
+
|
|
641
|
+
if (x.where) {
|
|
642
|
+
_validateXpr(x.where, _ignoredColumns, element._target, false, model)
|
|
643
|
+
}
|
|
644
|
+
if (x.orderBy) {
|
|
645
|
+
_validateXpr(x.orderBy, _ignoredColumns, element._target, false, model)
|
|
646
|
+
}
|
|
647
|
+
}
|
|
581
648
|
}
|
|
582
649
|
|
|
583
650
|
if (x.func) {
|
|
584
|
-
|
|
651
|
+
_validateXpr(x.args, ignoredColumns, target, isOne, model)
|
|
585
652
|
continue
|
|
586
653
|
}
|
|
654
|
+
|
|
655
|
+
if (x.SELECT) {
|
|
656
|
+
const { target } = targetFromPath(x.SELECT.from, model)
|
|
657
|
+
const _ignoredColumns = Object.values(target.elements ?? {})
|
|
658
|
+
.filter(element => element['@cds.api.ignore'])
|
|
659
|
+
.map(element => element.name)
|
|
660
|
+
_validateQuery(x.SELECT, _ignoredColumns, target, x.SELECT.one, model)
|
|
661
|
+
}
|
|
587
662
|
}
|
|
663
|
+
return _aliases
|
|
588
664
|
}
|
|
589
665
|
|
|
590
|
-
function
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
666
|
+
function _validateQuery(SELECT, ignoredColumns, target, isOne, model) {
|
|
667
|
+
const aliases = []
|
|
668
|
+
if (SELECT.from.SELECT) {
|
|
669
|
+
const { target } = targetFromPath(SELECT.from.SELECT.from, model)
|
|
670
|
+
const _ignoredColumns = Object.values(target.elements ?? {})
|
|
671
|
+
.filter(element => element['@cds.api.ignore'])
|
|
672
|
+
.map(element => element.name)
|
|
673
|
+
const subselectAliases = _validateQuery(SELECT.from.SELECT, _ignoredColumns, target, SELECT.from.SELECT.one, model)
|
|
674
|
+
aliases.push(...subselectAliases)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const columnAliases = _validateXpr(SELECT.columns, ignoredColumns, target, isOne, model)
|
|
678
|
+
aliases.push(...columnAliases)
|
|
679
|
+
_validateXpr(SELECT.orderBy, ignoredColumns, target, isOne, model, aliases)
|
|
680
|
+
_validateXpr(SELECT.where, ignoredColumns, target, isOne, model, aliases)
|
|
681
|
+
_validateXpr(SELECT.groupBy, ignoredColumns, target, isOne, model, aliases)
|
|
682
|
+
_validateXpr(SELECT.having, ignoredColumns, target, isOne, model, aliases)
|
|
683
|
+
return aliases
|
|
596
684
|
}
|
|
597
685
|
|
|
598
686
|
function _4service(service) {
|
|
@@ -618,7 +706,8 @@ function _4service(service) {
|
|
|
618
706
|
namespace
|
|
619
707
|
)
|
|
620
708
|
// REVISIT: 404 or 400?
|
|
621
|
-
if (!root)
|
|
709
|
+
if (!root)
|
|
710
|
+
cds.error(`Invalid resource path "${namespace}.${ref[0].id || ref[0]}"`, { code: '404', statusCode: 404 })
|
|
622
711
|
if (ref[0].id) ref[0].id = root.name
|
|
623
712
|
else ref[0] = root.name
|
|
624
713
|
|
|
@@ -627,6 +716,18 @@ function _4service(service) {
|
|
|
627
716
|
*/
|
|
628
717
|
const { one, current, target } = _processSegments(from, model, namespace, cqn, protocol)
|
|
629
718
|
|
|
719
|
+
if (cds.env.effective.odata.proxies && cds.env.effective.odata.xrefs && target) {
|
|
720
|
+
if (!target._service) {
|
|
721
|
+
// proxy navigation, add keys as columns only
|
|
722
|
+
const columns = []
|
|
723
|
+
for (const key in target.keys) {
|
|
724
|
+
if (target.keys[key].isAssociation) continue
|
|
725
|
+
columns.push({ ref: [key] })
|
|
726
|
+
}
|
|
727
|
+
cqn.SELECT.columns = columns
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
630
731
|
if (cqn.SELECT.where) {
|
|
631
732
|
_processWhere(cqn.SELECT.where, root)
|
|
632
733
|
}
|
|
@@ -646,11 +747,13 @@ function _4service(service) {
|
|
|
646
747
|
*/
|
|
647
748
|
_processColumns(cqn, current, protocol)
|
|
648
749
|
|
|
649
|
-
|
|
650
|
-
.
|
|
651
|
-
|
|
750
|
+
if (target && cds.env.features.odata_new_parser) {
|
|
751
|
+
const ignoredColumns = Object.values(target.elements ?? {})
|
|
752
|
+
.filter(element => element['@cds.api.ignore'])
|
|
753
|
+
.map(element => element.name)
|
|
652
754
|
|
|
653
|
-
|
|
755
|
+
_validateQuery(cqn.SELECT, ignoredColumns, target, one, model)
|
|
756
|
+
}
|
|
654
757
|
|
|
655
758
|
return cqn
|
|
656
759
|
}
|
|
@@ -254,7 +254,7 @@ function _getQueryTarget(entity, propOrEntity, model) {
|
|
|
254
254
|
|
|
255
255
|
const _params = (args, kind, target) => {
|
|
256
256
|
if (!args) {
|
|
257
|
-
throw cds.error(`Invalid call to "${target.name}". You need to navigate to Set`, {
|
|
257
|
+
throw cds.error(`Invalid call to "${target.name}". You need to navigate to Set`, { code: '400', statusCode: 400 })
|
|
258
258
|
}
|
|
259
259
|
const params = Object.keys(args)
|
|
260
260
|
if (params.length !== Object.keys(target.params).length) {
|
|
@@ -485,10 +485,9 @@
|
|
|
485
485
|
search_clause
|
|
486
486
|
= p:( n:NOT? {return n?[n]:[]} )(
|
|
487
487
|
OPEN xpr:search_clause CLOSE {p.push({xpr})}
|
|
488
|
-
/ (
|
|
488
|
+
/ (
|
|
489
489
|
val:doubleQuotedString {p.push({val})} /
|
|
490
|
-
val:
|
|
491
|
-
val:word {p.push({val})}
|
|
490
|
+
!"'" val:word {p.push({val})}
|
|
492
491
|
)
|
|
493
492
|
)( ao:(AND/OR/AND_SPACE) more:search_clause {p.push(ao,...more)} )*
|
|
494
493
|
{ return p }
|
|
@@ -715,7 +714,7 @@
|
|
|
715
714
|
function
|
|
716
715
|
= func:functionName OPEN args:functionArgs CLOSE {
|
|
717
716
|
if (strict && !(func.toLowerCase() in strict.functions)) {
|
|
718
|
-
throw Object.assign(new Error(`
|
|
717
|
+
throw Object.assign(new Error(`Function '${func}' is not supported`), { statusCode: 400 })
|
|
719
718
|
}
|
|
720
719
|
return { func: func.toLowerCase(), args }
|
|
721
720
|
}
|
|
@@ -857,7 +856,7 @@
|
|
|
857
856
|
{return s.replace(/''/g,"'")}
|
|
858
857
|
|
|
859
858
|
doubleQuotedString "a doubled quoted string"
|
|
860
|
-
= '"' s:$('\\"'/[^"])* '"'
|
|
859
|
+
= '"' s:$('\\"' / '\\\\' / [^"])* '"'
|
|
861
860
|
{return s.replace(/\\\\/g,"\\").replace(/\\"/g,'"')}
|
|
862
861
|
|
|
863
862
|
word "a string"
|