@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
224 lines (184 loc) • 7.26 kB
JavaScript
const cds = require('../../../lib')
const LOG = cds.log('odata')
const { appURL } = require('../../_runtime/common/utils/vcap')
const { where2obj } = require('../../_runtime/common/utils/cqn')
// copied from cds-compiler/lib/edm/edmUtils.js
const cds2edm = {
'cds.String': 'Edm.String',
// 'cds.hana.NCHAR': 'Edm.String',
'cds.LargeString': 'Edm.String',
// 'cds.hana.VARCHAR': 'Edm.String',
// 'cds.hana.CHAR': 'Edm.String',
// 'cds.hana.CLOB': 'Edm.String',
'cds.Binary': 'Edm.Binary',
// 'cds.hana.BINARY': 'Edm.Binary',
'cds.LargeBinary': 'Edm.Binary',
'cds.Decimal': 'Edm.Decimal',
'cds.DecimalFloat': 'Edm.Decimal',
// 'cds.hana.SMALLDECIMAL': 'Edm.Decimal', // V4: Scale="floating" Precision="16"
'cds.Integer64': 'Edm.Int64',
'cds.Integer': 'Edm.Int32',
'cds.Int64': 'Edm.Int64',
'cds.Int32': 'Edm.Int32',
'cds.Int16': 'Edm.Int16',
'cds.UInt8': 'Edm.Byte',
// 'cds.hana.SMALLINT': 'Edm.Int16',
// 'cds.hana.TINYINT': 'Edm.Byte',
'cds.Double': 'Edm.Double',
// 'cds.hana.REAL': 'Edm.Single',
'cds.Date': 'Edm.Date',
'cds.Time': 'Edm.TimeOfDay',
'cds.DateTime': 'Edm.DateTimeOffset',
'cds.Timestamp': 'Edm.DateTimeOffset',
'cds.Boolean': 'Edm.Boolean',
'cds.UUID': 'Edm.Guid'
// 'cds.hana.ST_POINT': 'Edm.GeometryPoint',
// 'cds.hana.ST_GEOMETRY': 'Edm.Geometry',
}
const _getContextAbsoluteUrl = _req => {
const { contextAbsoluteUrl } = cds.env.odata
if (!contextAbsoluteUrl) return ''
if (typeof contextAbsoluteUrl === 'string') {
try {
const userDefinedURL = new URL(contextAbsoluteUrl, contextAbsoluteUrl).toString()
return (!userDefinedURL.endsWith('/') && `${userDefinedURL}/`) || userDefinedURL
} catch (e) {
e.message = `cds.odata.contextAbsoluteUrl could not be parsed as URL: ${contextAbsoluteUrl}`
LOG._warn && LOG.warn(e)
}
}
const reqURL = _req && _req.get && _req.get('host') && `${_req.protocol || 'https'}://${_req.get('host')}`
const baseAppURL = appURL || reqURL || ''
const serviceUrl = `${(_req && _req.baseUrl) || ''}/`
return baseAppURL && new URL(serviceUrl, baseAppURL).toString()
}
const _isNavToDraftAdmin = path => path.length > 1 && path[path.length - 1] === 'DraftAdministrativeData'
const _lastValidRef = ref => {
for (let i = ref.length - 1; i >= 0; i--) {
if (ref[i] in { DraftAdministrativeData: 1, SiblingEntity: 1 }) continue
return ref[i]
}
}
const _getRef = query => {
if (query.SELECT?.from?.SELECT) return _getRef(query.SELECT.from)
return (
query.SELECT?.from?.ref ?? query.UPDATE?.entity?.ref ?? query.INSERT?.into?.ref ?? query.DELETE?.from?.ref ?? []
)
}
const _toBinaryKeyValue = value => `binary'${value.toString('base64')}'`
const _odataContext = (query, options) => {
const { contextAbsoluteUrl, context_with_columns } = cds.env.odata
let path = _getContextAbsoluteUrl(query._req) + '$metadata'
if (query._target.kind === 'service') return path
const {
_target: { _isSingleton: isSingleton },
_propertyAccess: propertyAccess
} = query
const { result, isCollection } = options
path += '#'
const ref = _getRef(query)
const isNavToDraftAdmin = _isNavToDraftAdmin(ref)
let edmName
if (options.edmName) edmName = options.edmName
else if (isNavToDraftAdmin) edmName = ref[0].id ?? ref[0]
else if (cds2edm[query._target.type]) edmName = cds2edm[query._target.type]
else edmName = query._target.name
const isType = query._target.kind === 'type'
if (isCollection && isType) edmName = `Collection(${edmName})`
const serviceName = query._target._service?.name
if (serviceName && !isType) edmName = edmName.replace(serviceName + '.', '').replace(/\./g, '_')
// prepend "../" parent segments for relative path
if (!contextAbsoluteUrl && ref.length > 1) path = '../'.repeat(ref.length - 1) + path
path += edmName
const isViewWithParams = query._target.kind === 'entity' && query._target.params
if (propertyAccess || isNavToDraftAdmin || isViewWithParams) {
if (!contextAbsoluteUrl && (propertyAccess || isViewWithParams)) path = '../' + path
const keyValuePairs = []
const lastValidRef = _lastValidRef(ref)
const lastRef = ref.at(-1)
const isSibling = lastRef === 'SiblingEntity'
let _keyValuePairs
if (lastValidRef.where) {
_keyValuePairs = Object.entries(where2obj(lastValidRef.where))
} else if (isViewWithParams) {
_keyValuePairs = Object.entries(lastValidRef.args).map(([k, v]) => [k, v.val])
} else if (!isSingleton) {
// use keys from result if not in query
_keyValuePairs = Object.entries(query._target.keys)
.filter(([, v]) => !v._isBacklink)
.map(([k]) => [k, result[k]])
}
if (Array.isArray(_keyValuePairs)) {
_keyValuePairs.forEach(([k, _v]) => {
const v = (() => {
if (k === 'IsActiveEntity' && isSibling) return !_v
if (Buffer.isBuffer(_v)) return _toBinaryKeyValue(_v)
return _v
})()
if (v !== undefined) keyValuePairs.push([k, v])
})
}
if (keyValuePairs.length) {
let keyString
// single keys just contain the value
if (keyValuePairs.length === 1) keyString = String(keyValuePairs[0][1])
// multiple keys should contain key value pairs
else keyString = keyValuePairs.map(([k, v]) => `${k}=${v}`).join(',')
path += '(' + keyString + ')'
}
if (isNavToDraftAdmin) path += '/' + lastRef
if (propertyAccess) path += '/' + propertyAccess
if (isViewWithParams) path += '/Set'
}
if (context_with_columns && query.SELECT && !propertyAccess) {
const _calculateStringFromColumn = column => {
if (column === '*') return
const refName = column.ref?.[0]
if (column.expand) {
// Process nested expands recursively
const expands = _calculateColumnsString(column.expand)
return `${refName}${expands ? expands : '()'}`
} else if (column.func) {
return column.as
}
return refName
}
const _calculateColumnsString = columns => {
const selects = []
const expands = []
columns.forEach(column => {
if (column.expand) expands.push(column)
else selects.push(column)
})
const columnStrings = []
// First process selects, then expands
selects.concat(expands).forEach(column => {
const stringFromColumn = _calculateStringFromColumn(column)
if (stringFromColumn) columnStrings.push(stringFromColumn)
})
if (columnStrings.length) return `(${columnStrings.join(',')})`
}
const columns = query.SELECT.columns || query.SELECT.from?.SELECT?.columns
if (columns) {
const columnsString = _calculateColumnsString(columns)
if (columnsString) path += columnsString
}
}
if (!isCollection && !isSingleton && !propertyAccess && !isType) path += '/$entity'
return path
}
/**
* TODO
*
* @param {*} query
* @param {*} [options]
* @param {*} [options.result]
* @param {*} [options.isCollection]
* @param {*} [options.edmName]
* @returns
*/
module.exports = function getODataMetadata(query, options = {}) {
if (!query._target) return
const context = _odataContext(query, options)
return { context }
}