Skip to content

Commit bf40004

Browse files
authored
feat: Add requestComplexity.subqueryLimit option to limit subquery results (#10420)
1 parent df5e97f commit bf40004

File tree

5 files changed

+158
-0
lines changed

5 files changed

+158
-0
lines changed

spec/RequestComplexity.spec.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ describe('request complexity', () => {
162162
includeDepth: -1,
163163
includeCount: -1,
164164
subqueryDepth: -1,
165+
subqueryLimit: -1,
165166
queryDepth: -1,
166167
graphQLDepth: -1,
167168
graphQLFields: -1,
@@ -732,4 +733,123 @@ describe('request complexity', () => {
732733
});
733734
});
734735
});
736+
737+
describe('subquery result limit', () => {
738+
let config;
739+
const totalObjects = 5;
740+
const resultLimit = 3;
741+
742+
beforeEach(async () => {
743+
await reconfigureServer({
744+
requestComplexity: { subqueryLimit: resultLimit },
745+
});
746+
config = Config.get('test');
747+
// Create target objects
748+
const targets = [];
749+
for (let i = 0; i < totalObjects; i++) {
750+
const obj = new Parse.Object('Target');
751+
obj.set('value', `v${i}`);
752+
targets.push(obj);
753+
}
754+
await Parse.Object.saveAll(targets);
755+
// Create source objects, each pointing to a target
756+
const sources = [];
757+
for (let i = 0; i < totalObjects; i++) {
758+
const obj = new Parse.Object('Source');
759+
obj.set('ref', targets[i]);
760+
obj.set('value', targets[i].get('value'));
761+
sources.push(obj);
762+
}
763+
await Parse.Object.saveAll(sources);
764+
});
765+
766+
it('should limit $inQuery subquery results', async () => {
767+
const where = {
768+
ref: {
769+
$inQuery: { className: 'Target', where: {} },
770+
},
771+
};
772+
const result = await rest.find(config, auth.nobody(config), 'Source', where);
773+
expect(result.results.length).toBe(resultLimit);
774+
});
775+
776+
it('should limit $notInQuery subquery results', async () => {
777+
const where = {
778+
ref: {
779+
$notInQuery: { className: 'Target', where: {} },
780+
},
781+
};
782+
const result = await rest.find(config, auth.nobody(config), 'Source', where);
783+
// With limit, only `resultLimit` targets are excluded, so (totalObjects - resultLimit) sources remain
784+
expect(result.results.length).toBe(totalObjects - resultLimit);
785+
});
786+
787+
it('should limit $select subquery results', async () => {
788+
const where = {
789+
value: {
790+
$select: { query: { className: 'Target', where: {} }, key: 'value' },
791+
},
792+
};
793+
const result = await rest.find(config, auth.nobody(config), 'Source', where);
794+
expect(result.results.length).toBe(resultLimit);
795+
});
796+
797+
it('should limit $dontSelect subquery results', async () => {
798+
const where = {
799+
value: {
800+
$dontSelect: { query: { className: 'Target', where: {} }, key: 'value' },
801+
},
802+
};
803+
const result = await rest.find(config, auth.nobody(config), 'Source', where);
804+
expect(result.results.length).toBe(totalObjects - resultLimit);
805+
});
806+
807+
it('should allow unlimited subquery results with master key', async () => {
808+
const where = {
809+
ref: {
810+
$inQuery: { className: 'Target', where: {} },
811+
},
812+
};
813+
const result = await rest.find(config, auth.master(config), 'Source', where);
814+
expect(result.results.length).toBe(totalObjects);
815+
});
816+
817+
it('should allow unlimited subquery results with maintenance key', async () => {
818+
const where = {
819+
ref: {
820+
$inQuery: { className: 'Target', where: {} },
821+
},
822+
};
823+
const result = await rest.find(config, auth.maintenance(config), 'Source', where);
824+
expect(result.results.length).toBe(totalObjects);
825+
});
826+
827+
it('should allow unlimited subquery results when subqueryLimit is -1', async () => {
828+
await reconfigureServer({
829+
requestComplexity: { subqueryLimit: -1 },
830+
});
831+
config = Config.get('test');
832+
const where = {
833+
ref: {
834+
$inQuery: { className: 'Target', where: {} },
835+
},
836+
};
837+
const result = await rest.find(config, auth.nobody(config), 'Source', where);
838+
expect(result.results.length).toBe(totalObjects);
839+
});
840+
841+
it('should include subqueryLimit in config defaults', async () => {
842+
await reconfigureServer({});
843+
config = Config.get('test');
844+
expect(config.requestComplexity.subqueryLimit).toBe(-1);
845+
});
846+
847+
it('should accept subqueryLimit in config validation', async () => {
848+
await expectAsync(
849+
reconfigureServer({
850+
requestComplexity: { subqueryLimit: 100 },
851+
})
852+
).toBeResolved();
853+
});
854+
});
735855
});

