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.
@@ -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
@@ -14,10 +14,10 @@ const jsPrimitives = {
14
14
  Object: { type: 'Object', value: 'object' },
15
15
  Array: { type: 'Array', value: 'array' },
16
16
  Date: { type: 'Date', value: 'string', format: 'date-time' },
17
- Buffer: { type: 'Buffer', value: 'string'},
18
- Uint8Array: { type: 'Uint8Array', value: 'string'},
17
+ Buffer: { type: 'Buffer'},
18
+ Uint8Array: { type: 'Uint8Array'},
19
19
  UploadFile: { type: 'UploadFile', value: 'string', format: 'binary' },
20
- File: { type: 'File', value: 'string'},
20
+ File: { type: 'File'},
21
21
  };
22
22
  const validatorDecorators = {
23
23
  Length: { name: 'Length'},
@@ -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,14 +607,12 @@ 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) {
540
613
  const propertySchema = { type: 'object' };
541
614
  const propertyType = property.type.toLowerCase().replace('[]', '').trim();
615
+ let isFile = false;
542
616
  switch (propertyType) {
543
617
  case constants.jsPrimitives.String.value:
544
618
  propertySchema.type = constants.jsPrimitives.String.value;
@@ -555,12 +629,13 @@ class SchemaTransformer {
555
629
  propertySchema.type = constants.jsPrimitives.Date.value;
556
630
  propertySchema.format = constants.jsPrimitives.Date.format;
557
631
  break;
558
- case constants.jsPrimitives.Buffer.value:
559
- case constants.jsPrimitives.Uint8Array.value:
560
- case constants.jsPrimitives.File.value:
561
- case constants.jsPrimitives.UploadFile.value:
632
+ case constants.jsPrimitives.Buffer.type.toLocaleLowerCase():
633
+ case constants.jsPrimitives.Uint8Array.type.toLocaleLowerCase():
634
+ case constants.jsPrimitives.File.type.toLocaleLowerCase():
635
+ case constants.jsPrimitives.UploadFile.type.toLocaleLowerCase():
562
636
  propertySchema.type = constants.jsPrimitives.UploadFile.value;
563
637
  propertySchema.format = constants.jsPrimitives.UploadFile.format;
638
+ isFile = true;
564
639
  break;
565
640
  case constants.jsPrimitives.Array.value:
566
641
  propertySchema.type = constants.jsPrimitives.Array.value;
@@ -579,7 +654,9 @@ class SchemaTransformer {
579
654
  }
580
655
  if (property.isArray) {
581
656
  propertySchema.type = `array`;
582
- propertySchema.items = { type: propertyType };
657
+ propertySchema.items = {
658
+ type: isFile ? constants.jsPrimitives.UploadFile.value : propertyType,
659
+ };
583
660
  }
584
661
  return propertySchema;
585
662
  }
@@ -695,7 +772,10 @@ class SchemaTransformer {
695
772
  return { name: cls.name, schema: {} };
696
773
  }
697
774
  const properties = this.getPropertiesByClassDeclaration(result.node);
698
- schema = this.getSchemaFromProperties({ properties });
775
+ schema = this.getSchemaFromProperties({
776
+ properties,
777
+ classDeclaration: result.node,
778
+ });
699
779
  return { name: cls.name, schema };
700
780
  }
701
781
  }