UNPKG

@sap/odata-v4

Version:

OData V4.0 server library

594 lines (522 loc) 24.7 kB
'use strict'; const Big = require('big.js'); const YEAR_RE = '(?:-?(?:(?:(?:0\\d{3})|(?:[1-9]\\d{3,}))))'; const MONTH_RE = '(?:(?:0[1-9])|(?:1[012]))'; const DAY_RE = '(?:(?:0[1-9])|(?:[12]\\d)|(?:3[01]))'; const HOURS_RE = '(?:(?:[01]\\d)|(?:2[0-3]))'; const MINUTES_RE = '[0-5]\\d'; const SECONDS_RE = MINUTES_RE; const FRACT_SECONDS_RE = '(\\d{1,12})'; const TIME_ZONE_RE = '(?:Z|(?:[+-]' + HOURS_RE + ':' + MINUTES_RE + '))'; // RegExp for Edm.Date values const DATE_REG_EXP = new RegExp('^(?:' + YEAR_RE + '-' + MONTH_RE + '-' + DAY_RE + ')$'); // RegExp for Edm.DateTimeOffset values const DATETIME_OFFSET_REG_EXP = new RegExp('^(?:' + YEAR_RE + '-' + MONTH_RE + '-' + DAY_RE + 'T' + HOURS_RE + ':' + MINUTES_RE + '(?::' + SECONDS_RE + '(?:\\.' + FRACT_SECONDS_RE + ')?)?' + TIME_ZONE_RE + ')$'); // RegExp for Edm.TimeOfDay values const TIME_OF_DAY_REG_EXP = new RegExp('^(?:' + HOURS_RE + ':' + MINUTES_RE + '(?::' + SECONDS_RE + '(?:\\.' + FRACT_SECONDS_RE + ')?)?)$'); const DURATION_TIME_RE = '(?:T(?:(?:(?:\\d+H)(?:\\d+M)?(?:\\d+(?:\\.(\\d+))?S)?)|(?:(?:\\d+M)(?:\\d+' + '(?:\\.(\\d+))?S)?)|(?:(?:\\d+(?:\\.(\\d+))?S))))'; // RegExp for Edm.Duration values const DURATION_REG_EXP = new RegExp('^(?:-?P(?:(?:(?:\\d+D)' + DURATION_TIME_RE + '?)|' + DURATION_TIME_RE + '))$'); // RegExp for Edm.Guid values const HEX_DIG = '[A-Fa-f0-9]'; const GUID_REG_EXP = new RegExp('^(?:' + HEX_DIG + '{8}-' + HEX_DIG + '{4}-' + HEX_DIG + '{4}-' + HEX_DIG + '{4}-' + HEX_DIG + '{12})$'); // RegExp for valid names of EPSG-defined coordinate reference systems used in geography/geometry values const GEO_CRS_NAME_REG_EXP = new RegExp('^EPSG:\\d{1,8}$'); // RegExp for ETag values const ETAG_VALUE_REG_EXP = new RegExp('^[!#-~\\x80-\\xFF]*$'); // %x21 / %x23-7E / obs-text // max value for Edm.Int64 const INT64_MAX = new Big('9223372036854775807'); // min value for Edm.Int64 const INT64_MIN = new Big('-9223372036854775808'); // min value for IEEE 754 binary32 (i.e. Edm.Single) const SINGLE_MIN = 1.401298464324817E-45; // max value for IEEE 754 binary32 (i.e. Edm.Single) const SINGLE_MAX = 3.4028234663852886E+38; function createBig(value) { try { return new Big(value); } catch (e) { // Big constructor throws NaN if the input is not a number. // Return NaN here to avoid yet another try-catch block in the calling function return Number.NaN; } } /** * Validator of values according to the OData ABNF Construction Rules. */ class ValueValidator { /** * Validates value of Edm.Binary type. * @param {Buffer} value - Edm.Binary value * @param {number} maxLength - value of MaxLength facet */ validateBinary(value, maxLength) { if (!Buffer.isBuffer(value)) { throw new Error( `Invalid value: ${value}. A Buffer instance must be specified for a value of Edm.Binary type.`); } this._checkMaxLength(value, maxLength, 'Edm.Binary'); } /** * Validates value of Edm.Boolean type. * @param {boolean} value - Edm.Boolean value */ validateBoolean(value) { if (typeof value !== 'boolean') { throw new Error( `Invalid value: ${value}. A boolean value must be specified for a value of Edm.Boolean type.`); } } /** * Validates value of Edm.Byte type. * @param {number} value - Edm.Byte value */ validateByte(value) { this._validateIntegerValue(value, 'Byte', 0, 255); } /** * Returns true if value is of type byte. * @param {number} value the value to check * @returns {boolean} true if value is byte, else false */ isByte(value) { return Number.isInteger(value) && value >= 0 && value <= 255; } /** * Validates value of Edm.SByte type. * @param {number} value - Edm.SByte value */ validateSByte(value) { this._validateIntegerValue(value, 'SByte', -128, 127); } /** * Returns true if value is of type sbyte. * @param {number} value the value to check * @returns {boolean} true if value is sbyte, else false */ isSByte(value) { return Number.isInteger(value) && value >= -128 && value <= 127; } /** * Validates value of Edm.Int16 type. * @param {number} value - Edm.Int16 value */ validateInt16(value) { this._validateIntegerValue(value, 'Int16', -32768, 32767); } /** * Returns true if value is of type int16. * @param {number} value the value to check * @returns {boolean} true if value is int16, else false */ isInt16(value) { return Number.isInteger(value) && value >= -32768 && value <= 32767; } /** * Validates value of Edm.Int32 type. * @param {number} value - Edm.Int32 value */ validateInt32(value) { this._validateIntegerValue(value, 'Int32', -2147483648, 2147483647); } /** * Returns true if value is of type int32. * @param {number} value the value to check * @returns {boolean} true if value is int32, else false */ isInt32(value) { return Number.isInteger(value) && value >= -2147483648 && value <= 2147483647; } /** * Validates value of Edm.Int64 type. * @param {number|string} value - Edm.Int64 value. Values in exponential notation are also supported. */ validateInt64(value) { if (!this.isInt64(value)) { throw new Error(`Invalid value: ${value}. The value does not representing an integer or its value` + ' is not in range from -9223372036854775808 to 9223372036854775807'); } } /** * Returns true if value is of type int64. * @param {number|string} value the value to check * @returns {boolean} true if value is int64, else false */ isInt64(value) { const bigValue = createBig(value); return !Number.isNaN(bigValue) && bigValue.round(0).eq(bigValue) && bigValue.gte(INT64_MIN) && bigValue.lte(INT64_MAX); } /** * Validates, whether the value is an integer value, which belongs to the specified value range, defined via 'from' * and 'to' input parameters. * * @param {number} value - Any value, which should be validated * @param {string} edmType - name of the EDM type, for which the value is validated * @param {number} from - beginning of the valid value range, which the value must belong to * @param {number} to - end of the valid value range, which the value must belong to * @private */ _validateIntegerValue(value, edmType, from, to) { if (!Number.isInteger(value)) { throw new Error(`Invalid value: ${value}. An integer value must be specified for a value of` + ` Edm.${edmType} type.`); } if (value < from || value > to) { throw new Error(`Invalid value: ${value}. Only number in the range from ${from} to ${to}` + ` is allowed for a value of Edm.${edmType} type.`); } } /** * Validates value of Edm.String type. * @param {string} value - Edm.String value * @param {number} maxLength - value of MaxLength facet */ validateString(value, maxLength) { if (typeof value !== 'string') { throw new Error( 'Invalid value: ' + value + '. A string value must be specified for a value of Edm.String type.'); } this._checkMaxLength(value, maxLength, 'Edm.String'); } /** * Checks that the value is not longer than the specified maximum length. * * @param {string} value value to be checked * @param {number} maxLength value of the MaxLength facet for the property, which has the specified value * @param {string} typeName name of the type of the value * @throws {Error} if the condition is not met * @private */ _checkMaxLength(value, maxLength, typeName) { // consider only integer maxLength values, ignoring both unspecified and the special 'max' value if (Number.isInteger(maxLength) && value.length > maxLength) { throw new Error(`Invalid value: ${value}. Length of the ${typeName} value must not be greater than the ` + `MaxLength facet value (${maxLength})`); } } /** * Validates value of Edm.Date type. * @param {string} value - Edm.Date value */ validateDate(value) { if (typeof value !== 'string' || !DATE_REG_EXP.test(value)) { throw new Error(`Invalid value: ${value}. A string value in the format YYYY-MM-DD must be specified for ` + 'a value of Edm.Date type.'); } } /** * Validates value of Edm.DateTimeOffset type. * @param {string} value - Edm.DateTimeOffset value * @param {number|string} [precision] - value of Precision facet */ validateDateTimeOffset(value, precision) { let result = DATETIME_OFFSET_REG_EXP.exec(value); if (typeof value !== 'string' || !result) { throw new Error(`Invalid value: ${value}. A string value in the format YYYY-MM-DDThh:mm:ss.sTZD must be ` + 'specified for a value of Edm.DateTimeOffset type.'); } const milliseconds = result[1]; this._checkMillisecondsPrecision(value, milliseconds, precision); } /** * Validates value of Edm.TimeOfDay type. * @param {string} value - Edm.TimeOfDay value * @param {number|string} [precision] - value of Precision facet */ validateTimeOfDay(value, precision) { let result = TIME_OF_DAY_REG_EXP.exec(value); if (typeof value !== 'string' || !result) { throw new Error(`Invalid value: ${value}. A string value in the format hh:mm:ss.s must be ` + 'specified for a value of Edm.TimeOfDay type.'); } const milliseconds = result[1]; this._checkMillisecondsPrecision(value, milliseconds, precision); } /** * Validates value of Edm.Duration type. * @param {string} value - Edm.Duration value * @param {number|string} [precision] - value of Precision facet */ validateDuration(value, precision) { let result = DURATION_REG_EXP.exec(value); if (typeof value !== 'string' || !result) { throw new Error(`Invalid value: ${value}. A string value in the format PnDTnHnMn.nS must be specified ` + 'for a value of Edm.Duration type.'); } // Because of the different combinations of the duration parts (HS, MS, S) we have 6 places (i.e. matching // groups) in the regular expression, which match milliseconds. Therefore slice() is called on the result // array to "extract" only these 6 matches and find the one, which matches, i.e. not empty const milliseconds = result.slice(1, 7).find(match => match != null); this._checkMillisecondsPrecision(value, milliseconds, precision); } /** * Checks whether the milliseconds satisfy the specified precision for the value. * * @param {string} value - temporal value, which can contain milliseconds * @param {string} milliseconds - part of the value, representing milliseconds * @param {number} precision - value of the Precision facet for the property, which has the specified value * @throws {Error} if the conditions are not met * @private */ _checkMillisecondsPrecision(value, milliseconds, precision) { // milliseconds is a string value, so just check its length if (milliseconds && precision != null && milliseconds.length > precision) { throw new Error(`Invalid value: ${value}. The number of milliseconds does not correspond to the ` + `Precision facet value ${precision}`); } } /** * Validates value of Edm.Decimal type. * * @param {number|string} value - Edm.Decimal value. Values in exponential notation are also supported. * @param {number|string} [precision] - value of Precision facet * @param {number|string} [scale] - value of Scale facet */ validateDecimal(value, precision, scale) { // precision and scale values are not validated assuming that the metadata validation is done before calling // the serializer const bigValue = createBig(value); // check that the value represents a number if (Number.isNaN(bigValue)) { throw new Error(`Invalid value: ${value}. A number or a string representing a number must be ` + 'specified for a value of Edm.Decimal type.'); } // check that the value has no more digits than specified for precision if (precision != null && bigValue.c.length > precision) { throw new Error(`Invalid value: ${value}. The specified Edm.Decimal value does not correspond to the ` + `Precision facet value ${precision}`); } if (scale == null || scale === 'variable') { return; } // specify 0 as the rounding mode to simply truncate the number wihout any sort of rounding const integerPart = bigValue.round(0, 0); if (precision === scale) { if (!integerPart.eq(0)) { throw new Error(`Invalid value: ${value}. If Precision is equal to Scale, a single zero must ` + 'precede the decimal point in the Edm.Decimal value'); } return; } // validate number of digits in the integer (i.e. left) part of the value if (precision != null && integerPart.c.length > (precision - scale)) { throw new Error(`Invalid value: ${value}. The number of digits to the left of the decimal point ` + `must not be greater than Precision minus Scale, i.e. ${precision - scale}`); } // validate number of digits in the decimal (i.e. right) part of the value const decimalPart = bigValue.minus(integerPart); if (decimalPart.c.length > scale && !decimalPart.eq(0)) { throw new Error(`Invalid value: ${value}. The specified Edm.Decimal value has more digits to the ` + `right of the decimal point than allowed by the Scale facet with ${scale} value`); } } /** * Validates value of Edm.Single type. * @param {number} value - Edm.Single value */ validateSingle(value) { if (!this.isSingle(value)) { throw new Error(`Invalid value: ${value}. Only a number having absolute value in the range from ` + `${SINGLE_MIN} to ${SINGLE_MAX} is allowed for a value of Edm.Single type.`); } } /** * Returns true if the provided value is a single precision float number * @param {number} value - Any value to check * @returns {boolean} True if the value is a valid single precision float number, else false */ isSingle(value) { if (typeof value === 'number') { const absValue = Math.abs(value); return absValue === 0 || absValue >= SINGLE_MIN && absValue <= SINGLE_MAX; } return false; } /** * Validates value of Edm.Double type. * @param {number} value - Edm.Double value */ validateDouble(value) { if (typeof value !== 'number') { throw new Error(`Invalid value: ${value}. A number value must be specified for a value of ` + 'Edm.Double type.'); } } /** * Validates value of Edm.Guid type. * @param {string} value - Edm.Guid value */ validateGuid(value) { if (typeof value !== 'string' || !GUID_REG_EXP.test(value)) { throw new Error(`Invalid value: ${value}. A string value in the format ` + '8HEXDIG-4HEXDIG-4HEXDIG-4HEXDIG-12HEXDIG must be specified for a value of Edm.Guid type.'); } } /** * Validates value of Edm.GeographyPoint or Edm.GeometryPoint type. * @param {{ type: string, coordinates: number[] }} value the value * @param {?(number|string)} [srid] value of SRID facet */ validateGeoPoint(value, srid) { if (!this._isGeoJsonObject('Point', 'coordinates', value, srid) || !this._isGeoPosition(value.coordinates)) { throw new Error( 'Invalid value: ' + JSON.stringify(value) + '. A JavaScript object with type and coordinates ' + 'must be specified for a value of geo-point type.'); } } /** * Validates value of Edm.GeographyLineString or Edm.GeometryLineString type. * @param {{ type: string, coordinates: Array.<number[]> }} value the value * @param {?(number|string)} [srid] value of SRID facet */ validateGeoLineString(value, srid) { if (!this._isGeoJsonObject('LineString', 'coordinates', value, srid) || !value.coordinates.every(this._isGeoPosition, this)) { throw new Error( 'Invalid value: ' + JSON.stringify(value) + '. A JavaScript object with type and coordinates ' + 'must be specified for a value of geo-linestring type.'); } } /** * Validates value of Edm.GeographyPolygon or Edm.GeometryPolygon type. * @param {{ type: string, coordinates: Array.<Array.<number[]>> }} value the value * @param {?(number|string)} [srid] value of SRID facet */ validateGeoPolygon(value, srid) { if (!this._isGeoJsonObject('Polygon', 'coordinates', value, srid) || !this._isGeoPolygon(value.coordinates)) { throw new Error( 'Invalid value: ' + JSON.stringify(value) + '. A JavaScript object with type and coordinates ' + 'must be specified for a value of geo-polygon type.'); } } /** * Validates value of Edm.GeographyMultiPoint or Edm.GeometryMultiPoint type. * @param {{ type: string, coordinates: Array.<number[]> }} value the value * @param {?(number|string)} [srid] value of SRID facet */ validateGeoMultiPoint(value, srid) { if (!this._isGeoJsonObject('MultiPoint', 'coordinates', value, srid) || !value.coordinates.every(this._isGeoPosition, this)) { throw new Error( 'Invalid value: ' + JSON.stringify(value) + '. A JavaScript object with type and coordinates ' + 'must be specified for a value of geo-multipoint type.'); } } /** * Validates value of Edm.GeographyMultiLineString or Edm.GeometryMultiLineString type. * @param {{ type: string, coordinates: Array.<Array.<number[]>> }} value the value * @param {?(number|string)} [srid] value of SRID facet */ validateGeoMultiLineString(value, srid) { if (!this._isGeoJsonObject('MultiLineString', 'coordinates', value, srid) || !value.coordinates.every(linestring => Array.isArray(linestring) && linestring.every(this._isGeoPosition, this))) { throw new Error( 'Invalid value: ' + JSON.stringify(value) + '. A JavaScript object with type and coordinates ' + 'must be specified for a value of geo-multilinestring type.'); } } /** * Validates value of Edm.GeographyMultiPolygon or Edm.GeometryMultiPolygon type. * @param {{ type: string, coordinates: Array.<Array.<Array.<number[]>>> }} value the value * @param {?(number|string)} [srid] value of SRID facet */ validateGeoMultiPolygon(value, srid) { if (!this._isGeoJsonObject('MultiPolygon', 'coordinates', value, srid) || !value.coordinates.every(this._isGeoPolygon, this)) { throw new Error( 'Invalid value: ' + JSON.stringify(value) + '. A JavaScript object with type and coordinates ' + 'must be specified for a value of geo-multipolygon type.'); } } /** * Validates value of Edm.GeographyCollection or Edm.GeometryCollection type. * @param {{ type: string, geometries: Array.<Object> }} value the value * @param {?(number|string)} [srid] value of SRID facet */ validateGeoCollection(value, srid) { if (!this._isGeoJsonObject('GeometryCollection', 'geometries', value, srid) || !value.geometries.every(geoObject => this._isGeoJsonObject('Point', 'coordinates', geoObject) && this._isGeoPosition(geoObject.coordinates) || this._isGeoJsonObject('LineString', 'coordinates', geoObject) && geoObject.coordinates.every(this._isGeoPosition, this) || this._isGeoJsonObject('Polygon', 'coordinates', geoObject) && this._isGeoPolygon(geoObject.coordinates) || this._isGeoJsonObject('MultiPoint', 'coordinates', geoObject) && geoObject.coordinates.every(this._isGeoPosition, this) || this._isGeoJsonObject('MultiLineString', 'coordinates', geoObject) && geoObject.coordinates.every(linestring => Array.isArray(linestring) && linestring.every(this._isGeoPosition, this)) || this._isGeoJsonObject('MultiPolygon', 'coordinates', geoObject) && geoObject.coordinates.every(this._isGeoPolygon, this))) { throw new Error( 'Invalid value: ' + JSON.stringify(value) + '. A JavaScript object with type and geometries ' + 'must be specified for a value of geo-collection type.'); } } /** * Returns true if the value is a GeoJSON object of the correct type, otherwise false. * @param {string} type name of the type * @param {string} content the name of the property with content ("coordinates" or "geometries") * @param {Object} value the value to be checked * @param {?(number|string)} [srid] value of SRID facet * @returns {boolean} whether the value is a GeoJSON object of the correct type * @private */ _isGeoJsonObject(type, content, value, srid) { return typeof value === 'object' && Object.keys(value).length === (srid === 'variable' ? 3 : 2) && value.type === type && Array.isArray(value[content]) && (srid !== 'variable' || value.crs && value.crs.type === 'name' && value.crs.properties && typeof value.crs.properties.name === 'string' && GEO_CRS_NAME_REG_EXP.test(value.crs.properties.name)); } /** * Returns true if the position is a GeoJSON position array, otherwise false. * @param {number[]} position the value to be checked * @returns {boolean} whether the position is a GeoJSON position array * @private */ _isGeoPosition(position) { return Array.isArray(position) && (position.length === 2 || position.length === 3) && typeof position[0] === 'number' && typeof position[1] === 'number' && (position[2] === undefined || typeof position[2] === 'number'); } /** * Returns true if the value is an array of coordinates for a GeoJSON polygon, otherwise false. * @param {Array.<Array.<number[]>>} polygon the value to be checked * @returns {boolean} whether the value is an array of coordinates for a GeoJSON polygon * @private */ _isGeoPolygon(polygon) { return polygon.length && polygon.every(ring => Array.isArray(ring) && ring.length >= 4 && ring.every(this._isGeoPosition, this) && ring[ring.length - 1][0] === ring[0][0] && ring[ring.length - 1][1] === ring[0][1] && ring[ring.length - 1][2] === ring[0][2]); } /** * Validates if the provided etag value matches the expected format. * The value must be a string of allowed characters as described in RFC 7232 * (see https://tools.ietf.org/html/rfc7232#section-2.3 for details). * * @param {string} value The provided etag value to validate * @returns {string} The provided value * @throws {Error} If the etag value doesn't match the required format */ validateEtagValue(value) { if (value == null) throw new Error('Invalid etag value. Value can not be null or undefined'); if (typeof value !== 'string') throw new Error('Invalid etag value. Etag must be type of String'); if (!ETAG_VALUE_REG_EXP.test(value)) throw new Error('Invalid etag value.'); return value; } } module.exports = ValueValidator;