1 | import { getNamedType, isEnumType, isInterfaceType, isListType, isNonNullType, isObjectType, isScalarType, isUnionType, Kind, } from 'graphql';
|
2 | import { getDefinedRootType, getRootTypeNames } from './rootTypes.js';
|
3 | let operationVariables = [];
|
4 | let fieldTypeMap = new Map();
|
5 | function addOperationVariable(variable) {
|
6 | operationVariables.push(variable);
|
7 | }
|
8 | function resetOperationVariables() {
|
9 | operationVariables = [];
|
10 | }
|
11 | function resetFieldMap() {
|
12 | fieldTypeMap = new Map();
|
13 | }
|
14 | export function buildOperationNodeForField({ schema, kind, field, models, ignore = [], depthLimit, circularReferenceDepth, argNames, selectedFields = true, }) {
|
15 | resetOperationVariables();
|
16 | resetFieldMap();
|
17 | const rootTypeNames = getRootTypeNames(schema);
|
18 | const operationNode = buildOperationAndCollectVariables({
|
19 | schema,
|
20 | fieldName: field,
|
21 | kind,
|
22 | models: models || [],
|
23 | ignore,
|
24 | depthLimit: depthLimit || Infinity,
|
25 | circularReferenceDepth: circularReferenceDepth || 1,
|
26 | argNames,
|
27 | selectedFields,
|
28 | rootTypeNames,
|
29 | });
|
30 |
|
31 | operationNode.variableDefinitions = [...operationVariables];
|
32 | resetOperationVariables();
|
33 | resetFieldMap();
|
34 | return operationNode;
|
35 | }
|
36 | function buildOperationAndCollectVariables({ schema, fieldName, kind, models, ignore, depthLimit, circularReferenceDepth, argNames, selectedFields, rootTypeNames, }) {
|
37 | const type = getDefinedRootType(schema, kind);
|
38 | const field = type.getFields()[fieldName];
|
39 | const operationName = `${fieldName}_${kind}`;
|
40 | if (field.args) {
|
41 | for (const arg of field.args) {
|
42 | const argName = arg.name;
|
43 | if (!argNames || argNames.includes(argName)) {
|
44 | addOperationVariable(resolveVariable(arg, argName));
|
45 | }
|
46 | }
|
47 | }
|
48 | return {
|
49 | kind: Kind.OPERATION_DEFINITION,
|
50 | operation: kind,
|
51 | name: {
|
52 | kind: Kind.NAME,
|
53 | value: operationName,
|
54 | },
|
55 | variableDefinitions: [],
|
56 | selectionSet: {
|
57 | kind: Kind.SELECTION_SET,
|
58 | selections: [
|
59 | resolveField({
|
60 | type,
|
61 | field,
|
62 | models,
|
63 | firstCall: true,
|
64 | path: [],
|
65 | ancestors: [],
|
66 | ignore,
|
67 | depthLimit,
|
68 | circularReferenceDepth,
|
69 | schema,
|
70 | depth: 0,
|
71 | argNames,
|
72 | selectedFields,
|
73 | rootTypeNames,
|
74 | }),
|
75 | ],
|
76 | },
|
77 | };
|
78 | }
|
79 | function resolveSelectionSet({ parent, type, models, firstCall, path, ancestors, ignore, depthLimit, circularReferenceDepth, schema, depth, argNames, selectedFields, rootTypeNames, }) {
|
80 | if (typeof selectedFields === 'boolean' && depth > depthLimit) {
|
81 | return;
|
82 | }
|
83 | if (isUnionType(type)) {
|
84 | const types = type.getTypes();
|
85 | return {
|
86 | kind: Kind.SELECTION_SET,
|
87 | selections: types
|
88 | .filter(t => !hasCircularRef([...ancestors, t], {
|
89 | depth: circularReferenceDepth,
|
90 | }))
|
91 | .map(t => {
|
92 | return {
|
93 | kind: Kind.INLINE_FRAGMENT,
|
94 | typeCondition: {
|
95 | kind: Kind.NAMED_TYPE,
|
96 | name: {
|
97 | kind: Kind.NAME,
|
98 | value: t.name,
|
99 | },
|
100 | },
|
101 | selectionSet: resolveSelectionSet({
|
102 | parent: type,
|
103 | type: t,
|
104 | models,
|
105 | path,
|
106 | ancestors,
|
107 | ignore,
|
108 | depthLimit,
|
109 | circularReferenceDepth,
|
110 | schema,
|
111 | depth,
|
112 | argNames,
|
113 | selectedFields,
|
114 | rootTypeNames,
|
115 | }),
|
116 | };
|
117 | })
|
118 | .filter(fragmentNode => fragmentNode?.selectionSet?.selections?.length > 0),
|
119 | };
|
120 | }
|
121 | if (isInterfaceType(type)) {
|
122 | const types = Object.values(schema.getTypeMap()).filter((t) => isObjectType(t) && t.getInterfaces().includes(type));
|
123 | return {
|
124 | kind: Kind.SELECTION_SET,
|
125 | selections: types
|
126 | .filter(t => !hasCircularRef([...ancestors, t], {
|
127 | depth: circularReferenceDepth,
|
128 | }))
|
129 | .map(t => {
|
130 | return {
|
131 | kind: Kind.INLINE_FRAGMENT,
|
132 | typeCondition: {
|
133 | kind: Kind.NAMED_TYPE,
|
134 | name: {
|
135 | kind: Kind.NAME,
|
136 | value: t.name,
|
137 | },
|
138 | },
|
139 | selectionSet: resolveSelectionSet({
|
140 | parent: type,
|
141 | type: t,
|
142 | models,
|
143 | path,
|
144 | ancestors,
|
145 | ignore,
|
146 | depthLimit,
|
147 | circularReferenceDepth,
|
148 | schema,
|
149 | depth,
|
150 | argNames,
|
151 | selectedFields,
|
152 | rootTypeNames,
|
153 | }),
|
154 | };
|
155 | })
|
156 | .filter(fragmentNode => fragmentNode?.selectionSet?.selections?.length > 0),
|
157 | };
|
158 | }
|
159 | if (isObjectType(type) && !rootTypeNames.has(type.name)) {
|
160 | const isIgnored = ignore.includes(type.name) || ignore.includes(`${parent.name}.${path[path.length - 1]}`);
|
161 | const isModel = models.includes(type.name);
|
162 | if (!firstCall && isModel && !isIgnored) {
|
163 | return {
|
164 | kind: Kind.SELECTION_SET,
|
165 | selections: [
|
166 | {
|
167 | kind: Kind.FIELD,
|
168 | name: {
|
169 | kind: Kind.NAME,
|
170 | value: 'id',
|
171 | },
|
172 | },
|
173 | ],
|
174 | };
|
175 | }
|
176 | const fields = type.getFields();
|
177 | return {
|
178 | kind: Kind.SELECTION_SET,
|
179 | selections: Object.keys(fields)
|
180 | .filter(fieldName => {
|
181 | return !hasCircularRef([...ancestors, getNamedType(fields[fieldName].type)], {
|
182 | depth: circularReferenceDepth,
|
183 | });
|
184 | })
|
185 | .map(fieldName => {
|
186 | const selectedSubFields = typeof selectedFields === 'object' ? selectedFields[fieldName] : true;
|
187 | if (selectedSubFields) {
|
188 | return resolveField({
|
189 | type,
|
190 | field: fields[fieldName],
|
191 | models,
|
192 | path: [...path, fieldName],
|
193 | ancestors,
|
194 | ignore,
|
195 | depthLimit,
|
196 | circularReferenceDepth,
|
197 | schema,
|
198 | depth,
|
199 | argNames,
|
200 | selectedFields: selectedSubFields,
|
201 | rootTypeNames,
|
202 | });
|
203 | }
|
204 | return null;
|
205 | })
|
206 | .filter((f) => {
|
207 | if (f == null) {
|
208 | return false;
|
209 | }
|
210 | else if ('selectionSet' in f) {
|
211 | return !!f.selectionSet?.selections?.length;
|
212 | }
|
213 | return true;
|
214 | }),
|
215 | };
|
216 | }
|
217 | }
|
218 | function resolveVariable(arg, name) {
|
219 | function resolveVariableType(type) {
|
220 | if (isListType(type)) {
|
221 | return {
|
222 | kind: Kind.LIST_TYPE,
|
223 | type: resolveVariableType(type.ofType),
|
224 | };
|
225 | }
|
226 | if (isNonNullType(type)) {
|
227 | return {
|
228 | kind: Kind.NON_NULL_TYPE,
|
229 |
|
230 | type: resolveVariableType(type.ofType),
|
231 | };
|
232 | }
|
233 | return {
|
234 | kind: Kind.NAMED_TYPE,
|
235 | name: {
|
236 | kind: Kind.NAME,
|
237 | value: type.name,
|
238 | },
|
239 | };
|
240 | }
|
241 | return {
|
242 | kind: Kind.VARIABLE_DEFINITION,
|
243 | variable: {
|
244 | kind: Kind.VARIABLE,
|
245 | name: {
|
246 | kind: Kind.NAME,
|
247 | value: name || arg.name,
|
248 | },
|
249 | },
|
250 | type: resolveVariableType(arg.type),
|
251 | };
|
252 | }
|
253 | function getArgumentName(name, path) {
|
254 | return [...path, name].join('_');
|
255 | }
|
256 | function resolveField({ type, field, models, firstCall, path, ancestors, ignore, depthLimit, circularReferenceDepth, schema, depth, argNames, selectedFields, rootTypeNames, }) {
|
257 | const namedType = getNamedType(field.type);
|
258 | let args = [];
|
259 | let removeField = false;
|
260 | if (field.args && field.args.length) {
|
261 | args = field.args
|
262 | .map(arg => {
|
263 | const argumentName = getArgumentName(arg.name, path);
|
264 | if (argNames && !argNames.includes(argumentName)) {
|
265 | if (isNonNullType(arg.type)) {
|
266 | removeField = true;
|
267 | }
|
268 | return null;
|
269 | }
|
270 | if (!firstCall) {
|
271 | addOperationVariable(resolveVariable(arg, argumentName));
|
272 | }
|
273 | return {
|
274 | kind: Kind.ARGUMENT,
|
275 | name: {
|
276 | kind: Kind.NAME,
|
277 | value: arg.name,
|
278 | },
|
279 | value: {
|
280 | kind: Kind.VARIABLE,
|
281 | name: {
|
282 | kind: Kind.NAME,
|
283 | value: getArgumentName(arg.name, path),
|
284 | },
|
285 | },
|
286 | };
|
287 | })
|
288 | .filter(Boolean);
|
289 | }
|
290 | if (removeField) {
|
291 | return null;
|
292 | }
|
293 | const fieldPath = [...path, field.name];
|
294 | const fieldPathStr = fieldPath.join('.');
|
295 | let fieldName = field.name;
|
296 | if (fieldTypeMap.has(fieldPathStr) && fieldTypeMap.get(fieldPathStr) !== field.type.toString()) {
|
297 | fieldName += field.type
|
298 | .toString()
|
299 | .replace('!', 'NonNull')
|
300 | .replace('[', 'List')
|
301 | .replace(']', '');
|
302 | }
|
303 | fieldTypeMap.set(fieldPathStr, field.type.toString());
|
304 | if (!isScalarType(namedType) && !isEnumType(namedType)) {
|
305 | return {
|
306 | kind: Kind.FIELD,
|
307 | name: {
|
308 | kind: Kind.NAME,
|
309 | value: field.name,
|
310 | },
|
311 | ...(fieldName !== field.name && { alias: { kind: Kind.NAME, value: fieldName } }),
|
312 | selectionSet: resolveSelectionSet({
|
313 | parent: type,
|
314 | type: namedType,
|
315 | models,
|
316 | firstCall,
|
317 | path: fieldPath,
|
318 | ancestors: [...ancestors, type],
|
319 | ignore,
|
320 | depthLimit,
|
321 | circularReferenceDepth,
|
322 | schema,
|
323 | depth: depth + 1,
|
324 | argNames,
|
325 | selectedFields,
|
326 | rootTypeNames,
|
327 | }) || undefined,
|
328 | arguments: args,
|
329 | };
|
330 | }
|
331 | return {
|
332 | kind: Kind.FIELD,
|
333 | name: {
|
334 | kind: Kind.NAME,
|
335 | value: field.name,
|
336 | },
|
337 | ...(fieldName !== field.name && { alias: { kind: Kind.NAME, value: fieldName } }),
|
338 | arguments: args,
|
339 | };
|
340 | }
|
341 | function hasCircularRef(types, config = {
|
342 | depth: 1,
|
343 | }) {
|
344 | const type = types[types.length - 1];
|
345 | if (isScalarType(type)) {
|
346 | return false;
|
347 | }
|
348 | const size = types.filter(t => t.name === type.name).length;
|
349 | return size > config.depth;
|
350 | }
|