UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

178 lines (148 loc) 5.61 kB
// Code adopted from @sap/cds-odata-v2-adapter-proxy // https://www.w3.org/TR/xmlschema11-2/#nt-duDTFrag const DurationRegex = /^P(?:(\d)Y)?(?:(\d{1,2})M)?(?:(\d{1,2})D)?T(?:(\d{1,2})H)?(?:(\d{2})M)?(?:(\d{2}(?:\.\d+)?)S)?$/i const DataTypeOData = { Binary: 'cds.Binary', Boolean: 'cds.Boolean', Byte: 'cds.Binary', DateTime: 'cds.DateTime', DateTimeOffset: 'cds.Timestamp', Decimal: 'cds.Decimal', Double: 'cds.Double', Single: 'cds.Double', Guid: 'cds.UUID', Int16: 'cds.Integer', Int32: 'cds.Integer', Int64: 'cds.Integer64', SByte: 'cds.Integer', String: 'cds.String', Date: 'cds.Date', Time: 'cds.TimeOfDay' } const _convertData = (data, target, convertValueFn, returnType) => { const _convertRecordFn = returnType ? _convertActionFuncResponse(returnType, convertValueFn) : _getConvertRecordFn(target, convertValueFn) if (Array.isArray(data)) { return data.map(_convertRecordFn) } return _convertRecordFn(data) } const _getConvertRecordFn = (target, convertValueFn) => record => { for (const key in record) { if (key === '__metadata') continue const element = target.elements[key] if (!element) continue const recordValue = record[key] const value = (recordValue && typeof recordValue === 'object' && 'results' in recordValue && recordValue.results) || recordValue if (value && (element.isAssociation || Array.isArray(value))) { record[key] = _convertData(value, element._target, convertValueFn) } else { record[key] = convertValueFn(value, element) } } return record } const _convertActionFuncResponse = (returnType, convertValueFn) => data => { // return type is entity/complex type or array of entities/complex types if (returnType.elements || (returnType.items && returnType.items.elements)) { const _convertRecordFn = _getConvertRecordFn(returnType.items || returnType, convertValueFn) return _convertRecordFn(data) } // return type is primitive type/array of primitive types return convertValueFn(data, returnType.items || returnType) } const _convertValue = () => (value, element) => { if (value == null) return value const type = _elementType(element) if (type === 'cds.Time') { const match = value.match(DurationRegex) if (match) { value = `${match[4] || '00'}:${match[5] || '00'}:${match[6] || '00'}` } } else if (type === 'cds.Timestamp' || type === 'cds.DateTime' || type === 'cds.Date') { const match = value.match(/\/Date\((.*)\)\//) const ticksAndOffset = match && match.pop() if (ticksAndOffset) { value = new Date(_calculateTicksOffsetSum(ticksAndOffset)).toISOString() // always UTC } if (type === 'cds.DateTime') { value = value.slice(0, 19) + 'Z' // Cut millis } else if (type === 'cds.Date') { value = value.slice(0, 10) // Cut time } } return value } const _PT = ([hh, mm, ss]) => `PT${hh}H${mm}M${ss}S` const _convertPayloadValue = (value, element) => { if (value == null) return value // see https://www.odata.org/documentation/odata-version-2-0/json-format/ const type = _elementType(element) switch (type) { case 'cds.Time': return value.match(/^(PT)([H,M,S,0-9])*$/) ? value : _PT(value.split(':')) case 'cds.Decimal': return typeof value === 'string' ? value : `${value}` case 'cds.Date': case 'cds.DateTime': return `/Date(${new Date(value).getTime()})/` case 'cds.Binary': case 'cds.LargeBinary': return Buffer.isBuffer(value) ? value.toString('base64') : value case 'cds.Timestamp': // According to OData V2 spec, and as cds.DateTime => (V2) Edm.DateTimeOffset => cds.Timestamp, // cds.Timestamp should be converted into Edm.DateTimeOffset literal form `datetimeoffset'${new Date(value).toISOString()}'` // However, odata-v2-proxy forwards it literaly as `datetimeoffset'...'` which is rejected by okra. // Note that OData V2 spec example also does not contain 'datetimeoffset' predicate. return new Date(value).toISOString() default: return value } } const _calculateTicksOffsetSum = text => { return (text.replace(/\s/g, '').match(/[+-]?([0-9]+)/g) || []).reduce((sum, value, index) => { return sum + parseFloat(value) * (index === 0 ? 1 : 60 * 1000) // ticks are milliseconds (0), offset are minutes (1) }, 0) } const _elementType = element => { let type if (element) { type = element._type if (element['@odata.Type']) { const odataType = element['@odata.Type'].match(/\w+$/) type = (odataType && DataTypeOData[odataType[0]]) || type } if (!type && element.items && element.items._type) { type = element.items._type } } return type } const convertV2ResponseData = (data, target, returnType) => { if (!((target && target.elements) || returnType)) return data return _convertData(data, target, _convertValue(), returnType) } const convertV2PayloadData = (data, target) => { if (!target || !target.elements) return data return _convertData(data, target, _convertPayloadValue) } const deepSanitize = arg => { if (Array.isArray(arg)) return arg.map(deepSanitize) if (typeof arg === 'object' && arg !== null) return Object.keys(arg).reduce((acc, cur) => { acc[cur] = deepSanitize(arg[cur]) return acc }, {}) return '***' } const hasAliasedColumns = (column = {}) => { return column.as || (column.expand && column.expand.some(hasAliasedColumns)) } module.exports = { convertV2ResponseData, convertV2PayloadData, deepSanitize, hasAliasedColumns }