on ‎2021 Mar 04 3:52 PM
Hello SAP-Community,
I got some error while I was using external services in a CAP application.
I could not call external OData V2 external services (SAP Standard Services eg. API_BUSINESS_PARTNER) with expand parameters. I imported the services metadata as .edmx-file. The query was built with additional $select query parameters and I got an segment error in the respective service.
I implemented a workaround and removed the $select on expanded entities. (see select.js line 86 and comments)
I replaced this file in the node_modules directory: @sap\cds-runtime\lib\rest\utils\query-generation
const { generateKeyPath, splitByAndGetValueByIndex, formatVal } = require('./utils')
const ODATA_COMPARATOR = {
'=': 'eq',
'<>': 'ne',
'>=': 'ge',
'<=': 'le',
'<': 'lt',
'>': 'gt'
}
const _between = (target, column, /* between */ lower, /* and */ upper) => {
const ref = column.ref.join('/')
return `(${ref} gt ${formatVal(lower.val, ref, target)} and ${ref} lt ${formatVal(upper.val, ref, target)})`
}
const _in = (target, column, /* in */ values) => {
// values could also be subselect
if (values.val && values.val.length) {
const ref = column.ref.join('/')
const expressions = values.val.map(value => `${ref} eq ${formatVal(value, ref, target)}`)
return `(${expressions.join(' or ')})`
}
}
const _stringifyFilter = filter => {
return filter.reduce((str, element, i) => {
if (i === 0) return element
if (typeof element === 'string') {
if (element.startsWith('(') && str.endsWith('(')) return `${str}${element}`
if (element.startsWith(')') && str.endsWith(')')) return `${str}${element}`
}
return `${str} ${element}`
}, '')
}
const _queryOptionsAsString = queryOptions => {
const queryOptionKeys = Object.keys(queryOptions)
if (!queryOptionKeys.length) return ''
return `?${Object.keys(queryOptions)
.map(key => `${key}=${queryOptions[key]}`)
.join('&')}`
}
const _parseColumnsDeepExpand = (colRes, resExpand = []) => {
if (Object.keys(colRes.expand).length !== 0) {
resExpand.push('(')
if (colRes.columns) resExpand.push(`$select=${colRes.columns};`)
resExpand.push('$expand=')
Object.keys(colRes.expand).forEach((key, index) => {
resExpand.push(key)
_parseColumnsDeepExpand(colRes.expand[key], resExpand)
if (Object.keys(colRes.expand).length !== index + 1) resExpand.push(',')
})
resExpand.push(')')
} else if (colRes.columns) resExpand.push('(', `$select=${colRes.columns}`, ')')
return resExpand
}
const _columnsMustBeExpanded = column => {
return column.expand.length === 0 || (column.expand.length === 1 && column.expand[[0]] === '*')
}
const _handleExpand = column => {
const expandedElement = column.ref[0]
if (_columnsMustBeExpanded(column)) {
return expandedElement
}
const queryOptions = []
const { select, expand } = _createSelectAndExpandStrings(column.expand)
//if (select) queryOptions.push(`$select=${select}`)
if (expand) queryOptions.push(`$expand=${expand}`)
return `${expandedElement}`
//`(${queryOptions.join(';')})`
}
const _createSelectAndExpandStrings = cqnColumns => {
const columns = []
const expands = []
for (const column of cqnColumns) {
if (column.func) {
throw new Error('Feature not supported: .func in columns of SELECT statement.')
//Changed by Ivan Derr: A column can have ref and expand at the same time
} else if (column.ref) {
if(column.expand){
expands.push(_handleExpand(column))
}
columns.push(column.ref.join('/'))
} else if (column.expand) {
expands.push(_handleExpand(column))
}
// ignore .val in columns
}
return {
select: columns.join(','),
expand: expands.join(',')
}
}
const _createOrderByString = orderBy => {
return orderBy.map(o => `${o.ref.join('/')} ${o.sort}`).join(',')
}
const _createFilterString = (where, target) => /* NOSONAR */ {
const condition = []
for (let i = 0; i < where.length; i++) {
const element = where[i]
if (typeof element === 'string') {
condition.push(ODATA_COMPARATOR[element] || element.toLowerCase())
} else if (element.ref) {
if (where[i + 1] === 'between') {
condition.push(_between(target, element, where[i + 2], where[i + 4]))
i += 4
} else if (where[i + 1] === 'in') {
const values = where[i + 2]
// when sending a where clause with "col in []" we currently ignore the where clause
// analog to interpretation for sql generation
// double check if this is the intended behavior
const inCondition = _in(target, element, values)
if (inCondition) condition.push(inCondition)
i += 2
} else {
condition.push(element.ref.join('/'))
}
} else if (element.val) {
//
const previousRefAsString = condition[condition.length - 2]
condition.push(formatVal(element.val, previousRefAsString, target))
} else if (element.func) {
throw new Error('Feature not supported: .func in where clause of SELECT statement.')
}
}
return _stringifyFilter(condition)
}
const select = (cqn, target, type) => /* NOSONAR */ {
const url = generateKeyPath(cqn.SELECT.from, type, cqn.SELECT._transitions)
const queryOptions = {}
if (cqn.SELECT.groupBy) {
throw new Error('Feature not supported: SELECT statement with .groupBy')
}
if (cqn.SELECT.columns) {
const { select, expand } = _createSelectAndExpandStrings(cqn.SELECT.columns)
if (select) queryOptions['$select'] = select
if (expand) queryOptions['$expand'] = expand
}
if (cqn.SELECT.where) {
const filter = _createFilterString(cqn.SELECT.where, target)
if (filter) queryOptions['$filter'] = filter
}
if (cqn.SELECT.limit) {
if (cqn.SELECT.limit.rows) queryOptions['$top'] = cqn.SELECT.limit.rows.val
if (cqn.SELECT.limit.offset) queryOptions['$skip'] = cqn.SELECT.limit.offset.val
}
if (cqn.SELECT.orderBy) {
queryOptions['$orderby'] = _createOrderByString(cqn.SELECT.orderBy)
}
if (cqn.SELECT.one) {
queryOptions['$top'] = '1'
}
const queryOptionsAsString = _queryOptionsAsString(queryOptions)
return {
path: `${splitByAndGetValueByIndex(url.path)}${url.keys || ''}${queryOptionsAsString}`,
method: 'GET'
}
}
module.exports = select
The second file where I got errors was utils.js in the same directory. I got an type error because a function tried to treat a String type like an Object type. (see utils.js line 50)
// needs to be more sophisticated, e. g. odata v2/v4 and resolving "author/id"
const formatVal = (val, element, target) => {
const csnElement = target.elements[element]
switch (csnElement.type) {
case 'cds.String':
return `'${val}'`
case 'cds.DateTime':
case 'cds.Date':
case 'cds.Timestamp':
return process.env.T19 ? `'${val}'` : val // TODO: improve
default:
return val
}
}
// only supported structure is ref, =, val
const _isValidWhereConditionRest = where => {
return where.length === 3 && where[0].ref && where[1] === '=' && 'val' in where[2]
}
const _generateRestKeyPath = where => {
if (!_isValidWhereConditionRest(where)) {
throw new Error("Feature not supported: complex where of fluent API (not in format 'a = 1')")
}
return where[2].val
}
const _isValidWhereConditionOdata = where => {
for (let i = 0; i < where.length; i++) {
if ((i === 0 || where[i - 1] === 'and') && where[i].ref) continue
if (where[i - 1].ref && where[i] === '=') continue
if (where[i - 1] === '=' && 'val' in where[i]) continue
if ('val' in where[i - 1] && where[i] === 'and') continue
return false
}
return true
}
// only supported structure is alternating ref, =, val combined with and
const _generateOdataKeyPath = (where, target) => {
// remove brackets
const whereNoBrackets = where.filter(e => e !== '(' && e !== ')')
if (!_isValidWhereConditionOdata(whereNoBrackets)) {
throw new Error("Feature not supported: complex where of fluent API (not in format 'a = 1')")
}
const keyPath = where.map((e, i) => {
if (e.ref) return e.ref[e.ref.length - 1]
//Changed by Ivan Derr
//Fixed a bug: TypeError: 'val'(String) in 'and'(String)
//Searching for a String property in a String, code expected an Object
if (e === '=') return e
if (e === 'and') return ','
if ('val' in e) {
const previousRef = where[i - 2].ref
return formatVal(e.val, previousRef[previousRef.length - 1], target)
}
})
return `(${keyPath.join('')})`
}
const generateKeysOfWhere = (where, type, target) => {
if (type === 'rest') return _generateRestKeyPath(where)
// odata v2 and v4
return _generateOdataKeyPath(where, target)
}
const splitByAndGetValueByIndex = (value, split = '.', offset = 0) => {
const parts = value.split(split)
return parts[parts.length - 1 - offset]
}
const generateKeyPath = (from, type, transitions) => {
if (typeof from === 'string') return { path: from }
return {
path: from.ref
.map((f, i) => {
if (f.id) {
let keyPath = generateKeysOfWhere(f.where, type, transitions[i].target)
if (type === 'rest') keyPath = `/${keyPath}`
return `${f.id}${keyPath}`
}
return f
})
.join('/')
}
}
module.exports = {
formatVal,
generateKeyPath,
generateKeysOfWhere,
splitByAndGetValueByIndex
}
I am using cds version 3.3.5.
The files I mentioned are attached to this message. Changed code is commented with my name inside the .js files.
Could you please review the code inside the framework an fix these issues?
Kind regards
Ivan Derr
Request clarification before answering.
Hi Ivan,
the currently implemented query generation only worked for OData V4 services, respectively the shared subset of OData V2 and V4 .
We are working on supporting OData V2 properly already.
Best regards,
Johannes
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
| User | Count |
|---|---|
| 12 | |
| 9 | |
| 7 | |
| 5 | |
| 4 | |
| 2 | |
| 2 | |
| 2 | |
| 2 | |
| 2 |
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.