2022-04-04 11:22:00 +05:30
|
|
|
|
import { ApolloClient, InMemoryCache, ApolloLink, HttpLink } from '@apollo/client/core';
|
|
|
|
|
import { BatchHttpLink } from '@apollo/client/link/batch-http';
|
2021-03-11 19:13:27 +05:30
|
|
|
|
import { createUploadLink } from 'apollo-upload-client';
|
2023-06-20 00:43:36 +05:30
|
|
|
|
import { persistCache } from 'apollo3-cache-persist';
|
2021-06-08 01:23:25 +05:30
|
|
|
|
import ActionCableLink from '~/actioncable_link';
|
2021-04-29 21:17:54 +05:30
|
|
|
|
import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link';
|
2022-06-21 17:19:12 +05:30
|
|
|
|
import possibleTypes from '~/graphql_shared/possible_types.json';
|
2021-03-11 19:13:27 +05:30
|
|
|
|
import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
|
2019-02-15 15:39:39 +05:30
|
|
|
|
import csrf from '~/lib/utils/csrf';
|
2021-09-30 23:02:18 +05:30
|
|
|
|
import { objectToQuery, queryToObject } from '~/lib/utils/url_utility';
|
2020-11-24 15:15:51 +05:30
|
|
|
|
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
|
2021-11-11 11:23:49 +05:30
|
|
|
|
import { getInstrumentationLink } from './apollo/instrumentation_link';
|
2021-11-18 22:05:49 +05:30
|
|
|
|
import { getSuppressNetworkErrorsDuringNavigationLink } from './apollo/suppress_network_errors_during_navigation_link';
|
2023-04-23 21:23:45 +05:30
|
|
|
|
import { getPersistLink } from './apollo/persist_link';
|
|
|
|
|
import { persistenceMapper } from './apollo/persistence_mapper';
|
2019-02-15 15:39:39 +05:30
|
|
|
|
|
2020-03-13 15:44:24 +05:30
|
|
|
|
export const fetchPolicies = {
|
|
|
|
|
CACHE_FIRST: 'cache-first',
|
|
|
|
|
CACHE_AND_NETWORK: 'cache-and-network',
|
|
|
|
|
NETWORK_ONLY: 'network-only',
|
|
|
|
|
NO_CACHE: 'no-cache',
|
|
|
|
|
CACHE_ONLY: 'cache-only',
|
|
|
|
|
};
|
|
|
|
|
|
2022-04-04 11:22:00 +05:30
|
|
|
|
export const typePolicies = {
|
|
|
|
|
Repository: {
|
|
|
|
|
merge: true,
|
|
|
|
|
},
|
|
|
|
|
UserPermissions: {
|
|
|
|
|
merge: true,
|
|
|
|
|
},
|
|
|
|
|
MergeRequestPermissions: {
|
|
|
|
|
merge: true,
|
|
|
|
|
},
|
|
|
|
|
ContainerRepositoryConnection: {
|
|
|
|
|
merge: true,
|
|
|
|
|
},
|
|
|
|
|
TimelogConnection: {
|
|
|
|
|
merge: true,
|
|
|
|
|
},
|
|
|
|
|
BranchList: {
|
|
|
|
|
merge: true,
|
|
|
|
|
},
|
|
|
|
|
InstanceSecurityDashboard: {
|
|
|
|
|
merge: true,
|
|
|
|
|
},
|
|
|
|
|
PipelinePermissions: {
|
|
|
|
|
merge: true,
|
|
|
|
|
},
|
|
|
|
|
DesignCollection: {
|
|
|
|
|
merge: true,
|
|
|
|
|
},
|
2022-06-21 17:19:12 +05:30
|
|
|
|
TreeEntry: {
|
|
|
|
|
keyFields: ['webPath'],
|
|
|
|
|
},
|
2023-06-20 00:43:36 +05:30
|
|
|
|
Subscription: {
|
|
|
|
|
fields: {
|
|
|
|
|
aiCompletionResponse: {
|
|
|
|
|
read(value) {
|
|
|
|
|
return value ?? null;
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2022-04-04 11:22:00 +05:30
|
|
|
|
};
|
|
|
|
|
|
2021-09-30 23:02:18 +05:30
|
|
|
|
export const stripWhitespaceFromQuery = (url, path) => {
|
|
|
|
|
/* eslint-disable-next-line no-unused-vars */
|
|
|
|
|
const [_, params] = url.split(path);
|
|
|
|
|
|
|
|
|
|
if (!params) {
|
|
|
|
|
return url;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const decoded = decodeURIComponent(params);
|
|
|
|
|
const paramsObj = queryToObject(decoded);
|
|
|
|
|
|
|
|
|
|
if (!paramsObj.query) {
|
|
|
|
|
return url;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const stripped = paramsObj.query
|
|
|
|
|
.split(/\s+|\n/)
|
|
|
|
|
.join(' ')
|
|
|
|
|
.trim();
|
|
|
|
|
paramsObj.query = stripped;
|
|
|
|
|
|
|
|
|
|
const reassembled = objectToQuery(paramsObj);
|
|
|
|
|
return `${path}?${reassembled}`;
|
|
|
|
|
};
|
|
|
|
|
|
2022-04-04 11:22:00 +05:30
|
|
|
|
const acs = [];
|
|
|
|
|
|
|
|
|
|
let pendingApolloMutations = 0;
|
|
|
|
|
|
|
|
|
|
// ### Why track pendingApolloMutations, but calculate pendingApolloRequests?
|
|
|
|
|
//
|
|
|
|
|
// In Apollo 2, we had a single link for counting operations.
|
|
|
|
|
//
|
|
|
|
|
// With Apollo 3, the `forward().map(...)` of deduped queries is never called.
|
|
|
|
|
// So, we resorted to calculating the sum of `inFlightLinkObservables?.size`.
|
|
|
|
|
// However! Mutations don't use `inFLightLinkObservables`, but since they are likely
|
|
|
|
|
// not deduped we can count them...
|
|
|
|
|
//
|
|
|
|
|
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55062#note_838943715
|
|
|
|
|
// https://www.apollographql.com/docs/react/v2/networking/network-layer/#query-deduplication
|
|
|
|
|
Object.defineProperty(window, 'pendingApolloRequests', {
|
|
|
|
|
get() {
|
|
|
|
|
return acs.reduce(
|
|
|
|
|
(sum, ac) => sum + (ac?.queryManager?.inFlightLinkObservables?.size || 0),
|
|
|
|
|
pendingApolloMutations,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2023-06-20 00:43:36 +05:30
|
|
|
|
function createApolloClient(resolvers = {}, config = {}) {
|
2021-06-08 01:23:25 +05:30
|
|
|
|
const {
|
|
|
|
|
baseUrl,
|
|
|
|
|
batchMax = 10,
|
2022-07-23 23:45:48 +05:30
|
|
|
|
cacheConfig = { typePolicies: {}, possibleTypes: {} },
|
2021-06-08 01:23:25 +05:30
|
|
|
|
fetchPolicy = fetchPolicies.CACHE_FIRST,
|
|
|
|
|
typeDefs,
|
|
|
|
|
path = '/api/graphql',
|
|
|
|
|
useGet = false,
|
|
|
|
|
} = config;
|
2022-04-04 11:22:00 +05:30
|
|
|
|
let ac = null;
|
2021-06-08 01:23:25 +05:30
|
|
|
|
let uri = `${gon.relative_url_root || ''}${path}`;
|
2019-07-31 22:56:46 +05:30
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
|
if (baseUrl) {
|
2019-07-31 22:56:46 +05:30
|
|
|
|
// Prepend baseUrl and ensure that `///` are replaced with `/`
|
2021-06-08 01:23:25 +05:30
|
|
|
|
uri = `${baseUrl}${uri}`.replace(/\/{3,}/g, '/');
|
2019-07-31 22:56:46 +05:30
|
|
|
|
}
|
|
|
|
|
|
2019-09-04 21:01:54 +05:30
|
|
|
|
const httpOptions = {
|
|
|
|
|
uri,
|
|
|
|
|
headers: {
|
|
|
|
|
[csrf.headerKey]: csrf.token,
|
|
|
|
|
},
|
2020-03-13 15:44:24 +05:30
|
|
|
|
// fetch won’t send cookies in older browsers, unless you set the credentials init option.
|
|
|
|
|
// We set to `same-origin` which is default value in modern browsers.
|
|
|
|
|
// See https://github.com/whatwg/fetch/pull/585 for more information.
|
|
|
|
|
credentials: 'same-origin',
|
2021-06-08 01:23:25 +05:30
|
|
|
|
batchMax,
|
2019-09-04 21:01:54 +05:30
|
|
|
|
};
|
|
|
|
|
|
2021-09-30 23:02:18 +05:30
|
|
|
|
/*
|
|
|
|
|
This custom fetcher intervention is to deal with an issue where we are using GET to access
|
|
|
|
|
eTag polling, but Apollo Client adds excessive whitespace, which causes the
|
|
|
|
|
request to fail on certain self-hosted stacks. When we can move
|
|
|
|
|
to subscriptions entirely or can land an upstream PR, this can be removed.
|
|
|
|
|
|
|
|
|
|
Related links
|
|
|
|
|
Bug report: https://gitlab.com/gitlab-org/gitlab/-/issues/329895
|
|
|
|
|
Moving to subscriptions: https://gitlab.com/gitlab-org/gitlab/-/issues/332485
|
|
|
|
|
Apollo Client issue: https://github.com/apollographql/apollo-feature-requests/issues/182
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const fetchIntervention = (url, options) => {
|
|
|
|
|
return fetch(stripWhitespaceFromQuery(url, uri), options);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const requestLink = ApolloLink.split(
|
|
|
|
|
() => useGet,
|
|
|
|
|
new HttpLink({ ...httpOptions, fetch: fetchIntervention }),
|
|
|
|
|
new BatchHttpLink(httpOptions),
|
|
|
|
|
);
|
|
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
|
const uploadsLink = ApolloLink.split(
|
2021-03-08 18:12:59 +05:30
|
|
|
|
(operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
|
2020-11-24 15:15:51 +05:30
|
|
|
|
createUploadLink(httpOptions),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const performanceBarLink = new ApolloLink((operation, forward) => {
|
2021-03-08 18:12:59 +05:30
|
|
|
|
return forward(operation).map((response) => {
|
2020-11-24 15:15:51 +05:30
|
|
|
|
const httpResponse = operation.getContext().response;
|
|
|
|
|
|
|
|
|
|
if (PerformanceBarService.interceptor) {
|
|
|
|
|
PerformanceBarService.interceptor({
|
|
|
|
|
config: {
|
|
|
|
|
url: httpResponse.url,
|
2022-07-23 23:45:48 +05:30
|
|
|
|
operationName: operation.operationName,
|
2023-06-20 00:43:36 +05:30
|
|
|
|
method: operation.getContext()?.fetchOptions?.method || 'POST', // If method is not explicitly set, we default to POST request
|
2020-11-24 15:15:51 +05:30
|
|
|
|
},
|
|
|
|
|
headers: {
|
|
|
|
|
'x-request-id': httpResponse.headers.get('x-request-id'),
|
|
|
|
|
'x-gitlab-from-cache': httpResponse.headers.get('x-gitlab-from-cache'),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
|
const hasSubscriptionOperation = ({ query: { definitions } }) => {
|
|
|
|
|
return definitions.some(
|
|
|
|
|
({ kind, operation }) => kind === 'OperationDefinition' && operation === 'subscription',
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2022-04-04 11:22:00 +05:30
|
|
|
|
const hasMutation = (operation) =>
|
|
|
|
|
(operation?.query?.definitions || []).some((x) => x.operation === 'mutation');
|
|
|
|
|
|
|
|
|
|
const requestCounterLink = new ApolloLink((operation, forward) => {
|
|
|
|
|
if (hasMutation(operation)) {
|
|
|
|
|
pendingApolloMutations += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return forward(operation).map((response) => {
|
|
|
|
|
if (hasMutation(operation)) {
|
|
|
|
|
pendingApolloMutations -= 1;
|
|
|
|
|
}
|
|
|
|
|
return response;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2023-04-23 21:23:45 +05:30
|
|
|
|
const persistLink = getPersistLink();
|
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
|
const appLink = ApolloLink.split(
|
|
|
|
|
hasSubscriptionOperation,
|
|
|
|
|
new ActionCableLink(),
|
2021-11-11 11:23:49 +05:30
|
|
|
|
ApolloLink.from(
|
|
|
|
|
[
|
2021-11-18 22:05:49 +05:30
|
|
|
|
getSuppressNetworkErrorsDuringNavigationLink(),
|
2021-11-11 11:23:49 +05:30
|
|
|
|
getInstrumentationLink(),
|
|
|
|
|
requestCounterLink,
|
|
|
|
|
performanceBarLink,
|
|
|
|
|
new StartupJSLink(),
|
|
|
|
|
apolloCaptchaLink,
|
2023-04-23 21:23:45 +05:30
|
|
|
|
persistLink,
|
2021-11-11 11:23:49 +05:30
|
|
|
|
uploadsLink,
|
|
|
|
|
requestLink,
|
|
|
|
|
].filter(Boolean),
|
|
|
|
|
),
|
2021-06-08 01:23:25 +05:30
|
|
|
|
);
|
|
|
|
|
|
2023-04-23 21:23:45 +05:30
|
|
|
|
const newCache = new InMemoryCache({
|
|
|
|
|
...cacheConfig,
|
|
|
|
|
typePolicies: {
|
|
|
|
|
...typePolicies,
|
|
|
|
|
...cacheConfig.typePolicies,
|
|
|
|
|
},
|
|
|
|
|
possibleTypes: {
|
|
|
|
|
...possibleTypes,
|
|
|
|
|
...cacheConfig.possibleTypes,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2022-04-04 11:22:00 +05:30
|
|
|
|
ac = new ApolloClient({
|
2021-06-08 01:23:25 +05:30
|
|
|
|
typeDefs,
|
|
|
|
|
link: appLink,
|
2022-08-27 11:52:29 +05:30
|
|
|
|
connectToDevTools: process.env.NODE_ENV !== 'production',
|
2023-04-23 21:23:45 +05:30
|
|
|
|
cache: newCache,
|
2019-07-07 11:18:12 +05:30
|
|
|
|
resolvers,
|
2020-03-13 15:44:24 +05:30
|
|
|
|
defaultOptions: {
|
|
|
|
|
query: {
|
2021-06-08 01:23:25 +05:30
|
|
|
|
fetchPolicy,
|
2020-03-13 15:44:24 +05:30
|
|
|
|
},
|
|
|
|
|
},
|
2019-07-07 11:18:12 +05:30
|
|
|
|
});
|
2022-04-04 11:22:00 +05:30
|
|
|
|
|
|
|
|
|
acs.push(ac);
|
|
|
|
|
|
2023-06-20 00:43:36 +05:30
|
|
|
|
return { client: ac, cache: newCache };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createApolloClientWithCaching(resolvers = {}, config = {}) {
|
|
|
|
|
const { localCacheKey = null } = config;
|
|
|
|
|
const { client, cache } = createApolloClient(resolvers, config);
|
|
|
|
|
|
|
|
|
|
if (localCacheKey) {
|
|
|
|
|
let storage;
|
|
|
|
|
|
|
|
|
|
// Test that we can use IndexedDB. If not, no persisting for you!
|
|
|
|
|
try {
|
|
|
|
|
const { IndexedDBPersistentStorage } = await import(
|
|
|
|
|
/* webpackChunkName: 'indexed_db_persistent_storage' */ './apollo/indexed_db_persistent_storage'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
storage = await IndexedDBPersistentStorage.create();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return client;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await persistCache({
|
|
|
|
|
cache,
|
|
|
|
|
// we leave NODE_ENV here temporarily for visibility so developers can easily see caching happening in dev mode
|
|
|
|
|
debug: process.env.NODE_ENV === 'development',
|
|
|
|
|
storage,
|
|
|
|
|
key: localCacheKey,
|
|
|
|
|
persistenceMapper,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return client;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default (resolvers = {}, config = {}) => {
|
|
|
|
|
const { client } = createApolloClient(resolvers, config);
|
|
|
|
|
|
|
|
|
|
return client;
|
2019-07-31 22:56:46 +05:30
|
|
|
|
};
|