@hazeljs/swagger 0.2.0-alpha.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.
- package/LICENSE +192 -0
- package/README.md +584 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/swagger.controller.d.ts +12 -0
- package/dist/swagger.controller.d.ts.map +1 -0
- package/dist/swagger.controller.js +243 -0
- package/dist/swagger.controller.test.d.ts +2 -0
- package/dist/swagger.controller.test.d.ts.map +1 -0
- package/dist/swagger.controller.test.js +250 -0
- package/dist/swagger.decorator.d.ts +7 -0
- package/dist/swagger.decorator.d.ts.map +1 -0
- package/dist/swagger.decorator.js +26 -0
- package/dist/swagger.decorator.test.d.ts +2 -0
- package/dist/swagger.decorator.test.d.ts.map +1 -0
- package/dist/swagger.decorator.test.js +72 -0
- package/dist/swagger.module.d.ts +5 -0
- package/dist/swagger.module.d.ts.map +1 -0
- package/dist/swagger.module.js +30 -0
- package/dist/swagger.service.d.ts +24 -0
- package/dist/swagger.service.d.ts.map +1 -0
- package/dist/swagger.service.js +121 -0
- package/dist/swagger.service.test.d.ts +2 -0
- package/dist/swagger.service.test.d.ts.map +1 -0
- package/dist/swagger.service.test.js +364 -0
- package/dist/swagger.types.d.ts +50 -0
- package/dist/swagger.types.d.ts.map +1 -0
- package/dist/swagger.types.js +2 -0
- package/package.json +55 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { SwaggerOperation, SwaggerSchema } from './swagger.types';
|
|
2
|
+
import { Type } from '@hazeljs/core';
|
|
3
|
+
export interface SwaggerSpec {
|
|
4
|
+
openapi: string;
|
|
5
|
+
info: {
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
};
|
|
10
|
+
paths: Record<string, Record<string, SwaggerOperation>>;
|
|
11
|
+
components: {
|
|
12
|
+
schemas: Record<string, SwaggerSchema>;
|
|
13
|
+
};
|
|
14
|
+
tags?: Array<{
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export declare class SwaggerService {
|
|
20
|
+
private spec;
|
|
21
|
+
generateSpec(controllers: Type<unknown>[]): SwaggerSpec;
|
|
22
|
+
private normalizePath;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=swagger.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"swagger.service.d.ts","sourceRoot":"","sources":["../src/swagger.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGlE,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAErC,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE;QACJ,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC;IACxD,UAAU,EAAE;QACV,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;KACxC,CAAC;IACF,IAAI,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAQD,qBACa,cAAc;IACzB,OAAO,CAAC,IAAI,CAOV;IAEF,YAAY,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,GAAG,WAAW;IAsGvD,OAAO,CAAC,aAAa;CAStB"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.SwaggerService = void 0;
|
|
13
|
+
const core_1 = require("@hazeljs/core");
|
|
14
|
+
const swagger_decorator_1 = require("./swagger.decorator");
|
|
15
|
+
const core_2 = __importDefault(require("@hazeljs/core"));
|
|
16
|
+
let SwaggerService = class SwaggerService {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.spec = {
|
|
19
|
+
openapi: '3.0.0',
|
|
20
|
+
info: {},
|
|
21
|
+
paths: {},
|
|
22
|
+
components: {
|
|
23
|
+
schemas: {},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
generateSpec(controllers) {
|
|
28
|
+
try {
|
|
29
|
+
if (!Array.isArray(controllers)) {
|
|
30
|
+
throw new Error('Controllers must be an array');
|
|
31
|
+
}
|
|
32
|
+
core_2.default.debug('Generating spec for controllers:', controllers.map((c) => c?.name || 'undefined'));
|
|
33
|
+
// Reset spec
|
|
34
|
+
this.spec = {
|
|
35
|
+
openapi: '3.0.0',
|
|
36
|
+
info: {},
|
|
37
|
+
paths: {},
|
|
38
|
+
components: {
|
|
39
|
+
schemas: {},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
// Process each controller
|
|
43
|
+
controllers.forEach((controller) => {
|
|
44
|
+
if (!controller || typeof controller !== 'function') {
|
|
45
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
46
|
+
core_2.default.warn('Invalid controller found:', controller);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Get Swagger metadata from the controller prototype
|
|
51
|
+
const swaggerOptions = (0, swagger_decorator_1.getSwaggerMetadata)(controller.prototype);
|
|
52
|
+
if (!swaggerOptions) {
|
|
53
|
+
core_2.default.debug(`No Swagger metadata found for controller: ${controller.name}`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
core_2.default.debug(`Processing controller: ${controller.name}`, swaggerOptions);
|
|
57
|
+
// Update info if not already set
|
|
58
|
+
if (!this.spec.info.title) {
|
|
59
|
+
this.spec.info = {
|
|
60
|
+
title: swaggerOptions.title,
|
|
61
|
+
description: swaggerOptions.description,
|
|
62
|
+
version: swaggerOptions.version,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Add tags if not already set
|
|
66
|
+
if (swaggerOptions.tags && !this.spec.tags) {
|
|
67
|
+
this.spec.tags = swaggerOptions.tags;
|
|
68
|
+
}
|
|
69
|
+
// Get controller path from metadata
|
|
70
|
+
const controllerMetadata = Reflect.getMetadata('hazel:controller', controller) || {};
|
|
71
|
+
const basePath = controllerMetadata.path || '';
|
|
72
|
+
// Get route metadata
|
|
73
|
+
const routes = Reflect.getMetadata('hazel:routes', controller);
|
|
74
|
+
if (!routes) {
|
|
75
|
+
core_2.default.debug(`No routes found for controller: ${controller.name}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
core_2.default.debug(`Found routes for ${controller.name}:`, routes);
|
|
79
|
+
// Process each route
|
|
80
|
+
routes.forEach((route) => {
|
|
81
|
+
const { path, method, propertyKey } = route;
|
|
82
|
+
const fullPath = this.normalizePath(`${basePath}${path}`);
|
|
83
|
+
const operation = (0, swagger_decorator_1.getOperationMetadata)(controller.prototype, propertyKey);
|
|
84
|
+
if (!operation) {
|
|
85
|
+
core_2.default.debug(`No operation metadata found for method: ${String(propertyKey)}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
core_2.default.debug(`Adding operation for ${method} ${fullPath}`);
|
|
89
|
+
// Add operation to paths
|
|
90
|
+
const pathItem = this.spec.paths[fullPath] || {};
|
|
91
|
+
pathItem[method.toLowerCase()] = {
|
|
92
|
+
...operation,
|
|
93
|
+
tags: operation.tags || [controller.name],
|
|
94
|
+
};
|
|
95
|
+
this.spec.paths[fullPath] = pathItem;
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
core_2.default.debug('Generated Swagger specification:', this.spec);
|
|
99
|
+
return this.spec;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
103
|
+
core_2.default.error('Failed to generate Swagger specification:', error);
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
normalizePath(path) {
|
|
109
|
+
// Remove trailing slash
|
|
110
|
+
let normalized = path.replace(/\/$/, '');
|
|
111
|
+
// Ensure path starts with slash
|
|
112
|
+
if (!normalized.startsWith('/')) {
|
|
113
|
+
normalized = '/' + normalized;
|
|
114
|
+
}
|
|
115
|
+
return normalized;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
exports.SwaggerService = SwaggerService;
|
|
119
|
+
exports.SwaggerService = SwaggerService = __decorate([
|
|
120
|
+
(0, core_1.Service)()
|
|
121
|
+
], SwaggerService);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"swagger.service.test.d.ts","sourceRoot":"","sources":["../src/swagger.service.test.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
require("reflect-metadata");
|
|
13
|
+
const swagger_service_1 = require("./swagger.service");
|
|
14
|
+
const swagger_decorator_1 = require("./swagger.decorator");
|
|
15
|
+
const core_1 = require("@hazeljs/core");
|
|
16
|
+
describe('SwaggerService', () => {
|
|
17
|
+
let swaggerService;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
swaggerService = new swagger_service_1.SwaggerService();
|
|
20
|
+
});
|
|
21
|
+
describe('generateSpec', () => {
|
|
22
|
+
it('should generate spec for a controller with Swagger metadata', () => {
|
|
23
|
+
const swaggerOptions = {
|
|
24
|
+
title: 'Test API',
|
|
25
|
+
description: 'Test API description',
|
|
26
|
+
version: '1.0.0',
|
|
27
|
+
tags: [{ name: 'test', description: 'Test operations' }],
|
|
28
|
+
};
|
|
29
|
+
const getOperation = {
|
|
30
|
+
summary: 'Get test',
|
|
31
|
+
description: 'Get test description',
|
|
32
|
+
tags: ['test'],
|
|
33
|
+
responses: {
|
|
34
|
+
'200': {
|
|
35
|
+
description: 'Success',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const postOperation = {
|
|
40
|
+
summary: 'Create test',
|
|
41
|
+
description: 'Create test description',
|
|
42
|
+
tags: ['test'],
|
|
43
|
+
responses: {
|
|
44
|
+
'201': {
|
|
45
|
+
description: 'Created',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
let TestController = class TestController {
|
|
50
|
+
getTest() { }
|
|
51
|
+
createTest() { }
|
|
52
|
+
};
|
|
53
|
+
__decorate([
|
|
54
|
+
(0, core_1.Get)(),
|
|
55
|
+
(0, swagger_decorator_1.ApiOperation)(getOperation),
|
|
56
|
+
__metadata("design:type", Function),
|
|
57
|
+
__metadata("design:paramtypes", []),
|
|
58
|
+
__metadata("design:returntype", void 0)
|
|
59
|
+
], TestController.prototype, "getTest", null);
|
|
60
|
+
__decorate([
|
|
61
|
+
(0, core_1.Post)(),
|
|
62
|
+
(0, swagger_decorator_1.ApiOperation)(postOperation),
|
|
63
|
+
__metadata("design:type", Function),
|
|
64
|
+
__metadata("design:paramtypes", []),
|
|
65
|
+
__metadata("design:returntype", void 0)
|
|
66
|
+
], TestController.prototype, "createTest", null);
|
|
67
|
+
TestController = __decorate([
|
|
68
|
+
(0, swagger_decorator_1.Swagger)(swaggerOptions),
|
|
69
|
+
(0, core_1.Controller)({ path: '/test' })
|
|
70
|
+
], TestController);
|
|
71
|
+
// Mock controller metadata
|
|
72
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
73
|
+
// Mock route metadata
|
|
74
|
+
Reflect.defineMetadata('hazel:routes', [
|
|
75
|
+
{ propertyKey: 'getTest', path: '', method: 'GET' },
|
|
76
|
+
{ propertyKey: 'createTest', path: '', method: 'POST' },
|
|
77
|
+
], TestController);
|
|
78
|
+
const spec = swaggerService.generateSpec([TestController]);
|
|
79
|
+
expect(spec).toBeDefined();
|
|
80
|
+
expect(spec.openapi).toBe('3.0.0');
|
|
81
|
+
expect(spec.info).toEqual({
|
|
82
|
+
title: swaggerOptions.title,
|
|
83
|
+
description: swaggerOptions.description,
|
|
84
|
+
version: swaggerOptions.version,
|
|
85
|
+
});
|
|
86
|
+
expect(spec.tags).toEqual(swaggerOptions.tags);
|
|
87
|
+
expect(spec.paths['/test']).toBeDefined();
|
|
88
|
+
expect(spec.paths['/test'].get).toEqual({
|
|
89
|
+
summary: getOperation.summary,
|
|
90
|
+
description: getOperation.description,
|
|
91
|
+
tags: getOperation.tags,
|
|
92
|
+
responses: getOperation.responses,
|
|
93
|
+
});
|
|
94
|
+
expect(spec.paths['/test'].post).toEqual({
|
|
95
|
+
summary: postOperation.summary,
|
|
96
|
+
description: postOperation.description,
|
|
97
|
+
tags: postOperation.tags,
|
|
98
|
+
responses: postOperation.responses,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
it('should handle controller without Swagger metadata', () => {
|
|
102
|
+
let TestController = class TestController {
|
|
103
|
+
getTest() { }
|
|
104
|
+
};
|
|
105
|
+
__decorate([
|
|
106
|
+
(0, core_1.Get)(),
|
|
107
|
+
__metadata("design:type", Function),
|
|
108
|
+
__metadata("design:paramtypes", []),
|
|
109
|
+
__metadata("design:returntype", void 0)
|
|
110
|
+
], TestController.prototype, "getTest", null);
|
|
111
|
+
TestController = __decorate([
|
|
112
|
+
(0, core_1.Controller)({ path: '/test' })
|
|
113
|
+
], TestController);
|
|
114
|
+
// Mock route metadata
|
|
115
|
+
Reflect.defineMetadata('hazel:routes', [{ propertyKey: 'getTest', path: '/test', method: 'GET' }], TestController);
|
|
116
|
+
const spec = swaggerService.generateSpec([TestController]);
|
|
117
|
+
expect(spec).toBeDefined();
|
|
118
|
+
expect(spec.paths).toEqual({});
|
|
119
|
+
});
|
|
120
|
+
it('should handle invalid controllers', () => {
|
|
121
|
+
class InvalidController {
|
|
122
|
+
}
|
|
123
|
+
const spec = swaggerService.generateSpec([InvalidController]);
|
|
124
|
+
expect(spec).toBeDefined();
|
|
125
|
+
expect(spec.paths).toEqual({});
|
|
126
|
+
});
|
|
127
|
+
it('should handle controller without route metadata', () => {
|
|
128
|
+
let TestController = class TestController {
|
|
129
|
+
getTest() { }
|
|
130
|
+
};
|
|
131
|
+
__decorate([
|
|
132
|
+
(0, core_1.Get)(),
|
|
133
|
+
(0, swagger_decorator_1.ApiOperation)({
|
|
134
|
+
summary: 'Get test',
|
|
135
|
+
description: 'Get test description',
|
|
136
|
+
tags: ['test'],
|
|
137
|
+
responses: { '200': { description: 'Success' } },
|
|
138
|
+
}),
|
|
139
|
+
__metadata("design:type", Function),
|
|
140
|
+
__metadata("design:paramtypes", []),
|
|
141
|
+
__metadata("design:returntype", void 0)
|
|
142
|
+
], TestController.prototype, "getTest", null);
|
|
143
|
+
TestController = __decorate([
|
|
144
|
+
(0, swagger_decorator_1.Swagger)({
|
|
145
|
+
title: 'Test API',
|
|
146
|
+
description: 'Test API description',
|
|
147
|
+
version: '1.0.0',
|
|
148
|
+
tags: [{ name: 'test', description: 'Test operations' }],
|
|
149
|
+
}),
|
|
150
|
+
(0, core_1.Controller)({ path: '/test' })
|
|
151
|
+
], TestController);
|
|
152
|
+
// Explicitly remove route metadata
|
|
153
|
+
Reflect.deleteMetadata('hazel:routes', TestController);
|
|
154
|
+
const spec = swaggerService.generateSpec([TestController]);
|
|
155
|
+
expect(spec).toBeDefined();
|
|
156
|
+
expect(spec.paths).toEqual({});
|
|
157
|
+
});
|
|
158
|
+
it('should handle controller with method without operation metadata', () => {
|
|
159
|
+
let TestController = class TestController {
|
|
160
|
+
getTest() { }
|
|
161
|
+
};
|
|
162
|
+
__decorate([
|
|
163
|
+
(0, core_1.Get)(),
|
|
164
|
+
__metadata("design:type", Function),
|
|
165
|
+
__metadata("design:paramtypes", []),
|
|
166
|
+
__metadata("design:returntype", void 0)
|
|
167
|
+
], TestController.prototype, "getTest", null);
|
|
168
|
+
TestController = __decorate([
|
|
169
|
+
(0, swagger_decorator_1.Swagger)({
|
|
170
|
+
title: 'Test API',
|
|
171
|
+
description: 'Test API description',
|
|
172
|
+
version: '1.0.0',
|
|
173
|
+
tags: [{ name: 'test', description: 'Test operations' }],
|
|
174
|
+
}),
|
|
175
|
+
(0, core_1.Controller)({ path: '/test' })
|
|
176
|
+
], TestController);
|
|
177
|
+
// Mock route metadata
|
|
178
|
+
Reflect.defineMetadata('hazel:routes', [{ propertyKey: 'getTest', path: '/test', method: 'GET' }], TestController);
|
|
179
|
+
const spec = swaggerService.generateSpec([TestController]);
|
|
180
|
+
expect(spec).toBeDefined();
|
|
181
|
+
expect(spec.paths).toEqual({});
|
|
182
|
+
});
|
|
183
|
+
it('should throw error when controllers is not an array', () => {
|
|
184
|
+
expect(() => {
|
|
185
|
+
swaggerService.generateSpec(null);
|
|
186
|
+
}).toThrow('Controllers must be an array');
|
|
187
|
+
});
|
|
188
|
+
it('should handle null and undefined controllers', () => {
|
|
189
|
+
const spec = swaggerService.generateSpec([null, undefined, {}]);
|
|
190
|
+
expect(spec).toBeDefined();
|
|
191
|
+
expect(spec.paths).toEqual({});
|
|
192
|
+
});
|
|
193
|
+
it('should handle error during spec generation', () => {
|
|
194
|
+
// Test error handling by making getSwaggerMetadata throw
|
|
195
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
196
|
+
const swaggerDecorator = require('./swagger.decorator');
|
|
197
|
+
jest.spyOn(swaggerDecorator, 'getSwaggerMetadata').mockImplementation(() => {
|
|
198
|
+
throw new Error('Metadata access error');
|
|
199
|
+
});
|
|
200
|
+
let TestController = class TestController {
|
|
201
|
+
getTest() { }
|
|
202
|
+
};
|
|
203
|
+
__decorate([
|
|
204
|
+
(0, core_1.Get)(),
|
|
205
|
+
__metadata("design:type", Function),
|
|
206
|
+
__metadata("design:paramtypes", []),
|
|
207
|
+
__metadata("design:returntype", void 0)
|
|
208
|
+
], TestController.prototype, "getTest", null);
|
|
209
|
+
TestController = __decorate([
|
|
210
|
+
(0, core_1.Controller)({ path: '/test' })
|
|
211
|
+
], TestController);
|
|
212
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
213
|
+
Reflect.defineMetadata('hazel:routes', [{ propertyKey: 'getTest', path: '', method: 'GET' }], TestController);
|
|
214
|
+
expect(() => {
|
|
215
|
+
swaggerService.generateSpec([TestController]);
|
|
216
|
+
}).toThrow('Metadata access error');
|
|
217
|
+
// Restore original
|
|
218
|
+
jest.restoreAllMocks();
|
|
219
|
+
});
|
|
220
|
+
it('should normalize paths correctly', () => {
|
|
221
|
+
const swaggerOptions = {
|
|
222
|
+
title: 'Test API',
|
|
223
|
+
description: 'Test API description',
|
|
224
|
+
version: '1.0.0',
|
|
225
|
+
};
|
|
226
|
+
const getOperation = {
|
|
227
|
+
summary: 'Get test',
|
|
228
|
+
responses: { '200': { description: 'Success' } },
|
|
229
|
+
};
|
|
230
|
+
let TestController = class TestController {
|
|
231
|
+
getTest() { }
|
|
232
|
+
};
|
|
233
|
+
__decorate([
|
|
234
|
+
(0, core_1.Get)('path') // Path without leading slash
|
|
235
|
+
,
|
|
236
|
+
(0, swagger_decorator_1.ApiOperation)(getOperation),
|
|
237
|
+
__metadata("design:type", Function),
|
|
238
|
+
__metadata("design:paramtypes", []),
|
|
239
|
+
__metadata("design:returntype", void 0)
|
|
240
|
+
], TestController.prototype, "getTest", null);
|
|
241
|
+
TestController = __decorate([
|
|
242
|
+
(0, swagger_decorator_1.Swagger)(swaggerOptions),
|
|
243
|
+
(0, core_1.Controller)({ path: 'test' }) // Path without leading slash
|
|
244
|
+
], TestController);
|
|
245
|
+
Reflect.defineMetadata('hazel:controller', { path: 'test' }, TestController);
|
|
246
|
+
Reflect.defineMetadata('hazel:routes', [{ propertyKey: 'getTest', path: 'path', method: 'GET' }], TestController);
|
|
247
|
+
const spec = swaggerService.generateSpec([TestController]);
|
|
248
|
+
// The normalizePath concatenates basePath and path: 'test' + 'path' = 'testpath', then normalizes to '/testpath'
|
|
249
|
+
// But actually, paths should be joined with '/' if both exist
|
|
250
|
+
// Let's check what path was actually created
|
|
251
|
+
const pathKeys = Object.keys(spec.paths);
|
|
252
|
+
expect(pathKeys.length).toBeGreaterThan(0);
|
|
253
|
+
// The normalized path should start with '/'
|
|
254
|
+
const createdPath = pathKeys[0];
|
|
255
|
+
expect(createdPath.startsWith('/')).toBe(true);
|
|
256
|
+
expect(spec.paths[createdPath].get).toBeDefined();
|
|
257
|
+
});
|
|
258
|
+
it('should handle path with trailing slash', () => {
|
|
259
|
+
const swaggerOptions = {
|
|
260
|
+
title: 'Test API',
|
|
261
|
+
description: 'Test API description',
|
|
262
|
+
version: '1.0.0',
|
|
263
|
+
};
|
|
264
|
+
const getOperation = {
|
|
265
|
+
summary: 'Get test',
|
|
266
|
+
responses: { '200': { description: 'Success' } },
|
|
267
|
+
};
|
|
268
|
+
let TestController = class TestController {
|
|
269
|
+
getTest() { }
|
|
270
|
+
};
|
|
271
|
+
__decorate([
|
|
272
|
+
(0, core_1.Get)(),
|
|
273
|
+
(0, swagger_decorator_1.ApiOperation)(getOperation),
|
|
274
|
+
__metadata("design:type", Function),
|
|
275
|
+
__metadata("design:paramtypes", []),
|
|
276
|
+
__metadata("design:returntype", void 0)
|
|
277
|
+
], TestController.prototype, "getTest", null);
|
|
278
|
+
TestController = __decorate([
|
|
279
|
+
(0, swagger_decorator_1.Swagger)(swaggerOptions),
|
|
280
|
+
(0, core_1.Controller)({ path: '/test/' }) // Path with trailing slash
|
|
281
|
+
], TestController);
|
|
282
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test/' }, TestController);
|
|
283
|
+
Reflect.defineMetadata('hazel:routes', [{ propertyKey: 'getTest', path: '', method: 'GET' }], TestController);
|
|
284
|
+
const spec = swaggerService.generateSpec([TestController]);
|
|
285
|
+
expect(spec.paths['/test']).toBeDefined();
|
|
286
|
+
});
|
|
287
|
+
it('should handle multiple controllers with same base path', () => {
|
|
288
|
+
const swaggerOptions = {
|
|
289
|
+
title: 'Test API',
|
|
290
|
+
description: 'Test API description',
|
|
291
|
+
version: '1.0.0',
|
|
292
|
+
};
|
|
293
|
+
const getOperation = {
|
|
294
|
+
summary: 'Get test',
|
|
295
|
+
responses: { '200': { description: 'Success' } },
|
|
296
|
+
};
|
|
297
|
+
const postOperation = {
|
|
298
|
+
summary: 'Post test',
|
|
299
|
+
responses: { '201': { description: 'Created' } },
|
|
300
|
+
};
|
|
301
|
+
let TestController = class TestController {
|
|
302
|
+
getTest() { }
|
|
303
|
+
postTest() { }
|
|
304
|
+
};
|
|
305
|
+
__decorate([
|
|
306
|
+
(0, core_1.Get)(),
|
|
307
|
+
(0, swagger_decorator_1.ApiOperation)(getOperation),
|
|
308
|
+
__metadata("design:type", Function),
|
|
309
|
+
__metadata("design:paramtypes", []),
|
|
310
|
+
__metadata("design:returntype", void 0)
|
|
311
|
+
], TestController.prototype, "getTest", null);
|
|
312
|
+
__decorate([
|
|
313
|
+
(0, core_1.Post)(),
|
|
314
|
+
(0, swagger_decorator_1.ApiOperation)(postOperation),
|
|
315
|
+
__metadata("design:type", Function),
|
|
316
|
+
__metadata("design:paramtypes", []),
|
|
317
|
+
__metadata("design:returntype", void 0)
|
|
318
|
+
], TestController.prototype, "postTest", null);
|
|
319
|
+
TestController = __decorate([
|
|
320
|
+
(0, swagger_decorator_1.Swagger)(swaggerOptions),
|
|
321
|
+
(0, core_1.Controller)({ path: '/test' })
|
|
322
|
+
], TestController);
|
|
323
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
324
|
+
Reflect.defineMetadata('hazel:routes', [
|
|
325
|
+
{ propertyKey: 'getTest', path: '', method: 'GET' },
|
|
326
|
+
{ propertyKey: 'postTest', path: '', method: 'POST' },
|
|
327
|
+
], TestController);
|
|
328
|
+
const spec = swaggerService.generateSpec([TestController]);
|
|
329
|
+
expect(spec.paths['/test']).toBeDefined();
|
|
330
|
+
expect(spec.paths['/test'].get).toBeDefined();
|
|
331
|
+
expect(spec.paths['/test'].post).toBeDefined();
|
|
332
|
+
});
|
|
333
|
+
it('should use controller name as tag when operation has no tags', () => {
|
|
334
|
+
const swaggerOptions = {
|
|
335
|
+
title: 'Test API',
|
|
336
|
+
description: 'Test API description',
|
|
337
|
+
version: '1.0.0',
|
|
338
|
+
};
|
|
339
|
+
const getOperation = {
|
|
340
|
+
summary: 'Get test',
|
|
341
|
+
responses: { '200': { description: 'Success' } },
|
|
342
|
+
// No tags property
|
|
343
|
+
};
|
|
344
|
+
let TestController = class TestController {
|
|
345
|
+
getTest() { }
|
|
346
|
+
};
|
|
347
|
+
__decorate([
|
|
348
|
+
(0, core_1.Get)(),
|
|
349
|
+
(0, swagger_decorator_1.ApiOperation)(getOperation),
|
|
350
|
+
__metadata("design:type", Function),
|
|
351
|
+
__metadata("design:paramtypes", []),
|
|
352
|
+
__metadata("design:returntype", void 0)
|
|
353
|
+
], TestController.prototype, "getTest", null);
|
|
354
|
+
TestController = __decorate([
|
|
355
|
+
(0, swagger_decorator_1.Swagger)(swaggerOptions),
|
|
356
|
+
(0, core_1.Controller)({ path: '/test' })
|
|
357
|
+
], TestController);
|
|
358
|
+
Reflect.defineMetadata('hazel:controller', { path: '/test' }, TestController);
|
|
359
|
+
Reflect.defineMetadata('hazel:routes', [{ propertyKey: 'getTest', path: '', method: 'GET' }], TestController);
|
|
360
|
+
const spec = swaggerService.generateSpec([TestController]);
|
|
361
|
+
expect(spec.paths['/test'].get.tags).toEqual(['TestController']);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface SwaggerOptions {
|
|
2
|
+
title: string;
|
|
3
|
+
description: string;
|
|
4
|
+
version: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
tags?: SwaggerTag[];
|
|
7
|
+
}
|
|
8
|
+
export interface SwaggerTag {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
}
|
|
12
|
+
export interface SwaggerOperation {
|
|
13
|
+
summary?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
tags?: string[];
|
|
16
|
+
parameters?: SwaggerParameter[];
|
|
17
|
+
requestBody?: {
|
|
18
|
+
required?: boolean;
|
|
19
|
+
content: {
|
|
20
|
+
[contentType: string]: {
|
|
21
|
+
schema: SwaggerSchema;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
responses?: Record<string, SwaggerResponse>;
|
|
26
|
+
}
|
|
27
|
+
export interface SwaggerParameter {
|
|
28
|
+
name: string;
|
|
29
|
+
in: 'path' | 'query' | 'header' | 'cookie';
|
|
30
|
+
description?: string;
|
|
31
|
+
required?: boolean;
|
|
32
|
+
schema: SwaggerSchema;
|
|
33
|
+
}
|
|
34
|
+
export interface SwaggerResponse {
|
|
35
|
+
description: string;
|
|
36
|
+
content?: {
|
|
37
|
+
[contentType: string]: {
|
|
38
|
+
schema: SwaggerSchema;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface SwaggerSchema {
|
|
43
|
+
type: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object';
|
|
44
|
+
format?: string;
|
|
45
|
+
items?: SwaggerSchema;
|
|
46
|
+
properties?: Record<string, SwaggerSchema>;
|
|
47
|
+
required?: string[];
|
|
48
|
+
example?: string | number | boolean | null | Record<string, unknown> | unknown[];
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=swagger.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"swagger.types.d.ts","sourceRoot":"","sources":["../src/swagger.types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,UAAU,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAChC,WAAW,CAAC,EAAE;QACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,OAAO,EAAE;YACP,CAAC,WAAW,EAAE,MAAM,GAAG;gBACrB,MAAM,EAAE,aAAa,CAAC;aACvB,CAAC;SACH,CAAC;KACH,CAAC;IACF,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE;QACR,CAAC,WAAW,EAAE,MAAM,GAAG;YACrB,MAAM,EAAE,aAAa,CAAC;SACvB,CAAC;KACH,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC3C,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,EAAE,CAAC;CAClF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hazeljs/swagger",
|
|
3
|
+
"version": "0.2.0-alpha.1",
|
|
4
|
+
"description": "Swagger/OpenAPI documentation module for HazelJS framework",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "jest --coverage",
|
|
13
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
14
|
+
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
15
|
+
"clean": "rm -rf dist"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"swagger-ui-express": "^5.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^20.17.50",
|
|
22
|
+
"@types/swagger-ui-express": "^4.1.6",
|
|
23
|
+
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
|
24
|
+
"@typescript-eslint/parser": "^8.18.2",
|
|
25
|
+
"eslint": "^8.56.0",
|
|
26
|
+
"jest": "^29.7.0",
|
|
27
|
+
"ts-jest": "^29.1.2",
|
|
28
|
+
"typescript": "^5.3.3"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/hazel-js/hazeljs.git",
|
|
36
|
+
"directory": "packages/swagger"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"hazeljs",
|
|
40
|
+
"swagger",
|
|
41
|
+
"openapi",
|
|
42
|
+
"api-docs",
|
|
43
|
+
"documentation"
|
|
44
|
+
],
|
|
45
|
+
"author": "Muhammad Arslan <muhammad.arslan@hazeljs.com>",
|
|
46
|
+
"license": "Apache-2.0",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/hazeljs/hazel-js/issues"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://hazeljs.com",
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@hazeljs/core": ">=0.2.0-beta.0"
|
|
53
|
+
},
|
|
54
|
+
"gitHead": "cbc5ee2c12ced28fd0576faf13c5f078c1e8421e"
|
|
55
|
+
}
|