Skip to content

New query condition support to match all strings that starts with some other given strings #3864

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0379019
feat: Convert $regex value to RegExp object
May 24, 2017
810b7ec
feat: Add lib folder
May 24, 2017
77580a0
Revert "feat: Add lib folder"
May 26, 2017
e209087
feat: Add $regex test in $all array
May 26, 2017
d7194c7
test: Test regex with $all only in MongoDB
May 28, 2017
59a57f7
Revert "test: Test regex with $all only in MongoDB"
May 28, 2017
132b0d6
feat: Add tests for containsAllStartingWith
Jun 15, 2017
ee84d53
feat: Add postgres support
Jul 7, 2017
52a394e
feat: Check that all values in $all must be regex or none
Jul 7, 2017
3432fc8
test: Check that $all vaules must be regex or none
Jul 7, 2017
81ecf4d
feat: Update tests to use only REST API
Jul 8, 2017
a891a0c
refactor: Move $all regex check to adapter
Jul 8, 2017
0dcdf83
feat: Check for valid $all values in progres
Jul 8, 2017
d6763d3
refactor: Update function name
Jul 8, 2017
556787b
fix: Postgres $all values regex checking
Jul 8, 2017
7ca8128
fix: Check starts with as string
Jul 8, 2017
b88d2e7
fix: Define contains all regex sql function
Jul 9, 2017
bbb7e67
fix: Wrong value check
Jul 9, 2017
c0e9a9b
Merge commit '8ec07b83d0b74b00153b7b414aa73d7247c55f85' into feat/con…
Jan 11, 2018
4fe6cb8
Merge commit '550b69e271f2d754b7ff774ffa66b2673bdb14a0' into feat/con…
Jan 30, 2018
c36854b
fix: Check valid data
Feb 4, 2018
1c54ede
fix: Check regex when there is only one value
Feb 4, 2018
c8fb446
fix: Constains all starting with string returns empty with bad params
Feb 4, 2018
d6b8a74
fix: Pass correct regex value
Feb 4, 2018
80772cf
feat: Add missing tests
Feb 16, 2018
4a75023
feat: Add missing tests
eduardbosch Feb 16, 2018
efc43ed
feat: Add more tests
eduardbosch Feb 16, 2018
cc0ed3f
fix: Unify MongoDB and PostgreSQL functionality
eduardbosch May 5, 2018
6381fdd
Merge commit '3acb3e7a9b19a7b4e43eae1fd8ad84d0a1b3c53e' into feat/con…
eduardbosch May 5, 2018
79f1f32
fix: Lint checks
eduardbosch May 5, 2018
c14ffd0
Merge commit 'bb1641419fb3708ac975ad7959dacb7318c65717' into feat/con…
eduardbosch May 5, 2018
da76c18
fix: Test broken
eduardbosch May 5, 2018
07a4d69
test for empty $all
dplewis May 16, 2018
917ec15
Merge branch 'master' into feat/contains-all-starting-with
dplewis May 16, 2018
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
35 changes: 35 additions & 0 deletions spec/MongoTransform.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,41 @@ describe('parseObjectToMongoObjectForCreate', () => {
expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z');
done();
});

it('$regex in $all list', (done) => {
const input = {
arrayField: {'$all': [{$regex: '^\\Qone\\E'}, {$regex: '^\\Qtwo\\E'}, {$regex: '^\\Qthree\\E'}]},
};
const outputValue = {
arrayField: {'$all': [/^\Qone\E/, /^\Qtwo\E/, /^\Qthree\E/]},
};

const output = transform.transformWhere(null, input);
jequal(outputValue.arrayField, output.arrayField);
done();
});

it('$regex in $all list must be { $regex: "string" }', (done) => {
const input = {
arrayField: {'$all': [{$regex: 1}]},
};

expect(() => {
transform.transformWhere(null, input)
}).toThrow();
done();
});

it('all values in $all must be $regex (start with string) or non $regex (start with string)', (done) => {
const input = {
arrayField: {'$all': [{$regex: '^\\Qone\\E'}, {$unknown: '^\\Qtwo\\E'}]},
};

expect(() => {
transform.transformWhere(null, input)
}).toThrow();
done();
});
});

describe('transformUpdate', () => {
Expand Down
245 changes: 245 additions & 0 deletions spec/ParseQuery.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,251 @@ describe('Parse.Query testing', () => {
});
});

it('containsAllStartingWith should match all strings that starts with string', (done) => {

const object = new Parse.Object('Object');
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);
const object2 = new Parse.Object('Object');
object2.set('strings', ['the', 'brown', 'fox', 'jumps']);
const object3 = new Parse.Object('Object');
object3.set('strings', ['over', 'the', 'lazy', 'dog']);

const objectList = [object, object2, object3];

Parse.Object.saveAll(objectList).then((results) => {
equal(objectList.length, results.length);

return require('request-promise').get({
url: Parse.serverURL + "/classes/Object",
json: {
where: {
strings: {
$all: [
{$regex: '\^\\Qthe\\E'},
{$regex: '\^\\Qfox\\E'},
{$regex: '\^\\Qlazy\\E'}
]
}
}
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Javascript-Key': Parse.javaScriptKey
}
})
.then(function (results) {
equal(results.results.length, 1);
arrayContains(results.results, object);

return require('request-promise').get({
url: Parse.serverURL + "/classes/Object",
json: {
where: {
strings: {
$all: [
{$regex: '\^\\Qthe\\E'},
{$regex: '\^\\Qlazy\\E'}
]
}
}
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Javascript-Key': Parse.javaScriptKey
}
});
})
.then(function (results) {
equal(results.results.length, 2);
arrayContains(results.results, object);
arrayContains(results.results, object3);

return require('request-promise').get({
url: Parse.serverURL + "/classes/Object",
json: {
where: {
strings: {
$all: [
{$regex: '\^\\Qhe\\E'},
{$regex: '\^\\Qlazy\\E'}
]
}
}
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Javascript-Key': Parse.javaScriptKey
}
});
})
.then(function (results) {
equal(results.results.length, 0);

done();
});
});
});

