UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

339 lines (300 loc) 10.2 kB
// TODO: split into multiple files const cds = require('../../..') const _etag = require('./etag') const { toBase64url } = require('../../_runtime/common/utils/binary') const { getSapMessages } = require('../middleware/error') const getSafeNumber = inputString => { if (typeof inputString !== 'string') return inputString // Try to parse the input string as a floating-point number using parseFloat const parsedFloat = parseFloat(inputString) // Check if the parsed value is not NaN and is equal to the original input string if (!isNaN(parsedFloat) && String(parsedFloat) === inputString) { return parsedFloat } // Try to parse the input string as an integer using parseInt const parsedInt = parseInt(inputString) // special case like '3.00000000000001', the precision is not lost and string is returned if (!isNaN(parsedInt) && String(parsedInt) === inputString.replace(/^-?\d+\.0+$/, inputString.split('.')[0])) { return parsedInt } // If none of the above conditions are met, return the input string as is return inputString } const _getElement = (csnTarget, key) => { if (csnTarget) { if (csnTarget.elements) { if (Array.isArray(key)) { const [navigation, ...targetElement] = key const element = csnTarget.elements[navigation] if (element && element.isAssociation) { return _getElement( csnTarget.elements[navigation]._target, targetElement.length > 1 ? targetElement : targetElement[0] ) } else if (element && element._isStructured) { return _getElement( csnTarget.elements[navigation], targetElement.length > 1 ? targetElement : targetElement[0] ) } } return ( csnTarget.elements[key] || { type: undefined } ) } else if (csnTarget.params) { return ( csnTarget.params[key] || { type: undefined } ) } } return { type: undefined } } const getPreferReturnHeader = req => { const preferReturn = req.headers.prefer?.match(/\W?return=(\w+)/i) if (preferReturn) return preferReturn[1] } const _PT = ([hh, mm, ss]) => `PT${hh}H${mm}M${ss}S` const _v2 = (val, element) => { switch (element.type) { case 'cds.UUID': return `guid'${val}'` // binaries case 'cds.Binary': case 'cds.LargeBinary': return `binary'${toBase64url(val)}'` // integers case 'cds.UInt8': case 'cds.Int16': case 'cds.Int32': case 'cds.Integer': return val // big integers case 'cds.Int64': case 'cds.Integer64': // inofficial flag to skip appending "L" return cds.env.remote?.skip_v2_appendix ? val : `${val}L`.replace(/ll$/i, 'L') // floating point numbers case 'cds.Decimal': // inofficial flag to skip appending "m" return cds.env.remote?.skip_v2_appendix ? val : `${val}m`.replace(/mm$/i, 'm') case 'cds.Double': // inofficial flag to skip appending "d" return cds.env.remote?.skip_v2_appendix ? val : `${val}d`.replace(/dd$/i, 'd') // dates et al case 'cds.Date': return element['@odata.Type'] === 'Edm.DateTimeOffset' ? `datetimeoffset'${val}T00:00:00'` : `datetime'${val}T00:00:00'` case 'cds.DateTime': return element['@odata.Type'] === 'Edm.DateTimeOffset' ? `datetimeoffset'${val}'` : val.endsWith('Z') ? `datetime'${val.slice(0, -1)}'` : `datetime'${val}'` case 'cds.Time': return `time'${_PT(val.split(':'))}'` case 'cds.Timestamp': return element['@odata.Type'] === 'Edm.DateTime' ? val.endsWith('Z') ? `datetime'${val.slice(0, -1)}'` : `datetime'${val}'` : `datetimeoffset'${val}'` // bool case 'cds.Boolean': return val // strings + default to string representation case 'cds.String': case 'cds.LargeString': default: return `'${val}'` } } const _v4 = (val, element) => { switch (element.type) { case 'cds.UUID': return val // binary case 'cds.Binary': case 'cds.LargeBinary': return `binary'${toBase64url(val)}'` // integers case 'cds.UInt8': case 'cds.Int16': case 'cds.Int32': case 'cds.Integer': return val // big integers case 'cds.Int64': case 'cds.Integer64': return val // floating point numbers case 'cds.Decimal': case 'cds.Double': return val // dates et al case 'cds.DateTime': case 'cds.Date': case 'cds.Timestamp': case 'cds.Time': return val // bool case 'cds.Boolean': return val // strings + default to string representation case 'cds.String': case 'cds.LargeString': default: return `'${val}'` } } const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i const MATH_FUNC = { round: 1, floor: 1, ceiling: 1 } const _isTimestamp = val => /^\d+-\d\d-\d\d(T\d\d:\d\d(:\d\d(\.\d+)?)?(Z|([+-]{1}\d\d:\d\d))?)?$/.test(val) && !isNaN(Date.parse(val)) const formatVal = (val, elementName, csnTarget, kind, func, literal) => { if (val === null || val === 'null') return 'null' if (typeof val === 'boolean') return val if (typeof val === 'string' && literal === 'number') return `${val}` if (typeof val === 'string') { if (!csnTarget && UUID.test(val)) return kind === 'odata-v2' ? `guid'${val}'` : val if (func in MATH_FUNC) return val } if (typeof val === 'number') val = getSafeNumber(val) if (!csnTarget) { if (typeof val !== 'string') return val // REVISIT: why do we need to check strings for timestamps? if (_isTimestamp(val)) return val return `'${val}'` } const element = _getElement(csnTarget, elementName) if (!element?.type) return typeof val === 'string' ? `'${val}'` : val if ((element.type === 'cds.LargeString' || element.type === 'cds.String') && val.indexOf("'") >= 0) val = val.replace(/'/g, "''") return kind === 'odata-v2' ? _v2(val, element) : _v4(val, element) } const skipToken = (token, cqn) => { const LOG = cds.log('odata') if (isNaN(token)) { let decoded try { decoded = JSON.parse(Buffer.from(token, 'base64').toString()) } catch { LOG.warn('$skiptoken is not in expected format. Ignoring it.') return } const xprs = decoded.c.map(() => []) for (let i = 0; i < xprs.length; i++) { const { k, v, a } = decoded.c[i] const ref = { ref: [k] } const val = { val: v } if (i > 0) xprs[i].push('and') xprs[i].push(ref, a ? '>' : '<', val) for (let j = i + 1; j < xprs.length; j++) { if (i > 0) xprs[j].push('and') xprs[j].push(ref, '=', val) } } const xpr = [] for (let i = 0; i < xprs.length; i++) { if (i > 0) xpr.push('or') xpr.push(...xprs[i]) } if (cqn.SELECT.where) { cqn.SELECT.where = [{ xpr: [...cqn.SELECT.where] }, 'and', { xpr }] } else { cqn.SELECT.where = [{ xpr }] } if (cds.context?.http.req.query.$top) { const top = parseInt(cds.context?.http.req.query.$top) if (top - decoded.r < cqn.SELECT.limit.rows.val) { cqn.SELECT.limit.rows.val = top - decoded.r } } } else { const { limit } = cqn.SELECT if (!limit) { cqn.SELECT.limit = { offset: { val: parseInt(token) } } } else { const { offset } = limit cqn.SELECT.limit.offset = { val: (offset && 'val' in offset ? offset.val : offset || 0) + parseInt(token) } } } } // REVISIT: When does that have to happen? -> always? or for OData v2 only? const handleSapMessages = (cdsReq, req, res) => { if (res.headersSent || !cdsReq.messages?.length) return const msgs = getSapMessages(cdsReq.messages, req) if (msgs) res.setHeader('sap-messages', msgs) } const isStream = query => { if (!query) return const { _propertyAccess } = query if (!_propertyAccess) return // FIXME: that should not be done in middlewares, but only in an event handler, after ensure_target const element = cds.infer.target(query).elements?.[_propertyAccess] return element._type === 'cds.LargeBinary' && element['@Core.MediaType'] } const isRedirect = query => { const { _propertyAccess } = query if (!_propertyAccess) return // FIXME: that should not be done in middlewares, but only in an event handler, after ensure_target const element = cds.infer.target(query).elements?.[_propertyAccess] return element['@Core.IsURL'] } const _addKeysDeep = (keys, keysCollector, ignoreManagedBackLinks) => { for (const keyName in keys) { const key = keys[keyName] const foreignKey = key._foreignKey4 if (key.isAssociation || foreignKey === 'up_' || key['@cds.api.ignore'] === true) continue if (ignoreManagedBackLinks && foreignKey) { const navigationElement = keys[foreignKey] if (!navigationElement.on && navigationElement._isBacklink) { // skip navigation elements that are backlinks continue } } if ('elements' in key) { _addKeysDeep(key.elements, keysCollector) continue } keysCollector.push(keyName) } } function keysOf(entity, ignoreManagedBackLinks) { const keysCollector = [] if (!entity || !entity.keys) return keysCollector _addKeysDeep(entity.keys, keysCollector, ignoreManagedBackLinks) return keysCollector } // case: single key without name, e.g., Foo(1) function addRefToWhereIfNecessary(where, entity, ignoreManagedBackLinks = false) { if (!where || where.length !== 1) return 0 const isView = !!entity.params const keys = isView ? Object.keys(entity.params) : keysOf(entity, ignoreManagedBackLinks) if (keys.length !== 1) return 0 where.unshift(...[{ ref: [keys[0]] }, '=']) return 1 } function getBoundary(req) { return req.headers['content-type']?.match(/boundary=([\d\w'()+_,\-./:=?]{1,70})/i)?.[1] } module.exports = { getSafeNumber, formatVal, skipToken, handleSapMessages, getPreferReturnHeader, isStream, isRedirect, keysOf, addRefToWhereIfNecessary, validateIfNoneMatch: _etag.validateIfNoneMatch, extractIfNoneMatch: _etag.extractIfNoneMatch, getBoundary }