Optimize metadata queries (#7013)

In this PR:

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
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 { 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 { 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 { renderApolloPlayground } from 'src/engine/utils/render-apollo-playground.util';
@ -69,13 +69,18 @@ export class GraphQLConfigService
let workspace: Workspace | undefined;
try {
if (!this.tokenService.isTokenPresent(context.req)) {
const { user, workspace, apiKey, workspaceMemberId } = context.req;
if (!workspace) {
return new GraphQLSchema({});
}
const data = await this.tokenService.validateToken(context.req);
return await this.createSchema(context, data);
return await this.createSchema(context, {
user,
workspace,
apiKey,
workspaceMemberId,
});
} catch (error) {
if (error instanceof UnauthorizedException) {
throw new GraphQLError('Unauthenticated', {

View File

@ -2,7 +2,7 @@ import { FindOptionsWhere, ObjectLiteral } from 'typeorm';
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';

View File

@ -3,9 +3,9 @@ import { FindOptionsWhere, Not, ObjectLiteral } from 'typeorm';
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 { 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 { 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 { capitalize } from 'src/utils/capitalize';
import { isPlainObject } from 'src/utils/is-plain-object';

View File

@ -1,8 +1,8 @@
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 { 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 { FieldMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
describe('GraphqlQueryOrderFieldParser', () => {
let parser: GraphqlQueryOrderFieldParser;

View File

@ -10,12 +10,11 @@ import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} 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 { 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 { capitalize } from 'src/utils/capitalize';
export class GraphqlQueryOrderFieldParser {
private fieldMetadataMap: FieldMetadataMap;

View File

@ -1,8 +1,8 @@
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 { 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 { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util';
export class GraphqlQuerySelectedFieldsRelationParser {
private objectMetadataMap: ObjectMetadataMap;

View File

@ -5,9 +5,9 @@ import {
GraphqlQueryRunnerExceptionCode,
} 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 { 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 { 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 { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { capitalize } from 'src/utils/capitalize';

View File

@ -17,7 +17,7 @@ import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql
import {
FieldMetadataMap,
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 {
private fieldMetadataMap: FieldMetadataMap;

View File

@ -9,12 +9,12 @@ import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} 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 { 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 { 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 { 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 { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { isPlainObject } from 'src/utils/is-plain-object';

View File

@ -19,11 +19,9 @@ import {
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 { 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 { 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';
export class GraphqlQueryFindManyResolverService {
@ -51,10 +49,10 @@ export class GraphqlQueryFindManyResolverService {
authContext.workspace.id,
objectMetadataItem.nameSingular,
);
const objectMetadataMap = convertObjectMetadataToMap(
const objectMetadataMap = generateObjectMetadataMap(
objectMetadataCollection,
);
const objectMetadata = getObjectMetadata(
const objectMetadata = getObjectMetadataOrThrow(
objectMetadataMap,
objectMetadataItem.nameSingular,
);

View File

@ -13,10 +13,8 @@ import {
} 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 { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper';
import {
convertObjectMetadataToMap,
getObjectMetadata,
} from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.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';
export class GraphqlQueryFindOneResolverService {
@ -40,10 +38,11 @@ export class GraphqlQueryFindOneResolverService {
authContext.workspace.id,
objectMetadataItem.nameSingular,
);
const objectMetadataMap = convertObjectMetadataToMap(
const objectMetadataMap = generateObjectMetadataMap(
objectMetadataCollection,
);
const objectMetadata = getObjectMetadata(
const objectMetadata = getObjectMetadataOrThrow(
objectMetadataMap,
objectMetadataItem.nameSingular,
);

View File

@ -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;
};

View File

@ -1,6 +1,6 @@
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 {
deduceRelationDirection,
RelationDirection,

View File

@ -4,11 +4,11 @@ import GraphQLJSON from 'graphql-type-json';
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 { 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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.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';
export const metadataModuleFactory = async (

View File

@ -56,13 +56,13 @@ export class WorkspaceSchemaFactory {
);
}
const objectMetadataCollection =
await this.workspaceCacheStorageService.getObjectMetadataCollection(
const objectMetadataMap =
await this.workspaceCacheStorageService.getObjectMetadataMap(
authContext.workspace.id,
currentCacheVersion,
);
if (!objectMetadataCollection) {
if (!objectMetadataMap) {
await this.workspaceMetadataCacheService.recomputeMetadataCache(
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
let typeDefs = await this.workspaceCacheStorageService.getGraphQLTypeDefs(
authContext.workspace.id,

View File

@ -4,10 +4,10 @@ import { Request, Response } from 'express';
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 { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/batch/*')
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard)
export class RestApiCoreBatchController {
constructor(private readonly restApiCoreService: RestApiCoreService) {}

View File

@ -14,10 +14,11 @@ import { Request, Response } from 'express';
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 { 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/*')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
export class RestApiCoreController {
constructor(private readonly restApiCoreService: RestApiCoreService) {}

View File

@ -1,23 +1,25 @@
import { Module } from '@nestjs/common';
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 { 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 { 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 { 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({
imports: [
CoreQueryBuilderModule,
MetadataQueryBuilderModule,
WorkspaceCacheStorageModule,
AuthModule,
HttpModule,
],

View File

@ -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 { AuthUser } from 'src/engine/decorators/auth/auth-user.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()
class GetAISQLQueryArgs {
@ -20,7 +21,7 @@ class GetAISQLQueryArgs {
text: string;
}
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@Resolver(() => AISQLQueryResult)
export class AISQLQueryResolver {
constructor(

View File

@ -1,21 +1,18 @@
import { Resolver, Mutation, Args, Context } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
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 { 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 { AnalyticsService } from './analytics.service';
import { CreateAnalyticsInput } from './dtos/create-analytics.input';
@UseGuards(OptionalJwtAuthGuard)
@Resolver(() => Analytics)
export class AnalyticsResolver {
constructor(

View File

@ -6,7 +6,7 @@ import {
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 { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
export const appTokenAutoResolverOpts: AutoResolverOpts<
any,
@ -34,6 +34,6 @@ export const appTokenAutoResolverOpts: AutoResolverOpts<
one: { disabled: true },
},
delete: { many: { disabled: true }, one: { disabled: true } },
guards: [JwtAuthGuard],
guards: [WorkspaceAuthGuard],
},
];

View File

@ -13,7 +13,7 @@ export class BeforeCreateOneAppToken<T extends AppToken>
instance: CreateOneInputType<T>,
context: any,
): Promise<CreateOneInputType<T>> {
const userId = context?.req?.user?.user?.id;
const userId = context?.req?.user?.id;
instance.input.userId = userId;
// FIXME: These fields should be autogenerated, we need to run a migration for this

View File

@ -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 { 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 { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard';
import { UserService } from 'src/engine/core-modules/user/services/user.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 { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { ChallengeInput } from './dto/challenge.input';
import { ImpersonateInput } from './dto/impersonate.input';
@ -111,7 +112,7 @@ export class AuthResolver {
}
@Mutation(() => TransientToken)
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
async generateTransientToken(
@AuthUser() user: User,
): Promise<TransientToken | void> {
@ -141,7 +142,7 @@ export class AuthResolver {
}
@Mutation(() => AuthorizeApp)
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
async authorizeApp(
@Args() authorizeAppInput: AuthorizeAppInput,
@AuthUser() user: User,
@ -155,7 +156,7 @@ export class AuthResolver {
}
@Mutation(() => AuthTokens)
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
async generateJWT(
@AuthUser() user: User,
@Args() args: GenerateJwtInput,
@ -177,7 +178,7 @@ export class AuthResolver {
return { tokens: tokens };
}
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@Mutation(() => Verify)
async impersonate(
@Args() impersonateInput: ImpersonateInput,
@ -186,7 +187,7 @@ export class AuthResolver {
return await this.authService.impersonate(impersonateInput.userId, user);
}
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@Mutation(() => ApiKeyToken)
async generateApiKeyToken(
@Args() args: ApiKeyTokenInput,

View File

@ -36,14 +36,14 @@ import {
JwtPayload,
} from 'src/engine/core-modules/auth/strategies/jwt.auth.strategy';
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 { User } from 'src/engine/core-modules/user/user.entity';
import {
Workspace,
WorkspaceActivationStatus,
} 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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';

View File

@ -11,9 +11,9 @@ import {
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
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 { 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 { 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) {
user = await this.userRepository.findOne({
where: { id: payload.sub },
relations: ['defaultWorkspace'],
});
if (!user) {
throw new AuthException(

View File

@ -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 { AuthUser } from 'src/engine/decorators/auth/auth-user.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()
export class BillingResolver {
@ -37,7 +38,7 @@ export class BillingResolver {
}
@Query(() => SessionEntity)
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
async billingPortalSession(
@AuthUser() user: User,
@Args() { returnUrlPath }: BillingSessionInput,
@ -51,7 +52,7 @@ export class BillingResolver {
}
@Mutation(() => SessionEntity)
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
async checkoutSession(
@AuthWorkspace() workspace: Workspace,
@AuthUser() user: User,
@ -79,7 +80,7 @@ export class BillingResolver {
}
@Mutation(() => UpdateBillingEntity)
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard)
async updateBillingSubscription(@AuthUser() user: User) {
await this.billingSubscriptionService.applyBillingSubscription(user);

View File

@ -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 { 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 { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@ArgsType()
class GetTimelineCalendarEventsFromPersonIdArgs {
@ -35,7 +35,7 @@ class GetTimelineCalendarEventsFromCompanyIdArgs {
pageSize: number;
}
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@Resolver(() => TimelineCalendarEventsWithTotal)
export class TimelineCalendarEventResolver {
constructor(

View File

@ -1,5 +1,6 @@
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 { FileUploadResolver } from './file-upload.resolver';
@ -15,6 +16,10 @@ describe('FileUploadResolver', () => {
provide: FileUploadService,
useValue: {},
},
{
provide: EnvironmentService,
useValue: {},
},
],
}).compile();

View File

@ -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 { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
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';
@UseGuards(JwtAuthGuard, DemoEnvGuard)
@UseGuards(WorkspaceAuthGuard, DemoEnvGuard)
@Resolver()
export class FileUploadResolver {
constructor(private readonly fileUploadService: FileUploadService) {}

View File

@ -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 { User } from 'src/engine/core-modules/user/user.entity';
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()
class GetTimelineThreadsFromPersonIdArgs {
@ -38,7 +39,7 @@ class GetTimelineThreadsFromCompanyIdArgs {
pageSize: number;
}
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@Resolver(() => TimelineThreadsWithTotal)
export class TimelineMessagingResolver {
constructor(

View File

@ -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 { AuthUser } from 'src/engine/decorators/auth/auth-user.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()
export class OnboardingResolver {
constructor(private readonly onboardingService: OnboardingService) {}

View File

@ -1,11 +1,11 @@
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 { PostgresCredentialsService } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
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)
export class PostgresCredentialsResolver {
@ -13,19 +13,19 @@ export class PostgresCredentialsResolver {
private readonly postgresCredentialsService: PostgresCredentialsService,
) {}
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@Mutation(() => PostgresCredentialsDTO)
async enablePostgresProxy(@AuthWorkspace() { id: workspaceId }: Workspace) {
return this.postgresCredentialsService.enablePostgresProxy(workspaceId);
}
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@Mutation(() => PostgresCredentialsDTO)
async disablePostgresProxy(@AuthWorkspace() { id: workspaceId }: Workspace) {
return this.postgresCredentialsService.disablePostgresProxy(workspaceId);
}
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@Query(() => PostgresCredentialsDTO, { nullable: true })
async getPostgresCredentials(
@AuthWorkspace() { id: workspaceId }: Workspace,

View File

@ -4,15 +4,15 @@ import { InjectRepository } from '@nestjs/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 { 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 { 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)
export class UserWorkspaceResolver {
constructor(

View File

@ -1,11 +1,11 @@
import {
AutoResolverOpts,
ReadResolverOpts,
PagingStrategies,
ReadResolverOpts,
} from '@ptc-org/nestjs-query-graphql';
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<
any,
@ -33,6 +33,6 @@ export const userAutoResolverOpts: AutoResolverOpts<
one: { disabled: true },
},
delete: { many: { disabled: true }, one: { disabled: true } },
guards: [JwtAuthGuard],
guards: [WorkspaceAuthGuard],
},
];

View File

@ -16,9 +16,10 @@ import { GraphQLJSONObject } from 'graphql-type-json';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
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 { 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 { FileService } from 'src/engine/core-modules/file/services/file.service';
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 { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
const getHMACKey = (email?: string, key?: string | null) => {
@ -43,7 +43,7 @@ const getHMACKey = (email?: string, key?: string | null) => {
return hmac.update(email).digest('hex');
};
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@Resolver(() => User)
export class UserResolver {
constructor(

View File

@ -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 { AuthUser } from 'src/engine/decorators/auth/auth-user.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';
@Resolver()
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@UseFilters(WorkflowTriggerGraphqlApiExceptionFilter)
export class WorkflowTriggerResolver {
constructor(

View File

@ -9,6 +9,8 @@ import { SendInviteLinkEmail } from 'twenty-emails';
import { Repository } from 'typeorm';
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 { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
@ -19,8 +21,6 @@ import {
Workspace,
WorkspaceActivationStatus,
} 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';
// 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({
id: user.defaultWorkspace.id,
id: user.defaultWorkspaceId,
});
if (!existingWorkspace) {
@ -69,21 +69,21 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
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,
});
await this.workspaceManagerService.init(user.defaultWorkspace.id);
await this.workspaceManagerService.init(user.defaultWorkspaceId);
await this.userWorkspaceService.createWorkspaceMember(
user.defaultWorkspace.id,
user.defaultWorkspaceId,
user,
);
await this.workspaceRepository.update(user.defaultWorkspace.id, {
await this.workspaceRepository.update(user.defaultWorkspaceId, {
displayName: data.displayName,
activationStatus: WorkspaceActivationStatus.ACTIVE,
});
return user.defaultWorkspace;
return existingWorkspace;
}
async softDeleteWorkspace(id: string) {

View File

@ -4,8 +4,8 @@ import {
ReadResolverOpts,
} 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 { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { Workspace } from './workspace.entity';
@ -36,6 +36,6 @@ export const workspaceAutoResolverOpts: AutoResolverOpts<
many: { disabled: true },
},
delete: { many: { disabled: true }, one: { disabled: true } },
guards: [JwtAuthGuard],
guards: [WorkspaceAuthGuard],
},
];

View File

@ -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 { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
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 { streamToBuffer } from 'src/utils/stream-to-buffer';
@ -33,7 +34,7 @@ import { Workspace } from './workspace.entity';
import { WorkspaceService } from './services/workspace.service';
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@Resolver(() => Workspace)
export class WorkspaceResolver {
constructor(
@ -54,7 +55,7 @@ export class WorkspaceResolver {
}
@Mutation(() => Workspace)
@UseGuards(JwtAuthGuard)
@UseGuards(UserAuthGuard)
async activateWorkspace(
@Args('data') data: ActivateWorkspaceInput,
@AuthUser() user: User,
@ -139,6 +140,7 @@ export class WorkspaceResolver {
}
@Mutation(() => SendInviteLink)
@UseGuards(UserAuthGuard)
async sendInviteLink(
@Args() sendInviteLinkInput: SendInviteLinkInput,
@AuthUser() user: User,

View File

@ -1,7 +1,11 @@
import DataLoader from 'dataloader';
import { RelationMetadataLoaderPayload } from 'src/engine/dataloaders/dataloader.service';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
export interface IDataloaders {
relationMetadataLoader: DataLoader<string, RelationMetadataEntity>;
relationMetadataLoader: DataLoader<
RelationMetadataLoaderPayload,
RelationMetadataEntity
>;
}

View File

@ -2,10 +2,20 @@ import { Injectable } from '@nestjs/common';
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 { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { RelationMetadataService } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.service';
export type RelationMetadataLoaderPayload = {
workspaceId: string;
fieldMetadata: Pick<
FieldMetadataInterface,
'type' | 'id' | 'objectMetadataId'
>;
};
@Injectable()
export class DataloaderService {
constructor(
@ -14,12 +24,18 @@ export class DataloaderService {
createLoaders(): IDataloaders {
const relationMetadataLoader = new DataLoader<
string,
RelationMetadataLoaderPayload,
RelationMetadataEntity
>(async (fieldMetadataIds: string[]) => {
>(async (dataLoaderParams: RelationMetadataLoaderPayload[]) => {
const workspaceId = dataLoaderParams[0].workspaceId;
const fieldMetadataItems = dataLoaderParams.map(
(dataLoaderParam) => dataLoaderParam.fieldMetadata,
);
const relationsMetadataCollection =
await this.relationMetadataService.findManyRelationMetadataByFieldMetadataIds(
fieldMetadataIds,
fieldMetadataItems,
workspaceId,
);
return relationsMetadataCollection;

View File

@ -14,10 +14,10 @@ export const AuthUser = createParamDecorator(
(options: DecoratorOptions | undefined, ctx: ExecutionContext) => {
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");
}
return request.user ? request.user.user : undefined;
return request.user;
},
);

View File

@ -6,6 +6,6 @@ export const AuthWorkspace = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = getRequest(ctx);
return request.user ? request.user.workspace : undefined;
return request.workspace;
},
);

View File

@ -1,27 +1,27 @@
import {
Injectable,
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} 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 { getRequest } from 'src/utils/extract-request';
@Injectable()
export class DemoEnvGuard extends AuthGuard(['jwt']) {
constructor(private readonly environmentService: EnvironmentService) {
super();
}
export class DemoEnvGuard implements CanActivate {
constructor(private readonly environmentService: EnvironmentService) {}
getRequest(context: ExecutionContext) {
return getRequest(context);
}
canActivate(
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 currentUserWorkspaceId = user?.workspace?.id;
const currentUserWorkspaceId = request.workspace?.id;
if (!currentUserWorkspaceId) {
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');
}
return user;
return true;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -44,7 +44,7 @@ registerEnumType(FieldMetadataType, {
@ObjectType('field')
@Authorize({
authorize: (context: any) => ({
workspaceId: { eq: context?.req?.user?.workspace?.id },
workspaceId: { eq: context?.req?.workspace?.id },
}),
})
@QueryOptions({
@ -132,6 +132,8 @@ export class FieldMetadataDTO<
@HideField()
workspaceId: string;
objectMetadataId: string;
@IsDateString()
@Field()
createdAt: Date;

View File

@ -9,13 +9,14 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.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 { 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 { 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 { 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 { 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';
@ -32,7 +33,10 @@ import { UpdateFieldInput } from './dtos/update-field.input';
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [
NestjsQueryTypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'),
NestjsQueryTypeOrmModule.forFeature(
[FieldMetadataEntity, ObjectMetadataEntity],
'metadata',
),
WorkspaceMigrationModule,
WorkspaceStatusModule,
WorkspaceMigrationRunnerModule,
@ -65,7 +69,7 @@ import { UpdateFieldInput } from './dtos/update-field.input';
many: { disabled: true },
},
delete: { disabled: true },
guards: [JwtAuthGuard],
guards: [WorkspaceAuthGuard],
interceptors: [FieldMetadataGraphqlApiExceptionInterceptor],
},
],

View File

@ -15,7 +15,7 @@ import {
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
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 { 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';
@ -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 { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util';
@UseGuards(JwtAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@Resolver(() => FieldMetadataDTO)
export class FieldMetadataResolver {
constructor(private readonly fieldMetadataService: FieldMetadataService) {}
@ -103,6 +103,7 @@ export class FieldMetadataResolver {
@ResolveField(() => RelationDefinitionDTO, { nullable: true })
async relationDefinition(
@AuthWorkspace() workspace: Workspace,
@Parent() fieldMetadata: FieldMetadataDTO,
@Context() context: { loaders: IDataloaders },
): Promise<RelationDefinitionDTO | null | undefined> {
@ -112,7 +113,10 @@ export class FieldMetadataResolver {
try {
const relationMetadataItem =
await context.loaders.relationMetadataLoader.load(fieldMetadata.id);
await context.loaders.relationMetadataLoader.load({
fieldMetadata,
workspaceId: workspace.id,
});
return await this.fieldMetadataService.getRelationDefinitionFromRelationMetadata(
fieldMetadata,

View File

@ -4,7 +4,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import isEmpty from 'lodash.isempty';
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 { 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,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly objectMetadataService: ObjectMetadataService,
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
private readonly workspaceMigrationService: WorkspaceMigrationService,
@ -87,6 +89,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
override async createOne(
fieldMetadataInput: CreateFieldInput,
): Promise<FieldMetadataEntity> {
console.time('createOne');
const queryRunner = this.metadataDataSource.createQueryRunner();
await queryRunner.connect();
@ -97,20 +100,23 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
queryRunner.manager.getRepository<FieldMetadataEntity>(
FieldMetadataEntity,
);
const objectMetadata =
await this.objectMetadataService.findOneWithinWorkspace(
fieldMetadataInput.workspaceId,
{
where: {
id: fieldMetadataInput.objectMetadataId,
},
},
);
console.time('createOne query');
const [objectMetadata] = await this.objectMetadataRepository.find({
where: {
id: fieldMetadataInput.objectMetadataId,
workspaceId: fieldMetadataInput.workspaceId,
},
relations: ['fields'],
order: {},
});
console.timeEnd('createOne query');
if (!objectMetadata) {
throw new FieldMetadataException(
'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,
);
const fieldAlreadyExists = await fieldMetadataRepository.findOne({
where: {
name: fieldMetadataInput.name,
objectMetadataId: fieldMetadataInput.objectMetadataId,
workspaceId: fieldMetadataInput.workspaceId,
},
});
if (fieldAlreadyExists) {
throw new FieldMetadataException(
'Field already exists',
FieldMetadataExceptionCode.FIELD_ALREADY_EXISTS,
);
}
console.time('createOne save');
const createdFieldMetadata = await fieldMetadataRepository.save({
id: v4(),
createdAt: new Date(),
updatedAt: new Date(),
...fieldMetadataInput,
isNullable: generateNullable(
fieldMetadataInput.type,
@ -190,7 +185,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
isCustom: true,
});
console.timeEnd('createOne save');
if (!fieldMetadataInput.isRemoteCreation) {
console.time('createOne migration create');
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`create-${createdFieldMetadata.name}`),
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(
fieldMetadataInput.workspaceId,
);
console.timeEnd('createOne migration run');
}
console.time('createOne workspace viewField');
// TODO: Move viewField creation to a cdc scheduler
const dataSourceMetadata =
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();
console.timeEnd('createOne internal commit');
} catch (error) {
await workspaceQueryRunner.rollbackTransaction();
throw error;
@ -279,7 +285,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
await workspaceQueryRunner.release();
}
console.time('createOne commit');
await queryRunner.commitTransaction();
console.timeEnd('createOne commit');
return createdFieldMetadata;
} catch (error) {
@ -287,9 +295,12 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
throw error;
} finally {
await queryRunner.release();
console.time('createOne increment');
await this.workspaceMetadataVersionService.incrementMetadataVersion(
fieldMetadataInput.workspaceId,
);
console.timeEnd('createOne increment');
console.timeEnd('createOne');
}
}
@ -308,7 +319,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
FieldMetadataEntity,
);
const existingFieldMetadata = await fieldMetadataRepository.findOne({
const [existingFieldMetadata] = await fieldMetadataRepository.find({
where: {
id,
workspaceId: fieldMetadataInput.workspaceId,
@ -322,15 +333,14 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
);
}
const objectMetadata =
await this.objectMetadataService.findOneWithinWorkspace(
fieldMetadataInput.workspaceId,
{
where: {
id: existingFieldMetadata?.objectMetadataId,
},
},
);
const [objectMetadata] = await this.objectMetadataRepository.find({
where: {
id: existingFieldMetadata.objectMetadataId,
workspaceId: fieldMetadataInput.workspaceId,
},
relations: ['fields'],
order: {},
});
if (!objectMetadata) {
throw new FieldMetadataException(
@ -475,7 +485,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
FieldMetadataEntity,
);
const fieldMetadata = await fieldMetadataRepository.findOne({
const [fieldMetadata] = await fieldMetadataRepository.find({
where: {
id: input.id,
workspaceId: workspaceId,
@ -489,12 +499,13 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
);
}
const objectMetadata =
await this.objectMetadataService.findOneWithinWorkspace(workspaceId, {
where: {
id: fieldMetadata?.objectMetadataId,
},
});
const [objectMetadata] = await this.objectMetadataRepository.find({
where: {
id: fieldMetadata.objectMetadataId,
},
relations: ['fields'],
order: {},
});
if (!objectMetadata) {
throw new FieldMetadataException(
@ -583,7 +594,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
id: string,
options?: FindOneOptions<FieldMetadataEntity>,
) {
const fieldMetadata = await this.fieldMetadataRepository.findOne({
const [fieldMetadata] = await this.fieldMetadataRepository.find({
...options,
where: {
...options?.where,
@ -605,13 +616,15 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
workspaceId: string,
options: FindOneOptions<FieldMetadataEntity>,
) {
return this.fieldMetadataRepository.findOne({
const [fieldMetadata] = await this.fieldMetadataRepository.find({
...options,
where: {
...options.where,
workspaceId,
},
});
return fieldMetadata;
}
private buildUpdatableStandardFieldInput(

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