diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js index 0e07f7d8e44a40912df4ae7148f55d3e832d6449..e0d9a903e0acccb63def9ec67d6fd3c18b3188c3 100644 --- a/app/assets/javascripts/lib/graphql.js +++ b/app/assets/javascripts/lib/graphql.js @@ -5,6 +5,7 @@ import { ApolloLink } from 'apollo-link'; import { BatchHttpLink } from 'apollo-link-batch-http'; import csrf from '~/lib/utils/csrf'; import PerformanceBarService from '~/performance_bar/services/performance_bar_service'; +import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link'; export const fetchPolicies = { CACHE_FIRST: 'cache-first', @@ -62,7 +63,7 @@ export default (resolvers = {}, config = {}) => { return new ApolloClient({ typeDefs: config.typeDefs, - link: ApolloLink.from([performanceBarLink, uploadsLink]), + link: ApolloLink.from([performanceBarLink, new StartupJSLink(), uploadsLink]), cache: new InMemoryCache({ ...config.cacheConfig, freezeResults: config.assumeImmutableResults, diff --git a/app/assets/javascripts/lib/utils/apollo_startup_js_link.js b/app/assets/javascripts/lib/utils/apollo_startup_js_link.js new file mode 100644 index 0000000000000000000000000000000000000000..5c120dd532f5a1131ac1ec2aa03c3f7968a88a7b --- /dev/null +++ b/app/assets/javascripts/lib/utils/apollo_startup_js_link.js @@ -0,0 +1,106 @@ +import { ApolloLink, Observable } from 'apollo-link'; +import { parse } from 'graphql'; +import { isEqual, pickBy } from 'lodash'; + +/** + * Remove undefined values from object + * @param obj + * @returns {Dictionary} + */ +const pickDefinedValues = obj => pickBy(obj, x => x !== undefined); + +/** + * Compares two set of variables, order independent + * + * Ignores undefined values (in the top level) and supports arrays etc. + */ +const variablesMatch = (var1 = {}, var2 = {}) => { + return isEqual(pickDefinedValues(var1), pickDefinedValues(var2)); +}; + +export class StartupJSLink extends ApolloLink { + constructor() { + super(); + this.startupCalls = new Map(); + this.parseStartupCalls(window.gl?.startup_graphql_calls || []); + } + + // Extract operationNames from the queries and ensure that we can + // match operationName => element from result array + parseStartupCalls(calls) { + calls.forEach(call => { + const { query, variables, fetchCall } = call; + const operationName = parse(query)?.definitions?.find(x => x.kind === 'OperationDefinition') + ?.name?.value; + + if (operationName) { + this.startupCalls.set(operationName, { + variables, + fetchCall, + }); + } + }); + } + + static noopRequest = (operation, forward) => forward(operation); + + disable() { + this.request = StartupJSLink.noopRequest; + this.startupCalls = null; + } + + request(operation, forward) { + // Disable StartupJSLink in case all calls are done or none are set up + if (this.startupCalls && this.startupCalls.size === 0) { + this.disable(); + return forward(operation); + } + + const { operationName } = operation; + + // Skip startup call if the operationName doesn't match + if (!this.startupCalls.has(operationName)) { + return forward(operation); + } + + const { variables: startupVariables, fetchCall } = this.startupCalls.get(operationName); + this.startupCalls.delete(operationName); + + // Skip startup call if the variables values do not match + if (!variablesMatch(startupVariables, operation.variables)) { + return forward(operation); + } + + return new Observable(observer => { + fetchCall + .then(response => { + // Handle HTTP errors + if (!response.ok) { + throw new Error('fetchCall failed'); + } + operation.setContext({ response }); + return response.json(); + }) + .then(result => { + if (result && (result.errors || !result.data)) { + throw new Error('Received GraphQL error'); + } + + // we have data and can send it to back up the link chain + observer.next(result); + observer.complete(); + }) + .catch(() => { + forward(operation).subscribe({ + next: result => { + observer.next(result); + }, + error: error => { + observer.error(error); + }, + complete: observer.complete.bind(observer), + }); + }); + }); + } +} diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue index 677cb2659421994a106e1c3956a3b6e8b247643e..a1f1c77df2f70fabd67683109b1d6de7030eb999 100644 --- a/app/assets/javascripts/repository/components/breadcrumbs.vue +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -6,12 +6,12 @@ import { GlDropdownItem, GlIcon, } from '@gitlab/ui'; +import permissionsQuery from 'shared_queries/repository/permissions.query.graphql'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import { __ } from '../../locale'; import getRefMixin from '../mixins/get_ref'; import projectShortPathQuery from '../queries/project_short_path.query.graphql'; import projectPathQuery from '../queries/project_path.query.graphql'; -import permissionsQuery from '../queries/permissions.query.graphql'; const ROW_TYPES = { header: 'header', diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue index 78b8baaa75e8425730a58ea446102a90ea542fde..b42f88631b52a315d751fdb0e2aefc05c0ba0a51 100644 --- a/app/assets/javascripts/repository/components/tree_content.vue +++ b/app/assets/javascripts/repository/components/tree_content.vue @@ -1,9 +1,9 @@