UNPKG

10.4 kBJavaScriptView Raw
1import { invariant } from "../../utilities/globals/index.js";
2import { argumentsObjectFromField, DeepMerger, isNonEmptyArray, isNonNullObject, } from "../../utilities/index.js";
3import { hasOwn, isArray } from "./helpers.js";
4// Mapping from JSON-encoded KeySpecifier strings to associated information.
5var specifierInfoCache = Object.create(null);
6function lookupSpecifierInfo(spec) {
7 // It's safe to encode KeySpecifier arrays with JSON.stringify, since they're
8 // just arrays of strings or nested KeySpecifier arrays, and the order of the
9 // array elements is important (and suitably preserved by JSON.stringify).
10 var cacheKey = JSON.stringify(spec);
11 return (specifierInfoCache[cacheKey] ||
12 (specifierInfoCache[cacheKey] = Object.create(null)));
13}
14export function keyFieldsFnFromSpecifier(specifier) {
15 var info = lookupSpecifierInfo(specifier);
16 return (info.keyFieldsFn || (info.keyFieldsFn = function (object, context) {
17 var extract = function (from, key) {
18 return context.readField(key, from);
19 };
20 var keyObject = (context.keyObject = collectSpecifierPaths(specifier, function (schemaKeyPath) {
21 var extracted = extractKeyPath(context.storeObject, schemaKeyPath,
22 // Using context.readField to extract paths from context.storeObject
23 // allows the extraction to see through Reference objects and respect
24 // custom read functions.
25 extract);
26 if (extracted === void 0 &&
27 object !== context.storeObject &&
28 hasOwn.call(object, schemaKeyPath[0])) {
29 // If context.storeObject fails to provide a value for the requested
30 // path, fall back to the raw result object, if it has a top-level key
31 // matching the first key in the path (schemaKeyPath[0]). This allows
32 // key fields included in the written data to be saved in the cache
33 // even if they are not selected explicitly in context.selectionSet.
34 // Not being mentioned by context.selectionSet is convenient here,
35 // since it means these extra fields cannot be affected by field
36 // aliasing, which is why we can use extractKey instead of
37 // context.readField for this extraction.
38 extracted = extractKeyPath(object, schemaKeyPath, extractKey);
39 }
40 invariant(extracted !== void 0, 4, schemaKeyPath.join("."), object);
41 return extracted;
42 }));
43 return "".concat(context.typename, ":").concat(JSON.stringify(keyObject));
44 }));
45}
46// The keyArgs extraction process is roughly analogous to keyFields extraction,
47// but there are no aliases involved, missing fields are tolerated (by merely
48// omitting them from the key), and drawing from field.directives or variables
49// is allowed (in addition to drawing from the field's arguments object).
50// Concretely, these differences mean passing a different key path extractor
51// function to collectSpecifierPaths, reusing the shared extractKeyPath helper
52// wherever possible.
53export function keyArgsFnFromSpecifier(specifier) {
54 var info = lookupSpecifierInfo(specifier);
55 return (info.keyArgsFn ||
56 (info.keyArgsFn = function (args, _a) {
57 var field = _a.field, variables = _a.variables, fieldName = _a.fieldName;
58 var collected = collectSpecifierPaths(specifier, function (keyPath) {
59 var firstKey = keyPath[0];
60 var firstChar = firstKey.charAt(0);
61 if (firstChar === "@") {
62 if (field && isNonEmptyArray(field.directives)) {
63 var directiveName_1 = firstKey.slice(1);
64 // If the directive appears multiple times, only the first
65 // occurrence's arguments will be used. TODO Allow repetition?
66 // TODO Cache this work somehow, a la aliasMap?
67 var d = field.directives.find(function (d) { return d.name.value === directiveName_1; });
68 // Fortunately argumentsObjectFromField works for DirectiveNode!
69 var directiveArgs = d && argumentsObjectFromField(d, variables);
70 // For directives without arguments (d defined, but directiveArgs ===
71 // null), the presence or absence of the directive still counts as
72 // part of the field key, so we return null in those cases. If no
73 // directive with this name was found for this field (d undefined and
74 // thus directiveArgs undefined), we return undefined, which causes
75 // this value to be omitted from the key object returned by
76 // collectSpecifierPaths.
77 return (directiveArgs &&
78 extractKeyPath(directiveArgs,
79 // If keyPath.length === 1, this code calls extractKeyPath with an
80 // empty path, which works because it uses directiveArgs as the
81 // extracted value.
82 keyPath.slice(1)));
83 }
84 // If the key started with @ but there was no corresponding directive,
85 // we want to omit this value from the key object, not fall through to
86 // treating @whatever as a normal argument name.
87 return;
88 }
89 if (firstChar === "$") {
90 var variableName = firstKey.slice(1);
91 if (variables && hasOwn.call(variables, variableName)) {
92 var varKeyPath = keyPath.slice(0);
93 varKeyPath[0] = variableName;
94 return extractKeyPath(variables, varKeyPath);
95 }
96 // If the key started with $ but there was no corresponding variable, we
97 // want to omit this value from the key object, not fall through to
98 // treating $whatever as a normal argument name.
99 return;
100 }
101 if (args) {
102 return extractKeyPath(args, keyPath);
103 }
104 });
105 var suffix = JSON.stringify(collected);
106 // If no arguments were passed to this field, and it didn't have any other
107 // field key contributions from directives or variables, hide the empty
108 // :{} suffix from the field key. However, a field passed no arguments can
109 // still end up with a non-empty :{...} suffix if its key configuration
110 // refers to directives or variables.
111 if (args || suffix !== "{}") {
112 fieldName += ":" + suffix;
113 }
114 return fieldName;
115 }));
116}
117export function collectSpecifierPaths(specifier, extractor) {
118 // For each path specified by specifier, invoke the extractor, and repeatedly
119 // merge the results together, with appropriate ancestor context.
120 var merger = new DeepMerger();
121 return getSpecifierPaths(specifier).reduce(function (collected, path) {
122 var _a;
123 var toMerge = extractor(path);
124 if (toMerge !== void 0) {
125 // This path is not expected to contain array indexes, so the toMerge
126 // reconstruction will not contain arrays. TODO Fix this?
127 for (var i = path.length - 1; i >= 0; --i) {
128 toMerge = (_a = {}, _a[path[i]] = toMerge, _a);
129 }
130 collected = merger.merge(collected, toMerge);
131 }
132 return collected;
133 }, Object.create(null));
134}
135export function getSpecifierPaths(spec) {
136 var info = lookupSpecifierInfo(spec);
137 if (!info.paths) {
138 var paths_1 = (info.paths = []);
139 var currentPath_1 = [];
140 spec.forEach(function (s, i) {
141 if (isArray(s)) {
142 getSpecifierPaths(s).forEach(function (p) { return paths_1.push(currentPath_1.concat(p)); });
143 currentPath_1.length = 0;
144 }
145 else {
146 currentPath_1.push(s);
147 if (!isArray(spec[i + 1])) {
148 paths_1.push(currentPath_1.slice(0));
149 currentPath_1.length = 0;
150 }
151 }
152 });
153 }
154 return info.paths;
155}
156function extractKey(object, key) {
157 return object[key];
158}
159export function extractKeyPath(object, path, extract) {
160 // For each key in path, extract the corresponding child property from obj,
161 // flattening arrays if encountered (uncommon for keyFields and keyArgs, but
162 // possible). The final result of path.reduce is normalized so unexpected leaf
163 // objects have their keys safely sorted. That final result is difficult to
164 // type as anything other than any. You're welcome to try to improve the
165 // return type, but keep in mind extractKeyPath is not a public function
166 // (exported only for testing), so the effort may not be worthwhile unless the
167 // limited set of actual callers (see above) pass arguments that TypeScript
168 // can statically type. If we know only that path is some array of strings
169 // (and not, say, a specific tuple of statically known strings), any (or
170 // possibly unknown) is the honest answer.
171 extract = extract || extractKey;
172 return normalize(path.reduce(function reducer(obj, key) {
173 return isArray(obj) ?
174 obj.map(function (child) { return reducer(child, key); })
175 : obj && extract(obj, key);
176 }, object));
177}
178function normalize(value) {
179 // Usually the extracted value will be a scalar value, since most primary
180 // key fields are scalar, but just in case we get an object or an array, we
181 // need to do some normalization of the order of (nested) keys.
182 if (isNonNullObject(value)) {
183 if (isArray(value)) {
184 return value.map(normalize);
185 }
186 return collectSpecifierPaths(Object.keys(value).sort(), function (path) {
187 return extractKeyPath(value, path);
188 });
189 }
190 return value;
191}
192//# sourceMappingURL=key-extractor.js.map
\No newline at end of file