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:
+129
@@ -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);
|
||||
};
|
||||
+24
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user