"use strict";
const util = require("util");
const types = require("./index");
const rust = require("../../index");
const { ResponseError } = require("../errors");
const TimeUuid = require("./time-uuid");
const Uuid = require("./uuid");
const Duration = require("./duration");
const LocalDate = require("./local-date");
const LocalTime = require("./local-time");
const InetAddress = require("./inet-address");
const { guessType } = require("./type-guessing");
const { arbitraryValueToBigInt } = require("../new-utils");
/**
* Ensures the value isn't one of many ways to express null value
* @param {string} message Error message if value is "null"
* @throws {TypeError} Throws an error if the value is "null"
*/
function ensureValue(val, message) {
if (val === null || typeof val === "undefined" || val === types.unset)
throw new TypeError(message);
}
/**
* Guess the type, and if the type cannot be guessed, throw an error.
* @param {any} value
* @returns {rust.ComplexType} type guess, converted to `ComplexType` object
*/
function guessTypeChecked(value) {
let type = rustConvertHint(guessType(value));
if (!type) {
throw new TypeError(
"Target data type could not be guessed, you should use prepared statements for accurate type mapping. Value: " +
util.inspect(value),
);
}
return type;
}
/**
* Wraps each of the elements into given subtype
* @param {Array} values
* @param {rust.ComplexType} subtype
* @returns {Array<rust.ComplexType|any[]>} Returns tuple: [rust.ComplexType, any[]]
*/
function encodeListLike(values, subtype) {
if (!Array.isArray(values))
throw new TypeError(
`Not a valid list value, expected Array obtained ${util.inspect(values)}`,
);
if (!subtype) subtype = guessTypeChecked(values[0]);
let res = [];
for (let i = 0; i < values.length; i++) {
// This requirement is based on the datastax code
ensureValue(
values[i],
"A collection can't contain null or unset values",
);
res.push(wrapValueBasedOnType(subtype, values[i])[1]);
}
return [subtype, res];
}
/**
* @param {*} value
* @param {rust.ComplexType} parentType
* @returns {Array<rust.ComplexType|any[][]>} Returns tuple: [rust.ComplexType, any[][]]
*/
function encodeMap(value, parentType) {
let keySubtype = parentType.getFirstSupportType();
let valueSubtype = parentType.getSecondSupportType();
let res = [];
for (const key in value) {
if (!Object.prototype.hasOwnProperty.call(value, key)) {
continue;
}
const val = value[key];
ensureValue(val, "A collection can't contain null or unset values");
ensureValue(key, "A collection can't contain null or unset values");
if (!keySubtype || !valueSubtype) {
if (valueSubtype || keySubtype) {
throw new Error(
`Internal error: Invalid support types for map`,
);
}
keySubtype = guessTypeChecked(key);
valueSubtype = guessTypeChecked(val);
parentType = parentType.remapMapSupportType(
keySubtype,
valueSubtype,
);
}
res.push([
wrapValueBasedOnType(keySubtype, key)[1],
wrapValueBasedOnType(valueSubtype, val)[1],
]);
}
return [parentType, res];
}
/**
* Ensures the provided value is non NaN. If it is, throws an error
* @param {any} value
*/
function ensureNumber(value) {
if (Number.isNaN(value)) {
throw new TypeError("Expected Number, obtained " + util.inspect(value));
}
}
/**
* Wraps tuple into format recognized by ParameterWrapper
* @param {types.Tuple} value
* @param {rust.ComplexType} type
* @returns {Array<rust.ComplexType|Array<any>>} Returns tuple: [rust.ComplexType, Array<any>]
*/
function encodeTuple(value, type) {
let res = [];
// TODO:
// Add proper type guessing for tuples
// If some of the types are not provided (as it's a possible case in type guessing)
// then the returned type will have this information, while type provided in argument - not.
let newTypes = [];
let types = type.getInnerTypes();
for (let i = 0; i < types.length; i++) {
const element = getWrapped(
types[i],
value.get(i) !== undefined ? value.get(i) : null,
);
newTypes.push(element[0]);
res.push(element[1]);
}
return [rust.ComplexType.remapTupleSupportType(newTypes), res];
}
/**
* Wraps value into format recognized by ParameterWrapper, based on the provided type
* @param {rust.ComplexType} type
* @param {*} value
* @returns {Array<rust.ComplexType|any>} Returns tuple: [rust.ComplexType, any] or []
*/
function getWrapped(type, value) {
// Unset was introduced in CQLv3, and the backend of the driver - Rust driver -
// works only with version >= 4 of CQL, so unset will always be supported.
if (value === null) {
return [];
} else if (value === types.unset) {
return [undefined];
}
return wrapValueBasedOnType(type, value);
}
/**
* Wrap value, which is not Unset, into type and value pair,
* ensuring value is converted into expected form
* @param {rust.ComplexType} type
* @param {*} value
* @returns {Array<rust.ComplexType|any>} Returns tuple: [rust.ComplexType, any]
*/
function wrapValueBasedOnType(type, value) {
let tmpElement;
// To increase clarity of the error messages, in case value is of different type then expected,
// when we call some methods on value variable, type is checked explicitly.
// In other cases default Error will be thrown, which has message meaningful for the user.
switch (type.baseType) {
// For these types, no action is required
case rust.CqlType.Ascii:
case rust.CqlType.Boolean:
case rust.CqlType.Blob:
case rust.CqlType.Decimal:
case rust.CqlType.Double:
case rust.CqlType.Empty:
case rust.CqlType.Float:
case rust.CqlType.Text:
break;
case rust.CqlType.BigInt:
value = arbitraryValueToBigInt(value);
break;
case rust.CqlType.Counter:
value = BigInt(value);
break;
case rust.CqlType.Date:
if (!(value instanceof LocalDate))
throw new TypeError(
"Expected LocalDate type to parse into Cql Date",
);
value = value.getInternal();
break;
case rust.CqlType.Duration:
if (!(value instanceof Duration))
throw new TypeError(
"Expected Duration type to parse into Cql Duration",
);
value = value.getInternal();
break;
case rust.CqlType.Int:
case rust.CqlType.SmallInt:
case rust.CqlType.TinyInt:
ensureNumber(value);
break;
case rust.CqlType.Set:
// TODO:
// This is part of the datastax logic for encoding set.
// Here, this way of providing set is not supported.
/* if (
this.encodingOptions.set &&
value instanceof this.encodingOptions.set
) {
const arr = [];
value.forEach(function (x) {
arr.push(x);
});
return this.encodeList(arr, subtype);
} */
value = encodeListLike(value, type.getFirstSupportType());
if (!type.getFirstSupportType())
type = type.remapListSupportType(value[0]);
value = value[1];
break;
case rust.CqlType.Timestamp:
tmpElement = value;
if (typeof value === "string") {
value = new Date(value);
}
if (value instanceof Date) {
// milliseconds since epoch
value = value.getTime();
if (isNaN(value)) {
throw new TypeError("Invalid date: " + tmpElement);
}
}
value = BigInt(value);
break;
case rust.CqlType.Inet:
// Other forms of providing InetAddress are kept as parity with old driver
if (typeof value === "string") {
value = InetAddress.fromString(value);
} else if (value instanceof Buffer) {
value = new InetAddress(value);
}
if (!(value instanceof InetAddress)) {
throw new TypeError(
"Expected InetAddress type to parse into Cql Inet",
);
}
value = value.getInternal();
break;
case rust.CqlType.List:
value = encodeListLike(value, type.getFirstSupportType());
if (!type.getFirstSupportType())
type = type.remapListSupportType(value[0]);
value = value[1];
break;
case rust.CqlType.Map:
// TODO:
// This is part of the datastax logic for encoding map.
// Here, this way of providing map is not supported.
/* if (
this.encodingOptions.map &&
value instanceof this.encodingOptions.map
) {
// Use Map#forEach() method to iterate
value.forEach(addItem);
} */
value = encodeMap(value, type);
type = value[0];
value = value[1];
break;
case rust.CqlType.Time:
// Other forms of providing LocalTime are kept as parity with old driver
if (typeof value == "string") {
value = LocalTime.fromString(value);
}
if (!(value instanceof LocalTime)) {
throw new TypeError(
"Expected LocalTime type to parse into Cql Time",
);
}
value = value.getInternal();
break;
case rust.CqlType.Uuid:
// Other forms of providing UUID are kept as parity with old driver
if (typeof value === "string") {
value = Uuid.fromString(value);
}
if (!(value instanceof Uuid)) {
throw new TypeError(
"Expected UUID type to parse into Cql Uuid",
);
}
value = value.getInternal();
break;
case rust.CqlType.Tuple:
value = encodeTuple(value, type);
type = value[0];
value = value[1];
break;
case rust.CqlType.Timeuuid:
// Other forms of providing TimeUUID are kept as parity with old driver
if (typeof value === "string") {
value = TimeUuid.fromString(value);
}
if (!(value instanceof TimeUuid)) {
throw new TypeError(
"Expected Time UUID type to parse into Cql Uuid",
);
}
value = value.getInternal();
break;
default:
// Or not yet implemented type
throw new ReferenceError(
`[INTERNAL DRIVER ERROR] Unknown type: ${type}`,
);
}
return [type, value];
}
/**
* Parses array of params into expected format according to preparedStatement expected types
*
* If `allowGuessing` is true, then for each missing field of `expectedTypes`, this function will try to guess a type.
* If the type cannot be guessed, error will be thrown.
* Field is missing if it is null, undefined or if the `expectedTypes` list is too short.
* @param {Array<rust.ComplexType | null>} expectedTypes List of expected types.
* @param {Array<any>} params
* @param {boolean} [allowGuessing]
* @returns {Array<rust.ComplexType|any>} Returns: [] for null values, [undefined] for unset values
* and [rust.ComplexType, any] for all other values.
* @throws ResponseError when received different amount of parameters than expected
*/
function parseParams(expectedTypes, params, allowGuessing) {
if (expectedTypes.length == 0 && !params) return [];
if (params.length != expectedTypes.length && !allowGuessing) {
throw new ResponseError(
types.responseErrorCodes.invalid,
`Expected ${expectedTypes.length}, got ${params.length} parameters.`,
);
}
let res = [];
for (let i = 0; i < params.length; i++) {
// Warning: Currently unset and logic is duplicated with getWrapped.
// TODO: Clean this up.
if (params[i] === types.unset) {
// Unset was introduced in CQLv3, and the backend of the driver - Rust driver -
// works only with version >= 4 of CQL, so unset will always be supported.
res.push([undefined]);
continue;
}
// undefined as null depends on encodingOptions.useUndefinedAsUnset option
// TODO: Add support for this option
if (params[i] === null || params[i] === undefined) {
res.push([]);
continue;
}
let type = expectedTypes[i];
if (!type && allowGuessing) type = guessTypeChecked(params[i]);
res.push(getWrapped(type, params[i]));
}
return res;
}
/**
* Convert the hints from the formats allowed by the driver, to internal type representation
* @param {Array<string | number | object | null>} hints
* @returns {Array<rust.ComplexType | null>}
*/
function convertHints(hints) {
let result = [];
if (!Array.isArray(hints)) {
return [];
}
for (const hint of hints) {
if (hint) {
/** @type {{code: Number, info: object}} */
let objectHint = {
code: null,
info: null,
};
if (typeof hint === "number") {
objectHint.code = hint;
} else if (typeof hint === "string") {
objectHint = types.dataTypes.getByName(hint);
} else if (typeof hint.code === "number") {
objectHint.code = hint.code;
objectHint.info = hint.info;
}
if (typeof objectHint.code !== "number") {
throw new TypeError(
"Type information not valid, only String and Number values are valid hints",
);
}
result.push(rustConvertHint(objectHint));
} else {
result.push(null);
}
}
return result;
}
/**
*
* @param {null | object | Array<object>} object
* @returns {rust.ComplexType | null}
*/
function rustConvertHint(object) {
if (object.info && !Array.isArray(object.info)) {
object.info = [object.info];
}
return rust.convertHint(object);
}
module.exports.parseParams = parseParams;
module.exports.convertHints = convertHints;
module.exports.rustConvertHint = rustConvertHint;
// For unit test usage
module.exports.getWrapped = getWrapped;