Fix various graphs bugs (#21311)
Some bugs fixed in this PR
1. From UI any field could be chosen to group the query by it, while for
instance, RAW_JSON type (eg workflowRun.state) is not supported by
PostgreSQL to group a query by. Fix: removed it from the "group by"
fields options in FE + in BE -->
2. The BE check existed (isFlatFieldMetadataSupportedInGroupBy) but the
signature was malformed: it expected`{ fieldMetadataType,
fieldMetadataName, fieldMetadataIsSystem }` while every caller passes a
flat field metadata object with type/name/isSystem. So the check is
mis-wired — at runtime the destructured props are undefined, making it
always return true (validation bypassed). Fixed this.
3. Group by does not work with Morph relations if their direction is
ONE_TO_MANY. Added that constraint.
4. Group by with morph relations were broken even for MANY_TO_ONE,
because a morph is stored as one field per target
(polymorphicOwnerRocket, polymorphicOwnerSurveyResult…), each with its
own join column, but the frontend collapsed them into a single
polymorphicOwner field — so the backend tried to resolve a non-existent
polymorphicOwnerId. Fix: Frontend: added a target picker so you choose
the specific morph target (then its sub-field), storing the real
per-target field id. Backend: fixed validate-relation-subfield to use
the per-target field's own relationTargetObjectMetadataId instead of the
multi-target resolver that returned null.
5. (improvement) When an error occured in the query, the graph showed
"No data". Updated it to "error". (screenshot 1)
6. When a field used as a filter on a graph is deleted, it is not
deleted as a graph filter (which is ok because it would involve parsing
all the graph's configuration json to find whether a field is
referenced; there is no foreign key), which prevented from further
modifying the graph's filters. Fixed this + add an indicator that the
filter is can/should be removed (see screenshot 2)
7. "Ambiguous column name" PG error occurs when ordering by "creation
date" of a related field, because both objects have createdAt field.
Fixed it by adding table alias as prefix.
8. (improvement) While working on #5 I did not understand why we could
directly do `"objectMetadataNameSingular"."columnName" `while I expected
that for custom objects it would have to be
`_objectMetadataNameSingular`. that's simply because we use an alias
from the beginning. To add clarity, within groupBy code I replaced
`objectMetadataNameSingular` with `objectAlias` everywhere it is indeed
inherited from us using objectAlias.
<img width="685" height="391" alt="Screenshot 2026-06-08 at 12 01 45"
src="https://github.com/user-attachments/assets/f2b15ca5-da39-4114-8188-69f58f3c4cbf"
/>
<img width="598" height="341" alt="Screenshot 2026-06-08 at 11 53 55"
src="https://github.com/user-attachments/assets/66372811-4a37-40d9-b43a-4af51f89b6e6"
/>
This commit is contained in:
+13
-2
@@ -1,5 +1,7 @@
|
||||
import { isFieldMetadataSupportedInGroupBy } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isFlatFieldMetadataSupportedInGroupBy } from 'src/engine/metadata-modules/field-metadata/utils/is-supported-in-group-by.util';
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
|
||||
export const isRelationNestedFieldSupportedInGroupBy = ({
|
||||
nestedFieldName,
|
||||
@@ -12,5 +14,14 @@ export const isRelationNestedFieldSupportedInGroupBy = ({
|
||||
return true;
|
||||
}
|
||||
|
||||
return isFlatFieldMetadataSupportedInGroupBy(nestedFieldMetadata);
|
||||
const relationType = isMorphOrRelationFlatFieldMetadata(nestedFieldMetadata)
|
||||
? nestedFieldMetadata.settings.relationType
|
||||
: null;
|
||||
|
||||
return isFieldMetadataSupportedInGroupBy({
|
||||
type: nestedFieldMetadata.type,
|
||||
name: nestedFieldMetadata.name,
|
||||
isSystem: nestedFieldMetadata.isSystem,
|
||||
relationType,
|
||||
});
|
||||
};
|
||||
|
||||
+23
-9
@@ -1,25 +1,28 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined, isPlainObject } from 'twenty-shared/utils';
|
||||
import {
|
||||
isDefined,
|
||||
isFieldMetadataSupportedInGroupBy,
|
||||
isPlainObject,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { type GroupByField } from 'src/engine/api/common/common-query-runners/types/group-by-field.types';
|
||||
import { isGroupByDateFieldDefinition } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/is-group-by-date-field-definition.util';
|
||||
import { validateAndTransformRelationGroupByFieldOrThrow } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/validate-and-transform-relation-group-by-field-or-throw.util';
|
||||
import { validateSingleKeyForGroupByOrThrow } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/validate-single-key-for-group-by-or-throw.util';
|
||||
import {
|
||||
CommonQueryRunnerException,
|
||||
CommonQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
|
||||
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
|
||||
import { isGroupByDateFieldDefinition } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/is-group-by-date-field-definition.util';
|
||||
import { validateAndTransformRelationGroupByFieldOrThrow } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/validate-and-transform-relation-group-by-field-or-throw.util';
|
||||
import { validateSingleKeyForGroupByOrThrow } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/validate-single-key-for-group-by-or-throw.util';
|
||||
import { type GroupByField } from 'src/engine/api/common/common-query-runners/types/group-by-field.types';
|
||||
import {
|
||||
ObjectRecordGroupByForAtomicField,
|
||||
ObjectRecordGroupByForCompositeField,
|
||||
ObjectRecordGroupByForDateField,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { getGroupableSubFieldsForCompositeType } from 'src/engine/metadata-modules/field-metadata/utils/get-groupable-sub-fields-for-composite-type.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { getGroupableSubFieldsForCompositeType } from 'src/engine/metadata-modules/field-metadata/utils/get-groupable-sub-fields-for-composite-type.util';
|
||||
import { isFlatFieldMetadataSupportedInGroupBy } from 'src/engine/metadata-modules/field-metadata/utils/is-supported-in-group-by.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
@@ -136,7 +139,18 @@ const validateAndTransformSingleGroupByFieldOrThrow = ({
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isFlatFieldMetadataSupportedInGroupBy(fieldMetadata)) {
|
||||
const relationType = isMorphOrRelationFlatFieldMetadata(fieldMetadata)
|
||||
? fieldMetadata.settings.relationType
|
||||
: null;
|
||||
|
||||
if (
|
||||
!isFieldMetadataSupportedInGroupBy({
|
||||
type: fieldMetadata.type,
|
||||
name: fieldMetadata.name,
|
||||
isSystem: fieldMetadata.isSystem,
|
||||
relationType,
|
||||
})
|
||||
) {
|
||||
throw new CommonQueryRunnerException(
|
||||
`Field "${fieldName}" is not supported in groupBy`,
|
||||
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
|
||||
+6
-10
@@ -87,11 +87,9 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
authContext,
|
||||
} = queryRunnerContext;
|
||||
|
||||
const objectMetadataNameSingular = flatObjectMetadata.nameSingular;
|
||||
const objectAlias = getObjectAlias(flatObjectMetadata);
|
||||
|
||||
let queryBuilder = repository.createQueryBuilder(
|
||||
objectMetadataNameSingular,
|
||||
);
|
||||
let queryBuilder = repository.createQueryBuilder(objectAlias);
|
||||
|
||||
const groupByFields =
|
||||
this.groupByArgProcessor.validateAndTransformGroupByFieldsOrThrow({
|
||||
@@ -101,8 +99,6 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
const objectAlias = getObjectAlias(flatObjectMetadata);
|
||||
|
||||
this.addJoinForGroupByOnRelationFields({
|
||||
queryBuilder,
|
||||
groupByFields,
|
||||
@@ -126,12 +122,12 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
|
||||
selectedAggregatedFields: args.selectedFieldsResult.aggregate,
|
||||
queryBuilder,
|
||||
objectMetadataNameSingular,
|
||||
objectMetadataNameSingular: objectAlias,
|
||||
});
|
||||
|
||||
const groupByDefinitions = getGroupByDefinitions({
|
||||
groupByFields,
|
||||
objectMetadataNameSingular,
|
||||
objectMetadataNameSingular: objectAlias,
|
||||
});
|
||||
|
||||
groupByDefinitions.forEach((groupByColumn, index) => {
|
||||
@@ -308,7 +304,7 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
workspaceId: string;
|
||||
commonQueryParser: GraphqlQueryParser;
|
||||
}): Promise<void> {
|
||||
const objectMetadataNameSingular = flatObjectMetadata.nameSingular;
|
||||
const objectAlias = getObjectAlias(flatObjectMetadata);
|
||||
|
||||
if (args.viewId) {
|
||||
appliedFilters = await this.addFiltersFromView({
|
||||
@@ -322,7 +318,7 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
|
||||
commonQueryParser.applyFilterToBuilder(
|
||||
queryBuilder,
|
||||
objectMetadataNameSingular,
|
||||
objectAlias,
|
||||
appliedFilters,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user