Add QueryError class for improved error handling in Minimongo operations

This commit is contained in:
italo jose
2025-07-03 12:16:27 -03:00
committed by Italo José
parent 74a5c112e6
commit 40de116de0
2 changed files with 39 additions and 30 deletions

View File

@@ -2,6 +2,13 @@ import LocalCollection from './local_collection.js';
export const hasOwn = Object.prototype.hasOwnProperty;
export class QueryError extends Error {
constructor(message) {
super(message);
this.name = 'QueryError';
}
}
// Each element selector contains:
// - compileElementSelector, a function with args:
// - operand - the "right hand side" of the operator
@@ -24,7 +31,7 @@ export const ELEMENT_OPERATORS = {
if (!(Array.isArray(operand) && operand.length === 2
&& typeof operand[0] === 'number'
&& typeof operand[1] === 'number')) {
throw Error('argument to $mod must be an array of two numbers');
throw new QueryError('argument to $mod must be an array of two numbers');
}
// XXX could require to be ints or round or something
@@ -38,7 +45,7 @@ export const ELEMENT_OPERATORS = {
$in: {
compileElementSelector(operand) {
if (!Array.isArray(operand)) {
throw Error('$in needs an array');
throw new QueryError('$in needs an array');
}
const elementMatchers = operand.map(option => {
@@ -47,7 +54,7 @@ export const ELEMENT_OPERATORS = {
}
if (isOperatorObject(option)) {
throw Error('cannot nest $ under $in');
throw new QueryError('cannot nest $ under $in');
}
return equalityElementMatcher(option);
@@ -74,7 +81,7 @@ export const ELEMENT_OPERATORS = {
// does.
operand = 0;
} else if (typeof operand !== 'number') {
throw Error('$size needs a number');
throw new QueryError('$size needs a number');
}
return value => Array.isArray(value) && value.length === operand;
@@ -112,16 +119,16 @@ export const ELEMENT_OPERATORS = {
'maxKey': 127,
};
if (!hasOwn.call(operandAliasMap, operand)) {
throw Error(`unknown string alias for $type: ${operand}`);
throw new QueryError(`unknown string alias for $type: ${operand}`);
}
operand = operandAliasMap[operand];
} else if (typeof operand === 'number') {
if (operand === 0 || operand < -1
|| (operand > 19 && operand !== 127)) {
throw Error(`Invalid numerical $type code: ${operand}`);
throw new QueryError(`Invalid numerical $type code: ${operand}`);
}
} else {
throw Error('argument to $type is not a number or a string');
throw new QueryError('argument to $type is not a number or a string');
}
return value => (
@@ -168,7 +175,7 @@ export const ELEMENT_OPERATORS = {
$regex: {
compileElementSelector(operand, valueSelector) {
if (!(typeof operand === 'string' || operand instanceof RegExp)) {
throw Error('$regex has to be a string or RegExp');
throw new QueryError('$regex has to be a string or RegExp');
}
let regexp;
@@ -180,7 +187,7 @@ export const ELEMENT_OPERATORS = {
// ones (eg, Mongo supports x and s). Ideally we would implement x and s
// by transforming the regexp, but not today...
if (/[^gim]/.test(valueSelector.$options)) {
throw new Error('Only the i, m, and g regexp options are supported');
throw new QueryError('Only the i, m, and g regexp options are supported');
}
const source = operand instanceof RegExp ? operand.source : operand;
@@ -198,7 +205,7 @@ export const ELEMENT_OPERATORS = {
dontExpandLeafArrays: true,
compileElementSelector(operand, valueSelector, matcher) {
if (!LocalCollection._isPlainObject(operand)) {
throw Error('$elemMatch need an object');
throw new QueryError('$elemMatch need an object');
}
const isDocMatcher = !isOperatorObject(
@@ -353,7 +360,7 @@ const VALUE_OPERATORS = {
// $options just provides options for $regex; its logic is inside $regex
$options(operand, valueSelector) {
if (!hasOwn.call(valueSelector, '$regex')) {
throw Error('$options needs a $regex');
throw new QueryError('$options needs a $regex');
}
return everythingMatcher;
@@ -361,14 +368,14 @@ const VALUE_OPERATORS = {
// $maxDistance is basically an argument to $near
$maxDistance(operand, valueSelector) {
if (!valueSelector.$near) {
throw Error('$maxDistance needs a $near');
throw new QueryError('$maxDistance needs a $near');
}
return everythingMatcher;
},
$all(operand, valueSelector, matcher) {
if (!Array.isArray(operand)) {
throw Error('$all requires array');
throw new QueryError('$all requires array');
}
// Not sure why, but this seems to be what MongoDB does.
@@ -379,7 +386,7 @@ const VALUE_OPERATORS = {
const branchedMatchers = operand.map(criterion => {
// XXX handle $all/$elemMatch combination
if (isOperatorObject(criterion)) {
throw Error('no $ expressions in $all');
throw new QueryError('no $ expressions in $all');
}
// This is always a regexp or equality selector.
@@ -392,7 +399,7 @@ const VALUE_OPERATORS = {
},
$near(operand, valueSelector, matcher, isRoot) {
if (!isRoot) {
throw Error('$near can\'t be inside another $ operator');
throw new QueryError('$near can\'t be inside another $ operator');
}
matcher._hasGeoQuery = true;
@@ -433,7 +440,7 @@ const VALUE_OPERATORS = {
maxDistance = valueSelector.$maxDistance;
if (!isIndexable(operand)) {
throw Error('$near argument must be coordinate pair or GeoJSON');
throw new QueryError('$near argument must be coordinate pair or GeoJSON');
}
point = pointToArray(operand);
@@ -549,12 +556,12 @@ const andBranchedMatchers = andSomeMatchers;
function compileArrayOfDocumentSelectors(selectors, matcher, inElemMatch) {
if (!Array.isArray(selectors) || selectors.length === 0) {
throw Error('$and/$or/$nor must be nonempty array');
throw new QueryError('$and/$or/$nor must be nonempty array');
}
return selectors.map(subSelector => {
if (!LocalCollection._isPlainObject(subSelector)) {
throw Error('$or/$and/$nor entries need to be full objects');
throw new QueryError('$or/$and/$nor entries need to be full objects');
}
return compileDocumentSelector(subSelector, matcher, {inElemMatch});
@@ -576,7 +583,7 @@ export function compileDocumentSelector(docSelector, matcher, options = {}) {
// Outer operators are either logical operators (they recurse back into
// this function), or $where.
if (!hasOwn.call(LOGICAL_OPERATORS, key)) {
throw new Error(`Unrecognized logical operator: ${key}`);
throw new QueryError(`Unrecognized logical operator: ${key}`);
}
matcher._isSimple = false;
@@ -682,7 +689,7 @@ function distanceCoordinatePairs(a, b) {
// for equality with that thing.
export function equalityElementMatcher(elementSelector) {
if (isOperatorObject(elementSelector)) {
throw Error('Can\'t create equalityValueSelector for operator object');
throw new QueryError('Can\'t create equalityValueSelector for operator object');
}
// Special-case: null and undefined are equal (if you got undefined in there
@@ -759,7 +766,7 @@ function getOperandBitmask(operand, selector) {
}
// bad operand
throw Error(
throw new QueryError(
`operand to ${selector} must be a numeric bitmask (representable as a ` +
'non-negative 32-bit signed integer), a bindata bitmask or an array with ' +
'bit positions (non-negative integers)'
@@ -813,12 +820,11 @@ function insertIntoDocument(document, key, value) {
(existingKey.length > key.length && existingKey.indexOf(`${key}.`) === 0) ||
(key.length > existingKey.length && key.indexOf(`${existingKey}.`) === 0)
) {
throw new Error(
`cannot infer query fields to set, both paths '${existingKey}' and ` +
`'${key}' are matched`
throw new QueryError(
`cannot infer query fields to set, both paths '${existingKey}' and '${key}' are matched`
);
} else if (existingKey === key) {
throw new Error(
throw new QueryError(
`cannot infer query fields to set, path '${key}' is matched twice`
);
}
@@ -863,7 +869,7 @@ export function isOperatorObject(valueSelector, inconsistentOK) {
theseAreOperators = thisIsOperator;
} else if (theseAreOperators !== thisIsOperator) {
if (!inconsistentOK) {
throw new Error(
throw new QueryError(
`Inconsistent operator: ${JSON.stringify(valueSelector)}`
);
}
@@ -1132,7 +1138,7 @@ function operatorBranchedMatcher(valueSelector, matcher, isRoot) {
);
}
throw new Error(`Unrecognized operator: ${operator}`);
throw new QueryError(`Unrecognized operator: ${operator}`);
});
return andBranchedMatchers(operatorMatchers);
@@ -1232,7 +1238,7 @@ function populateDocumentWithObject(document, key, value) {
// Literal (possibly empty) object ( or empty object )
// Don't allow mixing '$'-prefixed with non-'$'-prefixed fields
if (keys.length !== unprefixedKeys.length) {
throw new Error(`unknown operator: ${unprefixedKeys[0]}`);
throw new QueryError(`unknown operator: ${unprefixedKeys[0]}`);
}
validateObject(value, key);

View File

@@ -5,7 +5,7 @@ import { AsynchronousCursor } from './asynchronous_cursor';
import { Cursor } from './cursor';
import { CursorDescription } from './cursor_description';
import { DocFetcher } from './doc_fetcher';
import { MongoDB, replaceMeteorAtomWithMongo, replaceTypes, transformResult } from './mongo_common';
import { MongoDB, replaceMeteorAtomWithMongo, replaceTypes, transformResult, QueryError } from './mongo_common';
import { ObserveHandle } from './observe_handle';
import { ObserveMultiplexer } from './observe_multiplex';
import { OplogObserveDriver } from './oplog_observe_driver';
@@ -794,7 +794,7 @@ MongoConnection.prototype.tail = function (cursorDescription, docCallback, timeo
return {
stop: function () {
stopped = true;
cursor.close()
cursor.close();
}
};
};
@@ -886,6 +886,9 @@ Object.assign(MongoConnection.prototype, {
} catch (e) {
// XXX make all compilation errors MinimongoError or something
// so that this doesn't ignore unrelated exceptions
if (e instanceof QueryError) {
throw e;
}
return false;
}
},