Skip to content

Commit 74fe94f

Browse files
authored
feat(query-parser): add hint parsing COMPASS-9373 (#595)
1 parent 525b215 commit 74fe94f

File tree

2 files changed

+102
-2
lines changed

2 files changed

+102
-2
lines changed

packages/query-parser/src/index.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import _debug from 'debug';
66
import {
77
isCollationValid,
88
isFilterValid,
9+
isHintValid,
910
isLimitValid,
1011
isMaxTimeMSValid,
1112
isProjectValid,
@@ -719,6 +720,53 @@ e s`,
719720
});
720721
});
721722

723+
describe('hint', function () {
724+
it('should default to null', function () {
725+
assert.equal(isHintValid(''), null);
726+
assert.equal(isHintValid(' '), null);
727+
assert.equal(isHintValid('{}'), null);
728+
});
729+
730+
it('should parse hint objects', function () {
731+
assert.deepEqual(isHintValid('{_id: 1}'), { _id: 1 });
732+
assert.deepEqual(isHintValid('{_id: -1}'), { _id: -1 });
733+
assert.deepEqual(isHintValid('{pineapple: 1, age: -1}'), {
734+
pineapple: 1,
735+
age: -1,
736+
});
737+
});
738+
739+
it('should accept string index names', function () {
740+
assert.deepEqual(isHintValid('"pineapple"'), 'pineapple');
741+
assert.deepEqual(isHintValid("'pineapple'"), 'pineapple');
742+
});
743+
744+
it('should not accept arrays', function () {
745+
assert.deepEqual(isHintValid('["pineappleOne", "pineappleTwo"]'), false);
746+
});
747+
748+
it('should accept docs with numeric values', function () {
749+
assert.deepEqual(isHintValid('{pineapple: 0}'), { pineapple: 0 });
750+
assert.deepEqual(isHintValid('{pineapple: -1}'), { pineapple: -1 });
751+
assert.deepEqual(isHintValid('{pineapple: NaN}'), { pineapple: NaN });
752+
assert.deepEqual(isHintValid('{pineapple: 2}'), { pineapple: 2 });
753+
});
754+
755+
it('should reject broken objects', function () {
756+
assert.equal(isHintValid('{not_pineapple'), false);
757+
assert.equal(isHintValid('invalid pineapple: }'), false);
758+
assert.equal(isHintValid('{invalid pineapple}'), false);
759+
assert.equal(isHintValid('{invalid pineapple: }'), false);
760+
});
761+
762+
it('should reject non-string/non-object hint values', function () {
763+
assert.equal(isHintValid('true'), false);
764+
assert.equal(isHintValid('pineapple'), false);
765+
assert.equal(isHintValid('123'), false);
766+
assert.equal(isHintValid('null'), false);
767+
});
768+
});
769+
722770
describe('sort', function () {
723771
it('should default to null', function () {
724772
assert.equal(parseSort(''), null);

packages/query-parser/src/index.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const DEFAULT_COLLATION = null;
2525
/** @public */
2626
const DEFAULT_MAX_TIME_MS = 60000; // 1 minute in ms
2727
/** @public */
28-
const QUERY_PROPERTIES = ['filter', 'project', 'sort', 'skip', 'limit'];
28+
const DEFAULT_HINT = null;
2929

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

65+
function isValueOkForHint() {
66+
/**
67+
* Prior to MongoDB 7.0, hint would accept invalid values, like NaN.
68+
* So we're on the looser side of validation here.
69+
*/
70+
return true;
71+
}
72+
73+
function _parseHint(input: string) {
74+
return parseShellStringToEJSON(input, { mode: ParseMode.Loose });
75+
}
76+
6577
function _parseFilter(input: string) {
6678
return parseShellStringToEJSON(input, {
6779
mode: ParseMode.Loose,
@@ -251,6 +263,45 @@ export function isSortValid(input: string) {
251263
}
252264
}
253265

266+
/**
267+
* Validation function for a query `hint`.
268+
* Must be a string, array, or a document with only -1 or 1 as values.
269+
* @public
270+
*
271+
* @return false if not valid, otherwise the cleaned-up hint.
272+
*/
273+
export function isHintValid(input: string) {
274+
if (isEmpty(input)) {
275+
return DEFAULT_HINT;
276+
}
277+
278+
try {
279+
const parsed = _parseHint(input);
280+
281+
if (_.isString(parsed)) {
282+
return parsed;
283+
}
284+
285+
if (_.isArray(parsed) || !_.isObject(parsed)) {
286+
debug(
287+
'Hint "%s" is invalid. Only strings or documents are allowed',
288+
input,
289+
);
290+
return false;
291+
}
292+
293+
if (!_.every(parsed, isValueOkForHint)) {
294+
debug('Hint "%s" is invalid bc of its values', input);
295+
return false;
296+
}
297+
298+
return parsed;
299+
} catch (e) {
300+
debug('Hint "%s" is invalid', input, e);
301+
return false;
302+
}
303+
}
304+
254305
/**
255306
* Validation function for a query `maxTimeMS`. Must be digits only.
256307
* @public
@@ -299,6 +350,7 @@ const validatorFunctions = {
299350
isSkipValid,
300351
isCollationValid,
301352
isNumberValid,
353+
isHintValid,
302354
};
303355

304356
/** @public */
@@ -333,12 +385,12 @@ export default function queryParser(
333385
export {
334386
stringify,
335387
toJSString,
336-
QUERY_PROPERTIES,
337388
DEFAULT_FILTER,
338389
DEFAULT_SORT,
339390
DEFAULT_LIMIT,
340391
DEFAULT_SKIP,
341392
DEFAULT_PROJECT,
342393
DEFAULT_COLLATION,
343394
DEFAULT_MAX_TIME_MS,
395+
DEFAULT_HINT,
344396
};

0 commit comments

Comments
 (0)