Optimize metadata queries (#7013)

In this PR:

1. Refactor guards to avoid duplicated queries: WorkspaceAuthGuard and
UserAuthGuard only check for existence of workspace and user in the
request without querying the database
This commit is contained in:
Charles Bochet
2024-09-13 19:11:32 +02:00
committed by Charles Bochet
parent cf8b1161cc
commit 523df5398a
132 changed files with 818 additions and 6372 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
@ -219,7 +219,7 @@ export type ExecuteServerlessFunctionInput = {
/** Id of the serverless function to execute */
id: Scalars['UUID'];
/** Payload in JSON format */
payload?: InputMaybe<Scalars['JSON']>;
payload: Scalars['JSON'];
/** Version of the serverless function to execute */
version?: Scalars['String'];
};
@ -338,6 +338,7 @@ export enum MessageChannelVisibility {
export type Mutation = {
__typename?: 'Mutation';
activateWorkflowVersion: Scalars['Boolean'];
activateWorkspace: Workspace;
addUserToWorkspace: User;
authorizeApp: AuthorizeApp;
@ -347,15 +348,14 @@ export type Mutation = {
createOneObject: Object;
createOneServerlessFunction: ServerlessFunction;
createOneServerlessFunctionFromFile: ServerlessFunction;
deactivateWorkflowVersion: Scalars['Boolean'];
deleteCurrentWorkspace: Workspace;
deleteOneObject: Object;
deleteOneServerlessFunction: ServerlessFunction;
deleteUser: User;
disablePostgresProxy: PostgresCredentials;
disableWorkflowTrigger: Scalars['Boolean'];
emailPasswordResetLink: EmailPasswordResetLink;
enablePostgresProxy: PostgresCredentials;
enableWorkflowTrigger: Scalars['Boolean'];
exchangeAuthorizationCode: ExchangeAuthCode;
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
generateApiKeyToken: ApiKeyToken;
@ -382,6 +382,11 @@ export type Mutation = {
};
export type MutationActivateWorkflowVersionArgs = {
workflowVersionId: Scalars['String'];
};
export type MutationActivateWorkspaceArgs = {
data: ActivateWorkspaceInput;
};
@ -423,6 +428,11 @@ export type MutationCreateOneServerlessFunctionFromFileArgs = {
};
export type MutationDeactivateWorkflowVersionArgs = {
workflowVersionId: Scalars['String'];
};
export type MutationDeleteOneObjectArgs = {
input: DeleteOneObjectInput;
};
@ -433,21 +443,11 @@ export type MutationDeleteOneServerlessFunctionArgs = {
};
export type MutationDisableWorkflowTriggerArgs = {
workflowVersionId: Scalars['String'];
};
export type MutationEmailPasswordResetLinkArgs = {
email: Scalars['String'];
};
export type MutationEnableWorkflowTriggerArgs = {
workflowVersionId: Scalars['String'];
};
export type MutationExchangeAuthorizationCodeArgs = {
authorizationCode: Scalars['String'];
clientSecret?: InputMaybe<Scalars['String']>;
@ -638,9 +638,10 @@ export type Query = {
currentWorkspace: Workspace;
findWorkspaceFromInviteHash: Workspace;
getAISQLQuery: AisqlQueryResult;
getAvailablePackages: Scalars['JSON'];
getPostgresCredentials?: Maybe<PostgresCredentials>;
getProductPrices: ProductPricesEntity;
getServerlessFunctionSourceCode: Scalars['String'];
getServerlessFunctionSourceCode?: Maybe<Scalars['String']>;
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;

View File

@ -1,62 +0,0 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RelationType } from '@/settings/data-model/types/RelationType';
import {
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
export const getRelationDefinition = ({
objectMetadataItems,
fieldMetadataItemOnSourceRecord,
}: {
objectMetadataItems: ObjectMetadataItem[];
fieldMetadataItemOnSourceRecord: FieldMetadataItem;
}) => {
if (fieldMetadataItemOnSourceRecord.type !== FieldMetadataType.Relation) {
return null;
}
const relationMetadataItem =
fieldMetadataItemOnSourceRecord.fromRelationMetadata ||
fieldMetadataItemOnSourceRecord.toRelationMetadata;
if (!relationMetadataItem) return null;
const relationSourceFieldMetadataItemId =
'toFieldMetadataId' in relationMetadataItem
? relationMetadataItem.toFieldMetadataId
: relationMetadataItem.fromFieldMetadataId;
if (!relationSourceFieldMetadataItemId) return null;
// TODO: precise naming, is it relationTypeFromTargetPointOfView or relationTypeFromSourcePointOfView ?
const relationType =
relationMetadataItem.relationType === RelationMetadataType.OneToMany &&
fieldMetadataItemOnSourceRecord.toRelationMetadata
? ('MANY_TO_ONE' satisfies RelationType)
: (relationMetadataItem.relationType as RelationType);
const targetObjectMetadataNameSingular =
'toObjectMetadata' in relationMetadataItem
? relationMetadataItem.toObjectMetadata.nameSingular
: relationMetadataItem.fromObjectMetadata.nameSingular;
const targetObjectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === targetObjectMetadataNameSingular,
);
if (!targetObjectMetadataItem) return null;
const fieldMetadataItemOnTargetRecord = targetObjectMetadataItem.fields.find(
(field) => field.id === relationSourceFieldMetadataItemId,
);
if (!fieldMetadataItemOnTargetRecord) return null;
return {
fieldMetadataItemOnTargetRecord,
targetObjectMetadataItem,
relationType,
};
};

View File

@ -1,6 +1,5 @@
import { ApolloCache } from '@apollo/client';
import { getRelationDefinition } from '@/apollo/optimistic-effect/utils/getRelationDefinition';
import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
@ -45,16 +44,23 @@ export const triggerUpdateRelationsOptimisticEffect = ({
return;
}
const relationDefinition = getRelationDefinition({
fieldMetadataItemOnSourceRecord,
objectMetadataItems,
});
const relationDefinition =
fieldMetadataItemOnSourceRecord.relationDefinition;
if (!relationDefinition) {
return;
}
const { targetObjectMetadataItem, fieldMetadataItemOnTargetRecord } =
relationDefinition;
const { targetObjectMetadata, targetFieldMetadata } = relationDefinition;
const fullTargetObjectMetadataItem = objectMetadataItems.find(
({ nameSingular }) =>
nameSingular === targetObjectMetadata.nameSingular,
);
if (!fullTargetObjectMetadataItem) {
return;
}
const currentFieldValueOnSourceRecord:
| RecordGqlConnection
@ -80,7 +86,7 @@ export const triggerUpdateRelationsOptimisticEffect = ({
// it's an object record connection (we can still check it though as a safeguard)
const currentFieldValueOnSourceRecordIsARecordConnection =
isObjectRecordConnection(
targetObjectMetadataItem.nameSingular,
targetObjectMetadata.nameSingular,
currentFieldValueOnSourceRecord,
);
@ -93,7 +99,7 @@ export const triggerUpdateRelationsOptimisticEffect = ({
const updatedFieldValueOnSourceRecordIsARecordConnection =
isObjectRecordConnection(
targetObjectMetadataItem.nameSingular,
targetObjectMetadata.nameSingular,
updatedFieldValueOnSourceRecord,
);
@ -112,13 +118,13 @@ export const triggerUpdateRelationsOptimisticEffect = ({
// Instead of hardcoding it here
const shouldCascadeDeleteTargetRecords =
CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
targetObjectMetadataItem.nameSingular as CoreObjectNameSingular,
targetObjectMetadata.nameSingular as CoreObjectNameSingular,
);
if (shouldCascadeDeleteTargetRecords) {
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem: targetObjectMetadataItem,
objectMetadataItem: fullTargetObjectMetadataItem,
recordsToDelete: targetRecordsToDetachFrom,
objectMetadataItems,
});
@ -128,8 +134,8 @@ export const triggerUpdateRelationsOptimisticEffect = ({
cache,
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
sourceRecordId: currentSourceRecord.id,
fieldNameOnTargetRecord: fieldMetadataItemOnTargetRecord.name,
targetObjectNameSingular: targetObjectMetadataItem.nameSingular,
fieldNameOnTargetRecord: targetFieldMetadata.name,
targetObjectNameSingular: targetObjectMetadata.nameSingular,
targetRecordId: targetRecordToDetachFrom.id,
});
});
@ -145,8 +151,8 @@ export const triggerUpdateRelationsOptimisticEffect = ({
cache,
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
sourceRecordId: updatedSourceRecord.id,
fieldNameOnTargetRecord: fieldMetadataItemOnTargetRecord.name,
targetObjectNameSingular: targetObjectMetadataItem.nameSingular,
fieldNameOnTargetRecord: targetFieldMetadata.name,
targetObjectNameSingular: targetObjectMetadata.nameSingular,
targetRecordId: targetRecordToAttachTo.id,
}),
);

View File

@ -11,7 +11,7 @@ import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/compo
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useFavorites } from '../hooks/useFavorites';
const StyledContainer = styled(NavigationDrawerSection)`
@ -35,7 +35,7 @@ const StyledNavigationDrawerItem = styled(NavigationDrawerItem)`
`;
export const CurrentWorkspaceMemberFavorites = () => {
const currentUser = useRecoilValue(currentUserState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { favorites, handleReorderFavorite } = useFavorites();
const loading = useIsPrefetchLoading();
@ -44,12 +44,12 @@ export const CurrentWorkspaceMemberFavorites = () => {
useNavigationSection('Favorites');
const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState);
if (loading && isDefined(currentUser)) {
if (loading && isDefined(currentWorkspaceMember)) {
return <FavoritesSkeletonLoader />;
}
const currentWorkspaceMemberFavorites = favorites.filter(
(favorite) => favorite.workspaceMemberId === currentUser?.id,
(favorite) => favorite.workspaceMemberId === currentWorkspaceMember?.id,
);
if (

View File

@ -9,5 +9,6 @@ export type Favorite = {
avatarType: AvatarType;
link: string;
recordId: string;
workspaceMemberId: string;
__typename: 'Favorite';
};

View File

@ -19,8 +19,8 @@ export const sortFavorites = (
const relationObject = favorite[relationField.name];
const relationObjectNameSingular =
relationField.toRelationMetadata?.fromObjectMetadata.nameSingular ??
'';
relationField.relationDefinition?.targetObjectMetadata
.nameSingular ?? '';
const objectRecordIdentifier =
getObjectRecordIdentifierByNameSingular(
@ -38,6 +38,7 @@ export const sortFavorites = (
link: hasLinkToShowPage
? objectRecordIdentifier.linkToShowPage
: '',
workspaceMemberId: favorite.workspaceMemberId,
} as Favorite;
}
}

View File

@ -39,32 +39,6 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
isNullable
createdAt
updatedAt
fromRelationMetadata {
id
relationType
toObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
isRemote
}
toFieldMetadataId
}
toRelationMetadata {
id
relationType
fromObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
isRemote
}
fromFieldMetadataId
}
defaultValue
options
relationDefinition {

View File

@ -39,29 +39,27 @@ export const query = gql`
isNullable
createdAt
updatedAt
fromRelationMetadata {
id
relationType
toObjectMetadata {
relationDefinition {
relationId
direction
sourceObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
}
toFieldMetadataId
}
toRelationMetadata {
id
relationType
fromObjectMetadata {
sourceFieldMetadata {
id
name
}
targetObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
}
fromFieldMetadataId
targetFieldMetadata {
id
name
}
}
defaultValue
options

View File

@ -1,10 +1,10 @@
import { ReactNode } from 'react';
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
import { RelationMetadataType } from '~/generated/graphql';
import { RelationDefinitionType } from '~/generated/graphql';
import {
query,
@ -42,7 +42,7 @@ describe('useCreateOneRelationMetadataItem', () => {
await act(async () => {
const res = await result.current.createOneRelationMetadataItem({
relationType: RelationMetadataType.OneToOne,
relationType: RelationDefinitionType.OneToOne,
field: {
label: 'label',
},

View File

@ -1,11 +1,7 @@
import { useRecoilCallback } from 'recoil';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { RelationType } from '@/settings/data-model/types/RelationType';
import {
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
@ -17,39 +13,19 @@ export const useGetRelationMetadata = () =>
}: {
fieldMetadataItem: Pick<
FieldMetadataItem,
'fromRelationMetadata' | 'toRelationMetadata' | 'type'
'type' | 'relationDefinition'
>;
}) => {
if (fieldMetadataItem.type !== FieldMetadataType.Relation) return null;
const relationMetadata =
fieldMetadataItem.fromRelationMetadata ||
fieldMetadataItem.toRelationMetadata;
const relationDefinition = fieldMetadataItem.relationDefinition;
if (!relationMetadata) return null;
const relationFieldMetadataId =
'toFieldMetadataId' in relationMetadata
? relationMetadata.toFieldMetadataId
: relationMetadata.fromFieldMetadataId;
if (!relationFieldMetadataId) return null;
const relationType =
relationMetadata.relationType === RelationMetadataType.OneToMany &&
fieldMetadataItem.toRelationMetadata
? 'MANY_TO_ONE'
: (relationMetadata.relationType as RelationType);
const relationObjectMetadataNameSingular =
'toObjectMetadata' in relationMetadata
? relationMetadata.toObjectMetadata.nameSingular
: relationMetadata.fromObjectMetadata.nameSingular;
if (!relationDefinition) return null;
const relationObjectMetadataItem = snapshot
.getLoadable(
objectMetadataItemFamilySelector({
objectName: relationObjectMetadataNameSingular,
objectName: relationDefinition.targetObjectMetadata.nameSingular,
objectNameType: 'singular',
}),
)
@ -59,7 +35,7 @@ export const useGetRelationMetadata = () =>
const relationFieldMetadataItem =
relationObjectMetadataItem.fields.find(
(field) => field.id === relationFieldMetadataId,
(field) => field.id === relationDefinition.targetFieldMetadata.id,
);
if (!relationFieldMetadataItem) return null;
@ -67,7 +43,7 @@ export const useGetRelationMetadata = () =>
return {
relationFieldMetadataItem,
relationObjectMetadataItem,
relationType,
relationType: relationDefinition.direction,
};
},
[],

View File

@ -3,7 +3,6 @@ import { ThemeColor } from 'twenty-ui';
import {
Field,
Object as MetadataObject,
Relation,
RelationDefinition,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
@ -18,31 +17,9 @@ export type FieldMetadataItemOption = {
export type FieldMetadataItem = Omit<
Field,
| '__typename'
| 'fromRelationMetadata'
| 'toRelationMetadata'
| 'defaultValue'
| 'options'
| 'settings'
| 'relationDefinition'
'__typename' | 'defaultValue' | 'options' | 'settings' | 'relationDefinition'
> & {
__typename?: string;
fromRelationMetadata?:
| (Pick<Relation, 'id' | 'toFieldMetadataId' | 'relationType'> & {
toObjectMetadata: Pick<
Relation['toObjectMetadata'],
'id' | 'nameSingular' | 'namePlural' | 'isSystem' | 'isRemote'
>;
})
| null;
toRelationMetadata?:
| (Pick<Relation, 'id' | 'fromFieldMetadataId' | 'relationType'> & {
fromObjectMetadata: Pick<
Relation['fromObjectMetadata'],
'id' | 'nameSingular' | 'namePlural' | 'isSystem' | 'isRemote'
>;
})
| null;
defaultValue?: any;
options?: FieldMetadataItemOption[] | null;
relationDefinition?: {

View File

@ -1,5 +1,4 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
@ -20,17 +19,15 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
labelWidth,
}: FieldMetadataItemAsFieldDefinitionProps): FieldDefinition<FieldMetadata> => {
const relationObjectMetadataItem =
field.toRelationMetadata?.fromObjectMetadata ||
field.fromRelationMetadata?.toObjectMetadata;
field.relationDefinition?.targetObjectMetadata;
const relationFieldMetadataId =
field.toRelationMetadata?.fromFieldMetadataId ||
field.fromRelationMetadata?.toFieldMetadataId;
field.relationDefinition?.targetFieldMetadata.id;
const fieldDefintionMetadata = {
fieldName: field.name,
placeHolder: field.label,
relationType: parseFieldRelationType(field),
relationType: field.relationDefinition?.direction,
relationFieldMetadataId,
relationObjectMetadataNameSingular:
relationObjectMetadataItem?.nameSingular ?? '',

View File

@ -2,6 +2,7 @@ import { RelationType } from '@/settings/data-model/types/RelationType';
import {
CreateRelationInput,
Field,
RelationDefinitionType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
@ -24,8 +25,8 @@ export const formatRelationMetadataInput = (
// => Transform into ONE_TO_MANY and invert "from" and "to" data.
const isManyToOne = input.relationType === 'MANY_TO_ONE';
const relationType = isManyToOne
? RelationMetadataType.OneToMany
: (input.relationType as RelationMetadataType);
? RelationDefinitionType.OneToMany
: (input.relationType as RelationDefinitionType);
const { field: fromField, objectMetadataId: fromObjectMetadataId } =
isManyToOne ? input.connect : input;
const { field: toField, objectMetadataId: toObjectMetadataId } = isManyToOne
@ -51,7 +52,7 @@ export const formatRelationMetadataInput = (
fromLabel,
fromName,
fromObjectMetadataId,
relationType,
relationType: relationType as unknown as RelationMetadataType,
toDescription,
toIcon,
toLabel,

View File

@ -4,7 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import {
FieldMetadataType,
RelationMetadataType,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
@ -17,10 +17,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
computeReferences = false,
}: {
objectMetadataItems: ObjectMetadataItem[];
field: Pick<
FieldMetadataItem,
'name' | 'type' | 'toRelationMetadata' | 'fromRelationMetadata'
>;
field: Pick<FieldMetadataItem, 'name' | 'type' | 'relationDefinition'>;
relationrecordFields?: Record<string, any>;
computeReferences?: boolean;
}): any => {
@ -49,12 +46,12 @@ export const mapFieldMetadataToGraphQLQuery = ({
if (
fieldType === FieldMetadataType.Relation &&
field.toRelationMetadata?.relationType === RelationMetadataType.OneToMany
field.relationDefinition?.direction === RelationDefinitionType.ManyToOne
) {
const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
(field.toRelationMetadata as any)?.fromObjectMetadata?.id,
field.relationDefinition?.targetObjectMetadata.id,
);
if (isUndefined(relationMetadataItem)) {
@ -73,12 +70,12 @@ ${mapObjectMetadataToGraphQLQuery({
if (
fieldType === FieldMetadataType.Relation &&
field.fromRelationMetadata?.relationType === RelationMetadataType.OneToMany
field.relationDefinition?.direction === RelationDefinitionType.OneToMany
) {
const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
(field.fromRelationMetadata as any)?.toObjectMetadata?.id,
field.relationDefinition?.targetObjectMetadata.id,
);
if (isUndefined(relationMetadataItem)) {

View File

@ -1,55 +0,0 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldDefinitionRelationType } from '@/object-record/record-field/types/FieldDefinition';
import {
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined';
export const parseFieldRelationType = (
field: FieldMetadataItem | undefined,
): FieldDefinitionRelationType | undefined => {
if (!field || field.type !== FieldMetadataType.Relation) return;
const config: Record<
RelationMetadataType,
{ from: FieldDefinitionRelationType; to: FieldDefinitionRelationType }
> = {
[RelationMetadataType.ManyToMany]: {
from: 'FROM_MANY_OBJECTS',
to: 'TO_MANY_OBJECTS',
},
[RelationMetadataType.OneToMany]: {
from: 'FROM_MANY_OBJECTS',
to: 'TO_ONE_OBJECT',
},
[RelationMetadataType.ManyToOne]: {
from: 'TO_ONE_OBJECT',
to: 'FROM_MANY_OBJECTS',
},
[RelationMetadataType.OneToOne]: {
from: 'FROM_ONE_OBJECT',
to: 'TO_ONE_OBJECT',
},
};
if (
isDefined(field.fromRelationMetadata) &&
field.fromRelationMetadata.relationType in config
) {
return config[field.fromRelationMetadata.relationType].from;
}
if (
isDefined(field.toRelationMetadata) &&
field.toRelationMetadata.relationType in config
) {
return config[field.toRelationMetadata.relationType].to;
}
throw new Error(
`Cannot determine field relation type for field : ${JSON.stringify(
field,
)}.`,
);
};

View File

@ -6,7 +6,6 @@ import { metadataLabelSchema } from '@/object-metadata/validation-schemas/metada
import {
FieldMetadataType,
RelationDefinitionType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema';
@ -16,24 +15,6 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
createdAt: z.string().datetime(),
defaultValue: z.any().optional(),
description: z.string().trim().nullable().optional(),
fromRelationMetadata: z
.object({
__typename: z.literal('relation').optional(),
id: z.string().uuid(),
relationType: z.nativeEnum(RelationMetadataType),
toFieldMetadataId: z.string().uuid(),
toObjectMetadata: z.object({
__typename: z.literal('object').optional(),
dataSourceId: z.string().uuid(),
id: z.string().uuid(),
isRemote: z.boolean(),
isSystem: z.boolean(),
namePlural: z.string().trim().min(1),
nameSingular: z.string().trim().min(1),
}),
})
.nullable()
.optional(),
icon: z.string().startsWith('Icon').trim().nullable(),
id: z.string().uuid(),
isActive: z.boolean(),
@ -84,24 +65,6 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
})
.nullable()
.optional(),
toRelationMetadata: z
.object({
__typename: z.literal('relation').optional(),
id: z.string().uuid(),
relationType: z.nativeEnum(RelationMetadataType),
fromFieldMetadataId: z.string().uuid(),
fromObjectMetadata: z.object({
__typename: z.literal('object').optional(),
id: z.string().uuid(),
dataSourceId: z.string().uuid(),
isRemote: z.boolean(),
isSystem: z.boolean(),
namePlural: z.string().trim().min(1),
nameSingular: z.string().trim().min(1),
}),
})
.nullable()
.optional(),
type: z.nativeEnum(FieldMetadataType),
updatedAt: z.string().datetime(),
}) satisfies z.ZodType<FieldMetadataItem>;

View File

@ -4,17 +4,6 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldMetadata } from './FieldMetadata';
export type FieldDefinitionRelationType =
| 'FROM_MANY_OBJECTS'
| 'FROM_ONE_OBJECT'
| 'TO_MANY_OBJECTS'
| 'TO_ONE_OBJECT';
export type RelationDirections = {
from: FieldDefinitionRelationType;
to: FieldDefinitionRelationType;
};
export type FieldDefinition<T extends FieldMetadata> = {
fieldMetadataId: string;
label: string;

View File

@ -3,8 +3,8 @@ import { ThemeColor } from 'twenty-ui';
import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
import { ZodHelperLiteral } from '@/object-record/record-field/types/ZodHelperLiteral';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { WithNarrowedStringLiteralProperty } from '~/types/WithNarrowedStringLiteralProperty';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { CurrencyCode } from './CurrencyCode';
export type FieldUuidMetadata = {
@ -110,35 +110,17 @@ export type FieldPositionMetadata = {
fieldName: string;
};
export type FieldDefinitionRelationType =
| 'FROM_MANY_OBJECTS'
| 'FROM_ONE_OBJECT'
| 'TO_MANY_OBJECTS'
| 'TO_ONE_OBJECT';
export type FieldRelationMetadata = {
fieldName: string;
objectMetadataNameSingular?: string;
relationFieldMetadataId: string;
relationObjectMetadataNamePlural: string;
relationObjectMetadataNameSingular: string;
relationType?: FieldDefinitionRelationType;
relationType?: RelationDefinitionType;
targetFieldMetadataName?: string;
useEditButton?: boolean;
};
export type FieldRelationOneMetadata = WithNarrowedStringLiteralProperty<
FieldRelationMetadata,
'relationType',
'TO_ONE_OBJECT'
>;
export type FieldRelationManyMetadata = WithNarrowedStringLiteralProperty<
FieldRelationMetadata,
'relationType',
'FROM_MANY_OBJECTS'
>;
export type FieldSelectMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;

View File

@ -1,9 +1,11 @@
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldRelationManyMetadata } from '../FieldMetadata';
import { FieldMetadata } from '../FieldMetadata';
export const isFieldRelationFromManyObjects = (
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
): field is FieldDefinition<FieldRelationManyMetadata> =>
isFieldRelation(field) && field.metadata.relationType === 'FROM_MANY_OBJECTS';
): field is FieldDefinition<FieldMetadata> =>
isFieldRelation(field) &&
field.metadata.relationType === RelationDefinitionType.OneToMany;

View File

@ -1,9 +1,11 @@
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldRelationOneMetadata } from '../FieldMetadata';
import { FieldMetadata } from '../FieldMetadata';
export const isFieldRelationToOneObject = (
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
): field is FieldDefinition<FieldRelationOneMetadata> =>
isFieldRelation(field) && field.metadata.relationType === 'TO_ONE_OBJECT';
): field is FieldDefinition<FieldMetadata> =>
isFieldRelation(field) &&
field.metadata.relationType === RelationDefinitionType.ManyToOne;

View File

@ -1,6 +1,7 @@
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import {
csvDownloader,
displayedExportProgress,
@ -35,7 +36,10 @@ describe('generateCsv', () => {
{ label: 'Nested', metadata: { fieldName: 'nested' } },
{
label: 'Relation',
metadata: { fieldName: 'relation', relationType: 'TO_ONE_OBJECT' },
metadata: {
fieldName: 'relation',
relationType: RelationDefinitionType.ManyToOne,
},
},
] as ColumnDefinition<FieldMetadata>[];
const rows = [

View File

@ -9,6 +9,7 @@ import {
} from '@/object-record/record-index/options/hooks/useTableData';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -43,7 +44,7 @@ export const generateCsv: GenerateExport = ({
const columnsToExport = columns.filter(
(col) =>
!('relationType' in col.metadata && col.metadata.relationType) ||
col.metadata.relationType === 'TO_ONE_OBJECT',
col.metadata.relationType === RelationDefinitionType.ManyToOne,
);
const objectIdColumn: ColumnDefinition<FieldMetadata> = {

View File

@ -7,6 +7,7 @@ import { Task } from '@/activities/types/Task';
import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord';
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
@ -56,6 +57,8 @@ export const RecordShowContainer = ({
objectNameSingular,
});
const { objectMetadataItems } = useObjectMetadataItems();
const { labelIdentifierFieldMetadataItem } =
useLabelIdentifierFieldMetadataItem({
objectNameSingular,
@ -119,7 +122,7 @@ export const RecordShowContainer = ({
const availableFieldMetadataItems = objectMetadataItem.fields
.filter(
(fieldMetadataItem) =>
isFieldCellSupported(fieldMetadataItem) &&
isFieldCellSupported(fieldMetadataItem, objectMetadataItems) &&
fieldMetadataItem.id !== labelIdentifierFieldMetadataItem?.id,
)
.sort((fieldMetadataItemA, fieldMetadataItemB) =>

View File

@ -1,7 +1,7 @@
import { useCallback, useContext } from 'react';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useCallback, useContext } from 'react';
import {
IconChevronDown,
IconComponent,
@ -11,6 +11,7 @@ import {
} from 'twenty-ui';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { RecordChip } from '@/object-record/components/RecordChip';
@ -37,6 +38,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
const StyledListItem = styled(RecordDetailRecordsListItem)<{
isDropdownOpen?: boolean;
@ -89,12 +91,14 @@ export const RecordDetailRelationRecordsListItem = ({
relationType,
} = fieldDefinition.metadata as FieldRelationMetadata;
const isToOneObject = relationType === 'TO_ONE_OBJECT';
const isToOneObject = relationType === RelationDefinitionType.ManyToOne;
const { objectMetadataItem: relationObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular: relationObjectMetadataNameSingular,
});
const { objectMetadataItems } = useObjectMetadataItems();
const persistField = usePersistField();
const { updateOneRecord: updateOneRelationRecord } = useUpdateOneRecord({
@ -111,7 +115,7 @@ export const RecordDetailRelationRecordsListItem = ({
const availableRelationFieldMetadataItems = relationObjectMetadataItem.fields
.filter(
(fieldMetadataItem) =>
isFieldCellSupported(fieldMetadataItem) &&
isFieldCellSupported(fieldMetadataItem, objectMetadataItems) &&
fieldMetadataItem.id !==
relationObjectMetadataItem.labelIdentifierFieldMetadataId &&
fieldMetadataItem.id !== relationFieldMetadataId,

View File

@ -32,6 +32,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
type RecordDetailRelationSectionProps = {
loading: boolean;
@ -67,8 +68,8 @@ export const RecordDetailRelationSection = ({
>(recordStoreFamilySelector({ recordId, fieldName }));
// TODO: use new relation type
const isToOneObject = relationType === 'TO_ONE_OBJECT';
const isFromManyObjects = relationType === 'FROM_MANY_OBJECTS';
const isToOneObject = relationType === RelationDefinitionType.ManyToOne;
const isToManyObjects = RelationDefinitionType.OneToMany;
const relationRecords: ObjectRecord[] =
fieldValue && isToOneObject
@ -160,7 +161,7 @@ export const RecordDetailRelationSection = ({
<RecordDetailSectionHeader
title={fieldDefinition.label}
link={
isFromManyObjects
isToManyObjects
? {
to: filterLinkHref,
label: `All (${relationRecords.length})`,

View File

@ -34,22 +34,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: {
__typename: 'relation',
id: '0cf72416-3d94-4d94-abf3-7dc9d734435b',
relationType: 'ONE_TO_MANY',
fromObjectMetadata: {
__typename: 'object',
id: '79c2d29c-76f6-432f-91c9-df1259b73d95',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'company',
namePlural: 'companies',
isSystem: false,
isRemote: false,
},
fromFieldMetadataId: '7b281010-5f47-4771-b3f5-f4bcd24ed1b5',
},
defaultValue: null,
options: null,
relationDefinition: {
@ -94,8 +78,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: "''",
options: null,
relationDefinition: null,
@ -114,8 +96,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: "''",
options: null,
relationDefinition: null,
@ -134,22 +114,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: {
__typename: 'relation',
id: 'd76f949d-023d-4b45-a71e-f39e3b1562ba',
relationType: 'ONE_TO_MANY',
toObjectMetadata: {
__typename: 'object',
id: '82222ca2-dd40-44ec-b8c5-eb0eca9ec625',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'activityTarget',
namePlural: 'activityTargets',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: 'f5f515cc-6d8a-44c3-b2d4-f04b9868a9c5',
},
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: {
@ -194,22 +158,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: {
__typename: 'relation',
id: 'a5a61d23-8ac9-4014-9441-ec3a1781a661',
relationType: 'ONE_TO_MANY',
toObjectMetadata: {
__typename: 'object',
id: '494b9b7c-a44e-4d52-b274-cdfb0e322165',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'opportunity',
namePlural: 'opportunities',
isSystem: false,
isRemote: false,
},
toFieldMetadataId: '86559a6f-6afc-4d5c-9bed-fc74d063791b',
},
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: {
@ -254,22 +202,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: {
__typename: 'relation',
id: '456f7875-b48c-4795-a0c7-a69d7339afee',
relationType: 'ONE_TO_MANY',
toObjectMetadata: {
__typename: 'object',
id: 'eba13fca-57b7-470c-8c23-a0e640e04ffb',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'calendarEventParticipant',
namePlural: 'calendarEventParticipants',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: 'c1cdebda-b514-4487-9b9c-aa59d8fca8eb',
},
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: {
@ -314,8 +246,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: 'now',
options: null,
relationDefinition: null,
@ -334,22 +264,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: {
__typename: 'relation',
id: '31542774-fb15-4d01-b00b-8fc94887f458',
relationType: 'ONE_TO_MANY',
toObjectMetadata: {
__typename: 'object',
id: 'f08422e2-14cd-4966-9cd3-bce0302cc56f',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'favorite',
namePlural: 'favorites',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: '67d28b17-ff3c-49b4-a6da-1354be9634b0',
},
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: {
@ -394,8 +308,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: {
primaryLinkUrl: "''",
primaryLinkLabel: "''",
@ -417,22 +329,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: {
__typename: 'relation',
id: 'c0cc3456-afa4-46e0-820d-2db0b63a8273',
relationType: 'ONE_TO_MANY',
toObjectMetadata: {
__typename: 'object',
id: '0e3c9a9d-8a60-4671-a466-7b840a422da2',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'attachment',
namePlural: 'attachments',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: 'a920a0d6-8e71-4ab8-90b9-ab540e04732a',
},
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: {
@ -477,8 +373,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: "''",
options: null,
relationDefinition: null,
@ -497,8 +391,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: null,
@ -517,8 +409,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: "''",
options: null,
relationDefinition: null,
@ -537,8 +427,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: "''",
options: null,
relationDefinition: null,
@ -557,8 +445,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: 'now',
options: null,
relationDefinition: null,
@ -577,8 +463,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: null,
@ -597,22 +481,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: {
__typename: 'relation',
id: '25150feb-fcd7-407e-b5fa-ffe58a0450ac',
relationType: 'ONE_TO_MANY',
toObjectMetadata: {
__typename: 'object',
id: '83b5ff3e-975e-4dc9-ba4d-c645a0d8afb2',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'timelineActivity',
namePlural: 'timelineActivities',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: '556a12d4-ef0a-4232-963f-0f317f4c5ef5',
},
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: {
@ -657,8 +525,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: {
lastName: "''",
firstName: "''",
@ -680,8 +546,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: {
primaryLinkUrl: "''",
primaryLinkLabel: "''",
@ -703,22 +567,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: {
__typename: 'relation',
id: 'e2eb7156-6e65-4bf8-922b-670179744f27',
relationType: 'ONE_TO_MANY',
toObjectMetadata: {
__typename: 'object',
id: 'ffd8e640-84b7-4ed6-99e9-14def0f9d82b',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'messageParticipant',
namePlural: 'messageParticipants',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: '8c4593a1-ad40-4681-92fe-43ad4fe60205',
},
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: {
@ -763,8 +611,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: 'uuid',
options: null,
relationDefinition: null,

View File

@ -6,7 +6,10 @@ import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOp
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import {
FieldMetadataType,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
export const useOpenObjectRecordsSpreasheetImportDialog = (
objectNameSingular: string,
@ -37,7 +40,8 @@ export const useOpenObjectRecordsSpreasheetImportDialog = (
(!fieldMetadataItem.isSystem || fieldMetadataItem.name === 'id') &&
fieldMetadataItem.name !== 'createdAt' &&
(fieldMetadataItem.type !== FieldMetadataType.Relation ||
fieldMetadataItem.toRelationMetadata),
fieldMetadataItem.relationDefinition?.direction ===
RelationDefinitionType.ManyToOne),
)
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),

View File

@ -2,14 +2,14 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { TABLE_COLUMNS_DENY_LIST } from '@/object-record/relation-picker/constants/TableColumnsDenyList';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
export const filterAvailableTableColumns = (
columnDefinition: ColumnDefinition<FieldMetadata>,
): boolean => {
if (
isFieldRelation(columnDefinition) &&
columnDefinition.metadata?.relationType !== 'TO_ONE_OBJECT' &&
columnDefinition.metadata?.relationType !== 'FROM_MANY_OBJECTS'
columnDefinition.metadata?.relationType !== RelationDefinitionType.ManyToOne
) {
return false;
}

View File

@ -4,10 +4,7 @@ import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFiel
import { v4 } from 'uuid';
export const generateDefaultFieldValue = (
fieldMetadataItem: Pick<
FieldMetadataItem,
'defaultValue' | 'type' | 'fromRelationMetadata'
>,
fieldMetadataItem: Pick<FieldMetadataItem, 'defaultValue' | 'type'>,
) => {
const defaultValue = isFieldValueEmpty({
fieldValue: fieldMetadataItem.defaultValue,

View File

@ -1,10 +1,11 @@
import { isNonEmptyString } from '@sniptt/guards';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import {
FieldMetadataType,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
export const generateEmptyFieldValue = (
fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'fromRelationMetadata'>,
fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'relationDefinition'>,
) => {
switch (fieldMetadataItem.type) {
case FieldMetadataType.Email:
@ -62,10 +63,8 @@ export const generateEmptyFieldValue = (
}
case FieldMetadataType.Relation: {
if (
!isNonEmptyString(
fieldMetadataItem.fromRelationMetadata?.toObjectMetadata
?.nameSingular,
)
fieldMetadataItem.relationDefinition?.direction ===
RelationDefinitionType.ManyToOne
) {
return null;
}

View File

@ -1,12 +1,16 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
import {
FieldMetadataType,
RelationMetadataType,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
export const isFieldCellSupported = (fieldMetadataItem: FieldMetadataItem) => {
export const isFieldCellSupported = (
fieldMetadataItem: FieldMetadataItem,
objectMetadataItems: ObjectMetadataItem[],
) => {
if (
[
FieldMetadataType.Uuid,
@ -18,17 +22,17 @@ export const isFieldCellSupported = (fieldMetadataItem: FieldMetadataItem) => {
}
if (fieldMetadataItem.type === FieldMetadataType.Relation) {
const relationMetadata =
fieldMetadataItem.fromRelationMetadata ??
fieldMetadataItem.toRelationMetadata;
const relationObjectMetadataItem =
fieldMetadataItem.fromRelationMetadata?.toObjectMetadata ??
fieldMetadataItem.toRelationMetadata?.fromObjectMetadata;
const relationObjectMetadataItemId =
fieldMetadataItem.relationDefinition?.targetObjectMetadata.id;
const relationObjectMetadataItem = objectMetadataItems.find(
(item) => item.id === relationObjectMetadataItemId,
);
// Hack to display targets on Notes and Tasks
if (
fieldMetadataItem.fromRelationMetadata?.toObjectMetadata?.nameSingular ===
CoreObjectNameSingular.NoteTarget &&
fieldMetadataItem.relationDefinition?.targetObjectMetadata
?.nameSingular === CoreObjectNameSingular.NoteTarget &&
fieldMetadataItem.relationDefinition?.sourceObjectMetadata
.nameSingular === CoreObjectNameSingular.Note
) {
@ -36,8 +40,8 @@ export const isFieldCellSupported = (fieldMetadataItem: FieldMetadataItem) => {
}
if (
fieldMetadataItem.fromRelationMetadata?.toObjectMetadata?.nameSingular ===
CoreObjectNameSingular.TaskTarget &&
fieldMetadataItem.relationDefinition?.targetObjectMetadata
?.nameSingular === CoreObjectNameSingular.TaskTarget &&
fieldMetadataItem.relationDefinition?.sourceObjectMetadata
.nameSingular === CoreObjectNameSingular.Task
) {
@ -45,9 +49,10 @@ export const isFieldCellSupported = (fieldMetadataItem: FieldMetadataItem) => {
}
if (
!relationMetadata ||
!fieldMetadataItem.relationDefinition ||
// TODO: Many to many relations are not supported yet.
relationMetadata.relationType === RelationMetadataType.ManyToMany ||
fieldMetadataItem.relationDefinition.direction ===
RelationDefinitionType.ManyToMany ||
!relationObjectMetadataItem ||
!isObjectMetadataAvailableForRelation(relationObjectMetadataItem)
) {

View File

@ -1,8 +1,8 @@
import { isString } from '@sniptt/guards';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isFieldRelationToOneValue } from '@/object-record/record-field/types/guards/isFieldRelationToOneValue';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { getUrlHostName } from '~/utils/url/getUrlHostName';
@ -29,7 +29,8 @@ export const sanitizeRecordInput = ({
if (
fieldMetadataItem.type === FieldMetadataType.Relation &&
isFieldRelationToOneValue(fieldValue)
fieldMetadataItem.relationDefinition?.direction ===
RelationDefinitionType.ManyToOne
) {
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
@ -41,6 +42,14 @@ export const sanitizeRecordInput = ({
: undefined;
}
if (
fieldMetadataItem.type === FieldMetadataType.Relation &&
fieldMetadataItem.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
) {
return undefined;
}
return [fieldName, fieldValue];
})
.filter(isDefined),

View File

@ -1,12 +1,12 @@
import {
IconComponent,
IconRelationManyToMany,
IconRelationManyToOne,
IconRelationOneToMany,
IconRelationOneToOne,
} from 'twenty-ui';
import { RelationMetadataType } from '~/generated-metadata/graphql';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import OneToManySvg from '../assets/OneToMany.svg';
import OneToOneSvg from '../assets/OneToOne.svg';
import { RelationType } from '../types/RelationType';
@ -20,20 +20,27 @@ export const RELATION_TYPES: Record<
isImageFlipped?: boolean;
}
> = {
[RelationMetadataType.OneToMany]: {
[RelationDefinitionType.OneToMany]: {
label: 'Has many',
Icon: IconRelationOneToMany,
imageSrc: OneToManySvg,
},
[RelationMetadataType.OneToOne]: {
[RelationDefinitionType.OneToOne]: {
label: 'Has one',
Icon: IconRelationOneToOne,
imageSrc: OneToOneSvg,
},
MANY_TO_ONE: {
[RelationDefinitionType.ManyToOne]: {
label: 'Belongs to one',
Icon: IconRelationManyToOne,
imageSrc: OneToManySvg,
isImageFlipped: true,
},
// Not supported yet
[RelationDefinitionType.ManyToMany]: {
label: 'Belongs to many',
Icon: IconRelationManyToMany,
imageSrc: OneToManySvg,
isImageFlipped: true,
},
};

View File

@ -1,5 +1,5 @@
import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled';
import { Controller, useFormContext } from 'react-hook-form';
import { useIcons } from 'twenty-ui';
import { z } from 'zod';
@ -14,6 +14,7 @@ import { RelationType } from '@/settings/data-model/types/RelationType';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { Select } from '@/ui/input/components/Select';
import { TextInput } from '@/ui/input/components/TextInput';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
export const settingsDataModelFieldRelationFormSchema = z.object({
relation: z.object({
@ -23,7 +24,10 @@ export const settingsDataModelFieldRelationFormSchema = z.object({
}),
objectMetadataId: z.string().uuid(),
type: z.enum(
Object.keys(RELATION_TYPES) as [RelationType, ...RelationType[]],
Object.keys(RELATION_TYPES) as [
RelationDefinitionType,
...RelationDefinitionType[],
],
),
}),
});
@ -33,10 +37,7 @@ export type SettingsDataModelFieldRelationFormValues = z.infer<
>;
type SettingsDataModelFieldRelationFormProps = {
fieldMetadataItem: Pick<
FieldMetadataItem,
'fromRelationMetadata' | 'toRelationMetadata' | 'type'
>;
fieldMetadataItem: Pick<FieldMetadataItem, 'type'>;
};
const StyledContainer = styled.div`

View File

@ -1,5 +1,5 @@
import { useFormContext } from 'react-hook-form';
import styled from '@emotion/styled';
import { useFormContext } from 'react-hook-form';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';

View File

@ -5,15 +5,12 @@ import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMe
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
import { RelationMetadataType } from '~/generated-metadata/graphql';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
export const useRelationSettingsFormInitialValues = ({
fieldMetadataItem,
}: {
fieldMetadataItem?: Pick<
FieldMetadataItem,
'fromRelationMetadata' | 'toRelationMetadata' | 'type'
>;
fieldMetadataItem?: Pick<FieldMetadataItem, 'type' | 'relationDefinition'>;
}) => {
const { objectMetadataItems } = useFilteredObjectMetadataItems();
@ -39,7 +36,7 @@ export const useRelationSettingsFormInitialValues = ({
);
const initialRelationType =
relationTypeFromFieldMetadata ?? RelationMetadataType.OneToMany;
relationTypeFromFieldMetadata ?? RelationDefinitionType.OneToMany;
return {
disableFieldEdition:

View File

@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { Edge, Node } from 'reactflow';
import dagre from '@dagrejs/dagre';
import { useTheme } from '@emotion/react';
import { useEffect } from 'react';
import { Edge, Node } from 'reactflow';
import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
@ -43,10 +43,10 @@ export const SettingsDataModelOverviewEffect = ({
for (const field of object.fields) {
if (
isDefined(field.toRelationMetadata) &&
isDefined(field.relationDefinition) &&
isDefined(
items.find(
(x) => x.id === field.toRelationMetadata?.fromObjectMetadata.id,
(x) => x.id === field.relationDefinition?.targetObjectMetadata.id,
),
)
) {
@ -59,8 +59,8 @@ export const SettingsDataModelOverviewEffect = ({
id: `${sourceObj}-${targetObj}`,
source: object.namePlural,
sourceHandle: `${field.id}-right`,
target: field.toRelationMetadata.fromObjectMetadata.namePlural,
targetHandle: `${field.toRelationMetadata.fromFieldMetadataId}-left`,
target: field.relationDefinition.targetObjectMetadata.namePlural,
targetHandle: `${field.relationDefinition.targetObjectMetadata}-left`,
type: 'smoothstep',
style: {
strokeWidth: 1,
@ -70,8 +70,8 @@ export const SettingsDataModelOverviewEffect = ({
markerStart: 'marker',
data: {
sourceField: field.id,
targetField: field.toRelationMetadata.fromFieldMetadataId,
relation: field.toRelationMetadata.relationType,
targetField: field.relationDefinition.targetFieldMetadata.id,
relation: field.relationDefinition.direction,
sourceObject: sourceObj,
targetObject: targetObj,
},

View File

@ -6,6 +6,7 @@ import { useIcons } from 'twenty-ui';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
type ObjectFieldRowProps = {
field: FieldMetadataItem;
@ -42,21 +43,33 @@ export const ObjectFieldRow = ({ field }: ObjectFieldRowProps) => {
{Icon && <Icon size={theme.icon.size.md} />}
<StyledFieldName>{relatedObject?.labelPlural ?? ''}</StyledFieldName>
<Handle
type={field.toRelationMetadata ? 'source' : 'target'}
type={
field.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
? 'source'
: 'target'
}
position={Position.Right}
id={`${field.id}-right`}
className={
field.fromRelationMetadata
field.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
? 'right-handle source-handle'
: 'right-handle target-handle'
}
/>
<Handle
type={field.toRelationMetadata ? 'source' : 'target'}
type={
field.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
? 'source'
: 'target'
}
position={Position.Left}
id={`${field.id}-left`}
className={
field.fromRelationMetadata
field.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
? 'left-handle source-handle'
: 'left-handle target-handle'
}

View File

@ -29,7 +29,7 @@ import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMe
import { View } from '@/views/types/View';
import { useNavigate } from 'react-router-dom';
import { useRecoilState } from 'recoil';
import { RelationMetadataType } from '~/generated-metadata/graphql';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem';
import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType';
@ -224,8 +224,8 @@ export const SettingsObjectFieldItemTableRow = ({
<SettingsObjectFieldDataType
Icon={RelationIcon}
label={
relationType === RelationMetadataType.ManyToOne ||
relationType === RelationMetadataType.OneToOne
relationType === RelationDefinitionType.ManyToOne ||
relationType === RelationDefinitionType.OneToOne
? relationObjectMetadataItem?.labelSingular
: relationObjectMetadataItem?.labelPlural
}

View File

@ -1,5 +1,3 @@
import { RelationMetadataType } from '~/generated-metadata/graphql';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
export type RelationType =
| Exclude<RelationMetadataType, 'MANY_TO_MANY'>
| 'MANY_TO_ONE';
export type RelationType = RelationDefinitionType;

View File

@ -2,7 +2,10 @@ import { COMPANY_LABEL_IDENTIFIER_FIELD_METADATA_ID } from '@/object-metadata/ut
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import {
FieldMetadataType,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
[
@ -65,7 +68,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation,
metadata: {
fieldName: 'favorites',
relationType: 'FROM_MANY_OBJECTS',
relationType: RelationDefinitionType.OneToMany,
relationObjectMetadataNameSingular: '',
relationObjectMetadataNamePlural: '',
objectMetadataNameSingular: 'company',
@ -99,7 +102,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation,
metadata: {
fieldName: 'accountOwner',
relationType: 'TO_ONE_OBJECT',
relationType: RelationDefinitionType.ManyToOne,
relationObjectMetadataNameSingular: 'workspaceMember',
relationObjectMetadataNamePlural: 'workspaceMembers',
objectMetadataNameSingular: 'company',
@ -116,7 +119,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation,
metadata: {
fieldName: 'people',
relationType: 'FROM_MANY_OBJECTS',
relationType: RelationDefinitionType.OneToMany,
relationObjectMetadataNameSingular: '',
relationObjectMetadataNamePlural: '',
objectMetadataNameSingular: 'company',
@ -133,7 +136,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation,
metadata: {
fieldName: 'attachments',
relationType: 'FROM_MANY_OBJECTS',
relationType: RelationDefinitionType.OneToMany,
relationObjectMetadataNameSingular: '',
relationObjectMetadataNamePlural: '',
objectMetadataNameSingular: 'company',
@ -201,7 +204,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation,
metadata: {
fieldName: 'opportunities',
relationType: 'FROM_MANY_OBJECTS',
relationType: RelationDefinitionType.OneToMany,
relationObjectMetadataNameSingular: '',
relationObjectMetadataNamePlural: '',
objectMetadataNameSingular: 'company',
@ -235,7 +238,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation,
metadata: {
fieldName: 'activityTargets',
relationType: 'FROM_MANY_OBJECTS',
relationType: RelationDefinitionType.OneToMany,
relationObjectMetadataNameSingular: '',
relationObjectMetadataNamePlural: '',
objectMetadataNameSingular: 'company',

View File

@ -1,8 +1,8 @@
import { useMemo } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import { useApolloClient } from '@apollo/client';
import { isNonEmptyString } from '@sniptt/guards';
import qs from 'qs';
import { useMemo } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import z from 'zod';
@ -92,12 +92,12 @@ export const useViewFromQueryParams = () => {
if (isUndefinedOrNull(filterDefinition)) return null;
const relationObjectMetadataNameSingular =
fieldMetadataItem.toRelationMetadata?.fromObjectMetadata
.nameSingular;
fieldMetadataItem.relationDefinition?.targetObjectMetadata
?.nameSingular;
const relationObjectMetadataNamePlural =
fieldMetadataItem.toRelationMetadata?.fromObjectMetadata
.namePlural;
fieldMetadataItem.relationDefinition?.targetObjectMetadata
?.namePlural;
const relationObjectMetadataItem =
relationObjectMetadataNameSingular

View File

@ -83,22 +83,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
name: 'myCustom',
},
},
toRelationMetadata: null,
fromRelationMetadata: {
__typename: 'relation',
id: 'c5cdbacd-2489-4409-be9e-bb4cb38f6ddd',
relationType: 'ONE_TO_MANY',
toFieldMetadataId: 'c9607ed7-168d-4743-a56a-689ffcfffe98',
toObjectMetadata: {
__typename: 'object',
id: 'dba899da-7d88-41ac-b70e-5ea612ab4b2e',
dataSourceId: 'd36e6a2d-28bc-459d-afd5-fe18e4405729',
nameSingular: 'viewField',
namePlural: 'viewFields',
isSystem: true,
isRemote: false,
},
},
},
},
{
@ -120,8 +104,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
updatedAt: '2024-04-08T12:48:49.538Z',
defaultValue: null,
relationDefinition: null,
fromRelationMetadata: null,
toRelationMetadata: null,
},
},
{
@ -143,8 +125,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
updatedAt: '2024-04-08T12:48:49.538Z',
defaultValue: null,
relationDefinition: null,
fromRelationMetadata: null,
toRelationMetadata: null,
},
},
{
@ -166,8 +146,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
updatedAt: '2024-04-08T12:48:49.538Z',
defaultValue: "''",
relationDefinition: null,
fromRelationMetadata: null,
toRelationMetadata: null,
},
},
{
@ -189,8 +167,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
updatedAt: '2024-04-08T12:48:49.538Z',
defaultValue: 'now',
relationDefinition: null,
fromRelationMetadata: null,
toRelationMetadata: null,
},
},
{
@ -212,8 +188,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
updatedAt: '2024-04-08T12:48:49.538Z',
defaultValue: 'now',
relationDefinition: null,
fromRelationMetadata: null,
toRelationMetadata: null,
},
},
{
@ -235,8 +209,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
updatedAt: '2024-04-08T12:48:49.538Z',
defaultValue: 'uuid',
relationDefinition: null,
fromRelationMetadata: null,
toRelationMetadata: null,
},
},
{
@ -277,8 +249,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
updatedAt: '2024-04-08T12:48:49.538Z',
defaultValue: null,
relationDefinition: null,
fromRelationMetadata: null,
toRelationMetadata: null,
},
},
],