import {getDefinitionName, mergeDocuments} from 'coral-framework/utils'; import uniq from 'lodash/uniq'; import {gql} from 'react-apollo'; /* * Disable false-positive warning below, as it doesn't work well with how we currently * assemble the queries. * * Warning: fragment with name {fragment name} already exists. * graphql-tag enforces all fragment names across your application to be unique; read more about * this in the docs: http://dev.apollodata.com/core/fragments.html#unique-names */ gql.disableFragmentWarnings(); const getTypeName = (ast) => ast.definitions[0].typeCondition.name.value; class GraphQLRegistry { fragments = {}; mutationOptions = {}; queryOptions = {}; constructor(getSlotFragments) { this.getSlotFragments = getSlotFragments; } /** * Add fragment * * Example: * addFragment('MyFragment', gql` * fragment Plugin_MyFragment on Comment { * body * } * `); */ addFragment(key, document) { const type = getTypeName(document); const name = getDefinitionName(document); if (!(key in this.fragments)) { this.fragments[key] = {type, names: [name], documents: [document]}; } else { if (type !== this.fragments[key].type) { console.error(`Type mismatch ${type} !== ${this.fragments[key].type}`); } this.fragments[key].names.push(name); this.fragments[key].documents.push(document); } } /** * Add mutation options. * * Example: * // state is the current redux state, which is sometimes * // necessary to fill the optimistic response. * addMutationOptions('PostComment', ({variables, state}) => ({ * optimisticResponse: { * CreateComment: { * extra: '', * }, * }, * refetchQueries: [], * updateQueries: { * EmbedQuery: (previous, data) => { * return previous; * }, * }, * update: (proxy, result) => { * }, * }) */ addMutationOptions(key, config) { if (!(key in this.mutationOptions)) { this.mutationOptions[key] = [config]; } else { this.mutationOptions[key].push(config); } } /** * Add query options. * * Example: * addQueryOptions('EmbedQuery', { * reducer: (previousResult, action, variables) => previousResult, * }); */ addQueryOptions(key, config) { if (!(key in this.queryOptions)) { this.queryOptions[key] = [config]; } else { this.queryOptions[key].push(config); } } /** * Add all fragments, mutation options, and query options defined in the object. * * Example: * add({ * fragments: { * CreateCommentResponse: gql` * fragment CoralRandomEmoji_CreateCommentResponse on CreateCommentResponse { * [...] * }`, * }, * mutations: { * // state is the current redux state, which is sometimes * // necessary to fill the optimistic response. * PostComment: ({variables, state}) => ({ * optimisticResponse: { * [...] * }, * refetchQueries: [], * updateQueries: { * EmbedQuery: (previous, data) => { * return previous; * }, * }, * update: (proxy, result) => { * }, * }) * }, * queries: { * EmbedQuery: { * reducer: (previousResult, action, variables) => { * return previousResult; * }, * }, * }, * }); */ add(extension) { Object.keys(extension.fragments || []).forEach((key) => this.addFragment(key, extension.fragments[key])); Object.keys(extension.mutations || []).forEach((key) => this.addMutationOptions(key, extension.mutations[key])); Object.keys(extension.queries || []).forEach((key) => this.addQueryOptions(key, extension.queries[key])); } /** * Get a list of mutation options. */ getMutationOptions(key) { return this.mutationOptions[key] || []; } /** * Get a list of query options. */ getQueryOptions(key) { return this.queryOptions[key] || []; } /** * getSlotFragmentDocument handles `key`s in the form of TalkSlot_SlotName_Resource. * It parses the slot name and the resource and usees the plugin API to assemble * the fragment document. */ getSlotFragmentDocument(key) { const match = key.match(/TalkSlot_(.*)_(.*)/); if (!match) { return ''; } const slot = match[1][0].toLowerCase() + match[1].substr(1); const resource = match[2]; const documents = this.getSlotFragments(slot, resource); if (documents.length === 0) { return ''; } const names = documents.map((d) => getDefinitionName(d)); const typeName = getTypeName(documents[0]); // Assemble arguments for `gql` to call it directly without using template literals. const main = ` fragment ${key} on ${typeName} { ...${names.join('\n...')}\n } `; return mergeDocuments([main, ...documents]); } /** * getRegistryFragmentDocument assembles a fragment document using * all registered fragment under given `key`. */ getRegistryFragmentDocument(key) { if (!(key in this.fragments)) { return ''; } let documents = this.fragments[key].documents; let fields = `...${this.fragments[key].names.join('\n...')}\n`; // Assemble arguments for `gql` to call it directly without using template literals. const main = ` fragment ${key} on ${this.fragments[key].type} { ${fields} } `; return mergeDocuments([main, ...documents]); } /** * getFragmentDocument returns a fragment that assembles all registered * fragments under given `key` or if `key` refers to Slot fragments it will * return the slot fragments specified by this key. */ getFragmentDocument(key) { return this.getRegistryFragmentDocument(key) || this.getSlotFragmentDocument(key); } /** * resolveFragments finds fragment spread names and attachs * the related fragment document to the given root document. */ resolveFragments(document) { if (document.loc.source) { // Remember keys that we have already resolved. const resolvedKeys = []; // Spreads from slots that are empty and need to be removed. // (works around the issue that we don't know the resource type // if we don't have a fragment) const spreadsToBeRemoved = []; // fragments to be attached. const subFragments = []; // body contains the final result. let body = document.loc.source.body; let done = false; while (!done) { done = true; const matchedSubFragments = body.match(/\.\.\.([_a-zA-Z][_a-zA-Z0-9]*)/g) || []; uniq(matchedSubFragments.map((f) => f.replace('...', ''))) .filter((key) => resolvedKeys.indexOf(key) === -1) .forEach((key) => { const doc = this.getFragmentDocument(key); if (doc) { subFragments.push(doc); // We found a new fragment, so we are not done yet. done = false; } else if(key.startsWith('TalkSlot_')) { spreadsToBeRemoved.push(key); } resolvedKeys.push(key); }); body = mergeDocuments([body, ...subFragments]).loc.source.body; } spreadsToBeRemoved.forEach((key) => { const regex = new RegExp(`\\.\\.\\.${key}\n`, 'g'); body = body.replace(regex, ''); }); return gql`${body}`; } else { console.warn('Can only resolve fragments from documents definied using the gql tag.'); } return document; } } /** * createGraphQLRegistry * @param {Function} getSlotFragments A callback with signature `(slot, part) => [documents]` to retrieve slot fragments. * @return {Object} graphql registry */ export function createGraphQLRegistry(getSlotFragments) { return new GraphQLRegistry(getSlotFragments); }