1 | import { Kind, isScalarType, isInputObjectType, isEnumType, isObjectType, visit } from 'graphql';
|
2 | import { getCachedDocumentNodeFromSchema } from '@graphql-codegen/plugin-helpers';
|
3 | import { BaseVisitor, buildScalarsFromConfig, indent, transformComment, indentMultiline, getBaseTypeNode } from '@graphql-codegen/visitor-plugin-common';
|
4 | import { wrapTypeWithModifiers, buildPackageNameFromPath } from '@graphql-codegen/java-common';
|
5 | import { dirname, normalize } from 'path';
|
6 |
|
7 | const KOTLIN_SCALARS = {
|
8 | ID: 'Any',
|
9 | String: 'String',
|
10 | Boolean: 'Boolean',
|
11 | Int: 'Int',
|
12 | Float: 'Float',
|
13 | };
|
14 | class KotlinResolversVisitor extends BaseVisitor {
|
15 | constructor(rawConfig, _schema, defaultPackageName) {
|
16 | super(rawConfig, {
|
17 | enumValues: rawConfig.enumValues || {},
|
18 | listType: rawConfig.listType || 'Iterable',
|
19 | withTypes: rawConfig.withTypes || false,
|
20 | package: rawConfig.package || defaultPackageName,
|
21 | scalars: buildScalarsFromConfig(_schema, rawConfig, KOTLIN_SCALARS),
|
22 | });
|
23 | this._schema = _schema;
|
24 | }
|
25 | getPackageName() {
|
26 | return `package ${this.config.package}\n`;
|
27 | }
|
28 | getEnumValue(enumName, enumOption) {
|
29 | if (this.config.enumValues[enumName] &&
|
30 | typeof this.config.enumValues[enumName] === 'object' &&
|
31 | this.config.enumValues[enumName][enumOption]) {
|
32 | return this.config.enumValues[enumName][enumOption];
|
33 | }
|
34 | return enumOption;
|
35 | }
|
36 | EnumValueDefinition(node) {
|
37 | return (enumName) => {
|
38 | return indent(`${this.convertName(node, { useTypesPrefix: false, transformUnderscore: true })}("${this.getEnumValue(enumName, node.name.value)}")`);
|
39 | };
|
40 | }
|
41 | EnumTypeDefinition(node) {
|
42 | const comment = transformComment(node.description, 0);
|
43 | const enumName = this.convertName(node.name);
|
44 | const enumValues = indentMultiline(node.values.map(enumValue => enumValue(node.name.value)).join(',\n') + ';', 2);
|
45 | return `${comment}enum class ${enumName}(val label: String) {
|
46 | ${enumValues}
|
47 |
|
48 | companion object {
|
49 | @JvmStatic
|
50 | fun valueOfLabel(label: String): ${enumName}? {
|
51 | return values().find { it.label == label }
|
52 | }
|
53 | }
|
54 | }`;
|
55 | }
|
56 | resolveInputFieldType(typeNode) {
|
57 | const innerType = getBaseTypeNode(typeNode);
|
58 | const schemaType = this._schema.getType(innerType.name.value);
|
59 | const isArray = typeNode.kind === Kind.LIST_TYPE ||
|
60 | (typeNode.kind === Kind.NON_NULL_TYPE && typeNode.type.kind === Kind.LIST_TYPE);
|
61 | let result = null;
|
62 | const nullable = typeNode.kind !== Kind.NON_NULL_TYPE;
|
63 | if (isScalarType(schemaType)) {
|
64 | if (this.config.scalars[schemaType.name]) {
|
65 | result = {
|
66 | baseType: this.scalars[schemaType.name],
|
67 | typeName: this.scalars[schemaType.name],
|
68 | isScalar: true,
|
69 | isArray,
|
70 | nullable: nullable,
|
71 | };
|
72 | }
|
73 | else {
|
74 | result = { isArray, baseType: 'Any', typeName: 'Any', isScalar: true, nullable: nullable };
|
75 | }
|
76 | }
|
77 | else if (isInputObjectType(schemaType)) {
|
78 | const convertedName = this.convertName(schemaType.name);
|
79 | const typeName = convertedName.endsWith('Input') ? convertedName : `${convertedName}Input`;
|
80 | result = {
|
81 | baseType: typeName,
|
82 | typeName: typeName,
|
83 | isScalar: false,
|
84 | isArray,
|
85 | nullable: nullable,
|
86 | };
|
87 | }
|
88 | else if (isEnumType(schemaType) || isObjectType(schemaType)) {
|
89 | result = {
|
90 | isArray,
|
91 | baseType: this.convertName(schemaType.name),
|
92 | typeName: this.convertName(schemaType.name),
|
93 | isScalar: true,
|
94 | nullable: nullable,
|
95 | };
|
96 | }
|
97 | else {
|
98 | result = { isArray, baseType: 'Any', typeName: 'Any', isScalar: true, nullable: nullable };
|
99 | }
|
100 | if (result) {
|
101 | result.typeName = wrapTypeWithModifiers(result.typeName, typeNode, this.config.listType);
|
102 | }
|
103 | return result;
|
104 | }
|
105 | buildInputTransfomer(name, inputValueArray) {
|
106 | const classMembers = inputValueArray
|
107 | .map(arg => {
|
108 | const typeToUse = this.resolveInputFieldType(arg.type);
|
109 | const initialValue = this.initialValue(typeToUse.typeName, arg.defaultValue);
|
110 | const initial = initialValue ? ` = ${initialValue}` : typeToUse.nullable ? ' = null' : '';
|
111 | return indent(`val ${arg.name.value}: ${typeToUse.typeName}${typeToUse.nullable ? '?' : ''}${initial}`, 2);
|
112 | })
|
113 | .join(',\n');
|
114 | let suppress = '';
|
115 | const ctorSet = inputValueArray
|
116 | .map(arg => {
|
117 | const typeToUse = this.resolveInputFieldType(arg.type);
|
118 | const initialValue = this.initialValue(typeToUse.typeName, arg.defaultValue);
|
119 | const fallback = initialValue ? ` ?: ${initialValue}` : '';
|
120 | if (typeToUse.isArray && !typeToUse.isScalar) {
|
121 | suppress = '@Suppress("UNCHECKED_CAST")\n ';
|
122 | return indent(`args["${arg.name.value}"]${typeToUse.nullable || fallback ? '?' : '!!'}.let { ${arg.name.value} -> (${arg.name.value} as List<Map<String, Any>>).map { ${typeToUse.baseType}(it) } }${fallback}`, 3);
|
123 | }
|
124 | else if (typeToUse.isScalar) {
|
125 | return indent(`args["${arg.name.value}"] as ${typeToUse.typeName}${typeToUse.nullable || fallback ? '?' : ''}${fallback}`, 3);
|
126 | }
|
127 | else if (typeToUse.nullable || fallback) {
|
128 | suppress = '@Suppress("UNCHECKED_CAST")\n ';
|
129 | return indent(`args["${arg.name.value}"]?.let { ${typeToUse.typeName}(it as Map<String, Any>) }${fallback}`, 3);
|
130 | }
|
131 | else {
|
132 | suppress = '@Suppress("UNCHECKED_CAST")\n ';
|
133 | return indent(`${typeToUse.typeName}(args["${arg.name.value}"] as Map<String, Any>)`, 3);
|
134 | }
|
135 | })
|
136 | .join(',\n');
|
137 |
|
138 | return `data class ${name}(
|
139 | ${classMembers}
|
140 | ) {
|
141 | ${suppress}constructor(args: Map<String, Any>) : this(
|
142 | ${ctorSet}
|
143 | )
|
144 | }`;
|
145 | }
|
146 | buildTypeTransfomer(name, typeValueArray) {
|
147 | const classMembers = typeValueArray
|
148 | .map(arg => {
|
149 | if (!arg.type) {
|
150 | return '';
|
151 | }
|
152 | const typeToUse = this.resolveInputFieldType(arg.type);
|
153 | return indent(`val ${arg.name.value}: ${typeToUse.typeName}${typeToUse.nullable ? '?' : ''}`, 2);
|
154 | })
|
155 | .join(',\n');
|
156 |
|
157 | return `data class ${name}(
|
158 | ${classMembers}
|
159 | )`;
|
160 | }
|
161 | initialValue(typeName, defaultValue) {
|
162 | if (defaultValue) {
|
163 | if (defaultValue.kind === 'IntValue' ||
|
164 | defaultValue.kind === 'FloatValue' ||
|
165 | defaultValue.kind === 'BooleanValue') {
|
166 | return `${defaultValue.value}`;
|
167 | }
|
168 | else if (defaultValue.kind === 'StringValue') {
|
169 | return `"""${defaultValue.value}""".trimIndent()`;
|
170 | }
|
171 | else if (defaultValue.kind === 'EnumValue') {
|
172 | return `${typeName}.${defaultValue.value}`;
|
173 | }
|
174 | else if (defaultValue.kind === 'ListValue') {
|
175 | const list = defaultValue.values
|
176 | .map(value => {
|
177 | return this.initialValue(typeName, value);
|
178 | })
|
179 | .join(', ');
|
180 | return `listOf(${list})`;
|
181 | }
|
182 |
|
183 |
|
184 |
|
185 | }
|
186 | return undefined;
|
187 | }
|
188 | FieldDefinition(node) {
|
189 | if (node.arguments.length > 0) {
|
190 | const inputTransformer = (typeName) => {
|
191 | const transformerName = `${this.convertName(typeName, { useTypesPrefix: true })}${this.convertName(node.name.value, { useTypesPrefix: false })}Args`;
|
192 | return this.buildInputTransfomer(transformerName, node.arguments);
|
193 | };
|
194 | return { node, inputTransformer };
|
195 | }
|
196 | return { node };
|
197 | }
|
198 | InputObjectTypeDefinition(node) {
|
199 | const convertedName = this.convertName(node);
|
200 | const name = convertedName.endsWith('Input') ? convertedName : `${convertedName}Input`;
|
201 | return this.buildInputTransfomer(name, node.fields);
|
202 | }
|
203 | ObjectTypeDefinition(node) {
|
204 | const name = this.convertName(node);
|
205 | const fields = node.fields;
|
206 | const fieldNodes = [];
|
207 | const argsTypes = [];
|
208 | fields.forEach(({ node, inputTransformer }) => {
|
209 | if (node) {
|
210 | fieldNodes.push(node);
|
211 | }
|
212 | if (inputTransformer) {
|
213 | argsTypes.push(inputTransformer);
|
214 | }
|
215 | });
|
216 | let types = argsTypes.map(f => f(node.name.value)).filter(r => r);
|
217 | if (this.config.withTypes) {
|
218 | types = types.concat([this.buildTypeTransfomer(name, fieldNodes)]);
|
219 | }
|
220 | return types.join('\n');
|
221 | }
|
222 | }
|
223 |
|
224 | const plugin = async (schema, documents, config, { outputFile }) => {
|
225 | const relevantPath = dirname(normalize(outputFile));
|
226 | const defaultPackageName = buildPackageNameFromPath(relevantPath);
|
227 | const visitor = new KotlinResolversVisitor(config, schema, defaultPackageName);
|
228 | const astNode = getCachedDocumentNodeFromSchema(schema);
|
229 | const visitorResult = visit(astNode, { leave: visitor });
|
230 | const packageName = visitor.getPackageName();
|
231 | const blockContent = visitorResult.definitions.filter(d => typeof d === 'string').join('\n\n');
|
232 | return [packageName, blockContent].join('\n');
|
233 | };
|
234 |
|
235 | export { plugin };
|