@sap/odata-v4
Version:
OData V4.0 server library
619 lines (562 loc) • 28.8 kB
JavaScript
;
const ValueConverter = require('../utils/ValueConverter');
const ValueValidator = require('../validator/ValueValidator');
const JsonContentTypeInfo = require('../format/JsonContentTypeInfo');
const EdmTypeKind = require('../edm/EdmType').TypeKind;
const EdmPrimitiveTypeKind = require('../edm/EdmPrimitiveTypeKind');
const DeserializationError = require('../errors/DeserializationError');
const JsonAnnotations = require('../format/JsonFormat').Annotations;
const UriResource = require('../uri/UriResource');
const ExpandItem = require('../uri/ExpandItem');
const UriParser = require('../uri/UriParser');
const UriInfo = require('../uri/UriInfo');
const odataAnnotations = new Map()
.set(JsonAnnotations.CONTEXT);
/**
* This class deserializes and converts a provided payload into
* an OData object payload. All primitive values are converted into
* the corresponding OData values. I.e., a binary property will be
* converted into a Buffer instance.
*/
class ResourceJsonDeserializer {
/**
* Creates an instance of ResourceJsonDeserializer.
*
* @param {Edm} edm the current EDM instance
* @param {JsonContentTypeInfo} [formatParams] JSON format parameters
*/
constructor(edm, formatParams = new JsonContentTypeInfo()) {
this._edm = edm;
this._formatParams = formatParams;
this._converter = new ValueConverter(new ValueValidator(), formatParams);
}
/**
* Deserializes a provided json payload string of an entity.
*
* @param {EdmType} edmType The edm type of the entity
* @param {string} value The json data string to deserialize
* @param {ExpandItem[]} expand The current expand items
* @returns {Object} The deserialized result
* @throws {DeserializationError} Thrown if provided data can not be deserialized
*/
deserializeEntity(edmType, value, expand) {
try {
return this._deserializeStructuralType(edmType, JSON.parse(value), false, expand);
} catch (e) {
if (e instanceof DeserializationError) throw e;
throw new DeserializationError('An error occurred during deserialization of the entity.', e);
}
}
/**
* Deserializes a provided json payload string of an entity collection.
*
* @param {EdmType} edmType The edm type of the entity collection
* @param {string|Object[]} value The json data string to deserialize
* @param {ExpandItem[]} expand The current expand items
* @returns {Object[]} The deserialized result
* @throws {DeserializationError} Thrown if provided data can not be deserialized
*/
deserializeEntityCollection(edmType, value, expand) {
let tempData = JSON.parse(value);
if (typeof tempData !== 'object') {
throw new DeserializationError('Value for the collection must be an object.');
}
const wrongName = Object.keys(tempData).find(name => !odataAnnotations.has(name) && name !== 'value');
if (wrongName) throw new DeserializationError(`'${wrongName}' is not allowed in a collection value.`);
tempData = tempData.value;
if (!Array.isArray(tempData)) {
const type = edmType.getFullQualifiedName().toString();
throw new DeserializationError(`Input must be a collection of type '${type}'.`);
}
try {
return tempData.map(entityValue => this._deserializeStructuralType(edmType, entityValue, true, expand));
} catch (e) {
if (e instanceof DeserializationError) throw e;
throw new DeserializationError('An error occurred during deserialization of the collection.', e);
}
}
/**
* Deserializes a provided json payload string of a complex property.
*
* @param {EdmProperty} edmProperty The edm property of this complex property
* @param {string} propertyValue The json data string to deserialize
* @returns {Object} The deserialized result
* @throws {DeserializationError} Thrown if provided data can not be deserialized
*/
deserializeComplexProperty(edmProperty, propertyValue) {
return this.deserializeEntity(edmProperty.getType(), propertyValue);
}
/**
* Deserializes a provided json payload string of a complex property collection.
*
* @param {EdmProperty} edmProperty The edm property of this complex property collection
* @param {string} propertyValue The json data string
* @returns {Object[]} The deserialized result
* @throws {DeserializationError} Thrown if provided data can not be deserialized
*/
deserializeComplexPropertyCollection(edmProperty, propertyValue) {
return this.deserializeEntityCollection(edmProperty.getType(), propertyValue);
}
/**
* Deserializes a provided json payload string of a primitive property.
*
* @param {EdmProperty} edmProperty The edm property of this primitive property
* @param {string} propertyValue The json data string to deserialize
* @returns {number|string|boolean} The deserialized result
* @throws {DeserializationError} Thrown if provided data can not be deserialized
*/
deserializePrimitiveProperty(edmProperty, propertyValue) {
let tempData = JSON.parse(propertyValue);
if (typeof tempData !== 'object') {
throw new DeserializationError('Value for primitive property must be an object.');
}
const wrongName = Object.keys(tempData).find(name => !odataAnnotations.has(name) && name !== 'value');
if (wrongName) throw new DeserializationError(`'${wrongName}' is not allowed in a primitive value.`);
tempData = tempData.value;
if (tempData === undefined) throw new DeserializationError('Value can not be omitted.');
try {
return this._deserializePrimitive(edmProperty, tempData);
} catch (e) {
if (e instanceof DeserializationError) throw e;
throw new DeserializationError('An error occurred during deserialization of the property.', e);
}
}
/**
* Deserializes a provided json payload string of a primitive property collection.
*
* @param {EdmProperty} edmProperty The edm property of this primitive property collection
* @param {string} value The json data string to deserialize
* @returns {number[]|string[]|boolean[]} The deserialized result
* @throws {DeserializationError} Thrown if provided data can not be deserialized
*/
deserializePrimitivePropertyCollection(edmProperty, value) {
let tempData = JSON.parse(value);
if (typeof tempData !== 'object') {
throw new DeserializationError('Value for primitive collection property must be an object.');
}
const wrongName = Object.keys(tempData).find(name => !odataAnnotations.has(name) && name !== 'value');
if (wrongName) throw new DeserializationError(`'${wrongName}' is not allowed in a primitive-collection value.`);
tempData = tempData.value;
this._assertPropertyIsCollection(edmProperty, tempData);
try {
return this._deserializePrimitive(edmProperty, tempData);
} catch (e) {
if (e instanceof DeserializationError) throw e;
throw new DeserializationError('An error occurred during deserialization of the property.', e);
}
}
/**
* Deserializes a provided JSON payload string of an entity reference.
*
* @param {EdmType} edmType the EDM type of the entity
* @param {string} value the JSON data string to deserialize
* @returns {UriInfo} the deserialized result
* @throws {DeserializationError} if provided data can not be deserialized
*/
deserializeReference(edmType, value) {
const tempData = JSON.parse(value);
if (typeof tempData !== 'object') {
throw new DeserializationError('Value for reference must be an object.');
}
try {
return this._deserializeReference(edmType, tempData);
} catch (e) {
if (e instanceof DeserializationError) throw e;
throw new DeserializationError('An error occurred during deserialization of the reference.', e);
}
}
/**
* Deserializes a provided json payload string of action parameters.
*
* @param {Action} action The action to deserialize the payload for
* @param {string} value The json data string to deserialize
* @returns {Object} The deserialized result
* @throws {DeserializationError} Thrown if provided data can not be deserialized
*/
deserializeActionParameters(action, value) {
let data;
let result = {};
let parameters = Array.from(action.getParameters());
// Skip the first param if the action is bound
// because the first param of a bound action is the binding parameter
// which is not part of the payload data.
if (action.isBound()) parameters.shift();
for (const [paramName, edmParam] of parameters) {
if (value == null && !edmParam.isNullable()) {
throw new DeserializationError(`Parameter '${paramName}' is not nullable but payload is null`);
} else if (data == null) {
data = JSON.parse(value);
if (typeof data !== 'object') {
throw new DeserializationError('Value for action parameters must be an object.');
}
}
let paramValue = data[paramName];
if (!Object.prototype.hasOwnProperty.call(data, paramName)) {
// OData JSON Format Version 4.0 Plus Errata 03 - 17 Action Invocation:
// "Any parameter values not specified in the JSON object are assumed to have the null value."
//
// Set the value to null because further algorithm asserts nullable values already.
// Therefore the value must be null, not undefined.
paramValue = null;
}
this._assertPropertyIsCollection(edmParam, paramValue);
const edmType = edmParam.getType();
switch (edmType.getKind()) {
case EdmTypeKind.PRIMITIVE:
case EdmTypeKind.ENUM:
case EdmTypeKind.DEFINITION:
result[paramName] = this._deserializePrimitive(edmParam, paramValue);
break;
case EdmTypeKind.COMPLEX:
case EdmTypeKind.ENTITY: // Both are structured types.
result[paramName] = this._deserializeComplex(edmParam, paramValue);
break;
default:
throw new DeserializationError(
`Could not deserialize parameter '${paramName}'. EdmTypeKind '${edmType.getKind()}' is invalid.`
);
}
}
const names = data ? Object.keys(data) : [];
const wrongParameter = names.find(name => !parameters.some(p => p[0] === name));
if (wrongParameter) {
throw new DeserializationError(
`'${wrongParameter}' is not a non-binding parameter of action '${action.getName()}'.`);
}
return result;
}
/**
* Deserializes a value for a structural type with its properties.
* If the provided data is not type of object an error is thrown.
*
* @param {EdmType} edmType The edm type of the provided value
* @param {Object} valueParam The structural object to deserialize
* @param {boolean} isNested whether the structural object is nested in another object
* @param {ExpandItem[]} expand The current expand items
* @returns {Object} The deserialized result
* @throws {DeserializationError} if provided data can not be deserialized
* @private
*/
_deserializeStructuralType(edmType, valueParam, isNested, expand) {
if (typeof valueParam !== 'object' || Array.isArray(valueParam)) {
throw new DeserializationError('Value for structural type must be an object.');
}
const value = valueParam;
for (const propertyName of Object.keys(value)) {
const propertyValue = valueParam[propertyName];
if (propertyName.includes('@')) {
if (odataAnnotations.has(propertyName) && !isNested) {
value[propertyName] = propertyValue;
} else if (propertyName.endsWith(JsonAnnotations.BIND)) {
const navPropertyName =
propertyName.substring(0, propertyName.length - JsonAnnotations.BIND.length);
const edmNavigationProperty = edmType.getNavigationProperty(navPropertyName);
this._assertPropertyExists(edmNavigationProperty, edmType, navPropertyName);
this._assertPropertyIsCollection(edmNavigationProperty, propertyValue);
this._assertPropertyNullable(edmNavigationProperty, propertyValue);
const edmEntityType = edmNavigationProperty.getEntityType();
value[propertyName] = Array.isArray(propertyValue) ?
propertyValue.map(uri => this._parseEntityUri(edmEntityType, uri)) :
this._parseEntityUri(edmEntityType, propertyValue);
} else {
throw new DeserializationError(`Annotation '${propertyName}' is not supported.`);
}
} else {
const edmProperty = edmType.getProperty(propertyName);
this._assertPropertyExists(edmProperty, edmType, propertyName);
this._assertPropertyIsCollection(edmProperty, propertyValue);
const kind = edmProperty.getEntityType ?
edmProperty.getEntityType().getKind() :
edmProperty.getType().getKind();
switch (kind) {
case EdmTypeKind.COMPLEX:
value[propertyName] = this._deserializeComplex(edmProperty, propertyValue);
break;
case EdmTypeKind.ENTITY:
value[propertyName] = this._deserializeNavigation(edmProperty, propertyValue, isNested, expand);
break;
default:
value[propertyName] = this._deserializePrimitive(edmProperty, propertyValue);
break;
}
}
}
return value;
}
/**
* Deserialize a navigation property and fill an expand item for deep inserts.
*
* @param {EdmNavigationProperty} edmProperty The navigation property to deserialize
* @param {any} value The value of the navigation property
* @param {boolean} isNested True if the navigation property is nested
* @param {ExpandItem[]} expandArray the current expand items
* @returns {Object} Returns the deserialized navigation property
* @private
*/
_deserializeNavigation(edmProperty, value, isNested, expandArray) {
this._assertPropertyNullable(edmProperty, value);
let currentExpandItem = expandArray
.find(expandItem => expandItem.getPathSegments()[0].getNavigationProperty() === edmProperty);
if (!currentExpandItem) {
currentExpandItem = this._createExpandItem(edmProperty);
expandArray.push(currentExpandItem);
}
const type = edmProperty.getEntityType();
let newExpandOptionArray = [];
const result = edmProperty.isCollection() ?
value.map(entity => this._deserializeStructuralType(type, entity, isNested, newExpandOptionArray)) :
this._deserializeStructuralType(type, value, isNested, newExpandOptionArray);
// Attach the sub-expand to its parent only if there is at least one path segment
// available in the child expand. In this case at least one entity inside the collection
// has another navigation property.
let innerExpandItems = currentExpandItem.getOption(UriInfo.QueryOptions.EXPAND) || [];
for (const newExpand of newExpandOptionArray) {
if (!innerExpandItems.some(item =>
item.getPathSegments()[0].getNavigationProperty()
=== newExpand.getPathSegments()[0].getNavigationProperty()));
innerExpandItems.push(newExpand);
}
if (innerExpandItems.length) currentExpandItem.setOption(UriInfo.QueryOptions.EXPAND, innerExpandItems);
return result;
}
/**
* Deserializes a primitive and primitive collection value.
*
* @param {EdmProperty} edmProperty The edm property of this primitive value
* @param {number|string|Array|boolean} propertyValue The json value
* @returns {number|string|Array|boolean} The deserialized result
* @throws {DeserializationError} if provided data can not be deserialized
* @private
*/
_deserializePrimitive(edmProperty, propertyValue) {
if (edmProperty.isCollection()) {
return propertyValue.map(value => this._convertPrimitiveValue(edmProperty, value));
}
return this._convertPrimitiveValue(edmProperty, propertyValue);
}
/**
* Deserializes a complex and complex collection value.
*
* @param {EdmProperty} edmProperty The edm property of this complex value
* @param {Object} propertyValue The json value
* @returns {Object|Object[]} The deserialized result
* @throws {DeserializationError} Thrown if provided data can not be deserialized
* @private
*/
_deserializeComplex(edmProperty, propertyValue) {
if (edmProperty.isCollection()) {
return propertyValue.map(value => {
this._assertPropertyNullable(edmProperty, value);
return value === null ? null : this._deserializeStructuralType(edmProperty.getType(), value, true);
});
}
this._assertPropertyNullable(edmProperty, propertyValue);
return propertyValue === null ?
null :
this._deserializeStructuralType(edmProperty.getType(), propertyValue, true);
}
/**
* Asserts that the provided property value is a collection if the
* corresponding edm property is a collection.
*
* @param {EdmProperty|EdmNavigationProperty} edmProperty The edm property for the corresponding property value
* @param {*} propertyValue
* @throws {DeserializationError} Thrown if the property value is a collection while edm property is not
* and vice versa
* @private
*/
_assertPropertyIsCollection(edmProperty, propertyValue) {
if (edmProperty.isCollection() && !Array.isArray(propertyValue)) {
const type = edmProperty.getType ? edmProperty.getType() : edmProperty.getEntityType();
throw new DeserializationError(
`'${edmProperty.getName()}' must be a collection of type '${type.getFullQualifiedName()}'.`);
}
if (!edmProperty.isCollection() && Array.isArray(propertyValue)) {
throw new DeserializationError(`'${edmProperty.getName()}' must not be a collection.`);
}
}
/**
* Asserts that the provided edm property is defined
*
* @param {EdmProperty|EdmNavigationProperty} edmProperty The edm property to check
* @param {EdmType} edmType The edm type of the edm property
* @param {string} propertyName The name of the property
* @throws {DeserializationError} Thrown if the provided edm property is not defined
* @private
*/
_assertPropertyExists(edmProperty, edmType, propertyName) {
if (!edmProperty) {
const fqn = edmType.getFullQualifiedName();
throw new DeserializationError(`'${propertyName}' does not exist in type '${fqn}'.`);
}
}
/**
* Asserts that the provided EDM property is nullable if it has a null value.
*
* @param {EdmProperty|EdmNavigationProperty} edmProperty the EDM property to check
* @param {*} propertyValue the value of the property
* @throws {DeserializationError} if null is not allowed
* @private
*/
_assertPropertyNullable(edmProperty, propertyValue) {
if (propertyValue === null && !edmProperty.isNullable()) {
throw new DeserializationError(
`The property '${edmProperty.getName()}' is not nullable and must not have a null value.`);
}
}
/**
* Converts a provided value into the appropriate odata value.
* Available facets are taken into account. If the property's
* facet nullable=true the value is allowed to be null.
*
* @param {EdmProperty} edmProperty The corresponding edm property
* @param {?(number|string|boolean)} propertyValue The JSON value of the primitive
* @returns {?(number|string|boolean|Buffer)} The primitive javascript value
* @throws {DeserializationError} Thrown if the conversion was not successful
* @private
*/
_convertPrimitiveValue(edmProperty, propertyValue) {
const type = edmProperty.getType();
if (type === EdmPrimitiveTypeKind.Stream) {
throw new DeserializationError(
"The stream property '" + edmProperty.getName() + "' should not appear in the payload.");
}
if (propertyValue === undefined) {
throw new DeserializationError(`Missing value for property '${edmProperty.getName()}'.`);
}
this._assertPropertyNullable(edmProperty, propertyValue);
if (propertyValue === null) return null;
if (type === EdmPrimitiveTypeKind.Binary) {
const valueBuffer = Buffer.from(propertyValue, 'base64');
this._converter.convert(edmProperty, valueBuffer); // to check the MaxLength facet
// The method Buffer.from(...) does not throw an error on invalid input;
// it simply returns the result of the conversion of the content up to the first error.
// So we check if the length is correct, taking padding characters into account (see RFC 4648).
// Newline or other whitespace characters are not allowed according to the OData JSON format specification.
let length = propertyValue.length * 3 / 4; // Four base64 characters result in three octets.
if (propertyValue.length % 4) { // The length is not a multiple of four as it should be.
length = 3 * Math.floor(propertyValue.length / 4)
// The remainder (due to missing padding characters) will result in one or two octets.
+ Math.ceil((propertyValue.length % 4) / 2);
} else {
// Padding characters reduce the amount of expected octets.
if (propertyValue.endsWith('==')) length--;
if (propertyValue.endsWith('=')) length--;
}
if (valueBuffer.length < length) {
throw new DeserializationError(
`The value for property '${edmProperty.getName()}' is not valid base64 content.`);
}
return valueBuffer;
}
if (type === EdmPrimitiveTypeKind.Int64 || type === EdmPrimitiveTypeKind.Decimal) {
const kind = this._formatParams.getIEEE754Setting() ? 'string' : 'number';
// We don't allow JSON numbers as decimal values because there is no way to tell how
// they looked like originally; JSON.parse() rounds all numbers to 64-bit floating-point numbers.
if (type === EdmPrimitiveTypeKind.Decimal && kind === 'number') {
throw new DeserializationError(`A JSON number is not supported as ${type} value.`);
}
if (kind !== typeof propertyValue) {
throw new DeserializationError(
`Invalid value: ${propertyValue}. `
+ `A JSON ${kind} must be specified as value for property '${edmProperty.getName()}'.`);
}
const value = this._converter.convert(edmProperty, propertyValue);
return String(value);
}
if (type.getKind() === EdmTypeKind.ENUM) {
if (typeof propertyValue !== 'string') {
throw new DeserializationError(`Invalid value: ${propertyValue}. `
+ `A JSON string must be specified as value for property '${edmProperty.getName()}'.`);
}
let enumValue = null;
for (const value of propertyValue.split(',')) {
let memberValue = null;
for (const [name, member] of type.getMembers()) {
if (value === name || value === member.getValue().toString()) {
memberValue = member.getValue();
break;
}
}
if (memberValue === null || enumValue !== null && !type.isFlags()) {
throw new DeserializationError(
`Invalid value '${propertyValue}' for property '${edmProperty.getName()}'.`);
}
// Use bitwise OR operator to set the member-value bits in the enumeration value.
enumValue = enumValue === null ? memberValue : enumValue | memberValue;
}
return enumValue;
}
// The value converter also asserts maxLength, scale, precision facets.
return this._converter.convert(edmProperty, propertyValue);
}
/**
* Parses and checks a URI given as reference to an entity.
* @param {EdmEntityType} edmEntityType the EDM type of the entity
* @param {string} uri the URI
* @returns {UriInfo}
* @private
*/
_parseEntityUri(edmEntityType, uri) {
if (uri === null) return null;
if (typeof uri !== 'string') {
throw new DeserializationError(
`The reference URI for type '${edmEntityType.getFullQualifiedName()}' must be a string.`);
}
const uriInfo = new UriParser(this._edm).parseRelativeUri(uri);
if (!uriInfo.getPathSegments().every(segment =>
segment.getKind() === UriResource.ResourceKind.ENTITY
|| segment.getKind() === UriResource.ResourceKind.NAVIGATION_TO_ONE
&& segment.getNavigationProperty().containsTarget())
|| uriInfo.getFinalEdmType() !== edmEntityType) {
throw new DeserializationError(
`The reference URI '${uri}' is not suitable for type '${edmEntityType.getFullQualifiedName()}'.`);
}
return uriInfo;
}
/**
* Deserializes a provided entity-reference object.
*
* @param {EdmType} edmType the EDM type of the entity
* @param {Object} value the reference object to deserialize
* @returns {UriInfo} the deserialized result
* @throws {DeserializationError} if provided data can not be deserialized
* @private
*/
_deserializeReference(edmType, value) {
const objectKeys = Object.keys(value);
if (objectKeys.length === 0) {
throw new DeserializationError(`Value for type '${edmType.getFullQualifiedName()}' has no properties.`);
}
let uriInfo;
for (const propertyName of objectKeys) {
if (odataAnnotations.has(propertyName)) continue;
if (propertyName === JsonAnnotations.ID) {
uriInfo = this._parseEntityUri(edmType, value[propertyName]);
if (uriInfo === null) {
throw new DeserializationError(
`The reference URI for type '${edmType.getFullQualifiedName()}' must be a string.`);
}
} else {
throw new DeserializationError(`Property or annotation '${propertyName}' is not supported.`);
}
}
return uriInfo;
}
/**
* Creates an expand item for a navigation property.
* @param {EdmNavigationProperty} edmNavigationProperty the EDM navigation property
* @returns {ExpandItem} the expand item
* @private
*/
_createExpandItem(edmNavigationProperty) {
return new ExpandItem()
.setPathSegments([new UriResource()
.setKind(edmNavigationProperty.isCollection() ?
UriResource.ResourceKind.NAVIGATION_TO_MANY : UriResource.ResourceKind.NAVIGATION_TO_ONE)
.setNavigationProperty(edmNavigationProperty)
.setIsCollection(edmNavigationProperty.isCollection())
]);
}
}
module.exports = ResourceJsonDeserializer;