1 | import {
|
2 | DocumentNode,
|
3 | SelectionSetNode,
|
4 | FieldNode,
|
5 | FragmentDefinitionNode,
|
6 | InlineFragmentNode,
|
7 | } from 'graphql';
|
8 |
|
9 | import {
|
10 | getMainDefinition,
|
11 | getFragmentDefinitions,
|
12 | createFragmentMap,
|
13 | FragmentMap,
|
14 | DirectiveInfo,
|
15 | shouldInclude,
|
16 | getDirectiveInfoFromField,
|
17 | isField,
|
18 | isInlineFragment,
|
19 | resultKeyNameFromField,
|
20 | argumentsObjectFromField,
|
21 | } from 'apollo-utilities';
|
22 |
|
23 | export type Resolver = (
|
24 | fieldName: string,
|
25 | rootValue: any,
|
26 | args: any,
|
27 | context: any,
|
28 | info: ExecInfo,
|
29 | ) => any;
|
30 |
|
31 | export type VariableMap = { [name: string]: any };
|
32 |
|
33 | export type ResultMapper = (
|
34 | values: { [fieldName: string]: any },
|
35 | rootValue: any,
|
36 | ) => any;
|
37 | export type FragmentMatcher = (
|
38 | rootValue: any,
|
39 | typeCondition: string,
|
40 | context: any,
|
41 | ) => boolean;
|
42 |
|
43 | export type ExecContext = {
|
44 | fragmentMap: FragmentMap;
|
45 | contextValue: any;
|
46 | variableValues: VariableMap;
|
47 | resultMapper: ResultMapper;
|
48 | resolver: Resolver;
|
49 | fragmentMatcher: FragmentMatcher;
|
50 | };
|
51 |
|
52 | export type ExecInfo = {
|
53 | isLeaf: boolean;
|
54 | resultKey: string;
|
55 | directives: DirectiveInfo;
|
56 | field: FieldNode;
|
57 | };
|
58 |
|
59 | export type ExecOptions = {
|
60 | resultMapper?: ResultMapper;
|
61 | fragmentMatcher?: FragmentMatcher;
|
62 | };
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 | export function graphql(
|
81 | resolver: Resolver,
|
82 | document: DocumentNode,
|
83 | rootValue?: any,
|
84 | contextValue?: any,
|
85 | variableValues: VariableMap = {},
|
86 | execOptions: ExecOptions = {},
|
87 | ) {
|
88 | const mainDefinition = getMainDefinition(document);
|
89 |
|
90 | const fragments = getFragmentDefinitions(document);
|
91 | const fragmentMap = createFragmentMap(fragments);
|
92 |
|
93 | const resultMapper = execOptions.resultMapper;
|
94 |
|
95 |
|
96 | const fragmentMatcher = execOptions.fragmentMatcher || (() => true);
|
97 |
|
98 | const execContext: ExecContext = {
|
99 | fragmentMap,
|
100 | contextValue,
|
101 | variableValues,
|
102 | resultMapper,
|
103 | resolver,
|
104 | fragmentMatcher,
|
105 | };
|
106 |
|
107 | return executeSelectionSet(
|
108 | mainDefinition.selectionSet,
|
109 | rootValue,
|
110 | execContext,
|
111 | );
|
112 | }
|
113 |
|
114 | function executeSelectionSet(
|
115 | selectionSet: SelectionSetNode,
|
116 | rootValue: any,
|
117 | execContext: ExecContext,
|
118 | ) {
|
119 | const { fragmentMap, contextValue, variableValues: variables } = execContext;
|
120 |
|
121 | const result = {};
|
122 |
|
123 | selectionSet.selections.forEach(selection => {
|
124 | if (variables && !shouldInclude(selection, variables)) {
|
125 |
|
126 | return;
|
127 | }
|
128 |
|
129 | if (isField(selection)) {
|
130 | const fieldResult = executeField(selection, rootValue, execContext);
|
131 |
|
132 | const resultFieldKey = resultKeyNameFromField(selection);
|
133 |
|
134 | if (fieldResult !== undefined) {
|
135 | if (result[resultFieldKey] === undefined) {
|
136 | result[resultFieldKey] = fieldResult;
|
137 | } else {
|
138 | merge(result[resultFieldKey], fieldResult);
|
139 | }
|
140 | }
|
141 | } else {
|
142 | let fragment: InlineFragmentNode | FragmentDefinitionNode;
|
143 |
|
144 | if (isInlineFragment(selection)) {
|
145 | fragment = selection;
|
146 | } else {
|
147 |
|
148 | fragment = fragmentMap[selection.name.value];
|
149 |
|
150 | if (!fragment) {
|
151 | throw new Error(`No fragment named ${selection.name.value}`);
|
152 | }
|
153 | }
|
154 |
|
155 | const typeCondition = fragment.typeCondition.name.value;
|
156 |
|
157 | if (execContext.fragmentMatcher(rootValue, typeCondition, contextValue)) {
|
158 | const fragmentResult = executeSelectionSet(
|
159 | fragment.selectionSet,
|
160 | rootValue,
|
161 | execContext,
|
162 | );
|
163 |
|
164 | merge(result, fragmentResult);
|
165 | }
|
166 | }
|
167 | });
|
168 |
|
169 | if (execContext.resultMapper) {
|
170 | return execContext.resultMapper(result, rootValue);
|
171 | }
|
172 |
|
173 | return result;
|
174 | }
|
175 |
|
176 | function executeField(
|
177 | field: FieldNode,
|
178 | rootValue: any,
|
179 | execContext: ExecContext,
|
180 | ): any {
|
181 | const { variableValues: variables, contextValue, resolver } = execContext;
|
182 |
|
183 | const fieldName = field.name.value;
|
184 | const args = argumentsObjectFromField(field, variables);
|
185 |
|
186 | const info: ExecInfo = {
|
187 | isLeaf: !field.selectionSet,
|
188 | resultKey: resultKeyNameFromField(field),
|
189 | directives: getDirectiveInfoFromField(field, variables),
|
190 | field,
|
191 | };
|
192 |
|
193 | const result = resolver(fieldName, rootValue, args, contextValue, info);
|
194 |
|
195 | // Handle all scalar types here
|
196 | if (!field.selectionSet) {
|
197 | return result;
|
198 | }
|
199 |
|
200 | // From here down, the field has a selection set, which means it's trying to
|
201 | // query a GraphQLObjectType
|
202 | if (result == null) {
|
203 | // Basically any field in a GraphQL response can be null, or missing
|
204 | return result;
|
205 | }
|
206 |
|
207 | if (Array.isArray(result)) {
|
208 | return executeSubSelectedArray(field, result, execContext);
|
209 | }
|
210 |
|
211 | // Returned value is an object, and the query has a sub-selection. Recurse.
|
212 | return executeSelectionSet(field.selectionSet, result, execContext);
|
213 | }
|
214 |
|
215 | function executeSubSelectedArray(field, result, execContext) {
|
216 | return result.map(item => {
|
217 |
|
218 | if (item === null) {
|
219 | return null;
|
220 | }
|
221 |
|
222 |
|
223 | if (Array.isArray(item)) {
|
224 | return executeSubSelectedArray(field, item, execContext);
|
225 | }
|
226 |
|
227 |
|
228 | return executeSelectionSet(field.selectionSet, item, execContext);
|
229 | });
|
230 | }
|
231 |
|
232 | const hasOwn = Object.prototype.hasOwnProperty;
|
233 |
|
234 | export function merge(dest, src) {
|
235 | if (src !== null && typeof src === 'object') {
|
236 | Object.keys(src).forEach(key => {
|
237 | const srcVal = src[key];
|
238 | if (!hasOwn.call(dest, key)) {
|
239 | dest[key] = srcVal;
|
240 | } else {
|
241 | merge(dest[key], srcVal);
|
242 | }
|
243 | });
|
244 | }
|
245 | }
|
246 |
|
\ | No newline at end of file |