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