diff --git a/api/src/database/run-ast.ts b/api/src/database/run-ast.ts index 080543cd72..5f3b935bf7 100644 --- a/api/src/database/run-ast.ts +++ b/api/src/database/run-ast.ts @@ -76,7 +76,7 @@ export default async function runAST( // and nesting is done, we parse through the output structure, and filter out all non-requested // fields if (options?.child !== true) { - items = removeTemporaryFields(items, ast); + items = removeTemporaryFields(items, ast, primaryKeyField); } return items; @@ -241,7 +241,8 @@ function mergeWithParentItems( function removeTemporaryFields( rawItem: Item | Item[], - ast: AST | O2MNode | M2ONode + ast: AST | O2MNode | M2ONode, + primaryKeyField: string ): Item | Item[] { const rawItems: Item[] = Array.isArray(rawItem) ? rawItem : [rawItem]; @@ -257,14 +258,16 @@ function removeTemporaryFields( for (const rawItem of rawItems) { if (rawItem === null) return rawItem; - const item = fields.includes('*') ? rawItem : pick(rawItem, fields); + + const item = fields.length > 0 ? pick(rawItem, fields) : rawItem[primaryKeyField]; for (const nestedCollection of nestedCollections) { if (item[nestedCollection.fieldKey] !== null && nestedCollection.type !== 'm2a') { /** @TODO REMOVE M2A CHECK HERE */ item[nestedCollection.fieldKey] = removeTemporaryFields( rawItem[nestedCollection.fieldKey], - nestedCollection + nestedCollection, + nestedCollection.parentKey ); } } diff --git a/api/src/utils/get-ast-from-query.ts b/api/src/utils/get-ast-from-query.ts index beccf30e8e..fdd4815ebd 100644 --- a/api/src/utils/get-ast-from-query.ts +++ b/api/src/utils/get-ast-from-query.ts @@ -62,74 +62,16 @@ export default async function getASTFromQuery( delete query.fields; delete query.deep; - ast.children = (await parseFields(collection, fields, deep)).filter( - filterEmptyChildCollections - ); + ast.children = await parseFields(collection, fields, deep); return ast; - function convertWildcards(parentCollection: string, fields: string[]) { - const allowedFields = permissions - ? permissions - .find((permission) => parentCollection === permission.collection) - ?.fields?.split(',') - : ['*']; - - if (!allowedFields || allowedFields.length === 0) return []; - - for (let index = 0; index < fields.length; index++) { - const fieldKey = fields[index]; - - if (fieldKey.includes('*') === false) continue; - - if (fieldKey === '*') { - if (allowedFields.includes('*')) continue; - fields.splice(index, 1, ...allowedFields); - } - - // Swap *.* case for *,.*,.* - if (fieldKey.includes('.') && fieldKey.split('.')[0] === '*') { - const parts = fieldKey.split('.'); - - const relationalFields = allowedFields.includes('*') - ? relations - .filter( - (relation) => - relation.many_collection === parentCollection || - relation.one_collection === parentCollection - ) - .map((relation) => { - const isM2O = relation.many_collection === parentCollection; - return isM2O ? relation.many_field : relation.one_field; - }) - : allowedFields.filter((fieldKey) => !!getRelation(parentCollection, fieldKey)); - - const nonRelationalFields = allowedFields.filter( - (fieldKey) => relationalFields.includes(fieldKey) === false - ); - - fields.splice( - index, - 1, - ...[ - ...relationalFields.map((relationalField) => { - return `${relationalField}.${parts.slice(1).join('.')}`; - }), - ...nonRelationalFields, - ] - ); - } - } - - return fields; - } - async function parseFields( parentCollection: string, fields: string[], deep?: Record ) { - fields = convertWildcards(parentCollection, fields); + fields = await convertWildcards(parentCollection, fields); if (!fields) return []; @@ -138,9 +80,17 @@ export default async function getASTFromQuery( const relationalStructure: Record = {}; for (const field of fields) { - if (field.includes('.') === false) { - children.push({ type: 'field', name: field }); - } else { + const isRelational = + field.includes('.') || + !!relations.find( + (relation) => + (relation.many_collection === parentCollection && + relation.many_field === field) || + (relation.one_collection === parentCollection && + relation.one_field === field) + ); + + if (isRelational) { // field is relational const parts = field.split('.'); @@ -148,7 +98,11 @@ export default async function getASTFromQuery( relationalStructure[parts[0]] = []; } - relationalStructure[parts[0]].push(parts.slice(1).join('.')); + if (parts.length > 1) { + relationalStructure[parts[0]].push(parts.slice(1).join('.')); + } + } else { + children.push({ type: 'field', name: field }); } } @@ -192,9 +146,7 @@ export default async function getASTFromQuery( parentKey: await schemaInspector.primary(parentCollection), relation: relation, query: deep?.[relationalField] || {}, - children: (await parseFields(relatedCollection, nestedFields)).filter( - filterEmptyChildCollections - ), + children: await parseFields(relatedCollection, nestedFields), }; } @@ -204,6 +156,68 @@ export default async function getASTFromQuery( return children; } + async function convertWildcards(parentCollection: string, fields: string[]) { + const allowedFields = permissions + ? permissions + .find((permission) => parentCollection === permission.collection) + ?.fields?.split(',') + : ['*']; + + if (!allowedFields || allowedFields.length === 0) return []; + + for (let index = 0; index < fields.length; index++) { + const fieldKey = fields[index]; + + if (fieldKey.includes('*') === false) continue; + + if (fieldKey === '*') { + // Set to all fields in collection + if (allowedFields.includes('*')) { + const fieldsInCollection = await getFieldsInCollection(parentCollection); + fields.splice(index, 1, ...fieldsInCollection); + } else { + // Set to all allowed fields + fields.splice(index, 1, ...allowedFields); + } + } + + // Swap *.* case for *,.*,.* + if (fieldKey.includes('.') && fieldKey.split('.')[0] === '*') { + const parts = fieldKey.split('.'); + + const relationalFields = allowedFields.includes('*') + ? relations + .filter( + (relation) => + relation.many_collection === parentCollection || + relation.one_collection === parentCollection + ) + .map((relation) => { + const isM2O = relation.many_collection === parentCollection; + return isM2O ? relation.many_field : relation.one_field; + }) + : allowedFields.filter((fieldKey) => !!getRelation(parentCollection, fieldKey)); + + const nonRelationalFields = allowedFields.filter( + (fieldKey) => relationalFields.includes(fieldKey) === false + ); + + fields.splice( + index, + 1, + ...[ + ...relationalFields.map((relationalField) => { + return `${relationalField}.${parts.slice(1).join('.')}`; + }), + ...nonRelationalFields, + ] + ); + } + } + + return fields; + } + function getRelation(collection: string, field: string) { const relation = relations.find((relation) => { return ( @@ -229,12 +243,6 @@ export default async function getASTFromQuery( } } - function filterEmptyChildCollections(childNode: FieldNode | NestedCollectionNode) { - if (childNode.type === 'field') return true; - if (childNode.children.length > 0) return true; - return false; - } - function getRelationType( relatedCollection: string, relationalField: string, @@ -253,4 +261,20 @@ export default async function getASTFromQuery( return 'o2m'; } + + async function getFieldsInCollection(collection: string) { + const columns = (await schemaInspector.columns(collection)).map((column) => column.column); + const fields = ( + await database.select('field').from('directus_fields').where({ collection }) + ).map((field) => field.field); + + const fieldsInCollection = [ + ...columns, + ...fields.filter((field) => { + return columns.includes(field) === false; + }), + ]; + + return fieldsInCollection; + } }