Files
twenty/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts
T
Guillim 3d1842dcb3 Morph-front-display-table (#13979)
## Add Morph Relation Field Display Support for Tables (one to many &
many to one)

**Features:**
- Display morph relation fields (one-to-many, many-to-one) in table
cells
- Multi-field record retrieval for improved performance
- Enhanced table cell rendering with morph relation support

**Components Added:**
- `MorphRelationManyToOneFieldDisplay` - Shows many-to-one morph
relations
- `MorphRelationOneToManyFieldDisplay` - Shows one-to-many morph
relations


TODO : 
handle notes and tasks case if we choose these kind of objects as
targets/source for a morph relation

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2025-08-25 16:12:49 +02:00

307 lines
8.0 KiB
TypeScript

import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { isNonCompositeField } from '@/object-record/object-filter-dropdown/utils/isNonCompositeField';
import { type ObjectPermissions } from 'twenty-shared/types';
import {
computeMorphRelationFieldJoinColumnName,
computeMorphRelationFieldName,
isDefined,
} from 'twenty-shared/utils';
import { type FieldMetadataItem } from '../types/FieldMetadataItem';
type MapFieldMetadataToGraphQLQueryArgs = {
objectMetadataItems: ObjectMetadataItem[];
gqlField: string;
fieldMetadata: Pick<
FieldMetadataItem,
'name' | 'type' | 'relation' | 'morphRelations' | 'settings'
>;
relationRecordGqlFields?: RecordGqlFields;
computeReferences?: boolean;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
};
// TODO: change ObjectMetadataItems mock before refactoring with relation computed field
export const mapFieldMetadataToGraphQLQuery = ({
objectMetadataItems,
gqlField,
fieldMetadata,
relationRecordGqlFields,
computeReferences = false,
objectPermissionsByObjectMetadataId,
}: MapFieldMetadataToGraphQLQueryArgs): string => {
const fieldType = fieldMetadata.type;
const fieldIsNonCompositeField = isNonCompositeField(fieldType);
if (fieldIsNonCompositeField) {
return gqlField;
}
if (
fieldType === FieldMetadataType.MORPH_RELATION &&
(fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY ||
fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE)
) {
let gqlMorphField = '';
for (const morphRelation of fieldMetadata.morphRelations ?? []) {
const relationFieldName = computeMorphRelationFieldName({
fieldName: fieldMetadata.name,
relationDirection: fieldMetadata.settings?.relationType,
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
namePlural: morphRelation.targetObjectMetadata.namePlural,
});
const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id === morphRelation.targetObjectMetadata.id,
);
if (!isDefined(relationMetadataItem)) {
continue;
}
if (
isDefined(objectPermissionsByObjectMetadataId) &&
isDefined(relationMetadataItem.id)
) {
if (!isDefined(morphRelation.targetObjectMetadata.id)) {
throw new Error(
`Target object metadata id not found with field metadata ${fieldMetadata.name}`,
);
}
const objectPermission = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
morphRelation.targetObjectMetadata.id,
);
if (!objectPermission.canReadObjectRecords) {
continue;
}
}
if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) {
gqlMorphField += `${relationFieldName}
{
edges {
node ${mapObjectMetadataToGraphQLQuery({
objectMetadataItems,
objectMetadataItem: relationMetadataItem,
recordGqlFields: relationRecordGqlFields,
computeReferences,
isRootLevel: false,
objectPermissionsByObjectMetadataId,
})}
}
}`;
}
if (fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE) {
const joinColumnName = computeMorphRelationFieldJoinColumnName({
name: fieldMetadata.name,
targetObjectMetadataNameSingular:
morphRelation.targetObjectMetadata.nameSingular,
});
if (gqlField === joinColumnName) {
gqlMorphField += `${gqlField}
`;
continue;
}
gqlMorphField += `${relationFieldName}
${mapObjectMetadataToGraphQLQuery({
objectMetadataItems,
objectMetadataItem: relationMetadataItem,
recordGqlFields: relationRecordGqlFields,
computeReferences,
isRootLevel: false,
objectPermissionsByObjectMetadataId,
})}`;
}
}
return `${gqlMorphField}`;
}
if (
fieldType === FieldMetadataType.RELATION &&
fieldMetadata.relation?.type === RelationType.MANY_TO_ONE
) {
const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
fieldMetadata.relation?.targetObjectMetadata.id,
);
if (!isDefined(relationMetadataItem)) {
return '';
}
if (
isDefined(objectPermissionsByObjectMetadataId) &&
isDefined(relationMetadataItem.id)
) {
if (!isDefined(fieldMetadata.relation?.targetObjectMetadata.id)) {
throw new Error(
`Target object metadata id not found with field metadata ${fieldMetadata.name}`,
);
}
const objectPermission = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
fieldMetadata.relation?.targetObjectMetadata.id,
);
if (!objectPermission.canReadObjectRecords) {
return '';
}
}
if (gqlField === fieldMetadata.settings?.joinColumnName) {
return `${gqlField}`;
}
return `${gqlField}
${mapObjectMetadataToGraphQLQuery({
objectMetadataItems,
objectMetadataItem: relationMetadataItem,
recordGqlFields: relationRecordGqlFields,
computeReferences: computeReferences,
isRootLevel: false,
objectPermissionsByObjectMetadataId,
})}`;
}
if (
fieldType === FieldMetadataType.RELATION &&
fieldMetadata.relation?.type === RelationType.ONE_TO_MANY
) {
const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
fieldMetadata.relation?.targetObjectMetadata.id,
);
if (!isDefined(relationMetadataItem)) {
return '';
}
if (
isDefined(objectPermissionsByObjectMetadataId) &&
isDefined(relationMetadataItem.id)
) {
if (!isDefined(fieldMetadata.relation?.targetObjectMetadata.id)) {
throw new Error(
`Target object metadata id not found with field metadata ${fieldMetadata.name}`,
);
}
const objectPermission = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
fieldMetadata.relation?.targetObjectMetadata.id,
);
if (!objectPermission.canReadObjectRecords) {
return '';
}
}
return `${gqlField}
{
edges {
node ${mapObjectMetadataToGraphQLQuery({
objectMetadataItems,
objectMetadataItem: relationMetadataItem,
recordGqlFields: relationRecordGqlFields,
computeReferences,
isRootLevel: false,
objectPermissionsByObjectMetadataId,
})}
}
}`;
}
if (fieldType === FieldMetadataType.LINKS) {
return `${gqlField}
{
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}`;
}
if (fieldType === FieldMetadataType.CURRENCY) {
return `${gqlField}
{
amountMicros
currencyCode
}
`;
}
if (fieldType === FieldMetadataType.FULL_NAME) {
return `${gqlField}
{
firstName
lastName
}`;
}
if (fieldType === FieldMetadataType.ADDRESS) {
return `${gqlField}
{
addressStreet1
addressStreet2
addressCity
addressState
addressCountry
addressPostcode
addressLat
addressLng
}`;
}
if (fieldType === FieldMetadataType.ACTOR) {
return `${gqlField}
{
source
workspaceMemberId
name
context
}`;
}
if (fieldType === FieldMetadataType.EMAILS) {
return `${gqlField}
{
primaryEmail
additionalEmails
}`;
}
if (fieldType === FieldMetadataType.PHONES) {
return `${gqlField}
{
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}`;
}
if (fieldType === FieldMetadataType.RICH_TEXT_V2) {
return `${gqlField}
{
blocknote
markdown
}`;
}
return '';
};