src/Options/Definitions.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,12 @@ module.exports.RequestComplexityOptions = {
752752
action: parsers.numberParser('subqueryDepth'),
753753
default: -1,
754754
},
755+
subqueryLimit: {
756+
env: 'PARSE_SERVER_REQUEST_COMPLEXITY_SUBQUERY_LIMIT',
757+
help: 'Maximum number of results returned by a `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subquery. Set to `-1` to disable. Default is `-1`.',
758+
action: parsers.numberParser('subqueryLimit'),
759+
default: -1,
760+
},
755761
};
756762
module.exports.SecurityOptions = {
757763
checkGroups: {

src/Options/docs.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Options/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,9 @@ export interface RequestComplexityOptions {
460460
/* Maximum nesting depth of `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subqueries. Set to `-1` to disable. Default is `-1`.
461461
:DEFAULT: -1 */
462462
subqueryDepth: ?number;
463+
/* Maximum number of results returned by a `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subquery. Set to `-1` to disable. Default is `-1`.
464+
:DEFAULT: -1 */
465+
subqueryLimit: ?number;
463466
/* Maximum nesting depth of `$or`, `$and`, `$nor` query operators. Set to `-1` to disable. Default is `-1`.
464467
:DEFAULT: -1 */
465468
queryDepth: ?number;

src/RestQuery.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,13 @@ _UnsafeRestQuery.prototype.replaceInQuery = async function () {
563563
additionalOptions.readPreference = this.restOptions.readPreference;
564564
}
565565

566+
if (!this.auth.isMaster && !this.auth.isMaintenance) {
567+
const rc = this.config.requestComplexity;
568+
if (rc && rc.subqueryLimit > 0) {
569+
additionalOptions.limit = rc.subqueryLimit;
570+
}
571+
}
572+
566573
const childContext = { ...this.context, _subqueryDepth: (this.context._subqueryDepth || 0) + 1 };
567574
const subquery = await RestQuery({
568575
method: RestQuery.Method.find,
@@ -624,6 +631,13 @@ _UnsafeRestQuery.prototype.replaceNotInQuery = async function () {
624631
additionalOptions.readPreference = this.restOptions.readPreference;
625632
}
626633

634+
if (!this.auth.isMaster && !this.auth.isMaintenance) {
635+
const rc = this.config.requestComplexity;
636+
if (rc && rc.subqueryLimit > 0) {
637+
additionalOptions.limit = rc.subqueryLimit;
638+
}
639+
}
640+
627641
const childContext = { ...this.context, _subqueryDepth: (this.context._subqueryDepth || 0) + 1 };
628642
const subquery = await RestQuery({
629643
method: RestQuery.Method.find,
@@ -698,6 +712,13 @@ _UnsafeRestQuery.prototype.replaceSelect = async function () {
698712
additionalOptions.readPreference = this.restOptions.readPreference;
699713
}
700714

715+
if (!this.auth.isMaster && !this.auth.isMaintenance) {
716+
const rc = this.config.requestComplexity;
717+
if (rc && rc.subqueryLimit > 0) {
718+
additionalOptions.limit = rc.subqueryLimit;
719+
}
720+
}
721+
701722
const childContext = { ...this.context, _subqueryDepth: (this.context._subqueryDepth || 0) + 1 };
702723
const subquery = await RestQuery({
703724
method: RestQuery.Method.find,
@@ -762,6 +783,13 @@ _UnsafeRestQuery.prototype.replaceDontSelect = async function () {
762783
additionalOptions.readPreference = this.restOptions.readPreference;
763784
}
764785

786+
if (!this.auth.isMaster && !this.auth.isMaintenance) {
787+
const rc = this.config.requestComplexity;
788+
if (rc && rc.subqueryLimit > 0) {
789+
additionalOptions.limit = rc.subqueryLimit;
790+
}
791+
}
792+
765793
const childContext = { ...this.context, _subqueryDepth: (this.context._subqueryDepth || 0) + 1 };
766794
const subquery = await RestQuery({
767795
method: RestQuery.Method.find,

0 commit comments

Comments
 (0)