ts-class-to-openapi 1.3.4 → 1.4.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/README.md +24 -108
- package/dist/index.cjs +54 -7
- package/dist/index.mjs +54 -7
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -27,7 +27,26 @@ class User {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const schema = transform(User)
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
console.log(JSON.stringify(shema), null, 2)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Generated output:**
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"name": "User",
|
|
39
|
+
"schema": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": {
|
|
42
|
+
"id": { "type": "number" },
|
|
43
|
+
"name": { "type": "string" },
|
|
44
|
+
"email": { "type": "string" },
|
|
45
|
+
"age": { "type": "number" }
|
|
46
|
+
},
|
|
47
|
+
"required": ["id", "name", "email"]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
31
50
|
```
|
|
32
51
|
|
|
33
52
|
## 📦 Installation
|
|
@@ -55,57 +74,7 @@ npm install ts-class-to-openapi class-validator
|
|
|
55
74
|
|
|
56
75
|
## 🎨 Class Transformation Examples
|
|
57
76
|
|
|
58
|
-
### 1.
|
|
59
|
-
|
|
60
|
-
Fundamental method: transform any TypeScript class without decorators:
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
import { transform } from 'ts-class-to-openapi'
|
|
64
|
-
|
|
65
|
-
// Basic TypeScript class - no decorators required
|
|
66
|
-
class User {
|
|
67
|
-
id: number
|
|
68
|
-
name: string
|
|
69
|
-
email: string
|
|
70
|
-
age: number
|
|
71
|
-
active: boolean
|
|
72
|
-
tags: string[]
|
|
73
|
-
createdAt: Date
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Transform class to OpenAPI schema
|
|
77
|
-
const result = transform(User)
|
|
78
|
-
console.log(JSON.stringify(result, null, 2))
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
**Generated output:**
|
|
82
|
-
|
|
83
|
-
```json
|
|
84
|
-
{
|
|
85
|
-
"name": "User",
|
|
86
|
-
"schema": {
|
|
87
|
-
"type": "object",
|
|
88
|
-
"properties": {
|
|
89
|
-
"id": { "type": "number" },
|
|
90
|
-
"name": { "type": "string" },
|
|
91
|
-
"email": { "type": "string" },
|
|
92
|
-
"age": { "type": "number" },
|
|
93
|
-
"active": { "type": "boolean" },
|
|
94
|
-
"tags": {
|
|
95
|
-
"type": "array",
|
|
96
|
-
"items": { "type": "string" }
|
|
97
|
-
},
|
|
98
|
-
"createdAt": {
|
|
99
|
-
"type": "string",
|
|
100
|
-
"format": "date-time"
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
"required": ["id", "name", "email", "age", "active", "tags", "createdAt"]
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### 2. Class with Advanced Validations
|
|
77
|
+
### 1. Class with Advanced Validations
|
|
109
78
|
|
|
110
79
|
For more detailed schemas, class-validator decorators can be incorporated:
|
|
111
80
|
|
|
@@ -156,7 +125,7 @@ const result = transform(User)
|
|
|
156
125
|
}
|
|
157
126
|
```
|
|
158
127
|
|
|
159
|
-
###
|
|
128
|
+
### 2. Nested Objects and Arrays
|
|
160
129
|
|
|
161
130
|
Automatic processing of complex relationships:
|
|
162
131
|
|
|
@@ -229,9 +198,9 @@ const schema = transform(User)
|
|
|
229
198
|
}
|
|
230
199
|
```
|
|
231
200
|
|
|
232
|
-
###
|
|
201
|
+
### 3. Enumerations and Special Types
|
|
233
202
|
|
|
234
|
-
Full compatibility with TypeScript enumerations (both decorated and pure):
|
|
203
|
+
Full compatibility with TypeScript enumerations (both decorated and pure), and literal object as enums:
|
|
235
204
|
|
|
236
205
|
```typescript
|
|
237
206
|
import { transform } from 'ts-class-to-openapi'
|
|
@@ -299,59 +268,6 @@ const schema = transform(Task)
|
|
|
299
268
|
}
|
|
300
269
|
```
|
|
301
270
|
|
|
302
|
-
### 5. File Upload
|
|
303
|
-
|
|
304
|
-
Integrated support for binary file handling:
|
|
305
|
-
|
|
306
|
-
```typescript
|
|
307
|
-
import { transform } from 'ts-class-to-openapi'
|
|
308
|
-
import { IsNotEmpty, IsOptional } from 'class-validator'
|
|
309
|
-
|
|
310
|
-
// Custom file type definition
|
|
311
|
-
class UploadFile {}
|
|
312
|
-
|
|
313
|
-
class UserProfile {
|
|
314
|
-
@IsNotEmpty()
|
|
315
|
-
profilePicture: UploadFile
|
|
316
|
-
|
|
317
|
-
@IsOptional()
|
|
318
|
-
resume: UploadFile
|
|
319
|
-
|
|
320
|
-
documents: UploadFile[] // Multiple files
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const schema = transform(UserProfile)
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
**Generated output:**
|
|
327
|
-
|
|
328
|
-
```json
|
|
329
|
-
{
|
|
330
|
-
"name": "UserProfile",
|
|
331
|
-
"schema": {
|
|
332
|
-
"type": "object",
|
|
333
|
-
"properties": {
|
|
334
|
-
"profilePicture": {
|
|
335
|
-
"type": "string",
|
|
336
|
-
"format": "binary"
|
|
337
|
-
},
|
|
338
|
-
"resume": {
|
|
339
|
-
"type": "string",
|
|
340
|
-
"format": "binary"
|
|
341
|
-
},
|
|
342
|
-
"documents": {
|
|
343
|
-
"type": "array",
|
|
344
|
-
"items": {
|
|
345
|
-
"type": "string",
|
|
346
|
-
"format": "binary"
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
"required": ["profilePicture", "documents"]
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
```
|
|
354
|
-
|
|
355
271
|
## 🌐 REST API Integration
|
|
356
272
|
|
|
357
273
|
### Implementation with Express.js and Swagger UI
|
package/dist/index.cjs
CHANGED
|
@@ -332,6 +332,11 @@ var SchemaTransformer = class SchemaTransformer {
|
|
|
332
332
|
if (types.length > 0 && types[0]) return types[0];
|
|
333
333
|
return "object";
|
|
334
334
|
default:
|
|
335
|
+
if (typescript.default.isIndexedAccessTypeNode(typeNode)) {
|
|
336
|
+
const resolvedType = this.checker.getTypeAtLocation(typeNode);
|
|
337
|
+
const resolved = this.checker.typeToString(resolvedType);
|
|
338
|
+
if (this.isPrimitiveType(resolved)) return resolved;
|
|
339
|
+
}
|
|
335
340
|
const typeText = typeNode.getText();
|
|
336
341
|
if (genericTypeMap && genericTypeMap.has(typeText)) return genericTypeMap.get(typeText);
|
|
337
342
|
if (typeText.startsWith("Date")) return constants.jsPrimitives.Date.type;
|
|
@@ -838,6 +843,11 @@ var SchemaTransformer = class SchemaTransformer {
|
|
|
838
843
|
if (decorator.arguments.length === 0) return;
|
|
839
844
|
const arg = decorator.arguments[0];
|
|
840
845
|
if (arg && typeof arg === "object" && "kind" in arg) {
|
|
846
|
+
if (typescript.default.isArrayLiteralExpression(arg)) {
|
|
847
|
+
const values = this.extractValuesFromArrayLiteral(arg);
|
|
848
|
+
if (values.length > 0) this.applyEnumValues(values, schema);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
841
851
|
const type = this.checker.getTypeAtLocation(arg);
|
|
842
852
|
if (type.symbol && type.symbol.exports) {
|
|
843
853
|
const values = [];
|
|
@@ -849,15 +859,46 @@ var SchemaTransformer = class SchemaTransformer {
|
|
|
849
859
|
}
|
|
850
860
|
});
|
|
851
861
|
if (values.length > 0) {
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
const isNumber = values.every((v) => typeof v === "number");
|
|
855
|
-
if (isString) schema.type = "string";
|
|
856
|
-
else if (isNumber) schema.type = "number";
|
|
857
|
-
else schema.type = "string";
|
|
862
|
+
this.applyEnumValues(values, schema);
|
|
863
|
+
return;
|
|
858
864
|
}
|
|
859
865
|
}
|
|
866
|
+
const values = this.extractValuesFromObjectLiteral(type);
|
|
867
|
+
if (values.length > 0) this.applyEnumValues(values, schema);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
extractValuesFromArrayLiteral(arrayLiteral) {
|
|
871
|
+
const values = [];
|
|
872
|
+
for (const element of arrayLiteral.elements) if (typescript.default.isStringLiteral(element)) values.push(element.text);
|
|
873
|
+
else if (typescript.default.isNumericLiteral(element)) values.push(Number(element.text));
|
|
874
|
+
else if (typescript.default.isPrefixUnaryExpression(element) && element.operator === typescript.default.SyntaxKind.MinusToken && typescript.default.isNumericLiteral(element.operand)) values.push(-Number(element.operand.text));
|
|
875
|
+
return values;
|
|
876
|
+
}
|
|
877
|
+
extractValuesFromObjectLiteral(type) {
|
|
878
|
+
const values = [];
|
|
879
|
+
const properties = type.getProperties();
|
|
880
|
+
if (!properties || properties.length === 0) return values;
|
|
881
|
+
for (const prop of properties) {
|
|
882
|
+
const propType = this.checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration);
|
|
883
|
+
if (propType.isStringLiteral()) values.push(propType.value);
|
|
884
|
+
else if (propType.isNumberLiteral()) values.push(propType.value);
|
|
885
|
+
else if (prop.valueDeclaration && typescript.default.isPropertyAssignment(prop.valueDeclaration)) {
|
|
886
|
+
const initializer = prop.valueDeclaration.initializer;
|
|
887
|
+
if (typescript.default.isStringLiteral(initializer)) values.push(initializer.text);
|
|
888
|
+
else if (typescript.default.isNumericLiteral(initializer)) values.push(Number(initializer.text));
|
|
889
|
+
}
|
|
860
890
|
}
|
|
891
|
+
return values;
|
|
892
|
+
}
|
|
893
|
+
applyEnumValues(values, schema) {
|
|
894
|
+
schema.enum = values;
|
|
895
|
+
const isString = values.every((v) => typeof v === "string");
|
|
896
|
+
const isNumber = values.every((v) => typeof v === "number");
|
|
897
|
+
if (isString) schema.type = "string";
|
|
898
|
+
else if (isNumber) schema.type = "number";
|
|
899
|
+
else schema.type = "string";
|
|
900
|
+
delete schema.properties;
|
|
901
|
+
delete schema.additionalProperties;
|
|
861
902
|
}
|
|
862
903
|
applyDecorators(property, schema) {
|
|
863
904
|
for (const decorator of property.decorators) switch (decorator.name) {
|
|
@@ -935,7 +976,13 @@ var SchemaTransformer = class SchemaTransformer {
|
|
|
935
976
|
break;
|
|
936
977
|
case constants.validatorDecorators.IsEnum.name:
|
|
937
978
|
if (!property.isArray) this.applyEnumDecorator(decorator, schema);
|
|
938
|
-
else
|
|
979
|
+
else {
|
|
980
|
+
if (!schema.items) {
|
|
981
|
+
schema.type = "array";
|
|
982
|
+
schema.items = {};
|
|
983
|
+
}
|
|
984
|
+
this.applyEnumDecorator(decorator, schema.items);
|
|
985
|
+
}
|
|
939
986
|
break;
|
|
940
987
|
}
|
|
941
988
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -307,6 +307,11 @@ var SchemaTransformer = class SchemaTransformer {
|
|
|
307
307
|
if (types.length > 0 && types[0]) return types[0];
|
|
308
308
|
return "object";
|
|
309
309
|
default:
|
|
310
|
+
if (ts.isIndexedAccessTypeNode(typeNode)) {
|
|
311
|
+
const resolvedType = this.checker.getTypeAtLocation(typeNode);
|
|
312
|
+
const resolved = this.checker.typeToString(resolvedType);
|
|
313
|
+
if (this.isPrimitiveType(resolved)) return resolved;
|
|
314
|
+
}
|
|
310
315
|
const typeText = typeNode.getText();
|
|
311
316
|
if (genericTypeMap && genericTypeMap.has(typeText)) return genericTypeMap.get(typeText);
|
|
312
317
|
if (typeText.startsWith("Date")) return constants.jsPrimitives.Date.type;
|
|
@@ -813,6 +818,11 @@ var SchemaTransformer = class SchemaTransformer {
|
|
|
813
818
|
if (decorator.arguments.length === 0) return;
|
|
814
819
|
const arg = decorator.arguments[0];
|
|
815
820
|
if (arg && typeof arg === "object" && "kind" in arg) {
|
|
821
|
+
if (ts.isArrayLiteralExpression(arg)) {
|
|
822
|
+
const values = this.extractValuesFromArrayLiteral(arg);
|
|
823
|
+
if (values.length > 0) this.applyEnumValues(values, schema);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
816
826
|
const type = this.checker.getTypeAtLocation(arg);
|
|
817
827
|
if (type.symbol && type.symbol.exports) {
|
|
818
828
|
const values = [];
|
|
@@ -824,15 +834,46 @@ var SchemaTransformer = class SchemaTransformer {
|
|
|
824
834
|
}
|
|
825
835
|
});
|
|
826
836
|
if (values.length > 0) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const isNumber = values.every((v) => typeof v === "number");
|
|
830
|
-
if (isString) schema.type = "string";
|
|
831
|
-
else if (isNumber) schema.type = "number";
|
|
832
|
-
else schema.type = "string";
|
|
837
|
+
this.applyEnumValues(values, schema);
|
|
838
|
+
return;
|
|
833
839
|
}
|
|
834
840
|
}
|
|
841
|
+
const values = this.extractValuesFromObjectLiteral(type);
|
|
842
|
+
if (values.length > 0) this.applyEnumValues(values, schema);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
extractValuesFromArrayLiteral(arrayLiteral) {
|
|
846
|
+
const values = [];
|
|
847
|
+
for (const element of arrayLiteral.elements) if (ts.isStringLiteral(element)) values.push(element.text);
|
|
848
|
+
else if (ts.isNumericLiteral(element)) values.push(Number(element.text));
|
|
849
|
+
else if (ts.isPrefixUnaryExpression(element) && element.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(element.operand)) values.push(-Number(element.operand.text));
|
|
850
|
+
return values;
|
|
851
|
+
}
|
|
852
|
+
extractValuesFromObjectLiteral(type) {
|
|
853
|
+
const values = [];
|
|
854
|
+
const properties = type.getProperties();
|
|
855
|
+
if (!properties || properties.length === 0) return values;
|
|
856
|
+
for (const prop of properties) {
|
|
857
|
+
const propType = this.checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration);
|
|
858
|
+
if (propType.isStringLiteral()) values.push(propType.value);
|
|
859
|
+
else if (propType.isNumberLiteral()) values.push(propType.value);
|
|
860
|
+
else if (prop.valueDeclaration && ts.isPropertyAssignment(prop.valueDeclaration)) {
|
|
861
|
+
const initializer = prop.valueDeclaration.initializer;
|
|
862
|
+
if (ts.isStringLiteral(initializer)) values.push(initializer.text);
|
|
863
|
+
else if (ts.isNumericLiteral(initializer)) values.push(Number(initializer.text));
|
|
864
|
+
}
|
|
835
865
|
}
|
|
866
|
+
return values;
|
|
867
|
+
}
|
|
868
|
+
applyEnumValues(values, schema) {
|
|
869
|
+
schema.enum = values;
|
|
870
|
+
const isString = values.every((v) => typeof v === "string");
|
|
871
|
+
const isNumber = values.every((v) => typeof v === "number");
|
|
872
|
+
if (isString) schema.type = "string";
|
|
873
|
+
else if (isNumber) schema.type = "number";
|
|
874
|
+
else schema.type = "string";
|
|
875
|
+
delete schema.properties;
|
|
876
|
+
delete schema.additionalProperties;
|
|
836
877
|
}
|
|
837
878
|
applyDecorators(property, schema) {
|
|
838
879
|
for (const decorator of property.decorators) switch (decorator.name) {
|
|
@@ -910,7 +951,13 @@ var SchemaTransformer = class SchemaTransformer {
|
|
|
910
951
|
break;
|
|
911
952
|
case constants.validatorDecorators.IsEnum.name:
|
|
912
953
|
if (!property.isArray) this.applyEnumDecorator(decorator, schema);
|
|
913
|
-
else
|
|
954
|
+
else {
|
|
955
|
+
if (!schema.items) {
|
|
956
|
+
schema.type = "array";
|
|
957
|
+
schema.items = {};
|
|
958
|
+
}
|
|
959
|
+
this.applyEnumDecorator(decorator, schema.items);
|
|
960
|
+
}
|
|
914
961
|
break;
|
|
915
962
|
}
|
|
916
963
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-class-to-openapi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Transform TypeScript classes into OpenAPI 3.1.0 schema objects, which support class-validator decorators",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -81,9 +81,9 @@
|
|
|
81
81
|
"test": "node --import tsx --test test/testCases/**/*.test.ts",
|
|
82
82
|
"test:watch": "node --import tsx --inspect --test --watch test/testCases/**/*.test.ts",
|
|
83
83
|
"test:coverage": "node --import tsx --test --experimental-test-coverage test/testCases/**/*.test.ts",
|
|
84
|
-
"build": "tsc --noEmit &&
|
|
84
|
+
"build": "tsc --noEmit && tsdown",
|
|
85
85
|
"build:watch": "rm -rf dist && tsdown --watch",
|
|
86
|
-
"dev": "node --import tsx --
|
|
86
|
+
"dev": "node --import tsx --inspect --watch ./src/run.ts",
|
|
87
87
|
"format": "prettier --write .",
|
|
88
88
|
"format:check": "prettier --check .",
|
|
89
89
|
"prepublish": "pnpm build"
|