From 40de116de024366362b7efa0ca9a64cdf1aa6837 Mon Sep 17 00:00:00 2001 From: italo jose Date: Thu, 3 Jul 2025 12:16:27 -0300 Subject: [PATCH] Add QueryError class for improved error handling in Minimongo operations --- packages/minimongo/common.js | 62 ++++++++++++++++-------------- packages/mongo/mongo_connection.js | 7 +++- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/packages/minimongo/common.js b/packages/minimongo/common.js index 7a08e569b1..43393492fb 100644 --- a/packages/minimongo/common.js +++ b/packages/minimongo/common.js @@ -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); diff --git a/packages/mongo/mongo_connection.js b/packages/mongo/mongo_connection.js index 25a303a70c..7655cebb67 100644 --- a/packages/mongo/mongo_connection.js +++ b/packages/mongo/mongo_connection.js @@ -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; } },