1 | import { printSchemaWithDirectives } from '@graphql-tools/utils';
|
2 | import { BaseResolversVisitor, getConfigValue, parseMapper } from '@graphql-codegen/visitor-plugin-common';
|
3 | import { addFederationReferencesToSchema } from '@graphql-codegen/plugin-helpers';
|
4 | import { printSchema, parse, visit } from 'graphql';
|
5 | import autoBind from 'auto-bind';
|
6 | import { TypeScriptOperationVariablesToObject } from '@graphql-codegen/typescript';
|
7 |
|
8 | const ENUM_RESOLVERS_SIGNATURE = 'export type EnumResolverSignature<T, AllowedValues = any> = { [key in keyof T]?: AllowedValues };';
|
9 | class TypeScriptResolversVisitor extends BaseResolversVisitor {
|
10 | constructor(pluginConfig, schema) {
|
11 | super(pluginConfig, {
|
12 | avoidOptionals: getConfigValue(pluginConfig.avoidOptionals, false),
|
13 | useIndexSignature: getConfigValue(pluginConfig.useIndexSignature, false),
|
14 | wrapFieldDefinitions: getConfigValue(pluginConfig.wrapFieldDefinitions, false),
|
15 | allowParentTypeOverride: getConfigValue(pluginConfig.allowParentTypeOverride, false),
|
16 | optionalInfoArgument: getConfigValue(pluginConfig.optionalInfoArgument, false),
|
17 | }, schema);
|
18 | autoBind(this);
|
19 | this.setVariablesTransformer(new TypeScriptOperationVariablesToObject(this.scalars, this.convertName, this.config.avoidOptionals, this.config.immutableTypes, this.config.namespacedImportName, [], this.config.enumPrefix, this.config.enumValues));
|
20 | if (this.config.useIndexSignature) {
|
21 | this._declarationBlockConfig = {
|
22 | blockTransformer(block) {
|
23 | return `ResolversObject<${block}>`;
|
24 | },
|
25 | };
|
26 | }
|
27 | }
|
28 | transformParentGenericType(parentType) {
|
29 | if (this.config.allowParentTypeOverride) {
|
30 | return `ParentType = ${parentType}`;
|
31 | }
|
32 | return `ParentType extends ${parentType} = ${parentType}`;
|
33 | }
|
34 | formatRootResolver(schemaTypeName, resolverType, declarationKind) {
|
35 | return `${schemaTypeName}${this.config.avoidOptionals ? '' : '?'}: ${resolverType}${this.getPunctuation(declarationKind)}`;
|
36 | }
|
37 | clearOptional(str) {
|
38 | if (str.startsWith('Maybe')) {
|
39 | return str.replace(/Maybe<(.*?)>$/, '$1');
|
40 | }
|
41 | return str;
|
42 | }
|
43 | ListType(node) {
|
44 | return `Maybe<${super.ListType(node)}>`;
|
45 | }
|
46 | wrapWithListType(str) {
|
47 | return `${this.config.immutableTypes ? 'ReadonlyArray' : 'Array'}<${str}>`;
|
48 | }
|
49 | getParentTypeForSignature(node) {
|
50 | if (this._federation.isResolveReferenceField(node) && this.config.wrapFieldDefinitions) {
|
51 | return 'UnwrappedObject<ParentType>';
|
52 | }
|
53 | return 'ParentType';
|
54 | }
|
55 | NamedType(node) {
|
56 | return `Maybe<${super.NamedType(node)}>`;
|
57 | }
|
58 | NonNullType(node) {
|
59 | const baseValue = super.NonNullType(node);
|
60 | return this.clearOptional(baseValue);
|
61 | }
|
62 | getPunctuation(declarationKind) {
|
63 | return ';';
|
64 | }
|
65 | buildEnumResolverContentBlock(node, mappedEnumType) {
|
66 | const valuesMap = `{ ${(node.values || [])
|
67 | .map(v => `${v.name}${this.config.avoidOptionals ? '' : '?'}: any`)
|
68 | .join(', ')} }`;
|
69 | this._globalDeclarations.add(ENUM_RESOLVERS_SIGNATURE);
|
70 | return `EnumResolverSignature<${valuesMap}, ${mappedEnumType}>`;
|
71 | }
|
72 | buildEnumResolversExplicitMappedValues(node, valuesMapping) {
|
73 | return `{ ${(node.values || [])
|
74 | .map(v => {
|
75 | const valueName = v.name;
|
76 | const mappedValue = valuesMapping[valueName];
|
77 | return `${valueName}: ${typeof mappedValue === 'number' ? mappedValue : `'${mappedValue}'`}`;
|
78 | })
|
79 | .join(', ')} }`;
|
80 | }
|
81 | }
|
82 |
|
83 | const plugin = (schema, documents, config) => {
|
84 | const imports = [];
|
85 | if (!config.customResolveInfo) {
|
86 | imports.push('GraphQLResolveInfo');
|
87 | }
|
88 | const showUnusedMappers = typeof config.showUnusedMappers === 'boolean' ? config.showUnusedMappers : true;
|
89 | const noSchemaStitching = typeof config.noSchemaStitching === 'boolean' ? config.noSchemaStitching : false;
|
90 | if (config.noSchemaStitching === false) {
|
91 |
|
92 | console.warn(`The default behavior of 'noSchemaStitching' will be reversed in the next major release. Support for Schema Stitching will be disabled by default.`);
|
93 | }
|
94 | const indexSignature = config.useIndexSignature
|
95 | ? [
|
96 | 'export type WithIndex<TObject> = TObject & Record<string, any>;',
|
97 | 'export type ResolversObject<TObject> = WithIndex<TObject>;',
|
98 | ].join('\n')
|
99 | : '';
|
100 | const transformedSchema = config.federation ? addFederationReferencesToSchema(schema) : schema;
|
101 | const visitor = new TypeScriptResolversVisitor(config, transformedSchema);
|
102 | const namespacedImportPrefix = visitor.config.namespacedImportName ? `${visitor.config.namespacedImportName}.` : '';
|
103 | const printedSchema = config.federation
|
104 | ? printSchemaWithDirectives(transformedSchema)
|
105 | : printSchema(transformedSchema);
|
106 | const astNode = parse(printedSchema);
|
107 |
|
108 | const visitorResult = visit(astNode, { leave: visitor });
|
109 | const optionalSignForInfoArg = visitor.config.optionalInfoArgument ? '?' : '';
|
110 | const prepend = [];
|
111 | const defsToInclude = [];
|
112 | const legacyStitchingResolverType = `
|
113 | export type LegacyStitchingResolver<TResult, TParent, TContext, TArgs> = {
|
114 | fragment: string;
|
115 | resolve: ResolverFn<TResult, TParent, TContext, TArgs>;
|
116 | };`;
|
117 | const newStitchingResolverType = `
|
118 | export type NewStitchingResolver<TResult, TParent, TContext, TArgs> = {
|
119 | selectionSet: string;
|
120 | resolve: ResolverFn<TResult, TParent, TContext, TArgs>;
|
121 | };`;
|
122 | const stitchingResolverType = `export type StitchingResolver<TResult, TParent, TContext, TArgs> = LegacyStitchingResolver<TResult, TParent, TContext, TArgs> | NewStitchingResolver<TResult, TParent, TContext, TArgs>;`;
|
123 | const resolverType = `export type Resolver<TResult, TParent = {}, TContext = {}, TArgs = {}> =`;
|
124 | const resolverFnUsage = `ResolverFn<TResult, TParent, TContext, TArgs>`;
|
125 | const stitchingResolverUsage = `StitchingResolver<TResult, TParent, TContext, TArgs>`;
|
126 | if (visitor.hasFederation()) {
|
127 | if (visitor.config.wrapFieldDefinitions) {
|
128 | defsToInclude.push(`export type UnwrappedObject<T> = {
|
129 | [P in keyof T]: T[P] extends infer R | Promise<infer R> | (() => infer R2 | Promise<infer R2>)
|
130 | ? R & R2 : T[P]
|
131 | };`);
|
132 | }
|
133 | defsToInclude.push(`export type ReferenceResolver<TResult, TReference, TContext> = (
|
134 | reference: TReference,
|
135 | context: TContext,
|
136 | info${optionalSignForInfoArg}: GraphQLResolveInfo
|
137 | ) => Promise<TResult> | TResult;`);
|
138 | defsToInclude.push(`
|
139 | type ScalarCheck<T, S> = S extends true ? T : NullableCheck<T, S>;
|
140 | type NullableCheck<T, S> = Maybe<T> extends T ? Maybe<ListCheck<NonNullable<T>, S>> : ListCheck<T, S>;
|
141 | type ListCheck<T, S> = T extends (infer U)[] ? NullableCheck<U, S>[] : GraphQLRecursivePick<T, S>;
|
142 | export type GraphQLRecursivePick<T, S> = { [K in keyof T & keyof S]: ScalarCheck<T[K], S[K]> };
|
143 | `);
|
144 | }
|
145 | if (noSchemaStitching) {
|
146 |
|
147 | defsToInclude.push(`${resolverType} ${resolverFnUsage};`);
|
148 | }
|
149 | else {
|
150 |
|
151 |
|
152 |
|
153 |
|
154 | defsToInclude.push([
|
155 | legacyStitchingResolverType,
|
156 | newStitchingResolverType,
|
157 | stitchingResolverType,
|
158 | resolverType,
|
159 | ` | ${resolverFnUsage}`,
|
160 | ` | ${stitchingResolverUsage};`,
|
161 | ].join('\n'));
|
162 | }
|
163 | if (config.customResolverFn) {
|
164 | const parsedMapper = parseMapper(config.customResolverFn);
|
165 | if (parsedMapper.isExternal) {
|
166 | const importType = config.useTypeImports ? 'import type' : 'import';
|
167 | if (parsedMapper.default) {
|
168 | prepend.push(`${importType} ResolverFn from '${parsedMapper.source}';`);
|
169 | }
|
170 | else {
|
171 | prepend.push(`${importType} { ${parsedMapper.import} ${parsedMapper.import !== 'ResolverFn' ? 'as ResolverFn ' : ''}} from '${parsedMapper.source}';`);
|
172 | }
|
173 | prepend.push(`export${config.useTypeImports ? ' type' : ''} { ResolverFn };`);
|
174 | }
|
175 | else {
|
176 | prepend.push(`export type ResolverFn<TResult, TParent, TContext, TArgs> = ${parsedMapper.type}`);
|
177 | }
|
178 | }
|
179 | else {
|
180 | const defaultResolverFn = `
|
181 | export type ResolverFn<TResult, TParent, TContext, TArgs> = (
|
182 | parent: TParent,
|
183 | args: TArgs,
|
184 | context: TContext,
|
185 | info${optionalSignForInfoArg}: GraphQLResolveInfo
|
186 | ) => Promise<TResult> | TResult;`;
|
187 | defsToInclude.push(defaultResolverFn);
|
188 | }
|
189 | const header = `${indexSignature}
|
190 |
|
191 | ${visitor.getResolverTypeWrapperSignature()}
|
192 |
|
193 | ${defsToInclude.join('\n')}
|
194 |
|
195 | export type SubscriptionSubscribeFn<TResult, TParent, TContext, TArgs> = (
|
196 | parent: TParent,
|
197 | args: TArgs,
|
198 | context: TContext,
|
199 | info${optionalSignForInfoArg}: GraphQLResolveInfo
|
200 | ) => AsyncIterator<TResult> | Promise<AsyncIterator<TResult>>;
|
201 |
|
202 | export type SubscriptionResolveFn<TResult, TParent, TContext, TArgs> = (
|
203 | parent: TParent,
|
204 | args: TArgs,
|
205 | context: TContext,
|
206 | info${optionalSignForInfoArg}: GraphQLResolveInfo
|
207 | ) => TResult | Promise<TResult>;
|
208 |
|
209 | export interface SubscriptionSubscriberObject<TResult, TKey extends string, TParent, TContext, TArgs> {
|
210 | subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>;
|
211 | resolve?: SubscriptionResolveFn<TResult, { [key in TKey]: TResult }, TContext, TArgs>;
|
212 | }
|
213 |
|
214 | export interface SubscriptionResolverObject<TResult, TParent, TContext, TArgs> {
|
215 | subscribe: SubscriptionSubscribeFn<any, TParent, TContext, TArgs>;
|
216 | resolve: SubscriptionResolveFn<TResult, any, TContext, TArgs>;
|
217 | }
|
218 |
|
219 | export type SubscriptionObject<TResult, TKey extends string, TParent, TContext, TArgs> =
|
220 | | SubscriptionSubscriberObject<TResult, TKey, TParent, TContext, TArgs>
|
221 | | SubscriptionResolverObject<TResult, TParent, TContext, TArgs>;
|
222 |
|
223 | export type SubscriptionResolver<TResult, TKey extends string, TParent = {}, TContext = {}, TArgs = {}> =
|
224 | | ((...args: any[]) => SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>)
|
225 | | SubscriptionObject<TResult, TKey, TParent, TContext, TArgs>;
|
226 |
|
227 | export type TypeResolveFn<TTypes, TParent = {}, TContext = {}> = (
|
228 | parent: TParent,
|
229 | context: TContext,
|
230 | info${optionalSignForInfoArg}: GraphQLResolveInfo
|
231 | ) => ${namespacedImportPrefix}Maybe<TTypes> | Promise<${namespacedImportPrefix}Maybe<TTypes>>;
|
232 |
|
233 | export type IsTypeOfResolverFn<T = {}> = (obj: T, info${optionalSignForInfoArg}: GraphQLResolveInfo) => boolean | Promise<boolean>;
|
234 |
|
235 | export type NextResolverFn<T> = () => Promise<T>;
|
236 |
|
237 | export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs = {}> = (
|
238 | next: NextResolverFn<TResult>,
|
239 | parent: TParent,
|
240 | args: TArgs,
|
241 | context: TContext,
|
242 | info${optionalSignForInfoArg}: GraphQLResolveInfo
|
243 | ) => TResult | Promise<TResult>;
|
244 | `;
|
245 | const resolversTypeMapping = visitor.buildResolversTypes();
|
246 | const resolversParentTypeMapping = visitor.buildResolversParentTypes();
|
247 | const { getRootResolver, getAllDirectiveResolvers, mappersImports, unusedMappers, hasScalars } = visitor;
|
248 | if (hasScalars()) {
|
249 | imports.push('GraphQLScalarType', 'GraphQLScalarTypeConfig');
|
250 | }
|
251 | if (showUnusedMappers && unusedMappers.length) {
|
252 |
|
253 | console.warn(`Unused mappers: ${unusedMappers.join(',')}`);
|
254 | }
|
255 | if (imports.length) {
|
256 | prepend.push(`import { ${imports.join(', ')} } from 'graphql';`);
|
257 | }
|
258 | if (config.customResolveInfo) {
|
259 | const parsedMapper = parseMapper(config.customResolveInfo);
|
260 | if (parsedMapper.isExternal) {
|
261 | if (parsedMapper.default) {
|
262 | prepend.push(`import GraphQLResolveInfo from '${parsedMapper.source}'`);
|
263 | }
|
264 | prepend.push(`import { ${parsedMapper.import} ${parsedMapper.import !== 'GraphQLResolveInfo' ? 'as GraphQLResolveInfo' : ''} } from '${parsedMapper.source}';`);
|
265 | }
|
266 | else {
|
267 | prepend.push(`type GraphQLResolveInfo = ${parsedMapper.type}`);
|
268 | }
|
269 | }
|
270 | prepend.push(...mappersImports, ...visitor.globalDeclarations);
|
271 | return {
|
272 | prepend,
|
273 | content: [
|
274 | header,
|
275 | resolversTypeMapping,
|
276 | resolversParentTypeMapping,
|
277 | ...visitorResult.definitions.filter(d => typeof d === 'string'),
|
278 | getRootResolver(),
|
279 | getAllDirectiveResolvers(),
|
280 | ].join('\n'),
|
281 | };
|
282 | };
|
283 |
|
284 | export { TypeScriptResolversVisitor, plugin };
|
285 |
|