This repository has been archived by the owner on Jul 2, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initialze GQL Client after SecretFields are fetched
- Loading branch information
Showing
3 changed files
with
83 additions
and
220 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,225 +1,12 @@ | ||
import { | ||
ApolloClient, | ||
ApolloProvider, | ||
InMemoryCache, | ||
ApolloLink, | ||
HttpLink, | ||
} from "@apollo/client"; | ||
import { onError } from "@apollo/client/link/error"; | ||
import { RetryLink } from "@apollo/client/link/retry"; | ||
import { routes } from "constants/routes"; | ||
import { useAuthDispatchContext } from "context/Auth"; | ||
import { environmentVariables } from "utils"; | ||
import { | ||
leaveBreadcrumb, | ||
reportError, | ||
SentryBreadcrumb, | ||
} from "utils/errorReporting"; | ||
|
||
const { getGQLUrl } = environmentVariables; | ||
import { ApolloProvider } from "@apollo/client"; | ||
import { useCreateGQLCLient } from "hooks/useCreateGQLClient"; | ||
|
||
const GQLWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => { | ||
const { dispatchAuthenticated, logoutAndRedirect } = useAuthDispatchContext(); | ||
return ( | ||
<ApolloProvider | ||
client={getGQLClient({ | ||
credentials: "include", | ||
gqlURL: getGQLUrl(), | ||
logoutAndRedirect, | ||
dispatchAuthenticated, | ||
})} | ||
> | ||
{children} | ||
</ApolloProvider> | ||
); | ||
}; | ||
|
||
interface ClientLinkParams { | ||
credentials?: string; | ||
gqlURL?: string; | ||
logoutAndRedirect?: () => void; | ||
dispatchAuthenticated?: () => void; | ||
} | ||
|
||
const cache = new InMemoryCache({ | ||
typePolicies: { | ||
Query: { | ||
fields: { | ||
distroEvents: { | ||
keyArgs: ["$distroId"], | ||
}, | ||
projectEvents: { | ||
keyArgs: ["$identifier"], | ||
}, | ||
repoEvents: { | ||
keyArgs: ["$id"], | ||
}, | ||
}, | ||
}, | ||
GeneralSubscription: { | ||
keyFields: false, | ||
}, | ||
DistroEventsPayload: { | ||
fields: { | ||
count: { | ||
merge(existing = 0, incoming = 0) { | ||
return existing + incoming; | ||
}, | ||
}, | ||
eventLogEntries: { | ||
merge(existing = [], incoming = []) { | ||
return [...existing, ...incoming]; | ||
}, | ||
}, | ||
}, | ||
}, | ||
ProjectEvents: { | ||
fields: { | ||
count: { | ||
merge(existing = 0, incoming = 0) { | ||
return existing + incoming; | ||
}, | ||
}, | ||
eventLogEntries: { | ||
merge(existing = [], incoming = []) { | ||
return [...existing, ...incoming]; | ||
}, | ||
}, | ||
}, | ||
}, | ||
ProjectAlias: { | ||
keyFields: false, | ||
}, | ||
Project: { | ||
keyFields: false, | ||
}, | ||
User: { | ||
keyFields: ["userId"], | ||
}, | ||
Task: { | ||
keyFields: ["execution", "id"], | ||
fields: { | ||
annotation: { | ||
merge(existing, incoming, { mergeObjects }) { | ||
return mergeObjects(existing, incoming); | ||
}, | ||
}, | ||
taskLogs: { | ||
merge(_, incoming) { | ||
return incoming; | ||
}, | ||
}, | ||
}, | ||
}, | ||
Patch: { | ||
fields: { | ||
time: { | ||
merge(existing, incoming, { mergeObjects }) { | ||
return mergeObjects(existing, incoming); | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
const authLink = (logout: () => void): ApolloLink => | ||
onError(({ networkError }) => { | ||
if ( | ||
// must perform these checks so that TS does not complain bc typings for network does not include 'statusCode' | ||
networkError && | ||
"statusCode" in networkError && | ||
networkError.statusCode === 401 && | ||
window.location.pathname !== routes.login | ||
) { | ||
leaveBreadcrumb( | ||
"Not Authenticated", | ||
{ status_code: 401 }, | ||
SentryBreadcrumb.User, | ||
); | ||
logout(); | ||
} | ||
}); | ||
|
||
const logErrorsLink = onError(({ graphQLErrors, operation }) => { | ||
if (Array.isArray(graphQLErrors)) { | ||
graphQLErrors.forEach((gqlErr) => { | ||
const fingerprint = [operation.operationName]; | ||
if (gqlErr?.path?.length) { | ||
fingerprint.push(...gqlErr.path); | ||
} | ||
reportError(new Error(gqlErr.message), { | ||
fingerprint, | ||
tags: { operationName: operation.operationName }, | ||
context: { | ||
gqlErr, | ||
variables: operation.variables, | ||
}, | ||
}).warning(); | ||
}); | ||
const client = useCreateGQLCLient(); | ||
if (!client) { | ||
return null; | ||
} | ||
// dont track network errors here because they are | ||
// very common when a user is not authenticated | ||
}); | ||
|
||
const authenticateIfSuccessfulLink = ( | ||
dispatchAuthenticated: () => void, | ||
): ApolloLink => | ||
new ApolloLink((operation, forward) => | ||
forward(operation).map((response) => { | ||
if (response && response.data) { | ||
// if there is data in response then server responded with 200; therefore, is authenticated. | ||
dispatchAuthenticated(); | ||
} | ||
leaveBreadcrumb( | ||
"Graphql Request", | ||
{ | ||
operationName: operation.operationName, | ||
variables: operation.variables, | ||
status: !response.errors ? "OK" : "ERROR", | ||
errors: response.errors, | ||
}, | ||
SentryBreadcrumb.HTTP, | ||
); | ||
return response; | ||
}), | ||
); | ||
|
||
const retryLink = new RetryLink({ | ||
delay: { | ||
initial: 300, | ||
max: 3000, | ||
jitter: true, | ||
}, | ||
attempts: { | ||
max: 5, | ||
retryIf: (error): boolean => | ||
error && error.response && error.response.status >= 500, | ||
}, | ||
}); | ||
|
||
const getGQLClient = ({ | ||
credentials, | ||
dispatchAuthenticated, | ||
gqlURL, | ||
logoutAndRedirect, | ||
}: ClientLinkParams) => { | ||
const link = new HttpLink({ | ||
uri: gqlURL, | ||
credentials, | ||
}); | ||
|
||
const client = new ApolloClient({ | ||
cache, | ||
link: authenticateIfSuccessfulLink(dispatchAuthenticated) | ||
.concat(authLink(logoutAndRedirect)) | ||
.concat(logErrorsLink) | ||
.concat(retryLink) | ||
.concat(link), | ||
}); | ||
|
||
return client; | ||
return <ApolloProvider client={client}>{children}</ApolloProvider>; | ||
}; | ||
|
||
export { cache }; | ||
export default GQLWrapper; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { useEffect, useState } from "react"; | ||
import { HttpLink, ApolloClient, NormalizedCacheObject } from "@apollo/client"; | ||
import { OperationDefinitionNode } from "graphql"; | ||
import { useAuthDispatchContext } from "context/Auth"; | ||
import { cache } from "gql/client/cache"; | ||
import { | ||
authenticateIfSuccessfulLink, | ||
authLink, | ||
leaveBreadcrumbLink, | ||
logErrorsLink, | ||
retryLink, | ||
} from "gql/client/link"; | ||
import { SECRET_FIELDS } from "gql/queries"; | ||
import { environmentVariables } from "utils"; | ||
import { leaveBreadcrumb, SentryBreadcrumb } from "utils/errorReporting"; | ||
import { fetchWithRetry } from "utils/request"; | ||
|
||
const { getGQLUrl } = environmentVariables; | ||
|
||
export const useCreateGQLCLient = (): ApolloClient<NormalizedCacheObject> => { | ||
const { dispatchAuthenticated, logoutAndRedirect } = useAuthDispatchContext(); | ||
const [secretFields, setSecretFields] = useState<string[]>(); | ||
const [gqlClient, setGQLClient] = useState<any>(); | ||
|
||
useEffect(() => { | ||
fetchWithRetry(getGQLUrl(), secretFieldsReq) | ||
.then(({ data }) => { | ||
setSecretFields(data?.spruceConfig?.secretFields); | ||
}) | ||
.catch((err) => { | ||
leaveBreadcrumb( | ||
"SecretFields Query Error", | ||
{ | ||
err, | ||
}, | ||
SentryBreadcrumb.HTTP, | ||
); | ||
}); | ||
}, []); | ||
|
||
useEffect(() => { | ||
if (secretFields && !gqlClient) { | ||
const client = new ApolloClient({ | ||
cache, | ||
link: authenticateIfSuccessfulLink(dispatchAuthenticated) | ||
.concat(authLink(logoutAndRedirect)) | ||
.concat(leaveBreadcrumbLink(secretFields)) | ||
.concat(logErrorsLink) | ||
.concat(retryLink) | ||
.concat( | ||
new HttpLink({ | ||
uri: getGQLUrl(), | ||
credentials: "include", | ||
}), | ||
), | ||
}); | ||
setGQLClient(client); | ||
} | ||
}, [secretFields, gqlClient, dispatchAuthenticated, logoutAndRedirect]); | ||
|
||
return gqlClient; | ||
}; | ||
|
||
const secretFieldsReq = { | ||
credentials: "include" as RequestCredentials, | ||
headers: { | ||
"content-type": "application/json", | ||
}, | ||
body: JSON.stringify({ | ||
operationName: (SECRET_FIELDS.definitions[0] as OperationDefinitionNode) | ||
.name.value, | ||
query: SECRET_FIELDS.loc?.source.body, | ||
variables: {}, | ||
}), | ||
method: "POST", | ||
mode: "cors" as RequestMode, | ||
}; |