cancel
Showing results for 
Search instead for 
Did you mean: 
Read only

Errors in CDS runtime: @sap\cds-runtime\lib\rest\utils

ivande
Explorer
0 Likes
813

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

View Entire Topic
johannesvogel
Product and Topic Expert
Product and Topic Expert
0 Likes

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

ivande
Explorer
0 Likes

Hi Johannes,

Thank you for your answer!

Kind regards

Ivan