mirror of
https://github.com/wassname/talk.git
synced 2026-06-29 23:43:19 +08:00
279 lines
7.8 KiB
JavaScript
279 lines
7.8 KiB
JavaScript
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);
|
|
}
|