1 | import { CompilerContext } from "apollo-codegen-core/lib/compiler";
|
2 | import {
|
3 | InlineSelection,
|
4 | Typename,
|
5 | Maybe,
|
6 | List as ListType
|
7 | } from "./intermediates";
|
8 | import {
|
9 | Fragments,
|
10 | ExtendedSelection,
|
11 | ExtendedFieldType,
|
12 | ExtendedFields,
|
13 | ByTypename,
|
14 | ExtendedField,
|
15 | extendedSelectionsAreEqual
|
16 | } from "./extendedIntermediates";
|
17 | import { Map, List, Set } from "immutable";
|
18 |
|
19 | type NormalizableSelections = ByTypename<List<ExtendedFields>>;
|
20 |
|
21 | const normalizableSelectionsFromExtendedFieldType = (
|
22 | type: ExtendedFieldType
|
23 | ): NormalizableSelections => {
|
24 | switch (type.kind) {
|
25 | case "InlineSelection":
|
26 | return NormalizableSelections(type);
|
27 | case "Maybe":
|
28 | return normalizableSelectionsFromExtendedFieldType(type.ofType);
|
29 | case "List":
|
30 | return normalizableSelectionsFromExtendedFieldType(type.ofType);
|
31 | default:
|
32 | return Map();
|
33 | }
|
34 | };
|
35 |
|
36 | const selectTypename = (__typename: string) => (field: ExtendedField) =>
|
37 | field.type.kind == "Typename"
|
38 | ? { type: Typename(Set([__typename])), optional: field.optional }
|
39 | : field;
|
40 |
|
41 | const NormalizableSelections = (
|
42 | selection: ExtendedSelection
|
43 | ): NormalizableSelections =>
|
44 | selection.fields.reduce(
|
45 | (normalizedSelections, fields, __typename) =>
|
46 | fields.reduce(
|
47 | (normalizedSelections, field) =>
|
48 | normalizedSelections.mergeDeep(
|
49 | normalizableSelectionsFromExtendedFieldType(field.type)
|
50 | ),
|
51 | isNormalizable(fields)
|
52 | ? normalizedSelections.mergeDeep(
|
53 | Map({
|
54 | [__typename]: List([fields.map(selectTypename(__typename))])
|
55 | })
|
56 | )
|
57 | : normalizedSelections
|
58 | ),
|
59 | Map()
|
60 | );
|
61 |
|
62 | const normalizeFieldType = (type: ExtendedFieldType): ExtendedFieldType => {
|
63 | switch (type.kind) {
|
64 | case "InlineSelection":
|
65 | return normalizeSelection(type);
|
66 | case "Maybe":
|
67 | return Maybe(normalizeFieldType(type.ofType));
|
68 | case "List":
|
69 | return ListType(normalizeFieldType(type.ofType));
|
70 | default:
|
71 | return type;
|
72 | }
|
73 | };
|
74 |
|
75 | const normalizeField = ({ type, optional }: ExtendedField): ExtendedField => ({
|
76 | type: normalizeFieldType(type),
|
77 | optional
|
78 | });
|
79 |
|
80 | const normalizeSelection = (
|
81 | selection: ExtendedSelection
|
82 | ): ExtendedSelection => ({
|
83 | kind: "InlineSelection",
|
84 | fields: selection.fields.map(
|
85 | fields =>
|
86 | isNormalizable(fields)
|
87 | ? fields.filter(
|
88 | (_, fieldName) => fieldName == "__typename" || fieldName == "id"
|
89 | )
|
90 | : fields.map(normalizeField)
|
91 | )
|
92 | });
|
93 |
|
94 | const returnIfEqual = (
|
95 | __typename: string,
|
96 | fieldName: string,
|
97 | lhs: ExtendedFieldType,
|
98 | rhs: ExtendedFieldType
|
99 | ): ExtendedFieldType => {
|
100 | const differentKindsError = Error(
|
101 | `${lhs.kind} is not the same as ${rhs.kind}`
|
102 | );
|
103 | switch (lhs.kind) {
|
104 | case "Typename":
|
105 | if (rhs.kind != "Typename") {
|
106 | throw differentKindsError;
|
107 | }
|
108 | if (!lhs.possibleTypes.equals(rhs.possibleTypes)) {
|
109 | throw Error(
|
110 | `${lhs.possibleTypes} are not the same as ${rhs.possibleTypes}`
|
111 | );
|
112 | }
|
113 | return rhs;
|
114 | case "Enum":
|
115 | if (rhs.kind != "Enum") {
|
116 | throw differentKindsError;
|
117 | }
|
118 | if (lhs.name != rhs.name) {
|
119 | throw Error(`${lhs.name} is different than ${rhs.name}`);
|
120 | }
|
121 | if (lhs.values != rhs.values) {
|
122 | throw Error(
|
123 | `Possible values ${lhs.values} are not the same as ${rhs.values}`
|
124 | );
|
125 | }
|
126 | return rhs;
|
127 | case "Scalar":
|
128 | if (rhs.kind != "Scalar") {
|
129 | throw differentKindsError;
|
130 | }
|
131 | if (lhs.name != rhs.name) {
|
132 | throw Error(`Types ${lhs.name} and ${rhs.name} are not the same`);
|
133 | }
|
134 | return rhs;
|
135 | case "InlineSelection":
|
136 | if (rhs.kind != "InlineSelection") {
|
137 | throw differentKindsError;
|
138 | }
|
139 | if (!extendedSelectionsAreEqual(lhs, rhs)) {
|
140 | throw Error(
|
141 | `${__typename} can't be normalized because it has conflicting definitions for "${fieldName}" that could lead to runtime bugs. Either alias the field name or make the selections the same.`
|
142 | );
|
143 | }
|
144 | return rhs;
|
145 | case "List":
|
146 | if (rhs.kind != "List") {
|
147 | throw differentKindsError;
|
148 | }
|
149 | return ListType(
|
150 | returnIfEqual(__typename, fieldName, lhs.ofType, rhs.ofType)
|
151 | );
|
152 | case "Maybe":
|
153 | if (rhs.kind != "Maybe") {
|
154 | throw differentKindsError;
|
155 | }
|
156 | return Maybe(
|
157 | returnIfEqual(__typename, fieldName, lhs.ofType, rhs.ofType)
|
158 | );
|
159 | }
|
160 | };
|
161 |
|
162 | const mergeExtendedSelections = (
|
163 | lhs: ExtendedSelection,
|
164 | rhs: ExtendedSelection
|
165 | ): ExtendedSelection => {
|
166 | return {
|
167 | kind: "InlineSelection",
|
168 | fields: lhs.fields.mergeWith(
|
169 | (lhs, rhs, __typename) => mergeFields(__typename)(lhs, rhs),
|
170 | rhs.fields
|
171 | )
|
172 | };
|
173 | };
|
174 |
|
175 | const isNormalizable = (fields: ExtendedFields) =>
|
176 | fields.has("__typename") && fields.has("id");
|
177 |
|
178 | const mergeField = (__typename: string) => (
|
179 | lhs: ExtendedField,
|
180 | rhs: ExtendedField,
|
181 | fieldName: string
|
182 | ): ExtendedField => ({
|
183 | type:
|
184 | lhs.type.kind == "InlineSelection" &&
|
185 | rhs.type.kind == "InlineSelection" &&
|
186 | !lhs.type.fields.some(isNormalizable) &&
|
187 | !rhs.type.fields.some(isNormalizable)
|
188 | ? mergeExtendedSelections(lhs.type, rhs.type)
|
189 | : returnIfEqual(__typename, fieldName, lhs.type, rhs.type),
|
190 | optional: lhs.optional || rhs.optional
|
191 | });
|
192 |
|
193 | const mergeFields = (__typename: string) => (
|
194 | lhs: ExtendedFields,
|
195 | rhs: ExtendedFields
|
196 | ): ExtendedFields => {
|
197 | const intersection = Set(lhs.keySeq()).intersect(Set(rhs.keySeq()));
|
198 | return lhs
|
199 | .mergeWith(mergeField(__typename), rhs)
|
200 | .map(
|
201 | (field, fieldName) =>
|
202 | !intersection.contains(fieldName)
|
203 | ? { type: field.type, optional: true }
|
204 | : field
|
205 | );
|
206 | };
|
207 |
|
208 | export const normalizedTypes = (context: CompilerContext) => {
|
209 | const fragments: Fragments = Object.values(context.fragments).reduce(
|
210 | (fragments, fragment) => ({
|
211 | ...fragments,
|
212 | [fragment.fragmentName]: InlineSelection(fragment.selectionSet)
|
213 | }),
|
214 | {}
|
215 | );
|
216 | const extendedSelections = Object.values(context.operations).map(operation =>
|
217 | ExtendedSelection(InlineSelection(operation.selectionSet), fragments)
|
218 | );
|
219 | const normalizableSelections = extendedSelections.reduce(
|
220 | (normalizedSelections, extendedSelection) =>
|
221 | normalizedSelections.mergeDeep(NormalizableSelections(extendedSelection)),
|
222 | Map() as NormalizableSelections
|
223 | );
|
224 | return normalizableSelections.map((fields, __typename) =>
|
225 | fields.reduce(mergeFields(__typename)).map(normalizeField)
|
226 | );
|
227 | };
|