ts-class-to-openapi 1.0.6 → 1.1.0

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.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Test classes for circular reference scenarios
3
+ * These classes are designed to test the handling of recursive references
4
+ */
5
+ /**
6
+ * Self-referencing class through direct property
7
+ */
8
+ export declare class SelfReferenceDirectClass {
9
+ id: number;
10
+ name: string;
11
+ parent?: SelfReferenceDirectClass;
12
+ children: SelfReferenceDirectClass[];
13
+ }
14
+ /**
15
+ * Metadata class for nested circular references
16
+ */
17
+ export declare class NestedMetadata {
18
+ createdBy?: SelfReferenceNestedClass;
19
+ modifiedBy?: SelfReferenceNestedClass;
20
+ }
21
+ /**
22
+ * Self-referencing class through nested property
23
+ */
24
+ export declare class SelfReferenceNestedClass {
25
+ id: number;
26
+ metadata: NestedMetadata;
27
+ }
28
+ /**
29
+ * Second class in indirect circular reference
30
+ */
31
+ export declare class NodeDataClass {
32
+ description: string;
33
+ parentNode: NodeClass;
34
+ }
35
+ /**
36
+ * First class in indirect circular reference
37
+ */
38
+ export declare class NodeClass {
39
+ id: number;
40
+ name: string;
41
+ nodeData: NodeDataClass;
42
+ }
43
+ /**
44
+ * Third class in deep circular chain
45
+ */
46
+ export declare class ClassC {
47
+ id: number;
48
+ value: number;
49
+ nextRef: ClassA;
50
+ }
51
+ /**
52
+ * Second class in deep circular chain
53
+ */
54
+ export declare class ClassB {
55
+ id: number;
56
+ description: string;
57
+ nextRef: ClassC;
58
+ }
59
+ /**
60
+ * First class in deep circular chain
61
+ */
62
+ export declare class ClassA {
63
+ id: number;
64
+ name: string;
65
+ nextRef: ClassB;
66
+ }
67
+ /**
68
+ * Multiple circular paths in the same class
69
+ */
70
+ export declare class MultiPathCircularClass {
71
+ id: number;
72
+ selfRef1?: MultiPathCircularClass;
73
+ selfRef2?: MultiPathCircularClass;
74
+ manyRefs: MultiPathCircularClass[];
75
+ }
76
+ /**
77
+ * Base generic class that can create circular references
78
+ */
79
+ export declare class GenericContainer {
80
+ value: any;
81
+ metadata: Record<string, any>;
82
+ related?: GenericContainer;
83
+ }
84
+ /**
85
+ * Concrete implementation with self-reference
86
+ */
87
+ export declare class SelfReferencingGenericClass extends GenericContainer {
88
+ id: number;
89
+ name: string;
90
+ children: SelfReferencingGenericClass;
91
+ }
92
+ /**
93
+ * Classes for deeply nested circular references
94
+ */
95
+ export declare class Level3 {
96
+ data: boolean;
97
+ refToRoot?: DeepNestedProperClasses;
98
+ }
99
+ export declare class Level2 {
100
+ data: number;
101
+ level3: Level3;
102
+ }
103
+ export declare class Level1 {
104
+ data: string;
105
+ level2: Level2;
106
+ }
107
+ export declare class DeepNestedProperClasses {
108
+ id: number;
109
+ level1: Level1;
110
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Complex inter-dependent classes with multiple circular references
3
+ */
4
+ /**
5
+ * Core entity with relationships to other classes
6
+ */
7
+ export declare class User {
8
+ id: number;
9
+ name: string;
10
+ email: string;
11
+ posts: Post[];
12
+ comments: Comment[];
13
+ profile: Profile;
14
+ primaryGroup?: Group;
15
+ manager?: User;
16
+ directReports: User[];
17
+ }
18
+ /**
19
+ * Entity with back-reference to User
20
+ */
21
+ export declare class Post {
22
+ id: number;
23
+ title: string;
24
+ content: string;
25
+ author: User;
26
+ comments: Comment[];
27
+ categories: Category[];
28
+ relatedPosts: Post[];
29
+ }
30
+ /**
31
+ * Entity with multiple back-references creating complex circular dependencies
32
+ */
33
+ export declare class Comment {
34
+ id: number;
35
+ content: string;
36
+ author: User;
37
+ post: Post;
38
+ parentComment?: Comment;
39
+ replies: Comment[];
40
+ }
41
+ /**
42
+ * Entity with 1:1 relationship with User
43
+ */
44
+ export declare class Profile {
45
+ id: number;
46
+ bio: string;
47
+ avatar: string;
48
+ user: User;
49
+ }
50
+ /**
51
+ * Entity with many-to-many relationship with User
52
+ */
53
+ export declare class Group {
54
+ id: number;
55
+ name: string;
56
+ description: string;
57
+ members: User[];
58
+ admin: User;
59
+ parentGroup?: Group;
60
+ subGroups: Group[];
61
+ }
62
+ /**
63
+ * Entity with many-to-many relationship with Post
64
+ */
65
+ export declare class Category {
66
+ id: number;
67
+ name: string;
68
+ posts: Post[];
69
+ parentCategory?: Category;
70
+ subcategories: Category[];
71
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Interfaces and classes for handling generic circular references
3
+ */
4
+ /**
5
+ * Interface defining the contract for metadata properties
6
+ */
7
+ export interface IMetadata {
8
+ createdAt?: Date;
9
+ updatedAt?: Date;
10
+ version?: number;
11
+ [key: string]: any;
12
+ }
13
+ /**
14
+ * Interface defining the contract for entities with circular references
15
+ */
16
+ export interface ICircularReference<T> {
17
+ getReference(): T | undefined;
18
+ setReference(ref: T): void;
19
+ }
20
+ /**
21
+ * Interface for entities that can contain related items
22
+ */
23
+ export interface IRelatable<T> {
24
+ related?: T;
25
+ }
26
+ /**
27
+ * Base class for containers that hold generic values with metadata
28
+ */
29
+ export declare abstract class BaseContainer<T> implements IRelatable<BaseContainer<T>> {
30
+ abstract value: T;
31
+ metadata: IMetadata;
32
+ related?: BaseContainer<T>;
33
+ constructor();
34
+ protected abstract validateValue(value: T): boolean;
35
+ }
36
+ /**
37
+ * Generic container implementation with basic value validation
38
+ */
39
+ export declare class GenericContainer<T> extends BaseContainer<T> {
40
+ value: T;
41
+ metadata: IMetadata & {
42
+ additionalInfo?: string;
43
+ };
44
+ constructor(value: T);
45
+ protected validateValue(value: T): boolean;
46
+ }
47
+ /**
48
+ * Self-referencing class that extends GenericContainer
49
+ */
50
+ export declare class SelfReferencingGenericClass extends GenericContainer<SelfReferencingGenericClass> implements ICircularReference<SelfReferencingGenericClass> {
51
+ id: number;
52
+ name: string;
53
+ constructor(id: number, name: string);
54
+ getReference(): SelfReferencingGenericClass | undefined;
55
+ setReference(ref: SelfReferencingGenericClass): void;
56
+ protected validateValue(value: SelfReferencingGenericClass): boolean;
57
+ }
@@ -1,4 +1 @@
1
- /**
2
- * Main test file that imports all test cases
3
- */
4
- import './testCases/debug.test.js';
1
+ export {};
package/dist/index.esm.js CHANGED
@@ -66,9 +66,42 @@ class SchemaTransformer {
66
66
  this.program = ts.createProgram(fileNames, tsOptions);
67
67
  this.checker = this.program.getTypeChecker();
68
68
  }
69
- getPropertiesByClassDeclaration(classNode) {
69
+ getPropertiesByClassDeclaration(classNode, visitedDeclarations = new Set()) {
70
+ if (visitedDeclarations.has(classNode)) {
71
+ return [];
72
+ }
73
+ visitedDeclarations.add(classNode);
74
+ // if no heritage clauses, get properties directly from class
75
+ if (!classNode.heritageClauses) {
76
+ return this.getPropertiesByClassMembers(classNode.members, classNode);
77
+ } // use heritage clauses to get properties from base classes
78
+ else {
79
+ const heritageClause = classNode.heritageClauses[0];
80
+ if (heritageClause &&
81
+ heritageClause.token === ts.SyntaxKind.ExtendsKeyword) {
82
+ const type = heritageClause.types[0];
83
+ let properties = [];
84
+ let baseProperties = [];
85
+ if (!type)
86
+ return [];
87
+ const symbol = this.checker.getSymbolAtLocation(type.expression);
88
+ if (!symbol)
89
+ return [];
90
+ const declaration = symbol.declarations?.[0];
91
+ if (declaration && ts.isClassDeclaration(declaration)) {
92
+ baseProperties = this.getPropertiesByClassDeclaration(declaration, visitedDeclarations);
93
+ }
94
+ properties = this.getPropertiesByClassMembers(classNode.members, classNode);
95
+ return baseProperties.concat(properties);
96
+ }
97
+ else {
98
+ return this.getPropertiesByClassMembers(classNode.members, classNode);
99
+ }
100
+ }
101
+ }
102
+ getPropertiesByClassMembers(members, parentClassNode) {
70
103
  const properties = [];
71
- for (const member of classNode.members) {
104
+ for (const member of members) {
72
105
  if (ts.isPropertyDeclaration(member) &&
73
106
  member.name &&
74
107
  ts.isIdentifier(member.name)) {
@@ -78,6 +111,8 @@ class SchemaTransformer {
78
111
  const isOptional = !!member.questionToken;
79
112
  const isGeneric = this.isPropertyTypeGeneric(member);
80
113
  const isPrimitive = this.isPrimitiveType(type);
114
+ const isClassType = this.isClassType(member);
115
+ const isArray = this.isArrayProperty(member);
81
116
  const property = {
82
117
  name: propertyName,
83
118
  type,
@@ -86,9 +121,25 @@ class SchemaTransformer {
86
121
  isGeneric,
87
122
  originalProperty: member,
88
123
  isPrimitive,
89
- isClassType: this.isClassType(member),
90
- isArray: this.isArrayProperty(member),
124
+ isClassType,
125
+ isArray,
126
+ isRef: false,
91
127
  };
128
+ // Check for self-referencing properties to mark as $ref
129
+ if (property.isClassType) {
130
+ const declaration = this.getDeclarationProperty(property);
131
+ if (parentClassNode) {
132
+ if (declaration &&
133
+ declaration.name &&
134
+ this.checker.getSymbolAtLocation(declaration.name) ===
135
+ this.checker.getSymbolAtLocation(parentClassNode.name)) {
136
+ property.isRef = true;
137
+ }
138
+ }
139
+ else {
140
+ debugger;
141
+ }
142
+ }
92
143
  properties.push(property);
93
144
  }
94
145
  }
@@ -456,7 +507,7 @@ class SchemaTransformer {
456
507
  }
457
508
  return ts.isArrayTypeNode(propertyDeclaration.type);
458
509
  }
459
- getSchemaFromProperties({ properties, visitedClass, transformedSchema, }) {
510
+ getSchemaFromProperties({ properties, visitedClass, transformedSchema, classDeclaration, }) {
460
511
  let schema = {};
461
512
  const required = [];
462
513
  for (const property of properties) {
@@ -464,6 +515,7 @@ class SchemaTransformer {
464
515
  property,
465
516
  visitedClass,
466
517
  transformedSchema,
518
+ classDeclaration,
467
519
  });
468
520
  // this.applyDecorators(property, schema as SchemaType)
469
521
  if (!property.isOptional) {
@@ -476,17 +528,35 @@ class SchemaTransformer {
476
528
  required: required.length ? required : undefined,
477
529
  };
478
530
  }
479
- getSchemaFromProperty({ property, visitedClass, transformedSchema, }) {
531
+ getSchemaFromProperty({ property, visitedClass, transformedSchema, classDeclaration, }) {
480
532
  let schema = {};
481
533
  if (property.isPrimitive) {
482
534
  schema = this.getSchemaFromPrimitive(property);
483
535
  }
484
536
  else if (property.isClassType) {
485
- schema = this.getSchemaFromClass({
486
- property,
487
- visitedClass,
488
- transformedSchema,
489
- });
537
+ const declaration = this.getDeclarationProperty(property);
538
+ if (property.isRef && classDeclaration.name) {
539
+ // Self-referencing property, handle as a reference to avoid infinite recursion
540
+ if (property.isArray) {
541
+ schema.type = 'array';
542
+ schema.items = {
543
+ $ref: `#/components/schemas/${classDeclaration.name.text}`,
544
+ };
545
+ }
546
+ else {
547
+ schema = {
548
+ $ref: `#/components/schemas/${classDeclaration.name.text}`,
549
+ };
550
+ }
551
+ }
552
+ else {
553
+ schema = this.getSchemaFromClass({
554
+ isArray: property.isArray,
555
+ visitedClass,
556
+ transformedSchema,
557
+ declaration,
558
+ });
559
+ }
490
560
  }
491
561
  else {
492
562
  schema = { type: 'object', properties: {}, additionalProperties: true };
@@ -494,21 +564,26 @@ class SchemaTransformer {
494
564
  this.applyDecorators(property, schema);
495
565
  return schema;
496
566
  }
497
- getSchemaFromClass({ property, transformedSchema = new Map(), visitedClass = new Set(), }) {
567
+ getSchemaFromClass({ transformedSchema = new Map(), visitedClass = new Set(), declaration, isArray, }) {
498
568
  let schema = { type: 'object' };
499
- const declaration = this.getDeclarationProperty(property);
500
569
  if (!declaration ||
501
570
  !ts.isClassDeclaration(declaration) ||
502
571
  !declaration.name) {
503
572
  return { type: 'object' };
504
573
  }
505
574
  if (visitedClass.has(declaration)) {
506
- if (transformedSchema.has(declaration.name.text)) {
507
- return transformedSchema.get(declaration.name.text);
575
+ if (isArray) {
576
+ schema.type = 'array';
577
+ schema.items = {
578
+ $ref: `#/components/schemas/${declaration.name.text}`,
579
+ };
508
580
  }
509
- return {
510
- $ref: `#/components/schemas/${declaration.name.text}`,
511
- };
581
+ else {
582
+ schema = {
583
+ $ref: `#/components/schemas/${declaration.name.text}`,
584
+ };
585
+ }
586
+ return schema;
512
587
  }
513
588
  visitedClass.add(declaration);
514
589
  const properties = this.getPropertiesByClassDeclaration(declaration);
@@ -516,8 +591,9 @@ class SchemaTransformer {
516
591
  properties,
517
592
  visitedClass,
518
593
  transformedSchema: transformedSchema,
594
+ classDeclaration: declaration,
519
595
  });
520
- if (property.isArray) {
596
+ if (isArray) {
521
597
  schema.type = 'array';
522
598
  schema.items = {
523
599
  type: transformerProps.type,
@@ -531,9 +607,6 @@ class SchemaTransformer {
531
607
  schema.required = transformerProps.required;
532
608
  }
533
609
  transformedSchema.set(declaration.name.text, schema);
534
- if (schema.properties && Object.keys(schema.properties).length === 0) {
535
- schema = { type: 'object', properties: {}, additionalProperties: true };
536
- }
537
610
  return schema;
538
611
  }
539
612
  getSchemaFromPrimitive(property) {
@@ -695,7 +768,10 @@ class SchemaTransformer {
695
768
  return { name: cls.name, schema: {} };
696
769
  }
697
770
  const properties = this.getPropertiesByClassDeclaration(result.node);
698
- schema = this.getSchemaFromProperties({ properties });
771
+ schema = this.getSchemaFromProperties({
772
+ properties,
773
+ classDeclaration: result.node,
774
+ });
699
775
  return { name: cls.name, schema };
700
776
  }
701
777
  }
package/dist/index.js CHANGED
@@ -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,9 +609,6 @@ 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) {
@@ -697,7 +770,10 @@ class SchemaTransformer {
697
770
  return { name: cls.name, schema: {} };
698
771
  }
699
772
  const properties = this.getPropertiesByClassDeclaration(result.node);
700
- schema = this.getSchemaFromProperties({ properties });
773
+ schema = this.getSchemaFromProperties({
774
+ properties,
775
+ classDeclaration: result.node,
776
+ });
701
777
  return { name: cls.name, schema };
702
778
  }
703
779
  }