Skip to content
Merged
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
120 changes: 120 additions & 0 deletions spec/RequestComplexity.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ describe('request complexity', () => {
includeDepth: -1,
includeCount: -1,
subqueryDepth: -1,
subqueryLimit: -1,
queryDepth: -1,
graphQLDepth: -1,
graphQLFields: -1,
Expand Down Expand Up @@ -732,4 +733,123 @@ describe('request complexity', () => {
});
});
});

describe('subquery result limit', () => {
let config;
const totalObjects = 5;
const resultLimit = 3;

beforeEach(async () => {
await reconfigureServer({
requestComplexity: { subqueryLimit: resultLimit },
});
config = Config.get('test');
// Create target objects
const targets = [];
for (let i = 0; i < totalObjects; i++) {
const obj = new Parse.Object('Target');
obj.set('value', `v${i}`);
targets.push(obj);
}
await Parse.Object.saveAll(targets);
// Create source objects, each pointing to a target
const sources = [];
for (let i = 0; i < totalObjects; i++) {
const obj = new Parse.Object('Source');
obj.set('ref', targets[i]);
obj.set('value', targets[i].get('value'));
sources.push(obj);
}
await Parse.Object.saveAll(sources);
});

it('should limit $inQuery subquery results', async () => {
const where = {
ref: {
$inQuery: { className: 'Target', where: {} },
},
};
const result = await rest.find(config, auth.nobody(config), 'Source', where);
expect(result.results.length).toBe(resultLimit);
});

it('should limit $notInQuery subquery results', async () => {
const where = {
ref: {
$notInQuery: { className: 'Target', where: {} },
},
};
const result = await rest.find(config, auth.nobody(config), 'Source', where);
// With limit, only `resultLimit` targets are excluded, so (totalObjects - resultLimit) sources remain
expect(result.results.length).toBe(totalObjects - resultLimit);
});

it('should limit $select subquery results', async () => {
const where = {
value: {
$select: { query: { className: 'Target', where: {} }, key: 'value' },
},
};
const result = await rest.find(config, auth.nobody(config), 'Source', where);
expect(result.results.length).toBe(resultLimit);
});

it('should limit $dontSelect subquery results', async () => {
const where = {
value: {
$dontSelect: { query: { className: 'Target', where: {} }, key: 'value' },
},
};
const result = await rest.find(config, auth.nobody(config), 'Source', where);
expect(result.results.length).toBe(totalObjects - resultLimit);
});

it('should allow unlimited subquery results with master key', async () => {
const where = {
ref: {
$inQuery: { className: 'Target', where: {} },
},
};
const result = await rest.find(config, auth.master(config), 'Source', where);
expect(result.results.length).toBe(totalObjects);
});

it('should allow unlimited subquery results with maintenance key', async () => {
const where = {
ref: {
$inQuery: { className: 'Target', where: {} },
},
};
const result = await rest.find(config, auth.maintenance(config), 'Source', where);
expect(result.results.length).toBe(totalObjects);
});

it('should allow unlimited subquery results when subqueryLimit is -1', async () => {
await reconfigureServer({
requestComplexity: { subqueryLimit: -1 },
});
config = Config.get('test');
const where = {
ref: {
$inQuery: { className: 'Target', where: {} },
},
};
const result = await rest.find(config, auth.nobody(config), 'Source', where);
expect(result.results.length).toBe(totalObjects);
});

it('should include subqueryLimit in config defaults', async () => {
await reconfigureServer({});
config = Config.get('test');
expect(config.requestComplexity.subqueryLimit).toBe(-1);
});

it('should accept subqueryLimit in config validation', async () => {
await expectAsync(
reconfigureServer({
requestComplexity: { subqueryLimit: 100 },
})
).toBeResolved();
});
});
});
6 changes: 6 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,12 @@ module.exports.RequestComplexityOptions = {
action: parsers.numberParser('subqueryDepth'),
default: -1,
},
subqueryLimit: {
env: 'PARSE_SERVER_REQUEST_COMPLEXITY_SUBQUERY_LIMIT',
help: 'Maximum number of results returned by a `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subquery. Set to `-1` to disable. Default is `-1`.',
action: parsers.numberParser('subqueryLimit'),
default: -1,
},
};
module.exports.SecurityOptions = {
checkGroups: {
Expand Down
1 change: 1 addition & 0 deletions src/Options/docs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,9 @@ export interface RequestComplexityOptions {
/* Maximum nesting depth of `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subqueries. Set to `-1` to disable. Default is `-1`.
:DEFAULT: -1 */
subqueryDepth: ?number;
/* Maximum number of results returned by a `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subquery. Set to `-1` to disable. Default is `-1`.
:DEFAULT: -1 */
subqueryLimit: ?number;
/* Maximum nesting depth of `$or`, `$and`, `$nor` query operators. Set to `-1` to disable. Default is `-1`.
:DEFAULT: -1 */
queryDepth: ?number;
Expand Down
28 changes: 28 additions & 0 deletions src/RestQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,13 @@ _UnsafeRestQuery.prototype.replaceInQuery = async function () {
additionalOptions.readPreference = this.restOptions.readPreference;
}

if (!this.auth.isMaster && !this.auth.isMaintenance) {
const rc = this.config.requestComplexity;
if (rc && rc.subqueryLimit > 0) {
additionalOptions.limit = rc.subqueryLimit;
}
}

const childContext = { ...this.context, _subqueryDepth: (this.context._subqueryDepth || 0) + 1 };
const subquery = await RestQuery({
method: RestQuery.Method.find,
Expand Down Expand Up @@ -624,6 +631,13 @@ _UnsafeRestQuery.prototype.replaceNotInQuery = async function () {
additionalOptions.readPreference = this.restOptions.readPreference;
}

if (!this.auth.isMaster && !this.auth.isMaintenance) {
const rc = this.config.requestComplexity;
if (rc && rc.subqueryLimit > 0) {
additionalOptions.limit = rc.subqueryLimit;
}
}

const childContext = { ...this.context, _subqueryDepth: (this.context._subqueryDepth || 0) + 1 };
const subquery = await RestQuery({
method: RestQuery.Method.find,
Expand Down Expand Up @@ -698,6 +712,13 @@ _UnsafeRestQuery.prototype.replaceSelect = async function () {
additionalOptions.readPreference = this.restOptions.readPreference;
}

if (!this.auth.isMaster && !this.auth.isMaintenance) {
const rc = this.config.requestComplexity;
if (rc && rc.subqueryLimit > 0) {
additionalOptions.limit = rc.subqueryLimit;
}
}

const childContext = { ...this.context, _subqueryDepth: (this.context._subqueryDepth || 0) + 1 };
const subquery = await RestQuery({
method: RestQuery.Method.find,
Expand Down Expand Up @@ -762,6 +783,13 @@ _UnsafeRestQuery.prototype.replaceDontSelect = async function () {
additionalOptions.readPreference = this.restOptions.readPreference;
}

if (!this.auth.isMaster && !this.auth.isMaintenance) {
const rc = this.config.requestComplexity;
if (rc && rc.subqueryLimit > 0) {
additionalOptions.limit = rc.subqueryLimit;
}
}

const childContext = { ...this.context, _subqueryDepth: (this.context._subqueryDepth || 0) + 1 };
const subquery = await RestQuery({
method: RestQuery.Method.find,
Expand Down
Loading