UNPKG

19.6 kBJavaScriptView Raw
1import { __assign, __awaiter, __generator } from "tslib";
2import { invariant } from "../utilities/globals/index.js";
3import { visit, BREAK, isSelectionNode } from "graphql";
4import { argumentsObjectFromField, buildQueryFromSelectionSet, createFragmentMap, getFragmentDefinitions, getMainDefinition, hasDirectives, isField, isInlineFragment, mergeDeep, mergeDeepArray, removeClientSetsFromDocument, resultKeyNameFromField, shouldInclude, } from "../utilities/index.js";
5import { cacheSlot } from "../cache/index.js";
6var LocalState = /** @class */ (function () {
7 function LocalState(_a) {
8 var cache = _a.cache, client = _a.client, resolvers = _a.resolvers, fragmentMatcher = _a.fragmentMatcher;
9 this.selectionsToResolveCache = new WeakMap();
10 this.cache = cache;
11 if (client) {
12 this.client = client;
13 }
14 if (resolvers) {
15 this.addResolvers(resolvers);
16 }
17 if (fragmentMatcher) {
18 this.setFragmentMatcher(fragmentMatcher);
19 }
20 }
21 LocalState.prototype.addResolvers = function (resolvers) {
22 var _this = this;
23 this.resolvers = this.resolvers || {};
24 if (Array.isArray(resolvers)) {
25 resolvers.forEach(function (resolverGroup) {
26 _this.resolvers = mergeDeep(_this.resolvers, resolverGroup);
27 });
28 }
29 else {
30 this.resolvers = mergeDeep(this.resolvers, resolvers);
31 }
32 };
33 LocalState.prototype.setResolvers = function (resolvers) {
34 this.resolvers = {};
35 this.addResolvers(resolvers);
36 };
37 LocalState.prototype.getResolvers = function () {
38 return this.resolvers || {};
39 };
40 // Run local client resolvers against the incoming query and remote data.
41 // Locally resolved field values are merged with the incoming remote data,
42 // and returned. Note that locally resolved fields will overwrite
43 // remote data using the same field name.
44 LocalState.prototype.runResolvers = function (_a) {
45 var document = _a.document, remoteResult = _a.remoteResult, context = _a.context, variables = _a.variables, _b = _a.onlyRunForcedResolvers, onlyRunForcedResolvers = _b === void 0 ? false : _b;
46 return __awaiter(this, void 0, void 0, function () {
47 return __generator(this, function (_c) {
48 if (document) {
49 return [2 /*return*/, this.resolveDocument(document, remoteResult.data, context, variables, this.fragmentMatcher, onlyRunForcedResolvers).then(function (localResult) { return (__assign(__assign({}, remoteResult), { data: localResult.result })); })];
50 }
51 return [2 /*return*/, remoteResult];
52 });
53 });
54 };
55 LocalState.prototype.setFragmentMatcher = function (fragmentMatcher) {
56 this.fragmentMatcher = fragmentMatcher;
57 };
58 LocalState.prototype.getFragmentMatcher = function () {
59 return this.fragmentMatcher;
60 };
61 // Client queries contain everything in the incoming document (if a @client
62 // directive is found).
63 LocalState.prototype.clientQuery = function (document) {
64 if (hasDirectives(["client"], document)) {
65 if (this.resolvers) {
66 return document;
67 }
68 }
69 return null;
70 };
71 // Server queries are stripped of all @client based selection sets.
72 LocalState.prototype.serverQuery = function (document) {
73 return removeClientSetsFromDocument(document);
74 };
75 LocalState.prototype.prepareContext = function (context) {
76 var cache = this.cache;
77 return __assign(__assign({}, context), { cache: cache,
78 // Getting an entry's cache key is useful for local state resolvers.
79 getCacheKey: function (obj) {
80 return cache.identify(obj);
81 } });
82 };
83 // To support `@client @export(as: "someVar")` syntax, we'll first resolve
84 // @client @export fields locally, then pass the resolved values back to be
85 // used alongside the original operation variables.
86 LocalState.prototype.addExportedVariables = function (document, variables, context) {
87 if (variables === void 0) { variables = {}; }
88 if (context === void 0) { context = {}; }
89 return __awaiter(this, void 0, void 0, function () {
90 return __generator(this, function (_a) {
91 if (document) {
92 return [2 /*return*/, this.resolveDocument(document, this.buildRootValueFromCache(document, variables) || {}, this.prepareContext(context), variables).then(function (data) { return (__assign(__assign({}, variables), data.exportedVariables)); })];
93 }
94 return [2 /*return*/, __assign({}, variables)];
95 });
96 });
97 };
98 LocalState.prototype.shouldForceResolvers = function (document) {
99 var forceResolvers = false;
100 visit(document, {
101 Directive: {
102 enter: function (node) {
103 if (node.name.value === "client" && node.arguments) {
104 forceResolvers = node.arguments.some(function (arg) {
105 return arg.name.value === "always" &&
106 arg.value.kind === "BooleanValue" &&
107 arg.value.value === true;
108 });
109 if (forceResolvers) {
110 return BREAK;
111 }
112 }
113 },
114 },
115 });
116 return forceResolvers;
117 };
118 // Query the cache and return matching data.
119 LocalState.prototype.buildRootValueFromCache = function (document, variables) {
120 return this.cache.diff({
121 query: buildQueryFromSelectionSet(document),
122 variables: variables,
123 returnPartialData: true,
124 optimistic: false,
125 }).result;
126 };
127 LocalState.prototype.resolveDocument = function (document, rootValue, context, variables, fragmentMatcher, onlyRunForcedResolvers) {
128 if (context === void 0) { context = {}; }
129 if (variables === void 0) { variables = {}; }
130 if (fragmentMatcher === void 0) { fragmentMatcher = function () { return true; }; }
131 if (onlyRunForcedResolvers === void 0) { onlyRunForcedResolvers = false; }
132 return __awaiter(this, void 0, void 0, function () {
133 var mainDefinition, fragments, fragmentMap, selectionsToResolve, definitionOperation, defaultOperationType, _a, cache, client, execContext, isClientFieldDescendant;
134 return __generator(this, function (_b) {
135 mainDefinition = getMainDefinition(document);
136 fragments = getFragmentDefinitions(document);
137 fragmentMap = createFragmentMap(fragments);
138 selectionsToResolve = this.collectSelectionsToResolve(mainDefinition, fragmentMap);
139 definitionOperation = mainDefinition.operation;
140 defaultOperationType = definitionOperation ?
141 definitionOperation.charAt(0).toUpperCase() +
142 definitionOperation.slice(1)
143 : "Query";
144 _a = this, cache = _a.cache, client = _a.client;
145 execContext = {
146 fragmentMap: fragmentMap,
147 context: __assign(__assign({}, context), { cache: cache, client: client }),
148 variables: variables,
149 fragmentMatcher: fragmentMatcher,
150 defaultOperationType: defaultOperationType,
151 exportedVariables: {},
152 selectionsToResolve: selectionsToResolve,
153 onlyRunForcedResolvers: onlyRunForcedResolvers,
154 };
155 isClientFieldDescendant = false;
156 return [2 /*return*/, this.resolveSelectionSet(mainDefinition.selectionSet, isClientFieldDescendant, rootValue, execContext).then(function (result) { return ({
157 result: result,
158 exportedVariables: execContext.exportedVariables,
159 }); })];
160 });
161 });
162 };
163 LocalState.prototype.resolveSelectionSet = function (selectionSet, isClientFieldDescendant, rootValue, execContext) {
164 return __awaiter(this, void 0, void 0, function () {
165 var fragmentMap, context, variables, resultsToMerge, execute;
166 var _this = this;
167 return __generator(this, function (_a) {
168 fragmentMap = execContext.fragmentMap, context = execContext.context, variables = execContext.variables;
169 resultsToMerge = [rootValue];
170 execute = function (selection) { return __awaiter(_this, void 0, void 0, function () {
171 var fragment, typeCondition;
172 return __generator(this, function (_a) {
173 if (!isClientFieldDescendant &&
174 !execContext.selectionsToResolve.has(selection)) {
175 // Skip selections without @client directives
176 // (still processing if one of the ancestors or one of the child fields has @client directive)
177 return [2 /*return*/];
178 }
179 if (!shouldInclude(selection, variables)) {
180 // Skip this entirely.
181 return [2 /*return*/];
182 }
183 if (isField(selection)) {
184 return [2 /*return*/, this.resolveField(selection, isClientFieldDescendant, rootValue, execContext).then(function (fieldResult) {
185 var _a;
186 if (typeof fieldResult !== "undefined") {
187 resultsToMerge.push((_a = {},
188 _a[resultKeyNameFromField(selection)] = fieldResult,
189 _a));
190 }
191 })];
192 }
193 if (isInlineFragment(selection)) {
194 fragment = selection;
195 }
196 else {
197 // This is a named fragment.
198 fragment = fragmentMap[selection.name.value];
199 invariant(fragment, 18, selection.name.value);
200 }
201 if (fragment && fragment.typeCondition) {
202 typeCondition = fragment.typeCondition.name.value;
203 if (execContext.fragmentMatcher(rootValue, typeCondition, context)) {
204 return [2 /*return*/, this.resolveSelectionSet(fragment.selectionSet, isClientFieldDescendant, rootValue, execContext).then(function (fragmentResult) {
205 resultsToMerge.push(fragmentResult);
206 })];
207 }
208 }
209 return [2 /*return*/];
210 });
211 }); };
212 return [2 /*return*/, Promise.all(selectionSet.selections.map(execute)).then(function () {
213 return mergeDeepArray(resultsToMerge);
214 })];
215 });
216 });
217 };
218 LocalState.prototype.resolveField = function (field, isClientFieldDescendant, rootValue, execContext) {
219 return __awaiter(this, void 0, void 0, function () {
220 var variables, fieldName, aliasedFieldName, aliasUsed, defaultResult, resultPromise, resolverType, resolverMap, resolve;
221 var _this = this;
222 return __generator(this, function (_a) {
223 if (!rootValue) {
224 return [2 /*return*/, null];
225 }
226 variables = execContext.variables;
227 fieldName = field.name.value;
228 aliasedFieldName = resultKeyNameFromField(field);
229 aliasUsed = fieldName !== aliasedFieldName;
230 defaultResult = rootValue[aliasedFieldName] || rootValue[fieldName];
231 resultPromise = Promise.resolve(defaultResult);
232 // Usually all local resolvers are run when passing through here, but
233 // if we've specifically identified that we only want to run forced
234 // resolvers (that is, resolvers for fields marked with
235 // `@client(always: true)`), then we'll skip running non-forced resolvers.
236 if (!execContext.onlyRunForcedResolvers ||
237 this.shouldForceResolvers(field)) {
238 resolverType = rootValue.__typename || execContext.defaultOperationType;
239 resolverMap = this.resolvers && this.resolvers[resolverType];
240 if (resolverMap) {
241 resolve = resolverMap[aliasUsed ? fieldName : aliasedFieldName];
242 if (resolve) {
243 resultPromise = Promise.resolve(
244 // In case the resolve function accesses reactive variables,
245 // set cacheSlot to the current cache instance.
246 cacheSlot.withValue(this.cache, resolve, [
247 rootValue,
248 argumentsObjectFromField(field, variables),
249 execContext.context,
250 { field: field, fragmentMap: execContext.fragmentMap },
251 ]));
252 }
253 }
254 }
255 return [2 /*return*/, resultPromise.then(function (result) {
256 var _a, _b;
257 if (result === void 0) { result = defaultResult; }
258 // If an @export directive is associated with the current field, store
259 // the `as` export variable name and current result for later use.
260 if (field.directives) {
261 field.directives.forEach(function (directive) {
262 if (directive.name.value === "export" && directive.arguments) {
263 directive.arguments.forEach(function (arg) {
264 if (arg.name.value === "as" && arg.value.kind === "StringValue") {
265 execContext.exportedVariables[arg.value.value] = result;
266 }
267 });
268 }
269 });
270 }
271 // Handle all scalar types here.
272 if (!field.selectionSet) {
273 return result;
274 }
275 // From here down, the field has a selection set, which means it's trying
276 // to query a GraphQLObjectType.
277 if (result == null) {
278 // Basically any field in a GraphQL response can be null, or missing
279 return result;
280 }
281 var isClientField = (_b = (_a = field.directives) === null || _a === void 0 ? void 0 : _a.some(function (d) { return d.name.value === "client"; })) !== null && _b !== void 0 ? _b : false;
282 if (Array.isArray(result)) {
283 return _this.resolveSubSelectedArray(field, isClientFieldDescendant || isClientField, result, execContext);
284 }
285 // Returned value is an object, and the query has a sub-selection. Recurse.
286 if (field.selectionSet) {
287 return _this.resolveSelectionSet(field.selectionSet, isClientFieldDescendant || isClientField, result, execContext);
288 }
289 })];
290 });
291 });
292 };
293 LocalState.prototype.resolveSubSelectedArray = function (field, isClientFieldDescendant, result, execContext) {
294 var _this = this;
295 return Promise.all(result.map(function (item) {
296 if (item === null) {
297 return null;
298 }
299 // This is a nested array, recurse.
300 if (Array.isArray(item)) {
301 return _this.resolveSubSelectedArray(field, isClientFieldDescendant, item, execContext);
302 }
303 // This is an object, run the selection set on it.
304 if (field.selectionSet) {
305 return _this.resolveSelectionSet(field.selectionSet, isClientFieldDescendant, item, execContext);
306 }
307 }));
308 };
309 // Collect selection nodes on paths from document root down to all @client directives.
310 // This function takes into account transitive fragment spreads.
311 // Complexity equals to a single `visit` over the full document.
312 LocalState.prototype.collectSelectionsToResolve = function (mainDefinition, fragmentMap) {
313 var isSingleASTNode = function (node) { return !Array.isArray(node); };
314 var selectionsToResolveCache = this.selectionsToResolveCache;
315 function collectByDefinition(definitionNode) {
316 if (!selectionsToResolveCache.has(definitionNode)) {
317 var matches_1 = new Set();
318 selectionsToResolveCache.set(definitionNode, matches_1);
319 visit(definitionNode, {
320 Directive: function (node, _, __, ___, ancestors) {
321 if (node.name.value === "client") {
322 ancestors.forEach(function (node) {
323 if (isSingleASTNode(node) && isSelectionNode(node)) {
324 matches_1.add(node);
325 }
326 });
327 }
328 },
329 FragmentSpread: function (spread, _, __, ___, ancestors) {
330 var fragment = fragmentMap[spread.name.value];
331 invariant(fragment, 19, spread.name.value);
332 var fragmentSelections = collectByDefinition(fragment);
333 if (fragmentSelections.size > 0) {
334 // Fragment for this spread contains @client directive (either directly or transitively)
335 // Collect selection nodes on paths from the root down to fields with the @client directive
336 ancestors.forEach(function (node) {
337 if (isSingleASTNode(node) && isSelectionNode(node)) {
338 matches_1.add(node);
339 }
340 });
341 matches_1.add(spread);
342 fragmentSelections.forEach(function (selection) {
343 matches_1.add(selection);
344 });
345 }
346 },
347 });
348 }
349 return selectionsToResolveCache.get(definitionNode);
350 }
351 return collectByDefinition(mainDefinition);
352 };
353 return LocalState;
354}());
355export { LocalState };
356//# sourceMappingURL=LocalState.js.map
\No newline at end of file