1 | import { Kind, isUnionType, isInterfaceType, isObjectType, isNonNullType, isListType, concatAST, visit } from 'graphql';
|
2 | import { BaseVisitor, getConfigValue, buildScalars, indent, DeclarationBlock } from '@graphql-codegen/visitor-plugin-common';
|
3 | import { getBaseType } from '@graphql-codegen/plugin-helpers';
|
4 | import { pascalCase } from 'pascal-case';
|
5 |
|
6 | const handleTypeNameDuplicates = (result, name, prefix = '') => {
|
7 | let typeToUse = name;
|
8 | while (result[prefix + typeToUse]) {
|
9 | typeToUse = `_${typeToUse}`;
|
10 | }
|
11 | return prefix + typeToUse;
|
12 | };
|
13 | function selectionSetToTypes(typesPrefix, baseVisitor, schema, parentTypeName, stack, fieldName, selectionSet, preResolveTypes, result = {}) {
|
14 | const parentType = schema.getType(parentTypeName);
|
15 | const typeName = baseVisitor.convertName(fieldName);
|
16 | if (selectionSet && selectionSet.selections && selectionSet.selections.length) {
|
17 | const typeToUse = handleTypeNameDuplicates(result, typeName, typesPrefix);
|
18 | result[typeToUse] = { export: 'type', name: stack };
|
19 | for (const selection of selectionSet.selections) {
|
20 | switch (selection.kind) {
|
21 | case Kind.FIELD: {
|
22 | if (isObjectType(parentType) || isInterfaceType(parentType)) {
|
23 | const selectionName = selection.alias && selection.alias.value ? selection.alias.value : selection.name.value;
|
24 | if (!selectionName.startsWith('__')) {
|
25 | const field = parentType.getFields()[selection.name.value];
|
26 | const baseType = getBaseType(field.type);
|
27 | const wrapWithNonNull = (baseVisitor.config.strict || baseVisitor.config.preResolveTypes) && !isNonNullType(field.type);
|
28 | const isArray = (isNonNullType(field.type) && isListType(field.type.ofType)) || isListType(field.type);
|
29 | const typeRef = `${stack}['${selectionName}']`;
|
30 | const nonNullableInnerType = `${wrapWithNonNull ? `(NonNullable<${typeRef}>)` : typeRef}`;
|
31 | const arrayInnerType = isArray ? `${nonNullableInnerType}[0]` : nonNullableInnerType;
|
32 | const wrapArrayWithNonNull = baseVisitor.config.strict || baseVisitor.config.preResolveTypes;
|
33 | const newStack = isArray && wrapArrayWithNonNull ? `(NonNullable<${arrayInnerType}>)` : arrayInnerType;
|
34 | selectionSetToTypes(typesPrefix, baseVisitor, schema, baseType.name, newStack, selectionName, selection.selectionSet, preResolveTypes, result);
|
35 | }
|
36 | }
|
37 | break;
|
38 | }
|
39 | case Kind.INLINE_FRAGMENT: {
|
40 | const typeCondition = selection.typeCondition.name.value;
|
41 | const fragmentName = baseVisitor.convertName(typeCondition, { suffix: 'InlineFragment' });
|
42 | let inlineFragmentValue;
|
43 | if (isUnionType(parentType) || isInterfaceType(parentType)) {
|
44 | inlineFragmentValue = `DiscriminateUnion<RequireField<${stack}, '__typename'>, { __typename: '${typeCondition}' }>`;
|
45 | }
|
46 | else {
|
47 | let encounteredNestedInlineFragment = false;
|
48 | const subSelections = selection.selectionSet.selections
|
49 | .map(subSelection => {
|
50 | switch (subSelection.kind) {
|
51 | case Kind.FIELD:
|
52 | return `'${subSelection.name.value}'`;
|
53 | case Kind.FRAGMENT_SPREAD:
|
54 | return `keyof ${baseVisitor.convertName(subSelection.name.value, { suffix: 'Fragment' })}`;
|
55 | case Kind.INLINE_FRAGMENT:
|
56 | encounteredNestedInlineFragment = true;
|
57 | return null;
|
58 | }
|
59 | })
|
60 | .filter(a => a);
|
61 | if (encounteredNestedInlineFragment) {
|
62 | throw new Error('Nested inline fragments are not supported the `typescript-compatibility` plugin');
|
63 | }
|
64 | else if (subSelections.length) {
|
65 | inlineFragmentValue = `{ __typename: '${typeCondition}' } & Pick<${stack}, ${subSelections.join(' | ')}>`;
|
66 | }
|
67 | }
|
68 | if (inlineFragmentValue) {
|
69 | selectionSetToTypes(typesPrefix, baseVisitor, schema, typeCondition, `(${inlineFragmentValue})`, fragmentName, selection.selectionSet, preResolveTypes, result);
|
70 | }
|
71 | break;
|
72 | }
|
73 | }
|
74 | }
|
75 | }
|
76 | return result;
|
77 | }
|
78 |
|
79 | class CompatibilityPluginVisitor extends BaseVisitor {
|
80 | constructor(rawConfig, _schema, options) {
|
81 | super(rawConfig, {
|
82 | reactApollo: options.reactApollo,
|
83 | noNamespaces: getConfigValue(rawConfig.noNamespaces, false),
|
84 | preResolveTypes: getConfigValue(rawConfig.preResolveTypes, false),
|
85 | strict: getConfigValue(rawConfig.strict, false),
|
86 | scalars: buildScalars(_schema, rawConfig.scalars),
|
87 | });
|
88 | this._schema = _schema;
|
89 | }
|
90 | getRootType(operationType) {
|
91 | if (operationType === 'query') {
|
92 | return this._schema.getQueryType().name;
|
93 | }
|
94 | else if (operationType === 'mutation') {
|
95 | return this._schema.getMutationType().name;
|
96 | }
|
97 | else if (operationType === 'subscription') {
|
98 | return this._schema.getSubscriptionType().name;
|
99 | }
|
100 | return null;
|
101 | }
|
102 | buildOperationBlock(node) {
|
103 | const typeName = this.getRootType(node.operation);
|
104 | const baseName = this.convertName(node.name.value, { suffix: `${pascalCase(node.operation)}` });
|
105 | const typesPrefix = this.config.noNamespaces ? this.convertName(node.name.value) : '';
|
106 | const selectionSetTypes = {
|
107 | [typesPrefix + this.convertName('Variables')]: {
|
108 | export: 'type',
|
109 | name: this.convertName(node.name.value, { suffix: `${pascalCase(node.operation)}Variables` }),
|
110 | },
|
111 | };
|
112 | selectionSetToTypes(typesPrefix, this, this._schema, typeName, baseName, node.operation, node.selectionSet, this.config.preResolveTypes, selectionSetTypes);
|
113 | return selectionSetTypes;
|
114 | }
|
115 | buildFragmentBlock(node) {
|
116 | const typeName = this._schema.getType(node.typeCondition.name.value).name;
|
117 | const baseName = this.convertName(node.name.value, { suffix: `Fragment` });
|
118 | const typesPrefix = this.config.noNamespaces ? this.convertName(node.name.value) : '';
|
119 | const selectionSetTypes = {};
|
120 | selectionSetToTypes(typesPrefix, this, this._schema, typeName, baseName, 'fragment', node.selectionSet, this.config.preResolveTypes, selectionSetTypes);
|
121 | return selectionSetTypes;
|
122 | }
|
123 | printTypes(selectionSetTypes) {
|
124 | return Object.keys(selectionSetTypes)
|
125 | .filter(typeName => typeName !== selectionSetTypes[typeName].name)
|
126 | .map(typeName => `export ${selectionSetTypes[typeName].export} ${typeName} = ${selectionSetTypes[typeName].name};`)
|
127 | .map(m => (this.config.noNamespaces ? m : indent(m)))
|
128 | .join('\n');
|
129 | }
|
130 | FragmentDefinition(node) {
|
131 | const baseName = node.name.value;
|
132 | const results = [];
|
133 | const convertedName = this.convertName(baseName);
|
134 | const selectionSetTypes = this.buildFragmentBlock(node);
|
135 | const fragmentBlock = this.printTypes(selectionSetTypes);
|
136 | if (!this.config.noNamespaces) {
|
137 | results.push(new DeclarationBlock(this._declarationBlockConfig)
|
138 | .export()
|
139 | .asKind('namespace')
|
140 | .withName(convertedName)
|
141 | .withBlock(fragmentBlock).string);
|
142 | }
|
143 | else {
|
144 | results.push(fragmentBlock);
|
145 | }
|
146 | return results.join('\n');
|
147 | }
|
148 | OperationDefinition(node) {
|
149 | const baseName = node.name.value;
|
150 | const convertedName = this.convertName(baseName);
|
151 | const results = [];
|
152 | const selectionSetTypes = this.buildOperationBlock(node);
|
153 | if (this.config.reactApollo) {
|
154 | const reactApolloConfig = this.config.reactApollo;
|
155 | let hoc = true;
|
156 | let component = true;
|
157 | let hooks = false;
|
158 | if (typeof reactApolloConfig === 'object') {
|
159 | if (reactApolloConfig.withHOC === false) {
|
160 | hoc = false;
|
161 | }
|
162 | if (reactApolloConfig.withComponent === false) {
|
163 | component = false;
|
164 | }
|
165 | if (reactApolloConfig.withHooks) {
|
166 | hooks = true;
|
167 | }
|
168 | }
|
169 | const prefix = this.config.noNamespaces ? convertedName : '';
|
170 | selectionSetTypes[prefix + 'Document'] = {
|
171 | export: 'const',
|
172 | name: this.convertName(baseName, { suffix: 'Document' }),
|
173 | };
|
174 | if (hoc) {
|
175 | selectionSetTypes[prefix + 'Props'] = {
|
176 | export: 'type',
|
177 | name: this.convertName(baseName, { suffix: 'Props' }),
|
178 | };
|
179 | selectionSetTypes[prefix + 'HOC'] = {
|
180 | export: 'const',
|
181 | name: `with${convertedName}`,
|
182 | };
|
183 | }
|
184 | if (component) {
|
185 | selectionSetTypes[prefix + 'Component'] = {
|
186 | export: 'const',
|
187 | name: this.convertName(baseName, { suffix: 'Component' }),
|
188 | };
|
189 | }
|
190 | if (hooks) {
|
191 | selectionSetTypes['use' + prefix] = {
|
192 | export: 'const',
|
193 | name: 'use' + this.convertName(baseName, { suffix: pascalCase(node.operation) }),
|
194 | };
|
195 | }
|
196 | }
|
197 | const operationsBlock = this.printTypes(selectionSetTypes);
|
198 | if (!this.config.noNamespaces) {
|
199 | results.push(new DeclarationBlock(this._declarationBlockConfig)
|
200 | .export()
|
201 | .asKind('namespace')
|
202 | .withName(convertedName)
|
203 | .withBlock(operationsBlock).string);
|
204 | }
|
205 | else {
|
206 | results.push(operationsBlock);
|
207 | }
|
208 | return results.join('\n');
|
209 | }
|
210 | }
|
211 |
|
212 | const REACT_APOLLO_PLUGIN_NAME = 'typescript-react-apollo';
|
213 | const plugin = async (schema, documents, config, additionalData) => {
|
214 | const allAst = concatAST(documents.map(v => v.document));
|
215 | const reactApollo = ((additionalData || {}).allPlugins || []).find(p => Object.keys(p)[0] === REACT_APOLLO_PLUGIN_NAME);
|
216 | const visitor = new CompatibilityPluginVisitor(config, schema, {
|
217 | reactApollo: reactApollo
|
218 | ? {
|
219 | ...(config || {}),
|
220 | ...reactApollo[REACT_APOLLO_PLUGIN_NAME],
|
221 | }
|
222 | : null,
|
223 | });
|
224 | const visitorResult = visit(allAst, {
|
225 | leave: visitor,
|
226 | });
|
227 | const discriminateUnion = `type DiscriminateUnion<T, U> = T extends U ? T : never;\n`;
|
228 | const requireField = `type RequireField<T, TNames extends string> = T & { [P in TNames]: (T & { [name: string]: never })[P] };\n`;
|
229 | const result = visitorResult.definitions.filter(a => a && typeof a === 'string').join('\n');
|
230 | return result.includes('DiscriminateUnion') ? [discriminateUnion, requireField, result].join('\n') : result;
|
231 | };
|
232 |
|
233 | export { plugin };
|
234 |
|