UNPKG

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