UNPKG

5.41 kBPlain TextView Raw
1import {
2 ApolloLink,
3 Observable,
4 Operation,
5 NextLink,
6 FetchResult,
7} from 'apollo-link';
8import { ApolloCache } from 'apollo-cache';
9import { DocumentNode } from 'graphql';
10
11import { hasDirectives, getMainDefinition } from 'apollo-utilities';
12
13import * as Async from 'graphql-anywhere/lib/async';
14const { graphql } = Async;
15
16import { FragmentMatcher } from 'graphql-anywhere';
17
18import { removeClientSetsFromDocument, normalizeTypeDefs } from './utils';
19
20const capitalizeFirstLetter = str => str.charAt(0).toUpperCase() + str.slice(1);
21
22export type ClientStateConfig = {
23 cache?: ApolloCache<any>;
24 resolvers: any | (() => any);
25 defaults?: any;
26 typeDefs?: string | string[] | DocumentNode | DocumentNode[];
27 fragmentMatcher?: FragmentMatcher;
28};
29
30export 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 // rootValue[fieldName] is where the data is stored in the "canonical model"
78 // rootValue[info.resultKey] is where the user wants the data to be.
79 // If fieldName != info.resultKey -- then GraphQL Aliases are in play
80 // See also:
81 // - https://github.com/apollographql/apollo-client/tree/master/packages/graphql-anywhere#resolver-info
82 // - https://github.com/apollographql/apollo-link-rest/pull/113
83
84 // Support GraphQL Aliases!
85 const aliasedNode = rootValue[resultKey];
86 const preAliasingNode = rootValue[fieldName];
87 const aliasNeeded = resultKey !== fieldName;
88
89 // If aliasedValue is defined, some other link or server already returned a value
90 if (aliasedNode !== undefined || preAliasingNode !== undefined) {
91 return aliasedNode || preAliasingNode;
92 }
93
94 // Look for the field in the custom resolver map
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 // TODO: the proper thing to do here is throw an error saying to
102 // add `client.onResetStore(link.writeDefaults);`
103 // waiting on https://github.com/apollographql/apollo-client/pull/3010
104
105 return (
106 // Support nested fields
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 // Works around race condition between completion and graphql execution
122 // finishing. If complete is called during the graphql call, we will
123 // miss out on the result, since the observer will have completed
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 //data is from the server and provides the root value to this GraphQL resolution
133 //when there is no resolver, the data is taken from the context
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};