Skip to content

fix: Fixed issue where containedIn and matchesQuery does not work with nested objects #9737

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
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
71 changes: 71 additions & 0 deletions spec/MongoStorageAdapter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,77 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(undefined);
});


/**
* If we use equalTo to comparse the nested pointer it works
* But it does not work with contained in or matchesQuery
*/
it('Parse query works with nested objects if equal to is used', async () => {
const child = new Parse.Object('Child')
child.set('key','value')
await child.save();

const parent = new Parse.Object('Parent');
parent.set('some' ,{
nested : {
key : {
child
}
}
})
await parent.save();

const query1 = await new Parse.Query('Parent')
.equalTo('some.nested.key.child', child)
.find();

expect(query1.length).toEqual(1);
})

it('Parse query works when containedIn is used', async () => {
const child = new Parse.Object('Child')
child.set('key','value')
await child.save();

const parent = new Parse.Object('Parent');
parent.set('some' ,{
nested : {
key : {
child
}
}
})
await parent.save();

const query1 = await new Parse.Query('Parent')
.containedIn('some.nested.key.child', [child])
.find();

expect(query1.length).toEqual(1);
})

it('Parse query works when matchesQuery is used which in turn uses contained in', async () => {
const child = new Parse.Object('Child')
child.set('key','value')
await child.save();

const parent = new Parse.Object('Parent');
parent.set('some' ,{
nested : {
key : {
child
}
}
})
await parent.save();

const query1 = await new Parse.Query('Parent')
.matchesQuery('some.nested.key.child', new Parse.Query('Child').equalTo('key','value'))
.find();

expect(query1.length).toEqual(1);
})

