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:42:22 +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
+16 -15
View File
@@ -1,5 +1,5 @@
import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type Maybe<T> = T | null; export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>; export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }; 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 of the serverless function to execute */
id: Scalars['UUID']; id: Scalars['UUID'];
/** Payload in JSON format */ /** Payload in JSON format */
payload?: InputMaybe<Scalars['JSON']>; payload: Scalars['JSON'];
/** Version of the serverless function to execute */ /** Version of the serverless function to execute */
version?: Scalars['String']; version?: Scalars['String'];
}; };
@@ -338,6 +338,7 @@ export enum MessageChannelVisibility {
export type Mutation = { export type Mutation = {
__typename?: 'Mutation'; __typename?: 'Mutation';
activateWorkflowVersion: Scalars['Boolean'];
activateWorkspace: Workspace; activateWorkspace: Workspace;
addUserToWorkspace: User; addUserToWorkspace: User;
authorizeApp: AuthorizeApp; authorizeApp: AuthorizeApp;
@@ -347,15 +348,14 @@ export type Mutation = {
createOneObject: Object; createOneObject: Object;
createOneServerlessFunction: ServerlessFunction; createOneServerlessFunction: ServerlessFunction;
createOneServerlessFunctionFromFile: ServerlessFunction; createOneServerlessFunctionFromFile: ServerlessFunction;
deactivateWorkflowVersion: Scalars['Boolean'];
deleteCurrentWorkspace: Workspace; deleteCurrentWorkspace: Workspace;
deleteOneObject: Object; deleteOneObject: Object;
deleteOneServerlessFunction: ServerlessFunction; deleteOneServerlessFunction: ServerlessFunction;
deleteUser: User; deleteUser: User;
disablePostgresProxy: PostgresCredentials; disablePostgresProxy: PostgresCredentials;
disableWorkflowTrigger: Scalars['Boolean'];
emailPasswordResetLink: EmailPasswordResetLink; emailPasswordResetLink: EmailPasswordResetLink;
enablePostgresProxy: PostgresCredentials; enablePostgresProxy: PostgresCredentials;
enableWorkflowTrigger: Scalars['Boolean'];
exchangeAuthorizationCode: ExchangeAuthCode; exchangeAuthorizationCode: ExchangeAuthCode;
executeOneServerlessFunction: ServerlessFunctionExecutionResult; executeOneServerlessFunction: ServerlessFunctionExecutionResult;
generateApiKeyToken: ApiKeyToken; generateApiKeyToken: ApiKeyToken;
@@ -382,6 +382,11 @@ export type Mutation = {
}; };
export type MutationActivateWorkflowVersionArgs = {
workflowVersionId: Scalars['String'];
};
export type MutationActivateWorkspaceArgs = { export type MutationActivateWorkspaceArgs = {
data: ActivateWorkspaceInput; data: ActivateWorkspaceInput;
}; };
@@ -423,6 +428,11 @@ export type MutationCreateOneServerlessFunctionFromFileArgs = {
}; };
export type MutationDeactivateWorkflowVersionArgs = {
workflowVersionId: Scalars['String'];
};
export type MutationDeleteOneObjectArgs = { export type MutationDeleteOneObjectArgs = {
input: DeleteOneObjectInput; input: DeleteOneObjectInput;
}; };
@@ -433,21 +443,11 @@ export type MutationDeleteOneServerlessFunctionArgs = {
}; };
export type MutationDisableWorkflowTriggerArgs = {
workflowVersionId: Scalars['String'];
};
export type MutationEmailPasswordResetLinkArgs = { export type MutationEmailPasswordResetLinkArgs = {
email: Scalars['String']; email: Scalars['String'];
}; };
export type MutationEnableWorkflowTriggerArgs = {
workflowVersionId: Scalars['String'];
};
export type MutationExchangeAuthorizationCodeArgs = { export type MutationExchangeAuthorizationCodeArgs = {
authorizationCode: Scalars['String']; authorizationCode: Scalars['String'];
clientSecret?: InputMaybe<Scalars['String']>; clientSecret?: InputMaybe<Scalars['String']>;
@@ -638,9 +638,10 @@ export type Query = {
currentWorkspace: Workspace; currentWorkspace: Workspace;
findWorkspaceFromInviteHash: Workspace; findWorkspaceFromInviteHash: Workspace;
getAISQLQuery: AisqlQueryResult; getAISQLQuery: AisqlQueryResult;
getAvailablePackages: Scalars['JSON'];
getPostgresCredentials?: Maybe<PostgresCredentials>; getPostgresCredentials?: Maybe<PostgresCredentials>;
getProductPrices: ProductPricesEntity; getProductPrices: ProductPricesEntity;
getServerlessFunctionSourceCode: Scalars['String']; getServerlessFunctionSourceCode?: Maybe<Scalars['String']>;
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal; getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal; getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal; getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
@@ -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,
};
};
@@ -1,6 +1,5 @@
import { ApolloCache } from '@apollo/client'; import { ApolloCache } from '@apollo/client';
import { getRelationDefinition } from '@/apollo/optimistic-effect/utils/getRelationDefinition';
import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect'; import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect'; import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
@@ -45,16 +44,23 @@ export const triggerUpdateRelationsOptimisticEffect = ({
return; return;
} }
const relationDefinition = getRelationDefinition({ const relationDefinition =
fieldMetadataItemOnSourceRecord, fieldMetadataItemOnSourceRecord.relationDefinition;
objectMetadataItems,
});
if (!relationDefinition) { if (!relationDefinition) {
return; return;
} }
const { targetObjectMetadataItem, fieldMetadataItemOnTargetRecord } = const { targetObjectMetadata, targetFieldMetadata } = relationDefinition;
relationDefinition;
const fullTargetObjectMetadataItem = objectMetadataItems.find(
({ nameSingular }) =>
nameSingular === targetObjectMetadata.nameSingular,
);
if (!fullTargetObjectMetadataItem) {
return;
}
const currentFieldValueOnSourceRecord: const currentFieldValueOnSourceRecord:
| RecordGqlConnection | RecordGqlConnection
@@ -80,7 +86,7 @@ export const triggerUpdateRelationsOptimisticEffect = ({
// it's an object record connection (we can still check it though as a safeguard) // it's an object record connection (we can still check it though as a safeguard)
const currentFieldValueOnSourceRecordIsARecordConnection = const currentFieldValueOnSourceRecordIsARecordConnection =
isObjectRecordConnection( isObjectRecordConnection(
targetObjectMetadataItem.nameSingular, targetObjectMetadata.nameSingular,
currentFieldValueOnSourceRecord, currentFieldValueOnSourceRecord,
); );
@@ -93,7 +99,7 @@ export const triggerUpdateRelationsOptimisticEffect = ({
const updatedFieldValueOnSourceRecordIsARecordConnection = const updatedFieldValueOnSourceRecordIsARecordConnection =
isObjectRecordConnection( isObjectRecordConnection(
targetObjectMetadataItem.nameSingular, targetObjectMetadata.nameSingular,
updatedFieldValueOnSourceRecord, updatedFieldValueOnSourceRecord,
); );
@@ -112,13 +118,13 @@ export const triggerUpdateRelationsOptimisticEffect = ({
// Instead of hardcoding it here // Instead of hardcoding it here
const shouldCascadeDeleteTargetRecords = const shouldCascadeDeleteTargetRecords =
CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes( CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
targetObjectMetadataItem.nameSingular as CoreObjectNameSingular, targetObjectMetadata.nameSingular as CoreObjectNameSingular,
); );
if (shouldCascadeDeleteTargetRecords) { if (shouldCascadeDeleteTargetRecords) {
triggerDeleteRecordsOptimisticEffect({ triggerDeleteRecordsOptimisticEffect({
cache, cache,
objectMetadataItem: targetObjectMetadataItem, objectMetadataItem: fullTargetObjectMetadataItem,
recordsToDelete: targetRecordsToDetachFrom, recordsToDelete: targetRecordsToDetachFrom,
objectMetadataItems, objectMetadataItems,
}); });
@@ -128,8 +134,8 @@ export const triggerUpdateRelationsOptimisticEffect = ({
cache, cache,
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular, sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
sourceRecordId: currentSourceRecord.id, sourceRecordId: currentSourceRecord.id,
fieldNameOnTargetRecord: fieldMetadataItemOnTargetRecord.name, fieldNameOnTargetRecord: targetFieldMetadata.name,
targetObjectNameSingular: targetObjectMetadataItem.nameSingular, targetObjectNameSingular: targetObjectMetadata.nameSingular,
targetRecordId: targetRecordToDetachFrom.id, targetRecordId: targetRecordToDetachFrom.id,
}); });
}); });
@@ -145,8 +151,8 @@ export const triggerUpdateRelationsOptimisticEffect = ({
cache, cache,
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular, sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
sourceRecordId: updatedSourceRecord.id, sourceRecordId: updatedSourceRecord.id,
fieldNameOnTargetRecord: fieldMetadataItemOnTargetRecord.name, fieldNameOnTargetRecord: targetFieldMetadata.name,
targetObjectNameSingular: targetObjectMetadataItem.nameSingular, targetObjectNameSingular: targetObjectMetadata.nameSingular,
targetRecordId: targetRecordToAttachTo.id, targetRecordId: targetRecordToAttachTo.id,
}), }),
); );
@@ -11,7 +11,7 @@ import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/compo
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection'; 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'; import { useFavorites } from '../hooks/useFavorites';
const StyledContainer = styled(NavigationDrawerSection)` const StyledContainer = styled(NavigationDrawerSection)`
@@ -35,7 +35,7 @@ const StyledNavigationDrawerItem = styled(NavigationDrawerItem)`
`; `;
export const CurrentWorkspaceMemberFavorites = () => { export const CurrentWorkspaceMemberFavorites = () => {
const currentUser = useRecoilValue(currentUserState); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { favorites, handleReorderFavorite } = useFavorites(); const { favorites, handleReorderFavorite } = useFavorites();
const loading = useIsPrefetchLoading(); const loading = useIsPrefetchLoading();
@@ -44,12 +44,12 @@ export const CurrentWorkspaceMemberFavorites = () => {
useNavigationSection('Favorites'); useNavigationSection('Favorites');
const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState); const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState);
if (loading && isDefined(currentUser)) { if (loading && isDefined(currentWorkspaceMember)) {
return <FavoritesSkeletonLoader />; return <FavoritesSkeletonLoader />;
} }
const currentWorkspaceMemberFavorites = favorites.filter( const currentWorkspaceMemberFavorites = favorites.filter(
(favorite) => favorite.workspaceMemberId === currentUser?.id, (favorite) => favorite.workspaceMemberId === currentWorkspaceMember?.id,
); );
if ( if (
@@ -9,5 +9,6 @@ export type Favorite = {
avatarType: AvatarType; avatarType: AvatarType;
link: string; link: string;
recordId: string; recordId: string;
workspaceMemberId: string;
__typename: 'Favorite'; __typename: 'Favorite';
}; };
@@ -19,8 +19,8 @@ export const sortFavorites = (
const relationObject = favorite[relationField.name]; const relationObject = favorite[relationField.name];
const relationObjectNameSingular = const relationObjectNameSingular =
relationField.toRelationMetadata?.fromObjectMetadata.nameSingular ?? relationField.relationDefinition?.targetObjectMetadata
''; .nameSingular ?? '';
const objectRecordIdentifier = const objectRecordIdentifier =
getObjectRecordIdentifierByNameSingular( getObjectRecordIdentifierByNameSingular(
@@ -38,6 +38,7 @@ export const sortFavorites = (
link: hasLinkToShowPage link: hasLinkToShowPage
? objectRecordIdentifier.linkToShowPage ? objectRecordIdentifier.linkToShowPage
: '', : '',
workspaceMemberId: favorite.workspaceMemberId,
} as Favorite; } as Favorite;
} }
} }
@@ -39,32 +39,6 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
isNullable isNullable
createdAt createdAt
updatedAt updatedAt
fromRelationMetadata {
id
relationType
toObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
isRemote
}
toFieldMetadataId
}
toRelationMetadata {
id
relationType
fromObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
isRemote
}
fromFieldMetadataId
}
defaultValue defaultValue
options options
relationDefinition { relationDefinition {
@@ -39,29 +39,27 @@ export const query = gql`
isNullable isNullable
createdAt createdAt
updatedAt updatedAt
fromRelationMetadata { relationDefinition {
id relationId
relationType direction
toObjectMetadata { sourceObjectMetadata {
id id
dataSourceId
nameSingular nameSingular
namePlural namePlural
isSystem
} }
toFieldMetadataId sourceFieldMetadata {
} id
toRelationMetadata { name
id }
relationType targetObjectMetadata {
fromObjectMetadata {
id id
dataSourceId
nameSingular nameSingular
namePlural namePlural
isSystem
} }
fromFieldMetadataId targetFieldMetadata {
id
name
}
} }
defaultValue defaultValue
options options
@@ -1,10 +1,10 @@
import { ReactNode } from 'react';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem'; import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
import { RelationMetadataType } from '~/generated/graphql'; import { RelationDefinitionType } from '~/generated/graphql';
import { import {
query, query,
@@ -42,7 +42,7 @@ describe('useCreateOneRelationMetadataItem', () => {
await act(async () => { await act(async () => {
const res = await result.current.createOneRelationMetadataItem({ const res = await result.current.createOneRelationMetadataItem({
relationType: RelationMetadataType.OneToOne, relationType: RelationDefinitionType.OneToOne,
field: { field: {
label: 'label', label: 'label',
}, },
@@ -1,11 +1,7 @@
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { RelationType } from '@/settings/data-model/types/RelationType'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import {
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
import { FieldMetadataItem } from '../types/FieldMetadataItem'; import { FieldMetadataItem } from '../types/FieldMetadataItem';
@@ -17,39 +13,19 @@ export const useGetRelationMetadata = () =>
}: { }: {
fieldMetadataItem: Pick< fieldMetadataItem: Pick<
FieldMetadataItem, FieldMetadataItem,
'fromRelationMetadata' | 'toRelationMetadata' | 'type' 'type' | 'relationDefinition'
>; >;
}) => { }) => {
if (fieldMetadataItem.type !== FieldMetadataType.Relation) return null; if (fieldMetadataItem.type !== FieldMetadataType.Relation) return null;
const relationMetadata = const relationDefinition = fieldMetadataItem.relationDefinition;
fieldMetadataItem.fromRelationMetadata ||
fieldMetadataItem.toRelationMetadata;
if (!relationMetadata) return null; if (!relationDefinition) 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;
const relationObjectMetadataItem = snapshot const relationObjectMetadataItem = snapshot
.getLoadable( .getLoadable(
objectMetadataItemFamilySelector({ objectMetadataItemFamilySelector({
objectName: relationObjectMetadataNameSingular, objectName: relationDefinition.targetObjectMetadata.nameSingular,
objectNameType: 'singular', objectNameType: 'singular',
}), }),
) )
@@ -59,7 +35,7 @@ export const useGetRelationMetadata = () =>
const relationFieldMetadataItem = const relationFieldMetadataItem =
relationObjectMetadataItem.fields.find( relationObjectMetadataItem.fields.find(
(field) => field.id === relationFieldMetadataId, (field) => field.id === relationDefinition.targetFieldMetadata.id,
); );
if (!relationFieldMetadataItem) return null; if (!relationFieldMetadataItem) return null;
@@ -67,7 +43,7 @@ export const useGetRelationMetadata = () =>
return { return {
relationFieldMetadataItem, relationFieldMetadataItem,
relationObjectMetadataItem, relationObjectMetadataItem,
relationType, relationType: relationDefinition.direction,
}; };
}, },
[], [],
@@ -3,7 +3,6 @@ import { ThemeColor } from 'twenty-ui';
import { import {
Field, Field,
Object as MetadataObject, Object as MetadataObject,
Relation,
RelationDefinition, RelationDefinition,
RelationDefinitionType, RelationDefinitionType,
} from '~/generated-metadata/graphql'; } from '~/generated-metadata/graphql';
@@ -18,31 +17,9 @@ export type FieldMetadataItemOption = {
export type FieldMetadataItem = Omit< export type FieldMetadataItem = Omit<
Field, Field,
| '__typename' '__typename' | 'defaultValue' | 'options' | 'settings' | 'relationDefinition'
| 'fromRelationMetadata'
| 'toRelationMetadata'
| 'defaultValue'
| 'options'
| 'settings'
| 'relationDefinition'
> & { > & {
__typename?: string; __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; defaultValue?: any;
options?: FieldMetadataItemOption[] | null; options?: FieldMetadataItemOption[] | null;
relationDefinition?: { relationDefinition?: {
@@ -1,5 +1,4 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon'; import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
@@ -20,17 +19,15 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
labelWidth, labelWidth,
}: FieldMetadataItemAsFieldDefinitionProps): FieldDefinition<FieldMetadata> => { }: FieldMetadataItemAsFieldDefinitionProps): FieldDefinition<FieldMetadata> => {
const relationObjectMetadataItem = const relationObjectMetadataItem =
field.toRelationMetadata?.fromObjectMetadata || field.relationDefinition?.targetObjectMetadata;
field.fromRelationMetadata?.toObjectMetadata;
const relationFieldMetadataId = const relationFieldMetadataId =
field.toRelationMetadata?.fromFieldMetadataId || field.relationDefinition?.targetFieldMetadata.id;
field.fromRelationMetadata?.toFieldMetadataId;
const fieldDefintionMetadata = { const fieldDefintionMetadata = {
fieldName: field.name, fieldName: field.name,
placeHolder: field.label, placeHolder: field.label,
relationType: parseFieldRelationType(field), relationType: field.relationDefinition?.direction,
relationFieldMetadataId, relationFieldMetadataId,
relationObjectMetadataNameSingular: relationObjectMetadataNameSingular:
relationObjectMetadataItem?.nameSingular ?? '', relationObjectMetadataItem?.nameSingular ?? '',
@@ -2,6 +2,7 @@ import { RelationType } from '@/settings/data-model/types/RelationType';
import { import {
CreateRelationInput, CreateRelationInput,
Field, Field,
RelationDefinitionType,
RelationMetadataType, RelationMetadataType,
} from '~/generated-metadata/graphql'; } from '~/generated-metadata/graphql';
@@ -24,8 +25,8 @@ export const formatRelationMetadataInput = (
// => Transform into ONE_TO_MANY and invert "from" and "to" data. // => Transform into ONE_TO_MANY and invert "from" and "to" data.
const isManyToOne = input.relationType === 'MANY_TO_ONE'; const isManyToOne = input.relationType === 'MANY_TO_ONE';
const relationType = isManyToOne const relationType = isManyToOne
? RelationMetadataType.OneToMany ? RelationDefinitionType.OneToMany
: (input.relationType as RelationMetadataType); : (input.relationType as RelationDefinitionType);
const { field: fromField, objectMetadataId: fromObjectMetadataId } = const { field: fromField, objectMetadataId: fromObjectMetadataId } =
isManyToOne ? input.connect : input; isManyToOne ? input.connect : input;
const { field: toField, objectMetadataId: toObjectMetadataId } = isManyToOne const { field: toField, objectMetadataId: toObjectMetadataId } = isManyToOne
@@ -51,7 +52,7 @@ export const formatRelationMetadataInput = (
fromLabel, fromLabel,
fromName, fromName,
fromObjectMetadataId, fromObjectMetadataId,
relationType, relationType: relationType as unknown as RelationMetadataType,
toDescription, toDescription,
toIcon, toIcon,
toLabel, toLabel,
@@ -4,7 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { import {
FieldMetadataType, FieldMetadataType,
RelationMetadataType, RelationDefinitionType,
} from '~/generated-metadata/graphql'; } from '~/generated-metadata/graphql';
import { FieldMetadataItem } from '../types/FieldMetadataItem'; import { FieldMetadataItem } from '../types/FieldMetadataItem';
@@ -17,10 +17,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
computeReferences = false, computeReferences = false,
}: { }: {
objectMetadataItems: ObjectMetadataItem[]; objectMetadataItems: ObjectMetadataItem[];
field: Pick< field: Pick<FieldMetadataItem, 'name' | 'type' | 'relationDefinition'>;
FieldMetadataItem,
'name' | 'type' | 'toRelationMetadata' | 'fromRelationMetadata'
>;
relationrecordFields?: Record<string, any>; relationrecordFields?: Record<string, any>;
computeReferences?: boolean; computeReferences?: boolean;
}): any => { }): any => {
@@ -49,12 +46,12 @@ export const mapFieldMetadataToGraphQLQuery = ({
if ( if (
fieldType === FieldMetadataType.Relation && fieldType === FieldMetadataType.Relation &&
field.toRelationMetadata?.relationType === RelationMetadataType.OneToMany field.relationDefinition?.direction === RelationDefinitionType.ManyToOne
) { ) {
const relationMetadataItem = objectMetadataItems.find( const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) => (objectMetadataItem) =>
objectMetadataItem.id === objectMetadataItem.id ===
(field.toRelationMetadata as any)?.fromObjectMetadata?.id, field.relationDefinition?.targetObjectMetadata.id,
); );
if (isUndefined(relationMetadataItem)) { if (isUndefined(relationMetadataItem)) {
@@ -73,12 +70,12 @@ ${mapObjectMetadataToGraphQLQuery({
if ( if (
fieldType === FieldMetadataType.Relation && fieldType === FieldMetadataType.Relation &&
field.fromRelationMetadata?.relationType === RelationMetadataType.OneToMany field.relationDefinition?.direction === RelationDefinitionType.OneToMany
) { ) {
const relationMetadataItem = objectMetadataItems.find( const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) => (objectMetadataItem) =>
objectMetadataItem.id === objectMetadataItem.id ===
(field.fromRelationMetadata as any)?.toObjectMetadata?.id, field.relationDefinition?.targetObjectMetadata.id,
); );
if (isUndefined(relationMetadataItem)) { if (isUndefined(relationMetadataItem)) {
@@ -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,
)}.`,
);
};
@@ -6,7 +6,6 @@ import { metadataLabelSchema } from '@/object-metadata/validation-schemas/metada
import { import {
FieldMetadataType, FieldMetadataType,
RelationDefinitionType, RelationDefinitionType,
RelationMetadataType,
} from '~/generated-metadata/graphql'; } from '~/generated-metadata/graphql';
import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema'; import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema';
@@ -16,24 +15,6 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
createdAt: z.string().datetime(), createdAt: z.string().datetime(),
defaultValue: z.any().optional(), defaultValue: z.any().optional(),
description: z.string().trim().nullable().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(), icon: z.string().startsWith('Icon').trim().nullable(),
id: z.string().uuid(), id: z.string().uuid(),
isActive: z.boolean(), isActive: z.boolean(),
@@ -84,24 +65,6 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
}) })
.nullable() .nullable()
.optional(), .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), type: z.nativeEnum(FieldMetadataType),
updatedAt: z.string().datetime(), updatedAt: z.string().datetime(),
}) satisfies z.ZodType<FieldMetadataItem>; }) satisfies z.ZodType<FieldMetadataItem>;
@@ -4,17 +4,6 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldMetadata } from './FieldMetadata'; 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> = { export type FieldDefinition<T extends FieldMetadata> = {
fieldMetadataId: string; fieldMetadataId: string;
label: string; label: string;
@@ -3,8 +3,8 @@ import { ThemeColor } from 'twenty-ui';
import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues'; import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
import { ZodHelperLiteral } from '@/object-record/record-field/types/ZodHelperLiteral'; import { ZodHelperLiteral } from '@/object-record/record-field/types/ZodHelperLiteral';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { WithNarrowedStringLiteralProperty } from '~/types/WithNarrowedStringLiteralProperty';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { CurrencyCode } from './CurrencyCode'; import { CurrencyCode } from './CurrencyCode';
export type FieldUuidMetadata = { export type FieldUuidMetadata = {
@@ -110,35 +110,17 @@ export type FieldPositionMetadata = {
fieldName: string; fieldName: string;
}; };
export type FieldDefinitionRelationType =
| 'FROM_MANY_OBJECTS'
| 'FROM_ONE_OBJECT'
| 'TO_MANY_OBJECTS'
| 'TO_ONE_OBJECT';
export type FieldRelationMetadata = { export type FieldRelationMetadata = {
fieldName: string; fieldName: string;
objectMetadataNameSingular?: string; objectMetadataNameSingular?: string;
relationFieldMetadataId: string; relationFieldMetadataId: string;
relationObjectMetadataNamePlural: string; relationObjectMetadataNamePlural: string;
relationObjectMetadataNameSingular: string; relationObjectMetadataNameSingular: string;
relationType?: FieldDefinitionRelationType; relationType?: RelationDefinitionType;
targetFieldMetadataName?: string; targetFieldMetadataName?: string;
useEditButton?: boolean; useEditButton?: boolean;
}; };
export type FieldRelationOneMetadata = WithNarrowedStringLiteralProperty<
FieldRelationMetadata,
'relationType',
'TO_ONE_OBJECT'
>;
export type FieldRelationManyMetadata = WithNarrowedStringLiteralProperty<
FieldRelationMetadata,
'relationType',
'FROM_MANY_OBJECTS'
>;
export type FieldSelectMetadata = { export type FieldSelectMetadata = {
objectMetadataNameSingular?: string; objectMetadataNameSingular?: string;
fieldName: string; fieldName: string;
@@ -1,9 +1,11 @@
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation'; import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { FieldDefinition } from '../FieldDefinition'; import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldRelationManyMetadata } from '../FieldMetadata'; import { FieldMetadata } from '../FieldMetadata';
export const isFieldRelationFromManyObjects = ( export const isFieldRelationFromManyObjects = (
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>, field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
): field is FieldDefinition<FieldRelationManyMetadata> => ): field is FieldDefinition<FieldMetadata> =>
isFieldRelation(field) && field.metadata.relationType === 'FROM_MANY_OBJECTS'; isFieldRelation(field) &&
field.metadata.relationType === RelationDefinitionType.OneToMany;
@@ -1,9 +1,11 @@
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation'; import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { FieldDefinition } from '../FieldDefinition'; import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldRelationOneMetadata } from '../FieldMetadata'; import { FieldMetadata } from '../FieldMetadata';
export const isFieldRelationToOneObject = ( export const isFieldRelationToOneObject = (
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>, field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
): field is FieldDefinition<FieldRelationOneMetadata> => ): field is FieldDefinition<FieldMetadata> =>
isFieldRelation(field) && field.metadata.relationType === 'TO_ONE_OBJECT'; isFieldRelation(field) &&
field.metadata.relationType === RelationDefinitionType.ManyToOne;
@@ -1,6 +1,7 @@
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { import {
csvDownloader, csvDownloader,
displayedExportProgress, displayedExportProgress,
@@ -35,7 +36,10 @@ describe('generateCsv', () => {
{ label: 'Nested', metadata: { fieldName: 'nested' } }, { label: 'Nested', metadata: { fieldName: 'nested' } },
{ {
label: 'Relation', label: 'Relation',
metadata: { fieldName: 'relation', relationType: 'TO_ONE_OBJECT' }, metadata: {
fieldName: 'relation',
relationType: RelationDefinitionType.ManyToOne,
},
}, },
] as ColumnDefinition<FieldMetadata>[]; ] as ColumnDefinition<FieldMetadata>[];
const rows = [ const rows = [
@@ -9,6 +9,7 @@ import {
} from '@/object-record/record-index/options/hooks/useTableData'; } from '@/object-record/record-index/options/hooks/useTableData';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { FieldMetadataType } from '~/generated/graphql'; import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@@ -43,7 +44,7 @@ export const generateCsv: GenerateExport = ({
const columnsToExport = columns.filter( const columnsToExport = columns.filter(
(col) => (col) =>
!('relationType' in col.metadata && col.metadata.relationType) || !('relationType' in col.metadata && col.metadata.relationType) ||
col.metadata.relationType === 'TO_ONE_OBJECT', col.metadata.relationType === RelationDefinitionType.ManyToOne,
); );
const objectIdColumn: ColumnDefinition<FieldMetadata> = { const objectIdColumn: ColumnDefinition<FieldMetadata> = {
@@ -7,6 +7,7 @@ import { Task } from '@/activities/types/Task';
import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord'; import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord';
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem'; import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
@@ -56,6 +57,8 @@ export const RecordShowContainer = ({
objectNameSingular, objectNameSingular,
}); });
const { objectMetadataItems } = useObjectMetadataItems();
const { labelIdentifierFieldMetadataItem } = const { labelIdentifierFieldMetadataItem } =
useLabelIdentifierFieldMetadataItem({ useLabelIdentifierFieldMetadataItem({
objectNameSingular, objectNameSingular,
@@ -119,7 +122,7 @@ export const RecordShowContainer = ({
const availableFieldMetadataItems = objectMetadataItem.fields const availableFieldMetadataItems = objectMetadataItem.fields
.filter( .filter(
(fieldMetadataItem) => (fieldMetadataItem) =>
isFieldCellSupported(fieldMetadataItem) && isFieldCellSupported(fieldMetadataItem, objectMetadataItems) &&
fieldMetadataItem.id !== labelIdentifierFieldMetadataItem?.id, fieldMetadataItem.id !== labelIdentifierFieldMetadataItem?.id,
) )
.sort((fieldMetadataItemA, fieldMetadataItemB) => .sort((fieldMetadataItemA, fieldMetadataItemB) =>
@@ -1,7 +1,7 @@
import { useCallback, useContext } from 'react';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useCallback, useContext } from 'react';
import { import {
IconChevronDown, IconChevronDown,
IconComponent, IconComponent,
@@ -11,6 +11,7 @@ import {
} from 'twenty-ui'; } from 'twenty-ui';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { RecordChip } from '@/object-record/components/RecordChip'; 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 { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut'; import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
const StyledListItem = styled(RecordDetailRecordsListItem)<{ const StyledListItem = styled(RecordDetailRecordsListItem)<{
isDropdownOpen?: boolean; isDropdownOpen?: boolean;
@@ -89,12 +91,14 @@ export const RecordDetailRelationRecordsListItem = ({
relationType, relationType,
} = fieldDefinition.metadata as FieldRelationMetadata; } = fieldDefinition.metadata as FieldRelationMetadata;
const isToOneObject = relationType === 'TO_ONE_OBJECT'; const isToOneObject = relationType === RelationDefinitionType.ManyToOne;
const { objectMetadataItem: relationObjectMetadataItem } = const { objectMetadataItem: relationObjectMetadataItem } =
useObjectMetadataItem({ useObjectMetadataItem({
objectNameSingular: relationObjectMetadataNameSingular, objectNameSingular: relationObjectMetadataNameSingular,
}); });
const { objectMetadataItems } = useObjectMetadataItems();
const persistField = usePersistField(); const persistField = usePersistField();
const { updateOneRecord: updateOneRelationRecord } = useUpdateOneRecord({ const { updateOneRecord: updateOneRelationRecord } = useUpdateOneRecord({
@@ -111,7 +115,7 @@ export const RecordDetailRelationRecordsListItem = ({
const availableRelationFieldMetadataItems = relationObjectMetadataItem.fields const availableRelationFieldMetadataItems = relationObjectMetadataItem.fields
.filter( .filter(
(fieldMetadataItem) => (fieldMetadataItem) =>
isFieldCellSupported(fieldMetadataItem) && isFieldCellSupported(fieldMetadataItem, objectMetadataItems) &&
fieldMetadataItem.id !== fieldMetadataItem.id !==
relationObjectMetadataItem.labelIdentifierFieldMetadataId && relationObjectMetadataItem.labelIdentifierFieldMetadataId &&
fieldMetadataItem.id !== relationFieldMetadataId, fieldMetadataItem.id !== relationFieldMetadataId,
@@ -32,6 +32,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams'; import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
type RecordDetailRelationSectionProps = { type RecordDetailRelationSectionProps = {
loading: boolean; loading: boolean;
@@ -67,8 +68,8 @@ export const RecordDetailRelationSection = ({
>(recordStoreFamilySelector({ recordId, fieldName })); >(recordStoreFamilySelector({ recordId, fieldName }));
// TODO: use new relation type // TODO: use new relation type
const isToOneObject = relationType === 'TO_ONE_OBJECT'; const isToOneObject = relationType === RelationDefinitionType.ManyToOne;
const isFromManyObjects = relationType === 'FROM_MANY_OBJECTS'; const isToManyObjects = RelationDefinitionType.OneToMany;
const relationRecords: ObjectRecord[] = const relationRecords: ObjectRecord[] =
fieldValue && isToOneObject fieldValue && isToOneObject
@@ -160,7 +161,7 @@ export const RecordDetailRelationSection = ({
<RecordDetailSectionHeader <RecordDetailSectionHeader
title={fieldDefinition.label} title={fieldDefinition.label}
link={ link={
isFromManyObjects isToManyObjects
? { ? {
to: filterLinkHref, to: filterLinkHref,
label: `All (${relationRecords.length})`, label: `All (${relationRecords.length})`,
@@ -34,22 +34,6 @@ export const mockPerformance = {
isNullable: true, isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '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, defaultValue: null,
options: null, options: null,
relationDefinition: { relationDefinition: {
@@ -94,8 +78,6 @@ export const mockPerformance = {
isNullable: false, isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z', updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: "''", defaultValue: "''",
options: null, options: null,
relationDefinition: null, relationDefinition: null,
@@ -114,8 +96,6 @@ export const mockPerformance = {
isNullable: false, isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z', updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: "''", defaultValue: "''",
options: null, options: null,
relationDefinition: null, relationDefinition: null,
@@ -134,22 +114,6 @@ export const mockPerformance = {
isNullable: true, isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '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, defaultValue: null,
options: null, options: null,
relationDefinition: { relationDefinition: {
@@ -194,22 +158,6 @@ export const mockPerformance = {
isNullable: true, isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '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, defaultValue: null,
options: null, options: null,
relationDefinition: { relationDefinition: {
@@ -254,22 +202,6 @@ export const mockPerformance = {
isNullable: true, isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '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, defaultValue: null,
options: null, options: null,
relationDefinition: { relationDefinition: {
@@ -314,8 +246,6 @@ export const mockPerformance = {
isNullable: false, isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z', updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: 'now', defaultValue: 'now',
options: null, options: null,
relationDefinition: null, relationDefinition: null,
@@ -334,22 +264,6 @@ export const mockPerformance = {
isNullable: true, isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '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, defaultValue: null,
options: null, options: null,
relationDefinition: { relationDefinition: {
@@ -394,8 +308,6 @@ export const mockPerformance = {
isNullable: true, isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z', updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: { defaultValue: {
primaryLinkUrl: "''", primaryLinkUrl: "''",
primaryLinkLabel: "''", primaryLinkLabel: "''",
@@ -417,22 +329,6 @@ export const mockPerformance = {
isNullable: true, isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '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, defaultValue: null,
options: null, options: null,
relationDefinition: { relationDefinition: {
@@ -477,8 +373,6 @@ export const mockPerformance = {
isNullable: false, isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z', updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: "''", defaultValue: "''",
options: null, options: null,
relationDefinition: null, relationDefinition: null,
@@ -497,8 +391,6 @@ export const mockPerformance = {
isNullable: true, isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z', updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: null, defaultValue: null,
options: null, options: null,
relationDefinition: null, relationDefinition: null,
@@ -517,8 +409,6 @@ export const mockPerformance = {
isNullable: false, isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z', updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: "''", defaultValue: "''",
options: null, options: null,
relationDefinition: null, relationDefinition: null,
@@ -537,8 +427,6 @@ export const mockPerformance = {
isNullable: false, isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z', updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: "''", defaultValue: "''",
options: null, options: null,
relationDefinition: null, relationDefinition: null,
@@ -557,8 +445,6 @@ export const mockPerformance = {
isNullable: false, isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z', updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: 'now', defaultValue: 'now',
options: null, options: null,
relationDefinition: null, relationDefinition: null,
@@ -577,8 +463,6 @@ export const mockPerformance = {
isNullable: true, isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z', updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: null, defaultValue: null,
options: null, options: null,
relationDefinition: null, relationDefinition: null,
@@ -597,22 +481,6 @@ export const mockPerformance = {
isNullable: true, isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '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, defaultValue: null,
options: null, options: null,
relationDefinition: { relationDefinition: {
@@ -657,8 +525,6 @@ export const mockPerformance = {
isNullable: true, isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z', updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: { defaultValue: {
lastName: "''", lastName: "''",
firstName: "''", firstName: "''",
@@ -680,8 +546,6 @@ export const mockPerformance = {
isNullable: true, isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z', updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: { defaultValue: {
primaryLinkUrl: "''", primaryLinkUrl: "''",
primaryLinkLabel: "''", primaryLinkLabel: "''",
@@ -703,22 +567,6 @@ export const mockPerformance = {
isNullable: true, isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '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, defaultValue: null,
options: null, options: null,
relationDefinition: { relationDefinition: {
@@ -763,8 +611,6 @@ export const mockPerformance = {
isNullable: false, isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z', createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z', updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: 'uuid', defaultValue: 'uuid',
options: null, options: null,
relationDefinition: null, relationDefinition: null,
@@ -6,7 +6,10 @@ import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOp
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types'; import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; 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 = ( export const useOpenObjectRecordsSpreasheetImportDialog = (
objectNameSingular: string, objectNameSingular: string,
@@ -37,7 +40,8 @@ export const useOpenObjectRecordsSpreasheetImportDialog = (
(!fieldMetadataItem.isSystem || fieldMetadataItem.name === 'id') && (!fieldMetadataItem.isSystem || fieldMetadataItem.name === 'id') &&
fieldMetadataItem.name !== 'createdAt' && fieldMetadataItem.name !== 'createdAt' &&
(fieldMetadataItem.type !== FieldMetadataType.Relation || (fieldMetadataItem.type !== FieldMetadataType.Relation ||
fieldMetadataItem.toRelationMetadata), fieldMetadataItem.relationDefinition?.direction ===
RelationDefinitionType.ManyToOne),
) )
.sort((fieldMetadataItemA, fieldMetadataItemB) => .sort((fieldMetadataItemA, fieldMetadataItemB) =>
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name), fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
@@ -2,14 +2,14 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation'; import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { TABLE_COLUMNS_DENY_LIST } from '@/object-record/relation-picker/constants/TableColumnsDenyList'; import { TABLE_COLUMNS_DENY_LIST } from '@/object-record/relation-picker/constants/TableColumnsDenyList';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
export const filterAvailableTableColumns = ( export const filterAvailableTableColumns = (
columnDefinition: ColumnDefinition<FieldMetadata>, columnDefinition: ColumnDefinition<FieldMetadata>,
): boolean => { ): boolean => {
if ( if (
isFieldRelation(columnDefinition) && isFieldRelation(columnDefinition) &&
columnDefinition.metadata?.relationType !== 'TO_ONE_OBJECT' && columnDefinition.metadata?.relationType !== RelationDefinitionType.ManyToOne
columnDefinition.metadata?.relationType !== 'FROM_MANY_OBJECTS'
) { ) {
return false; return false;
} }
@@ -4,10 +4,7 @@ import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFiel
import { v4 } from 'uuid'; import { v4 } from 'uuid';
export const generateDefaultFieldValue = ( export const generateDefaultFieldValue = (
fieldMetadataItem: Pick< fieldMetadataItem: Pick<FieldMetadataItem, 'defaultValue' | 'type'>,
FieldMetadataItem,
'defaultValue' | 'type' | 'fromRelationMetadata'
>,
) => { ) => {
const defaultValue = isFieldValueEmpty({ const defaultValue = isFieldValueEmpty({
fieldValue: fieldMetadataItem.defaultValue, fieldValue: fieldMetadataItem.defaultValue,
@@ -1,10 +1,11 @@
import { isNonEmptyString } from '@sniptt/guards';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import {
FieldMetadataType,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
export const generateEmptyFieldValue = ( export const generateEmptyFieldValue = (
fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'fromRelationMetadata'>, fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'relationDefinition'>,
) => { ) => {
switch (fieldMetadataItem.type) { switch (fieldMetadataItem.type) {
case FieldMetadataType.Email: case FieldMetadataType.Email:
@@ -62,10 +63,8 @@ export const generateEmptyFieldValue = (
} }
case FieldMetadataType.Relation: { case FieldMetadataType.Relation: {
if ( if (
!isNonEmptyString( fieldMetadataItem.relationDefinition?.direction ===
fieldMetadataItem.fromRelationMetadata?.toObjectMetadata RelationDefinitionType.ManyToOne
?.nameSingular,
)
) { ) {
return null; return null;
} }
@@ -1,12 +1,16 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation'; import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
import { import {
FieldMetadataType, FieldMetadataType,
RelationMetadataType, RelationDefinitionType,
} from '~/generated-metadata/graphql'; } from '~/generated-metadata/graphql';
export const isFieldCellSupported = (fieldMetadataItem: FieldMetadataItem) => { export const isFieldCellSupported = (
fieldMetadataItem: FieldMetadataItem,
objectMetadataItems: ObjectMetadataItem[],
) => {
if ( if (
[ [
FieldMetadataType.Uuid, FieldMetadataType.Uuid,
@@ -18,17 +22,17 @@ export const isFieldCellSupported = (fieldMetadataItem: FieldMetadataItem) => {
} }
if (fieldMetadataItem.type === FieldMetadataType.Relation) { if (fieldMetadataItem.type === FieldMetadataType.Relation) {
const relationMetadata = const relationObjectMetadataItemId =
fieldMetadataItem.fromRelationMetadata ?? fieldMetadataItem.relationDefinition?.targetObjectMetadata.id;
fieldMetadataItem.toRelationMetadata;
const relationObjectMetadataItem = const relationObjectMetadataItem = objectMetadataItems.find(
fieldMetadataItem.fromRelationMetadata?.toObjectMetadata ?? (item) => item.id === relationObjectMetadataItemId,
fieldMetadataItem.toRelationMetadata?.fromObjectMetadata; );
// Hack to display targets on Notes and Tasks // Hack to display targets on Notes and Tasks
if ( if (
fieldMetadataItem.fromRelationMetadata?.toObjectMetadata?.nameSingular === fieldMetadataItem.relationDefinition?.targetObjectMetadata
CoreObjectNameSingular.NoteTarget && ?.nameSingular === CoreObjectNameSingular.NoteTarget &&
fieldMetadataItem.relationDefinition?.sourceObjectMetadata fieldMetadataItem.relationDefinition?.sourceObjectMetadata
.nameSingular === CoreObjectNameSingular.Note .nameSingular === CoreObjectNameSingular.Note
) { ) {
@@ -36,8 +40,8 @@ export const isFieldCellSupported = (fieldMetadataItem: FieldMetadataItem) => {
} }
if ( if (
fieldMetadataItem.fromRelationMetadata?.toObjectMetadata?.nameSingular === fieldMetadataItem.relationDefinition?.targetObjectMetadata
CoreObjectNameSingular.TaskTarget && ?.nameSingular === CoreObjectNameSingular.TaskTarget &&
fieldMetadataItem.relationDefinition?.sourceObjectMetadata fieldMetadataItem.relationDefinition?.sourceObjectMetadata
.nameSingular === CoreObjectNameSingular.Task .nameSingular === CoreObjectNameSingular.Task
) { ) {
@@ -45,9 +49,10 @@ export const isFieldCellSupported = (fieldMetadataItem: FieldMetadataItem) => {
} }
if ( if (
!relationMetadata || !fieldMetadataItem.relationDefinition ||
// TODO: Many to many relations are not supported yet. // TODO: Many to many relations are not supported yet.
relationMetadata.relationType === RelationMetadataType.ManyToMany || fieldMetadataItem.relationDefinition.direction ===
RelationDefinitionType.ManyToMany ||
!relationObjectMetadataItem || !relationObjectMetadataItem ||
!isObjectMetadataAvailableForRelation(relationObjectMetadataItem) !isObjectMetadataAvailableForRelation(relationObjectMetadataItem)
) { ) {
@@ -1,8 +1,8 @@
import { isString } from '@sniptt/guards'; import { isString } from '@sniptt/guards';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; 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 { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { FieldMetadataType } from '~/generated/graphql'; import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { getUrlHostName } from '~/utils/url/getUrlHostName'; import { getUrlHostName } from '~/utils/url/getUrlHostName';
@@ -29,7 +29,8 @@ export const sanitizeRecordInput = ({
if ( if (
fieldMetadataItem.type === FieldMetadataType.Relation && fieldMetadataItem.type === FieldMetadataType.Relation &&
isFieldRelationToOneValue(fieldValue) fieldMetadataItem.relationDefinition?.direction ===
RelationDefinitionType.ManyToOne
) { ) {
const relationIdFieldName = `${fieldMetadataItem.name}Id`; const relationIdFieldName = `${fieldMetadataItem.name}Id`;
const relationIdFieldMetadataItem = objectMetadataItem.fields.find( const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
@@ -41,6 +42,14 @@ export const sanitizeRecordInput = ({
: undefined; : undefined;
} }
if (
fieldMetadataItem.type === FieldMetadataType.Relation &&
fieldMetadataItem.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
) {
return undefined;
}
return [fieldName, fieldValue]; return [fieldName, fieldValue];
}) })
.filter(isDefined), .filter(isDefined),
@@ -1,12 +1,12 @@
import { import {
IconComponent, IconComponent,
IconRelationManyToMany,
IconRelationManyToOne, IconRelationManyToOne,
IconRelationOneToMany, IconRelationOneToMany,
IconRelationOneToOne, IconRelationOneToOne,
} from 'twenty-ui'; } from 'twenty-ui';
import { RelationMetadataType } from '~/generated-metadata/graphql'; import { RelationDefinitionType } from '~/generated-metadata/graphql';
import OneToManySvg from '../assets/OneToMany.svg'; import OneToManySvg from '../assets/OneToMany.svg';
import OneToOneSvg from '../assets/OneToOne.svg'; import OneToOneSvg from '../assets/OneToOne.svg';
import { RelationType } from '../types/RelationType'; import { RelationType } from '../types/RelationType';
@@ -20,20 +20,27 @@ export const RELATION_TYPES: Record<
isImageFlipped?: boolean; isImageFlipped?: boolean;
} }
> = { > = {
[RelationMetadataType.OneToMany]: { [RelationDefinitionType.OneToMany]: {
label: 'Has many', label: 'Has many',
Icon: IconRelationOneToMany, Icon: IconRelationOneToMany,
imageSrc: OneToManySvg, imageSrc: OneToManySvg,
}, },
[RelationMetadataType.OneToOne]: { [RelationDefinitionType.OneToOne]: {
label: 'Has one', label: 'Has one',
Icon: IconRelationOneToOne, Icon: IconRelationOneToOne,
imageSrc: OneToOneSvg, imageSrc: OneToOneSvg,
}, },
MANY_TO_ONE: { [RelationDefinitionType.ManyToOne]: {
label: 'Belongs to one', label: 'Belongs to one',
Icon: IconRelationManyToOne, Icon: IconRelationManyToOne,
imageSrc: OneToManySvg, imageSrc: OneToManySvg,
isImageFlipped: true, isImageFlipped: true,
}, },
// Not supported yet
[RelationDefinitionType.ManyToMany]: {
label: 'Belongs to many',
Icon: IconRelationManyToMany,
imageSrc: OneToManySvg,
isImageFlipped: true,
},
}; };
@@ -1,5 +1,5 @@
import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Controller, useFormContext } from 'react-hook-form';
import { useIcons } from 'twenty-ui'; import { useIcons } from 'twenty-ui';
import { z } from 'zod'; import { z } from 'zod';
@@ -14,6 +14,7 @@ import { RelationType } from '@/settings/data-model/types/RelationType';
import { IconPicker } from '@/ui/input/components/IconPicker'; import { IconPicker } from '@/ui/input/components/IconPicker';
import { Select } from '@/ui/input/components/Select'; import { Select } from '@/ui/input/components/Select';
import { TextInput } from '@/ui/input/components/TextInput'; import { TextInput } from '@/ui/input/components/TextInput';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
export const settingsDataModelFieldRelationFormSchema = z.object({ export const settingsDataModelFieldRelationFormSchema = z.object({
relation: z.object({ relation: z.object({
@@ -23,7 +24,10 @@ export const settingsDataModelFieldRelationFormSchema = z.object({
}), }),
objectMetadataId: z.string().uuid(), objectMetadataId: z.string().uuid(),
type: z.enum( 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 = { type SettingsDataModelFieldRelationFormProps = {
fieldMetadataItem: Pick< fieldMetadataItem: Pick<FieldMetadataItem, 'type'>;
FieldMetadataItem,
'fromRelationMetadata' | 'toRelationMetadata' | 'type'
>;
}; };
const StyledContainer = styled.div` const StyledContainer = styled.div`
@@ -1,5 +1,5 @@
import { useFormContext } from 'react-hook-form';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useFormContext } from 'react-hook-form';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
@@ -5,15 +5,12 @@ import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMe
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation'; import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
import { RelationMetadataType } from '~/generated-metadata/graphql'; import { RelationDefinitionType } from '~/generated-metadata/graphql';
export const useRelationSettingsFormInitialValues = ({ export const useRelationSettingsFormInitialValues = ({
fieldMetadataItem, fieldMetadataItem,
}: { }: {
fieldMetadataItem?: Pick< fieldMetadataItem?: Pick<FieldMetadataItem, 'type' | 'relationDefinition'>;
FieldMetadataItem,
'fromRelationMetadata' | 'toRelationMetadata' | 'type'
>;
}) => { }) => {
const { objectMetadataItems } = useFilteredObjectMetadataItems(); const { objectMetadataItems } = useFilteredObjectMetadataItems();
@@ -39,7 +36,7 @@ export const useRelationSettingsFormInitialValues = ({
); );
const initialRelationType = const initialRelationType =
relationTypeFromFieldMetadata ?? RelationMetadataType.OneToMany; relationTypeFromFieldMetadata ?? RelationDefinitionType.OneToMany;
return { return {
disableFieldEdition: disableFieldEdition:
@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { Edge, Node } from 'reactflow';
import dagre from '@dagrejs/dagre'; import dagre from '@dagrejs/dagre';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { useEffect } from 'react';
import { Edge, Node } from 'reactflow';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
@@ -43,10 +43,10 @@ export const SettingsDataModelOverviewEffect = ({
for (const field of object.fields) { for (const field of object.fields) {
if ( if (
isDefined(field.toRelationMetadata) && isDefined(field.relationDefinition) &&
isDefined( isDefined(
items.find( 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}`, id: `${sourceObj}-${targetObj}`,
source: object.namePlural, source: object.namePlural,
sourceHandle: `${field.id}-right`, sourceHandle: `${field.id}-right`,
target: field.toRelationMetadata.fromObjectMetadata.namePlural, target: field.relationDefinition.targetObjectMetadata.namePlural,
targetHandle: `${field.toRelationMetadata.fromFieldMetadataId}-left`, targetHandle: `${field.relationDefinition.targetObjectMetadata}-left`,
type: 'smoothstep', type: 'smoothstep',
style: { style: {
strokeWidth: 1, strokeWidth: 1,
@@ -70,8 +70,8 @@ export const SettingsDataModelOverviewEffect = ({
markerStart: 'marker', markerStart: 'marker',
data: { data: {
sourceField: field.id, sourceField: field.id,
targetField: field.toRelationMetadata.fromFieldMetadataId, targetField: field.relationDefinition.targetFieldMetadata.id,
relation: field.toRelationMetadata.relationType, relation: field.relationDefinition.direction,
sourceObject: sourceObj, sourceObject: sourceObj,
targetObject: targetObj, targetObject: targetObj,
}, },
@@ -6,6 +6,7 @@ import { useIcons } from 'twenty-ui';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
type ObjectFieldRowProps = { type ObjectFieldRowProps = {
field: FieldMetadataItem; field: FieldMetadataItem;
@@ -42,21 +43,33 @@ export const ObjectFieldRow = ({ field }: ObjectFieldRowProps) => {
{Icon && <Icon size={theme.icon.size.md} />} {Icon && <Icon size={theme.icon.size.md} />}
<StyledFieldName>{relatedObject?.labelPlural ?? ''}</StyledFieldName> <StyledFieldName>{relatedObject?.labelPlural ?? ''}</StyledFieldName>
<Handle <Handle
type={field.toRelationMetadata ? 'source' : 'target'} type={
field.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
? 'source'
: 'target'
}
position={Position.Right} position={Position.Right}
id={`${field.id}-right`} id={`${field.id}-right`}
className={ className={
field.fromRelationMetadata field.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
? 'right-handle source-handle' ? 'right-handle source-handle'
: 'right-handle target-handle' : 'right-handle target-handle'
} }
/> />
<Handle <Handle
type={field.toRelationMetadata ? 'source' : 'target'} type={
field.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
? 'source'
: 'target'
}
position={Position.Left} position={Position.Left}
id={`${field.id}-left`} id={`${field.id}-left`}
className={ className={
field.fromRelationMetadata field.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
? 'left-handle source-handle' ? 'left-handle source-handle'
: 'left-handle target-handle' : 'left-handle target-handle'
} }
@@ -29,7 +29,7 @@ import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMe
import { View } from '@/views/types/View'; import { View } from '@/views/types/View';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useRecoilState } from 'recoil'; 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 { SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem';
import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType'; import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType';
@@ -224,8 +224,8 @@ export const SettingsObjectFieldItemTableRow = ({
<SettingsObjectFieldDataType <SettingsObjectFieldDataType
Icon={RelationIcon} Icon={RelationIcon}
label={ label={
relationType === RelationMetadataType.ManyToOne || relationType === RelationDefinitionType.ManyToOne ||
relationType === RelationMetadataType.OneToOne relationType === RelationDefinitionType.OneToOne
? relationObjectMetadataItem?.labelSingular ? relationObjectMetadataItem?.labelSingular
: relationObjectMetadataItem?.labelPlural : relationObjectMetadataItem?.labelPlural
} }
@@ -1,5 +1,3 @@
import { RelationMetadataType } from '~/generated-metadata/graphql'; import { RelationDefinitionType } from '~/generated-metadata/graphql';
export type RelationType = export type RelationType = RelationDefinitionType;
| Exclude<RelationMetadataType, 'MANY_TO_MANY'>
| 'MANY_TO_ONE';
@@ -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 { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns'; 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 = ( export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
[ [
@@ -65,7 +68,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation, type: FieldMetadataType.Relation,
metadata: { metadata: {
fieldName: 'favorites', fieldName: 'favorites',
relationType: 'FROM_MANY_OBJECTS', relationType: RelationDefinitionType.OneToMany,
relationObjectMetadataNameSingular: '', relationObjectMetadataNameSingular: '',
relationObjectMetadataNamePlural: '', relationObjectMetadataNamePlural: '',
objectMetadataNameSingular: 'company', objectMetadataNameSingular: 'company',
@@ -99,7 +102,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation, type: FieldMetadataType.Relation,
metadata: { metadata: {
fieldName: 'accountOwner', fieldName: 'accountOwner',
relationType: 'TO_ONE_OBJECT', relationType: RelationDefinitionType.ManyToOne,
relationObjectMetadataNameSingular: 'workspaceMember', relationObjectMetadataNameSingular: 'workspaceMember',
relationObjectMetadataNamePlural: 'workspaceMembers', relationObjectMetadataNamePlural: 'workspaceMembers',
objectMetadataNameSingular: 'company', objectMetadataNameSingular: 'company',
@@ -116,7 +119,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation, type: FieldMetadataType.Relation,
metadata: { metadata: {
fieldName: 'people', fieldName: 'people',
relationType: 'FROM_MANY_OBJECTS', relationType: RelationDefinitionType.OneToMany,
relationObjectMetadataNameSingular: '', relationObjectMetadataNameSingular: '',
relationObjectMetadataNamePlural: '', relationObjectMetadataNamePlural: '',
objectMetadataNameSingular: 'company', objectMetadataNameSingular: 'company',
@@ -133,7 +136,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation, type: FieldMetadataType.Relation,
metadata: { metadata: {
fieldName: 'attachments', fieldName: 'attachments',
relationType: 'FROM_MANY_OBJECTS', relationType: RelationDefinitionType.OneToMany,
relationObjectMetadataNameSingular: '', relationObjectMetadataNameSingular: '',
relationObjectMetadataNamePlural: '', relationObjectMetadataNamePlural: '',
objectMetadataNameSingular: 'company', objectMetadataNameSingular: 'company',
@@ -201,7 +204,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation, type: FieldMetadataType.Relation,
metadata: { metadata: {
fieldName: 'opportunities', fieldName: 'opportunities',
relationType: 'FROM_MANY_OBJECTS', relationType: RelationDefinitionType.OneToMany,
relationObjectMetadataNameSingular: '', relationObjectMetadataNameSingular: '',
relationObjectMetadataNamePlural: '', relationObjectMetadataNamePlural: '',
objectMetadataNameSingular: 'company', objectMetadataNameSingular: 'company',
@@ -235,7 +238,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation, type: FieldMetadataType.Relation,
metadata: { metadata: {
fieldName: 'activityTargets', fieldName: 'activityTargets',
relationType: 'FROM_MANY_OBJECTS', relationType: RelationDefinitionType.OneToMany,
relationObjectMetadataNameSingular: '', relationObjectMetadataNameSingular: '',
relationObjectMetadataNamePlural: '', relationObjectMetadataNamePlural: '',
objectMetadataNameSingular: 'company', objectMetadataNameSingular: 'company',
@@ -1,8 +1,8 @@
import { useMemo } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import qs from 'qs'; import qs from 'qs';
import { useMemo } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import { useRecoilCallback, useRecoilValue } from 'recoil'; import { useRecoilCallback, useRecoilValue } from 'recoil';
import z from 'zod'; import z from 'zod';
@@ -92,12 +92,12 @@ export const useViewFromQueryParams = () => {
if (isUndefinedOrNull(filterDefinition)) return null; if (isUndefinedOrNull(filterDefinition)) return null;
const relationObjectMetadataNameSingular = const relationObjectMetadataNameSingular =
fieldMetadataItem.toRelationMetadata?.fromObjectMetadata fieldMetadataItem.relationDefinition?.targetObjectMetadata
.nameSingular; ?.nameSingular;
const relationObjectMetadataNamePlural = const relationObjectMetadataNamePlural =
fieldMetadataItem.toRelationMetadata?.fromObjectMetadata fieldMetadataItem.relationDefinition?.targetObjectMetadata
.namePlural; ?.namePlural;
const relationObjectMetadataItem = const relationObjectMetadataItem =
relationObjectMetadataNameSingular relationObjectMetadataNameSingular
@@ -83,22 +83,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
name: 'myCustom', 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', updatedAt: '2024-04-08T12:48:49.538Z',
defaultValue: null, defaultValue: null,
relationDefinition: null, relationDefinition: null,
fromRelationMetadata: null,
toRelationMetadata: null,
}, },
}, },
{ {
@@ -143,8 +125,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
updatedAt: '2024-04-08T12:48:49.538Z', updatedAt: '2024-04-08T12:48:49.538Z',
defaultValue: null, defaultValue: null,
relationDefinition: null, relationDefinition: null,
fromRelationMetadata: null,
toRelationMetadata: null,
}, },
}, },
{ {
@@ -166,8 +146,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
updatedAt: '2024-04-08T12:48:49.538Z', updatedAt: '2024-04-08T12:48:49.538Z',
defaultValue: "''", defaultValue: "''",
relationDefinition: null, relationDefinition: null,
fromRelationMetadata: null,
toRelationMetadata: null,
}, },
}, },
{ {
@@ -189,8 +167,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
updatedAt: '2024-04-08T12:48:49.538Z', updatedAt: '2024-04-08T12:48:49.538Z',
defaultValue: 'now', defaultValue: 'now',
relationDefinition: null, relationDefinition: null,
fromRelationMetadata: null,
toRelationMetadata: null,
}, },
}, },
{ {
@@ -212,8 +188,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
updatedAt: '2024-04-08T12:48:49.538Z', updatedAt: '2024-04-08T12:48:49.538Z',
defaultValue: 'now', defaultValue: 'now',
relationDefinition: null, relationDefinition: null,
fromRelationMetadata: null,
toRelationMetadata: null,
}, },
}, },
{ {
@@ -235,8 +209,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
updatedAt: '2024-04-08T12:48:49.538Z', updatedAt: '2024-04-08T12:48:49.538Z',
defaultValue: 'uuid', defaultValue: 'uuid',
relationDefinition: null, relationDefinition: null,
fromRelationMetadata: null,
toRelationMetadata: null,
}, },
}, },
{ {
@@ -277,8 +249,6 @@ const customObjectMetadataItemEdge: ObjectEdge = {
updatedAt: '2024-04-08T12:48:49.538Z', updatedAt: '2024-04-08T12:48:49.538Z',
defaultValue: null, defaultValue: null,
relationDefinition: null, relationDefinition: null,
fromRelationMetadata: null,
toRelationMetadata: null,
}, },
}, },
], ],
@@ -17,12 +17,12 @@ import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.
import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module'; import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { useSentryTracing } from 'src/engine/core-modules/exception-handler/hooks/use-sentry-tracing'; import { useSentryTracing } from 'src/engine/core-modules/exception-handler/hooks/use-sentry-tracing';
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { handleExceptionAndConvertToGraphQLError } from 'src/engine/utils/global-exception-handler.util'; import { handleExceptionAndConvertToGraphQLError } from 'src/engine/utils/global-exception-handler.util';
import { renderApolloPlayground } from 'src/engine/utils/render-apollo-playground.util'; import { renderApolloPlayground } from 'src/engine/utils/render-apollo-playground.util';
@@ -69,13 +69,18 @@ export class GraphQLConfigService
let workspace: Workspace | undefined; let workspace: Workspace | undefined;
try { try {
if (!this.tokenService.isTokenPresent(context.req)) { const { user, workspace, apiKey, workspaceMemberId } = context.req;
if (!workspace) {
return new GraphQLSchema({}); return new GraphQLSchema({});
} }
const data = await this.tokenService.validateToken(context.req); return await this.createSchema(context, {
user,
return await this.createSchema(context, data); workspace,
apiKey,
workspaceMemberId,
});
} catch (error) { } catch (error) {
if (error instanceof UnauthorizedException) { if (error instanceof UnauthorizedException) {
throw new GraphQLError('Unauthenticated', { throw new GraphQLError('Unauthenticated', {
@@ -2,7 +2,7 @@ import { FindOptionsWhere, ObjectLiteral } from 'typeorm';
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser'; import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser';
@@ -3,9 +3,9 @@ import { FindOptionsWhere, Not, ObjectLiteral } from 'typeorm';
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { capitalize } from 'src/utils/capitalize'; import { capitalize } from 'src/utils/capitalize';
import { isPlainObject } from 'src/utils/is-plain-object'; import { isPlainObject } from 'src/utils/is-plain-object';
@@ -1,8 +1,8 @@
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser'; import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser';
import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
describe('GraphqlQueryOrderFieldParser', () => { describe('GraphqlQueryOrderFieldParser', () => {
let parser: GraphqlQueryOrderFieldParser; let parser: GraphqlQueryOrderFieldParser;
@@ -10,12 +10,11 @@ import {
GraphqlQueryRunnerException, GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode, GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { capitalize } from 'src/utils/capitalize'; import { capitalize } from 'src/utils/capitalize';
export class GraphqlQueryOrderFieldParser { export class GraphqlQueryOrderFieldParser {
private fieldMetadataMap: FieldMetadataMap; private fieldMetadataMap: FieldMetadataMap;
@@ -1,8 +1,8 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
import { ObjectMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util'; import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util';
import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
export class GraphqlQuerySelectedFieldsRelationParser { export class GraphqlQuerySelectedFieldsRelationParser {
private objectMetadataMap: ObjectMetadataMap; private objectMetadataMap: ObjectMetadataMap;
@@ -5,9 +5,9 @@ import {
GraphqlQueryRunnerExceptionCode, GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser'; import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser';
import { ObjectMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { capitalize } from 'src/utils/capitalize'; import { capitalize } from 'src/utils/capitalize';
@@ -17,7 +17,7 @@ import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql
import { import {
FieldMetadataMap, FieldMetadataMap,
ObjectMetadataMap, ObjectMetadataMap,
} from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
export class GraphqlQueryParser { export class GraphqlQueryParser {
private fieldMetadataMap: FieldMetadataMap; private fieldMetadataMap: FieldMetadataMap;
@@ -9,12 +9,12 @@ import {
GraphqlQueryRunnerException, GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode, GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { ObjectMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util'; import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { isPlainObject } from 'src/utils/is-plain-object'; import { isPlainObject } from 'src/utils/is-plain-object';
@@ -19,11 +19,9 @@ import {
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper'; import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
import { applyRangeFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util'; import { applyRangeFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util';
import {
convertObjectMetadataToMap,
getObjectMetadata,
} from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
import { decodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; import { decodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util';
import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
export class GraphqlQueryFindManyResolverService { export class GraphqlQueryFindManyResolverService {
@@ -51,10 +49,10 @@ export class GraphqlQueryFindManyResolverService {
authContext.workspace.id, authContext.workspace.id,
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
); );
const objectMetadataMap = convertObjectMetadataToMap( const objectMetadataMap = generateObjectMetadataMap(
objectMetadataCollection, objectMetadataCollection,
); );
const objectMetadata = getObjectMetadata( const objectMetadata = getObjectMetadataOrThrow(
objectMetadataMap, objectMetadataMap,
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
); );
@@ -13,10 +13,8 @@ import {
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper'; import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
import { import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util';
convertObjectMetadataToMap, import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
getObjectMetadata,
} from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
export class GraphqlQueryFindOneResolverService { export class GraphqlQueryFindOneResolverService {
@@ -40,10 +38,11 @@ export class GraphqlQueryFindOneResolverService {
authContext.workspace.id, authContext.workspace.id,
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
); );
const objectMetadataMap = convertObjectMetadataToMap( const objectMetadataMap = generateObjectMetadataMap(
objectMetadataCollection, objectMetadataCollection,
); );
const objectMetadata = getObjectMetadata(
const objectMetadata = getObjectMetadataOrThrow(
objectMetadataMap, objectMetadataMap,
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
); );
@@ -0,0 +1,21 @@
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
export const getObjectMetadataOrThrow = (
objectMetadataMap: Record<string, any>,
objectName: string,
): ObjectMetadataMapItem => {
const objectMetadata = objectMetadataMap[objectName];
if (!objectMetadata) {
throw new GraphqlQueryRunnerException(
`Object metadata not found for ${objectName}`,
GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
return objectMetadata;
};
@@ -1,6 +1,6 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
import { import {
deduceRelationDirection, deduceRelationDirection,
RelationDirection, RelationDirection,
@@ -4,11 +4,11 @@ import GraphQLJSON from 'graphql-type-json';
import { useCachedMetadata } from 'src/engine/api/graphql/graphql-config/hooks/use-cached-metadata'; import { useCachedMetadata } from 'src/engine/api/graphql/graphql-config/hooks/use-cached-metadata';
import { useThrottler } from 'src/engine/api/graphql/graphql-config/hooks/use-throttler'; import { useThrottler } from 'src/engine/api/graphql/graphql-config/hooks/use-throttler';
import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module'; import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module';
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
import { DataloaderService } from 'src/engine/dataloaders/dataloader.service';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/cache-storage.service'; import { CacheStorageService } from 'src/engine/core-modules/cache-storage/cache-storage.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
import { DataloaderService } from 'src/engine/dataloaders/dataloader.service';
import { renderApolloPlayground } from 'src/engine/utils/render-apollo-playground.util'; import { renderApolloPlayground } from 'src/engine/utils/render-apollo-playground.util';
export const metadataModuleFactory = async ( export const metadataModuleFactory = async (
@@ -56,13 +56,13 @@ export class WorkspaceSchemaFactory {
); );
} }
const objectMetadataCollection = const objectMetadataMap =
await this.workspaceCacheStorageService.getObjectMetadataCollection( await this.workspaceCacheStorageService.getObjectMetadataMap(
authContext.workspace.id, authContext.workspace.id,
currentCacheVersion, currentCacheVersion,
); );
if (!objectMetadataCollection) { if (!objectMetadataMap) {
await this.workspaceMetadataCacheService.recomputeMetadataCache( await this.workspaceMetadataCacheService.recomputeMetadataCache(
authContext.workspace.id, authContext.workspace.id,
); );
@@ -72,6 +72,13 @@ export class WorkspaceSchemaFactory {
); );
} }
const objectMetadataCollection = Object.values(objectMetadataMap).map(
(objectMetadataItem) => ({
...objectMetadataItem,
fields: Object.values(objectMetadataItem.fields),
}),
);
// Get typeDefs from cache // Get typeDefs from cache
let typeDefs = await this.workspaceCacheStorageService.getGraphQLTypeDefs( let typeDefs = await this.workspaceCacheStorageService.getGraphQLTypeDefs(
authContext.workspace.id, authContext.workspace.id,
@@ -4,10 +4,10 @@ import { Request, Response } from 'express';
import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.service'; import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.service';
import { cleanGraphQLResponse } from 'src/engine/api/rest/utils/clean-graphql-response.utils'; import { cleanGraphQLResponse } from 'src/engine/api/rest/utils/clean-graphql-response.utils';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/batch/*') @Controller('rest/batch/*')
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard)
export class RestApiCoreBatchController { export class RestApiCoreBatchController {
constructor(private readonly restApiCoreService: RestApiCoreService) {} constructor(private readonly restApiCoreService: RestApiCoreService) {}
@@ -14,10 +14,11 @@ import { Request, Response } from 'express';
import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.service'; import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.service';
import { cleanGraphQLResponse } from 'src/engine/api/rest/utils/clean-graphql-response.utils'; import { cleanGraphQLResponse } from 'src/engine/api/rest/utils/clean-graphql-response.utils';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/*') @Controller('rest/*')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
export class RestApiCoreController { export class RestApiCoreController {
constructor(private readonly restApiCoreService: RestApiCoreService) {} constructor(private readonly restApiCoreService: RestApiCoreService) {}
@@ -1,23 +1,25 @@
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios'; import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller';
import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.service';
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { RestApiMetadataController } from 'src/engine/api/rest/metadata/rest-api-metadata.controller';
import { RestApiMetadataService } from 'src/engine/api/rest/metadata/rest-api-metadata.service';
import { RestApiCoreBatchController } from 'src/engine/api/rest/core/controllers/rest-api-core-batch.controller'; import { RestApiCoreBatchController } from 'src/engine/api/rest/core/controllers/rest-api-core-batch.controller';
import { RestApiService } from 'src/engine/api/rest/rest-api.service'; import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller';
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
import { RestApiCoreService } from 'src/engine/api/rest/core/rest-api-core.service';
import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory'; import { EndingBeforeInputFactory } from 'src/engine/api/rest/input-factories/ending-before-input.factory';
import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory'; import { LimitInputFactory } from 'src/engine/api/rest/input-factories/limit-input.factory';
import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory'; import { StartingAfterInputFactory } from 'src/engine/api/rest/input-factories/starting-after-input.factory';
import { MetadataQueryBuilderModule } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.module'; import { MetadataQueryBuilderModule } from 'src/engine/api/rest/metadata/query-builder/metadata-query-builder.module';
import { RestApiMetadataController } from 'src/engine/api/rest/metadata/rest-api-metadata.controller';
import { RestApiMetadataService } from 'src/engine/api/rest/metadata/rest-api-metadata.service';
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@Module({ @Module({
imports: [ imports: [
CoreQueryBuilderModule, CoreQueryBuilderModule,
MetadataQueryBuilderModule, MetadataQueryBuilderModule,
WorkspaceCacheStorageModule,
AuthModule, AuthModule,
HttpModule, HttpModule,
], ],
@@ -12,7 +12,8 @@ import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@ArgsType() @ArgsType()
class GetAISQLQueryArgs { class GetAISQLQueryArgs {
@@ -20,7 +21,7 @@ class GetAISQLQueryArgs {
text: string; text: string;
} }
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@Resolver(() => AISQLQueryResult) @Resolver(() => AISQLQueryResult)
export class AISQLQueryResolver { export class AISQLQueryResolver {
constructor( constructor(
@@ -1,21 +1,18 @@
import { Resolver, Mutation, Args, Context } from '@nestjs/graphql'; import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { Request } from 'express'; import { Request } from 'express';
import { OptionalJwtAuthGuard } from 'src/engine/guards/optional-jwt.auth.guard';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { AnalyticsService } from './analytics.service';
import { Analytics } from './analytics.entity'; import { Analytics } from './analytics.entity';
import { AnalyticsService } from './analytics.service';
import { CreateAnalyticsInput } from './dtos/create-analytics.input'; import { CreateAnalyticsInput } from './dtos/create-analytics.input';
@UseGuards(OptionalJwtAuthGuard)
@Resolver(() => Analytics) @Resolver(() => Analytics)
export class AnalyticsResolver { export class AnalyticsResolver {
constructor( constructor(
@@ -6,7 +6,7 @@ import {
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity'; import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { CreateAppTokenInput } from 'src/engine/core-modules/app-token/dtos/create-app-token.input'; import { CreateAppTokenInput } from 'src/engine/core-modules/app-token/dtos/create-app-token.input';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
export const appTokenAutoResolverOpts: AutoResolverOpts< export const appTokenAutoResolverOpts: AutoResolverOpts<
any, any,
@@ -34,6 +34,6 @@ export const appTokenAutoResolverOpts: AutoResolverOpts<
one: { disabled: true }, one: { disabled: true },
}, },
delete: { many: { disabled: true }, one: { disabled: true } }, delete: { many: { disabled: true }, one: { disabled: true } },
guards: [JwtAuthGuard], guards: [WorkspaceAuthGuard],
}, },
]; ];
@@ -13,7 +13,7 @@ export class BeforeCreateOneAppToken<T extends AppToken>
instance: CreateOneInputType<T>, instance: CreateOneInputType<T>,
context: any, context: any,
): Promise<CreateOneInputType<T>> { ): Promise<CreateOneInputType<T>> {
const userId = context?.req?.user?.user?.id; const userId = context?.req?.user?.id;
instance.input.userId = userId; instance.input.userId = userId;
// FIXME: These fields should be autogenerated, we need to run a migration for this // FIXME: These fields should be autogenerated, we need to run a migration for this
@@ -16,13 +16,14 @@ import { UpdatePasswordViaResetTokenInput } from 'src/engine/core-modules/auth/d
import { ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.entity'; import { ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.entity';
import { ValidatePasswordResetTokenInput } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.input'; import { ValidatePasswordResetTokenInput } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.input';
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter'; import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard';
import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity'; import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { ChallengeInput } from './dto/challenge.input'; import { ChallengeInput } from './dto/challenge.input';
import { ImpersonateInput } from './dto/impersonate.input'; import { ImpersonateInput } from './dto/impersonate.input';
@@ -111,7 +112,7 @@ export class AuthResolver {
} }
@Mutation(() => TransientToken) @Mutation(() => TransientToken)
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard, UserAuthGuard)
async generateTransientToken( async generateTransientToken(
@AuthUser() user: User, @AuthUser() user: User,
): Promise<TransientToken | void> { ): Promise<TransientToken | void> {
@@ -141,7 +142,7 @@ export class AuthResolver {
} }
@Mutation(() => AuthorizeApp) @Mutation(() => AuthorizeApp)
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard, UserAuthGuard)
async authorizeApp( async authorizeApp(
@Args() authorizeAppInput: AuthorizeAppInput, @Args() authorizeAppInput: AuthorizeAppInput,
@AuthUser() user: User, @AuthUser() user: User,
@@ -155,7 +156,7 @@ export class AuthResolver {
} }
@Mutation(() => AuthTokens) @Mutation(() => AuthTokens)
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard, UserAuthGuard)
async generateJWT( async generateJWT(
@AuthUser() user: User, @AuthUser() user: User,
@Args() args: GenerateJwtInput, @Args() args: GenerateJwtInput,
@@ -177,7 +178,7 @@ export class AuthResolver {
return { tokens: tokens }; return { tokens: tokens };
} }
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@Mutation(() => Verify) @Mutation(() => Verify)
async impersonate( async impersonate(
@Args() impersonateInput: ImpersonateInput, @Args() impersonateInput: ImpersonateInput,
@@ -186,7 +187,7 @@ export class AuthResolver {
return await this.authService.impersonate(impersonateInput.userId, user); return await this.authService.impersonate(impersonateInput.userId, user);
} }
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard)
@Mutation(() => ApiKeyToken) @Mutation(() => ApiKeyToken)
async generateApiKeyToken( async generateApiKeyToken(
@Args() args: ApiKeyTokenInput, @Args() args: ApiKeyTokenInput,
@@ -36,14 +36,14 @@ import {
JwtPayload, JwtPayload,
} from 'src/engine/core-modules/auth/strategies/jwt.auth.strategy'; } from 'src/engine/core-modules/auth/strategies/jwt.auth.strategy';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
import { User } from 'src/engine/core-modules/user/user.entity'; import { User } from 'src/engine/core-modules/user/user.entity';
import { import {
Workspace, Workspace,
WorkspaceActivationStatus, WorkspaceActivationStatus,
} from 'src/engine/core-modules/workspace/workspace.entity'; } from 'src/engine/core-modules/workspace/workspace.entity';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -11,9 +11,9 @@ import {
AuthExceptionCode, AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception'; } from 'src/engine/core-modules/auth/auth.exception';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { User } from 'src/engine/core-modules/user/user.entity'; import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity'; import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
@@ -90,7 +90,6 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
if (payload.workspaceId) { if (payload.workspaceId) {
user = await this.userRepository.findOne({ user = await this.userRepository.findOne({
where: { id: payload.sub }, where: { id: payload.sub },
relations: ['defaultWorkspace'],
}); });
if (!user) { if (!user) {
throw new AuthException( throw new AuthException(
@@ -16,7 +16,8 @@ import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Resolver() @Resolver()
export class BillingResolver { export class BillingResolver {
@@ -37,7 +38,7 @@ export class BillingResolver {
} }
@Query(() => SessionEntity) @Query(() => SessionEntity)
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard, UserAuthGuard)
async billingPortalSession( async billingPortalSession(
@AuthUser() user: User, @AuthUser() user: User,
@Args() { returnUrlPath }: BillingSessionInput, @Args() { returnUrlPath }: BillingSessionInput,
@@ -51,7 +52,7 @@ export class BillingResolver {
} }
@Mutation(() => SessionEntity) @Mutation(() => SessionEntity)
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard, UserAuthGuard)
async checkoutSession( async checkoutSession(
@AuthWorkspace() workspace: Workspace, @AuthWorkspace() workspace: Workspace,
@AuthUser() user: User, @AuthUser() user: User,
@@ -79,7 +80,7 @@ export class BillingResolver {
} }
@Mutation(() => UpdateBillingEntity) @Mutation(() => UpdateBillingEntity)
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard)
async updateBillingSubscription(@AuthUser() user: User) { async updateBillingSubscription(@AuthUser() user: User) {
await this.billingSubscriptionService.applyBillingSubscription(user); await this.billingSubscriptionService.applyBillingSubscription(user);
@@ -7,7 +7,7 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
import { TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants'; import { TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants';
import { TimelineCalendarEventsWithTotal } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto'; import { TimelineCalendarEventsWithTotal } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto';
import { TimelineCalendarEventService } from 'src/engine/core-modules/calendar/timeline-calendar-event.service'; import { TimelineCalendarEventService } from 'src/engine/core-modules/calendar/timeline-calendar-event.service';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@ArgsType() @ArgsType()
class GetTimelineCalendarEventsFromPersonIdArgs { class GetTimelineCalendarEventsFromPersonIdArgs {
@@ -35,7 +35,7 @@ class GetTimelineCalendarEventsFromCompanyIdArgs {
pageSize: number; pageSize: number;
} }
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard)
@Resolver(() => TimelineCalendarEventsWithTotal) @Resolver(() => TimelineCalendarEventsWithTotal)
export class TimelineCalendarEventResolver { export class TimelineCalendarEventResolver {
constructor( constructor(
@@ -1,5 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { FileUploadResolver } from './file-upload.resolver'; import { FileUploadResolver } from './file-upload.resolver';
@@ -15,6 +16,10 @@ describe('FileUploadResolver', () => {
provide: FileUploadService, provide: FileUploadService,
useValue: {}, useValue: {},
}, },
{
provide: EnvironmentService,
useValue: {},
},
], ],
}).compile(); }).compile();
@@ -9,10 +9,10 @@ import { FileUploadService } from 'src/engine/core-modules/file/file-upload/serv
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard'; import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { streamToBuffer } from 'src/utils/stream-to-buffer'; import { streamToBuffer } from 'src/utils/stream-to-buffer';
@UseGuards(JwtAuthGuard, DemoEnvGuard) @UseGuards(WorkspaceAuthGuard, DemoEnvGuard)
@Resolver() @Resolver()
export class FileUploadResolver { export class FileUploadResolver {
constructor(private readonly fileUploadService: FileUploadService) {} constructor(private readonly fileUploadService: FileUploadService) {}
@@ -10,7 +10,8 @@ import { GetMessagesService } from 'src/engine/core-modules/messaging/services/g
import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity'; import { User } from 'src/engine/core-modules/user/user.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@ArgsType() @ArgsType()
class GetTimelineThreadsFromPersonIdArgs { class GetTimelineThreadsFromPersonIdArgs {
@@ -38,7 +39,7 @@ class GetTimelineThreadsFromCompanyIdArgs {
pageSize: number; pageSize: number;
} }
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@Resolver(() => TimelineThreadsWithTotal) @Resolver(() => TimelineThreadsWithTotal)
export class TimelineMessagingResolver { export class TimelineMessagingResolver {
constructor( constructor(
@@ -7,9 +7,10 @@ import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@Resolver() @Resolver()
export class OnboardingResolver { export class OnboardingResolver {
constructor(private readonly onboardingService: OnboardingService) {} constructor(private readonly onboardingService: OnboardingService) {}
@@ -1,11 +1,11 @@
import { UseGuards } from '@nestjs/common'; import { UseGuards } from '@nestjs/common';
import { Resolver, Mutation, Query } from '@nestjs/graphql'; import { Mutation, Query, Resolver } from '@nestjs/graphql';
import { PostgresCredentialsDTO } from 'src/engine/core-modules/postgres-credentials/dtos/postgres-credentials.dto'; import { PostgresCredentialsDTO } from 'src/engine/core-modules/postgres-credentials/dtos/postgres-credentials.dto';
import { PostgresCredentialsService } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.service'; import { PostgresCredentialsService } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Resolver(() => PostgresCredentialsDTO) @Resolver(() => PostgresCredentialsDTO)
export class PostgresCredentialsResolver { export class PostgresCredentialsResolver {
@@ -13,19 +13,19 @@ export class PostgresCredentialsResolver {
private readonly postgresCredentialsService: PostgresCredentialsService, private readonly postgresCredentialsService: PostgresCredentialsService,
) {} ) {}
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard)
@Mutation(() => PostgresCredentialsDTO) @Mutation(() => PostgresCredentialsDTO)
async enablePostgresProxy(@AuthWorkspace() { id: workspaceId }: Workspace) { async enablePostgresProxy(@AuthWorkspace() { id: workspaceId }: Workspace) {
return this.postgresCredentialsService.enablePostgresProxy(workspaceId); return this.postgresCredentialsService.enablePostgresProxy(workspaceId);
} }
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard)
@Mutation(() => PostgresCredentialsDTO) @Mutation(() => PostgresCredentialsDTO)
async disablePostgresProxy(@AuthWorkspace() { id: workspaceId }: Workspace) { async disablePostgresProxy(@AuthWorkspace() { id: workspaceId }: Workspace) {
return this.postgresCredentialsService.disablePostgresProxy(workspaceId); return this.postgresCredentialsService.disablePostgresProxy(workspaceId);
} }
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard)
@Query(() => PostgresCredentialsDTO, { nullable: true }) @Query(() => PostgresCredentialsDTO, { nullable: true })
async getPostgresCredentials( async getPostgresCredentials(
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
@@ -4,15 +4,15 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceInviteHashValidInput } from 'src/engine/core-modules/auth/dto/workspace-invite-hash.input'; import { WorkspaceInviteHashValidInput } from 'src/engine/core-modules/auth/dto/workspace-invite-hash.input';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard)
@Resolver(() => UserWorkspace) @Resolver(() => UserWorkspace)
export class UserWorkspaceResolver { export class UserWorkspaceResolver {
constructor( constructor(
@@ -1,11 +1,11 @@
import { import {
AutoResolverOpts, AutoResolverOpts,
ReadResolverOpts,
PagingStrategies, PagingStrategies,
ReadResolverOpts,
} from '@ptc-org/nestjs-query-graphql'; } from '@ptc-org/nestjs-query-graphql';
import { User } from 'src/engine/core-modules/user/user.entity'; import { User } from 'src/engine/core-modules/user/user.entity';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
export const userAutoResolverOpts: AutoResolverOpts< export const userAutoResolverOpts: AutoResolverOpts<
any, any,
@@ -33,6 +33,6 @@ export const userAutoResolverOpts: AutoResolverOpts<
one: { disabled: true }, one: { disabled: true },
}, },
delete: { many: { disabled: true }, one: { disabled: true } }, delete: { many: { disabled: true }, one: { disabled: true } },
guards: [JwtAuthGuard], guards: [WorkspaceAuthGuard],
}, },
]; ];
@@ -16,9 +16,10 @@ import { GraphQLJSONObject } from 'graphql-type-json';
import { FileUpload, GraphQLUpload } from 'graphql-upload'; import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface'; import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum'; import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
@@ -31,8 +32,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard'; import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { streamToBuffer } from 'src/utils/stream-to-buffer'; import { streamToBuffer } from 'src/utils/stream-to-buffer';
const getHMACKey = (email?: string, key?: string | null) => { const getHMACKey = (email?: string, key?: string | null) => {
@@ -43,7 +43,7 @@ const getHMACKey = (email?: string, key?: string | null) => {
return hmac.update(email).digest('hex'); return hmac.update(email).digest('hex');
}; };
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard)
@Resolver(() => User) @Resolver(() => User)
export class UserResolver { export class UserResolver {
constructor( constructor(
@@ -7,11 +7,12 @@ import { WorkflowRunDTO } from 'src/engine/core-modules/workflow/dtos/workflow-r
import { WorkflowTriggerGraphqlApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter'; import { WorkflowTriggerGraphqlApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator'; import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service'; import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service';
@Resolver() @Resolver()
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@UseFilters(WorkflowTriggerGraphqlApiExceptionFilter) @UseFilters(WorkflowTriggerGraphqlApiExceptionFilter)
export class WorkflowTriggerResolver { export class WorkflowTriggerResolver {
constructor( constructor(
@@ -9,6 +9,8 @@ import { SendInviteLinkEmail } from 'twenty-emails';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
@@ -19,8 +21,6 @@ import {
Workspace, Workspace,
WorkspaceActivationStatus, WorkspaceActivationStatus,
} from 'src/engine/core-modules/workspace/workspace.entity'; } from 'src/engine/core-modules/workspace/workspace.entity';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
// eslint-disable-next-line @nx/workspace-inject-workspace-repository // eslint-disable-next-line @nx/workspace-inject-workspace-repository
@@ -48,7 +48,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
} }
const existingWorkspace = await this.workspaceRepository.findOneBy({ const existingWorkspace = await this.workspaceRepository.findOneBy({
id: user.defaultWorkspace.id, id: user.defaultWorkspaceId,
}); });
if (!existingWorkspace) { if (!existingWorkspace) {
@@ -69,21 +69,21 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
throw new Error('Worspace is not pending creation'); throw new Error('Worspace is not pending creation');
} }
await this.workspaceRepository.update(user.defaultWorkspace.id, { await this.workspaceRepository.update(user.defaultWorkspaceId, {
activationStatus: WorkspaceActivationStatus.ONGOING_CREATION, activationStatus: WorkspaceActivationStatus.ONGOING_CREATION,
}); });
await this.workspaceManagerService.init(user.defaultWorkspace.id); await this.workspaceManagerService.init(user.defaultWorkspaceId);
await this.userWorkspaceService.createWorkspaceMember( await this.userWorkspaceService.createWorkspaceMember(
user.defaultWorkspace.id, user.defaultWorkspaceId,
user, user,
); );
await this.workspaceRepository.update(user.defaultWorkspace.id, { await this.workspaceRepository.update(user.defaultWorkspaceId, {
displayName: data.displayName, displayName: data.displayName,
activationStatus: WorkspaceActivationStatus.ACTIVE, activationStatus: WorkspaceActivationStatus.ACTIVE,
}); });
return user.defaultWorkspace; return existingWorkspace;
} }
async softDeleteWorkspace(id: string) { async softDeleteWorkspace(id: string) {
@@ -4,8 +4,8 @@ import {
ReadResolverOpts, ReadResolverOpts,
} from '@ptc-org/nestjs-query-graphql'; } from '@ptc-org/nestjs-query-graphql';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { UpdateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/update-workspace-input'; import { UpdateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/update-workspace-input';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { Workspace } from './workspace.entity'; import { Workspace } from './workspace.entity';
@@ -36,6 +36,6 @@ export const workspaceAutoResolverOpts: AutoResolverOpts<
many: { disabled: true }, many: { disabled: true },
}, },
delete: { many: { disabled: true }, one: { disabled: true } }, delete: { many: { disabled: true }, one: { disabled: true } },
guards: [JwtAuthGuard], guards: [WorkspaceAuthGuard],
}, },
]; ];
@@ -25,7 +25,8 @@ import { UpdateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/upd
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard'; import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { assert } from 'src/utils/assert'; import { assert } from 'src/utils/assert';
import { streamToBuffer } from 'src/utils/stream-to-buffer'; import { streamToBuffer } from 'src/utils/stream-to-buffer';
@@ -33,7 +34,7 @@ import { Workspace } from './workspace.entity';
import { WorkspaceService } from './services/workspace.service'; import { WorkspaceService } from './services/workspace.service';
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard)
@Resolver(() => Workspace) @Resolver(() => Workspace)
export class WorkspaceResolver { export class WorkspaceResolver {
constructor( constructor(
@@ -54,7 +55,7 @@ export class WorkspaceResolver {
} }
@Mutation(() => Workspace) @Mutation(() => Workspace)
@UseGuards(JwtAuthGuard) @UseGuards(UserAuthGuard)
async activateWorkspace( async activateWorkspace(
@Args('data') data: ActivateWorkspaceInput, @Args('data') data: ActivateWorkspaceInput,
@AuthUser() user: User, @AuthUser() user: User,
@@ -139,6 +140,7 @@ export class WorkspaceResolver {
} }
@Mutation(() => SendInviteLink) @Mutation(() => SendInviteLink)
@UseGuards(UserAuthGuard)
async sendInviteLink( async sendInviteLink(
@Args() sendInviteLinkInput: SendInviteLinkInput, @Args() sendInviteLinkInput: SendInviteLinkInput,
@AuthUser() user: User, @AuthUser() user: User,
@@ -1,7 +1,11 @@
import DataLoader from 'dataloader'; import DataLoader from 'dataloader';
import { RelationMetadataLoaderPayload } from 'src/engine/dataloaders/dataloader.service';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
export interface IDataloaders { export interface IDataloaders {
relationMetadataLoader: DataLoader<string, RelationMetadataEntity>; relationMetadataLoader: DataLoader<
RelationMetadataLoaderPayload,
RelationMetadataEntity
>;
} }
@@ -2,10 +2,20 @@ import { Injectable } from '@nestjs/common';
import DataLoader from 'dataloader'; import DataLoader from 'dataloader';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { IDataloaders } from 'src/engine/dataloaders/dataloader.interface'; import { IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { RelationMetadataService } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.service'; import { RelationMetadataService } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.service';
export type RelationMetadataLoaderPayload = {
workspaceId: string;
fieldMetadata: Pick<
FieldMetadataInterface,
'type' | 'id' | 'objectMetadataId'
>;
};
@Injectable() @Injectable()
export class DataloaderService { export class DataloaderService {
constructor( constructor(
@@ -14,12 +24,18 @@ export class DataloaderService {
createLoaders(): IDataloaders { createLoaders(): IDataloaders {
const relationMetadataLoader = new DataLoader< const relationMetadataLoader = new DataLoader<
string, RelationMetadataLoaderPayload,
RelationMetadataEntity RelationMetadataEntity
>(async (fieldMetadataIds: string[]) => { >(async (dataLoaderParams: RelationMetadataLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const fieldMetadataItems = dataLoaderParams.map(
(dataLoaderParam) => dataLoaderParam.fieldMetadata,
);
const relationsMetadataCollection = const relationsMetadataCollection =
await this.relationMetadataService.findManyRelationMetadataByFieldMetadataIds( await this.relationMetadataService.findManyRelationMetadataByFieldMetadataIds(
fieldMetadataIds, fieldMetadataItems,
workspaceId,
); );
return relationsMetadataCollection; return relationsMetadataCollection;
@@ -14,10 +14,10 @@ export const AuthUser = createParamDecorator(
(options: DecoratorOptions | undefined, ctx: ExecutionContext) => { (options: DecoratorOptions | undefined, ctx: ExecutionContext) => {
const request = getRequest(ctx); const request = getRequest(ctx);
if (!options?.allowUndefined && (!request.user || !request.user.user)) { if (!options?.allowUndefined && !request.user) {
throw new ForbiddenException("You're not authorized to do this"); throw new ForbiddenException("You're not authorized to do this");
} }
return request.user ? request.user.user : undefined; return request.user;
}, },
); );
@@ -6,6 +6,6 @@ export const AuthWorkspace = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => { (data: unknown, ctx: ExecutionContext) => {
const request = getRequest(ctx); const request = getRequest(ctx);
return request.user ? request.user.workspace : undefined; return request.workspace;
}, },
); );
@@ -1,27 +1,27 @@
import { import {
Injectable, CanActivate,
ExecutionContext, ExecutionContext,
Injectable,
UnauthorizedException, UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport'; import { GqlExecutionContext } from '@nestjs/graphql';
import { Observable } from 'rxjs';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { getRequest } from 'src/utils/extract-request';
@Injectable() @Injectable()
export class DemoEnvGuard extends AuthGuard(['jwt']) { export class DemoEnvGuard implements CanActivate {
constructor(private readonly environmentService: EnvironmentService) { constructor(private readonly environmentService: EnvironmentService) {}
super();
}
getRequest(context: ExecutionContext) { canActivate(
return getRequest(context); context: ExecutionContext,
} ): boolean | Promise<boolean> | Observable<boolean> {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext().req;
// TODO: input should be typed
handleRequest(err: any, user: any) {
const demoWorkspaceIds = this.environmentService.get('DEMO_WORKSPACE_IDS'); const demoWorkspaceIds = this.environmentService.get('DEMO_WORKSPACE_IDS');
const currentUserWorkspaceId = user?.workspace?.id; const currentUserWorkspaceId = request.workspace?.id;
if (!currentUserWorkspaceId) { if (!currentUserWorkspaceId) {
throw new UnauthorizedException('Unauthorized for not logged in user'); throw new UnauthorizedException('Unauthorized for not logged in user');
@@ -31,6 +31,6 @@ export class DemoEnvGuard extends AuthGuard(['jwt']) {
throw new UnauthorizedException('Unauthorized for demo workspace'); throw new UnauthorizedException('Unauthorized for demo workspace');
} }
return user; return true;
} }
} }
@@ -0,0 +1,35 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private readonly tokenService: TokenService,
private readonly workspaceStorageCacheService: WorkspaceCacheStorageService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
try {
const data = await this.tokenService.validateToken(request);
const metadataVersion =
await this.workspaceStorageCacheService.getMetadataVersion(
data.workspace.id,
);
request.user = data.user;
request.apiKey = data.apiKey;
request.workspace = data.workspace;
request.workspaceId = data.workspace.id;
request.workspaceMetadataVersion = metadataVersion;
request.workspaceMemberId = data.workspaceMemberId;
return true;
} catch (error) {
return false;
}
}
}
@@ -1,40 +0,0 @@
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { JsonWebTokenError } from 'jsonwebtoken';
import { assert } from 'src/utils/assert';
import { getRequest } from 'src/utils/extract-request';
@Injectable()
export class JwtAuthGuard extends AuthGuard(['jwt']) {
constructor() {
super();
}
getRequest(context: ExecutionContext) {
return getRequest(context);
}
handleRequest(err: any, user: any, info: any) {
assert(user, '', UnauthorizedException);
if (err) {
throw err;
}
if (info && info instanceof Error) {
if (info instanceof JsonWebTokenError) {
info = String(info);
}
throw new UnauthorizedException(info);
}
return user;
}
}
@@ -1,23 +0,0 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { getRequest } from 'src/utils/extract-request';
@Injectable()
export class OptionalJwtAuthGuard extends AuthGuard(['jwt']) {
constructor() {
super();
}
getRequest(context: ExecutionContext) {
const request = getRequest(context);
return request;
}
handleRequest(err, user, info) {
if (err || info) return null;
return user;
}
}
@@ -0,0 +1,15 @@
import { CanActivate, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Observable } from 'rxjs';
export class UserAuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext().req;
return request.user !== undefined;
}
}
@@ -0,0 +1,15 @@
import { CanActivate, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Observable } from 'rxjs';
export class WorkspaceAuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext().req;
return request.workspace !== undefined;
}
}
@@ -44,7 +44,7 @@ registerEnumType(FieldMetadataType, {
@ObjectType('field') @ObjectType('field')
@Authorize({ @Authorize({
authorize: (context: any) => ({ authorize: (context: any) => ({
workspaceId: { eq: context?.req?.user?.workspace?.id }, workspaceId: { eq: context?.req?.workspace?.id },
}), }),
}) })
@QueryOptions({ @QueryOptions({
@@ -132,6 +132,8 @@ export class FieldMetadataDTO<
@HideField() @HideField()
workspaceId: string; workspaceId: string;
objectMetadataId: string;
@IsDateString() @IsDateString()
@Field() @Field()
createdAt: Date; createdAt: Date;
@@ -9,13 +9,14 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { ActorModule } from 'src/engine/core-modules/actor/actor.module'; import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
import { FieldMetadataResolver } from 'src/engine/metadata-modules/field-metadata/field-metadata.resolver'; import { FieldMetadataResolver } from 'src/engine/metadata-modules/field-metadata/field-metadata.resolver';
import { FieldMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/field-metadata/interceptors/field-metadata-graphql-api-exception.interceptor'; import { FieldMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/field-metadata/interceptors/field-metadata-graphql-api-exception.interceptor';
import { IsFieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator'; import { IsFieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator';
import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator'; import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
@@ -32,7 +33,10 @@ import { UpdateFieldInput } from './dtos/update-field.input';
imports: [ imports: [
NestjsQueryGraphQLModule.forFeature({ NestjsQueryGraphQLModule.forFeature({
imports: [ imports: [
NestjsQueryTypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'), NestjsQueryTypeOrmModule.forFeature(
[FieldMetadataEntity, ObjectMetadataEntity],
'metadata',
),
WorkspaceMigrationModule, WorkspaceMigrationModule,
WorkspaceStatusModule, WorkspaceStatusModule,
WorkspaceMigrationRunnerModule, WorkspaceMigrationRunnerModule,
@@ -65,7 +69,7 @@ import { UpdateFieldInput } from './dtos/update-field.input';
many: { disabled: true }, many: { disabled: true },
}, },
delete: { disabled: true }, delete: { disabled: true },
guards: [JwtAuthGuard], guards: [WorkspaceAuthGuard],
interceptors: [FieldMetadataGraphqlApiExceptionInterceptor], interceptors: [FieldMetadataGraphqlApiExceptionInterceptor],
}, },
], ],
@@ -15,7 +15,7 @@ import {
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { IDataloaders } from 'src/engine/dataloaders/dataloader.interface'; import { IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { CreateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; import { CreateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input'; import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
@@ -25,7 +25,7 @@ import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/fi
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util'; import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util';
@UseGuards(JwtAuthGuard) @UseGuards(WorkspaceAuthGuard)
@Resolver(() => FieldMetadataDTO) @Resolver(() => FieldMetadataDTO)
export class FieldMetadataResolver { export class FieldMetadataResolver {
constructor(private readonly fieldMetadataService: FieldMetadataService) {} constructor(private readonly fieldMetadataService: FieldMetadataService) {}
@@ -103,6 +103,7 @@ export class FieldMetadataResolver {
@ResolveField(() => RelationDefinitionDTO, { nullable: true }) @ResolveField(() => RelationDefinitionDTO, { nullable: true })
async relationDefinition( async relationDefinition(
@AuthWorkspace() workspace: Workspace,
@Parent() fieldMetadata: FieldMetadataDTO, @Parent() fieldMetadata: FieldMetadataDTO,
@Context() context: { loaders: IDataloaders }, @Context() context: { loaders: IDataloaders },
): Promise<RelationDefinitionDTO | null | undefined> { ): Promise<RelationDefinitionDTO | null | undefined> {
@@ -112,7 +113,10 @@ export class FieldMetadataResolver {
try { try {
const relationMetadataItem = const relationMetadataItem =
await context.loaders.relationMetadataLoader.load(fieldMetadata.id); await context.loaders.relationMetadataLoader.load({
fieldMetadata,
workspaceId: workspace.id,
});
return await this.fieldMetadataService.getRelationDefinitionFromRelationMetadata( return await this.fieldMetadataService.getRelationDefinitionFromRelationMetadata(
fieldMetadata, fieldMetadata,
@@ -4,7 +4,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import isEmpty from 'lodash.isempty'; import isEmpty from 'lodash.isempty';
import { DataSource, FindOneOptions, Repository } from 'typeorm'; import { DataSource, FindOneOptions, Repository } from 'typeorm';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4, v4 } from 'uuid';
import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
@@ -72,6 +72,8 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
private readonly metadataDataSource: DataSource, private readonly metadataDataSource: DataSource,
@InjectRepository(FieldMetadataEntity, 'metadata') @InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>, private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly objectMetadataService: ObjectMetadataService, private readonly objectMetadataService: ObjectMetadataService,
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
private readonly workspaceMigrationService: WorkspaceMigrationService, private readonly workspaceMigrationService: WorkspaceMigrationService,
@@ -87,6 +89,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
override async createOne( override async createOne(
fieldMetadataInput: CreateFieldInput, fieldMetadataInput: CreateFieldInput,
): Promise<FieldMetadataEntity> { ): Promise<FieldMetadataEntity> {
console.time('createOne');
const queryRunner = this.metadataDataSource.createQueryRunner(); const queryRunner = this.metadataDataSource.createQueryRunner();
await queryRunner.connect(); await queryRunner.connect();
@@ -97,20 +100,23 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
queryRunner.manager.getRepository<FieldMetadataEntity>( queryRunner.manager.getRepository<FieldMetadataEntity>(
FieldMetadataEntity, FieldMetadataEntity,
); );
const objectMetadata =
await this.objectMetadataService.findOneWithinWorkspace( console.time('createOne query');
fieldMetadataInput.workspaceId, const [objectMetadata] = await this.objectMetadataRepository.find({
{ where: {
where: { id: fieldMetadataInput.objectMetadataId,
id: fieldMetadataInput.objectMetadataId, workspaceId: fieldMetadataInput.workspaceId,
}, },
}, relations: ['fields'],
); order: {},
});
console.timeEnd('createOne query');
if (!objectMetadata) { if (!objectMetadata) {
throw new FieldMetadataException( throw new FieldMetadataException(
'Object metadata does not exist', 'Object metadata does not exist',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT, FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
); );
} }
@@ -155,22 +161,11 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
objectMetadata, objectMetadata,
); );
const fieldAlreadyExists = await fieldMetadataRepository.findOne({ console.time('createOne save');
where: {
name: fieldMetadataInput.name,
objectMetadataId: fieldMetadataInput.objectMetadataId,
workspaceId: fieldMetadataInput.workspaceId,
},
});
if (fieldAlreadyExists) {
throw new FieldMetadataException(
'Field already exists',
FieldMetadataExceptionCode.FIELD_ALREADY_EXISTS,
);
}
const createdFieldMetadata = await fieldMetadataRepository.save({ const createdFieldMetadata = await fieldMetadataRepository.save({
id: v4(),
createdAt: new Date(),
updatedAt: new Date(),
...fieldMetadataInput, ...fieldMetadataInput,
isNullable: generateNullable( isNullable: generateNullable(
fieldMetadataInput.type, fieldMetadataInput.type,
@@ -190,7 +185,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
isCustom: true, isCustom: true,
}); });
console.timeEnd('createOne save');
if (!fieldMetadataInput.isRemoteCreation) { if (!fieldMetadataInput.isRemoteCreation) {
console.time('createOne migration create');
await this.workspaceMigrationService.createCustomMigration( await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`create-${createdFieldMetadata.name}`), generateMigrationName(`create-${createdFieldMetadata.name}`),
fieldMetadataInput.workspaceId, fieldMetadataInput.workspaceId,
@@ -206,11 +204,16 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
], ],
); );
console.timeEnd('createOne migration create');
console.time('createOne migration run');
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
fieldMetadataInput.workspaceId, fieldMetadataInput.workspaceId,
); );
console.timeEnd('createOne migration run');
} }
console.time('createOne workspace viewField');
// TODO: Move viewField creation to a cdc scheduler // TODO: Move viewField creation to a cdc scheduler
const dataSourceMetadata = const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
@@ -270,8 +273,11 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
); );
} }
} }
console.timeEnd('createOne workspace viewField');
console.time('createOne internal commit');
await workspaceQueryRunner.commitTransaction(); await workspaceQueryRunner.commitTransaction();
console.timeEnd('createOne internal commit');
} catch (error) { } catch (error) {
await workspaceQueryRunner.rollbackTransaction(); await workspaceQueryRunner.rollbackTransaction();
throw error; throw error;
@@ -279,7 +285,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
await workspaceQueryRunner.release(); await workspaceQueryRunner.release();
} }
console.time('createOne commit');
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
console.timeEnd('createOne commit');
return createdFieldMetadata; return createdFieldMetadata;
} catch (error) { } catch (error) {
@@ -287,9 +295,12 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
throw error; throw error;
} finally { } finally {
await queryRunner.release(); await queryRunner.release();
console.time('createOne increment');
await this.workspaceMetadataVersionService.incrementMetadataVersion( await this.workspaceMetadataVersionService.incrementMetadataVersion(
fieldMetadataInput.workspaceId, fieldMetadataInput.workspaceId,
); );
console.timeEnd('createOne increment');
console.timeEnd('createOne');
} }
} }
@@ -308,7 +319,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
FieldMetadataEntity, FieldMetadataEntity,
); );
const existingFieldMetadata = await fieldMetadataRepository.findOne({ const [existingFieldMetadata] = await fieldMetadataRepository.find({
where: { where: {
id, id,
workspaceId: fieldMetadataInput.workspaceId, workspaceId: fieldMetadataInput.workspaceId,
@@ -322,15 +333,14 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
); );
} }
const objectMetadata = const [objectMetadata] = await this.objectMetadataRepository.find({
await this.objectMetadataService.findOneWithinWorkspace( where: {
fieldMetadataInput.workspaceId, id: existingFieldMetadata.objectMetadataId,
{ workspaceId: fieldMetadataInput.workspaceId,
where: { },
id: existingFieldMetadata?.objectMetadataId, relations: ['fields'],
}, order: {},
}, });
);
if (!objectMetadata) { if (!objectMetadata) {
throw new FieldMetadataException( throw new FieldMetadataException(
@@ -475,7 +485,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
FieldMetadataEntity, FieldMetadataEntity,
); );
const fieldMetadata = await fieldMetadataRepository.findOne({ const [fieldMetadata] = await fieldMetadataRepository.find({
where: { where: {
id: input.id, id: input.id,
workspaceId: workspaceId, workspaceId: workspaceId,
@@ -489,12 +499,13 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
); );
} }
const objectMetadata = const [objectMetadata] = await this.objectMetadataRepository.find({
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, { where: {
where: { id: fieldMetadata.objectMetadataId,
id: fieldMetadata?.objectMetadataId, },
}, relations: ['fields'],
}); order: {},
});
if (!objectMetadata) { if (!objectMetadata) {
throw new FieldMetadataException( throw new FieldMetadataException(
@@ -583,7 +594,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
id: string, id: string,
options?: FindOneOptions<FieldMetadataEntity>, options?: FindOneOptions<FieldMetadataEntity>,
) { ) {
const fieldMetadata = await this.fieldMetadataRepository.findOne({ const [fieldMetadata] = await this.fieldMetadataRepository.find({
...options, ...options,
where: { where: {
...options?.where, ...options?.where,
@@ -605,13 +616,15 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
workspaceId: string, workspaceId: string,
options: FindOneOptions<FieldMetadataEntity>, options: FindOneOptions<FieldMetadataEntity>,
) { ) {
return this.fieldMetadataRepository.findOne({ const [fieldMetadata] = await this.fieldMetadataRepository.find({
...options, ...options,
where: { where: {
...options.where, ...options.where,
workspaceId, workspaceId,
}, },
}); });
return fieldMetadata;
} }
private buildUpdatableStandardFieldInput( private buildUpdatableStandardFieldInput(

Some files were not shown because too many files have changed in this diff Show More