Skip to content

Commit c0e3672

Browse files
eduardboschdplewis
authored andcommitted
New query condition support to match all strings that starts with some other given strings (#3864)
* feat: Convert $regex value to RegExp object * feat: Add lib folder * Revert "feat: Add lib folder" This reverts commit c9dfbcb. * feat: Add $regex test in $all array * test: Test regex with $all only in MongoDB * Revert "test: Test regex with $all only in MongoDB" This reverts commit d7194c7. * feat: Add tests for containsAllStartingWith * feat: Add postgres support Thanks to @dplewis * feat: Check that all values in $all must be regex or none * test: Check that $all vaules must be regex or none * feat: Update tests to use only REST API * refactor: Move $all regex check to adapter * feat: Check for valid $all values in progres * refactor: Update function name * fix: Postgres $all values regex checking * fix: Check starts with as string * fix: Define contains all regex sql function * fix: Wrong value check * fix: Check valid data * fix: Check regex when there is only one value * fix: Constains all starting with string returns empty with bad params * fix: Pass correct regex value * feat: Add missing tests * feat: Add missing tests * feat: Add more tests * fix: Unify MongoDB and PostgreSQL functionality * fix: Lint checks * fix: Test broken $regex in $all list must be { $regex: "string" } * test for empty $all
1 parent 2c357df commit c0e3672

File tree

7 files changed

+395
-2
lines changed

7 files changed

+395
-2
lines changed

spec/MongoTransform.spec.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,41 @@ describe('parseObjectToMongoObjectForCreate', () => {
353353
expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z');
354354
done();
355355
});
356+
357+
it('$regex in $all list', (done) => {
358+
const input = {
359+
arrayField: {'$all': [{$regex: '^\\Qone\\E'}, {$regex: '^\\Qtwo\\E'}, {$regex: '^\\Qthree\\E'}]},
360+
};
361+
const outputValue = {
362+
arrayField: {'$all': [/^\Qone\E/, /^\Qtwo\E/, /^\Qthree\E/]},
363+
};
364+
365+
const output = transform.transformWhere(null, input);
366+
jequal(outputValue.arrayField, output.arrayField);
367+
done();
368+
});
369+
370+
it('$regex in $all list must be { $regex: "string" }', (done) => {
371+
const input = {
372+
arrayField: {'$all': [{$regex: 1}]},
373+
};
374+
375+
expect(() => {
376+
transform.transformWhere(null, input)
377+
}).toThrow();
378+
done();
379+
});
380+
381+
it('all values in $all must be $regex (start with string) or non $regex (start with string)', (done) => {
382+
const input = {
383+
arrayField: {'$all': [{$regex: '^\\Qone\\E'}, {$unknown: '^\\Qtwo\\E'}]},
384+
};
385+
386+
expect(() => {
387+
transform.transformWhere(null, input)
388+
}).toThrow();
389+
done();
390+
});
356391
});
357392

358393
describe('transformUpdate', () => {

spec/ParseQuery.spec.js

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,251 @@ describe('Parse.Query testing', () => {
509509
});
510510
});
511511

