Skip to content
This repository has been archived by the owner on Jul 2, 2024. It is now read-only.

Commit

Permalink
Initialze GQL Client after SecretFields are fetched
Browse files Browse the repository at this point in the history
  • Loading branch information
SupaJoon committed Mar 1, 2024
1 parent 0b5eda7 commit d38e6cd
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 220 deletions.
1 change: 0 additions & 1 deletion src/components/Content/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export const Layout: React.FC = () => {
const { isAuthenticated } = useAuthStateContext();
useAnalyticsAttributes();
useAnnouncementToast();

// this top-level query is required for authentication to work
// afterware is used at apollo link level to authenticate or deauthenticate user based on response to query
// therefore this could be any query as long as it is top-level
Expand Down
225 changes: 6 additions & 219 deletions src/gql/GQLWrapper.tsx
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;
77 changes: 77 additions & 0 deletions src/hooks/useCreateGQLClient/index.ts
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,
};

0 comments on commit d38e6cd

Please sign in to comment.