mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
feat: Add requestComplexity.subqueryLimit option to limit subquery results (#10420)
This commit is contained in:
@@ -162,6 +162,7 @@ describe('request complexity', () => {
|
||||
includeDepth: -1,
|
||||
includeCount: -1,
|
||||
subqueryDepth: -1,
|
||||
subqueryLimit: -1,
|
||||
queryDepth: -1,
|
||||
graphQLDepth: -1,
|
||||
graphQLFields: -1,
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -144,6 +144,7 @@
|
||||
* @property {Number} includeDepth Maximum depth of include pointer chains (e.g. `a.b.c` = depth 3). Set to `-1` to disable. Default is `-1`.
|
||||
* @property {Number} queryDepth Maximum nesting depth of `$or`, `$and`, `$nor` query operators. Set to `-1` to disable. Default is `-1`.
|
||||
* @property {Number} subqueryDepth Maximum nesting depth of `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subqueries. Set to `-1` to disable. Default is `-1`.
|
||||
* @property {Number} subqueryLimit Maximum number of results returned by a `$inQuery`, `$notInQuery`, `$select`, `$dontSelect` subquery. Set to `-1` to disable. Default is `-1`.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user