@sap/cds 9.4.4 → 9.5.1
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 +81 -1
- package/_i18n/messages_en_US_saptrc.properties +1 -1
- package/common.cds +5 -2
- package/lib/compile/cds-compile.js +1 -0
- package/lib/compile/for/assert.js +64 -0
- package/lib/compile/for/flows.js +194 -58
- package/lib/compile/for/lean_drafts.js +75 -7
- package/lib/compile/parse.js +1 -1
- package/lib/compile/to/csn.js +6 -2
- package/lib/compile/to/edm.js +1 -1
- package/lib/compile/to/yaml.js +8 -1
- package/lib/dbs/cds-deploy.js +2 -2
- package/lib/env/cds-env.js +14 -4
- package/lib/env/defaults.js +6 -1
- package/lib/i18n/localize.js +1 -1
- package/lib/index.js +7 -7
- package/lib/req/event.js +4 -0
- package/lib/req/validate.js +4 -1
- package/lib/srv/cds.Service.js +2 -1
- package/lib/srv/middlewares/auth/ias-auth.js +5 -7
- package/lib/srv/middlewares/auth/index.js +1 -1
- package/lib/srv/protocols/index.js +7 -6
- package/lib/srv/srv-handlers.js +7 -0
- package/libx/_runtime/common/Service.js +5 -1
- package/libx/_runtime/common/constants/events.js +1 -0
- package/libx/_runtime/common/generic/assert.js +220 -0
- package/libx/_runtime/common/generic/flows.js +168 -108
- package/libx/_runtime/common/generic/input.js +6 -4
- package/libx/_runtime/common/utils/cqn.js +0 -24
- package/libx/_runtime/common/utils/normalizeTimestamp.js +2 -2
- package/libx/_runtime/common/utils/resolveView.js +8 -2
- package/libx/_runtime/common/utils/templateProcessor.js +10 -1
- package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +21 -9
- package/libx/_runtime/fiori/lean-draft.js +511 -379
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +39 -35
- package/libx/_runtime/messaging/enterprise-messaging.js +2 -2
- package/libx/_runtime/remote/Service.js +4 -5
- package/libx/_runtime/ucl/Service.js +111 -15
- package/libx/common/utils/streaming.js +1 -1
- package/libx/odata/middleware/batch.js +8 -6
- package/libx/odata/middleware/create.js +2 -2
- package/libx/odata/middleware/delete.js +2 -2
- package/libx/odata/middleware/metadata.js +18 -11
- package/libx/odata/middleware/read.js +2 -2
- package/libx/odata/middleware/service-document.js +1 -1
- package/libx/odata/middleware/update.js +1 -1
- package/libx/odata/parse/afterburner.js +46 -36
- package/libx/odata/parse/cqn2odata.js +2 -6
- package/libx/odata/parse/grammar.peggy +91 -13
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/index.js +2 -2
- package/libx/odata/utils/readAfterWrite.js +2 -0
- package/libx/queue/TaskRunner.js +26 -1
- package/libx/queue/index.js +11 -1
- package/package.json +1 -1
- package/srv/ucl-service.cds +2 -0
|
@@ -150,19 +150,19 @@ function _convertVal(value, element) {
|
|
|
150
150
|
case 'cds.Int32':
|
|
151
151
|
if (!/^-?\+?\d+$/.test(value)) {
|
|
152
152
|
const msg = `Element "${element.name}" does not contain a valid Integer`
|
|
153
|
-
|
|
153
|
+
cds.error({ status: 400, message: msg })
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// eslint-disable-next-line no-case-declarations
|
|
157
157
|
const n = Number(value)
|
|
158
158
|
if (!Number.isSafeInteger(n)) {
|
|
159
159
|
const msg = `Element "${element.name}" does not contain a valid Integer`
|
|
160
|
-
|
|
160
|
+
cds.error({ status: 400, message: msg })
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
if (element._type === 'cds.UInt8' && n < 0) {
|
|
164
164
|
const msg = `Element "${element.name}" does not contain a valid positive Integer`
|
|
165
|
-
|
|
165
|
+
cds.error({ status: 400, message: msg })
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
return n
|
|
@@ -191,7 +191,7 @@ function _convertVal(value, element) {
|
|
|
191
191
|
case 'cds.UUID':
|
|
192
192
|
if (!RELAXED_UUID_REGEX.test(value)) {
|
|
193
193
|
const msg = `Element "${element.name}" does not contain a valid UUID`
|
|
194
|
-
|
|
194
|
+
cds.error({ status: 400, message: msg })
|
|
195
195
|
}
|
|
196
196
|
return value
|
|
197
197
|
|
|
@@ -230,10 +230,9 @@ const getStructTargetName = element => {
|
|
|
230
230
|
const _getDataFromParams = (params, operation) => {
|
|
231
231
|
try {
|
|
232
232
|
return Object.keys(params).reduce((acc, cur) => {
|
|
233
|
+
const def = operation.params?.[cur]
|
|
233
234
|
acc[cur] =
|
|
234
|
-
typeof params[cur] === 'string' && (
|
|
235
|
-
? JSON.parse(params[cur])
|
|
236
|
-
: params[cur]
|
|
235
|
+
typeof params[cur] === 'string' && (def?.elements || def?.items) ? JSON.parse(params[cur]) : params[cur]
|
|
237
236
|
return acc
|
|
238
237
|
}, {})
|
|
239
238
|
} catch (e) {
|
|
@@ -262,7 +261,7 @@ function _handleCollectionBoundActions(current, ref, i, namespace, one) {
|
|
|
262
261
|
const msg = `${action.kind.at(0).toUpperCase() + action.kind.slice(1)} "${
|
|
263
262
|
action.name
|
|
264
263
|
}" must be called on a collection of ${current.name}`
|
|
265
|
-
|
|
264
|
+
cds.error({ status: 400, message: msg })
|
|
266
265
|
}
|
|
267
266
|
|
|
268
267
|
if (incompleteKeys) {
|
|
@@ -270,7 +269,7 @@ function _handleCollectionBoundActions(current, ref, i, namespace, one) {
|
|
|
270
269
|
const msg = `${action.kind.at(0).toUpperCase() + action.kind.slice(1)} "${
|
|
271
270
|
action.name
|
|
272
271
|
}" must be called on a single instance of ${current.name}`
|
|
273
|
-
|
|
272
|
+
cds.error({ status: 400, message: msg })
|
|
274
273
|
}
|
|
275
274
|
|
|
276
275
|
incompleteKeys = false
|
|
@@ -337,7 +336,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
337
336
|
|
|
338
337
|
ref[i] = null
|
|
339
338
|
ref[i - keyCount] = base
|
|
340
|
-
incompleteKeys = keyCount < keys.length
|
|
339
|
+
incompleteKeys = keyCount < keys.filter(k => k !== 'IsActiveEntity').length
|
|
341
340
|
} else {
|
|
342
341
|
// > entity or property (incl. nested) or navigation or action or function
|
|
343
342
|
keys = null
|
|
@@ -363,7 +362,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
363
362
|
} else {
|
|
364
363
|
// parentheses are missing
|
|
365
364
|
const msg = `Invalid call to "${current.name}". Parentheses are missing`
|
|
366
|
-
|
|
365
|
+
cds.error({ status: 400, message: msg })
|
|
367
366
|
}
|
|
368
367
|
|
|
369
368
|
_addDefaultParams(ref[i], current)
|
|
@@ -384,7 +383,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
384
383
|
if (ref[i + 1] !== 'Set') {
|
|
385
384
|
// /Set is missing
|
|
386
385
|
const msg = `Invalid call to "${current.name}". You need to navigate to Set`
|
|
387
|
-
|
|
386
|
+
cds.error({ status: 400, message: msg })
|
|
388
387
|
}
|
|
389
388
|
|
|
390
389
|
ref[++i] = null
|
|
@@ -406,19 +405,19 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
406
405
|
|
|
407
406
|
if (keyCount === 0 && !Object.keys(params).length && whereRef.length === 1) {
|
|
408
407
|
const msg = `Entity "${current.name}" can not be accessed by key.`
|
|
409
|
-
|
|
408
|
+
cds.error({ status: 400, message: msg })
|
|
410
409
|
}
|
|
411
410
|
}
|
|
412
411
|
} else if ({ action: 1, function: 1 }[current.kind]) {
|
|
413
412
|
// > action or function
|
|
414
413
|
if (current.kind === 'action' && ref && ref.at(-1)?.where?.length === 0) {
|
|
415
414
|
const msg = `Parentheses are not allowed for action calls.`
|
|
416
|
-
|
|
415
|
+
cds.error({ status: 400, message: msg })
|
|
417
416
|
}
|
|
418
417
|
|
|
419
418
|
if (i !== ref.length - 1) {
|
|
420
419
|
const msg = `${i ? 'Bound' : 'Unbound'} ${current.kind}s are only supported as the last path segment`
|
|
421
|
-
|
|
420
|
+
cds.error({ status: 400, message: msg })
|
|
422
421
|
}
|
|
423
422
|
|
|
424
423
|
ref[i] = { operation: current.name }
|
|
@@ -449,10 +448,11 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
449
448
|
target = current.returns.items ?? current.returns
|
|
450
449
|
}
|
|
451
450
|
} else if (current.isAssociation) {
|
|
452
|
-
if (
|
|
451
|
+
if (current._target._service !== _get_service_of(target)) {
|
|
453
452
|
// not exposed target
|
|
454
|
-
cds.error(
|
|
455
|
-
|
|
453
|
+
cds.error({
|
|
454
|
+
status: 400,
|
|
455
|
+
message: `Property "${current.name}" does not exist in "${target.name.replace(namespace + '.', '')}"`
|
|
456
456
|
})
|
|
457
457
|
}
|
|
458
458
|
|
|
@@ -503,7 +503,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
503
503
|
const propRef = ref.slice(i)
|
|
504
504
|
if (propRef[0].where?.length === 0) {
|
|
505
505
|
const msg = 'Parentheses are not allowed when addressing properties.'
|
|
506
|
-
|
|
506
|
+
cds.error({ status: 400, message: msg })
|
|
507
507
|
}
|
|
508
508
|
cqn.SELECT.columns.push({ ref: propRef })
|
|
509
509
|
|
|
@@ -527,7 +527,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
527
527
|
const msg = `Entity "${current.name}" has ${keysOf(current).length} keys. Only ${keyCount} ${
|
|
528
528
|
keyCount === 1 ? 'was' : 'were'
|
|
529
529
|
} provided.`
|
|
530
|
-
|
|
530
|
+
cds.error({ status: 400, message: msg })
|
|
531
531
|
}
|
|
532
532
|
|
|
533
533
|
// remove all nulled refs
|
|
@@ -607,9 +607,9 @@ function _processColumns(cqn, target, protocol) {
|
|
|
607
607
|
|
|
608
608
|
// Error if groupBy is present but no columns are selected
|
|
609
609
|
if (columns && !columns.length && cqn.SELECT.groupBy) {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
610
|
+
cds.error({
|
|
611
|
+
status: 400,
|
|
612
|
+
message: 'Explicit select must include at least one column available in the result set of groupby'
|
|
613
613
|
})
|
|
614
614
|
}
|
|
615
615
|
|
|
@@ -695,9 +695,8 @@ function _processColumns(cqn, target, protocol) {
|
|
|
695
695
|
if (processedColumn.func === null) {
|
|
696
696
|
processedColumn.func = aggregatedElement?.[AGGR_DFLT]?.['#']?.toLowerCase()
|
|
697
697
|
if (!customAggregate)
|
|
698
|
-
|
|
699
|
-
if (!processedColumn.func)
|
|
700
|
-
throw cds.error(`Default aggregation for property "${aggregatedPropertyName}" not found`)
|
|
698
|
+
cds.error(`Result type for custom aggregation of property "${aggregatedPropertyName}" not found`)
|
|
699
|
+
if (!processedColumn.func) cds.error(`Default aggregation for property "${aggregatedPropertyName}" not found`)
|
|
701
700
|
if (processedColumn.func === 'count_distinct') processedColumn.func = 'countdistinct'
|
|
702
701
|
}
|
|
703
702
|
|
|
@@ -705,7 +704,7 @@ function _processColumns(cqn, target, protocol) {
|
|
|
705
704
|
const semanticsAmountElementName = aggregatedElement?.[SMTCS_AMT_CC] ?? aggregatedElement?.[SMTCS_AMT_UOM]
|
|
706
705
|
if (!semanticsAmountElementName) continue
|
|
707
706
|
if (!target.elements[semanticsAmountElementName])
|
|
708
|
-
|
|
707
|
+
cds.error(`Referenced semantics amount element not found: ${semanticsAmountElementName}`)
|
|
709
708
|
const semanticsAmountElementRef = [...prefixRef, semanticsAmountElementName]
|
|
710
709
|
|
|
711
710
|
columns[i] = {
|
|
@@ -737,7 +736,7 @@ const _checkAllKeysProvided = (params, entity) => {
|
|
|
737
736
|
if (isView) {
|
|
738
737
|
// view with params
|
|
739
738
|
if (params === undefined) {
|
|
740
|
-
|
|
739
|
+
cds.error({ status: 400, message: `Invalid call to "${entity.name}". You need to navigate to Set` })
|
|
741
740
|
}
|
|
742
741
|
|
|
743
742
|
keysOfEntity = Object.keys(entity.params)
|
|
@@ -747,7 +746,7 @@ const _checkAllKeysProvided = (params, entity) => {
|
|
|
747
746
|
|
|
748
747
|
if (!keysOfEntity) return
|
|
749
748
|
for (const keyOfEntity of keysOfEntity) {
|
|
750
|
-
if (!(keyOfEntity in params)) {
|
|
749
|
+
if (keyOfEntity !== 'IsActiveEntity' && !(keyOfEntity in params)) {
|
|
751
750
|
if (isView && entity.params[keyOfEntity].default) {
|
|
752
751
|
// will be added later?
|
|
753
752
|
continue
|
|
@@ -755,16 +754,22 @@ const _checkAllKeysProvided = (params, entity) => {
|
|
|
755
754
|
|
|
756
755
|
// prettier-ignore
|
|
757
756
|
const msg = `${isView ? 'Parameter' : 'Key'} "${keyOfEntity}" is missing for ${isView ? 'view' : 'entity'} "${entity.name}"`
|
|
758
|
-
|
|
757
|
+
cds.error({ status: 400, message: msg })
|
|
759
758
|
}
|
|
760
759
|
}
|
|
761
760
|
}
|
|
762
761
|
|
|
763
762
|
const _doesNotExistError = (isExpand, refName, targetName, targetKind) => {
|
|
764
763
|
const msg = isExpand
|
|
765
|
-
? `Navigation property "${refName}"
|
|
764
|
+
? `Navigation property "${refName}" does not exist in "${targetName}"`
|
|
766
765
|
: `Property "${refName}" does not exist in ${targetKind === 'type' ? 'type ' : ''}"${targetName}"`
|
|
767
|
-
|
|
766
|
+
cds.error({ status: 400, message: msg })
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const _get_service_of = element => {
|
|
770
|
+
if (element._service) return element._service
|
|
771
|
+
if (element.parent) return _get_service_of(element.parent)
|
|
772
|
+
return null
|
|
768
773
|
}
|
|
769
774
|
|
|
770
775
|
function _validateXpr(xpr, target, isOne, model, aliases = []) {
|
|
@@ -818,8 +823,13 @@ function _validateXpr(xpr, target, isOne, model, aliases = []) {
|
|
|
818
823
|
_validateXpr([{ ref: x.ref.slice(1) }], element, isOne, model)
|
|
819
824
|
element = _structProperty(x.ref.slice(1), element)
|
|
820
825
|
}
|
|
821
|
-
|
|
822
|
-
|
|
826
|
+
const target_service = _get_service_of(target)
|
|
827
|
+
if (!element._target || element._target._service !== target_service) {
|
|
828
|
+
_doesNotExistError(
|
|
829
|
+
true,
|
|
830
|
+
refName,
|
|
831
|
+
target_service ? target.name.replace(target_service.name + '.', '') : target.name
|
|
832
|
+
)
|
|
823
833
|
}
|
|
824
834
|
_validateXpr(x.expand, element._target, false, model)
|
|
825
835
|
if (x.where) {
|
|
@@ -872,14 +882,14 @@ module.exports = (cqn, model, namespace, protocol) => {
|
|
|
872
882
|
let edmName = ref[0].id || ref[0]
|
|
873
883
|
// REVISIT: shouldn't be necessary
|
|
874
884
|
if (edmName.split('.').length > 1)
|
|
875
|
-
//required for concat query, where the root is already identified with the first query and subsequent queries already have correct root
|
|
885
|
+
// required for concat query, where the root is already identified with the first query and subsequent queries already have correct root
|
|
876
886
|
edmName = edmName.split('.')[edmName.split('.').length - 1]
|
|
877
887
|
|
|
878
888
|
// Make first path segment fully qualified
|
|
879
889
|
const root = findCsnTargetFor(edmName, model, namespace)
|
|
880
890
|
|
|
881
891
|
if (!root) {
|
|
882
|
-
//404 else we would expose knowledge to potential attackers
|
|
892
|
+
// 404 else we would expose knowledge to potential attackers
|
|
883
893
|
throw new cds.error(404, `Invalid resource path "${namespace}.${ref[0].id || ref[0]}"`)
|
|
884
894
|
}
|
|
885
895
|
if (cds.env.effective.odata.containment && model.definitions[namespace]._containedEntities.has(root.name)) {
|
|
@@ -210,7 +210,7 @@ function _xpr(expr, target, kind, isLambda, navPrefix = []) {
|
|
|
210
210
|
res.push(OPERATORS[cur] || cur.toLowerCase())
|
|
211
211
|
}
|
|
212
212
|
} else {
|
|
213
|
-
const ref = expr[i - 2]
|
|
213
|
+
const ref = expr[i - 1] in OPERATORS ? expr[i - 2] : expr[i + 1] in OPERATORS ? expr[i + 2] : null
|
|
214
214
|
const formatted = _format(
|
|
215
215
|
cur,
|
|
216
216
|
ref?.ref && (ref.ref.length ? ref.ref : ref.ref[0]),
|
|
@@ -281,7 +281,7 @@ function _getQueryTarget(entity, propOrEntity, model) {
|
|
|
281
281
|
|
|
282
282
|
const _params = (args, kind, target) => {
|
|
283
283
|
if (!args) {
|
|
284
|
-
|
|
284
|
+
cds.error({ status: 400, message: `Invalid call to "${target.name}". You need to navigate to Set` })
|
|
285
285
|
}
|
|
286
286
|
const params = Object.keys(args)
|
|
287
287
|
if (params.length !== Object.keys(target.params).length) {
|
|
@@ -298,10 +298,6 @@ const _params = (args, kind, target) => {
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
function _from(from, kind, model) {
|
|
301
|
-
if (typeof from === 'string') {
|
|
302
|
-
return { url: _entityUrl(from), queryTarget: model && model.definitions[from] }
|
|
303
|
-
}
|
|
304
|
-
|
|
305
301
|
let ref = getProp(from, 'ref')
|
|
306
302
|
ref = (Array.isArray(ref) && ref) || [ref]
|
|
307
303
|
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
}
|
|
190
190
|
if (newCqn.SELECT?.recurse && cqn.where) {
|
|
191
191
|
const where = cqn.where
|
|
192
|
-
delete
|
|
192
|
+
delete newCqn.SELECT.where
|
|
193
193
|
const columns = newCqn.SELECT.columns
|
|
194
194
|
delete newCqn.SELECT.columns
|
|
195
195
|
newCqn = { SELECT: { from: newCqn, where, columns } }
|
|
@@ -515,8 +515,58 @@
|
|
|
515
515
|
})
|
|
516
516
|
return isAliased
|
|
517
517
|
}
|
|
518
|
+
|
|
519
|
+
const _replaceComputedProperties = (columns, col) => {
|
|
520
|
+
for (let i = 0; i < columns.length; i++) {
|
|
521
|
+
if (columns[i].ref && columns[i].ref.at(-1) === col.as && (col.xpr || col.ref || col.val)) {
|
|
522
|
+
columns[i] = col
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const _checkComputedPropsUsage = (elements, computedAliases, context) => {
|
|
530
|
+
if (!elements || !computedAliases.length) return
|
|
531
|
+
|
|
532
|
+
const _checkElement = element => {
|
|
533
|
+
if (element.ref && computedAliases.includes(element.ref.at(-1))) {
|
|
534
|
+
const err = new Error(`Computed property "${element.ref.at(-1)}" cannot be used in ${context}. Computed properties are only supported in $select`)
|
|
535
|
+
err.statusCode = 501
|
|
536
|
+
throw err
|
|
537
|
+
}
|
|
538
|
+
if (Array.isArray(element.xpr)) element.xpr.forEach(_checkElement)
|
|
539
|
+
if (Array.isArray(element.args)) element.args.forEach(_checkElement)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (Array.isArray(elements)) elements.forEach(_checkElement)
|
|
543
|
+
else _checkElement(elements)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const _validateComputedPropsUsage = (SELECT) => {
|
|
547
|
+
const computedAliases = SELECT.compute.map(col => col.as)
|
|
548
|
+
|
|
549
|
+
if (SELECT.where) _checkComputedPropsUsage(SELECT.where, computedAliases, '$filter')
|
|
550
|
+
if (SELECT.orderBy) _checkComputedPropsUsage(SELECT.orderBy, computedAliases, '$orderby')
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const _processComputedProps = (SELECT) => {
|
|
554
|
+
_validateComputedPropsUsage(SELECT)
|
|
555
|
+
|
|
556
|
+
for (const col of SELECT.compute) {
|
|
557
|
+
if (SELECT.columns) {
|
|
558
|
+
const propFound = _replaceComputedProperties(SELECT.columns, col)
|
|
559
|
+
if (!propFound) SELECT.columns.push(col)
|
|
560
|
+
} else {
|
|
561
|
+
SELECT.columns = ['*']
|
|
562
|
+
SELECT.columns.push(col)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
delete SELECT.compute
|
|
566
|
+
}
|
|
518
567
|
}
|
|
519
568
|
|
|
569
|
+
|
|
520
570
|
// ---------- Entity Paths ---------------
|
|
521
571
|
|
|
522
572
|
ODataRelativeURI // Note: case-sensitive!
|
|
@@ -551,6 +601,8 @@
|
|
|
551
601
|
SELECT.__countAggregated = true
|
|
552
602
|
if(SELECT.apply)
|
|
553
603
|
return _handleApply(SELECT, SELECT.apply, onlyColumnsFromExpand)
|
|
604
|
+
if(SELECT.compute)
|
|
605
|
+
_processComputedProps(SELECT)
|
|
554
606
|
return { SELECT }
|
|
555
607
|
}
|
|
556
608
|
|
|
@@ -640,10 +692,11 @@
|
|
|
640
692
|
aliasedParamEqualsValOrPrefixParam /
|
|
641
693
|
deltaToken
|
|
642
694
|
// @OData spec for $expand:
|
|
643
|
-
// "Allowed system query options are $filter, $select, $orderby, $skip, $top, $count, $search, $expand and
|
|
695
|
+
// "Allowed system query options are $filter, $select, $compute, $orderby, $skip, $top, $count, $search, $expand and
|
|
644
696
|
// $apply (https://go.sap.corp/0jzs)."
|
|
645
697
|
ExpandOption =
|
|
646
698
|
"$select=" o select ( COMMA select )* /
|
|
699
|
+
"$compute=" o compute ( COMMA compute )* /
|
|
647
700
|
"$expand=" o expand ( COMMA expand )* expandCount? /
|
|
648
701
|
"$filter=" o f:filter { SELECT.where = f } /
|
|
649
702
|
"$orderby=" o o:orderby ( COMMA o2:orderby{_setOrderBy(o2)} )* {_setOrderBy(o,true)} /
|
|
@@ -748,6 +801,14 @@
|
|
|
748
801
|
= val:$( [^;)]+ ) { return [{ val }] }
|
|
749
802
|
/ o // Do not add search property for space only
|
|
750
803
|
|
|
804
|
+
compute
|
|
805
|
+
= expr:mathCalc as:asAlias {
|
|
806
|
+
SELECT.compute = Array.isArray(SELECT.compute) ? SELECT.compute : []
|
|
807
|
+
const comp = expr.xpr ? { xpr: expr.xpr, as } : { ...expr, as }
|
|
808
|
+
SELECT.compute.push(comp)
|
|
809
|
+
return comp
|
|
810
|
+
}
|
|
811
|
+
|
|
751
812
|
filter
|
|
752
813
|
= p:where_clause { return p }
|
|
753
814
|
|
|
@@ -877,6 +938,33 @@
|
|
|
877
938
|
//
|
|
878
939
|
// ---------- Expressions ------------
|
|
879
940
|
|
|
941
|
+
mathCalc
|
|
942
|
+
= head:mathOperand tail:(o op:("add" / "sub" / "mul" / "divby" / "div" ) o operand:mathOperand { return { op, operand } })* {
|
|
943
|
+
const opMap = { sub: '-', add: '+', mul: '*', divby: '/', div: '/' }
|
|
944
|
+
|
|
945
|
+
const toXprElement = (operand) => {
|
|
946
|
+
if (typeof operand === 'number') return { val: operand }
|
|
947
|
+
if (typeof operand === 'string') return { ref: [operand] }
|
|
948
|
+
return operand // xpr
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
let xpr = [toXprElement(head)]
|
|
952
|
+
for (let i = 0; i < tail.length; i++) {
|
|
953
|
+
xpr.push(opMap[tail[i].op])
|
|
954
|
+
xpr.push(toXprElement(tail[i].operand))
|
|
955
|
+
}
|
|
956
|
+
return tail.length === 0 ? xpr[0] : { xpr }
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
mathOperand
|
|
960
|
+
= integer / negativeIdentifier / identifier / parenthesizedMath
|
|
961
|
+
|
|
962
|
+
negativeIdentifier
|
|
963
|
+
= "-" id:identifier { return { xpr: ['-', { ref: [id] }] } }
|
|
964
|
+
|
|
965
|
+
parenthesizedMath
|
|
966
|
+
= OPEN expr:mathCalc CLOSE { return expr }
|
|
967
|
+
|
|
880
968
|
comparison
|
|
881
969
|
= a:operand _ o:$("eq" / "ne" / "lt" / "gt" / "le" / "ge") _ b:operand {
|
|
882
970
|
return [ a, OPERATORS[o] || o, b ]
|
|
@@ -888,9 +976,6 @@
|
|
|
888
976
|
= a:operand _ "in" _ b:listFilterParam {
|
|
889
977
|
return [ a, "in", b ]
|
|
890
978
|
}
|
|
891
|
-
|
|
892
|
-
mathCalc
|
|
893
|
-
= operand (_ ("add" / "sub" / "mul" / "div" / "mod") _ operand)*
|
|
894
979
|
|
|
895
980
|
operand
|
|
896
981
|
= navigationCount / function / val / ref / jsonObject / jsonArray / list
|
|
@@ -1025,7 +1110,6 @@
|
|
|
1025
1110
|
// REVISIT: All transformations below need improvment
|
|
1026
1111
|
"search" search:searchTrafo{return search} /
|
|
1027
1112
|
"concat" con:concatTrafo{return con} / //> Return con so that concat string is not returned
|
|
1028
|
-
"compute" compute:computeTrafo{return compute} /
|
|
1029
1113
|
"top" top:topTrafo{return top} /
|
|
1030
1114
|
"skip" skip:skipTrafo{return skip} /
|
|
1031
1115
|
"orderby" order:orderbyTrafo{return order} /
|
|
@@ -1054,8 +1138,7 @@
|
|
|
1054
1138
|
) { return res }
|
|
1055
1139
|
aggregateExpr
|
|
1056
1140
|
= path:(
|
|
1057
|
-
ref
|
|
1058
|
-
// / mathCalc - needs CAP support
|
|
1141
|
+
ref
|
|
1059
1142
|
)
|
|
1060
1143
|
func:aggregateWith? aggregateFrom? as:asAlias?
|
|
1061
1144
|
{ return { func, args: [ path ], as: as ?? path.ref[0] } }
|
|
@@ -1102,11 +1185,6 @@
|
|
|
1102
1185
|
return {concat: [trafo1, ...trafo2]}
|
|
1103
1186
|
}
|
|
1104
1187
|
|
|
1105
|
-
// REVISIT: support compute - current implementation is deviating from odata
|
|
1106
|
-
computeTrafo = OPEN o computeExpr (o COMMA o computeExpr)* o CLOSE
|
|
1107
|
-
|
|
1108
|
-
computeExpr = where_clause asAlias
|
|
1109
|
-
|
|
1110
1188
|
commonFuncTrafo = OPEN o first:operand o COMMA o second:operand o CLOSE { return [first, second] }
|
|
1111
1189
|
|
|
1112
1190
|
// REVISIT: support identity
|