diff --git a/src/database/run-ast.ts b/src/database/run-ast.ts index b43ee7a1c0..f5a55a7bed 100644 --- a/src/database/run-ast.ts +++ b/src/database/run-ast.ts @@ -39,6 +39,10 @@ export default async function runAST(ast: AST, query = ast.query) { let dbQuery = database.select([...toplevelFields, ...tempFields]).from(ast.name); + // Query defaults + query.limit = query.limit || 100; + query.sort = query.sort || [{ column: primaryKeyField, order: 'asc' }]; + if (query.filter) { applyFilter(dbQuery, query.filter); } @@ -88,6 +92,7 @@ export default async function runAST(ast: AST, query = ast.query) { let batchQuery: Query = {}; let tempField: string = null; + let tempLimit: number = null; if (m2o) { // Make sure we always fetch the nested items primary key field to ensure we have the key to match the item by @@ -131,6 +136,17 @@ export default async function runAST(ast: AST, query = ast.query) { }, }, }; + + /** + * The nested queries are done with a WHERE m2o IN (pk, pk, pk) query. We have to remove + * LIMIT from that equation to ensure we limit `n` items _per parent record_ instead of + * `n` items in total. This limit will then be re-applied in the stitching process + * down below + */ + if (batchQuery.limit) { + tempLimit = batchQuery.limit; + delete batchQuery.limit; + } } const nestedResults = await runAST(batch, batchQuery); @@ -157,26 +173,32 @@ export default async function runAST(ast: AST, query = ast.query) { } // o2m + let resultsForCurrentRecord = nestedResults + .filter((nestedRecord) => { + return ( + nestedRecord[batch.relation.field_many] === + record[batch.relation.primary_one] || + // In case of nested object: + nestedRecord[batch.relation.field_many]?.[batch.relation.primary_many] === + record[batch.relation.primary_one] + ); + }) + .map((nestedRecord) => { + if (tempField) { + delete nestedRecord[tempField]; + } + + return nestedRecord; + }); + + // Reapply LIMIT query on a per-record basis + if (tempLimit) { + resultsForCurrentRecord = resultsForCurrentRecord.slice(0, tempLimit); + } + const newRecord = { ...record, - [batch.fieldKey]: nestedResults - .filter((nestedRecord) => { - /** - * @todo - * pull the name ID from somewhere real - */ - return ( - nestedRecord[batch.relation.field_many] === record.id || - nestedRecord[batch.relation.field_many]?.id === record.id - ); - }) - .map((nestedRecord) => { - if (tempField) { - delete nestedRecord[tempField]; - } - - return nestedRecord; - }), + [batch.fieldKey]: resultsForCurrentRecord, }; return newRecord; diff --git a/src/middleware/sanitize-query.ts b/src/middleware/sanitize-query.ts index fd1408dd46..fa8023c432 100644 --- a/src/middleware/sanitize-query.ts +++ b/src/middleware/sanitize-query.ts @@ -13,9 +13,12 @@ const sanitizeQuery: RequestHandler = (req, res, next) => { const query: Query = { fields: sanitizeFields(req.query.fields) || ['*'], - limit: sanitizeLimit(req.query.limit) || 100, }; + if (req.query.limit) { + query.limit = sanitizeLimit(req.query.limit); + } + if (req.query.sort) { query.sort = sanitizeSort(req.query.sort); } diff --git a/src/services/permissions.ts b/src/services/permissions.ts index 37f8571fd9..d769c1eb01 100644 --- a/src/services/permissions.ts +++ b/src/services/permissions.ts @@ -148,6 +148,17 @@ export const processAST = async (ast: AST, role: string | null): Promise => }, }; + if (permissions.limit && ast.query.limit > permissions.limit) { + throw new ForbiddenException( + `You can't read more than ${permissions.limit} items at a time.` + ); + } + + // Default to the permissions limit if limit hasn't been set + if (permissions.limit && !ast.query.limit) { + ast.query.limit = permissions.limit; + } + ast.children = ast.children.map(applyFilters) as (NestedCollectionAST | FieldAST)[]; }