ts-class-to-openapi 1.0.6 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16,10 +16,10 @@ const jsPrimitives = {
16
16
  Object: { type: 'Object', value: 'object' },
17
17
  Array: { type: 'Array', value: 'array' },
18
18
  Date: { type: 'Date', value: 'string', format: 'date-time' },
19
- Buffer: { type: 'Buffer', value: 'string'},
20
- Uint8Array: { type: 'Uint8Array', value: 'string'},
19
+ Buffer: { type: 'Buffer'},
20
+ Uint8Array: { type: 'Uint8Array'},
21
21
  UploadFile: { type: 'UploadFile', value: 'string', format: 'binary' },
22
- File: { type: 'File', value: 'string'},
22
+ File: { type: 'File'},
23
23
  };
24
24
  const validatorDecorators = {
25
25
  Length: { name: 'Length'},
@@ -68,9 +68,42 @@ class SchemaTransformer {
68
68
  this.program = ts.createProgram(fileNames, tsOptions);
69
69
  this.checker = this.program.getTypeChecker();
70
70
  }
71
- getPropertiesByClassDeclaration(classNode) {
71
+ getPropertiesByClassDeclaration(classNode, visitedDeclarations = new Set()) {
72
+ if (visitedDeclarations.has(classNode)) {
73
+ return [];
74
+ }
75
+ visitedDeclarations.add(classNode);
76
+ // if no heritage clauses, get properties directly from class
77
+ if (!classNode.heritageClauses) {
78
+ return this.getPropertiesByClassMembers(classNode.members, classNode);
79
+ } // use heritage clauses to get properties from base classes
80
+ else {
81
+ const heritageClause = classNode.heritageClauses[0];
82
+ if (heritageClause &&
83
+ heritageClause.token === ts.SyntaxKind.ExtendsKeyword) {
84
+ const type = heritageClause.types[0];
85
+ let properties = [];
86
+ let baseProperties = [];
87
+ if (!type)
88
+ return [];
89
+ const symbol = this.checker.getSymbolAtLocation(type.expression);
90
+ if (!symbol)
91
+ return [];
92
+ const declaration = symbol.declarations?.[0];
93
+ if (declaration && ts.isClassDeclaration(declaration)) {
94
+ baseProperties = this.getPropertiesByClassDeclaration(declaration, visitedDeclarations);
95
+ }
96
+ properties = this.getPropertiesByClassMembers(classNode.members, classNode);
97
+ return baseProperties.concat(properties);
98
+ }
99
+ else {
100
+ return this.getPropertiesByClassMembers(classNode.members, classNode);
101
+ }
102
+ }
103
+ }
104
+ getPropertiesByClassMembers(members, parentClassNode) {
72
105
  const properties = [];
73
- for (const member of classNode.members) {
106
+ for (const member of members) {
74
107
  if (ts.isPropertyDeclaration(member) &&
75
108
  member.name &&
76
109
  ts.isIdentifier(member.name)) {
@@ -80,6 +113,8 @@ class SchemaTransformer {
80
113
  const isOptional = !!member.questionToken;
81
114
  const isGeneric = this.isPropertyTypeGeneric(member);
82
115
  const isPrimitive = this.isPrimitiveType(type);
116
+ const isClassType = this.isClassType(member);
117
+ const isArray = this.isArrayProperty(member);
83
118
  const property = {
84
119
  name: propertyName,
85
120
  type,
@@ -88,9 +123,25 @@ class SchemaTransformer {
88
123
  isGeneric,
89
124
  originalProperty: member,
90
125
  isPrimitive,
91
- isClassType: this.isClassType(member),
92
- isArray: this.isArrayProperty(member),
126
+ isClassType,
127
+ isArray,
128
+ isRef: false,
93
129
  };
130
+ // Check for self-referencing properties to mark as $ref
131
+ if (property.isClassType) {
132
+ const declaration = this.getDeclarationProperty(property);
133
+ if (parentClassNode) {
134
+ if (declaration &&
135
+ declaration.name &&
136
+ this.checker.getSymbolAtLocation(declaration.name) ===
137
+ this.checker.getSymbolAtLocation(parentClassNode.name)) {
138
+ property.isRef = true;
139
+ }
140
+ }
141
+ else {
142
+ debugger;
143
+ }
144
+ }
94
145
  properties.push(property);
95
146
  }
96
147
  }
@@ -458,7 +509,7 @@ class SchemaTransformer {
458
509
  }
459
510
  return ts.isArrayTypeNode(propertyDeclaration.type);
460
511
  }
461
- getSchemaFromProperties({ properties, visitedClass, transformedSchema, }) {
512
+ getSchemaFromProperties({ properties, visitedClass, transformedSchema, classDeclaration, }) {
462
513
  let schema = {};
463
514
  const required = [];
464
515
  for (const property of properties) {
@@ -466,6 +517,7 @@ class SchemaTransformer {
466
517
  property,
467
518
  visitedClass,
468
519
  transformedSchema,
520
+ classDeclaration,
469
521
  });
470
522
  // this.applyDecorators(property, schema as SchemaType)
471
523
  if (!property.isOptional) {
@@ -478,17 +530,35 @@ class SchemaTransformer {
478
530
  required: required.length ? required : undefined,
479
531
  };
480
532
  }
481
- getSchemaFromProperty({ property, visitedClass, transformedSchema, }) {
533
+ getSchemaFromProperty({ property, visitedClass, transformedSchema, classDeclaration, }) {
482
534
  let schema = {};
483
535
  if (property.isPrimitive) {
484
536
  schema = this.getSchemaFromPrimitive(property);
485
537
  }
486
538
  else if (property.isClassType) {
487
- schema = this.getSchemaFromClass({
488
- property,
489
- visitedClass,
490
- transformedSchema,
491
- });
539
+ const declaration = this.getDeclarationProperty(property);
540
+ if (property.isRef && classDeclaration.name) {
541
+ // Self-referencing property, handle as a reference to avoid infinite recursion
542
+ if (property.isArray) {
543
+ schema.type = 'array';
544
+ schema.items = {
545
+ $ref: `#/components/schemas/${classDeclaration.name.text}`,
546
+ };
547
+ }
548
+ else {
549
+ schema = {
550
+ $ref: `#/components/schemas/${classDeclaration.name.text}`,
551
+ };
552
+ }
553
+ }
554
+ else {
555
+ schema = this.getSchemaFromClass({
556
+ isArray: property.isArray,
557
+ visitedClass,
558
+ transformedSchema,
559
+ declaration,
560
+ });
561
+ }
492
562
  }
493
563
  else {
494
564
  schema = { type: 'object', properties: {}, additionalProperties: true };
@@ -496,21 +566,26 @@ class SchemaTransformer {
496
566
  this.applyDecorators(property, schema);
497
567
  return schema;
498
568
  }
499
- getSchemaFromClass({ property, transformedSchema = new Map(), visitedClass = new Set(), }) {
569
+ getSchemaFromClass({ transformedSchema = new Map(), visitedClass = new Set(), declaration, isArray, }) {
500
570
  let schema = { type: 'object' };
501
- const declaration = this.getDeclarationProperty(property);
502
571
  if (!declaration ||
503
572
  !ts.isClassDeclaration(declaration) ||
504
573
  !declaration.name) {
505
574
  return { type: 'object' };
506
575
  }
507
576
  if (visitedClass.has(declaration)) {
508
- if (transformedSchema.has(declaration.name.text)) {
509
- return transformedSchema.get(declaration.name.text);
577
+ if (isArray) {
578
+ schema.type = 'array';
579
+ schema.items = {
580
+ $ref: `#/components/schemas/${declaration.name.text}`,
581
+ };
510
582
  }
511
- return {
512
- $ref: `#/components/schemas/${declaration.name.text}`,
513
- };
583
+ else {
584
+ schema = {
585
+ $ref: `#/components/schemas/${declaration.name.text}`,
586
+ };
587
+ }
588
+ return schema;
514
589
  }
515
590
  visitedClass.add(declaration);
516
591
  const properties = this.getPropertiesByClassDeclaration(declaration);
@@ -518,8 +593,9 @@ class SchemaTransformer {
518
593
  properties,
519
594
  visitedClass,
520
595
  transformedSchema: transformedSchema,
596
+ classDeclaration: declaration,
521
597
  });
522
- if (property.isArray) {
598
+ if (isArray) {
523
599
  schema.type = 'array';
524
600
  schema.items = {
525
601
  type: transformerProps.type,
@@ -533,14 +609,12 @@ class SchemaTransformer {
533
609
  schema.required = transformerProps.required;
534
610
  }
535
611
  transformedSchema.set(declaration.name.text, schema);
536
- if (schema.properties && Object.keys(schema.properties).length === 0) {
537
- schema = { type: 'object', properties: {}, additionalProperties: true };
538
- }
539
612
  return schema;
540
613
  }
541
614
  getSchemaFromPrimitive(property) {
542
615
  const propertySchema = { type: 'object' };
543
616
  const propertyType = property.type.toLowerCase().replace('[]', '').trim();
617
+ let isFile = false;
544
618
  switch (propertyType) {
545
619
  case constants.jsPrimitives.String.value:
546
620
  propertySchema.type = constants.jsPrimitives.String.value;
@@ -557,12 +631,13 @@ class SchemaTransformer {
557
631
  propertySchema.type = constants.jsPrimitives.Date.value;
558
632
  propertySchema.format = constants.jsPrimitives.Date.format;
559
633
  break;
560
- case constants.jsPrimitives.Buffer.value:
561
- case constants.jsPrimitives.Uint8Array.value:
562
- case constants.jsPrimitives.File.value:
563
- case constants.jsPrimitives.UploadFile.value:
634
+ case constants.jsPrimitives.Buffer.type.toLocaleLowerCase():
635
+ case constants.jsPrimitives.Uint8Array.type.toLocaleLowerCase():
636
+ case constants.jsPrimitives.File.type.toLocaleLowerCase():
637
+ case constants.jsPrimitives.UploadFile.type.toLocaleLowerCase():
564
638
  propertySchema.type = constants.jsPrimitives.UploadFile.value;
565
639
  propertySchema.format = constants.jsPrimitives.UploadFile.format;
640
+ isFile = true;
566
641
  break;
567
642
  case constants.jsPrimitives.Array.value:
568
643
  propertySchema.type = constants.jsPrimitives.Array.value;
@@ -581,7 +656,9 @@ class SchemaTransformer {
581
656
  }
582
657
  if (property.isArray) {
583
658
  propertySchema.type = `array`;
584
- propertySchema.items = { type: propertyType };
659
+ propertySchema.items = {
660
+ type: isFile ? constants.jsPrimitives.UploadFile.value : propertyType,
661
+ };
585
662
  }
586
663
  return propertySchema;
587
664
  }
@@ -697,7 +774,10 @@ class SchemaTransformer {
697
774
  return { name: cls.name, schema: {} };
698
775
  }
699
776
  const properties = this.getPropertiesByClassDeclaration(result.node);
700
- schema = this.getSchemaFromProperties({ properties });
777
+ schema = this.getSchemaFromProperties({
778
+ properties,
779
+ classDeclaration: result.node,
780
+ });
701
781
  return { name: cls.name, schema };
702
782
  }
703
783
  }