Commit 78e71f16 authored by Lukas 'Eipi' Eipert's avatar Lukas 'Eipi' Eipert Committed by Natalia Tepluhina

Refactor StartupJS for GraphQL

This refactors our StartupJS for GraphQL to be a Apollo Link [0] based
application. Instead of manually filling the cache, we build an
interceptor which "short-circuits" the Apollo Link pipeline in case the
query can be found in StartupJS. In case the query fails, is not cached,
has different variables, is done more than once, we skip it down the
pipeline. Also if all Startup Queries have been done, it self-disables.

We also now batch all StartupJS requests into one GraphQL call.

[0]: https://www.apollographql.com/docs/link/overview/
parent 441ea69a
......@@ -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,
......
import { ApolloLink, Observable } from 'apollo-link';
import { parse } from 'graphql';
import { isEqual, pickBy } from 'lodash';
/**
* Remove undefined values from object
* @param obj
* @returns {Dictionary<unknown>}
*/
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),
});
});
});
}
}
......@@ -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',
......
<script>
import filesQuery from 'shared_queries/repository/files.query.graphql';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '../../locale';
import FileTable from './table/index.vue';
import getRefMixin from '../mixins/get_ref';
import filesQuery from '../queries/files.query.graphql';
import projectPathQuery from '../queries/project_path.query.graphql';
import FilePreview from './preview/index.vue';
import { readmeFile } from '../utils/readme';
......
import Vue from 'vue';
import PathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { escapeFileUrl } from '../lib/utils/url_utility';
import createRouter from './router';
import App from './components/app.vue';
......@@ -19,10 +18,6 @@ export default function setupVueRepositoryList() {
const { dataset } = el;
const { projectPath, projectShortPath, ref, escapedRef, fullName } = dataset;
const router = createRouter(projectPath, escapedRef);
const pathRegex = /-\/tree\/[^/]+\/(.+$)/;
const matches = window.location.href.match(pathRegex);
const currentRoutePath = matches ? matches[1] : '';
apolloProvider.clients.defaultClient.cache.writeData({
data: {
......@@ -48,28 +43,7 @@ export default function setupVueRepositoryList() {
},
});
if (window.gl.startup_graphql_calls) {
const query = window.gl.startup_graphql_calls.find(
call => call.operationName === 'pathLastCommit',
);
query.fetchCall
.then(res => res.json())
.then(res => {
apolloProvider.clients.defaultClient.writeQuery({
query: PathLastCommitQuery,
data: res.data,
variables: {
projectPath,
ref,
path: currentRoutePath,
},
});
})
.catch(() => {})
.finally(() => initLastCommitApp());
} else {
initLastCommitApp();
}
initLastCommitApp();
router.afterEach(({ params: { path } }) => {
setTitle(path, ref, fullName);
......
import filesQuery from '../queries/files.query.graphql';
import filesQuery from 'shared_queries/repository/files.query.graphql';
import getRefMixin from './get_ref';
import projectPathQuery from '../queries/project_path.query.graphql';
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
fragment PageInfo on PageInfo {
__typename
hasNextPage
hasPreviousPage
startCursor
endCursor
}
fragment TreeEntry on Entry {
__typename
id
sha
name
......@@ -16,10 +23,15 @@ query getFiles(
$nextPageCursor: String
) {
project(fullPath: $projectPath) {
__typename
repository {
__typename
tree(path: $path, ref: $ref) {
__typename
trees(first: $pageSize, after: $nextPageCursor) {
__typename
edges {
__typename
node {
...TreeEntry
webPath
......@@ -30,7 +42,9 @@ query getFiles(
}
}
submodules(first: $pageSize, after: $nextPageCursor) {
__typename
edges {
__typename
node {
...TreeEntry
webUrl
......@@ -42,7 +56,9 @@ query getFiles(
}
}
blobs(first: $pageSize, after: $nextPageCursor) {
__typename
edges {
__typename
node {
...TreeEntry
mode
......
query getPermissions($projectPath: ID!) {
project(fullPath: $projectPath) {
__typename
userPermissions {
__typename
pushCode
forkProject
createMergeRequestIn
......
......@@ -25,7 +25,7 @@
};
gl.startup_graphql_calls = gl.startup_graphql_calls.map(call => ({
operationName: call.query.match(/^query (.+)\(/)[1],
...call,
fetchCall: fetch(url, {
...opts,
credentials: 'same-origin',
......
- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1]
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path })
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" })
- add_page_startup_graphql_call('repository/permissions', { projectPath: @project.full_path })
- add_page_startup_graphql_call('repository/files', { nextPageCursor: "", pageSize: 100, projectPath: @project.full_path, ref: current_ref, path: current_route_path || "/"})
- breadcrumb_title _("Repository")
- @content_class = "limit-container-width" unless fluid_layout
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment