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 <thomast@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Thomas Trompette 2024-01-22 16:00:16 +01:00 committed by GitHub
parent 764374f6b8
commit f1b3d1537a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 324 additions and 113 deletions

View File

@ -6,6 +6,8 @@ module.exports = {
'./src/modules/**/*.tsx', './src/modules/**/*.tsx',
'./src/modules/**/*.ts', './src/modules/**/*.ts',
'!./src/**/*.test.tsx', '!./src/**/*.test.tsx',
'!./src/**/__mocks__/*.ts',
'!./src/modules/users/graphql/queries/getCurrentUserAndViews.ts'
], ],
overwrite: true, overwrite: true,
generates: { generates: {

View File

@ -8,6 +8,7 @@ import { DefaultLayout } from '@/ui/layout/page/DefaultLayout';
import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect'; import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect';
import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect'; import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect';
import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath';
import { CreateProfile } from '~/pages/auth/CreateProfile'; import { CreateProfile } from '~/pages/auth/CreateProfile';
import { CreateWorkspace } from '~/pages/auth/CreateWorkspace'; import { CreateWorkspace } from '~/pages/auth/CreateWorkspace';
import { PlanRequired } from '~/pages/auth/PlanRequired'; import { PlanRequired } from '~/pages/auth/PlanRequired';
@ -39,7 +40,10 @@ import { getPageTitleFromPath } from '~/utils/title-utils';
export const App = () => { export const App = () => {
const { pathname } = useLocation(); const { pathname } = useLocation();
const { defaultHomePagePath } = useDefaultHomePagePath();
const pageTitle = getPageTitleFromPath(pathname); const pageTitle = getPageTitleFromPath(pathname);
return ( return (
<> <>
<PageTitle title={pageTitle} /> <PageTitle title={pageTitle} />
@ -54,7 +58,7 @@ export const App = () => {
<Route path={AppPath.CreateWorkspace} element={<CreateWorkspace />} /> <Route path={AppPath.CreateWorkspace} element={<CreateWorkspace />} />
<Route path={AppPath.CreateProfile} element={<CreateProfile />} /> <Route path={AppPath.CreateProfile} element={<CreateProfile />} />
<Route path={AppPath.PlanRequired} element={<PlanRequired />} /> <Route path={AppPath.PlanRequired} element={<PlanRequired />} />
<Route path="/" element={<Navigate to="/objects/companies" />} /> <Route path="/" element={<Navigate to={defaultHomePagePath} />} />
<Route path={AppPath.TasksPage} element={<Tasks />} /> <Route path={AppPath.TasksPage} element={<Tasks />} />
<Route path={AppPath.Impersonate} element={<ImpersonateEffect />} /> <Route path={AppPath.Impersonate} element={<ImpersonateEffect />} />

View File

@ -436,7 +436,7 @@ export enum RelationMetadataType {
export type Sentry = { export type Sentry = {
__typename?: 'Sentry'; __typename?: 'Sentry';
dsn: Scalars['String']; dsn?: Maybe<Scalars['String']>;
}; };
/** Sort Directions */ /** Sort Directions */
@ -747,7 +747,7 @@ export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; 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<{ export type UploadFileMutationVariables = Exact<{
file: Scalars['Upload']; file: Scalars['Upload'];
@ -779,11 +779,6 @@ export type UploadProfilePictureMutationVariables = Exact<{
export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProfilePicture: string }; 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; }>; export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>;
@ -1452,40 +1447,6 @@ export function useUploadProfilePictureMutation(baseOptions?: Apollo.MutationHoo
export type UploadProfilePictureMutationHookResult = ReturnType<typeof useUploadProfilePictureMutation>; export type UploadProfilePictureMutationHookResult = ReturnType<typeof useUploadProfilePictureMutation>;
export type UploadProfilePictureMutationResult = Apollo.MutationResult<UploadProfilePictureMutation>; export type UploadProfilePictureMutationResult = Apollo.MutationResult<UploadProfilePictureMutation>;
export type UploadProfilePictureMutationOptions = Apollo.BaseMutationOptions<UploadProfilePictureMutation, UploadProfilePictureMutationVariables>; export type UploadProfilePictureMutationOptions = Apollo.BaseMutationOptions<UploadProfilePictureMutation, UploadProfilePictureMutationVariables>;
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<GetCurrentUserQuery, GetCurrentUserQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetCurrentUserQuery, GetCurrentUserQueryVariables>(GetCurrentUserDocument, options);
}
export function useGetCurrentUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetCurrentUserQuery, GetCurrentUserQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetCurrentUserQuery, GetCurrentUserQueryVariables>(GetCurrentUserDocument, options);
}
export type GetCurrentUserQueryHookResult = ReturnType<typeof useGetCurrentUserQuery>;
export type GetCurrentUserLazyQueryHookResult = ReturnType<typeof useGetCurrentUserLazyQuery>;
export type GetCurrentUserQueryResult = Apollo.QueryResult<GetCurrentUserQuery, GetCurrentUserQueryVariables>;
export const DeleteCurrentWorkspaceDocument = gql` export const DeleteCurrentWorkspaceDocument = gql`
mutation DeleteCurrentWorkspace { mutation DeleteCurrentWorkspace {
deleteCurrentWorkspace { deleteCurrentWorkspace {

View File

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

View File

@ -20,11 +20,12 @@ import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/Sn
import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
import { ThemeType } from '@/ui/theme/constants/theme'; import { ThemeType } from '@/ui/theme/constants/theme';
import { UserProvider } from '@/users/components/UserProvider'; import { UserProvider } from '@/users/components/UserProvider';
import { App } from '~/App';
import { PageChangeEffect } from '~/effect-components/PageChangeEffect'; import { PageChangeEffect } from '~/effect-components/PageChangeEffect';
import '@emotion/react'; import '@emotion/react';
import { App } from './App';
import './index.css'; import './index.css';
import 'react-loading-skeleton/dist/skeleton.css'; import 'react-loading-skeleton/dist/skeleton.css';

View File

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

View File

@ -1,15 +1,38 @@
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { useCachedRootQuery } from '@/apollo/hooks/useCachedRootQuery';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; 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 { useIcons } from '@/ui/display/icon/hooks/useIcons';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
export const ObjectMetadataNavItems = () => { export const ObjectMetadataNavItems = () => {
const { activeObjectMetadataItems } = useObjectMetadataItemForSettings(); const { activeObjectMetadataItems, findObjectMetadataItemByNamePlural } =
useObjectMetadataItemForSettings();
const navigate = useNavigate(); const navigate = useNavigate();
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const currentPath = useLocation().pathname; 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 ( return (
<> <>
{[ {[
@ -39,18 +62,28 @@ export const ObjectMetadataNavItems = () => {
? 1 ? 1
: -1; : -1;
}), }),
].map((objectMetadataItem) => ( ].map((objectMetadataItem) => {
const viewId = views?.find(
(view: any) => view?.objectMetadataId === objectMetadataItem.id,
)?.id;
const navigationPath = `/objects/${objectMetadataItem.namePlural}${
viewId ? `?view=${viewId}` : ''
}`;
return (
<NavigationDrawerItem <NavigationDrawerItem
key={objectMetadataItem.id} key={objectMetadataItem.id}
label={objectMetadataItem.labelPlural} label={objectMetadataItem.labelPlural}
to={`/objects/${objectMetadataItem.namePlural}`} to={navigationPath}
active={currentPath === `/objects/${objectMetadataItem.namePlural}`} active={currentPath === navigationPath}
Icon={getIcon(objectMetadataItem.icon)} Icon={getIcon(objectMetadataItem.icon)}
onClick={() => { onClick={() => {
navigate(`/objects/${objectMetadataItem.namePlural}`); navigate(navigationPath);
}} }}
/> />
))} );
})}
</> </>
); );
}; };

View File

@ -0,0 +1,4 @@
export enum QueryMethodName {
FindOne = 'findOne',
FindMany = 'findMany',
}

View File

@ -20,7 +20,7 @@ export const useGenerateFindManyRecordsQuery = () => {
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
)}FilterInput, $orderBy: ${capitalize( )}FilterInput, $orderBy: ${capitalize(
objectMetadataItem.nameSingular, objectMetadataItem.nameSingular,
)}OrderByInput, $lastCursor: String, $limit: Float = 30) { )}OrderByInput, $lastCursor: String, $limit: Float = 60) {
${ ${
objectMetadataItem.namePlural objectMetadataItem.namePlural
}(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){ }(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){

View File

@ -1,11 +1,12 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useQuery } from '@apollo/client';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; 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 { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
import { useGetCurrentUserQuery } from '~/generated/graphql';
export const UserProvider = ({ children }: React.PropsWithChildren) => { export const UserProvider = ({ children }: React.PropsWithChildren) => {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -16,16 +17,18 @@ export const UserProvider = ({ children }: React.PropsWithChildren) => {
currentWorkspaceMemberState, currentWorkspaceMemberState,
); );
const { data: userData, loading: userLoading } = useGetCurrentUserQuery({}); const { loading: queryLoading, data: queryData } = useQuery(
GET_CURRENT_USER_AND_VIEWS,
);
useEffect(() => { useEffect(() => {
if (!userLoading) { if (!queryLoading) {
setIsLoading(false); setIsLoading(false);
} }
if (userData?.currentUser?.workspaceMember) { if (queryData?.currentUser?.workspaceMember) {
setCurrentUser(userData.currentUser); setCurrentUser(queryData.currentUser);
setCurrentWorkspace(userData.currentUser.defaultWorkspace); setCurrentWorkspace(queryData.currentUser.defaultWorkspace);
const workspaceMember = userData.currentUser.workspaceMember; const workspaceMember = queryData.currentUser.workspaceMember;
setCurrentWorkspaceMember({ setCurrentWorkspaceMember({
...workspaceMember, ...workspaceMember,
colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'Light', colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'Light',
@ -34,10 +37,10 @@ export const UserProvider = ({ children }: React.PropsWithChildren) => {
}, [ }, [
setCurrentUser, setCurrentUser,
isLoading, isLoading,
userLoading, queryLoading,
setCurrentWorkspace, setCurrentWorkspace,
setCurrentWorkspaceMember, setCurrentWorkspaceMember,
userData?.currentUser, queryData?.currentUser,
]); ]);
return isLoading ? <></> : <>{children}</>; return isLoading ? <></> : <>{children}</>;

View File

@ -1,9 +0,0 @@
import { gql } from '@apollo/client';
export const GET_CURRENT_USER = gql`
query GetCurrentUser {
currentUser {
...UserQueryFragment
}
}
`;

View File

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

View File

@ -7,6 +7,7 @@ import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/c
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { TopBar } from '@/ui/layout/top-bar/TopBar'; import { TopBar } from '@/ui/layout/top-bar/TopBar';
import { FilterQueryParamsEffect } from '@/views/components/FilterQueryParamsEffect'; import { FilterQueryParamsEffect } from '@/views/components/FilterQueryParamsEffect';
import { ViewBarEffect } from '@/views/components/ViewBarEffect';
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect'; import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
import { ViewBarSortEffect } from '@/views/components/ViewBarSortEffect'; import { ViewBarSortEffect } from '@/views/components/ViewBarSortEffect';
import { useViewBar } from '@/views/hooks/useViewBar'; import { useViewBar } from '@/views/hooks/useViewBar';
@ -19,7 +20,6 @@ import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup'; import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
import { ViewBarDetails } from './ViewBarDetails'; import { ViewBarDetails } from './ViewBarDetails';
import { ViewBarEffect } from './ViewBarEffect';
import { ViewsDropdownButton } from './ViewsDropdownButton'; import { ViewsDropdownButton } from './ViewsDropdownButton';
export type ViewBarProps = { export type ViewBarProps = {

View File

@ -4,7 +4,7 @@ import { within } from '@storybook/test';
import { graphql, HttpResponse } from 'msw'; import { graphql, HttpResponse } from 'msw';
import { AppPath } from '@/types/AppPath'; 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 { import {
PageDecorator, PageDecorator,
PageDecoratorArgs, PageDecoratorArgs,
@ -22,13 +22,16 @@ const meta: Meta<PageDecoratorArgs> = {
parameters: { parameters: {
msw: { msw: {
handlers: [ handlers: [
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { graphql.query(
getOperationName(GET_CURRENT_USER_AND_VIEWS) ?? '',
() => {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
currentUser: mockedOnboardingUsersData[0], currentUser: mockedOnboardingUsersData[0],
}, },
}); });
}), },
),
graphqlMocks.handlers, graphqlMocks.handlers,
], ],
}, },

View File

@ -4,7 +4,7 @@ import { within } from '@storybook/test';
import { graphql, HttpResponse } from 'msw'; import { graphql, HttpResponse } from 'msw';
import { AppPath } from '@/types/AppPath'; 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 { import {
PageDecorator, PageDecorator,
PageDecoratorArgs, PageDecoratorArgs,
@ -22,7 +22,9 @@ const meta: Meta<PageDecoratorArgs> = {
parameters: { parameters: {
msw: { msw: {
handlers: [ handlers: [
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { graphql.query(
getOperationName(GET_CURRENT_USER_AND_VIEWS) ?? '',
() => {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
currentUser: { currentUser: {
@ -34,7 +36,8 @@ const meta: Meta<PageDecoratorArgs> = {
}, },
}, },
}); });
}), },
),
graphqlMocks.handlers, graphqlMocks.handlers,
], ],
}, },

View File

@ -4,7 +4,7 @@ import { fireEvent, within } from '@storybook/test';
import { graphql, HttpResponse } from 'msw'; import { graphql, HttpResponse } from 'msw';
import { AppPath } from '@/types/AppPath'; 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 { import {
PageDecorator, PageDecorator,
PageDecoratorArgs, PageDecoratorArgs,
@ -22,13 +22,16 @@ const meta: Meta<PageDecoratorArgs> = {
parameters: { parameters: {
msw: { msw: {
handlers: [ handlers: [
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { graphql.query(
getOperationName(GET_CURRENT_USER_AND_VIEWS) ?? '',
() => {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
currentUser: mockedOnboardingUsersData[0], currentUser: mockedOnboardingUsersData[0],
}, },
}); });
}), },
),
graphqlMocks.handlers, graphqlMocks.handlers,
], ],
}, },

View File

@ -4,7 +4,7 @@ import { graphql, HttpResponse } from 'msw';
import { CREATE_EVENT } from '@/analytics/graphql/queries/createEvent'; import { CREATE_EVENT } from '@/analytics/graphql/queries/createEvent';
import { GET_CLIENT_CONFIG } from '@/client-config/graphql/queries/getClientConfig'; import { GET_CLIENT_CONFIG } from '@/client-config/graphql/queries/getClientConfig';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries'; 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 { REACT_APP_SERVER_BASE_URL } from '~/config';
import { mockedActivities } from '~/testing/mock-data/activities'; import { mockedActivities } from '~/testing/mock-data/activities';
import { mockedCompaniesData } from '~/testing/mock-data/companies'; import { mockedCompaniesData } from '~/testing/mock-data/companies';
@ -22,10 +22,22 @@ const metadataGraphql = graphql.link(`${REACT_APP_SERVER_BASE_URL}/metadata`);
export const graphqlMocks = { export const graphqlMocks = {
handlers: [ handlers: [
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => { graphql.query(getOperationName(GET_CURRENT_USER_AND_VIEWS) ?? '', () => {
return HttpResponse.json({ return HttpResponse.json({
data: { data: {
currentUser: mockedUsersData[0], currentUser: mockedUsersData[0],
views: {
edges: mockedViewsData.map((view) => ({
node: view,
cursor: null,
})),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
},
},
}, },
}); });
}), }),

View File

@ -172,6 +172,12 @@ export class TokenService {
return { token }; return { token };
} }
isTokenPresent(request: Request): boolean {
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
return !!token;
}
async validateToken(request: Request): Promise<Workspace> { async validateToken(request: Request): Promise<Workspace> {
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request); const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);

View File

@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common'; import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ContextIdFactory, ModuleRef } from '@nestjs/core'; import { ContextIdFactory, ModuleRef } from '@nestjs/core';
import { GqlOptionsFactory } from '@nestjs/graphql'; import { GqlOptionsFactory } from '@nestjs/graphql';
@ -56,17 +56,22 @@ export class GraphQLConfigService
}, },
conditionalSchema: async (context) => { conditionalSchema: async (context) => {
try { try {
let workspace: Workspace; if (!this.tokenService.isTokenPresent(context.req)) {
// If token is not valid, it will return an empty schema
try {
workspace = await this.tokenService.validateToken(context.req);
} catch (err) {
return new GraphQLSchema({}); return new GraphQLSchema({});
} }
const workspace = await this.tokenService.validateToken(context.req);
return await this.createSchema(context, workspace); return await this.createSchema(context, workspace);
} catch (error) { } catch (error) {
if (error instanceof UnauthorizedException) {
throw new GraphQLError('Unauthenticated', {
extensions: {
code: 'UNAUTHENTICATED',
},
});
}
if (error instanceof JsonWebTokenError) { if (error instanceof JsonWebTokenError) {
//mockedUserJWT //mockedUserJWT
throw new GraphQLError('Unauthenticated', { throw new GraphQLError('Unauthenticated', {