oracle-nosqldb
Version:
Node.js driver for Oracle NoSQL Database
426 lines (400 loc) • 15.5 kB
JavaScript
/*-
* Copyright (c) 2018, 2024 Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Universal Permissive License v 1.0 as shown at
* https://oss.oracle.com/licenses/upl/
*/
'use strict';
const assert = require('assert');
const util = require('util');
const NoSQLArgumentError = require('./error').NoSQLArgumentError;
const propertyByString = require('./utils').propertyByString;
const isPosInt32 = require('./utils').isPosInt32;
const requireNoWP = require('./utils').requireNoWP;
const RoundingModes = {
UP: 0,
DOWN: 1,
CEILING: 2,
FLOOR: 3,
HALF_UP: 4,
HALF_DOWN: 5,
HALF_EVEN: 6,
UNNECESSARY: 7
};
const DEFAULT_PRECISION = 20;
class NumberTypeHandler {
constructor(cfg) {
assert(cfg.dbNumber != null);
try {
this._initCons(cfg);
//These can be customized in future to allow creation of number
//instances by callable functions (instead of constructor).
this.isInstance = val => val instanceof this._cons;
this.create = val => new this._cons(val);
this._initMethod(cfg, 'stringValue', 1, [ 'toString' ], []);
this._initMethod(cfg, 'compare', 2,
[ 'comparedTo', 'compareTo', 'cmp', 'compare' ]);
this._initMethod(cfg, 'valuesEqual', 2,
[ 'equals', 'isEqualTo', 'eq' ], [], true);
if (this.valuesEqual == null) {
this.valuesEqual = (v1, v2) => this.compare(v1, v2) === 0;
}
this._initMethod(cfg, 'numberValue', 1, [ 'toNumber' ], [], true);
if (this.numberValue == null) {
this.numberValue = v => Number(this.stringValue(v));
}
this._initMethod(cfg, 'add', 2, [ 'plus', 'add' ]);
this._initMethod(cfg, 'subtract', 2,
[ 'minus', 'sub', 'subtract' ]);
this._initMethod(cfg, 'multiply', 2,
[ 'times', 'multipliedBy', 'multiply', 'mul' ]);
this._initMethod(cfg, 'divide', 2,
[ 'dividedBy', 'divide', 'div' ]);
this._initRoundingModesMap(cfg);
this._initGetRoundingMode(cfg);
this._initGetPrecision(cfg);
this._initRoundingMode(cfg);
this._initPrecision(cfg);
} catch(err) {
throw new NoSQLArgumentError('Failed to initialize dbNumber \
configuration', cfg, err);
}
}
get precision() {
return this._precision;
}
get roundingMode() {
return this._roundingMode;
}
_initCons(cfg) {
switch(typeof cfg.dbNumber) {
case 'function':
this._cons = cfg.dbNumber;
return;
case 'string':
this._cons = requireNoWP(cfg.dbNumber);
break;
case 'object':
if (cfg.dbNumber.module != null) {
if (typeof cfg.dbNumber.module !== 'string') {
throw new NoSQLArgumentError('Invalid dbNumber.module \
property, must be string', cfg);
}
this._mod = requireNoWP(cfg.dbNumber.module);
if (cfg.dbNumber.Constructor == null) {
this._cons = this._mod;
break;
} else if (typeof cfg.dbNumber.Constructor === 'string') {
this._cons = this._mod[cfg.dbNumber.Constructor];
break;
}
}
this._cons = typeof cfg.dbNumber.Constructor === 'string' ?
requireNoWP(cfg.dbNumber.Constructor) :
cfg.dbNumber.Constructor;
break;
default:
throw new NoSQLArgumentError('Invalid dbNumber property', cfg);
}
if (this._cons == null) {
throw new NoSQLArgumentError('Missing dbNumber constructor', cfg);
}
if (typeof this._cons !== 'function') {
throw new NoSQLArgumentError('Invalid dbNumber constructor', cfg);
}
}
_findFuncByProp(cfg, name, isStatic) {
if (typeof cfg.dbNumber !== 'object') {
return null;
}
const subName = isStatic ? 'static' : 'instance';
const sub = cfg.dbNumber[subName];
if (sub == null) {
return null;
}
if (typeof sub !== 'object') {
throw new NoSQLArgumentError(
`Invalid dbNumber.${subName} property`, cfg);
}
const obj = isStatic ? this._cons : this._cons.prototype;
let func = sub[name];
if (func == null) {
return null;
}
if (typeof func === 'string') {
func = obj[func];
}
if (typeof func !== 'function') {
throw new NoSQLArgumentError(`Property \
dbNumber.${subName}.${name} points to missing or invalid method of \
${this._cons.name}`, cfg);
}
return func;
}
_findFunc(cfg, name, cands, staticCands, retNull) {
let func = this._findFuncByProp(cfg, name, false);
if (func != null) {
return { isStatic: false, func };
}
func = this._findFuncByProp(cfg, name, true);
if (func != null) {
return { isStatic: true, func };
}
if (cands != null) {
for(let cand of cands) {
func = this._cons.prototype[cand];
if (typeof func === 'function') {
return { isStatic: false, func };
}
}
}
if (staticCands == null) {
staticCands = cands;
}
if (staticCands != null) {
for(let cand of staticCands) {
func = this._cons[cand];
if (typeof func === 'function') {
return { isStatic: true, func };
}
}
}
if (retNull) {
return null;
}
throw new NoSQLArgumentError(`Could not find dbNumber method for \
${name}`, cfg);
}
_initMethod(cfg, name, numArgs, cands, staticCands, isSoft) {
const meth = this._findFunc(cfg, name, cands, staticCands, isSoft);
if (meth == null) {
return;
}
if (meth.isStatic) {
this[name] = meth.func.bind(this._cons);
} else {
//not certain if this optimization is needed or the default case
//is sufficient
switch(numArgs) {
case 1:
this[name] = v => meth.func.call(v);
break;
case 2:
this[name] = (v1, v2) => meth.func.call(v1, v2);
break;
default:
this[name] = (inst, ...args) => meth.func.call(inst, ...args);
break;
}
}
}
_findRoundingModes(cfg) {
const candObjs = [ this._cons ];
if (this._mod != null) {
candObjs.push(this._mod);
}
//First look at dbNumber.RoundingModes which should be either a
//property name of rounding modes object or that object itself
if (cfg.dbNumber.RoundingModes != null) {
if (typeof cfg.dbNumber.RoundingModes === 'string') {
for(let candObj of candObjs) {
const roundingModes = propertyByString(candObj,
cfg.dbNumber.RoundingModes);
if (roundingModes !== null &&
typeof roundingModes === 'object' ||
typeof roundingModes === 'function') {
return roundingModes;
}
}
throw new NoSQLArgumentError(`Missing or invalid value for \
dbNumber.RoundingModes property ${cfg.dbNumber.RoundingModes}`);
}
if (typeof cfg.dbNumber.RoundingModes !== 'object' &&
typeof cfg.dbNumber.RoundingModes !== 'function') {
throw new NoSQLArgumentError(`Invalid \
dbNumber.RoundingModes: ${util.inspect(cfg.dbNumber.RoundingModes)}`, cfg);
}
return cfg.dbNumber.RoundingModes;
}
//Otherwise check candidate property names on module or
//constructor
const candProps = [ 'RoundingModes', 'RoundingMode' ];
for(let candObj of candObjs) {
for(let candProp of candProps) {
const roundingModes = candObj[candProp];
if (roundingModes !== null &&
typeof roundingModes === 'object' ||
typeof roundingModes === 'function') {
return roundingModes;
}
}
}
//If still not found, assume that constructor itself contains rounding
//mode constants
return this._cons;
}
//create a Map mapping 3rd party rounding mode constants to Java's
_initRoundingModesMap(cfg) {
this._roundingModes = this._findRoundingModes(cfg);
//Create a mapping from 3rd party rounding mode values to Java's
this._roundingModesMap = new Map();
for(let ent of Object.entries(RoundingModes)) {
//Look for properties with and without ROUND_ prefix
let val = this._roundingModes['ROUND_' + ent[0]];
if (val == null) {
val = this._roundingModes[ent[0]];
}
if (val != null) {
this._roundingModesMap.set(val, ent[1]);
}
}
}
_initGetRoundingMode(cfg) {
let rm;
let rmFunc;
if (cfg.dbNumber.getRoundingMode != null) {
switch(typeof cfg.dbNumber.getRoundingMode) {
case 'function':
rmFunc = cfg.dbNumber.getRoundingMode;
break;
case 'string':
rm = propertyByString(this._cons,
cfg.dbNumber.getRoundingMode);
if (rm == null) {
throw new NoSQLArgumentError(`Missing value for \
dbNumber.getRoundingMode property ${cfg.dbNumber.getRoundingMode}`, cfg);
}
rmFunc = cons => propertyByString(cons,
cfg.dbNumber.getRoundingMode);
break;
default:
throw new NoSQLArgumentError(`Invalid \
dbNumber.getRoundingMode: ${util.inspect(cfg.dbNumber.getRoundingMode)}`);
}
} else {
//If dbNumber.getRoundingMode is not set and rounding modes
//mapping is not available, assume that the user just wants to set
//dbNumber.roundingMode explicitly or use default
if (this._roundingModesMap.size === 0) {
return;
}
//Otherwise check candidate names
let cands = [ 'rounding', 'roundingMode', 'ROUNDING_MODE',
'RM' ];
for(let cand of cands) {
rm = this._cons[cand];
if (rm != null) {
rmFunc = cons => cons[cand];
break;
}
}
//If not found any way to get rounding mode, let the user set
//dbNumber.roundingMode explicitly or use default
if (rmFunc == null) {
return;
}
}
this._getRoundingMode = cons => {
const rmVal = rmFunc(cons);
const rm = this._roundingModesMap.get(rmVal);
if (rm == null) {
throw new NoSQLArgumentError(`Could not determine rounding \
mode with value ${util.inspect(rmVal)}, please check configuration \
properties dbNumber.RoundingModes and dbNumber.getRoundingMode`, cfg);
}
return rm;
};
}
_initGetPrecision(cfg) {
let prec;
let precFunc;
if (cfg.dbNumber.getPrecision != null) {
switch(typeof cfg.dbNumber.getPrecision) {
case 'function':
precFunc = cfg.dbNumber.getPrecision;
break;
case 'string':
prec = propertyByString(this._cons,
cfg.dbNumber.getPrecision);
if (!isPosInt32(prec)) {
throw new NoSQLArgumentError(`Missing or invalid value \
for dbNumber.getRoundingMode property ${cfg.dbNumber.getPrecision}`, cfg);
}
precFunc = cons => propertyByString(cons,
cfg.dbNumber.getPrecision);
break;
default:
throw new NoSQLArgumentError(`Invalid \
dbNumber.getPrecision: ${util.inspect(cfg.dbNumber.getPrecision)}`);
}
}
if (precFunc == null) {
const cands = [ 'precision', 'PRECISION' ];
for(let cand of cands) {
prec = this._cons[cand];
if (isPosInt32(prec)) {
precFunc = cons => cons[cand];
break;
}
}
if (precFunc == null) {
return;
}
}
this._getPrecision = cons => {
let prec = precFunc(cons);
if (!isPosInt32(prec)) {
throw new NoSQLArgumentError(`Got invalid precision value: \
${util.inspect(prec)}, please check configuration property \
dbNumber.getPrecision`, cfg);
}
return prec;
};
}
//get rounding mode constant in Java's format via _roundingModesMap
_initRoundingMode(cfg) {
if (cfg.dbNumber.roundingMode != null) {
if (typeof cfg.dbNumber.roundingMode === 'string') {
//check if dbNumber.roundingMode could be just a name of
//the rounding mode enumeration constant (with or without
//ROUND_ prefix)
const rm = cfg.dbNumber.roundingMode.startsWith('ROUND_') ?
cfg.dbNumber.roundingMode.substring(6) :
cfg.dbNumber.roundingMode;
if (rm in RoundingModes) {
this._roundingMode = RoundingModes[rm];
return;
}
}
this._roundingMode = this._roundingModesMap.get(
cfg.dbNumber.roundingMode);
if (this._roundingMode == null) {
throw new NoSQLArgumentError(`Could not determine rounding \
mode set by configuration property dbNumber.roundingMode with value \
${util.inspect(cfg.dbNumber.roundingMode)}, please check configuration \
properties dbNumber.roundingMode and dbNumber.RoundingModes`, cfg);
}
return;
}
if (this._getRoundingMode != null) {
this._roundingMode = this._getRoundingMode(this._cons);
} else {
//if rounding mode cannot be found, use default ROUND_HALF_UP
this._roundingMode = RoundingModes.HALF_UP;
}
}
_initPrecision(cfg) {
if (cfg.dbNumber.precision != null) {
if (!isPosInt32(cfg.dbNumber.precision)) {
throw new NoSQLArgumentError(`Invalid value of \
dbNumber.precision: ${cfg.dbNumber.precision}`, cfg);
}
this._precision = cfg.dbNumber.precision;
} else if (this._getPrecision != null) {
this._precision = this._getPrecision(this._cons);
} else {
this._precision = DEFAULT_PRECISION;
}
}
}
module.exports = NumberTypeHandler;