c27c8c88b0
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"
/>
89 lines
2.9 KiB
TypeScript
89 lines
2.9 KiB
TypeScript
import { FieldMetadataType } from 'twenty-shared/types';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
|
|
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
|
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
|
import { getCompositeSubfieldNames } from 'src/engine/metadata-modules/page-layout-widget/utils/get-composite-subfield-names.util';
|
|
import { resolveMorphTargetObjectId } from 'src/engine/metadata-modules/page-layout-widget/utils/resolve-morph-target-object-id.util';
|
|
import { validateCompositeSubfield } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-composite-subfield.util';
|
|
|
|
export const validateRelationSubfield = ({
|
|
field,
|
|
subFieldName,
|
|
paramName,
|
|
allFields,
|
|
fieldsByObjectId,
|
|
}: {
|
|
field: FlatFieldMetadata;
|
|
subFieldName: string | null | undefined;
|
|
paramName: string;
|
|
allFields: FlatFieldMetadata[];
|
|
fieldsByObjectId: Map<string, FlatFieldMetadata[]>;
|
|
}): void => {
|
|
if (!isDefined(subFieldName)) {
|
|
return;
|
|
}
|
|
|
|
const dotIndex = subFieldName.indexOf('.');
|
|
const nestedFieldName =
|
|
dotIndex === -1 ? subFieldName : subFieldName.slice(0, dotIndex);
|
|
const nestedSubFieldName =
|
|
dotIndex === -1 ? undefined : subFieldName.slice(dotIndex + 1);
|
|
|
|
if (!nestedFieldName) {
|
|
throw new Error(`Relation subfield "${subFieldName}" is invalid.`);
|
|
}
|
|
|
|
if (isDefined(nestedSubFieldName) && nestedSubFieldName.includes('.')) {
|
|
throw new Error(`Relation subfield "${subFieldName}" is invalid.`);
|
|
}
|
|
|
|
let targetObjectId = field.relationTargetObjectMetadataId ?? null;
|
|
|
|
if (
|
|
field.type === FieldMetadataType.MORPH_RELATION &&
|
|
!isDefined(targetObjectId)
|
|
) {
|
|
targetObjectId = resolveMorphTargetObjectId({ field, allFields });
|
|
}
|
|
|
|
if (!isDefined(targetObjectId)) {
|
|
throw new Error(
|
|
`Relation field "${paramName}" does not have a resolvable target object.`,
|
|
);
|
|
}
|
|
|
|
const targetFields = fieldsByObjectId.get(targetObjectId) ?? [];
|
|
const nestedField = targetFields.find(
|
|
(targetField) => targetField.name === nestedFieldName,
|
|
);
|
|
|
|
if (!isDefined(nestedField)) {
|
|
throw new Error(
|
|
`Relation subfield "${nestedFieldName}" not found for "${paramName}".`,
|
|
);
|
|
}
|
|
|
|
if (!isDefined(nestedSubFieldName)) {
|
|
if (isCompositeFieldMetadataType(nestedField.type)) {
|
|
const allowed = getCompositeSubfieldNames(nestedField.type);
|
|
|
|
throw new Error(
|
|
`Composite field "${nestedFieldName}" requires a subfield. Use "${nestedFieldName}.<subfield>" where subfield is one of: ${allowed.join(', ')}`,
|
|
);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!isCompositeFieldMetadataType(nestedField.type)) {
|
|
throw new Error(`Field "${nestedFieldName}" is not composite.`);
|
|
}
|
|
|
|
validateCompositeSubfield({
|
|
field: nestedField,
|
|
subFieldName: nestedSubFieldName,
|
|
paramName: nestedFieldName,
|
|
});
|
|
};
|