From 71dcbc34b761d752739372bd2b26ab6c4aa302e3 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Fri, 9 Oct 2020 15:27:55 -0400 Subject: [PATCH] Return primary keys if no explicit fields are requested --- api/src/database/run-ast.ts | 89 ++++++++++++----- api/src/utils/get-ast-from-query.ts | 147 +++++++++++++++++----------- 2 files changed, 153 insertions(+), 83 deletions(-) diff --git a/api/src/database/run-ast.ts b/api/src/database/run-ast.ts index f7903aec2a..75d56e2114 100644 --- a/api/src/database/run-ast.ts +++ b/api/src/database/run-ast.ts @@ -13,14 +13,20 @@ type RunASTOptions = { child?: boolean; }; -export default async function runAST(originalAST: AST, options?: RunASTOptions): Promise { +export default async function runAST( + originalAST: AST, + options?: RunASTOptions +): Promise { const ast = cloneDeep(originalAST); const query = options?.query || ast.query; const knex = options?.knex || database; // Retrieve the database columns to select in the current AST - const { columnsToSelect, primaryKeyField, nestedCollectionASTs } = await parseCurrentLevel(ast, knex); + const { columnsToSelect, primaryKeyField, nestedCollectionASTs } = await parseCurrentLevel( + ast, + knex + ); // The actual knex query builder instance. This is a promise that resolves with the raw items from the db const dbQuery = await getDBQuery(knex, ast.name, columnsToSelect, query, primaryKeyField); @@ -63,7 +69,7 @@ export default async function runAST(originalAST: AST, options?: RunASTOptions): // and nesting is done, we parse through the output structure, and filter out all non-requested // fields if (options?.child !== true) { - items = removeTemporaryFields(items, originalAST); + items = removeTemporaryFields(items, originalAST, primaryKeyField); } return items; @@ -109,7 +115,13 @@ async function parseCurrentLevel(ast: AST, knex: Knex) { return { columnsToSelect, nestedCollectionASTs, primaryKeyField }; } -async function getDBQuery(knex: Knex, table: string, columns: string[], query: Query, primaryKeyField: string): Promise { +async function getDBQuery( + knex: Knex, + table: string, + columns: string[], + query: Query, + primaryKeyField: string +): Promise { let dbQuery = knex.select(columns.map((column) => `${table}.${column}`)).from(table); const queryCopy = clone(query); @@ -127,7 +139,10 @@ async function getDBQuery(knex: Knex, table: string, columns: string[], query: Q return dbQuery; } -function applyParentFilters(nestedCollectionASTs: NestedCollectionAST[], parentItem: Item | Item[]) { +function applyParentFilters( + nestedCollectionASTs: NestedCollectionAST[], + parentItem: Item | Item[] +) { const parentItems = Array.isArray(parentItem) ? parentItem : [parentItem]; for (const nestedAST of nestedCollectionASTs) { @@ -139,15 +154,15 @@ function applyParentFilters(nestedCollectionASTs: NestedCollectionAST[], parentI filter: { ...(nestedAST.query.filter || {}), [nestedAST.relation.one_primary]: { - _in: uniq(parentItems.map((res) => res[nestedAST.relation.many_field])).filter( - (id) => id - ), - } - } - } + _in: uniq( + parentItems.map((res) => res[nestedAST.relation.many_field]) + ).filter((id) => id), + }, + }, + }; } else { const relatedM2OisFetched = !!nestedAST.children.find((child) => { - return child.type === 'field' && child.name === nestedAST.relation.many_field + return child.type === 'field' && child.name === nestedAST.relation.many_field; }); if (relatedM2OisFetched === false) { @@ -159,24 +174,33 @@ function applyParentFilters(nestedCollectionASTs: NestedCollectionAST[], parentI filter: { ...(nestedAST.query.filter || {}), [nestedAST.relation.many_field]: { - _in: uniq(parentItems.map((res) => res[nestedAST.parentKey])).filter((id) => id), - } - } - } + _in: uniq(parentItems.map((res) => res[nestedAST.parentKey])).filter( + (id) => id + ), + }, + }, + }; } } return nestedCollectionASTs; } -function mergeWithParentItems(nestedItem: Item | Item[], parentItem: Item | Item[], nestedAST: NestedCollectionAST, o2mLimit?: number | null) { +function mergeWithParentItems( + nestedItem: Item | Item[], + parentItem: Item | Item[], + nestedAST: NestedCollectionAST, + o2mLimit?: number | null +) { const nestedItems = Array.isArray(nestedItem) ? nestedItem : [nestedItem]; const parentItems = clone(Array.isArray(parentItem) ? parentItem : [parentItem]); if (isM2O(nestedAST)) { for (const parentItem of parentItems) { const itemChild = nestedItems.find((nestedItem) => { - return nestedItem[nestedAST.relation.one_primary] === parentItem[nestedAST.fieldKey]; + return ( + nestedItem[nestedAST.relation.one_primary] === parentItem[nestedAST.fieldKey] + ); }); parentItem[nestedAST.fieldKey] = itemChild || null; @@ -188,8 +212,10 @@ function mergeWithParentItems(nestedItem: Item | Item[], parentItem: Item | Item if (Array.isArray(nestedItem[nestedAST.relation.many_field])) return true; return ( - nestedItem[nestedAST.relation.many_field] === parentItem[nestedAST.relation.one_primary] || - nestedItem[nestedAST.relation.many_field]?.[nestedAST.relation.many_primary] === parentItem[nestedAST.relation.one_primary] + nestedItem[nestedAST.relation.many_field] === + parentItem[nestedAST.relation.one_primary] || + nestedItem[nestedAST.relation.many_field]?.[nestedAST.relation.many_primary] === + parentItem[nestedAST.relation.one_primary] ); }); @@ -206,21 +232,34 @@ function mergeWithParentItems(nestedItem: Item | Item[], parentItem: Item | Item return Array.isArray(parentItem) ? parentItems : parentItems[0]; } -function removeTemporaryFields(rawItem: Item | Item[], ast: AST | NestedCollectionAST): Item | Item[] { +function removeTemporaryFields( + rawItem: Item | Item[], + ast: AST | NestedCollectionAST, + primaryKeyField: string +): Item | Item[] { const rawItems: Item[] = Array.isArray(rawItem) ? rawItem : [rawItem]; const items: Item[] = []; - const fields = ast.children.filter((child) => child.type === 'field').map((child) => child.name); - const nestedCollections = ast.children.filter((child) => child.type === 'collection') as NestedCollectionAST[]; + const fields = ast.children + .filter((child) => child.type === 'field') + .map((child) => child.name); + const nestedCollections = ast.children.filter( + (child) => child.type === 'collection' + ) as NestedCollectionAST[]; 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) { - item[nestedCollection.fieldKey] = removeTemporaryFields(rawItem[nestedCollection.fieldKey], nestedCollection); + item[nestedCollection.fieldKey] = removeTemporaryFields( + rawItem[nestedCollection.fieldKey], + nestedCollection, + nestedCollection.parentKey + ); } } diff --git a/api/src/utils/get-ast-from-query.ts b/api/src/utils/get-ast-from-query.ts index 346a901d5b..93a5bf78f7 100644 --- a/api/src/utils/get-ast-from-query.ts +++ b/api/src/utils/get-ast-from-query.ts @@ -62,11 +62,76 @@ 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[]) { + async function parseFields( + parentCollection: string, + fields: string[], + deep?: Record + ) { + fields = await convertWildcards(parentCollection, fields); + + if (!fields) return []; + + const children: (NestedCollectionAST | FieldAST)[] = []; + + const relationalStructure: Record = {}; + + for (const field of fields) { + 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('.'); + + if (relationalStructure.hasOwnProperty(parts[0]) === false) { + relationalStructure[parts[0]] = []; + } + + if (parts.length > 1) { + relationalStructure[parts[0]].push(parts.slice(1).join('.')); + } + } else { + children.push({ type: 'field', name: field }); + } + } + + for (const [relationalField, nestedFields] of Object.entries(relationalStructure)) { + const relatedCollection = getRelatedCollection(parentCollection, relationalField); + + if (!relatedCollection) continue; + + const relation = getRelation(parentCollection, relationalField); + + if (!relation) continue; + + const child: NestedCollectionAST = { + type: 'collection', + name: relatedCollection, + fieldKey: relationalField, + parentKey: await schemaInspector.primary(parentCollection), + relation: relation, + query: deep?.[relationalField] || {}, + children: await parseFields(relatedCollection, nestedFields), + }; + + children.push(child); + } + + return children; + } + + async function convertWildcards(parentCollection: string, fields: string[]) { const allowedFields = permissions ? permissions .find((permission) => parentCollection === permission.collection) @@ -81,8 +146,14 @@ export default async function getASTFromQuery( if (fieldKey.includes('*') === false) continue; if (fieldKey === '*') { - if (allowedFields.includes('*')) continue; - fields.splice(index, 1, ...allowedFields); + // 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 *,.*,.* @@ -122,57 +193,6 @@ export default async function getASTFromQuery( return fields; } - async function parseFields(parentCollection: string, fields: string[], deep?: Record) { - fields = convertWildcards(parentCollection, fields); - - if (!fields) return []; - - const children: (NestedCollectionAST | FieldAST)[] = []; - - const relationalStructure: Record = {}; - - for (const field of fields) { - if (field.includes('.') === false) { - children.push({ type: 'field', name: field }); - } else { - // field is relational - const parts = field.split('.'); - - if (relationalStructure.hasOwnProperty(parts[0]) === false) { - relationalStructure[parts[0]] = []; - } - - relationalStructure[parts[0]].push(parts.slice(1).join('.')); - } - } - - for (const [relationalField, nestedFields] of Object.entries(relationalStructure)) { - const relatedCollection = getRelatedCollection(parentCollection, relationalField); - - if (!relatedCollection) continue; - - const relation = getRelation(parentCollection, relationalField); - - if (!relation) continue; - - const child: NestedCollectionAST = { - type: 'collection', - name: relatedCollection, - fieldKey: relationalField, - parentKey: await schemaInspector.primary(parentCollection), - relation: relation, - query: deep?.[relationalField] || {}, - children: (await parseFields(relatedCollection, nestedFields)).filter( - filterEmptyChildCollections - ), - }; - - children.push(child); - } - - return children; - } - function getRelation(collection: string, field: string) { const relation = relations.find((relation) => { return ( @@ -198,8 +218,19 @@ export default async function getASTFromQuery( } } - function filterEmptyChildCollections(childAST: FieldAST | NestedCollectionAST) { - if (childAST.type === 'collection' && childAST.children.length === 0) return false; - return true; + 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; } }