ts-class-to-openapi 1.3.3 → 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
@@ -188,9 +188,6 @@ var SchemaTransformer = class SchemaTransformer {
188
188
  classCache = /* @__PURE__ */ new WeakMap();
189
189
  maxCacheSize;
190
190
  autoCleanup;
191
- loadedFiles = /* @__PURE__ */ new Set();
192
- processingClasses = /* @__PURE__ */ new Set();
193
- sourceFiles;
194
191
  classFileIndex = /* @__PURE__ */ new Map();
195
192
  constructor(tsConfigPath = constants.TS_CONFIG_DEFAULT_PATH, options = {}) {
196
193
  this.maxCacheSize = options.maxCacheSize ?? 100;
@@ -203,13 +200,7 @@ var SchemaTransformer = class SchemaTransformer {
203
200
  const { options: tsOptions, fileNames } = typescript.default.parseJsonConfigFileContent(config, typescript.default.sys, "./");
204
201
  this.program = typescript.default.createProgram(fileNames, tsOptions);
205
202
  this.checker = this.program.getTypeChecker();
206
- this.sourceFiles = this.program.getSourceFiles().filter((sf) => {
207
- if (sf.isDeclarationFile) return false;
208
- if (sf.fileName.includes(".d.ts")) return false;
209
- if (sf.fileName.includes("node_modules")) return false;
210
- return true;
211
- });
212
- this.sourceFiles.forEach((sf) => {
203
+ this.program.getSourceFiles().forEach((sf) => {
213
204
  sf.statements.forEach((stmt) => {
214
205
  if (typescript.default.isClassDeclaration(stmt) && stmt.name) {
215
206
  const name = stmt.name.text;
@@ -341,6 +332,11 @@ var SchemaTransformer = class SchemaTransformer {
341
332
  if (types.length > 0 && types[0]) return types[0];
342
333
  return "object";
343
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
+ }
344
340
  const typeText = typeNode.getText();
345
341
  if (genericTypeMap && genericTypeMap.has(typeText)) return genericTypeMap.get(typeText);
346
342
  if (typeText.startsWith("Date")) return constants.jsPrimitives.Date.type;
@@ -556,7 +552,7 @@ var SchemaTransformer = class SchemaTransformer {
556
552
  if (sourceOptions === null || sourceOptions === void 0 ? void 0 : sourceOptions.isExternal) return this.program.getSourceFiles().filter((sf) => {
557
553
  return sf.fileName.includes(sourceOptions.packageName) && (!sourceOptions.filePath || sf.fileName === sourceOptions.filePath);
558
554
  });
559
- return this.sourceFiles.filter((sf) => {
555
+ return this.program.getSourceFiles().filter((sf) => {
560
556
  if ((sourceOptions === null || sourceOptions === void 0 ? void 0 : sourceOptions.filePath) && !sf.fileName.includes(sourceOptions.filePath)) return false;
561
557
  return true;
562
558
  });
@@ -847,6 +843,11 @@ var SchemaTransformer = class SchemaTransformer {
847
843
  if (decorator.arguments.length === 0) return;
848
844
  const arg = decorator.arguments[0];
849
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
+ }
850
851
  const type = this.checker.getTypeAtLocation(arg);
851
852
  if (type.symbol && type.symbol.exports) {
852
853
  const values = [];
@@ -858,15 +859,46 @@ var SchemaTransformer = class SchemaTransformer {
858
859
  }
859
860
  });
860
861
  if (values.length > 0) {
861
- schema.enum = values;
862
- const isString = values.every((v) => typeof v === "string");
863
- const isNumber = values.every((v) => typeof v === "number");
864
- if (isString) schema.type = "string";
865
- else if (isNumber) schema.type = "number";
866
- else schema.type = "string";
862
+ this.applyEnumValues(values, schema);
863
+ return;
867
864
  }
868
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
+ }
869
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;
870
902
  }
871
903
  applyDecorators(property, schema) {
872
904
  for (const decorator of property.decorators) switch (decorator.name) {
@@ -944,7 +976,13 @@ var SchemaTransformer = class SchemaTransformer {
944
976
  break;
945
977
  case constants.validatorDecorators.IsEnum.name:
946
978
  if (!property.isArray) this.applyEnumDecorator(decorator, schema);
947
- 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
+ }
948
986
  break;
949
987
  }
950
988
  }
package/dist/index.mjs CHANGED
@@ -163,9 +163,6 @@ var SchemaTransformer = class SchemaTransformer {
163
163
  classCache = /* @__PURE__ */ new WeakMap();
164
164
  maxCacheSize;
165
165
  autoCleanup;
166
- loadedFiles = /* @__PURE__ */ new Set();
167
- processingClasses = /* @__PURE__ */ new Set();
168
- sourceFiles;
169
166
  classFileIndex = /* @__PURE__ */ new Map();
170
167
  constructor(tsConfigPath = constants.TS_CONFIG_DEFAULT_PATH, options = {}) {
171
168
  this.maxCacheSize = options.maxCacheSize ?? 100;
@@ -178,13 +175,7 @@ var SchemaTransformer = class SchemaTransformer {
178
175
  const { options: tsOptions, fileNames } = ts.parseJsonConfigFileContent(config, ts.sys, "./");
179
176
  this.program = ts.createProgram(fileNames, tsOptions);
180
177
  this.checker = this.program.getTypeChecker();
181
- this.sourceFiles = this.program.getSourceFiles().filter((sf) => {
182
- if (sf.isDeclarationFile) return false;
183
- if (sf.fileName.includes(".d.ts")) return false;
184
- if (sf.fileName.includes("node_modules")) return false;
185
- return true;
186
- });
187
- this.sourceFiles.forEach((sf) => {
178
+ this.program.getSourceFiles().forEach((sf) => {
188
179
  sf.statements.forEach((stmt) => {
189
180
  if (ts.isClassDeclaration(stmt) && stmt.name) {
190
181
  const name = stmt.name.text;
@@ -316,6 +307,11 @@ var SchemaTransformer = class SchemaTransformer {
316
307
  if (types.length > 0 && types[0]) return types[0];
317
308
  return "object";
318
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
+ }
319
315
  const typeText = typeNode.getText();
320
316
  if (genericTypeMap && genericTypeMap.has(typeText)) return genericTypeMap.get(typeText);
321
317
  if (typeText.startsWith("Date")) return constants.jsPrimitives.Date.type;
@@ -531,7 +527,7 @@ var SchemaTransformer = class SchemaTransformer {
531
527
  if (sourceOptions === null || sourceOptions === void 0 ? void 0 : sourceOptions.isExternal) return this.program.getSourceFiles().filter((sf) => {
532
528
  return sf.fileName.includes(sourceOptions.packageName) && (!sourceOptions.filePath || sf.fileName === sourceOptions.filePath);
533
529
  });
534
- return this.sourceFiles.filter((sf) => {
530
+ return this.program.getSourceFiles().filter((sf) => {
535
531
  if ((sourceOptions === null || sourceOptions === void 0 ? void 0 : sourceOptions.filePath) && !sf.fileName.includes(sourceOptions.filePath)) return false;
536
532
  return true;
537
533
  });
@@ -822,6 +818,11 @@ var SchemaTransformer = class SchemaTransformer {
822
818
  if (decorator.arguments.length === 0) return;
823
819
  const arg = decorator.arguments[0];
824
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
+ }
825
826
  const type = this.checker.getTypeAtLocation(arg);
826
827
  if (type.symbol && type.symbol.exports) {
827
828
  const values = [];
@@ -833,15 +834,46 @@ var SchemaTransformer = class SchemaTransformer {
833
834
  }
834
835
  });
835
836
  if (values.length > 0) {
836
- schema.enum = values;
837
- const isString = values.every((v) => typeof v === "string");
838
- const isNumber = values.every((v) => typeof v === "number");
839
- if (isString) schema.type = "string";
840
- else if (isNumber) schema.type = "number";
841
- else schema.type = "string";
837
+ this.applyEnumValues(values, schema);
838
+ return;
842
839
  }
843
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
+ }
844
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;
845
877
  }
846
878
  applyDecorators(property, schema) {
847
879
  for (const decorator of property.decorators) switch (decorator.name) {
@@ -919,7 +951,13 @@ var SchemaTransformer = class SchemaTransformer {
919
951
  break;
920
952
  case constants.validatorDecorators.IsEnum.name:
921
953
  if (!property.isArray) this.applyEnumDecorator(decorator, schema);
922
- 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
+ }
923
961
  break;
924
962
  }
925
963
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "ts-class-to-openapi",
3
- "version": "1.3.3",
4
- "type": "module",
3
+ "version": "1.4.0",
5
4
  "description": "Transform TypeScript classes into OpenAPI 3.1.0 schema objects, which support class-validator decorators",
5
+ "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.mjs",
8
8
  "types": "./dist/index.d.mts",
@@ -67,7 +67,7 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "class-validator": "0.15.1",
70
- "typescript": "5.9.3"
70
+ "typescript": "6.0.2"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@types/node": "24.2.1",
@@ -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": "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"