1 | import {
|
2 | ApolloLink,
|
3 | Observable,
|
4 | Operation,
|
5 | NextLink,
|
6 | FetchResult,
|
7 | } from 'apollo-link';
|
8 | import { ApolloCache } from 'apollo-cache';
|
9 | import { DocumentNode } from 'graphql';
|
10 |
|
11 | import { hasDirectives, getMainDefinition } from 'apollo-utilities';
|
12 |
|
13 | import * as Async from 'graphql-anywhere/lib/async';
|
14 | const { graphql } = Async;
|
15 |
|
16 | import { FragmentMatcher } from 'graphql-anywhere';
|
17 |
|
18 | import { removeClientSetsFromDocument, normalizeTypeDefs } from './utils';
|
19 |
|
20 | const capitalizeFirstLetter = str => str.charAt(0).toUpperCase() + str.slice(1);
|
21 |
|
22 | export type ClientStateConfig = {
|
23 | cache?: ApolloCache<any>;
|
24 | resolvers: any | (() => any);
|
25 | defaults?: any;
|
26 | typeDefs?: string | string[] | DocumentNode | DocumentNode[];
|
27 | fragmentMatcher?: FragmentMatcher;
|
28 | };
|
29 |
|
30 | export const withClientState = (
|
31 | clientStateConfig: ClientStateConfig = { resolvers: {}, defaults: {} },
|
32 | ) => {
|
33 | const { defaults, cache, typeDefs, fragmentMatcher } = clientStateConfig;
|
34 | if (cache && defaults) {
|
35 | cache.writeData({ data: defaults });
|
36 | }
|
37 |
|
38 | return new class StateLink extends ApolloLink {
|
39 | public writeDefaults() {
|
40 | if (cache && defaults) {
|
41 | cache.writeData({ data: defaults });
|
42 | }
|
43 | }
|
44 |
|
45 | public request(
|
46 | operation: Operation,
|
47 | forward: NextLink = () => Observable.of({ data: {} }),
|
48 | ): Observable<FetchResult> {
|
49 | if (typeDefs) {
|
50 | const directives = 'directive @client on FIELD';
|
51 |
|
52 | const definition = normalizeTypeDefs(typeDefs);
|
53 |
|
54 | operation.setContext(({ schemas = [] }) => ({
|
55 | schemas: schemas.concat([{ definition, directives }]),
|
56 | }));
|
57 | }
|
58 |
|
59 | const isClient = hasDirectives(['client'], operation.query);
|
60 |
|
61 | if (!isClient) return forward(operation);
|
62 |
|
63 | const resolvers =
|
64 | typeof clientStateConfig.resolvers === 'function'
|
65 | ? clientStateConfig.resolvers()
|
66 | : clientStateConfig.resolvers;
|
67 | const server = removeClientSetsFromDocument(operation.query);
|
68 | const { query } = operation;
|
69 | const type =
|
70 | capitalizeFirstLetter(
|
71 | (getMainDefinition(query) || ({} as any)).operation,
|
72 | ) || 'Query';
|
73 |
|
74 | const resolver = (fieldName, rootValue = {}, args, context, info) => {
|
75 | const { resultKey } = info;
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 | const aliasedNode = rootValue[resultKey];
|
86 | const preAliasingNode = rootValue[fieldName];
|
87 | const aliasNeeded = resultKey !== fieldName;
|
88 |
|
89 |
|
90 | if (aliasedNode !== undefined || preAliasingNode !== undefined) {
|
91 | return aliasedNode || preAliasingNode;
|
92 | }
|
93 |
|
94 |
|
95 | const resolverMap = resolvers[(rootValue as any).__typename || type];
|
96 | if (resolverMap) {
|
97 | const resolve = resolverMap[fieldName];
|
98 | if (resolve) return resolve(rootValue, args, context, info);
|
99 | }
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 | return (
|
106 |
|
107 | (aliasNeeded ? aliasedNode : preAliasingNode) ||
|
108 | (defaults || {})[fieldName]
|
109 | );
|
110 | };
|
111 |
|
112 | if (server) operation.query = server;
|
113 | const obs =
|
114 | server && forward
|
115 | ? forward(operation)
|
116 | : Observable.of({
|
117 | data: {},
|
118 | });
|
119 |
|
120 | return new Observable(observer => {
|
121 |
|
122 |
|
123 |
|
124 | let complete = false;
|
125 | let handlingNext = false;
|
126 | obs.subscribe({
|
127 | next: ({ data, errors }) => {
|
128 | const observerErrorHandler = observer.error.bind(observer);
|
129 | const context = operation.getContext();
|
130 |
|
131 | handlingNext = true;
|
132 |
|
133 |
|
134 | graphql(resolver, query, data, context, operation.variables, {
|
135 | fragmentMatcher,
|
136 | })
|
137 | .then(nextData => {
|
138 | observer.next({
|
139 | data: nextData,
|
140 | errors,
|
141 | });
|
142 | if (complete) {
|
143 | observer.complete();
|
144 | }
|
145 | handlingNext = false;
|
146 | })
|
147 | .catch(observerErrorHandler);
|
148 | },
|
149 | error: observer.error.bind(observer),
|
150 | complete: () => {
|
151 | if (!handlingNext) {
|
152 | observer.complete();
|
153 | }
|
154 | complete = true;
|
155 | },
|
156 | });
|
157 | });
|
158 | }
|
159 | }();
|
160 | };
|