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 CHANGED
@@ -27,7 +27,26 @@ class User {
27
27
  }
28
28
 
29
29
  const schema = transform(User)
30
- // Returns complete OpenAPI schema ready for Swagger/API documentation
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. Basic TypeScript Class
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
- ### 3. Nested Objects and Arrays
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
- ### 4. Enumerations and Special Types
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
- schema.enum = values;
853
- const isString = values.every((v) => typeof v === "string");
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 if (schema.items) this.applyEnumDecorator(decorator, schema.items);
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
- schema.enum = values;
828
- const isString = values.every((v) => typeof v === "string");
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 if (schema.items) this.applyEnumDecorator(decorator, schema.items);
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.4",
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 && rm -rf dist && tsdown",
84
+ "build": "tsc --noEmit && tsdown",
85
85
  "build:watch": "rm -rf dist && tsdown --watch",
86
- "dev": "node --import tsx --test --inspect --watch ./src/run.ts",
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"