summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/lib/apollo/persist_link.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/lib/apollo/persist_link.js')
-rw-r--r--app/assets/javascripts/lib/apollo/persist_link.js141
1 files changed, 141 insertions, 0 deletions
diff --git a/app/assets/javascripts/lib/apollo/persist_link.js b/app/assets/javascripts/lib/apollo/persist_link.js
new file mode 100644
index 00000000000..9d95409d96c
--- /dev/null
+++ b/app/assets/javascripts/lib/apollo/persist_link.js
@@ -0,0 +1,141 @@
+// this file is based on https://github.com/apollographql/apollo-cache-persist/blob/master/examples/react-native/src/utils/persistence/persistLink.ts
+// with some heavy refactororing
+
+/* eslint-disable consistent-return */
+/* eslint-disable @gitlab/require-i18n-strings */
+/* eslint-disable no-param-reassign */
+import { visit } from 'graphql';
+import { ApolloLink } from '@apollo/client/core';
+import traverse from 'traverse';
+
+const extractPersistDirectivePaths = (originalQuery, directive = 'persist') => {
+ const paths = [];
+ const fragmentPaths = {};
+ const fragmentPersistPaths = {};
+
+ const query = visit(originalQuery, {
+ FragmentSpread: ({ name: { value: name } }, _key, _parent, _path, ancestors) => {
+ const root = ancestors.find(
+ ({ kind }) => kind === 'OperationDefinition' || kind === 'FragmentDefinition',
+ );
+
+ const rootKey = root.kind === 'FragmentDefinition' ? root.name.value : '$ROOT';
+
+ const fieldPath = ancestors
+ .filter(({ kind }) => kind === 'Field')
+ .map(({ name: { value } }) => value);
+
+ fragmentPaths[name] = [rootKey].concat(fieldPath);
+ },
+ Directive: ({ name: { value: name } }, _key, _parent, _path, ancestors) => {
+ if (name === directive) {
+ const fieldPath = ancestors
+ .filter(({ kind }) => kind === 'Field')
+ .map(({ name: { value } }) => value);
+
+ const fragmentDefinition = ancestors.find(({ kind }) => kind === 'FragmentDefinition');
+
+ // If we are inside a fragment, we must save the reference.
+ if (fragmentDefinition) {
+ fragmentPersistPaths[fragmentDefinition.name.value] = fieldPath;
+ } else if (fieldPath.length) {
+ paths.push(fieldPath);
+ }
+ return null;
+ }
+ },
+ });
+
+ // In case there are any FragmentDefinition items, we need to combine paths.
+ if (Object.keys(fragmentPersistPaths).length) {
+ visit(originalQuery, {
+ FragmentSpread: ({ name: { value: name } }, _key, _parent, _path, ancestors) => {
+ if (fragmentPersistPaths[name]) {
+ let fieldPath = ancestors
+ .filter(({ kind }) => kind === 'Field')
+ .map(({ name: { value } }) => value);
+
+ fieldPath = fieldPath.concat(fragmentPersistPaths[name]);
+
+ const fragment = name;
+ let parent = fragmentPaths[fragment][0];
+
+ while (parent && parent !== '$ROOT' && fragmentPaths[parent]) {
+ fieldPath = fragmentPaths[parent].slice(1).concat(fieldPath);
+ // eslint-disable-next-line prefer-destructuring
+ parent = fragmentPaths[parent][0];
+ }
+
+ paths.push(fieldPath);
+ }
+ },
+ });
+ }
+
+ return { query, paths };
+};
+
+/**
+ * Given a data result object path, return the equivalent query selection path.
+ *
+ * @param {Array} path The data result object path. i.e.: ["a", 0, "b"]
+ * @return {String} the query selection path. i.e.: "a.b"
+ */
+const toQueryPath = (path) => path.filter((key) => Number.isNaN(Number(key))).join('.');
+
+const attachPersists = (paths, object) => {
+ const queryPaths = paths.map(toQueryPath);
+ function mapperFunction() {
+ if (
+ !this.isRoot &&
+ this.node &&
+ typeof this.node === 'object' &&
+ Object.keys(this.node).length &&
+ !Array.isArray(this.node)
+ ) {
+ const path = toQueryPath(this.path);
+
+ this.update({
+ __persist: Boolean(
+ queryPaths.find(
+ (queryPath) => queryPath.indexOf(path) === 0 || path.indexOf(queryPath) === 0,
+ ),
+ ),
+ ...this.node,
+ });
+ }
+ }
+
+ return traverse(object).map(mapperFunction);
+};
+
+export const getPersistLink = () => {
+ return new ApolloLink((operation, forward) => {
+ const { query, paths } = extractPersistDirectivePaths(operation.query);
+
+ // Noop if not a persist query
+ if (!paths.length) {
+ return forward(operation);
+ }
+
+ // Replace query with one without @persist directives.
+ operation.query = query;
+
+ // Remove requesting __persist fields.
+ operation.query = visit(operation.query, {
+ Field: ({ name: { value: name } }) => {
+ if (name === '__persist') {
+ return null;
+ }
+ },
+ });
+
+ return forward(operation).map((result) => {
+ if (result.data) {
+ result.data = attachPersists(paths, result.data);
+ }
+
+ return result;
+ });
+ });
+};