512+
it('containsAllStartingWith should match all strings that starts with string', (done) => {
513+
514+
const object = new Parse.Object('Object');
515+
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);
516+
const object2 = new Parse.Object('Object');
517+
object2.set('strings', ['the', 'brown', 'fox', 'jumps']);
518+
const object3 = new Parse.Object('Object');
519+
object3.set('strings', ['over', 'the', 'lazy', 'dog']);
520+
521+
const objectList = [object, object2, object3];
522+
523+
Parse.Object.saveAll(objectList).then((results) => {
524+
equal(objectList.length, results.length);
525+
526+
return require('request-promise').get({
527+
url: Parse.serverURL + "/classes/Object",
528+
json: {
529+
where: {
530+
strings: {
531+
$all: [
532+
{$regex: '\^\\Qthe\\E'},
533+
{$regex: '\^\\Qfox\\E'},
534+
{$regex: '\^\\Qlazy\\E'}
535+
]
536+
}
537+
}
538+
},
539+
headers: {
540+
'X-Parse-Application-Id': Parse.applicationId,
541+
'X-Parse-Javascript-Key': Parse.javaScriptKey
542+
}
543+
})
544+
.then(function (results) {
545+
equal(results.results.length, 1);
546+
arrayContains(results.results, object);
547+
548+
return require('request-promise').get({
549+
url: Parse.serverURL + "/classes/Object",
550+
json: {
551+
where: {
552+
strings: {
553+
$all: [
554+
{$regex: '\^\\Qthe\\E'},
555+
{$regex: '\^\\Qlazy\\E'}
556+
]
557+
}
558+
}
559+
},
560+
headers: {
561+
'X-Parse-Application-Id': Parse.applicationId,
562+
'X-Parse-Javascript-Key': Parse.javaScriptKey
563+
}
564+
});
565+
})
566+
.then(function (results) {
567+
equal(results.results.length, 2);
568+
arrayContains(results.results, object);
569+
arrayContains(results.results, object3);
570+
571+
return require('request-promise').get({
572+
url: Parse.serverURL + "/classes/Object",
573+
json: {
574+
where: {
575+
strings: {
576+
$all: [
577+
{$regex: '\^\\Qhe\\E'},
578+
{$regex: '\^\\Qlazy\\E'}
579+
]
580+
}
581+
}
582+
},
583+
headers: {
584+
'X-Parse-Application-Id': Parse.applicationId,
585+
'X-Parse-Javascript-Key': Parse.javaScriptKey
586+
}
587+
});
588+
})
589+
.then(function (results) {
590+
equal(results.results.length, 0);
591+
592+
done();
593+
});
594+
});
595+
});
596+
597+
it('containsAllStartingWith values must be all of type starting with regex', (done) => {
598+
599+
const object = new Parse.Object('Object');
600+
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);
601+
602+
object.save().then(() => {
603+
equal(object.isNew(), false);
604+
605+
return require('request-promise').get({
606+
url: Parse.serverURL + "/classes/Object",
607+
json: {
608+
where: {
609+
strings: {
610+
$all: [
611+
{$regex: '\^\\Qthe\\E'},
612+
{$regex: '\^\\Qlazy\\E'},
613+
{$regex: '\^\\Qfox\\E'},
614+
{$unknown: /unknown/}
615+
]
616+
}
617+
}
618+
},
619+
headers: {
620+
'X-Parse-Application-Id': Parse.applicationId,
621+
'X-Parse-Javascript-Key': Parse.javaScriptKey
622+
}
623+
});
624+
})
625+
.then(function () {
626+
}, function () {
627+
done();
628+
});
629+
});
630+
631+
it('containsAllStartingWith empty array values should return empty results', (done) => {
632+
633+
const object = new Parse.Object('Object');
634+
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);
635+
636+
object.save().then(() => {
637+
equal(object.isNew(), false);
638+
639+
return require('request-promise').get({
640+
url: Parse.serverURL + "/classes/Object",
641+
json: {
642+
where: {
643+
strings: {
644+
$all: []
645+
}
646+
}
647+
},
648+
headers: {
649+
'X-Parse-Application-Id': Parse.applicationId,
650+
'X-Parse-Javascript-Key': Parse.javaScriptKey
651+
}
652+
});
653+
})
654+
.then(function (results) {
655+
equal(results.results.length, 0);
656+
done();
657+
}, function () {
658+
});
659+
});
660+
661+
it('containsAllStartingWith single empty value returns empty results', (done) => {
662+
663+
const object = new Parse.Object('Object');
664+
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);
665+
666+
object.save().then(() => {
667+
equal(object.isNew(), false);
668+
669+
return require('request-promise').get({
670+
url: Parse.serverURL + "/classes/Object",
671+
json: {
672+
where: {
673+
strings: {
674+
$all: [ {} ]
675+
}
676+
}
677+
},
678+
headers: {
679+
'X-Parse-Application-Id': Parse.applicationId,
680+
'X-Parse-Javascript-Key': Parse.javaScriptKey
681+
}
682+
});
683+
})
684+
.then(function (results) {
685+
equal(results.results.length, 0);
686+
done();
687+
}, function () {
688+
});
689+
});
690+
691+
it('containsAllStartingWith single regex value should return corresponding matching results', (done) => {
692+
693+
const object = new Parse.Object('Object');
694+
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);
695+
const object2 = new Parse.Object('Object');
696+
object2.set('strings', ['the', 'brown', 'fox', 'jumps']);
697+
const object3 = new Parse.Object('Object');
698+
object3.set('strings', ['over', 'the', 'lazy', 'dog']);
699+
700+
const objectList = [object, object2, object3];
701+
702+
Parse.Object.saveAll(objectList).then((results) => {
703+
equal(objectList.length, results.length);
704+
705+
return require('request-promise').get({
706+
url: Parse.serverURL + "/classes/Object",
707+
json: {
708+
where: {
709+
strings: {
710+
$all: [ {$regex: '\^\\Qlazy\\E'} ]
711+
}
712+
}
713+
},
714+
headers: {
715+
'X-Parse-Application-Id': Parse.applicationId,
716+
'X-Parse-Javascript-Key': Parse.javaScriptKey
717+
}
718+
});
719+
})
720+
.then(function (results) {
721+
equal(results.results.length, 2);
722+
done();
723+
}, function () {
724+
});
725+
});
726+
727+
it('containsAllStartingWith single invalid regex returns empty results', (done) => {
728+
729+
const object = new Parse.Object('Object');
730+
object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']);
731+
732+
object.save().then(() => {
733+
equal(object.isNew(), false);
734+
735+
return require('request-promise').get({
736+
url: Parse.serverURL + "/classes/Object",
737+
json: {
738+
where: {
739+
strings: {
740+
$all: [ {$unknown: '\^\\Qlazy\\E'} ]
741+
}
742+
}
743+
},
744+
headers: {
745+
'X-Parse-Application-Id': Parse.applicationId,
746+
'X-Parse-Javascript-Key': Parse.javaScriptKey
747+
}
748+
});
749+
})
750+
.then(function (results) {
751+
equal(results.results.length, 0);
752+
done();
753+
}, function () {
754+
});
755+
});
756+
512757
const BoxedNumber = Parse.Object.extend({
513758
className: "BoxedNumber"
514759
});

