@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/lib/auth/ias-auth.js +5 -3
  3. package/lib/auth/jwt-auth.js +4 -2
  4. package/lib/compile/cdsc.js +0 -10
  5. package/lib/compile/for/java.js +9 -5
  6. package/lib/compile/for/lean_drafts.js +1 -1
  7. package/lib/compile/to/edm.js +2 -1
  8. package/lib/compile/to/sql.js +0 -21
  9. package/lib/compile/to/srvinfo.js +13 -4
  10. package/lib/dbs/cds-deploy.js +7 -7
  11. package/lib/env/cds-requires.js +6 -0
  12. package/lib/index.js +4 -3
  13. package/lib/linked/classes.js +151 -88
  14. package/lib/linked/entities.js +27 -23
  15. package/lib/linked/models.js +57 -36
  16. package/lib/linked/types.js +42 -104
  17. package/lib/ql/Whereable.js +3 -3
  18. package/lib/req/context.js +8 -4
  19. package/lib/srv/protocols/hcql.js +2 -1
  20. package/lib/srv/protocols/http.js +7 -7
  21. package/lib/srv/protocols/index.js +31 -13
  22. package/lib/srv/protocols/odata-v4.js +79 -58
  23. package/lib/srv/srv-api.js +7 -6
  24. package/lib/srv/srv-dispatch.js +1 -12
  25. package/lib/srv/srv-tx.js +9 -13
  26. package/lib/utils/cds-utils.js +6 -5
  27. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +11 -8
  28. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +21 -12
  29. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +5 -3
  30. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +3 -7
  31. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +5 -0
  32. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +2 -1
  33. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -7
  34. package/libx/_runtime/cds-services/services/utils/columns.js +6 -3
  35. package/libx/_runtime/cds.js +0 -13
  36. package/libx/_runtime/common/generic/input.js +3 -0
  37. package/libx/_runtime/common/generic/sorting.js +8 -6
  38. package/libx/_runtime/common/i18n/messages.properties +1 -0
  39. package/libx/_runtime/common/utils/cqn.js +5 -0
  40. package/libx/_runtime/common/utils/foreignKeyPropagations.js +7 -1
  41. package/libx/_runtime/common/utils/keys.js +2 -2
  42. package/libx/_runtime/common/utils/resolveView.js +2 -1
  43. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
  44. package/libx/_runtime/common/utils/stream.js +0 -10
  45. package/libx/_runtime/common/utils/template.js +20 -35
  46. package/libx/_runtime/db/Service.js +5 -1
  47. package/libx/_runtime/db/utils/columns.js +1 -1
  48. package/libx/_runtime/fiori/lean-draft.js +14 -2
  49. package/libx/_runtime/messaging/Outbox.js +7 -5
  50. package/libx/_runtime/messaging/kafka.js +266 -0
  51. package/libx/_runtime/messaging/service.js +7 -5
  52. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +1 -0
  53. package/libx/common/assert/validation.js +1 -1
  54. package/libx/odata/index.js +8 -2
  55. package/libx/odata/middleware/batch.js +340 -0
  56. package/libx/odata/middleware/create.js +43 -46
  57. package/libx/odata/middleware/delete.js +27 -15
  58. package/libx/odata/middleware/error.js +6 -5
  59. package/libx/odata/middleware/metadata.js +16 -15
  60. package/libx/odata/middleware/operation.js +107 -59
  61. package/libx/odata/middleware/parse.js +15 -7
  62. package/libx/odata/middleware/read.js +150 -24
  63. package/libx/odata/middleware/service-document.js +17 -6
  64. package/libx/odata/middleware/stream.js +34 -17
  65. package/libx/odata/middleware/update.js +123 -87
  66. package/libx/odata/parse/afterburner.js +131 -28
  67. package/libx/odata/parse/cqn2odata.js +1 -1
  68. package/libx/odata/parse/grammar.peggy +4 -5
  69. package/libx/odata/parse/multipartToJson.js +163 -0
  70. package/libx/odata/parse/parser.js +1 -1
  71. package/libx/odata/utils/index.js +29 -47
  72. package/libx/odata/utils/path.js +72 -0
  73. package/libx/odata/utils/result.js +123 -20
  74. package/package.json +1 -1
  75. 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 { odataError, getKeysFromPath } = require('../utils')
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
- const { _query: query } = req
51
-
47
+ // REVISIT: better solution for _propertyAccess
52
48
  const {
53
- SELECT: { one, from }
54
- } = query
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
- return res.status(405).json(odataError('405', `Method ${req.method} not allowed for ENTITY.COLLECTION`))
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
- // REVISIT: better
62
- const isPropertyAccess = !!query._propertyAccess
63
-
64
- const updateData = isPropertyAccess ? { [query._propertyAccess]: req.body.value } : deepCopy(req.body)
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
- if (!isPropertyAccess) {
67
- // add keys from url into payload (overwriting if already present)
68
- Object.assign(updateData, getKeysFromPath(from, srv))
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(updateData, query.target, assertOptions)
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
- const updateQuery = UPDATE.entity(from).with(updateData)
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
- // we need the cds request, so we can access req._.readAfterWrite
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 (query.target._isDraftEnabled) cdsReq.event = 'PATCH'
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
- .dispatch(cdsReq)
94
- .then(async result => {
95
- if (!(isPropertyAccess && !_hasEtag(query.target)) && cdsReq._.readAfterWrite) {
96
- // TODO see if in old odata impl for other checks that should happen
97
- result = await readAfterWrite(cdsReq, srv, { operation: { result } })
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(isPropertyAccess ? query : updateQuery, 'UPDATE', srv, result, req)
102
-
103
- if (
104
- result == null ||
105
- req._preferReturn === 'minimal' ||
106
- (isPropertyAccess && result[query._propertyAccess] == null) ||
107
- info.metadata.isStream
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
- return res.send(result)
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
- statusCode: 400,
337
- code: 400
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`, { statusCode: 400, code: 400 })
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
- function _cleanupIgnoredXpr(xpr, ignoredColumns, target, isOne) {
564
- if (!xpr) return
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
- _cleanupIgnoredXpr(x.xpr, ignoredColumns, target, isOne)
589
+ _validateXpr(x.xpr, ignoredColumns, target, isOne, model)
569
590
  continue
570
591
  }
571
592
 
572
- if (x.ref && ignoredColumns.includes(x.ref[0])) {
573
- cds.error(
574
- `Property '${x.ref}' does not exist in type '${
575
- isOne ? target.name.replace(`${target._service.name}.`, '') : target.name
576
- }'`,
577
- {
578
- code: 400
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
- _cleanupIgnoredXpr(x.args, ignoredColumns, target, isOne)
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 _cleanupIgnored(SELECT, ignoredColumns, target, isOne) {
591
- _cleanupIgnoredXpr(SELECT.columns, ignoredColumns, target, isOne)
592
- _cleanupIgnoredXpr(SELECT.orderBy, ignoredColumns, target, isOne)
593
- _cleanupIgnoredXpr(SELECT.where, ignoredColumns, target, isOne)
594
- _cleanupIgnoredXpr(SELECT.groupBy, ignoredColumns, target, isOne)
595
- _cleanupIgnoredXpr(SELECT.having, ignoredColumns, target, isOne)
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) cds.error(`Invalid resource path "${namespace}.${ref[0].id || ref[0]}"`, { code: 404 })
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
- const ignoredColumns = Object.values(root.elements ?? {})
650
- .filter(element => element['@cds.api.ignore'])
651
- .map(element => element.name)
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
- _cleanupIgnored(cqn.SELECT, ignoredColumns, root, one)
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`, { status: 400, code: 400 })
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:string {p.push({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(`"${func}" is an unknown function in OData URL spec (strict mode)`), { statusCode: 400 })
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"