/// import ApolloClient from 'apollo-client'; import { assign, } from 'lodash'; import { GraphQLResult, } from 'graphql'; import { isEqual, noop, forIn, } from 'lodash'; export interface ApolloOptionsQueries { context: any; }; export interface ApolloOptionsMutations { context: any; }; export declare interface ApolloOptions { client: ApolloClient; queries?(opts: ApolloOptionsQueries): any; mutations?(opts: ApolloOptionsMutations): any; }; export function Apollo({ client, queries, mutations, }: ApolloOptions) { const { watchQuery, mutate } = client; // noop by default queries = queries || noop; mutations = mutations || noop; // holds latest values to track changes const lastQueryVariables = {}; const queryHandles = {}; return (sourceTarget: any) => { const target = sourceTarget; const oldHooks = {}; const hooks = { /** * Initialize the component * after Angular initializes the data-bound input properties. */ ngOnInit() { // use component's context handleQueries(this); handleMutations(this); }, /** * Detect and act upon changes that Angular can or won't detect on its own. * Called every change detection run. */ ngDoCheck() { // use component's context handleQueries(this); handleMutations(this); }, /** * Stop all of watchQuery subscriptions */ ngOnDestroy() { unsubscribe(); }, }; // attach hooks forIn(hooks, (hook, name) => { wrapPrototype(name, hook); }); function handleQueries(component: any) { forIn(queries(component), (options, queryName: string) => { if (!equalVariablesOf(queryName, options.variables)) { createQuery(component, queryName, options); } }); } function handleMutations(component: any) { forIn(mutations(component), (method: Function, mutationName: string) => { createMutation(component, mutationName, method); }); } /** * Assings WatchQueryHandle to the component * * @param {any} component Component's context * @param {string} queryName Query's name * @param {Object} options Query's options */ function createQuery(component: any, queryName: string, options) { // save variables so they can be used in futher comparasion lastQueryVariables[queryName] = options.variables; // assign to component's context subscribe(component, queryName, watchQuery(options)); } /** * Assings wrapper of mutation to the component * * @param {any} component Component's context * @param {string} mutationName Mutation's name * @param {Function} method Method returning mutation options * @return {Promise} Mutation result */ function createMutation(component: any, mutationName: string, method: Function) { // assign to component's context component[mutationName] = (...args): Promise => { const { mutation, variables } = method.apply(client, args); return mutate({ mutation, variables }); }; } function subscribe(component: any, queryName: string, obs: any) { component[queryName] = { errors: null, loading: true, }; const setQuery = ({ errors, data = {} }: any) => { component[queryName] = assign({ errors, loading: false, unsubscribe: queryHandles[queryName].unsubscribe, refetch: queryHandles[queryName].refetch, stopPolling: queryHandles[queryName].stopPolling, startPolling: queryHandles[queryName].startPolling, }, data); }; // we don't want to have multiple subscriptions unsubscribe(queryName); queryHandles[queryName] = obs.subscribe({ next: setQuery, error(errors) { setQuery({ errors }); }, }); }; function unsubscribe(queryName?: string) { if (queryHandles) { if (queryName) { // just one if (queryHandles[queryName]) { queryHandles[queryName].unsubscribe(); } } else { // loop through all for (const key in queryHandles) { if (!queryHandles.hasOwnProperty(key)) { continue; } queryHandles[key].unsubscribe(); } } } } /** * Compares current variables with previous ones. * * @param {string} queryName Query's name * @param {any} variables current variables * @return {boolean} comparasion result */ function equalVariablesOf(queryName: string, variables: any): boolean { return lastQueryVariables.hasOwnProperty(queryName) && isEqual(lastQueryVariables[queryName], variables); } /** * Creates a new prototype method which is a wrapper function * that calls new function before old one. * * @param {string} name * @param {Function} func */ function wrapPrototype(name: string, func: Function) { oldHooks[name] = sourceTarget.prototype[name]; // create a wrapper target.prototype[name] = function(...args) { // to call a new prototype method func.apply(this, args); // call the old prototype method if (oldHooks[name]) { oldHooks[name].apply(this, args); } }; } // return decorated target return target; }; }