[groupBy] groupBy relation fields (#15951)

Example query
Here person has 
- a N - 1 relationship with company
- a N - 1 morph relationship with pet or company

<img width="862" height="374" alt="image"
src="https://github.com/user-attachments/assets/59bc9b82-c943-43de-ad82-d3393b76904b"
/>
<img width="415" height="629" alt="image"
src="https://github.com/user-attachments/assets/9a3176bc-99cd-4983-8611-68ca3a2cf527"
/>

truncated response
<img width="299" height="447" alt="image"
src="https://github.com/user-attachments/assets/45af0322-9e66-4eae-8353-6c0dda487bbe"
/>

We don't allow grouping by relations of relations.

Left to do
- rest api
- tests on permissions
This commit is contained in:
Marie
2025-11-25 10:03:16 +01:00
committed by GitHub
parent 316aec3c40
commit 46d1ea6505
34 changed files with 1910 additions and 143 deletions
@@ -215,7 +215,20 @@ export const validateQueryIsPermittedOrThrow = ({
return;
}
const allFieldsSelected = expressionMap.selects.some(
let expressionMapSelectsOnMainEntity = expressionMap.selects;
if (!isEmpty(expressionMap.joinAttributes)) {
const { selectsWithoutJoinedAliases } =
validatePermissionsForJoinsAndReturnSelectsWithoutJoins({
expressionMap,
objectsPermissions,
objectMetadataMaps,
});
expressionMapSelectsOnMainEntity = selectsWithoutJoinedAliases;
}
const allFieldsSelected = expressionMapSelectsOnMainEntity.some(
(select) => select.selection === mainEntity,
);
@@ -224,7 +237,8 @@ export const validateQueryIsPermittedOrThrow = ({
selectedColumns = getSelectedColumnsFromExpressionMap({
operationType,
expressionMap,
expressionMapReturning: expressionMap.returning,
expressionMapSelects: expressionMapSelectsOnMainEntity,
allFieldsSelected,
});
@@ -259,6 +273,63 @@ export const validateQueryIsPermittedOrThrow = ({
});
};
const validatePermissionsForJoinsAndReturnSelectsWithoutJoins = ({
expressionMap,
objectsPermissions,
objectMetadataMaps,
}: {
expressionMap: QueryExpressionMap;
objectsPermissions: ObjectsPermissions;
objectMetadataMaps: ObjectMetadataMaps;
}) => {
const joinAttributesAliases = new Set(
expressionMap.joinAttributes.map((join) => join.alias.name),
);
const indexesOfSelectsForJoinedAlias: number[] = [];
for (const [_index, joinedAlias] of joinAttributesAliases.entries()) {
const entity = expressionMap.aliases.find(
(alias) => alias.type === 'join' && alias.name === joinedAlias,
)?.metadata;
if (isDefined(entity)) {
for (const [index, select] of expressionMap.selects.entries()) {
const regex = /"(\w+)"\."(\w+)"/;
const extractedAlias = select.selection.match(regex)?.[1]; // "person"."name" -> "person"
if (isDefined(extractedAlias) && extractedAlias === joinedAlias) {
indexesOfSelectsForJoinedAlias.push(index);
const selectedColumns = getSelectedColumnsFromExpressionMap({
operationType: 'select',
expressionMapSelects: expressionMap.selects.filter(
(_select, indexOfSelect) => indexOfSelect === index,
),
allFieldsSelected: false,
});
validateOperationIsPermittedOrThrow({
entityName: entity.name,
operationType: 'select' as OperationType,
objectsPermissions,
objectMetadataMaps,
selectedColumns,
allFieldsSelected: false,
updatedColumns: [],
});
}
}
}
}
const selectsWithoutJoinedAliases = expressionMap.selects.filter(
(_select, index) => !indexesOfSelectsForJoinedAlias.includes(index),
);
return { selectsWithoutJoinedAliases };
};
const validateReadFieldPermissionOrThrow = ({
restrictedFields,
selectedColumns,
@@ -336,12 +407,14 @@ const validateUpdateFieldPermissionOrThrow = ({
const getSelectedColumnsFromExpressionMap = ({
operationType,
expressionMap,
expressionMapReturning,
expressionMapSelects,
allFieldsSelected,
}: {
operationType: string;
expressionMap: QueryExpressionMap;
expressionMapSelects: { selection: string }[];
allFieldsSelected: boolean;
expressionMapReturning?: string | string[];
}) => {
let selectedColumns: string[] | '*' = [];
@@ -350,17 +423,16 @@ const getSelectedColumnsFromExpressionMap = ({
operationType,
)
) {
if (!isDefined(expressionMap.returning)) {
if (!isDefined(expressionMapReturning)) {
throw new InternalServerError(
'Returning columns are not set for update query',
);
}
selectedColumns =
expressionMap.returning === '*' ? '*' : [expressionMap.returning].flat();
expressionMapReturning === '*' ? '*' : [expressionMapReturning].flat();
} else if (!allFieldsSelected) {
selectedColumns = getSelectedColumnsFromExpressionMapSelects(
expressionMap.selects,
);
selectedColumns =
getSelectedColumnsFromExpressionMapSelects(expressionMapSelects);
}
return selectedColumns;