http://git-wip-us.apache.org/repos/asf/ignite/blob/c56d16fb/modules/platforms/nodejs/lib/internal/BinaryUtils.js ---------------------------------------------------------------------- diff --git a/modules/platforms/nodejs/lib/internal/BinaryUtils.js b/modules/platforms/nodejs/lib/internal/BinaryUtils.js new file mode 100644 index 0000000..ab2d40c --- /dev/null +++ b/modules/platforms/nodejs/lib/internal/BinaryUtils.js @@ -0,0 +1,598 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const Decimal = require('decimal.js'); +const ObjectType = require('../ObjectType').ObjectType; +const CompositeType = require('../ObjectType').CompositeType; +const MapObjectType = require('../ObjectType').MapObjectType; +const CollectionObjectType = require('../ObjectType').CollectionObjectType; +const ComplexObjectType = require('../ObjectType').ComplexObjectType; +const ObjectArrayType = require('../ObjectType').ObjectArrayType; +const Timestamp = require('../Timestamp'); +const EnumItem = require('../EnumItem'); +const Errors = require('../Errors'); +const ArgumentChecker = require('./ArgumentChecker'); + +// Operation codes +const OPERATION = Object.freeze({ + // Key-Value Queries + CACHE_GET : 1000, + CACHE_PUT : 1001, + CACHE_PUT_IF_ABSENT : 1002, + CACHE_GET_ALL : 1003, + CACHE_PUT_ALL : 1004, + CACHE_GET_AND_PUT : 1005, + CACHE_GET_AND_REPLACE : 1006, + CACHE_GET_AND_REMOVE : 1007, + CACHE_GET_AND_PUT_IF_ABSENT : 1008, + CACHE_REPLACE : 1009, + CACHE_REPLACE_IF_EQUALS : 1010, + CACHE_CONTAINS_KEY : 1011, + CACHE_CONTAINS_KEYS : 1012, + CACHE_CLEAR : 1013, + CACHE_CLEAR_KEY : 1014, + CACHE_CLEAR_KEYS : 1015, + CACHE_REMOVE_KEY : 1016, + CACHE_REMOVE_IF_EQUALS : 1017, + CACHE_REMOVE_KEYS : 1018, + CACHE_REMOVE_ALL : 1019, + CACHE_GET_SIZE : 1020, + // Cache Configuration + CACHE_GET_NAMES : 1050, + CACHE_CREATE_WITH_NAME : 1051, + CACHE_GET_OR_CREATE_WITH_NAME : 1052, + CACHE_CREATE_WITH_CONFIGURATION : 1053, + CACHE_GET_OR_CREATE_WITH_CONFIGURATION : 1054, + CACHE_GET_CONFIGURATION : 1055, + CACHE_DESTROY : 1056, + // SQL and Scan Queries + QUERY_SCAN : 2000, + QUERY_SCAN_CURSOR_GET_PAGE : 2001, + QUERY_SQL : 2002, + QUERY_SQL_CURSOR_GET_PAGE : 2003, + QUERY_SQL_FIELDS : 2004, + QUERY_SQL_FIELDS_CURSOR_GET_PAGE : 2005, + RESOURCE_CLOSE : 0, + // Binary Types + GET_BINARY_TYPE : 3002, + PUT_BINARY_TYPE : 3003 +}); + +const TYPE_CODE = Object.assign({ + BINARY_OBJECT : 27, + BINARY_ENUM : 38 + }, + ObjectType.PRIMITIVE_TYPE, + ObjectType.COMPOSITE_TYPE); + + +const TYPE_INFO = Object.freeze({ + [TYPE_CODE.BYTE] : { + NAME : 'byte', + SIZE : 1 + }, + [TYPE_CODE.SHORT] : { + NAME : 'short', + SIZE : 2 + }, + [TYPE_CODE.INTEGER] : { + NAME : 'integer', + SIZE : 4 + }, + [TYPE_CODE.LONG] : { + NAME : 'long', + SIZE : 8 + }, + [TYPE_CODE.FLOAT] : { + NAME : 'float', + SIZE : 4 + }, + [TYPE_CODE.DOUBLE] : { + NAME : 'double', + SIZE : 8 + }, + [TYPE_CODE.CHAR] : { + NAME : 'char', + SIZE : 2 + }, + [TYPE_CODE.BOOLEAN] : { + NAME : 'boolean', + SIZE : 1 + }, + [TYPE_CODE.STRING] : { + NAME : 'string', + NULLABLE : true + }, + [TYPE_CODE.UUID] : { + NAME : 'UUID', + SIZE : 16, + NULLABLE : true + }, + [TYPE_CODE.DATE] : { + NAME : 'date', + SIZE : 8, + NULLABLE : true + }, + [TYPE_CODE.BYTE_ARRAY] : { + NAME : 'byte array', + ELEMENT_TYPE : TYPE_CODE.BYTE, + NULLABLE : true + }, + [TYPE_CODE.SHORT_ARRAY] : { + NAME : 'short array', + ELEMENT_TYPE : TYPE_CODE.SHORT, + NULLABLE : true + }, + [TYPE_CODE.INTEGER_ARRAY] : { + NAME : 'integer array', + ELEMENT_TYPE : TYPE_CODE.INTEGER, + NULLABLE : true + }, + [TYPE_CODE.LONG_ARRAY] : { + NAME : 'long array', + ELEMENT_TYPE : TYPE_CODE.LONG, + NULLABLE : true + }, + [TYPE_CODE.FLOAT_ARRAY] : { + NAME : 'float array', + ELEMENT_TYPE : TYPE_CODE.FLOAT, + NULLABLE : true + }, + [TYPE_CODE.DOUBLE_ARRAY] : { + NAME : 'double array', + ELEMENT_TYPE : TYPE_CODE.DOUBLE, + NULLABLE : true + }, + [TYPE_CODE.CHAR_ARRAY] : { + NAME : 'char array', + ELEMENT_TYPE : TYPE_CODE.CHAR, + NULLABLE : true + }, + [TYPE_CODE.BOOLEAN_ARRAY] : { + NAME : 'boolean array', + ELEMENT_TYPE : TYPE_CODE.BOOLEAN, + NULLABLE : true + }, + [TYPE_CODE.STRING_ARRAY] : { + NAME : 'string array', + ELEMENT_TYPE : TYPE_CODE.STRING, + KEEP_ELEMENT_TYPE : true, + NULLABLE : true + }, + [TYPE_CODE.DATE_ARRAY] : { + NAME : 'date array', + ELEMENT_TYPE : TYPE_CODE.DATE, + KEEP_ELEMENT_TYPE : true, + NULLABLE : true + }, + [TYPE_CODE.UUID_ARRAY] : { + NAME : 'UUID array', + ELEMENT_TYPE : TYPE_CODE.UUID, + KEEP_ELEMENT_TYPE : true, + NULLABLE : true + }, + [TYPE_CODE.OBJECT_ARRAY] : { + NAME : 'object array', + ELEMENT_TYPE : TYPE_CODE.COMPLEX_OBJECT, + KEEP_ELEMENT_TYPE : true, + NULLABLE : true + }, + [TYPE_CODE.COLLECTION] : { + NAME : 'collection', + NULLABLE : true + }, + [TYPE_CODE.MAP] : { + NAME : 'map', + NULLABLE : true + }, + [TYPE_CODE.BINARY_OBJECT] : { + NAME : 'BinaryObject', + NULLABLE : true + }, + [TYPE_CODE.ENUM] : { + NAME : 'enum', + NULLABLE : true + }, + [TYPE_CODE.ENUM_ARRAY] : { + NAME : 'enum array', + ELEMENT_TYPE : TYPE_CODE.ENUM, + KEEP_ELEMENT_TYPE : true, + NULLABLE : true + }, + [TYPE_CODE.DECIMAL] : { + NAME : 'decimal', + NULLABLE : true + }, + [TYPE_CODE.DECIMAL_ARRAY] : { + NAME : 'decimal array', + ELEMENT_TYPE : TYPE_CODE.DECIMAL, + KEEP_ELEMENT_TYPE : true, + NULLABLE : true + }, + [TYPE_CODE.TIMESTAMP] : { + NAME : 'timestamp', + NULLABLE : true + }, + [TYPE_CODE.TIMESTAMP_ARRAY] : { + NAME : 'timestamp array', + ELEMENT_TYPE : TYPE_CODE.TIMESTAMP, + KEEP_ELEMENT_TYPE : true, + NULLABLE : true + }, + [TYPE_CODE.TIME] : { + NAME : 'time', + NULLABLE : true + }, + [TYPE_CODE.TIME_ARRAY] : { + NAME : 'time array', + ELEMENT_TYPE : TYPE_CODE.TIME, + KEEP_ELEMENT_TYPE : true, + NULLABLE : true + }, + [TYPE_CODE.NULL] : { + NAME : 'null', + NULLABLE : true + }, + [TYPE_CODE.COMPLEX_OBJECT] : { + NAME : 'complex object', + NULLABLE : true + } +}); + +const UTF8_ENCODING = 'utf8'; + +class BinaryUtils { + static get OPERATION() { + return OPERATION; + } + + static get TYPE_CODE() { + return TYPE_CODE; + } + + static get TYPE_INFO() { + return TYPE_INFO; + } + + static getSize(typeCode) { + const size = TYPE_INFO[typeCode].SIZE; + return size ? size : 0; + } + + static get ENCODING() { + return UTF8_ENCODING; + } + + static getTypeName(type) { + if (typeof type === 'string') { + return type; + } + const typeCode = BinaryUtils.getTypeCode(type); + return TYPE_INFO[typeCode] ? TYPE_INFO[typeCode].NAME : 'type code ' + typeCode; + } + + static isNullable(type) { + return TYPE_INFO[BinaryUtils.getTypeCode(type)].NULLABLE === true; + } + + static getTypeCode(type) { + return type instanceof CompositeType ? type._typeCode : type; + } + + static checkObjectType(type, argName) { + if (type === null || type instanceof CompositeType) { + return; + } + ArgumentChecker.hasValueFrom(type, argName, false, ObjectType.PRIMITIVE_TYPE); + } + + static calcObjectType(object) { + const BinaryObject = require('../BinaryObject'); + const objectType = typeof object; + if (object === null) { + return BinaryUtils.TYPE_CODE.NULL; + } + else if (objectType === 'number') { + return BinaryUtils.TYPE_CODE.DOUBLE; + } + else if (objectType === 'string') { + return BinaryUtils.TYPE_CODE.STRING; + } + else if (objectType === 'boolean') { + return BinaryUtils.TYPE_CODE.BOOLEAN; + } + else if (object instanceof Timestamp) { + return BinaryUtils.TYPE_CODE.TIMESTAMP; + } + else if (object instanceof Date) { + return BinaryUtils.TYPE_CODE.DATE; + } + else if (object instanceof EnumItem) { + return BinaryUtils.TYPE_CODE.ENUM; + } + else if (object instanceof Decimal) { + return BinaryUtils.TYPE_CODE.DECIMAL; + } + else if (object instanceof Array) { + if (object.length > 0 && object[0] !== null) { + return BinaryUtils.getArrayType(BinaryUtils.calcObjectType(object[0])); + } + } + else if (object instanceof Set) { + return new CollectionObjectType(CollectionObjectType.COLLECTION_SUBTYPE.HASH_SET); + } + else if (object instanceof Map) { + return new MapObjectType(); + } + else if (object instanceof BinaryObject) { + return BinaryUtils.TYPE_CODE.BINARY_OBJECT; + } + else if (objectType === 'object') { + return new ComplexObjectType(object); + } + throw Errors.IgniteClientError.unsupportedTypeError(objectType); + } + + static checkCompatibility(value, type) { + if (!type) { + return; + } + const typeCode = BinaryUtils.getTypeCode(type); + if (value === null) { + if (!BinaryUtils.isNullable(typeCode)) { + throw Errors.IgniteClientError.typeCastError(BinaryUtils.TYPE_CODE.NULL, typeCode); + } + return; + } + else if (BinaryUtils.isStandardType(typeCode)) { + BinaryUtils.checkStandardTypeCompatibility(value, typeCode, type); + return; + } + const valueTypeCode = BinaryUtils.getTypeCode(BinaryUtils.calcObjectType(value)); + if (typeCode !== valueTypeCode) { + throw Errors.IgniteClientError.typeCastError(valueTypeCode, typeCode); + } + } + + static isStandardType(typeCode) { + return typeCode !== BinaryUtils.TYPE_CODE.BINARY_OBJECT && + typeCode !== BinaryUtils.TYPE_CODE.COMPLEX_OBJECT; + } + + static checkStandardTypeCompatibility(value, typeCode, type = null) { + const valueType = typeof value; + switch (typeCode) { + case BinaryUtils.TYPE_CODE.BYTE: + case BinaryUtils.TYPE_CODE.SHORT: + case BinaryUtils.TYPE_CODE.INTEGER: + case BinaryUtils.TYPE_CODE.LONG: + if (!Number.isInteger(value)) { + throw Errors.IgniteClientError.valueCastError(value, typeCode); + } + return; + case BinaryUtils.TYPE_CODE.FLOAT: + case BinaryUtils.TYPE_CODE.DOUBLE: + if (valueType !== 'number') { + throw Errors.IgniteClientError.valueCastError(value, typeCode); + } + return; + case BinaryUtils.TYPE_CODE.CHAR: + if (valueType !== 'string' || value.length !== 1) { + throw Errors.IgniteClientError.valueCastError(value, typeCode); + } + return; + case BinaryUtils.TYPE_CODE.BOOLEAN: + if (valueType !== 'boolean') { + throw Errors.IgniteClientError.valueCastError(value, typeCode); + } + return; + case BinaryUtils.TYPE_CODE.STRING: + if (valueType !== 'string') { + throw Errors.IgniteClientError.valueCastError(value, typeCode); + } + return; + case BinaryUtils.TYPE_CODE.UUID: + if (!value instanceof Array || + value.length !== BinaryUtils.getSize(BinaryUtils.TYPE_CODE.UUID)) { + throw Errors.IgniteClientError.valueCastError(value, typeCode); + } + value.forEach(element => + BinaryUtils.checkStandardTypeCompatibility(element, BinaryUtils.TYPE_CODE.BYTE)); + return; + case BinaryUtils.TYPE_CODE.DATE: + if (!value instanceof Date) { + throw Errors.IgniteClientError.valueCastError(value, typeCode); + } + return; + case BinaryUtils.TYPE_CODE.ENUM: + if (!value instanceof EnumItem) { + throw Errors.IgniteClientError.valueCastError(value, typeCode); + } + return; + case BinaryUtils.TYPE_CODE.DECIMAL: + if (!value instanceof Decimal) { + throw Errors.IgniteClientError.valueCastError(value, typeCode); + } + return; + case BinaryUtils.TYPE_CODE.TIMESTAMP: + if (!value instanceof Timestamp) { + throw Errors.IgniteClientError.valueCastError(value, typeCode); + } + return; + case BinaryUtils.TYPE_CODE.TIME: + if (!value instanceof Date) { + throw Errors.IgniteClientError.valueCastError(value, typeCode); + } + return; + case BinaryUtils.TYPE_CODE.BYTE_ARRAY: + case BinaryUtils.TYPE_CODE.SHORT_ARRAY: + case BinaryUtils.TYPE_CODE.INTEGER_ARRAY: + case BinaryUtils.TYPE_CODE.LONG_ARRAY: + case BinaryUtils.TYPE_CODE.FLOAT_ARRAY: + case BinaryUtils.TYPE_CODE.DOUBLE_ARRAY: + case BinaryUtils.TYPE_CODE.CHAR_ARRAY: + case BinaryUtils.TYPE_CODE.BOOLEAN_ARRAY: + case BinaryUtils.TYPE_CODE.STRING_ARRAY: + case BinaryUtils.TYPE_CODE.UUID_ARRAY: + case BinaryUtils.TYPE_CODE.DATE_ARRAY: + case BinaryUtils.TYPE_CODE.OBJECT_ARRAY: + case BinaryUtils.TYPE_CODE.ENUM_ARRAY: + case BinaryUtils.TYPE_CODE.DECIMAL_ARRAY: + case BinaryUtils.TYPE_CODE.TIMESTAMP_ARRAY: + case BinaryUtils.TYPE_CODE.TIME_ARRAY: + if (!value instanceof Array) { + throw Errors.IgniteClientError.typeCastError(valueType, typeCode); + } + return; + case BinaryUtils.TYPE_CODE.MAP: + if (!value instanceof Map) { + throw Errors.IgniteClientError.typeCastError(valueType, typeCode); + } + return; + case BinaryUtils.TYPE_CODE.COLLECTION: + if (!(type && type._isSet() && value instanceof Set || value instanceof Array)) { + throw Errors.IgniteClientError.typeCastError(valueType, type && type._isSet() ? 'set' : typeCode); + } + return; + case BinaryUtils.TYPE_CODE.NULL: + if (value !== null) { + throw Errors.IgniteClientError.typeCastError('not null', typeCode); + } + return; + default: + const valueTypeCode = BinaryUtils.getTypeCode(BinaryUtils.calcObjectType(value)); + if (valueTypeCode === BinaryUtils.TYPE_CODE.BINARY_OBJECT) { + throw Errors.IgniteClientError.typeCastError(valueTypeCode, typeCode); + } + return; + } + } + + static checkTypesComatibility(expectedType, actualTypeCode) { + if (expectedType === null) { + return; + } + const expectedTypeCode = BinaryUtils.getTypeCode(expectedType); + if (actualTypeCode === BinaryUtils.TYPE_CODE.NULL) { + return; + } + else if (expectedTypeCode === BinaryUtils.TYPE_CODE.BINARY_OBJECT || + actualTypeCode === BinaryUtils.TYPE_CODE.BINARY_OBJECT && + expectedTypeCode === BinaryUtils.TYPE_CODE.COMPLEX_OBJECT) { + return; + } + else if (actualTypeCode !== expectedTypeCode) { + throw Errors.IgniteClientError.typeCastError(actualTypeCode, expectedTypeCode); + } + } + + static getArrayElementType(arrayType) { + if (arrayType instanceof ObjectArrayType) { + return arrayType._elementType; + } + else if (arrayType === BinaryUtils.TYPE_CODE.OBJECT_ARRAY) { + return null; + } + const elementTypeCode = TYPE_INFO[arrayType].ELEMENT_TYPE; + if (!elementTypeCode) { + throw Errors.IgniteClientError.internalError(); + } + return elementTypeCode; + } + + static getArrayType(elementType) { + switch (BinaryUtils.getTypeCode(elementType)) { + case BinaryUtils.TYPE_CODE.BYTE: + return BinaryUtils.TYPE_CODE.BYTE_ARRAY; + case BinaryUtils.TYPE_CODE.SHORT: + return BinaryUtils.TYPE_CODE.SHORT_ARRAY; + case BinaryUtils.TYPE_CODE.INTEGER: + return BinaryUtils.TYPE_CODE.INTEGER_ARRAY; + case BinaryUtils.TYPE_CODE.LONG: + return BinaryUtils.TYPE_CODE.LONG_ARRAY; + case BinaryUtils.TYPE_CODE.FLOAT: + return BinaryUtils.TYPE_CODE.FLOAT_ARRAY; + case BinaryUtils.TYPE_CODE.DOUBLE: + return BinaryUtils.TYPE_CODE.DOUBLE_ARRAY; + case BinaryUtils.TYPE_CODE.CHAR: + return BinaryUtils.TYPE_CODE.CHAR_ARRAY; + case BinaryUtils.TYPE_CODE.BOOLEAN: + return BinaryUtils.TYPE_CODE.BOOLEAN_ARRAY; + case BinaryUtils.TYPE_CODE.STRING: + return BinaryUtils.TYPE_CODE.STRING_ARRAY; + case BinaryUtils.TYPE_CODE.UUID: + return BinaryUtils.TYPE_CODE.UUID_ARRAY; + case BinaryUtils.TYPE_CODE.DATE: + return BinaryUtils.TYPE_CODE.DATE_ARRAY; + case BinaryUtils.TYPE_CODE.ENUM: + return BinaryUtils.TYPE_CODE.ENUM_ARRAY; + case BinaryUtils.TYPE_CODE.DECIMAL: + return BinaryUtils.TYPE_CODE.DECIMAL_ARRAY; + case BinaryUtils.TYPE_CODE.TIMESTAMP: + return BinaryUtils.TYPE_CODE.TIMESTAMP_ARRAY; + case BinaryUtils.TYPE_CODE.TIME: + return BinaryUtils.TYPE_CODE.TIME_ARRAY; + case BinaryUtils.TYPE_CODE.BINARY_OBJECT: + return new ObjectArrayType(); + default: + return new ObjectArrayType(elementType); + } + } + + static keepArrayElementType(arrayTypeCode) { + return TYPE_INFO[arrayTypeCode].KEEP_ELEMENT_TYPE === true; + } + + static getJsObjectFieldNames(jsObject) { + var fields = new Array(); + for (let field in jsObject) { + if (typeof jsObject[field] !== 'function') { + fields.push(field); + } + } + return fields; + } + + static hashCode(str) { + let hash = 0, char; + if (str && str.length > 0) { + for (let i = 0; i < str.length; i++) { + char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash |= 0; // Convert to 32bit integer + } + } + return hash; + } + + static hashCodeLowerCase(str) { + return BinaryUtils.hashCode(str ? str.toLowerCase() : str); + } + + static contentHashCode(buffer, startPos, endPos) { + let hash = 1; + for (let i = startPos; i <= endPos; i++) { + hash = 31 * hash + buffer._buffer[i]; + hash |= 0; // Convert to 32bit integer + } + return hash; + } +} + +module.exports = BinaryUtils;
http://git-wip-us.apache.org/repos/asf/ignite/blob/c56d16fb/modules/platforms/nodejs/lib/internal/BinaryWriter.js ---------------------------------------------------------------------- diff --git a/modules/platforms/nodejs/lib/internal/BinaryWriter.js b/modules/platforms/nodejs/lib/internal/BinaryWriter.js new file mode 100644 index 0000000..3686bb4 --- /dev/null +++ b/modules/platforms/nodejs/lib/internal/BinaryWriter.js @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const Decimal = require('decimal.js'); +const Errors = require('../Errors'); +const ComplexObjectType = require('../ObjectType').ComplexObjectType; +const BinaryUtils = require('./BinaryUtils'); + +class BinaryWriter { + + static async writeString(buffer, value) { + await BinaryWriter.writeObject(buffer, value, BinaryUtils.TYPE_CODE.STRING); + } + + static async writeObject(buffer, object, objectType = null, writeObjectType = true) { + BinaryUtils.checkCompatibility(object, objectType); + if (object === null) { + buffer.writeByte(BinaryUtils.TYPE_CODE.NULL); + return; + } + + objectType = objectType ? objectType : BinaryUtils.calcObjectType(object); + const objectTypeCode = BinaryUtils.getTypeCode(objectType); + + if (writeObjectType) { + buffer.writeByte(objectTypeCode); + } + switch (objectTypeCode) { + case BinaryUtils.TYPE_CODE.BYTE: + case BinaryUtils.TYPE_CODE.SHORT: + case BinaryUtils.TYPE_CODE.INTEGER: + case BinaryUtils.TYPE_CODE.FLOAT: + case BinaryUtils.TYPE_CODE.DOUBLE: + buffer.writeNumber(object, objectTypeCode); + break; + case BinaryUtils.TYPE_CODE.LONG: + buffer.writeLong(object); + break; + case BinaryUtils.TYPE_CODE.CHAR: + buffer.writeChar(object); + break; + case BinaryUtils.TYPE_CODE.BOOLEAN: + buffer.writeBoolean(object); + break; + case BinaryUtils.TYPE_CODE.STRING: + buffer.writeString(object); + break; + case BinaryUtils.TYPE_CODE.UUID: + BinaryWriter._writeUUID(buffer, object); + break; + case BinaryUtils.TYPE_CODE.DATE: + buffer.writeDate(object); + break; + case BinaryUtils.TYPE_CODE.ENUM: + await BinaryWriter._writeEnum(buffer, object); + break; + case BinaryUtils.TYPE_CODE.DECIMAL: + BinaryWriter._writeDecimal(buffer, object); + break; + case BinaryUtils.TYPE_CODE.TIMESTAMP: + BinaryWriter._writeTimestamp(buffer, object); + break; + case BinaryUtils.TYPE_CODE.TIME: + BinaryWriter._writeTime(buffer, object); + break; + case BinaryUtils.TYPE_CODE.BYTE_ARRAY: + case BinaryUtils.TYPE_CODE.SHORT_ARRAY: + case BinaryUtils.TYPE_CODE.INTEGER_ARRAY: + case BinaryUtils.TYPE_CODE.LONG_ARRAY: + case BinaryUtils.TYPE_CODE.FLOAT_ARRAY: + case BinaryUtils.TYPE_CODE.DOUBLE_ARRAY: + case BinaryUtils.TYPE_CODE.CHAR_ARRAY: + case BinaryUtils.TYPE_CODE.BOOLEAN_ARRAY: + case BinaryUtils.TYPE_CODE.STRING_ARRAY: + case BinaryUtils.TYPE_CODE.UUID_ARRAY: + case BinaryUtils.TYPE_CODE.DATE_ARRAY: + case BinaryUtils.TYPE_CODE.OBJECT_ARRAY: + case BinaryUtils.TYPE_CODE.ENUM_ARRAY: + case BinaryUtils.TYPE_CODE.DECIMAL_ARRAY: + case BinaryUtils.TYPE_CODE.TIMESTAMP_ARRAY: + case BinaryUtils.TYPE_CODE.TIME_ARRAY: + await BinaryWriter._writeArray(buffer, object, objectType, objectTypeCode); + break; + case BinaryUtils.TYPE_CODE.COLLECTION: + await BinaryWriter._writeCollection(buffer, object, objectType); + break; + case BinaryUtils.TYPE_CODE.MAP: + await BinaryWriter._writeMap(buffer, object, objectType); + break; + case BinaryUtils.TYPE_CODE.BINARY_OBJECT: + await BinaryWriter._writeBinaryObject(buffer, object, objectType); + break; + case BinaryUtils.TYPE_CODE.COMPLEX_OBJECT: + await BinaryWriter._writeComplexObject(buffer, object, objectType); + break; + default: + throw Errors.IgniteClientError.unsupportedTypeError(objectType); + } + } + + static _writeUUID(buffer, value) { + buffer.writeBuffer(Buffer.from(value)); + } + + static async _writeEnum(buffer, enumValue) { + await enumValue._write(buffer); + } + + static _writeDecimal(buffer, decimal) { + let strValue = decimal.toExponential(); + let expIndex = strValue.indexOf('e'); + if (expIndex < 0) { + expIndex = strValue.indexOf('E'); + } + let scale = 0; + if (expIndex >= 0) { + scale = parseInt(strValue.substring(expIndex + 1)); + strValue = strValue.substring(0, expIndex); + } + const isNegative = strValue.startsWith('-'); + if (isNegative) { + strValue = strValue.substring(1); + } + const dotIndex = strValue.indexOf('.'); + if (dotIndex >= 0) { + scale -= strValue.length - dotIndex - 1; + strValue = strValue.substring(0, dotIndex) + strValue.substring(dotIndex + 1); + } + scale = -scale; + let hexValue = new Decimal(strValue).toHexadecimal().substring(2); + hexValue = ((hexValue.length % 2 !== 0) ? '000' : '00') + hexValue; + const valueBuffer = Buffer.from(hexValue, 'hex'); + if (isNegative) { + valueBuffer[0] |= 0x80; + } + buffer.writeInteger(scale); + buffer.writeInteger(valueBuffer.length); + buffer.writeBuffer(valueBuffer); + } + + static _writeTimestamp(buffer, timestamp) { + buffer.writeDate(timestamp); + buffer.writeInteger(timestamp.getNanos()); + } + + static _writeTime(buffer, time) { + const midnight = new Date(time); + midnight.setHours(0, 0, 0, 0); + buffer.writeLong(time.getTime() - midnight.getTime()); + } + + static async _writeArray(buffer, array, arrayType, arrayTypeCode) { + const BinaryType = require('./BinaryType'); + const elementType = BinaryUtils.getArrayElementType(arrayType); + const keepElementType = BinaryUtils.keepArrayElementType(arrayTypeCode); + if (arrayTypeCode === BinaryUtils.TYPE_CODE.OBJECT_ARRAY) { + buffer.writeInteger(elementType instanceof ComplexObjectType ? + BinaryType._calculateId(elementType._typeName) : -1); + } + buffer.writeInteger(array.length); + for (let elem of array) { + await BinaryWriter.writeObject(buffer, elem, elementType, keepElementType); + } + } + + static async _writeCollection(buffer, collection, collectionType) { + buffer.writeInteger(collection instanceof Set ? collection.size : collection.length); + buffer.writeByte(collectionType._subType); + for (let element of collection) { + await BinaryWriter.writeObject(buffer, element, collectionType._elementType); + } + } + + static async _writeMap(buffer, map, mapType) { + buffer.writeInteger(map.size); + buffer.writeByte(mapType._subType); + for (let [key, value] of map.entries()) { + await BinaryWriter.writeObject(buffer, key, mapType._keyType); + await BinaryWriter.writeObject(buffer, value, mapType._valueType); + } + } + + static async _writeBinaryObject(buffer, binaryObject) { + buffer.position = buffer.position - 1; + await binaryObject._write(buffer); + } + + static async _writeComplexObject(buffer, object, objectType) { + const BinaryObject = require('../BinaryObject'); + await BinaryWriter._writeBinaryObject(buffer, await BinaryObject.fromObject(object, objectType)); + } +} + +module.exports = BinaryWriter; http://git-wip-us.apache.org/repos/asf/ignite/blob/c56d16fb/modules/platforms/nodejs/lib/internal/ClientFailoverSocket.js ---------------------------------------------------------------------- diff --git a/modules/platforms/nodejs/lib/internal/ClientFailoverSocket.js b/modules/platforms/nodejs/lib/internal/ClientFailoverSocket.js new file mode 100644 index 0000000..3ef9c76 --- /dev/null +++ b/modules/platforms/nodejs/lib/internal/ClientFailoverSocket.js @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const Util = require('util'); +const Errors = require('../Errors'); +const IgniteClient = require('../IgniteClient'); +const ClientSocket = require('./ClientSocket'); +const Logger = require('./Logger'); + +/** Socket wrapper with failover functionality: reconnects on failure. */ +class ClientFailoverSocket { + + constructor(onStateChanged) { + this._socket = null; + this._state = IgniteClient.STATE.DISCONNECTED; + this._onStateChanged = onStateChanged; + } + + async connect(config) { + if (this._state !== IgniteClient.STATE.DISCONNECTED) { + throw new Errors.IllegalStateError(); + } + this._config = config; + this._endpointsNumber = this._config._endpoints.length; + this._endpointIndex = this._getRandomInt(this._endpointsNumber - 1); + await this._connect(); + } + + async send(opCode, payloadWriter, payloadReader = null) { + if (this._state !== IgniteClient.STATE.CONNECTED) { + throw new Errors.IllegalStateError(); + } + await this._socket.sendRequest(opCode, payloadWriter, payloadReader); + } + + disconnect() { + if (this._state !== IgniteClient.STATE.DISCONNECTED) { + this._changeState(IgniteClient.STATE.DISCONNECTED); + if (this._socket) { + this._socket.disconnect(); + this._socket = null; + } + } + } + + async _onSocketDisconnect(error = null) { + this._changeState(IgniteClient.STATE.CONNECTING, null, error); + this._socket = null; + this._endpointIndex++; + try { + await this._connect(); + } + catch (err) { + } + } + + async _connect() { + const errors = new Array(); + let index, endpoint; + for (let i = 0; i < this._endpointsNumber; i++) { + index = (this._endpointIndex + i) % this._endpointsNumber; + endpoint = this._config._endpoints[index]; + try { + this._changeState(IgniteClient.STATE.CONNECTING, endpoint); + this._socket = new ClientSocket( + endpoint, this._config, this._onSocketDisconnect.bind(this)); + await this._socket.connect(); + this._changeState(IgniteClient.STATE.CONNECTED, endpoint); + return; + } + catch (err) { + errors.push(Util.format('[%s] %s', endpoint, err.message)); + } + } + const error = errors.join('; '); + this._changeState(IgniteClient.STATE.DISCONNECTED, endpoint, error); + this._socket = null; + throw new Errors.IgniteClientError(error); + } + + _changeState(state, endpoint = null, reason = null) { + if (Logger.debug) { + Logger.logDebug(Util.format('Socket %s: %s -> %s'), + endpoint ? endpoint : this._socket ? this._socket._endpoint : '', + this._getState(this._state), + this._getState(state)); + } + if (this._state !== state) { + this._state = state; + if (this._onStateChanged) { + this._onStateChanged(state, reason); + } + } + } + + _getState(state) { + switch (state) { + case IgniteClient.STATE.DISCONNECTED: + return 'DISCONNECTED'; + case IgniteClient.STATE.CONNECTING: + return 'CONNECTING'; + case IgniteClient.STATE.CONNECTED: + return 'CONNECTED'; + default: + return 'UNKNOWN'; + } + } + + // returns a random integer between 0 and max + _getRandomInt(max) { + if (max === 0) { + return 0; + } + return Math.floor(Math.random() * (max + 1)); + } +} + +module.exports = ClientFailoverSocket; http://git-wip-us.apache.org/repos/asf/ignite/blob/c56d16fb/modules/platforms/nodejs/lib/internal/ClientSocket.js ---------------------------------------------------------------------- diff --git a/modules/platforms/nodejs/lib/internal/ClientSocket.js b/modules/platforms/nodejs/lib/internal/ClientSocket.js new file mode 100644 index 0000000..1f12040 --- /dev/null +++ b/modules/platforms/nodejs/lib/internal/ClientSocket.js @@ -0,0 +1,434 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const net = require('net'); +const tls = require('tls'); +const URL = require('url'); +const Long = require('long'); +const Util = require('util'); +const Errors = require('../Errors'); +const IgniteClientConfiguration = require('../IgniteClientConfiguration'); +const MessageBuffer = require('./MessageBuffer'); +const BinaryUtils = require('./BinaryUtils'); +const BinaryReader = require('./BinaryReader'); +const BinaryWriter = require('./BinaryWriter'); +const ArgumentChecker = require('./ArgumentChecker'); +const Logger = require('./Logger'); + +const HANDSHAKE_SUCCESS_STATUS_CODE = 1; +const REQUEST_SUCCESS_STATUS_CODE = 0; +const PORT_DEFAULT = 10800; + +class ProtocolVersion { + + constructor(major = null, minor = null, patch = null) { + this._major = major; + this._minor = minor; + this._patch = patch; + } + + compareTo(other) { + let diff = this._major - other._major; + if (diff !== 0) { + return diff; + } + diff = this._minor - other._minor; + if (diff !== 0) { + return diff; + } + return this._patch - other._patch; + } + + equals(other) { + return this.compareTo(other) === 0; + } + + toString() { + return Util.format('%d.%d.%d', this._major, this._minor, this._patch); + } + + read(buffer) { + this._major = buffer.readShort(); + this._minor = buffer.readShort(); + this._patch = buffer.readShort(); + } + + write(buffer) { + buffer.writeShort(this._major); + buffer.writeShort(this._minor); + buffer.writeShort(this._patch); + } +} + +const PROTOCOL_VERSION_1_0_0 = new ProtocolVersion(1, 0, 0); +const PROTOCOL_VERSION_1_1_0 = new ProtocolVersion(1, 1, 0); + +const SUPPORTED_VERSIONS = [ + // PROTOCOL_VERSION_1_0_0, // Support for QueryField precision/scale fields breaks 1.0.0 compatibility + PROTOCOL_VERSION_1_1_0 +]; + +const STATE = Object.freeze({ + INITIAL : 0, + HANDSHAKE : 1, + CONNECTED : 2, + DISCONNECTED : 3 +}); + +class ClientSocket { + + constructor(endpoint, config, onSocketDisconnect) { + ArgumentChecker.notEmpty(endpoint, 'endpoints'); + this._endpoint = endpoint; + this._parseEndpoint(endpoint); + this._config = config; + this._state = STATE.INITIAL; + this._socket = null; + this._requestId = Long.ZERO; + this._handshakeRequestId = null; + this._protocolVersion = null; + this._requests = new Map(); + this._onSocketDisconnect = onSocketDisconnect; + this._error = null; + this._wasConnected = false; + } + + async connect() { + return new Promise((resolve, reject) => { + this._connectSocket( + this._getHandshake(PROTOCOL_VERSION_1_1_0, resolve, reject)); + }); + } + + disconnect() { + this._disconnect(true, false); + } + + get requestId() { + const id = this._requestId; + this._requestId = this._requestId.add(1); + return id; + } + + async sendRequest(opCode, payloadWriter, payloadReader = null) { + if (this._state === STATE.CONNECTED) { + return new Promise(async (resolve, reject) => { + const request = new Request(this.requestId, opCode, payloadWriter, payloadReader, resolve, reject); + this._addRequest(request); + await this._sendRequest(request); + }); + } + else { + throw new Errors.IllegalStateError(); + } + } + + _connectSocket(handshakeRequest) { + const onConnected = async () => { + this._state = STATE.HANDSHAKE; + // send handshake + await this._sendRequest(handshakeRequest); + }; + + const options = Object.assign({}, + this._config._options, + { host : this._host, port : this._port, version : this._version }); + if (this._config._useTLS) { + this._socket = tls.connect(options, onConnected); + } + else { + this._socket = net.createConnection(options, onConnected); + } + + this._socket.on('data', async (data) => { + try { + await this._processResponse(data); + } + catch (err) { + this._error = err.message; + this._disconnect(); + } + }); + this._socket.on('close', () => { + this._disconnect(false); + }); + this._socket.on('error', (error) => { + this._error = this._state === STATE.INITIAL ? + 'Connection failed: ' + error : error; + this._disconnect(); + }); + } + + _addRequest(request) { + this._requests.set(request.id.toString(), request); + } + + async _sendRequest(request) { + try { + const message = await request.getMessage(); + this._logMessage(request.id.toString(), true, message); + this._socket.write(message); + } + catch (err) { + this._requests.delete(request.id); + request.reject(err); + } + } + + async _processResponse(message) { + if (this._state === STATE.DISCONNECTED) { + return; + } + let offset = 0; + while (offset < message.length) { + let buffer = MessageBuffer.from(message, offset); + // Response length + const length = buffer.readInteger(); + offset += length + BinaryUtils.getSize(BinaryUtils.TYPE_CODE.INTEGER); + let requestId, isSuccess; + const isHandshake = this._state === STATE.HANDSHAKE; + + if (isHandshake) { + // Handshake status + isSuccess = (buffer.readByte() === HANDSHAKE_SUCCESS_STATUS_CODE) + requestId = this._handshakeRequestId.toString(); + } + else { + // Request id + requestId = buffer.readLong().toString(); + // Status code + isSuccess = (buffer.readInteger() === REQUEST_SUCCESS_STATUS_CODE); + } + + this._logMessage(requestId, false, buffer.data); + + if (this._requests.has(requestId)) { + const request = this._requests.get(requestId); + this._requests.delete(requestId); + if (isHandshake) { + await this._finalizeHandshake(buffer, request, isSuccess); + } + else { + await this._finalizeResponse(buffer, request, isSuccess); + } + } + else { + throw Errors.IgniteClientError.internalError('Invalid response id: ' + requestId); + } + } + } + + async _finalizeHandshake(buffer, request, isSuccess) { + if (!isSuccess) { + // Server protocol version + const serverVersion = new ProtocolVersion(); + serverVersion.read(buffer); + // Error message + const errMessage = await BinaryReader.readObject(buffer); + + if (!this._protocolVersion.equals(serverVersion)) { + if (!this._isSupportedVersion(serverVersion) || + serverVersion.compareTo(PROTOCOL_VERSION_1_1_0) < 0 && this._config._userName) { + request.reject(new Errors.OperationError( + Util.format('Protocol version mismatch: client %s / server %s. Server details: %s', + this._protocolVersion.toString(), serverVersion.toString(), errMessage))); + this._disconnect(); + } + else { + // retry handshake with server version + const handshakeRequest = this._getHandshake(serverVersion, request.resolve, request.reject); + await this._sendRequest(handshakeRequest); + } + } + else { + request.reject(new Errors.OperationError(errMessage)); + this._disconnect(); + } + } + else { + this._state = STATE.CONNECTED; + this._wasConnected = true; + request.resolve(); + } + } + + async _finalizeResponse(buffer, request, isSuccess) { + if (!isSuccess) { + // Error message + const errMessage = await BinaryReader.readObject(buffer); + request.reject(new Errors.OperationError(errMessage)); + } + else { + try { + if (request.payloadReader) { + await request.payloadReader(buffer); + } + request.resolve(); + } + catch (err) { + request.reject(err); + } + } + } + + async _handshakePayloadWriter(payload) { + // Handshake code + payload.writeByte(1); + // Protocol version + this._protocolVersion.write(payload); + // Client code + payload.writeByte(2); + if (this._config._userName) { + await BinaryWriter.writeString(payload, this._config._userName); + await BinaryWriter.writeString(payload, this._config._password); + } + } + + _getHandshake(version, resolve, reject) { + this._protocolVersion = version; + const handshakeRequest = new Request( + this.requestId, null, this._handshakePayloadWriter.bind(this), null, resolve, reject); + this._addRequest(handshakeRequest); + this._handshakeRequestId = handshakeRequest.id; + return handshakeRequest; + } + + _isSupportedVersion(protocolVersion) { + for (let version of SUPPORTED_VERSIONS) { + if (version.equals(protocolVersion)) { + return true; + } + } + return false; + } + + _disconnect(close = true, callOnDisconnect = true) { + this._state = STATE.DISCONNECTED; + this._requests.forEach((request, id) => { + request.reject(new Errors.LostConnectionError(this._error)); + this._requests.delete(id); + }); + if (this._wasConnected && callOnDisconnect && this._onSocketDisconnect) { + this._onSocketDisconnect(this._error); + } + if (close) { + this._onSocketDisconnect = null; + this._socket.end(); + } + } + + _parseEndpoint(endpoint) { + endpoint = endpoint.trim(); + this._host = endpoint; + this._port = null; + const colonCnt = endpoint.split(':').length - 1; + if (colonCnt > 1) { + // IPv6 address + this._version = 6; + const index = endpoint.lastIndexOf(']:'); + if (index >= 0) { + this._host = endpoint.substring(0, index + 1); + this._port = endpoint.substring(index + 2); + } + if (this._host.startsWith('[') || this._host.endsWith(']')) { + if (this._host.startsWith('[') && this._host.endsWith(']')) { + this._host = this._host.substring(1, this._host.length - 1); + } + else { + throw Errors.IgniteClientError.illegalArgumentError('Incorrect endpoint format: ' + endpoint); + } + } + } + else { + // IPv4 address + this._version = 4; + const index = endpoint.lastIndexOf(':'); + if (index >= 0) { + this._host = endpoint.substring(0, index); + this._port = endpoint.substring(index + 1); + } + } + if (!this._port) { + this._port = PORT_DEFAULT; + } + else { + this._port = parseInt(this._port); + if (isNaN(this._port)) { + throw Errors.IgniteClientError.illegalArgumentError('Incorrect endpoint format: ' + endpoint); + } + } + } + + _logMessage(requestId, isRequest, message) { + if (Logger.debug) { + Logger.logDebug((isRequest ? 'Request: ' : 'Response: ') + requestId); + Logger.logDebug('[' + [...message] + ']'); + } + } +} + +class Request { + constructor(id, opCode, payloadWriter, payloadReader, resolve, reject) { + this._id = id; + this._opCode = opCode; + this._payloadWriter = payloadWriter; + this._payloadReader = payloadReader; + this._resolve = resolve; + this._reject = reject; + } + + get id() { + return this._id; + } + + get payloadReader() { + return this._payloadReader; + } + + get resolve() { + return this._resolve; + } + + get reject() { + return this._reject; + } + + async getMessage() { + const message = new MessageBuffer(); + // Skip message length + const messageStartPos = BinaryUtils.getSize(BinaryUtils.TYPE_CODE.INTEGER); + message.position = messageStartPos; + if (this._opCode !== null) { + // Op code + message.writeShort(this._opCode); + // Request id + message.writeLong(this._id); + } + if (this._payloadWriter) { + // Payload + await this._payloadWriter(message); + } + // Message length + message.position = 0; + message.writeInteger(message.length - messageStartPos); + return message.data; + } +} + +module.exports = ClientSocket; \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/c56d16fb/modules/platforms/nodejs/lib/internal/Logger.js ---------------------------------------------------------------------- diff --git a/modules/platforms/nodejs/lib/internal/Logger.js b/modules/platforms/nodejs/lib/internal/Logger.js new file mode 100644 index 0000000..628c70e --- /dev/null +++ b/modules/platforms/nodejs/lib/internal/Logger.js @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +/** Utility class for logging errors and debug messages. */ +class Logger { + static get debug() { + return Logger._debug; + } + + static set debug(value) { + Logger._debug = value; + } + + static logDebug(data, ...args) { + if (Logger._debug) { + console.log(data, ...args); + } + } + + static logError(data, ...args) { + if (Logger._debug) { + console.log('ERROR: ' + data, ...args); + } + } +} + +Logger._debug = false; + +module.exports = Logger; http://git-wip-us.apache.org/repos/asf/ignite/blob/c56d16fb/modules/platforms/nodejs/lib/internal/MessageBuffer.js ---------------------------------------------------------------------- diff --git a/modules/platforms/nodejs/lib/internal/MessageBuffer.js b/modules/platforms/nodejs/lib/internal/MessageBuffer.js new file mode 100644 index 0000000..f1407bf --- /dev/null +++ b/modules/platforms/nodejs/lib/internal/MessageBuffer.js @@ -0,0 +1,272 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const Long = require('long'); +const BinaryUtils = require('./BinaryUtils'); +const Errors = require('../Errors'); + +const BUFFER_CAPACITY_DEFAULT = 256; +const BYTE_ZERO = 0; +const BYTE_ONE = 1; + +class MessageBuffer { + constructor(capacity = BUFFER_CAPACITY_DEFAULT) { + this._buffer = Buffer.allocUnsafe(capacity); + this._capacity = capacity; + this._length = 0; + this._position = 0; + } + + static from(source, position) { + const buf = new MessageBuffer(); + buf._buffer = Buffer.from(source); + buf._position = position; + buf._length = buf._buffer.length; + buf._capacity = buf._length; + return buf; + } + + get position() { + return this._position; + } + + set position(position) { + this._position = position; + } + + get length() { + return this._length; + } + + get data() { + return this.getSlice(0, this.length); + } + + get buffer() { + return this._buffer; + } + + getSlice(start, end) { + return this._buffer.slice(start, end); + } + + writeByte(value) { + this.writeNumber(value, BinaryUtils.TYPE_CODE.BYTE); + } + + writeShort(value) { + this.writeNumber(value, BinaryUtils.TYPE_CODE.SHORT); + } + + writeInteger(value) { + this.writeNumber(value, BinaryUtils.TYPE_CODE.INTEGER); + } + + writeLong(value) { + try { + if (!Long.isLong(value)) { + value = Long.fromValue(value); + } + } + catch (err) { + throw Errors.IgniteClientError.valueCastError(value, BinaryUtils.TYPE_CODE.LONG); + } + const buffer = Buffer.from(value.toBytesLE()); + this.writeBuffer(buffer); + } + + writeFloat(value) { + this.writeNumber(value, BinaryUtils.TYPE_CODE.FLOAT); + } + + writeDouble(value) { + this.writeNumber(value, BinaryUtils.TYPE_CODE.DOUBLE); + } + + writeNumber(value, type) { + const size = BinaryUtils.getSize(type); + this._ensureCapacity(size); + try { + switch (type) { + case BinaryUtils.TYPE_CODE.BYTE: + this._buffer.writeInt8(value, this._position); + break; + case BinaryUtils.TYPE_CODE.SHORT: + this._buffer.writeInt16LE(value, this._position); + break; + case BinaryUtils.TYPE_CODE.INTEGER: + this._buffer.writeInt32LE(value, this._position); + break; + case BinaryUtils.TYPE_CODE.FLOAT: + this._buffer.writeFloatLE(value, this._position); + break; + case BinaryUtils.TYPE_CODE.DOUBLE: + this._buffer.writeDoubleLE(value, this._position); + break; + default: + throw Errors.IgniteClientError.internalError(); + } + } + catch (err) { + throw Errors.IgniteClientError.valueCastError(value, type); + } + this._position += size; + } + + writeBoolean(value) { + this.writeByte(value ? BYTE_ONE : BYTE_ZERO); + } + + writeChar(value) { + this.writeShort(value.charCodeAt(0)); + } + + writeString(value) { + const buffer = Buffer.from(value, BinaryUtils.ENCODING); + const length = buffer.length; + this.writeInteger(length); + if (length > 0) { + this.writeBuffer(buffer); + } + } + + writeDate(value) { + this.writeLong(value.getTime()); + } + + readByte() { + return this.readNumber(BinaryUtils.TYPE_CODE.BYTE); + } + + readShort() { + return this.readNumber(BinaryUtils.TYPE_CODE.SHORT); + } + + readInteger() { + return this.readNumber(BinaryUtils.TYPE_CODE.INTEGER); + } + + readLong() { + const size = BinaryUtils.getSize(BinaryUtils.TYPE_CODE.LONG) + this._ensureSize(size); + const value = Long.fromBytesLE([...this._buffer.slice(this._position, this._position + size)]); + this._position += size; + return value; + } + + readFloat() { + return this.readNumber(BinaryUtils.TYPE_CODE.FLOAT); + } + + readDouble() { + return this.readNumber(BinaryUtils.TYPE_CODE.DOUBLE); + } + + readNumber(type) { + const size = BinaryUtils.getSize(type); + this._ensureSize(size); + let value; + switch (type) { + case BinaryUtils.TYPE_CODE.BYTE: + value = this._buffer.readInt8(this._position); + break; + case BinaryUtils.TYPE_CODE.SHORT: + value = this._buffer.readInt16LE(this._position); + break; + case BinaryUtils.TYPE_CODE.INTEGER: + value = this._buffer.readInt32LE(this._position); + break; + case BinaryUtils.TYPE_CODE.FLOAT: + value = this._buffer.readFloatLE(this._position); + break; + case BinaryUtils.TYPE_CODE.DOUBLE: + value = this._buffer.readDoubleLE(this._position); + break; + default: + throw Errors.IgniteClientError.internalError(); + } + this._position += size; + return value; + } + + readBoolean() { + return this.readByte() === BYTE_ONE; + } + + readChar() { + return String.fromCharCode(this.readShort()); + } + + readString() { + const bytesCount = this.readInteger(); + this._ensureSize(bytesCount); + const result = this._buffer.toString(BinaryUtils.ENCODING, this._position, this._position + bytesCount); + this._position += bytesCount; + return result; + } + + readBuffer(length) { + this._ensureSize(length); + const result = this._buffer.slice(this._position, this._position + length); + this._position += length; + return result; + } + + readDate() { + return new Date(this.readLong().toNumber()); + } + + writeBuffer(buffer, start = undefined, end = undefined) { + if (start === undefined) { + start = 0; + } + if (end === undefined) { + end = buffer.length; + } + const size = end - start; + this._ensureCapacity(size); + buffer.copy(this._buffer, this._position, start, end); + this._position += size; + } + + _ensureSize(size) { + if (this._position + size > this._length) { + throw Errors.IgniteClientError.internalError('Unexpected format of response'); + } + } + + _ensureCapacity(valueSize) { + if (valueSize <= 0) { + throw Errors.IgniteClientError.internalError(); + } + let newCapacity = this._capacity; + while (this._position + valueSize > newCapacity) { + newCapacity = newCapacity * 2; + } + if (this._capacity < newCapacity) { + this._buffer = Buffer.concat([this._buffer, Buffer.allocUnsafe(newCapacity - this._capacity)], newCapacity); + this._capacity = newCapacity; + } + if (this._position + valueSize > this._length) { + this._length = this._position + valueSize; + } + } +} + +module.exports = MessageBuffer; http://git-wip-us.apache.org/repos/asf/ignite/blob/c56d16fb/modules/platforms/nodejs/package-lock.json ---------------------------------------------------------------------- diff --git a/modules/platforms/nodejs/package-lock.json b/modules/platforms/nodejs/package-lock.json new file mode 100644 index 0000000..e62bdbb --- /dev/null +++ b/modules/platforms/nodejs/package-lock.json @@ -0,0 +1,168 @@ +{ + "name": "apache-ignite-client", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "add-matchers": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/add-matchers/-/add-matchers-0.5.0.tgz", + "integrity": "sha1-UCGQ5HUM1XIWGDkyaLYaFXNm52U=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "decimal.js": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.0.0.tgz", + "integrity": "sha512-oTJ6FdBXq28TSDB/e0CfuXWxZE1+Jj3vV9+6eL73d+YpryRqD/+R4O5kAmZjNHiL2zw9lXrnZR8NhJtualzufg==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "jasmine": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.1.0.tgz", + "integrity": "sha1-K9Wf1+xuwOistk4J9Fpo7SrRlSo=", + "dev": true, + "requires": { + "glob": "7.1.2", + "jasmine-core": "3.1.0" + } + }, + "jasmine-core": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.1.0.tgz", + "integrity": "sha1-pHheE11d9lAk38kiSVPfWFvSdmw=", + "dev": true + }, + "jasmine-expect": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/jasmine-expect/-/jasmine-expect-3.8.3.tgz", + "integrity": "sha512-VZu4V0O/YWBfUa2dgb9cz/WcQdWRFtdM4SV0Iy+abORLEkqP6vUrMvoFtxP9TJ+Rtor+z6857Vz5ySOaTFEt2A==", + "dev": true, + "requires": { + "add-matchers": "0.5.0" + } + }, + "jasmine-reporters": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jasmine-reporters/-/jasmine-reporters-2.3.1.tgz", + "integrity": "sha1-9C1XjplmlhY0MdkRwxZ5cZ+0Ozs=", + "dev": true, + "requires": { + "mkdirp": "0.5.1", + "xmldom": "0.1.27" + } + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xmldom": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", + "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=", + "dev": true + } + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/c56d16fb/modules/platforms/nodejs/package.json ---------------------------------------------------------------------- diff --git a/modules/platforms/nodejs/package.json b/modules/platforms/nodejs/package.json new file mode 100644 index 0000000..4ffb809 --- /dev/null +++ b/modules/platforms/nodejs/package.json @@ -0,0 +1,25 @@ +{ + "name": "apache-ignite-client", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + }, + "dependencies": { + "long": "latest", + "decimal.js": "latest" + }, + "scripts": { + "test": "jasmine", + "test:examples": "node ./spec/ExamplesExecutor.js Examples", + "test:auth_example": "node ./spec/ExamplesExecutor.js AuthExample" + }, + "devDependencies": { + "jasmine": "latest", + "jasmine-expect": "latest", + "jasmine-reporters": "latest" + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/c56d16fb/modules/platforms/nodejs/spec/ExamplesExecutor.js ---------------------------------------------------------------------- diff --git a/modules/platforms/nodejs/spec/ExamplesExecutor.js b/modules/platforms/nodejs/spec/ExamplesExecutor.js new file mode 100644 index 0000000..7f263d9 --- /dev/null +++ b/modules/platforms/nodejs/spec/ExamplesExecutor.js @@ -0,0 +1,11 @@ +const Jasmine = require('jasmine'); + +const jasmine = new Jasmine(); +jasmine.loadConfig({ + 'spec_dir': 'spec', + 'spec_files': [ + `examples/${process.argv[2]}.spec.js` + ], + "random": false +}); +jasmine.execute(); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ignite/blob/c56d16fb/modules/platforms/nodejs/spec/README.md ---------------------------------------------------------------------- diff --git a/modules/platforms/nodejs/spec/README.md b/modules/platforms/nodejs/spec/README.md new file mode 100644 index 0000000..4b947c4 --- /dev/null +++ b/modules/platforms/nodejs/spec/README.md @@ -0,0 +1,42 @@ +# Tests # + +NodeJS Client for Apache Ignite contains [Jasmine](https://www.npmjs.com/package/jasmine) tests to check the behavior of the client. the tests include: +- functional tests which cover all API methods of the client +- examples executors which run all examples except AuthTlsExample +- AuthTlsExample executor + +## Tests Installation ## + +(temporary, while the NPM module is not released on [npmjs](https://www.npmjs.com)) + +Tests are installed along with the client. +Follow the [instructions in the main readme](../README.md#installation). + +## Tests Running ## + +1. Run Apache Ignite server locally or remotely with default configuration. +2. Set the environment variable: + - **APACHE_IGNITE_CLIENT_ENDPOINTS** - comma separated list of Ignite node endpoints. + - **APACHE_IGNITE_CLIENT_DEBUG** - (optional) if *true*, tests will display additional output (default: *false*). +3. Alternatively, instead of the environment variables setting, you can directly specify the values of the corresponding variables in [local_ignite_path/modules/platforms/nodejspec/config.js](./config.js) file. +4. Run the tests: + +### Run Functional Tests ### + +Call `npm test` command from `local_ignite_path/modules/platforms/nodejs` folder. + +### Run Examples Executors ### + +Call `npm run test:examples` command from `local_ignite_path/modules/platforms/nodejs` folder. + +### Run AuthTlsExample Executor ### + +It requires running Apache Ignite server with non-default configuration (authentication and TLS switched on). + +If the server runs locally: +- setup the server to accept TLS. During the setup use `keystore.jks` and `truststore.jks` certificates from `local_ignite_path/modules/platforms/nodejs/examples/certs/` folder. Password for the files: `123456` +- switch on the authentication on the server. Use the default username/password. + +If the server runs remotely, and/or other certificates are required, and/or non-default username/password is required - see this [instruction](../examples/README.md#additional-setup-for-authtlsexample). + +Call `npm run test:auth_example` command from `local_ignite_path/modules/platforms/nodejs` folder. http://git-wip-us.apache.org/repos/asf/ignite/blob/c56d16fb/modules/platforms/nodejs/spec/TestingHelper.js ---------------------------------------------------------------------- diff --git a/modules/platforms/nodejs/spec/TestingHelper.js b/modules/platforms/nodejs/spec/TestingHelper.js new file mode 100644 index 0000000..79a5336 --- /dev/null +++ b/modules/platforms/nodejs/spec/TestingHelper.js @@ -0,0 +1,384 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +require('jasmine-expect'); +const JasmineReporters = require('jasmine-reporters'); + +const Util = require('util'); +const exec = require('child_process').exec; +const config = require('./config'); +const IgniteClient = require('apache-ignite-client'); +const IgniteClientConfiguration = IgniteClient.IgniteClientConfiguration; +const Errors = IgniteClient.Errors; +const EnumItem = IgniteClient.EnumItem; +const Timestamp = IgniteClient.Timestamp; +const Decimal = IgniteClient.Decimal; +const BinaryObject = IgniteClient.BinaryObject; +const ObjectType = IgniteClient.ObjectType; + +const TIMEOUT_MS = 60000; + +jasmine.getEnv().addReporter(new JasmineReporters.TeamCityReporter()); + +const dateComparator = (date1, date2) => { return !date1 && !date2 || date1.value === date2.value; }; +const floatComparator = (date1, date2) => { return Math.abs(date1 - date2) < 0.00001; }; +const defaultComparator = (value1, value2) => { return value1 === value2; }; +const enumComparator = (value1, value2) => { + return value1.getTypeId() === value2.getTypeId() && + value1.getOrdinal() === value2.getOrdinal(); }; +const decimalComparator = (value1, value2) => { + return value1 === null && value2 === null || + value1.equals(value2); +}; +const timestampComparator = (value1, value2) => { + return value1 === null && value2 === null || + dateComparator(value1.getTime(), value2.getTime()) && + value1.getNanos() === value2.getNanos(); }; + +const numericValueModificator = (data) => { return data > 0 ? data - 10 : data + 10; }; +const charValueModificator = (data) => { return String.fromCharCode(data.charCodeAt(0) + 5); }; +const booleanValueModificator = (data) => { return !data; }; +const stringValueModificator = (data) => { return data + 'xxx'; }; +const dateValueModificator = (data) => { return new Date(data.getTime() + 12345); }; +const UUIDValueModificator = (data) => { return data.reverse(); }; +const enumValueModificator = (data) => { return new EnumItem(data.getTypeId(), data.getOrdinal() + 1); }; +const decimalValueModificator = (data) => { return data.add(12345); }; +const timestampValueModificator = (data) => { return new Timestamp(new Date(data.getTime() + 12345), data.getNanos() + 123); }; + +const primitiveValues = { + [ObjectType.PRIMITIVE_TYPE.BYTE] : { + values : [-128, 0, 127], + isMapKey : true, + modificator : numericValueModificator + }, + [ObjectType.PRIMITIVE_TYPE.SHORT] : { + values : [-32768, 0, 32767], + isMapKey : true, + modificator : numericValueModificator + }, + [ObjectType.PRIMITIVE_TYPE.INTEGER] : { + values : [12345, 0, -54321], + isMapKey : true, + modificator : numericValueModificator + }, + [ObjectType.PRIMITIVE_TYPE.LONG] : { + values : [12345678912345, 0, -98765432112345], + isMapKey : true, + modificator : numericValueModificator + }, + [ObjectType.PRIMITIVE_TYPE.FLOAT] : { + values : [-1.155, 0, 123e-5], + isMapKey : false, + modificator : numericValueModificator + }, + [ObjectType.PRIMITIVE_TYPE.DOUBLE] : { + values : [-123e5, 0, 0.0001], + typeOptional : true, + isMapKey : false, + modificator : numericValueModificator + }, + [ObjectType.PRIMITIVE_TYPE.CHAR] : { + values : ['a', String.fromCharCode(0x1234)], + isMapKey : true, + modificator : charValueModificator + }, + [ObjectType.PRIMITIVE_TYPE.BOOLEAN] : { + values : [true, false], + isMapKey : true, + typeOptional : true, + modificator : booleanValueModificator + }, + [ObjectType.PRIMITIVE_TYPE.STRING] : { + values : ['abc', ''], + isMapKey : true, + typeOptional : true, + modificator : stringValueModificator + }, + [ObjectType.PRIMITIVE_TYPE.UUID] : { + values : [ + [ 18, 70, 2, 119, 154, 254, 198, 254, 195, 146, 33, 60, 116, 230, 0, 146 ], + [ 141, 77, 31, 194, 127, 36, 184, 255, 192, 4, 118, 57, 253, 209, 111, 147 ] + ], + isMapKey : false, + modificator : UUIDValueModificator + }, + [ObjectType.PRIMITIVE_TYPE.DATE] : { + values : [new Date(), new Date('1995-12-17T03:24:00'), new Date(0)], + typeOptional : true, + isMapKey : false, + modificator : dateValueModificator + }, + // [ObjectType.PRIMITIVE_TYPE.ENUM] : { + // values : [new EnumItem(12345, 7), new EnumItem(0, 0)], + // typeOptional : true, + // isMapKey : false, + // modificator : enumValueModificator + // }, + [ObjectType.PRIMITIVE_TYPE.DECIMAL] : { + values : [new Decimal('123456789.6789345'), new Decimal(0), new Decimal('-98765.4321e15')], + typeOptional : true, + isMapKey : false, + modificator : decimalValueModificator + }, + [ObjectType.PRIMITIVE_TYPE.TIMESTAMP] : { + values : [new Timestamp(new Date().getTime(), 12345), new Timestamp(new Date('1995-12-17T03:24:00').getTime(), 543), new Timestamp(0, 0)], + typeOptional : true, + isMapKey : false, + modificator : timestampValueModificator + }, + [ObjectType.PRIMITIVE_TYPE.TIME] : { + values : [new Date(), new Date('1995-12-17T03:24:00'), new Date(123)], + isMapKey : false, + modificator : dateValueModificator + } +}; + +const arrayValues = { + [ObjectType.PRIMITIVE_TYPE.BYTE_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.BYTE }, + [ObjectType.PRIMITIVE_TYPE.SHORT_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.SHORT }, + [ObjectType.PRIMITIVE_TYPE.INTEGER_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.INTEGER }, + [ObjectType.PRIMITIVE_TYPE.LONG_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.LONG }, + [ObjectType.PRIMITIVE_TYPE.FLOAT_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.FLOAT }, + [ObjectType.PRIMITIVE_TYPE.DOUBLE_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.DOUBLE, typeOptional : true }, + [ObjectType.PRIMITIVE_TYPE.CHAR_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.CHAR }, + [ObjectType.PRIMITIVE_TYPE.BOOLEAN_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.BOOLEAN, typeOptional : true }, + [ObjectType.PRIMITIVE_TYPE.STRING_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.STRING, typeOptional : true }, + [ObjectType.PRIMITIVE_TYPE.UUID_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.UUID }, + [ObjectType.PRIMITIVE_TYPE.DATE_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.DATE, typeOptional : true }, + //[ObjectType.PRIMITIVE_TYPE.ENUM_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.ENUM, typeOptional : true }, + [ObjectType.PRIMITIVE_TYPE.DECIMAL_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.DECIMAL, typeOptional : true }, + [ObjectType.PRIMITIVE_TYPE.TIMESTAMP_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.TIMESTAMP, typeOptional : true }, + [ObjectType.PRIMITIVE_TYPE.TIME_ARRAY] : { elemType : ObjectType.PRIMITIVE_TYPE.TIME } +}; + +// Helper class for testing apache-ignite-client library. +// Contains common methods for testing environment initialization and cleanup. +class TestingHelper { + static get TIMEOUT() { + return TIMEOUT_MS; + } + + static get primitiveValues() { + return primitiveValues; + } + + static get arrayValues() { + return arrayValues; + } + + // Initializes testing environment: creates and starts the library client, sets default jasmine test timeout. + // Should be called from any test suite beforeAll method. + static async init() { + jasmine.DEFAULT_TIMEOUT_INTERVAL = TIMEOUT_MS; + + TestingHelper._igniteClient = new IgniteClient(); + TestingHelper._igniteClient.setDebug(config.debug); + await TestingHelper._igniteClient.connect(new IgniteClientConfiguration(...config.endpoints)); + } + + // Cleans up testing environment. + // Should be called from any test suite afterAll method. + static async cleanUp() { + await TestingHelper.igniteClient.disconnect(); + } + + static get igniteClient() { + return TestingHelper._igniteClient; + } + + static async destroyCache(cacheName, done) { + try { + await TestingHelper.igniteClient.destroyCache(cacheName); + } + catch (err) { + TestingHelper.checkOperationError(err, done); + } + } + + static executeExample(name, outputChecker) { + return new Promise((resolve, reject) => { + exec('node ' + name, (error, stdout, stderr) => { + TestingHelper.logDebug(stdout); + resolve(stdout); + }) + }). + then(output => { + expect(output).not.toMatch('ERROR:'); + expect(output).toMatch('Client is started'); + }); + } + + static checkOperationError(error, done) { + TestingHelper.checkError(error, Errors.OperationError, done) + } + + static checkIllegalArgumentError(error, done) { + TestingHelper.checkError(error, Errors.IgniteClientError, done) + } + + static checkError(error, errorType, done) { + if (!(error instanceof errorType)) { + done.fail('unexpected error: ' + error); + } + } + + static logDebug(message) { + if (config.debug) { + console.log(message); + } + } + + static printValue(value) { + const val = Util.inspect(value, false, null); + const length = 500; + return val.length > length ? val.substr(0, length) + '...' : val; + } + + static async compare(value1, value2) { + TestingHelper.logDebug(Util.format('compare: %s and %s', TestingHelper.printValue(value1), TestingHelper.printValue(value2))); + if (value1 === undefined || value2 === undefined) { + TestingHelper.logDebug(Util.format('compare: unexpected "undefined" value')); + return false; + } + if (value1 === null && value2 === null) { + return true; + } + if (value1 === null && value2 !== null || value1 !== null && value2 === null) { + return false; + } + if (typeof value1 !== typeof value2) { + TestingHelper.logDebug(Util.format('compare: value types are different: %s and %s', + typeof value1, typeof value2)); + return false; + } + if (typeof value1 === 'number') { + return floatComparator(value1, value2); + } + else if (typeof value1 !== 'object') { + return defaultComparator(value1, value2); + } + else if (value1.constructor.name !== value2.constructor.name && !value2 instanceof BinaryObject) { + TestingHelper.logDebug(Util.format('compare: value types are different: %s and %s', + value1.constructor.name, value2.constructor.name)); + return false; + } + else if (value1 instanceof Date && value2 instanceof Date) { + return dateComparator(value1, value2); + } + else if (value1 instanceof EnumItem && value2 instanceof EnumItem) { + return enumComparator(value1, value2); + } + else if (value1 instanceof Decimal && value2 instanceof Decimal) { + return decimalComparator(value1, value2); + } + else if (value1 instanceof Timestamp && value2 instanceof Timestamp) { + return timestampComparator(value1, value2); + } + else if (value1 instanceof Array && value2 instanceof Array) { + if (value1.length !== value2.length) { + TestingHelper.logDebug(Util.format('compare: array lengths are different')); + return false; + } + for (var i = 0; i < value1.length; i++) { + if (!await TestingHelper.compare(value1[i], value2[i])) { + TestingHelper.logDebug(Util.format('compare: array elements are different: %s, %s', + TestingHelper.printValue(value1[i]), TestingHelper.printValue(value2[i]))); + return false; + } + } + return true; + } + else if (value1 instanceof Map && value2 instanceof Map) { + if (value1.size !== value2.size) { + TestingHelper.logDebug(Util.format('compare: map sizes are different')); + return false; + } + for (var [key, val] of value1) { + if (!value2.has(key)) { + TestingHelper.logDebug(Util.format('compare: maps are different: %s key is absent', TestingHelper.printValue(key))); + return false; + } + if (!(await TestingHelper.compare(val, value2.get(key)))) { + TestingHelper.logDebug(Util.format('compare: map values are different: %s, %s', + TestingHelper.printValue(val), TestingHelper.printValue(value2.get(key)))); + return false; + } + } + return true; + } + else if (value1 instanceof Set && value2 instanceof Set) { + if (value1.size !== value2.size) { + TestingHelper.logDebug(Util.format('compare: set sizes are different')); + return false; + } + const value1Arr = [...value1].sort(); + const value2Arr = [...value2].sort(); + if (!await TestingHelper.compare(value1Arr, value2Arr)) { + TestingHelper.logDebug(Util.format('compare: sets are different: %s and %s', + TestingHelper.printValue(value1Arr), TestingHelper.printValue(value2Arr))); + return false; + } + return true; + } + else if (value2 instanceof BinaryObject) { + if (value1 instanceof BinaryObject) { + if (value1.getTypeName() !== value2.getTypeName()) { + TestingHelper.logDebug(Util.format('compare: binary object type names are different')); + return false; + } + if (!await TestingHelper.compare(value1.getFieldNames(), value2.getFieldNames())) { + TestingHelper.logDebug(Util.format('compare: binary object field names are different')); + return false; + } + for (let fieldName of value1.getFieldNames()) { + if (!value1.hasField(fieldName) || !value2.hasField(fieldName) || + !await TestingHelper.compare(await value1.getField(fieldName), await value1.getField(fieldName))) { + TestingHelper.logDebug(Util.format('compare: binary objects field "%s" values are different', fieldName)); + return false; + } + } + return true; + } + else { + let value; + for (let key of Object.keys(value1)) { + value = await value2.getField(key); + if (!(await TestingHelper.compare(value1[key], value))) { + TestingHelper.logDebug(Util.format('compare: binary object values for key %s are different: %s and %s', + TestingHelper.printValue(key), TestingHelper.printValue(value1[key]), TestingHelper.printValue(value))); + return false; + } + } + return true; + } + } + else { + for (let key of Object.keys(value1)) { + if (!(await TestingHelper.compare(value1[key], value2[key]))) { + TestingHelper.logDebug(Util.format('compare: object values for key %s are different: %s and %s', + TestingHelper.printValue(key), TestingHelper.printValue(value1[key]), TestingHelper.printValue(value2[key]))); + return false; + } + } + return true; + } + } +} + +module.exports = TestingHelper;