UNPKG

5.69 kBPlain TextView Raw
1/// <reference path="../typings/browser.d.ts" />
2
3import ApolloClient from 'apollo-client';
4
5import {
6 assign,
7} from 'lodash';
8
9import {
10 GraphQLResult,
11} from 'graphql';
12
13import {
14 isEqual,
15 noop,
16 forIn,
17} from 'lodash';
18
19export interface ApolloOptionsQueries {
20 context: any;
21};
22
23export interface ApolloOptionsMutations {
24 context: any;
25};
26
27export declare interface ApolloOptions {
28 client: ApolloClient;
29 queries?(opts: ApolloOptionsQueries): any;
30 mutations?(opts: ApolloOptionsMutations): any;
31};
32
33export function Apollo({
34 client,
35 queries,
36 mutations,
37}: ApolloOptions) {
38 const { watchQuery, mutate } = client;
39 // noop by default
40 queries = queries || noop;
41 mutations = mutations || noop;
42
43 // holds latest values to track changes
44 const lastQueryVariables = {};
45 const queryHandles = {};
46
47 return (sourceTarget: any) => {
48 const target = sourceTarget;
49
50 const oldHooks = {};
51 const hooks = {
52 /**
53 * Initialize the component
54 * after Angular initializes the data-bound input properties.
55 */
56 ngOnInit() {
57 // use component's context
58 handleQueries(this);
59 handleMutations(this);
60 },
61 /**
62 * Detect and act upon changes that Angular can or won't detect on its own.
63 * Called every change detection run.
64 */
65 ngDoCheck() {
66 // use component's context
67 handleQueries(this);
68 handleMutations(this);
69 },
70 /**
71 * Stop all of watchQuery subscriptions
72 */
73 ngOnDestroy() {
74 unsubscribe();
75 },
76 };
77
78 // attach hooks
79 forIn(hooks, (hook, name) => {
80 wrapPrototype(name, hook);
81 });
82
83 function handleQueries(component: any) {
84 forIn(queries(component), (options, queryName: string) => {
85 if (!equalVariablesOf(queryName, options.variables)) {
86 createQuery(component, queryName, options);
87 }
88 });
89 }
90
91 function handleMutations(component: any) {
92 forIn(mutations(component), (method: Function, mutationName: string) => {
93 createMutation(component, mutationName, method);
94 });
95 }
96
97 /**
98 * Assings WatchQueryHandle to the component
99 *
100 * @param {any} component Component's context
101 * @param {string} queryName Query's name
102 * @param {Object} options Query's options
103 */
104 function createQuery(component: any, queryName: string, options) {
105 // save variables so they can be used in futher comparasion
106 lastQueryVariables[queryName] = options.variables;
107 // assign to component's context
108 subscribe(component, queryName, watchQuery(options));
109 }
110
111 /**
112 * Assings wrapper of mutation to the component
113 *
114 * @param {any} component Component's context
115 * @param {string} mutationName Mutation's name
116 * @param {Function} method Method returning mutation options
117 * @return {Promise} Mutation result
118 */
119 function createMutation(component: any, mutationName: string, method: Function) {
120 // assign to component's context
121 component[mutationName] = (...args): Promise<GraphQLResult> => {
122 const { mutation, variables } = method.apply(client, args);
123
124 return mutate({ mutation, variables });
125 };
126 }
127
128 function subscribe(component: any, queryName: string, obs: any) {
129 component[queryName] = {
130 errors: null,
131 loading: true,
132 };
133
134 const setQuery = ({ errors, data = {} }: any) => {
135 component[queryName] = assign({
136 errors,
137 loading: false,
138 unsubscribe: queryHandles[queryName].unsubscribe,
139 refetch: queryHandles[queryName].refetch,
140 stopPolling: queryHandles[queryName].stopPolling,
141 startPolling: queryHandles[queryName].startPolling,
142 }, data);
143 };
144
145 // we don't want to have multiple subscriptions
146 unsubscribe(queryName);
147
148 queryHandles[queryName] = obs.subscribe({
149 next: setQuery,
150 error(errors) {
151 setQuery({ errors });
152 },
153 });
154 };
155
156 function unsubscribe(queryName?: string) {
157 if (queryHandles) {
158 if (queryName) {
159 // just one
160 if (queryHandles[queryName]) {
161 queryHandles[queryName].unsubscribe();
162 }
163 } else {
164 // loop through all
165 for (const key in queryHandles) {
166 if (!queryHandles.hasOwnProperty(key)) {
167 continue;
168 }
169
170 queryHandles[key].unsubscribe();
171 }
172 }
173 }
174 }
175
176 /**
177 * Compares current variables with previous ones.
178 *
179 * @param {string} queryName Query's name
180 * @param {any} variables current variables
181 * @return {boolean} comparasion result
182 */
183 function equalVariablesOf(queryName: string, variables: any): boolean {
184 return lastQueryVariables.hasOwnProperty(queryName) && isEqual(lastQueryVariables[queryName], variables);
185 }
186
187 /**
188 * Creates a new prototype method which is a wrapper function
189 * that calls new function before old one.
190 *
191 * @param {string} name
192 * @param {Function} func
193 */
194 function wrapPrototype(name: string, func: Function) {
195 oldHooks[name] = sourceTarget.prototype[name];
196 // create a wrapper
197 target.prototype[name] = function(...args) {
198 // to call a new prototype method
199 func.apply(this, args);
200
201 // call the old prototype method
202 if (oldHooks[name]) {
203 oldHooks[name].apply(this, args);
204 }
205 };
206 }
207
208 // return decorated target
209 return target;
210 };
211}