summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/lib/utils/apollo_startup_js_link.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/lib/utils/apollo_startup_js_link.js')
-rw-r--r--app/assets/javascripts/lib/utils/apollo_startup_js_link.js106
1 files changed, 106 insertions, 0 deletions
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 00000000000..5c120dd532f
--- /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<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),
+ });
+ });
+ });
+ }
+}