src/Adapters/Storage/Mongo/MongoTransform.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,44 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
123123
return {key, value};
124124
}
125125

126+
const isRegex = value => {
127+
return value && (value instanceof RegExp)
128+
}
129+
130+
const isStartsWithRegex = value => {
131+
if (!isRegex(value)) {
132+
return false;
133+
}
134+
135+
const matches = value.toString().match(/\/\^\\Q.*\\E\//);
136+
return !!matches;
137+
}
138+
139+
const isAllValuesRegexOrNone = values => {
140+
if (!values || !Array.isArray(values) || values.length === 0) {
141+
return true;
142+
}
143+
144+
const firstValuesIsRegex = isStartsWithRegex(values[0]);
145+
if (values.length === 1) {
146+
return firstValuesIsRegex;
147+
}
148+
149+
for (let i = 1, length = values.length; i < length; ++i) {
150+
if (firstValuesIsRegex !== isStartsWithRegex(values[i])) {
151+
return false;
152+
}
153+
}
154+
155+
return true;
156+
}
157+
158+
const isAnyValueRegex = values => {
159+
return values.some(function (value) {
160+
return isRegex(value);
161+
});
162+
}
163+
126164
const transformInteriorValue = restValue => {
127165
if (restValue !== null && typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) {
128166
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
@@ -469,6 +507,8 @@ const transformInteriorAtom = (atom) => {
469507
return DateCoder.JSONToDatabase(atom);
470508
} else if (BytesCoder.isValidJSON(atom)) {
471509
return BytesCoder.JSONToDatabase(atom);
510+
} else if (typeof atom === 'object' && atom && atom.$regex !== undefined) {
511+
return new RegExp(atom.$regex);
472512
} else {
473513
return atom;
474514
}
@@ -740,6 +780,13 @@ function transformConstraint(constraint, field) {
740780
'bad ' + key + ' value');
741781
}
742782
answer[key] = arr.map(transformInteriorAtom);
783+
784+
const values = answer[key];
785+
if (isAnyValueRegex(values) && !isAllValuesRegexOrNone(values)) {
786+
throw new Parse.Error(Parse.Error.INVALID_JSON, 'All $all values must be of regex type or none: '
787+
+ values);
788+
}
789+
743790
break;
744791
}
745792
case '$regex':

0 commit comments

Comments
 (0)