UNPKG

6.31 kBPlain TextView Raw
1import ApolloClient from 'apollo-client';
2
3import {
4 GraphQLResult,
5} from 'graphql';
6
7import {
8 isEqual,
9 forIn,
10 assign,
11} from 'lodash';
12
13export interface ApolloOptions {
14 client: ApolloClient;
15 queries?: (component?: any) => any;
16 mutations?: (component?: any) => any;
17}
18
19interface ApolloHandleOptions extends ApolloOptions {
20 component: any;
21}
22
23class ApolloHandle {
24 private lastQueryVariables: Object = {};
25 private queryHandles: Object = {};
26
27 private component;
28 private client;
29 private queries;
30 private mutations;
31
32
33 public constructor({
34 component,
35 client,
36 queries,
37 mutations,
38 }: ApolloHandleOptions) {
39 this.component = component;
40 this.client = client;
41 this.queries = queries;
42 this.mutations = mutations;
43 }
44
45 public handleQueries(): void {
46 if (!this.queries) {
47 return;
48 }
49
50 forIn(this.queries(this.component), (options, queryName: string) => {
51 if (this.hasVariablesChanged(queryName, options.variables)) {
52 this.createQuery(queryName, options);
53 }
54 });
55 }
56
57 public handleMutations(): void {
58 if (!this.mutations) {
59 return;
60 }
61
62 forIn(this.mutations(this.component), (method: Function, mutationName: string) => {
63 this.createMutation(mutationName, method);
64 });
65 }
66
67 public unsubscribe(queryName?: string): void {
68 const allQueries = this.getAllQueries();
69
70 if (allQueries) {
71 if (queryName) {
72 const single = allQueries[queryName];
73 // just one
74 if (single) {
75 single.unsubscribe();
76 }
77 } else {
78 // loop through all
79 for (const name in allQueries) {
80 if (allQueries.hasOwnProperty(name)) {
81 allQueries[name].unsubscribe();
82 }
83 }
84 }
85 }
86 }
87
88 private setQuery(name, handle): void {
89 this.queryHandles[name] = handle;
90 }
91
92 private getQuery(name) {
93 return this.queryHandles[name];
94 }
95
96 private getAllQueries() {
97 return this.queryHandles;
98 }
99
100 /**
101 * Saves variables so they can be used in futher comparasion
102 * @param {string} queryName Query's name
103 * @param {any} variables used variables
104 */
105 private saveVariables(queryName: string, variables: any): void {
106 this.lastQueryVariables[queryName] = variables;
107 }
108
109 /**
110 * Compares current variables with previous ones.
111 * @param {string} queryName Query's name
112 * @param {any} variables current variables
113 * @return {boolean} comparasion result
114 */
115 private hasVariablesChanged(queryName: string, variables: any): boolean {
116 return !(
117 this.lastQueryVariables.hasOwnProperty(queryName)
118 && isEqual(this.lastQueryVariables[queryName], variables)
119 );
120 }
121
122 private hasDataChanged(queryName: string, data: any): boolean {
123 let changed = false;
124
125 forIn(data, (value, key) => {
126 if (!isEqual(this.component[queryName][key], value)) {
127 changed = true;
128 return;
129 }
130 });
131
132 return changed;
133 }
134
135 private createQuery(queryName: string, options: any) {
136 // save variables so they can be used in futher comparasion
137 this.saveVariables(queryName, options.variables);
138 // assign to component's context
139 this.subscribe(queryName, this.client.watchQuery(options));
140 }
141
142 private createMutation(mutationName: string, method: Function) {
143 // assign to component's context
144 this.component[mutationName] = (...args): Promise<GraphQLResult> => {
145 const { mutation, variables } = method.apply(this.client, args);
146
147 return this.client.mutate({ mutation, variables });
148 };
149 }
150
151 private subscribe(queryName: string, obs: any) {
152 this.component[queryName] = {
153 errors: null,
154 loading: true,
155 };
156
157 const setQuery = ({ errors, data = {} }: any) => {
158 const changed = this.hasDataChanged(queryName, data);
159
160 assign(this.component[queryName], {
161 errors,
162 loading: false,
163 unsubscribe: () => this.getQuery(queryName).unsubscribe(),
164 refetch: (...args) => this.getQuery(queryName).refetch(...args),
165 stopPolling: () => this.getQuery(queryName).stopPolling(),
166 startPolling: (...args) => this.getQuery(queryName).startPolling(...args),
167 }, changed ? data : {});
168 };
169
170 // we don't want to have multiple subscriptions
171 this.unsubscribe(queryName);
172
173 this.setQuery(queryName, obs.subscribe({
174 next: setQuery,
175 error(errors) {
176 setQuery({ errors });
177 },
178 }));
179 }
180}
181
182export function Apollo({
183 client,
184 queries,
185 mutations,
186}: ApolloOptions) {
187 const apolloProp = '__apolloHandle';
188
189 return (sourceTarget: any) => {
190 const target = sourceTarget;
191
192 const oldHooks = {};
193 const hooks = {
194 /**
195 * Initialize the component
196 * after Angular initializes the data-bound input properties.
197 */
198 ngOnInit() {
199 if (!this[apolloProp]) {
200 this[apolloProp] = new ApolloHandle({
201 component: this,
202 client,
203 queries,
204 mutations,
205 });
206 }
207
208 this[apolloProp].handleQueries();
209 this[apolloProp].handleMutations();
210 },
211 /**
212 * Detect and act upon changes that Angular can or won't detect on its own.
213 * Called every change detection run.
214 */
215 ngDoCheck() {
216 this[apolloProp].handleQueries();
217 this[apolloProp].handleMutations();
218 },
219 /**
220 * Stop all of watchQuery subscriptions
221 */
222 ngOnDestroy() {
223 this[apolloProp].unsubscribe();
224 },
225 };
226
227 // attach hooks
228 forIn(hooks, (hook, name) => {
229 wrapPrototype(name, hook);
230 });
231
232 /**
233 * Creates a new prototype method which is a wrapper function
234 * that calls new function before old one.
235 *
236 * @param {string} name
237 * @param {Function} func
238 */
239 function wrapPrototype(name: string, func: Function) {
240 oldHooks[name] = sourceTarget.prototype[name];
241 // create a wrapper
242 target.prototype[name] = function(...args) {
243 // to call a new prototype method
244 func.apply(this, args);
245
246 // call the old prototype method
247 if (oldHooks[name]) {
248 oldHooks[name].apply(this, args);
249 }
250 };
251 }
252
253 // return decorated target
254 return target;
255 };
256}