UNPKG

@sap/odata-v4

Version:

OData V4.0 server library

207 lines (188 loc) 9.15 kB
'use strict'; const QueryString = require('querystring'); const EdmTypeKind = require('../edm/EdmType').TypeKind; const EdmPrimitiveTypeKind = require('../edm/EdmPrimitiveTypeKind'); const UriSyntaxError = require('../errors/UriSyntaxError'); const IllegalArgumentError = require('../errors/IllegalArgumentError'); const NotImplementedError = require('../errors/NotImplementedError'); const validateThat = require('../validator/ParameterValidator').validateThat; const ValueConverter = require('../utils/ValueConverter'); const ValueValidator = require('../validator/ValueValidator'); const JsonContentTypeInfo = require('../format/JsonContentTypeInfo'); const REGEXP_SINGLE_QUOTE = new RegExp("'", 'g'); const REGEXP_TWO_SINGLE_QUOTES = new RegExp("''", 'g'); /** * UriHelper has utility methods for reading and constructing URIs. */ class UriHelper { /** * Parse the url query string parameters. Leading '?' is allowed. * Overloaded parameters will result in an error. * * Example: * Input: '?foo=bar&bar=foo' * Output: { foo: 'bar1', bar: 'foo' } * * @param {string} queryString the query string * @throws {IllegalArgumentError} if any parameter is overloaded * @returns {Object} the parsed url query string represented as key:value pairs */ static parseQueryString(queryString) { if (!queryString) return null; const temp = queryString.startsWith('?') ? queryString.substring(1) : queryString; const result = QueryString.parse(temp); const duplicate = Object.keys(result).find(name => Array.isArray(result[name])); if (duplicate) { throw new UriSyntaxError(UriSyntaxError.Message.DUPLICATED_OPTION, duplicate); } return result; } /** * Build the normalized string literal form of a value according to its edm type. * * @param {string} uriLiteral The current uri literal * @param {EdmType} edmType The current edm type for converting the literal into * @returns {?string} the converted string or null if uriLiteral is null */ static fromUriLiteral(uriLiteral, edmType) { if (edmType === EdmPrimitiveTypeKind.String) { return uriLiteral.substring(1, uriLiteral.length - 1).replace(REGEXP_TWO_SINGLE_QUOTES, "'"); } else if (edmType === EdmPrimitiveTypeKind.Duration || edmType === EdmPrimitiveTypeKind.Binary || edmType.getKind() === EdmTypeKind.ENUM || edmType.getName().startsWith('Geo')) { return uriLiteral.substring(uriLiteral.indexOf("'") + 1, uriLiteral.length - 1); } else if (edmType.getKind() === EdmTypeKind.DEFINITION) { return UriHelper.fromUriLiteral(uriLiteral, edmType.getUnderlyingType()); } return uriLiteral; } /** * Build the URI string literal form of a value given as string according to its EDM type. * * @param {string} value the value * @param {EdmType} edmType the EDM type of the value * @returns {string} the converted string */ static toUriLiteral(value, edmType) { if (value === null) return 'null'; if (edmType === EdmPrimitiveTypeKind.String) return "'" + value.replace(REGEXP_SINGLE_QUOTE, "''") + "'"; if (edmType === EdmPrimitiveTypeKind.Duration) return "duration'" + value + "'"; if (edmType === EdmPrimitiveTypeKind.Binary) return "binary'" + value + "'"; if (edmType.getKind() === EdmTypeKind.DEFINITION) { return UriHelper.toUriLiteral(value, edmType.getUnderlyingType()); } if (edmType.getKind() === EdmTypeKind.ENUM) return edmType.getFullQualifiedName() + "'" + value + "'"; if (edmType.getName().startsWith('Geography')) return "geography'" + value + "'"; if (edmType.getName().startsWith('Geometry')) return "geometry'" + value + "'"; return value; } /** * Decode each array element with js built-in decodeURIComponent(). * * @param {string[]} components Array of strings * @returns {string[]} Array of decoded strings */ static decodeUriComponents(components) { return components.map(decodeURIComponent); } /** * Builds an abstract output structure for a given array of UriParameter. The structure would look like * [ * { type: <type of property>, name: <name of key property>, value: <value of key> }, * ... * ] * * @param {UriParameter[]} keyPredicates The array of UriParameter * @returns {Object[]} The key predicates */ static buildKeyPredicates(keyPredicates) { if (!Array.isArray(keyPredicates)) return []; return keyPredicates.map(elem => { const edmRef = elem.getEdmRef(); return { type: edmRef.getProperty().getType(), name: edmRef.getAlias() || edmRef.getName(), value: elem.getText() }; }); } /** * Build an array of objects with names, types, and values of the keys of an entity. * @param {EdmEntityType} edmEntityType the entity type * @param {*} entity the entity * @returns {Object[]} the keys * @throws {IllegalArgumentError} if the key properties in edmEntityType do not match the keys in entity */ static buildEntityKeys(edmEntityType, entity) { validateThat('edmEntityType', edmEntityType).notNullNorUndefined().instanceOf(Object); validateThat('entity', entity).notNullNorUndefined(); let keys = []; const valueConverter = new ValueConverter(new ValueValidator(), // The output is a string, so the parameter to expect the format according to IEEE754 // can be set unconditionally. This is needed, e.g., for large Int64 values. new JsonContentTypeInfo().addParameter(JsonContentTypeInfo.FormatParameter.IEEE754, 'true')); for (const [name, keyPropertyRef] of edmEntityType.getKeyPropertyRefs()) { let value = entity; const property = keyPropertyRef.getProperty(); for (const pathElement of keyPropertyRef.getName().split('/')) { value = value[pathElement]; if (value === undefined) { throw new IllegalArgumentError(`The key '${pathElement}' does not exist in the given entity`); } } value = valueConverter.convert(property, value); keys.push({ type: property.getType(), name, value }); } return keys; } /** * Build the key as string out of key information in URI form, including parentheses. * @param {Object[]} keys the keys of the entity as array of objects with name, value, and type * @returns {string} the key */ static buildKeyString(keys) { let url; for (const key of keys) { url = url ? (url + ',') : ''; if (keys.length > 1) url += encodeURIComponent(key.name) + '='; url += encodeURIComponent(UriHelper.toUriLiteral(key.value, key.type)); } return '(' + url + ')'; } /** * Build the canonical URL of an entity. * @param {UriResource[]} pathSegments the path segments leading to the entity * @param {Object[]} keys the keys of the entity as array of objects with name, value, and type * @returns {string} the canonical URL */ static buildCanonicalUrl(pathSegments, keys) { let result = ''; let isCollection; for (const pathSegment of pathSegments) { if (pathSegment.getEntitySet()) { result = encodeURIComponent(pathSegment.getEntitySet().getName()); isCollection = true; } else if (pathSegment.getTarget()) { result = encodeURIComponent(pathSegment.getTarget().getName()); isCollection = true; } else if (pathSegment.getNavigationProperty() && pathSegment.getNavigationProperty().containsTarget()) { result += '/' + encodeURIComponent(pathSegment.getNavigationProperty().getName()); isCollection = pathSegment.isCollection(); } else if (pathSegment.getFunction()) { throw new NotImplementedError('Determination of the canonical URL for the result of a ' + 'function import or of a bound function without entity-set definition is not supported.'); } if (pathSegment.getKeyPredicates().length // With referential constraints, some key predicates can be omitted. // In that case, we rely on the key values provided with the data. && pathSegment.getKeyPredicates().length === pathSegment.getEdmType().getKeyPropertyRefs().size) { result += UriHelper.buildKeyString(UriHelper.buildKeyPredicates(pathSegment.getKeyPredicates())); isCollection = false; } } if (isCollection) result += UriHelper.buildKeyString(keys); return result; } } module.exports = UriHelper;