From f1b3d1537a224e0700233d60a7120f849b0e0ba7 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Mon, 22 Jan 2024 16:00:16 +0100 Subject: [PATCH] Load views on user load and read in cache (#3552) * WIP * Poc * Use cached root query + remove proloaded views state * Fix storybook test + fix codegen * Return default schema if token is absent, unauthenticated if token is invalid * Use enum instead of bool --------- Co-authored-by: Thomas Trompette Co-authored-by: Charles Bochet --- packages/twenty-front/codegen.cjs | 2 + packages/twenty-front/src/App.tsx | 6 +- .../twenty-front/src/generated/graphql.tsx | 43 +------- .../src/hooks/useDefaultHomePagePath.tsx | 27 +++++ packages/twenty-front/src/index.tsx | 3 +- .../apollo/hooks/useCachedRootQuery.ts | 50 +++++++++ .../components/ObjectMetadataNavItems.tsx | 59 +++++++--- .../object-metadata/types/QueryMethodName.ts | 4 + .../hooks/useGenerateFindManyRecordsQuery.ts | 2 +- .../modules/users/components/UserProvider.tsx | 21 ++-- .../users/graphql/queries/getCurrentUser.ts | 9 -- .../graphql/queries/getCurrentUserAndViews.ts | 103 ++++++++++++++++++ .../src/modules/views/components/ViewBar.tsx | 2 +- .../__stories__/CreateProfile.stories.tsx | 19 ++-- .../auth/__stories__/PlanRequired.stories.tsx | 27 +++-- .../auth/__stories__/SignInUp.stories.tsx | 19 ++-- .../twenty-front/src/testing/graphqlMocks.ts | 16 ++- .../src/core/auth/services/token.service.ts | 6 + .../src/graphql-config.service.ts | 19 ++-- 19 files changed, 324 insertions(+), 113 deletions(-) create mode 100644 packages/twenty-front/src/hooks/useDefaultHomePagePath.tsx create mode 100644 packages/twenty-front/src/modules/apollo/hooks/useCachedRootQuery.ts create mode 100644 packages/twenty-front/src/modules/object-metadata/types/QueryMethodName.ts delete mode 100644 packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts create mode 100644 packages/twenty-front/src/modules/users/graphql/queries/getCurrentUserAndViews.ts diff --git a/packages/twenty-front/codegen.cjs b/packages/twenty-front/codegen.cjs index f318d5de0b..eeb1da9af2 100644 --- a/packages/twenty-front/codegen.cjs +++ b/packages/twenty-front/codegen.cjs @@ -6,6 +6,8 @@ module.exports = { './src/modules/**/*.tsx', './src/modules/**/*.ts', '!./src/**/*.test.tsx', + '!./src/**/__mocks__/*.ts', + '!./src/modules/users/graphql/queries/getCurrentUserAndViews.ts' ], overwrite: true, generates: { diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx index adcdc606c7..827e1751b5 100644 --- a/packages/twenty-front/src/App.tsx +++ b/packages/twenty-front/src/App.tsx @@ -8,6 +8,7 @@ import { DefaultLayout } from '@/ui/layout/page/DefaultLayout'; import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect'; import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect'; +import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath'; import { CreateProfile } from '~/pages/auth/CreateProfile'; import { CreateWorkspace } from '~/pages/auth/CreateWorkspace'; import { PlanRequired } from '~/pages/auth/PlanRequired'; @@ -39,7 +40,10 @@ import { getPageTitleFromPath } from '~/utils/title-utils'; export const App = () => { const { pathname } = useLocation(); + const { defaultHomePagePath } = useDefaultHomePagePath(); + const pageTitle = getPageTitleFromPath(pathname); + return ( <> @@ -54,7 +58,7 @@ export const App = () => { } /> } /> } /> - } /> + } /> } /> } /> diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index ed10d96ecb..9d21cdb899 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -436,7 +436,7 @@ export enum RelationMetadataType { export type Sentry = { __typename?: 'Sentry'; - dsn: Scalars['String']; + dsn?: Maybe; }; /** Sort Directions */ @@ -747,7 +747,7 @@ export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __ export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl: string }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn: string } } }; +export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl: string }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null } } }; export type UploadFileMutationVariables = Exact<{ file: Scalars['Upload']; @@ -779,11 +779,6 @@ export type UploadProfilePictureMutationVariables = Exact<{ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProfilePicture: string }; -export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; - - -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } } }; - export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>; @@ -1452,40 +1447,6 @@ export function useUploadProfilePictureMutation(baseOptions?: Apollo.MutationHoo export type UploadProfilePictureMutationHookResult = ReturnType; export type UploadProfilePictureMutationResult = Apollo.MutationResult; export type UploadProfilePictureMutationOptions = Apollo.BaseMutationOptions; -export const GetCurrentUserDocument = gql` - query GetCurrentUser { - currentUser { - ...UserQueryFragment - } -} - ${UserQueryFragmentFragmentDoc}`; - -/** - * __useGetCurrentUserQuery__ - * - * To run a query within a React component, call `useGetCurrentUserQuery` and pass it any options that fit your needs. - * When your component renders, `useGetCurrentUserQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useGetCurrentUserQuery({ - * variables: { - * }, - * }); - */ -export function useGetCurrentUserQuery(baseOptions?: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(GetCurrentUserDocument, options); - } -export function useGetCurrentUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(GetCurrentUserDocument, options); - } -export type GetCurrentUserQueryHookResult = ReturnType; -export type GetCurrentUserLazyQueryHookResult = ReturnType; -export type GetCurrentUserQueryResult = Apollo.QueryResult; export const DeleteCurrentWorkspaceDocument = gql` mutation DeleteCurrentWorkspace { deleteCurrentWorkspace { diff --git a/packages/twenty-front/src/hooks/useDefaultHomePagePath.tsx b/packages/twenty-front/src/hooks/useDefaultHomePagePath.tsx new file mode 100644 index 0000000000..6f0c25d4a7 --- /dev/null +++ b/packages/twenty-front/src/hooks/useDefaultHomePagePath.tsx @@ -0,0 +1,27 @@ +import { useCachedRootQuery } from '@/apollo/hooks/useCachedRootQuery'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { QueryMethodName } from '@/object-metadata/types/QueryMethodName'; + +export const useDefaultHomePagePath = () => { + const { objectMetadataItem: companyObjectMetadataItem } = + useObjectMetadataItem({ + objectNameSingular: CoreObjectNameSingular.Company, + }); + const { objectMetadataItem: viewObjectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: CoreObjectNameSingular.View, + }); + const { cachedRootQuery } = useCachedRootQuery({ + objectMetadataItem: viewObjectMetadataItem, + queryMethodName: QueryMethodName.FindMany, + }); + + const companyViewId = cachedRootQuery?.views?.edges?.find( + (view: any) => + view?.node?.objectMetadataId === companyObjectMetadataItem.id, + )?.node.id; + const defaultHomePagePath = + '/objects/companies' + (companyViewId ? `?view=${companyViewId}` : ''); + + return { defaultHomePagePath }; +}; diff --git a/packages/twenty-front/src/index.tsx b/packages/twenty-front/src/index.tsx index 9dfb43090d..3fe6f23ad7 100644 --- a/packages/twenty-front/src/index.tsx +++ b/packages/twenty-front/src/index.tsx @@ -20,11 +20,12 @@ import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/Sn import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; import { ThemeType } from '@/ui/theme/constants/theme'; import { UserProvider } from '@/users/components/UserProvider'; -import { App } from '~/App'; import { PageChangeEffect } from '~/effect-components/PageChangeEffect'; import '@emotion/react'; +import { App } from './App'; + import './index.css'; import 'react-loading-skeleton/dist/skeleton.css'; diff --git a/packages/twenty-front/src/modules/apollo/hooks/useCachedRootQuery.ts b/packages/twenty-front/src/modules/apollo/hooks/useCachedRootQuery.ts new file mode 100644 index 0000000000..f3d7b981a5 --- /dev/null +++ b/packages/twenty-front/src/modules/apollo/hooks/useCachedRootQuery.ts @@ -0,0 +1,50 @@ +import { useApolloClient } from '@apollo/client/react/hooks/useApolloClient'; +import gql from 'graphql-tag'; + +import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { QueryMethodName } from '@/object-metadata/types/QueryMethodName'; + +export const useCachedRootQuery = ({ + objectMetadataItem, + queryMethodName, +}: { + objectMetadataItem: ObjectMetadataItem | undefined; + queryMethodName: QueryMethodName; +}) => { + const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery(); + const apolloClient = useApolloClient(); + + if (!objectMetadataItem) { + return { cachedRootQuery: null }; + } + + const buildRecordFieldsFragment = () => { + return objectMetadataItem.fields + .filter((field) => field.type !== 'RELATION') + .map((field) => mapFieldMetadataToGraphQLQuery(field)) + .join(' \n'); + }; + + const cacheReadFragment = gql` + fragment RootQuery on Query { + ${ + QueryMethodName.FindMany === queryMethodName + ? objectMetadataItem.namePlural + : objectMetadataItem.nameSingular + } { + ${QueryMethodName.FindMany === queryMethodName ? 'edges { node { ' : ''} + ${buildRecordFieldsFragment()} + ${QueryMethodName.FindMany === queryMethodName ? '}}' : ''} + + } + } + `; + + const cachedRootQuery = apolloClient.readFragment({ + id: 'ROOT_QUERY', + fragment: cacheReadFragment, + }); + + return { cachedRootQuery }; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx index 4f5939c442..83194ee567 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx @@ -1,15 +1,38 @@ import { useLocation, useNavigate } from 'react-router-dom'; +import { useCachedRootQuery } from '@/apollo/hooks/useCachedRootQuery'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { QueryMethodName } from '@/object-metadata/types/QueryMethodName'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useIcons } from '@/ui/display/icon/hooks/useIcons'; import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; export const ObjectMetadataNavItems = () => { - const { activeObjectMetadataItems } = useObjectMetadataItemForSettings(); + const { activeObjectMetadataItems, findObjectMetadataItemByNamePlural } = + useObjectMetadataItemForSettings(); const navigate = useNavigate(); const { getIcon } = useIcons(); const currentPath = useLocation().pathname; + const viewObjectMetadataItem = findObjectMetadataItemByNamePlural('views'); + + const { cachedRootQuery } = useCachedRootQuery({ + objectMetadataItem: viewObjectMetadataItem, + queryMethodName: QueryMethodName.FindMany, + }); + + const { records } = useFindManyRecords({ + skip: cachedRootQuery?.views, + objectNameSingular: CoreObjectNameSingular.View, + useRecordsWithoutConnection: true, + }); + + const views = + records.length > 0 + ? records + : cachedRootQuery?.views?.edges?.map((edge: any) => edge?.node); + return ( <> {[ @@ -39,18 +62,28 @@ export const ObjectMetadataNavItems = () => { ? 1 : -1; }), - ].map((objectMetadataItem) => ( - { - navigate(`/objects/${objectMetadataItem.namePlural}`); - }} - /> - ))} + ].map((objectMetadataItem) => { + const viewId = views?.find( + (view: any) => view?.objectMetadataId === objectMetadataItem.id, + )?.id; + + const navigationPath = `/objects/${objectMetadataItem.namePlural}${ + viewId ? `?view=${viewId}` : '' + }`; + + return ( + { + navigate(navigationPath); + }} + /> + ); + })} ); }; diff --git a/packages/twenty-front/src/modules/object-metadata/types/QueryMethodName.ts b/packages/twenty-front/src/modules/object-metadata/types/QueryMethodName.ts new file mode 100644 index 0000000000..851dcffc08 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/types/QueryMethodName.ts @@ -0,0 +1,4 @@ +export enum QueryMethodName { + FindOne = 'findOne', + FindMany = 'findMany', +} diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts index bf9043023d..10f0aa06c2 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts @@ -20,7 +20,7 @@ export const useGenerateFindManyRecordsQuery = () => { objectMetadataItem.nameSingular, )}FilterInput, $orderBy: ${capitalize( objectMetadataItem.nameSingular, - )}OrderByInput, $lastCursor: String, $limit: Float = 30) { + )}OrderByInput, $lastCursor: String, $limit: Float = 60) { ${ objectMetadataItem.namePlural }(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){ diff --git a/packages/twenty-front/src/modules/users/components/UserProvider.tsx b/packages/twenty-front/src/modules/users/components/UserProvider.tsx index cb31acb048..fc39fe6e1b 100644 --- a/packages/twenty-front/src/modules/users/components/UserProvider.tsx +++ b/packages/twenty-front/src/modules/users/components/UserProvider.tsx @@ -1,11 +1,12 @@ import { useEffect, useState } from 'react'; +import { useQuery } from '@apollo/client'; import { useSetRecoilState } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { GET_CURRENT_USER_AND_VIEWS } from '@/users/graphql/queries/getCurrentUserAndViews'; import { ColorScheme } from '@/workspace-member/types/WorkspaceMember'; -import { useGetCurrentUserQuery } from '~/generated/graphql'; export const UserProvider = ({ children }: React.PropsWithChildren) => { const [isLoading, setIsLoading] = useState(true); @@ -16,16 +17,18 @@ export const UserProvider = ({ children }: React.PropsWithChildren) => { currentWorkspaceMemberState, ); - const { data: userData, loading: userLoading } = useGetCurrentUserQuery({}); + const { loading: queryLoading, data: queryData } = useQuery( + GET_CURRENT_USER_AND_VIEWS, + ); useEffect(() => { - if (!userLoading) { + if (!queryLoading) { setIsLoading(false); } - if (userData?.currentUser?.workspaceMember) { - setCurrentUser(userData.currentUser); - setCurrentWorkspace(userData.currentUser.defaultWorkspace); - const workspaceMember = userData.currentUser.workspaceMember; + if (queryData?.currentUser?.workspaceMember) { + setCurrentUser(queryData.currentUser); + setCurrentWorkspace(queryData.currentUser.defaultWorkspace); + const workspaceMember = queryData.currentUser.workspaceMember; setCurrentWorkspaceMember({ ...workspaceMember, colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'Light', @@ -34,10 +37,10 @@ export const UserProvider = ({ children }: React.PropsWithChildren) => { }, [ setCurrentUser, isLoading, - userLoading, + queryLoading, setCurrentWorkspace, setCurrentWorkspaceMember, - userData?.currentUser, + queryData?.currentUser, ]); return isLoading ? <> : <>{children}; diff --git a/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts b/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts deleted file mode 100644 index 8b1a6eac54..0000000000 --- a/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { gql } from '@apollo/client'; - -export const GET_CURRENT_USER = gql` - query GetCurrentUser { - currentUser { - ...UserQueryFragment - } - } -`; diff --git a/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUserAndViews.ts b/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUserAndViews.ts new file mode 100644 index 0000000000..9b0da4252d --- /dev/null +++ b/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUserAndViews.ts @@ -0,0 +1,103 @@ +// This query cannot be put in the graphQL folder because it cannot be generated by the graphQL codegen. +import { gql } from '@apollo/client'; + +export const GET_CURRENT_USER_AND_VIEWS = gql` + query GetCurrentUserAndViews { + currentUser { + id + firstName + lastName + email + canImpersonate + supportUserHash + workspaceMember { + id + name { + firstName + lastName + } + colorScheme + avatarUrl + locale + } + defaultWorkspace { + id + displayName + logo + domainName + inviteHash + allowImpersonation + subscriptionStatus + featureFlags { + id + key + value + workspaceId + } + } + } + views { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + edges { + cursor + node { + id + createdAt + updatedAt + name + objectMetadataId + type + deletedAt + viewFilters { + edges { + cursor + node { + id + createdAt + updatedAt + fieldMetadataId + operand + value + displayValue + deletedAt + } + } + } + viewSorts { + edges { + cursor + node { + id + createdAt + updatedAt + fieldMetadataId + direction + deletedAt + } + } + } + viewFields { + edges { + cursor + node { + id + createdAt + updatedAt + fieldMetadataId + isVisible + size + position + deletedAt + } + } + } + } + } + } + } +`; diff --git a/packages/twenty-front/src/modules/views/components/ViewBar.tsx b/packages/twenty-front/src/modules/views/components/ViewBar.tsx index f1797d0d6e..220c1af64e 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBar.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBar.tsx @@ -7,6 +7,7 @@ import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/c import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { TopBar } from '@/ui/layout/top-bar/TopBar'; import { FilterQueryParamsEffect } from '@/views/components/FilterQueryParamsEffect'; +import { ViewBarEffect } from '@/views/components/ViewBarEffect'; import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect'; import { ViewBarSortEffect } from '@/views/components/ViewBarSortEffect'; import { useViewBar } from '@/views/hooks/useViewBar'; @@ -19,7 +20,6 @@ import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope'; import { UpdateViewButtonGroup } from './UpdateViewButtonGroup'; import { ViewBarDetails } from './ViewBarDetails'; -import { ViewBarEffect } from './ViewBarEffect'; import { ViewsDropdownButton } from './ViewsDropdownButton'; export type ViewBarProps = { diff --git a/packages/twenty-front/src/pages/auth/__stories__/CreateProfile.stories.tsx b/packages/twenty-front/src/pages/auth/__stories__/CreateProfile.stories.tsx index 302883712f..953be52cde 100644 --- a/packages/twenty-front/src/pages/auth/__stories__/CreateProfile.stories.tsx +++ b/packages/twenty-front/src/pages/auth/__stories__/CreateProfile.stories.tsx @@ -4,7 +4,7 @@ import { within } from '@storybook/test'; import { graphql, HttpResponse } from 'msw'; import { AppPath } from '@/types/AppPath'; -import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; +import { GET_CURRENT_USER_AND_VIEWS } from '@/users/graphql/queries/getCurrentUserAndViews'; import { PageDecorator, PageDecoratorArgs, @@ -22,13 +22,16 @@ const meta: Meta = { parameters: { msw: { handlers: [ - graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { - return HttpResponse.json({ - data: { - currentUser: mockedOnboardingUsersData[0], - }, - }); - }), + graphql.query( + getOperationName(GET_CURRENT_USER_AND_VIEWS) ?? '', + () => { + return HttpResponse.json({ + data: { + currentUser: mockedOnboardingUsersData[0], + }, + }); + }, + ), graphqlMocks.handlers, ], }, diff --git a/packages/twenty-front/src/pages/auth/__stories__/PlanRequired.stories.tsx b/packages/twenty-front/src/pages/auth/__stories__/PlanRequired.stories.tsx index f325f824ca..7cfea3caef 100644 --- a/packages/twenty-front/src/pages/auth/__stories__/PlanRequired.stories.tsx +++ b/packages/twenty-front/src/pages/auth/__stories__/PlanRequired.stories.tsx @@ -4,7 +4,7 @@ import { within } from '@storybook/test'; import { graphql, HttpResponse } from 'msw'; import { AppPath } from '@/types/AppPath'; -import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; +import { GET_CURRENT_USER_AND_VIEWS } from '@/users/graphql/queries/getCurrentUserAndViews'; import { PageDecorator, PageDecoratorArgs, @@ -22,19 +22,22 @@ const meta: Meta = { parameters: { msw: { handlers: [ - graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { - return HttpResponse.json({ - data: { - currentUser: { - ...mockedOnboardingUsersData[0], - defaultWorkspace: { - ...mockedOnboardingUsersData[0].defaultWorkspace, - subscriptionStatus: 'incomplete', + graphql.query( + getOperationName(GET_CURRENT_USER_AND_VIEWS) ?? '', + () => { + return HttpResponse.json({ + data: { + currentUser: { + ...mockedOnboardingUsersData[0], + defaultWorkspace: { + ...mockedOnboardingUsersData[0].defaultWorkspace, + subscriptionStatus: 'incomplete', + }, }, }, - }, - }); - }), + }); + }, + ), graphqlMocks.handlers, ], }, diff --git a/packages/twenty-front/src/pages/auth/__stories__/SignInUp.stories.tsx b/packages/twenty-front/src/pages/auth/__stories__/SignInUp.stories.tsx index 2c40e0359c..f9ce7546b0 100644 --- a/packages/twenty-front/src/pages/auth/__stories__/SignInUp.stories.tsx +++ b/packages/twenty-front/src/pages/auth/__stories__/SignInUp.stories.tsx @@ -4,7 +4,7 @@ import { fireEvent, within } from '@storybook/test'; import { graphql, HttpResponse } from 'msw'; import { AppPath } from '@/types/AppPath'; -import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; +import { GET_CURRENT_USER_AND_VIEWS } from '@/users/graphql/queries/getCurrentUserAndViews'; import { PageDecorator, PageDecoratorArgs, @@ -22,13 +22,16 @@ const meta: Meta = { parameters: { msw: { handlers: [ - graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { - return HttpResponse.json({ - data: { - currentUser: mockedOnboardingUsersData[0], - }, - }); - }), + graphql.query( + getOperationName(GET_CURRENT_USER_AND_VIEWS) ?? '', + () => { + return HttpResponse.json({ + data: { + currentUser: mockedOnboardingUsersData[0], + }, + }); + }, + ), graphqlMocks.handlers, ], }, diff --git a/packages/twenty-front/src/testing/graphqlMocks.ts b/packages/twenty-front/src/testing/graphqlMocks.ts index 9520c35cae..0a6e92dfa0 100644 --- a/packages/twenty-front/src/testing/graphqlMocks.ts +++ b/packages/twenty-front/src/testing/graphqlMocks.ts @@ -4,7 +4,7 @@ import { graphql, HttpResponse } from 'msw'; import { CREATE_EVENT } from '@/analytics/graphql/queries/createEvent'; import { GET_CLIENT_CONFIG } from '@/client-config/graphql/queries/getClientConfig'; import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries'; -import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; +import { GET_CURRENT_USER_AND_VIEWS } from '@/users/graphql/queries/getCurrentUserAndViews'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { mockedActivities } from '~/testing/mock-data/activities'; import { mockedCompaniesData } from '~/testing/mock-data/companies'; @@ -22,10 +22,22 @@ const metadataGraphql = graphql.link(`${REACT_APP_SERVER_BASE_URL}/metadata`); export const graphqlMocks = { handlers: [ - graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { + graphql.query(getOperationName(GET_CURRENT_USER_AND_VIEWS) ?? '', () => { return HttpResponse.json({ data: { currentUser: mockedUsersData[0], + views: { + edges: mockedViewsData.map((view) => ({ + node: view, + cursor: null, + })), + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }, + }, }, }); }), diff --git a/packages/twenty-server/src/core/auth/services/token.service.ts b/packages/twenty-server/src/core/auth/services/token.service.ts index 438b5fd377..a95c8f9efc 100644 --- a/packages/twenty-server/src/core/auth/services/token.service.ts +++ b/packages/twenty-server/src/core/auth/services/token.service.ts @@ -172,6 +172,12 @@ export class TokenService { return { token }; } + isTokenPresent(request: Request): boolean { + const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request); + + return !!token; + } + async validateToken(request: Request): Promise { const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request); diff --git a/packages/twenty-server/src/graphql-config.service.ts b/packages/twenty-server/src/graphql-config.service.ts index fa18d65329..aa543c8e4c 100644 --- a/packages/twenty-server/src/graphql-config.service.ts +++ b/packages/twenty-server/src/graphql-config.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ContextIdFactory, ModuleRef } from '@nestjs/core'; import { GqlOptionsFactory } from '@nestjs/graphql'; @@ -56,17 +56,22 @@ export class GraphQLConfigService }, conditionalSchema: async (context) => { try { - let workspace: Workspace; - - // If token is not valid, it will return an empty schema - try { - workspace = await this.tokenService.validateToken(context.req); - } catch (err) { + if (!this.tokenService.isTokenPresent(context.req)) { return new GraphQLSchema({}); } + const workspace = await this.tokenService.validateToken(context.req); + return await this.createSchema(context, workspace); } catch (error) { + if (error instanceof UnauthorizedException) { + throw new GraphQLError('Unauthenticated', { + extensions: { + code: 'UNAUTHENTICATED', + }, + }); + } + if (error instanceof JsonWebTokenError) { //mockedUserJWT throw new GraphQLError('Unauthenticated', {