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.
- package/dist/__test__/entities/circular-reference-cases.d.ts +1 -0
- package/dist/__test__/entities/circular-reference-classes.d.ts +110 -0
- package/dist/__test__/entities/complex-circular-dependencies.d.ts +71 -0
- package/dist/__test__/entities/deep-nested-classes.d.ts +1 -0
- package/dist/__test__/entities/generic-circular-classes.d.ts +57 -0
- package/dist/__test__/test.d.ts +1 -4
- package/dist/__test__/testCases/circular-references.test.d.ts +1 -0
- package/dist/index.esm.js +99 -23
- package/dist/index.js +99 -23
- package/dist/run.js +100 -24
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
}
|
package/dist/__test__/test.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
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
|
|
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
|
|
90
|
-
isArray
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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({
|
|
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 (
|
|
507
|
-
|
|
575
|
+
if (isArray) {
|
|
576
|
+
schema.type = 'array';
|
|
577
|
+
schema.items = {
|
|
578
|
+
$ref: `#/components/schemas/${declaration.name.text}`,
|
|
579
|
+
};
|
|
508
580
|
}
|
|
509
|
-
|
|
510
|
-
|
|
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 (
|
|
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({
|
|
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
|
|
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
|
|
92
|
-
isArray
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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({
|
|
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 (
|
|
509
|
-
|
|
577
|
+
if (isArray) {
|
|
578
|
+
schema.type = 'array';
|
|
579
|
+
schema.items = {
|
|
580
|
+
$ref: `#/components/schemas/${declaration.name.text}`,
|
|
581
|
+
};
|
|
510
582
|
}
|
|
511
|
-
|
|
512
|
-
|
|
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 (
|
|
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({
|
|
773
|
+
schema = this.getSchemaFromProperties({
|
|
774
|
+
properties,
|
|
775
|
+
classDeclaration: result.node,
|
|
776
|
+
});
|
|
701
777
|
return { name: cls.name, schema };
|
|
702
778
|
}
|
|
703
779
|
}
|