UNPKG

@sap/odata-v4

Version:

OData V4.0 server library

619 lines (562 loc) 28.8 kB
'use strict'; 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;