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:
Marie
2026-06-09 16:08:22 +02:00
committed by GitHub
parent 137fe45cf6
commit c27c8c88b0
49 changed files with 1464 additions and 414 deletions
@@ -0,0 +1,129 @@
import { FieldMetadataType, RelationType } from '@/types';
import { isFieldMetadataSupportedInGroupBy } from '@/utils/fieldMetadata/isFieldMetadataSupportedInGroupBy';
describe('isFieldMetadataSupportedInGroupBy', () => {
it('returns false for field types not supported in groupBy', () => {
expect(
isFieldMetadataSupportedInGroupBy({
type: FieldMetadataType.RAW_JSON,
name: 'rawJsonField',
isSystem: false,
}),
).toBe(false);
expect(
isFieldMetadataSupportedInGroupBy({
type: FieldMetadataType.TS_VECTOR,
name: 'tsVectorField',
isSystem: false,
}),
).toBe(false);
expect(
isFieldMetadataSupportedInGroupBy({
type: FieldMetadataType.FILES,
name: 'filesField',
isSystem: false,
}),
).toBe(false);
expect(
isFieldMetadataSupportedInGroupBy({
type: FieldMetadataType.POSITION,
name: 'position',
isSystem: false,
}),
).toBe(false);
});
it('returns true for regular field types', () => {
expect(
isFieldMetadataSupportedInGroupBy({
type: FieldMetadataType.SELECT,
name: 'stage',
isSystem: false,
}),
).toBe(true);
});
it('returns false for ONE_TO_MANY relation fields', () => {
// ONE_TO_MANY relations keep their foreign key on the target object, so
// there is no column on this object to group by.
expect(
isFieldMetadataSupportedInGroupBy({
type: FieldMetadataType.MORPH_RELATION,
name: 'polymorphicHelperRockets',
isSystem: false,
relationType: RelationType.ONE_TO_MANY,
}),
).toBe(false);
expect(
isFieldMetadataSupportedInGroupBy({
type: FieldMetadataType.RELATION,
name: 'opportunities',
isSystem: false,
relationType: RelationType.ONE_TO_MANY,
}),
).toBe(false);
});
it('returns true for MANY_TO_ONE relation fields', () => {
expect(
isFieldMetadataSupportedInGroupBy({
type: FieldMetadataType.MORPH_RELATION,
name: 'polymorphicOwnerRocket',
isSystem: false,
relationType: RelationType.MANY_TO_ONE,
}),
).toBe(true);
expect(
isFieldMetadataSupportedInGroupBy({
type: FieldMetadataType.RELATION,
name: 'company',
isSystem: false,
relationType: RelationType.MANY_TO_ONE,
}),
).toBe(true);
});
it('returns false for internal field names', () => {
expect(
isFieldMetadataSupportedInGroupBy({
type: FieldMetadataType.TEXT,
name: 'id',
isSystem: false,
}),
).toBe(false);
expect(
isFieldMetadataSupportedInGroupBy({
type: FieldMetadataType.DATE_TIME,
name: 'deletedAt',
isSystem: false,
}),
).toBe(false);
});
it('returns false for system fields', () => {
expect(
isFieldMetadataSupportedInGroupBy({
type: FieldMetadataType.TEXT,
name: 'customSystemField',
isSystem: true,
}),
).toBe(false);
});
it('returns true for createdAt and updatedAt even when system', () => {
expect(
isFieldMetadataSupportedInGroupBy({
type: FieldMetadataType.DATE_TIME,
name: 'createdAt',
isSystem: true,
}),
).toBe(true);
expect(
isFieldMetadataSupportedInGroupBy({
type: FieldMetadataType.DATE_TIME,
name: 'updatedAt',
isSystem: true,
}),
).toBe(true);
});
});
@@ -3,4 +3,6 @@ export * from './isFieldMetadataDateKind';
export * from './isFieldMetadataEligibleForFieldsWidget';
export * from './isFieldMetadataNumericKind';
export * from './isFieldMetadataSelectKind';
export * from './isFieldMetadataSupportedInGroupBy';
export * from './isFieldMetadataTextKind';
export * from './shouldExcludeFieldFromAgentToolSchema';
@@ -0,0 +1,46 @@
import { FIELD_METADATA_TYPES_NOT_SUPPORTED_IN_GROUP_BY } from '@/constants';
import { FieldMetadataType, RelationType } from '@/types';
import { isFieldMetadataDateKind } from '@/utils/fieldMetadata/isFieldMetadataDateKind';
import { shouldExcludeFieldFromAgentToolSchema } from '@/utils/fieldMetadata/shouldExcludeFieldFromAgentToolSchema';
const ALWAYS_GROUPABLE_SYSTEM_DATE_FIELD_NAMES = new Set([
'createdAt',
'updatedAt',
]);
const RELATION_FIELD_METADATA_TYPES = new Set<FieldMetadataType>([
FieldMetadataType.RELATION,
FieldMetadataType.MORPH_RELATION,
]);
export const isFieldMetadataSupportedInGroupBy = ({
type,
name,
isSystem,
relationType,
}: {
type: FieldMetadataType;
name: string;
isSystem: boolean;
relationType?: RelationType | null;
}): boolean => {
const isAlwaysGroupableSystemDateField =
ALWAYS_GROUPABLE_SYSTEM_DATE_FIELD_NAMES.has(name) &&
isFieldMetadataDateKind(type);
if (
!isAlwaysGroupableSystemDateField &&
shouldExcludeFieldFromAgentToolSchema({ fieldName: name, isSystem })
) {
return false;
}
if (
RELATION_FIELD_METADATA_TYPES.has(type) &&
relationType === RelationType.ONE_TO_MANY
) {
return false;
}
return !FIELD_METADATA_TYPES_NOT_SUPPORTED_IN_GROUP_BY.has(type);
};
@@ -0,0 +1,24 @@
import { EXCLUDED_FIELD_NAMES_FROM_AGENT_TOOL_SCHEMA } from '@/constants';
export const shouldExcludeFieldFromAgentToolSchema = ({
fieldName,
isSystem,
excludeId = true,
additionalExcludedFieldNames = [],
}: {
fieldName: string;
isSystem: boolean;
excludeId?: boolean;
additionalExcludedFieldNames?: string[];
}): boolean => {
const excludedFieldNames = [
...EXCLUDED_FIELD_NAMES_FROM_AGENT_TOOL_SCHEMA,
...additionalExcludedFieldNames,
];
if (excludeId) {
excludedFieldNames.push('id');
}
return excludedFieldNames.includes(fieldName) || isSystem;
};