@sap/odata-v4
Version:
OData V4.0 server library
207 lines (188 loc) • 9.15 kB
JavaScript
;
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;