diff --git a/api/src/database/run-ast.ts b/api/src/database/run-ast.ts index 3dda776fdc..49087e2348 100644 --- a/api/src/database/run-ast.ts +++ b/api/src/database/run-ast.ts @@ -41,8 +41,8 @@ export default async function runAST(originalAST: AST, options?: RunASTOptions) // all nested items for all parent items at once. Because of this, we can't limit that query // to the "standard" item limit. Instead of _n_ nested items per parent item, it would mean // that there's _n_ items, which are then divided on the parent items. (no good) - if (isO2M(nestedAST) && typeof query.limit === 'number') { - tempLimit = query.limit; + if (isO2M(nestedAST) && typeof nestedAST.query.limit === 'number') { + tempLimit = nestedAST.query.limit; nestedAST.query.limit = -1; } diff --git a/api/src/middleware/sanitize-query.ts b/api/src/middleware/sanitize-query.ts index ee2858c5bd..64d32111cc 100644 --- a/api/src/middleware/sanitize-query.ts +++ b/api/src/middleware/sanitize-query.ts @@ -12,62 +12,84 @@ const sanitizeQuery: RequestHandler = (req, res, next) => { req.sanitizedQuery = {}; if (!req.query) return; - const query: Query = { - fields: sanitizeFields(req.query.fields) || ['*'], - }; + req.sanitizedQuery = sanitize( + { + fields: req.query.fields || '*', + ...req.query + }, + req.accountability || null + ); - if (req.query.limit !== undefined) { - const limit = sanitizeLimit(req.query.limit); + Object.freeze(req.sanitizedQuery); + + return next(); +}; + +function sanitize(rawQuery: Record, accountability: Accountability | null) { + const query: Query = {}; + + if (rawQuery.limit !== undefined) { + const limit = sanitizeLimit(rawQuery.limit); if (typeof limit === 'number') { query.limit = limit; } } - if (req.query.sort) { - query.sort = sanitizeSort(req.query.sort); + if (rawQuery.fields) { + query.fields = sanitizeFields(rawQuery.fields); } - if (req.query.filter) { - query.filter = sanitizeFilter(req.query.filter, req.accountability || null); + if (rawQuery.sort) { + query.sort = sanitizeSort(rawQuery.sort); } - if (req.query.limit == '-1') { + if (rawQuery.filter) { + query.filter = sanitizeFilter(rawQuery.filter, accountability || null); + } + + if (rawQuery.limit == '-1') { delete query.limit; } - if (req.query.offset) { - query.offset = sanitizeOffset(req.query.offset); + if (rawQuery.offset) { + query.offset = sanitizeOffset(rawQuery.offset); } - if (req.query.page) { - query.page = sanitizePage(req.query.page); + if (rawQuery.page) { + query.page = sanitizePage(rawQuery.page); } - if (req.query.single) { - query.single = sanitizeSingle(req.query.single); + if (rawQuery.single) { + query.single = sanitizeSingle(rawQuery.single); } - if (req.query.meta) { - query.meta = sanitizeMeta(req.query.meta); + if (rawQuery.meta) { + query.meta = sanitizeMeta(rawQuery.meta); } - if (req.query.search && typeof req.query.search === 'string') { - query.search = req.query.search; + if (rawQuery.search && typeof rawQuery.search === 'string') { + query.search = rawQuery.search; } if ( - req.query.export && - typeof req.query.export === 'string' && - ['json', 'csv'].includes(req.query.export) + rawQuery.export && + typeof rawQuery.export === 'string' && + ['json', 'csv'].includes(rawQuery.export) ) { - query.export = req.query.export as 'json' | 'csv'; + query.export = rawQuery.export as 'json' | 'csv'; } - req.sanitizedQuery = query; - Object.freeze(req.sanitizedQuery); - return next(); -}; + if (rawQuery.deep as Record) { + if (!query.deep) query.deep = {}; + + for (const [field, deepRawQuery] of Object.entries(rawQuery.deep)) { + query.deep[field] = sanitize(deepRawQuery as any, accountability); + } + } + + return query; +} export default sanitizeQuery; diff --git a/api/src/types/query.ts b/api/src/types/query.ts index 0c81facc6d..960f053059 100644 --- a/api/src/types/query.ts +++ b/api/src/types/query.ts @@ -11,6 +11,7 @@ export type Query = { meta?: Meta[]; search?: string; export?: 'json' | 'csv'; + deep?: Record; }; export type Sort = { diff --git a/api/src/utils/get-ast-from-query.ts b/api/src/utils/get-ast-from-query.ts index d349d3d3a9..346a901d5b 100644 --- a/api/src/utils/get-ast-from-query.ts +++ b/api/src/utils/get-ast-from-query.ts @@ -14,6 +14,7 @@ import { import database from '../database'; import { clone } from 'lodash'; import Knex from 'knex'; +import SchemaInspector from 'knex-schema-inspector'; type GetASTOptions = { accountability?: Accountability | null; @@ -25,14 +26,13 @@ export default async function getASTFromQuery( collection: string, query: Query, options?: GetASTOptions - // accountability?: Accountability | null, - // action?: PermissionsAction ): Promise { query = clone(query); const accountability = options?.accountability; const action = options?.action || 'read'; const knex = options?.knex || database; + const schemaInspector = SchemaInspector(knex); /** * we might not need al this info at all times, but it's easier to fetch it all once, than trying to fetch it for every @@ -56,11 +56,13 @@ export default async function getASTFromQuery( }; const fields = query.fields || ['*']; + const deep = query.deep || {}; - // Prevent fields from showing up in the query object + // Prevent fields/deep from showing up in the query object in further use delete query.fields; + delete query.deep; - ast.children = parseFields(collection, fields).filter(filterEmptyChildCollections); + ast.children = (await parseFields(collection, fields, deep)).filter(filterEmptyChildCollections); return ast; @@ -120,7 +122,7 @@ export default async function getASTFromQuery( return fields; } - function parseFields(parentCollection: string, fields: string[]) { + async function parseFields(parentCollection: string, fields: string[], deep?: Record) { fields = convertWildcards(parentCollection, fields); if (!fields) return []; @@ -157,10 +159,10 @@ export default async function getASTFromQuery( type: 'collection', name: relatedCollection, fieldKey: relationalField, - parentKey: 'id' /** @todo this needs to come from somewhere real */, + parentKey: await schemaInspector.primary(parentCollection), relation: relation, - query: {} /** @todo inject nested query here: ?deep[foo]=bar */, - children: parseFields(relatedCollection, nestedFields).filter( + query: deep?.[relationalField] || {}, + children: (await parseFields(relatedCollection, nestedFields)).filter( filterEmptyChildCollections ), };