it_only_mongodb_version('<5.1 || >=6')('should use index for caseInsensitive query', async () => {
const user = new Parse.User();
user.set('username', 'Bugs');
Expand Down
91 changes: 47 additions & 44 deletions src/Adapters/Storage/Mongo/MongoTransform.js
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@
}

// Handle query constraints
const transformedConstraint = transformConstraint(value, field, count);
const transformedConstraint = transformConstraint(value, field, key, count);
if (transformedConstraint !== CannotTransform) {
if (transformedConstraint.$text) {
return { key: '$text', value: transformedConstraint.$text };
Expand Down Expand Up @@ -651,12 +651,15 @@
// If it is not a valid constraint but it could be a valid something
// else, return CannotTransform.
// inArray is whether this is an array field.
function transformConstraint(constraint, field, count = false) {
function transformConstraint(constraint, field, key, count = false) {
const inArray = field && field.type && field.type === 'Array';
// Check wether the given key has `.`
const isNestedKey = key.indexOf('.') > -1;
if (typeof constraint !== 'object' || !constraint) {
return CannotTransform;
}
const transformFunction = inArray ? transformInteriorAtom : transformTopLevelAtom;
// For inArray or nested key, we need to transform the interior atom
const transformFunction = (inArray || isNestedKey) ? transformInteriorAtom : transformTopLevelAtom;
const transformer = atom => {
const result = transformFunction(atom, field);
if (result === CannotTransform) {
Expand All @@ -668,18 +671,18 @@
// This is a hack so that:
// $regex is handled before $options
// $nearSphere is handled before $maxDistance
var keys = Object.keys(constraint).sort().reverse();
var constraintKeys = Object.keys(constraint).sort().reverse();
var answer = {};
for (var key of keys) {
switch (key) {
for (var constraintKey of constraintKeys) {
switch (constraintKey) {
case '$lt':
case '$lte':
case '$gt':
case '$gte':
case '$exists':
case '$ne':
case '$eq': {
const val = constraint[key];
const val = constraint[constraintKey];
if (val && typeof val === 'object' && val.$relativeTime) {
if (field && field.type !== 'Date') {
throw new Parse.Error(
Expand All @@ -688,7 +691,7 @@
);
}

switch (key) {
switch (constraintKey) {
case '$exists':
case '$ne':
case '$eq':
Expand All @@ -700,28 +703,28 @@

const parserResult = Utils.relativeTimeToDate(val.$relativeTime);
if (parserResult.status === 'success') {
answer[key] = parserResult.result;
answer[constraintKey] = parserResult.result;
break;
}

log.info('Error while parsing relative date', parserResult);
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $relativeTime (${key}) value. ${parserResult.info}`
`bad $relativeTime (${constraintKey}) value. ${parserResult.info}`
);
}

answer[key] = transformer(val);
answer[constraintKey] = transformer(val);
break;
}

case '$in':
case '$nin': {
const arr = constraint[key];
const arr = constraint[constraintKey];
if (!(arr instanceof Array)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + key + ' value');
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + constraintKey + ' value');
}
answer[key] = _.flatMap(arr, value => {
answer[constraintKey] = _.flatMap(arr, value => {
return (atom => {
if (Array.isArray(atom)) {
return value.map(transformer);
Expand All @@ -733,13 +736,13 @@
break;
}
case '$all': {
const arr = constraint[key];
const arr = constraint[constraintKey];
if (!(arr instanceof Array)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + key + ' value');
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + constraintKey + ' value');

Check warning on line 741 in src/Adapters/Storage/Mongo/MongoTransform.js

View check run for this annotation

Codecov / codecov/patch

src/Adapters/Storage/Mongo/MongoTransform.js#L741

Added line #L741 was not covered by tests
}
answer[key] = arr.map(transformInteriorAtom);
answer[constraintKey] = arr.map(transformInteriorAtom);

const values = answer[key];
const values = answer[constraintKey];
if (isAnyValueRegex(values) && !isAllValuesRegexOrNone(values)) {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
Expand All @@ -750,15 +753,15 @@
break;
}
case '$regex':
var s = constraint[key];
var s = constraint[constraintKey];
if (typeof s !== 'string') {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad regex: ' + s);
}
answer[key] = s;
answer[constraintKey] = s;
break;

case '$containedBy': {
const arr = constraint[key];
const arr = constraint[constraintKey];
if (!(arr instanceof Array)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, `bad $containedBy: should be an array`);
}
Expand All @@ -768,87 +771,87 @@
break;
}
case '$options':
answer[key] = constraint[key];
answer[constraintKey] = constraint[constraintKey];
break;

case '$text': {
const search = constraint[key].$search;
const search = constraint[constraintKey].$search;
if (typeof search !== 'object') {
throw new Parse.Error(Parse.Error.INVALID_JSON, `bad $text: $search, should be object`);
}
if (!search.$term || typeof search.$term !== 'string') {
throw new Parse.Error(Parse.Error.INVALID_JSON, `bad $text: $term, should be string`);
} else {
answer[key] = {
answer[constraintKey] = {
$search: search.$term,
};
}
if (search.$language && typeof search.$language !== 'string') {
throw new Parse.Error(Parse.Error.INVALID_JSON, `bad $text: $language, should be string`);
} else if (search.$language) {
answer[key].$language = search.$language;
answer[constraintKey].$language = search.$language;
}
if (search.$caseSensitive && typeof search.$caseSensitive !== 'boolean') {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $text: $caseSensitive, should be boolean`
);
} else if (search.$caseSensitive) {
answer[key].$caseSensitive = search.$caseSensitive;
answer[constraintKey].$caseSensitive = search.$caseSensitive;
}
if (search.$diacriticSensitive && typeof search.$diacriticSensitive !== 'boolean') {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
`bad $text: $diacriticSensitive, should be boolean`
);
} else if (search.$diacriticSensitive) {
answer[key].$diacriticSensitive = search.$diacriticSensitive;
answer[constraintKey].$diacriticSensitive = search.$diacriticSensitive;
}
break;
}
case '$nearSphere': {
const point = constraint[key];
const point = constraint[constraintKey];
if (count) {
answer.$geoWithin = {
$centerSphere: [[point.longitude, point.latitude], constraint.$maxDistance],
};
} else {
answer[key] = [point.longitude, point.latitude];
answer[constraintKey] = [point.longitude, point.latitude];
}
break;
}
case '$maxDistance': {
if (count) {
break;
}
answer[key] = constraint[key];
answer[constraintKey] = constraint[constraintKey];
break;
}
// The SDKs don't seem to use these but they are documented in the
// REST API docs.
case '$maxDistanceInRadians':
answer['$maxDistance'] = constraint[key];
answer['$maxDistance'] = constraint[constraintKey];

Check warning on line 833 in src/Adapters/Storage/Mongo/MongoTransform.js

View check run for this annotation

Codecov / codecov/patch

src/Adapters/Storage/Mongo/MongoTransform.js#L833

Added line #L833 was not covered by tests
break;
case '$maxDistanceInMiles':
answer['$maxDistance'] = constraint[key] / 3959;
answer['$maxDistance'] = constraint[constraintKey] / 3959;

Check warning on line 836 in src/Adapters/Storage/Mongo/MongoTransform.js

View check run for this annotation

Codecov / codecov/patch

src/Adapters/Storage/Mongo/MongoTransform.js#L836

Added line #L836 was not covered by tests
break;
case '$maxDistanceInKilometers':
answer['$maxDistance'] = constraint[key] / 6371;
answer['$maxDistance'] = constraint[constraintKey] / 6371;

Check warning on line 839 in src/Adapters/Storage/Mongo/MongoTransform.js

View check run for this annotation

Codecov / codecov/patch

src/Adapters/Storage/Mongo/MongoTransform.js#L839

Added line #L839 was not covered by tests
break;

case '$select':
case '$dontSelect':
throw new Parse.Error(
Parse.Error.COMMAND_UNAVAILABLE,
'the ' + key + ' constraint is not supported yet'
'the ' + constraintKey + ' constraint is not supported yet'
);

case '$within':
var box = constraint[key]['$box'];
var box = constraint[constraintKey]['$box'];
if (!box || box.length != 2) {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'malformatted $within arg');
}
answer[key] = {
answer[constraintKey] = {
$box: [
[box[0].longitude, box[0].latitude],
[box[1].longitude, box[1].latitude],
Expand All @@ -857,8 +860,8 @@
break;

case '$geoWithin': {
const polygon = constraint[key]['$polygon'];
const centerSphere = constraint[key]['$centerSphere'];
const polygon = constraint[constraintKey]['$polygon'];
const centerSphere = constraint[constraintKey]['$centerSphere'];
if (polygon !== undefined) {
let points;
if (typeof polygon === 'object' && polygon.__type === 'Polygon') {
Expand Down Expand Up @@ -895,7 +898,7 @@
}
return [point.longitude, point.latitude];
});
answer[key] = {
answer[constraintKey] = {
$polygon: points,
};
} else if (centerSphere !== undefined) {
Expand Down Expand Up @@ -924,14 +927,14 @@
'bad $geoWithin value; $centerSphere distance invalid'
);
}
answer[key] = {
answer[constraintKey] = {
$centerSphere: [[point.longitude, point.latitude], distance],
};
}
break;
}
case '$geoIntersects': {
const point = constraint[key]['$point'];
const point = constraint[constraintKey]['$point'];
if (!GeoPointCoder.isValidJSON(point)) {
throw new Parse.Error(
Parse.Error.INVALID_JSON,
Expand All @@ -940,7 +943,7 @@
} else {
Parse.GeoPoint._validate(point.latitude, point.longitude);
}
answer[key] = {
answer[constraintKey] = {
$geometry: {
type: 'Point',
coordinates: [point.longitude, point.latitude],
Expand All @@ -949,8 +952,8 @@
break;
}
default:
if (key.match(/^\$+/)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad constraint: ' + key);
if (constraintKey.match(/^\$+/)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad constraint: ' + constraintKey);
}
return CannotTransform;
}
Expand Down
Loading