it('containsAllStartingWith values must be all of type starting with regex', (done) => {

const object = new Parse.Object('Object');
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);

object.save().then(() => {
equal(object.isNew(), false);

return require('request-promise').get({
url: Parse.serverURL + "/classes/Object",
json: {
where: {
strings: {
$all: [
{$regex: '\^\\Qthe\\E'},
{$regex: '\^\\Qlazy\\E'},
{$regex: '\^\\Qfox\\E'},
{$unknown: /unknown/}
]
}
}
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Javascript-Key': Parse.javaScriptKey
}
});
})
.then(function () {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eduardbosch I figured they would return different values for both databases. Some of your tests are missing .then() return values. If you add the return values for PG I can look at the script I wrote to see if I can get similar functionality to mongo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dplewis I don't understand. Some miss .then() return values because they throw an error, so that function is never called.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry missed that. Looking at your test, it should be easy to make PG throw an error instead of returning zero results for invalid $all.

}, function () {
done();
});
});

it('containsAllStartingWith empty array values should return empty results', (done) => {

const object = new Parse.Object('Object');
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);

object.save().then(() => {
equal(object.isNew(), false);

return require('request-promise').get({
url: Parse.serverURL + "/classes/Object",
json: {
where: {
strings: {
$all: []
}
}
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Javascript-Key': Parse.javaScriptKey
}
});
})
.then(function (results) {
equal(results.results.length, 0);
done();
}, function () {
});
});

it('containsAllStartingWith single empty value returns empty results', (done) => {

const object = new Parse.Object('Object');
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);

object.save().then(() => {
equal(object.isNew(), false);

return require('request-promise').get({
url: Parse.serverURL + "/classes/Object",
json: {
where: {
strings: {
$all: [ {} ]
}
}
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Javascript-Key': Parse.javaScriptKey
}
});
})
.then(function (results) {
equal(results.results.length, 0);
done();
}, function () {
});
});

it('containsAllStartingWith single regex value should return corresponding matching results', (done) => {

const object = new Parse.Object('Object');
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);
const object2 = new Parse.Object('Object');
object2.set('strings', ['the', 'brown', 'fox', 'jumps']);
const object3 = new Parse.Object('Object');
object3.set('strings', ['over', 'the', 'lazy', 'dog']);

const objectList = [object, object2, object3];

Parse.Object.saveAll(objectList).then((results) => {
equal(objectList.length, results.length);

return require('request-promise').get({
url: Parse.serverURL + "/classes/Object",
json: {
where: {
strings: {
$all: [ {$regex: '\^\\Qlazy\\E'} ]
}
}
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Javascript-Key': Parse.javaScriptKey
}
});
})
.then(function (results) {
equal(results.results.length, 2);
done();
}, function () {
});
});

it('containsAllStartingWith single invalid regex returns empty results', (done) => {

const object = new Parse.Object('Object');
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);

object.save().then(() => {
equal(object.isNew(), false);

return require('request-promise').get({
url: Parse.serverURL + "/classes/Object",
json: {
where: {
strings: {
$all: [ {$unknown: '\^\\Qlazy\\E'} ]
}
}
},
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Javascript-Key': Parse.javaScriptKey
}
});
})
.then(function (results) {
equal(results.results.length, 0);
done();
}, function () {
});
});

const BoxedNumber = Parse.Object.extend({
className: "BoxedNumber"
});
Expand Down
47 changes: 47 additions & 0 deletions src/Adapters/Storage/Mongo/MongoTransform.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,44 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
return {key, value};
}

const isRegex = value => {
return value && (value instanceof RegExp)
}

const isStartsWithRegex = value => {
if (!isRegex(value)) {
return false;
}

const matches = value.toString().match(/\/\^\\Q.*\\E\//);
return !!matches;
}

const isAllValuesRegexOrNone = values => {
if (!values || !Array.isArray(values) || values.length === 0) {
return true;
}

const firstValuesIsRegex = isStartsWithRegex(values[0]);
if (values.length === 1) {
return firstValuesIsRegex;
}

for (let i = 1, length = values.length; i < length; ++i) {
if (firstValuesIsRegex !== isStartsWithRegex(values[i])) {
return false;
}
}

return true;
}

const isAnyValueRegex = values => {
return values.some(function (value) {
return isRegex(value);
});
}

const transformInteriorValue = restValue => {
if (restValue !== null && typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) {
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
Expand Down Expand Up @@ -469,6 +507,8 @@ const transformInteriorAtom = (atom) => {
return DateCoder.JSONToDatabase(atom);
} else if (BytesCoder.isValidJSON(atom)) {
return BytesCoder.JSONToDatabase(atom);
} else if (typeof atom === 'object' && atom && atom.$regex !== undefined) {
return new RegExp(atom.$regex);
} else {
return atom;
}
Expand Down Expand Up @@ -740,6 +780,13 @@ function transformConstraint(constraint, field) {
'bad ' + key + ' value');
}
answer[key] = arr.map(transformInteriorAtom);

const values = answer[key];
if (isAnyValueRegex(values) && !isAllValuesRegexOrNone(values)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'All $all values must be of regex type or none: '
+ values);
}

break;
}
case '$regex':
Expand Down
Loading