diff --git a/src/components/Content/Layout.tsx b/src/components/Content/Layout.tsx
index 6e870453ed..e74f2d40ed 100644
--- a/src/components/Content/Layout.tsx
+++ b/src/components/Content/Layout.tsx
@@ -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
diff --git a/src/gql/GQLWrapper.tsx b/src/gql/GQLWrapper.tsx
index 6c2e5e857e..6072b29330 100644
--- a/src/gql/GQLWrapper.tsx
+++ b/src/gql/GQLWrapper.tsx
@@ -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 (
-
- {children}
-
- );
-};
-
-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 {children};
};
-export { cache };
export default GQLWrapper;
diff --git a/src/hooks/useCreateGQLClient/index.ts b/src/hooks/useCreateGQLClient/index.ts
new file mode 100644
index 0000000000..36639e017e
--- /dev/null
+++ b/src/hooks/useCreateGQLClient/index.ts
@@ -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 => {
+ const { dispatchAuthenticated, logoutAndRedirect } = useAuthDispatchContext();
+ const [secretFields, setSecretFields] = useState();
+ const [gqlClient, setGQLClient] = useState();
+
+ 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,
+};