1 | import { invariant } from "../../utilities/globals/index.js";
|
2 | import { argumentsObjectFromField, DeepMerger, isNonEmptyArray, isNonNullObject, } from "../../utilities/index.js";
|
3 | import { hasOwn, isArray } from "./helpers.js";
|
4 | // Mapping from JSON-encoded KeySpecifier strings to associated information.
|
5 | var specifierInfoCache = Object.create(null);
|
6 | function 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 | }
|
14 | export 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.
|
53 | export 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 | }
|
117 | export 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 | }
|
135 | export 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 | }
|
156 | function extractKey(object, key) {
|
157 | return object[key];
|
158 | }
|
159 | export 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 | }
|
178 | function 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 |