Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions packages/query-parser/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import _debug from 'debug';
import {
isCollationValid,
isFilterValid,
isHintValid,
isLimitValid,
isMaxTimeMSValid,
isProjectValid,
Expand Down Expand Up @@ -719,6 +720,53 @@ e s`,
});
});

describe('hint', function () {
it('should default to null', function () {
assert.equal(isHintValid(''), null);
assert.equal(isHintValid(' '), null);
assert.equal(isHintValid('{}'), null);
});

it('should parse hint objects', function () {
assert.deepEqual(isHintValid('{_id: 1}'), { _id: 1 });
assert.deepEqual(isHintValid('{_id: -1}'), { _id: -1 });
assert.deepEqual(isHintValid('{pineapple: 1, age: -1}'), {
pineapple: 1,
age: -1,
});
});

it('should accept string index names', function () {
assert.deepEqual(isHintValid('"pineapple"'), 'pineapple');
assert.deepEqual(isHintValid("'pineapple'"), 'pineapple');
});

it('should not accept arrays', function () {
assert.deepEqual(isHintValid('["pineappleOne", "pineappleTwo"]'), false);
});

it('should accept docs with numeric values', function () {
assert.deepEqual(isHintValid('{pineapple: 0}'), { pineapple: 0 });
assert.deepEqual(isHintValid('{pineapple: -1}'), { pineapple: -1 });
assert.deepEqual(isHintValid('{pineapple: NaN}'), { pineapple: NaN });
assert.deepEqual(isHintValid('{pineapple: 2}'), { pineapple: 2 });
});

it('should reject broken objects', function () {
assert.equal(isHintValid('{not_pineapple'), false);
assert.equal(isHintValid('invalid pineapple: }'), false);
assert.equal(isHintValid('{invalid pineapple}'), false);
assert.equal(isHintValid('{invalid pineapple: }'), false);
});

it('should reject non-string/non-object hint values', function () {
assert.equal(isHintValid('true'), false);
assert.equal(isHintValid('pineapple'), false);
assert.equal(isHintValid('123'), false);
assert.equal(isHintValid('null'), false);
});
});

describe('sort', function () {
it('should default to null', function () {
assert.equal(parseSort(''), null);
Expand Down
56 changes: 54 additions & 2 deletions packages/query-parser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const DEFAULT_COLLATION = null;
/** @public */
const DEFAULT_MAX_TIME_MS = 60000; // 1 minute in ms
/** @public */
const QUERY_PROPERTIES = ['filter', 'project', 'sort', 'skip', 'limit'];
const DEFAULT_HINT = null;

function isEmpty(input: string | number | null | undefined): boolean {
if (input === null || input === undefined) {
Expand Down Expand Up @@ -62,6 +62,18 @@ export function parseSort(input: string) {
return parseShellStringToEJSON(input, { mode: ParseMode.Loose });
}

function isValueOkForHint() {
/**
* Prior to MongoDB 7.0, hint would accept invalid values, like NaN.
* So we're on the looser side of validation here.
*/
return true;
}

function _parseHint(input: string) {
return parseShellStringToEJSON(input, { mode: ParseMode.Loose });
}

function _parseFilter(input: string) {
return parseShellStringToEJSON(input, {
mode: ParseMode.Loose,
Expand Down Expand Up @@ -251,6 +263,45 @@ export function isSortValid(input: string) {
}
}

/**
* Validation function for a query `hint`.
* Must be a string, array, or a document with only -1 or 1 as values.
* @public
*
* @return false if not valid, otherwise the cleaned-up hint.
*/
export function isHintValid(input: string) {
if (isEmpty(input)) {
return DEFAULT_HINT;
}

try {
const parsed = _parseHint(input);

if (_.isString(parsed)) {
return parsed;
}

if (_.isArray(parsed) || !_.isObject(parsed)) {
debug(
'Hint "%s" is invalid. Only strings or documents are allowed',
input,
);
return false;
}

if (!_.every(parsed, isValueOkForHint)) {
debug('Hint "%s" is invalid bc of its values', input);
return false;
}

return parsed;
} catch (e) {
debug('Hint "%s" is invalid', input, e);
return false;
}
}

/**
* Validation function for a query `maxTimeMS`. Must be digits only.
* @public
Expand Down Expand Up @@ -299,6 +350,7 @@ const validatorFunctions = {
isSkipValid,
isCollationValid,
isNumberValid,
isHintValid,
};

/** @public */
Expand Down Expand Up @@ -333,12 +385,12 @@ export default function queryParser(
export {
stringify,
toJSString,
QUERY_PROPERTIES,
DEFAULT_FILTER,
DEFAULT_SORT,
DEFAULT_LIMIT,
DEFAULT_SKIP,
DEFAULT_PROJECT,
DEFAULT_COLLATION,
DEFAULT_MAX_TIME_MS,
DEFAULT_HINT,
};
Loading