UNPKG

20.5 kBJavaScriptView Raw
1import { Kind, isScalarType, isInputObjectType, isEnumType, printSchema, parse, visit } from 'graphql';
2import { indent, indentMultiline, BaseVisitor, buildScalarsFromConfig, getBaseTypeNode } from '@graphql-codegen/visitor-plugin-common';
3import { pascalCase } from 'change-case-all';
4
5const C_SHARP_SCALARS = {
6 ID: 'string',
7 String: 'string',
8 Boolean: 'bool',
9 Int: 'int',
10 Float: 'float',
11 Date: 'DateTime',
12};
13// All native C# built-in value types
14// See https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/built-in-types
15const 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
31function 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}
46function isStringValueNode(node) {
47 return node && typeof node === 'object' && node.kind === Kind.STRING;
48}
49function isValueType(type) {
50 // Limitation: only checks the list of known built in value types
51 // Eg .NET types and struct types won't be detected correctly
52 return csharpNativeValueTypes.includes(type);
53}
54function 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}
73function 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}
84function 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
94class 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
184class 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 * C# keywords
199 * https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/
200 */
201const 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
282class 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 * Checks name against list of keywords. If it is, will prefix value with @
297 *
298 * Note:
299 * This class should first invoke the convertName from base-visitor to convert the string or node
300 * value according the naming configuration, eg upper or lower case. Then resulting string checked
301 * against the list or keywords.
302 * However the generated C# code is not yet able to handle fields that are in a different case so
303 * the invocation of convertName is omitted purposely.
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 // Required field is optional when default value specified, see #4273
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 // record
515 fieldName = this.convertSafeName(pascalCase(this.convertName(arg.name)));
516 getterSetter = '{ get; }';
517 }
518 else {
519 // class
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
588const 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
600export { plugin };
601//# sourceMappingURL=index.esm.js.map