1 | import { Kind, isScalarType, isInputObjectType, isEnumType, printSchema, parse, visit } from 'graphql';
|
2 | import { indent, indentMultiline, BaseVisitor, buildScalarsFromConfig, getBaseTypeNode } from '@graphql-codegen/visitor-plugin-common';
|
3 | import { pascalCase } from 'change-case-all';
|
4 |
|
5 | const C_SHARP_SCALARS = {
|
6 | ID: 'string',
|
7 | String: 'string',
|
8 | Boolean: 'bool',
|
9 | Int: 'int',
|
10 | Float: 'float',
|
11 | Date: 'DateTime',
|
12 | };
|
13 |
|
14 |
|
15 | const csharpNativeValueTypes = [
|
16 | 'bool',
|
17 | 'byte',
|
18 | 'sbyte',
|
19 | 'char',
|
20 | 'decimal',
|
21 | 'double',
|
22 | 'float',
|
23 | 'int',
|
24 | 'uint',
|
25 | 'long',
|
26 | 'ulong',
|
27 | 'short',
|
28 | 'ushort',
|
29 | ];
|
30 |
|
31 | function transformComment(comment, indentLevel = 0) {
|
32 | if (!comment) {
|
33 | return '';
|
34 | }
|
35 | if (isStringValueNode(comment)) {
|
36 | comment = comment.value;
|
37 | }
|
38 | comment = comment.trimStart().split('*/').join('*\\/');
|
39 | let lines = comment.split('\n');
|
40 | lines = ['/// <summary>', ...lines.map(line => `/// ${line}`), '/// </summary>'];
|
41 | return lines
|
42 | .map(line => indent(line, indentLevel))
|
43 | .concat('')
|
44 | .join('\n');
|
45 | }
|
46 | function isStringValueNode(node) {
|
47 | return node && typeof node === 'object' && node.kind === Kind.STRING;
|
48 | }
|
49 | function isValueType(type) {
|
50 |
|
51 |
|
52 | return csharpNativeValueTypes.includes(type);
|
53 | }
|
54 | function getListTypeField(typeNode) {
|
55 | if (typeNode.kind === Kind.LIST_TYPE) {
|
56 | return {
|
57 | required: false,
|
58 | type: getListTypeField(typeNode.type),
|
59 | };
|
60 | }
|
61 | else if (typeNode.kind === Kind.NON_NULL_TYPE && typeNode.type.kind === Kind.LIST_TYPE) {
|
62 | return Object.assign(getListTypeField(typeNode.type), {
|
63 | required: true,
|
64 | });
|
65 | }
|
66 | else if (typeNode.kind === Kind.NON_NULL_TYPE) {
|
67 | return getListTypeField(typeNode.type);
|
68 | }
|
69 | else {
|
70 | return undefined;
|
71 | }
|
72 | }
|
73 | function getListInnerTypeNode(typeNode) {
|
74 | if (typeNode.kind === Kind.LIST_TYPE) {
|
75 | return getListInnerTypeNode(typeNode.type);
|
76 | }
|
77 | else if (typeNode.kind === Kind.NON_NULL_TYPE && typeNode.type.kind === Kind.LIST_TYPE) {
|
78 | return getListInnerTypeNode(typeNode.type);
|
79 | }
|
80 | else {
|
81 | return typeNode;
|
82 | }
|
83 | }
|
84 | function wrapFieldType(fieldType, listTypeField, listType = 'IEnumerable') {
|
85 | if (listTypeField) {
|
86 | const innerType = wrapFieldType(fieldType, listTypeField.type, listType);
|
87 | return `${listType}<${innerType}>`;
|
88 | }
|
89 | else {
|
90 | return fieldType.innerTypeName;
|
91 | }
|
92 | }
|
93 |
|
94 | class CSharpDeclarationBlock {
|
95 | constructor() {
|
96 | this._name = null;
|
97 | this._extendStr = [];
|
98 | this._implementsStr = [];
|
99 | this._kind = null;
|
100 | this._access = 'public';
|
101 | this._final = false;
|
102 | this._static = false;
|
103 | this._block = null;
|
104 | this._comment = null;
|
105 | this._nestedClasses = [];
|
106 | }
|
107 | nestedClass(nstCls) {
|
108 | this._nestedClasses.push(nstCls);
|
109 | return this;
|
110 | }
|
111 | access(access) {
|
112 | this._access = access;
|
113 | return this;
|
114 | }
|
115 | asKind(kind) {
|
116 | this._kind = kind;
|
117 | return this;
|
118 | }
|
119 | final() {
|
120 | this._final = true;
|
121 | return this;
|
122 | }
|
123 | static() {
|
124 | this._static = true;
|
125 | return this;
|
126 | }
|
127 | withComment(comment) {
|
128 | if (comment) {
|
129 | this._comment = transformComment(comment, 1);
|
130 | }
|
131 | return this;
|
132 | }
|
133 | withBlock(block) {
|
134 | this._block = block;
|
135 | return this;
|
136 | }
|
137 | extends(extendStr) {
|
138 | this._extendStr = extendStr;
|
139 | return this;
|
140 | }
|
141 | implements(implementsStr) {
|
142 | this._implementsStr = implementsStr;
|
143 | return this;
|
144 | }
|
145 | withName(name) {
|
146 | this._name = typeof name === 'object' ? name.value : name;
|
147 | return this;
|
148 | }
|
149 | get string() {
|
150 | let result = '';
|
151 | if (this._kind) {
|
152 | let name = '';
|
153 | if (this._name) {
|
154 | name = this._name;
|
155 | }
|
156 | if (this._kind === 'namespace') {
|
157 | result += `${this._kind} ${name} `;
|
158 | }
|
159 | else {
|
160 | let extendStr = '';
|
161 | let implementsStr = '';
|
162 | const final = this._final ? ' final' : '';
|
163 | const isStatic = this._static ? ' static' : '';
|
164 | if (this._extendStr.length > 0) {
|
165 | extendStr = ` : ${this._extendStr.join(', ')}`;
|
166 | }
|
167 | if (this._implementsStr.length > 0) {
|
168 | implementsStr = ` : ${this._implementsStr.join(', ')}`;
|
169 | }
|
170 | result += `${this._access}${isStatic}${final} ${this._kind} ${name}${extendStr}${implementsStr} `;
|
171 | }
|
172 | }
|
173 | const nestedClasses = this._nestedClasses.length
|
174 | ? this._nestedClasses.map(c => indentMultiline(c.string)).join('\n\n')
|
175 | : null;
|
176 | const before = '{';
|
177 | const after = '}';
|
178 | const block = [before, nestedClasses, this._block, after].filter(f => f).join('\n');
|
179 | result += block;
|
180 | return (this._comment ? this._comment : '') + result + '\n';
|
181 | }
|
182 | }
|
183 |
|
184 | class CSharpFieldType {
|
185 | constructor(fieldType) {
|
186 | Object.assign(this, fieldType);
|
187 | }
|
188 | get innerTypeName() {
|
189 | const nullable = this.baseType.valueType && !this.baseType.required ? '?' : '';
|
190 | return `${this.baseType.type}${nullable}`;
|
191 | }
|
192 | get isOuterTypeRequired() {
|
193 | return this.listType ? this.listType.required : this.baseType.required;
|
194 | }
|
195 | }
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 | const csharpKeywords = [
|
202 | 'abstract',
|
203 | 'as',
|
204 | 'base',
|
205 | 'bool',
|
206 | 'break',
|
207 | 'byte',
|
208 | 'case',
|
209 | 'catch',
|
210 | 'char',
|
211 | 'checked',
|
212 | 'class',
|
213 | 'const',
|
214 | 'continue',
|
215 | 'decimal',
|
216 | 'default',
|
217 | 'delegate',
|
218 | 'do',
|
219 | 'double',
|
220 | 'else',
|
221 | 'enum',
|
222 | 'event',
|
223 | 'explicit',
|
224 | 'extern',
|
225 | 'false',
|
226 | 'finally',
|
227 | 'fixed',
|
228 | 'float',
|
229 | 'for',
|
230 | 'foreach',
|
231 | 'goto',
|
232 | 'if',
|
233 | 'implicit',
|
234 | 'in',
|
235 | 'int',
|
236 | 'interface',
|
237 | 'internal',
|
238 | 'is',
|
239 | 'lock',
|
240 | 'long',
|
241 | 'namespace',
|
242 | 'new',
|
243 | 'null',
|
244 | 'object',
|
245 | 'operator',
|
246 | 'out',
|
247 | 'override',
|
248 | 'params',
|
249 | 'private',
|
250 | 'protected',
|
251 | 'public',
|
252 | 'readonly',
|
253 | 'record',
|
254 | 'ref',
|
255 | 'return',
|
256 | 'sbyte',
|
257 | 'sealed',
|
258 | 'short',
|
259 | 'sizeof',
|
260 | 'stackalloc',
|
261 | 'static',
|
262 | 'string',
|
263 | 'struct',
|
264 | 'switch',
|
265 | 'this',
|
266 | 'throw',
|
267 | 'true',
|
268 | 'try',
|
269 | 'typeof',
|
270 | 'uint',
|
271 | 'ulong',
|
272 | 'unchecked',
|
273 | 'unsafe',
|
274 | 'ushort',
|
275 | 'using',
|
276 | 'virtual',
|
277 | 'void',
|
278 | 'volatile',
|
279 | 'while',
|
280 | ];
|
281 |
|
282 | class CSharpResolversVisitor extends BaseVisitor {
|
283 | constructor(rawConfig, _schema) {
|
284 | super(rawConfig, {
|
285 | enumValues: rawConfig.enumValues || {},
|
286 | listType: rawConfig.listType || 'List',
|
287 | namespaceName: rawConfig.namespaceName || 'GraphQLCodeGen',
|
288 | className: rawConfig.className || 'Types',
|
289 | emitRecords: rawConfig.emitRecords || false,
|
290 | scalars: buildScalarsFromConfig(_schema, rawConfig, C_SHARP_SCALARS),
|
291 | });
|
292 | this._schema = _schema;
|
293 | this.keywords = new Set(csharpKeywords);
|
294 | }
|
295 | |
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 | convertSafeName(node) {
|
306 | const name = typeof node === 'string' ? node : node.value;
|
307 | return this.keywords.has(name) ? `@${name}` : name;
|
308 | }
|
309 | getImports() {
|
310 | const allImports = ['System', 'System.Collections.Generic', 'Newtonsoft.Json', 'GraphQL'];
|
311 | return allImports.map(i => `using ${i};`).join('\n') + '\n';
|
312 | }
|
313 | wrapWithNamespace(content) {
|
314 | return new CSharpDeclarationBlock()
|
315 | .asKind('namespace')
|
316 | .withName(this.config.namespaceName)
|
317 | .withBlock(indentMultiline(content)).string;
|
318 | }
|
319 | wrapWithClass(content) {
|
320 | return new CSharpDeclarationBlock()
|
321 | .access('public')
|
322 | .asKind('class')
|
323 | .withName(this.convertSafeName(this.config.className))
|
324 | .withBlock(indentMultiline(content)).string;
|
325 | }
|
326 | getEnumValue(enumName, enumOption) {
|
327 | if (this.config.enumValues[enumName] &&
|
328 | typeof this.config.enumValues[enumName] === 'object' &&
|
329 | this.config.enumValues[enumName][enumOption]) {
|
330 | return this.config.enumValues[enumName][enumOption];
|
331 | }
|
332 | return enumOption;
|
333 | }
|
334 | EnumValueDefinition(node) {
|
335 | return (enumName) => {
|
336 | const enumHeader = this.getFieldHeader(node);
|
337 | const enumOption = this.convertSafeName(node.name);
|
338 | return enumHeader + indent(this.getEnumValue(enumName, enumOption));
|
339 | };
|
340 | }
|
341 | EnumTypeDefinition(node) {
|
342 | const enumName = this.convertName(node.name);
|
343 | const enumValues = node.values.map(enumValue => enumValue(node.name.value)).join(',\n');
|
344 | const enumBlock = [enumValues].join('\n');
|
345 | return new CSharpDeclarationBlock()
|
346 | .access('public')
|
347 | .asKind('enum')
|
348 | .withComment(node.description)
|
349 | .withName(enumName)
|
350 | .withBlock(enumBlock).string;
|
351 | }
|
352 | getFieldHeader(node, fieldType) {
|
353 | var _a;
|
354 | const attributes = [];
|
355 | const commentText = transformComment((_a = node.description) === null || _a === void 0 ? void 0 : _a.value);
|
356 | const deprecationDirective = node.directives.find(v => { var _a; return ((_a = v.name) === null || _a === void 0 ? void 0 : _a.value) === 'deprecated'; });
|
357 | if (deprecationDirective) {
|
358 | const deprecationReason = this.getDeprecationReason(deprecationDirective);
|
359 | attributes.push(`[Obsolete("${deprecationReason}")]`);
|
360 | }
|
361 | if (node.kind === Kind.FIELD_DEFINITION) {
|
362 | attributes.push(`[JsonProperty("${node.name.value}")]`);
|
363 | }
|
364 | if (node.kind === Kind.INPUT_VALUE_DEFINITION && fieldType.isOuterTypeRequired) {
|
365 | attributes.push(`[JsonRequired]`);
|
366 | }
|
367 | if (commentText || attributes.length > 0) {
|
368 | const summary = commentText ? indentMultiline(commentText.trimRight()) + '\n' : '';
|
369 | const attributeLines = attributes.length > 0
|
370 | ? attributes
|
371 | .map(attr => indent(attr))
|
372 | .concat('')
|
373 | .join('\n')
|
374 | : '';
|
375 | return summary + attributeLines;
|
376 | }
|
377 | return '';
|
378 | }
|
379 | getDeprecationReason(directive) {
|
380 | if (directive.name.value !== 'deprecated') {
|
381 | return '';
|
382 | }
|
383 | const hasArguments = directive.arguments.length > 0;
|
384 | let reason = 'Field no longer supported';
|
385 | if (hasArguments && directive.arguments[0].value.kind === Kind.STRING) {
|
386 | reason = directive.arguments[0].value.value;
|
387 | }
|
388 | return reason;
|
389 | }
|
390 | resolveInputFieldType(typeNode, hasDefaultValue = false) {
|
391 | const innerType = getBaseTypeNode(typeNode);
|
392 | const schemaType = this._schema.getType(innerType.name.value);
|
393 | const listType = getListTypeField(typeNode);
|
394 | const required = getListInnerTypeNode(typeNode).kind === Kind.NON_NULL_TYPE;
|
395 | let result = null;
|
396 | if (isScalarType(schemaType)) {
|
397 | if (this.scalars[schemaType.name]) {
|
398 | const baseType = this.scalars[schemaType.name];
|
399 | result = new CSharpFieldType({
|
400 | baseType: {
|
401 | type: baseType,
|
402 | required,
|
403 | valueType: isValueType(baseType),
|
404 | },
|
405 | listType,
|
406 | });
|
407 | }
|
408 | else {
|
409 | result = new CSharpFieldType({
|
410 | baseType: {
|
411 | type: 'object',
|
412 | required,
|
413 | valueType: false,
|
414 | },
|
415 | listType,
|
416 | });
|
417 | }
|
418 | }
|
419 | else if (isInputObjectType(schemaType)) {
|
420 | result = new CSharpFieldType({
|
421 | baseType: {
|
422 | type: `${this.convertName(schemaType.name)}`,
|
423 | required,
|
424 | valueType: false,
|
425 | },
|
426 | listType,
|
427 | });
|
428 | }
|
429 | else if (isEnumType(schemaType)) {
|
430 | result = new CSharpFieldType({
|
431 | baseType: {
|
432 | type: this.convertName(schemaType.name),
|
433 | required,
|
434 | valueType: true,
|
435 | },
|
436 | listType,
|
437 | });
|
438 | }
|
439 | else {
|
440 | result = new CSharpFieldType({
|
441 | baseType: {
|
442 | type: `${schemaType.name}`,
|
443 | required,
|
444 | valueType: false,
|
445 | },
|
446 | listType,
|
447 | });
|
448 | }
|
449 | if (hasDefaultValue) {
|
450 |
|
451 | (result.listType || result.baseType).required = false;
|
452 | }
|
453 | return result;
|
454 | }
|
455 | buildRecord(name, description, inputValueArray, interfaces) {
|
456 | const classSummary = transformComment(description === null || description === void 0 ? void 0 : description.value);
|
457 | const interfaceImpl = interfaces && interfaces.length > 0 ? ` : ${interfaces.map(ntn => ntn.name.value).join(', ')}` : '';
|
458 | const recordMembers = inputValueArray
|
459 | .map(arg => {
|
460 | const fieldType = this.resolveInputFieldType(arg.type);
|
461 | const fieldHeader = this.getFieldHeader(arg, fieldType);
|
462 | const fieldName = this.convertSafeName(pascalCase(this.convertName(arg.name)));
|
463 | const csharpFieldType = wrapFieldType(fieldType, fieldType.listType, this.config.listType);
|
464 | return fieldHeader + indent(`public ${csharpFieldType} ${fieldName} { get; init; } = ${fieldName};`);
|
465 | })
|
466 | .join('\n\n');
|
467 | const recordInitializer = inputValueArray
|
468 | .map(arg => {
|
469 | const fieldType = this.resolveInputFieldType(arg.type);
|
470 | const fieldName = this.convertSafeName(pascalCase(this.convertName(arg.name)));
|
471 | const csharpFieldType = wrapFieldType(fieldType, fieldType.listType, this.config.listType);
|
472 | return `${csharpFieldType} ${fieldName}`;
|
473 | })
|
474 | .join(', ');
|
475 | return `
|
476 | #region ${name}
|
477 | ${classSummary}public record ${this.convertSafeName(name)}(${recordInitializer})${interfaceImpl} {
|
478 | #region members
|
479 | ${recordMembers}
|
480 | #endregion
|
481 | }
|
482 | #endregion`;
|
483 | }
|
484 | buildClass(name, description, inputValueArray, interfaces) {
|
485 | const classSummary = transformComment(description === null || description === void 0 ? void 0 : description.value);
|
486 | const interfaceImpl = interfaces && interfaces.length > 0 ? ` : ${interfaces.map(ntn => ntn.name.value).join(', ')}` : '';
|
487 | const classMembers = inputValueArray
|
488 | .map(arg => {
|
489 | const fieldType = this.resolveInputFieldType(arg.type);
|
490 | const fieldHeader = this.getFieldHeader(arg, fieldType);
|
491 | const fieldName = this.convertSafeName(arg.name);
|
492 | const csharpFieldType = wrapFieldType(fieldType, fieldType.listType, this.config.listType);
|
493 | return fieldHeader + indent(`public ${csharpFieldType} ${fieldName} { get; set; }`);
|
494 | })
|
495 | .join('\n\n');
|
496 | return `
|
497 | #region ${name}
|
498 | ${classSummary}public class ${this.convertSafeName(name)}${interfaceImpl} {
|
499 | #region members
|
500 | ${classMembers}
|
501 | #endregion
|
502 | }
|
503 | #endregion`;
|
504 | }
|
505 | buildInterface(name, description, inputValueArray) {
|
506 | const classSummary = transformComment(description === null || description === void 0 ? void 0 : description.value);
|
507 | const classMembers = inputValueArray
|
508 | .map(arg => {
|
509 | const fieldType = this.resolveInputFieldType(arg.type);
|
510 | const fieldHeader = this.getFieldHeader(arg, fieldType);
|
511 | let fieldName;
|
512 | let getterSetter;
|
513 | if (this.config.emitRecords) {
|
514 |
|
515 | fieldName = this.convertSafeName(pascalCase(this.convertName(arg.name)));
|
516 | getterSetter = '{ get; }';
|
517 | }
|
518 | else {
|
519 |
|
520 | fieldName = this.convertSafeName(arg.name);
|
521 | getterSetter = '{ get; set; }';
|
522 | }
|
523 | const csharpFieldType = wrapFieldType(fieldType, fieldType.listType, this.config.listType);
|
524 | return fieldHeader + indent(`public ${csharpFieldType} ${fieldName} ${getterSetter}`);
|
525 | })
|
526 | .join('\n\n');
|
527 | return `
|
528 | ${classSummary}public interface ${this.convertSafeName(name)} {
|
529 | ${classMembers}
|
530 | }`;
|
531 | }
|
532 | buildInputTransformer(name, description, inputValueArray) {
|
533 | const classSummary = transformComment(description === null || description === void 0 ? void 0 : description.value);
|
534 | const classMembers = inputValueArray
|
535 | .map(arg => {
|
536 | const fieldType = this.resolveInputFieldType(arg.type, !!arg.defaultValue);
|
537 | const fieldHeader = this.getFieldHeader(arg, fieldType);
|
538 | const fieldName = this.convertSafeName(arg.name);
|
539 | const csharpFieldType = wrapFieldType(fieldType, fieldType.listType, this.config.listType);
|
540 | return fieldHeader + indent(`public ${csharpFieldType} ${fieldName} { get; set; }`);
|
541 | })
|
542 | .join('\n\n');
|
543 | return `
|
544 | #region ${name}
|
545 | ${classSummary}public class ${this.convertSafeName(name)} {
|
546 | #region members
|
547 | ${classMembers}
|
548 | #endregion
|
549 |
|
550 | #region methods
|
551 | public dynamic GetInputObject()
|
552 | {
|
553 | IDictionary<string, object> d = new System.Dynamic.ExpandoObject();
|
554 |
|
555 | var properties = GetType().GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
|
556 | foreach (var propertyInfo in properties)
|
557 | {
|
558 | var value = propertyInfo.GetValue(this);
|
559 | var defaultValue = propertyInfo.PropertyType.IsValueType ? Activator.CreateInstance(propertyInfo.PropertyType) : null;
|
560 |
|
561 | var requiredProp = propertyInfo.GetCustomAttributes(typeof(JsonRequiredAttribute), false).Length > 0;
|
562 | if (requiredProp || value != defaultValue)
|
563 | {
|
564 | d[propertyInfo.Name] = value;
|
565 | }
|
566 | }
|
567 | return d;
|
568 | }
|
569 | #endregion
|
570 | }
|
571 | #endregion`;
|
572 | }
|
573 | InputObjectTypeDefinition(node) {
|
574 | const name = `${this.convertName(node)}`;
|
575 | return this.buildInputTransformer(name, node.description, node.fields);
|
576 | }
|
577 | ObjectTypeDefinition(node) {
|
578 | if (this.config.emitRecords) {
|
579 | return this.buildRecord(node.name.value, node.description, node.fields, node.interfaces);
|
580 | }
|
581 | return this.buildClass(node.name.value, node.description, node.fields, node.interfaces);
|
582 | }
|
583 | InterfaceTypeDefinition(node) {
|
584 | return this.buildInterface(node.name.value, node.description, node.fields);
|
585 | }
|
586 | }
|
587 |
|
588 | const plugin = async (schema, documents, config) => {
|
589 | const visitor = new CSharpResolversVisitor(config, schema);
|
590 | const printedSchema = printSchema(schema);
|
591 | const astNode = parse(printedSchema);
|
592 | const visitorResult = visit(astNode, { leave: visitor });
|
593 | const imports = visitor.getImports();
|
594 | const blockContent = visitorResult.definitions.filter(d => typeof d === 'string').join('\n');
|
595 | const wrappedBlockContent = visitor.wrapWithClass(blockContent);
|
596 | const wrappedContent = visitor.wrapWithNamespace(wrappedBlockContent);
|
597 | return [imports, wrappedContent].join('\n');
|
598 | };
|
599 |
|
600 | export { plugin };
|
601 |
|