@buenojs/bueno 0.8.4 → 0.8.6
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 +264 -17
- package/dist/cli/{index.js → bin.js} +413 -332
- package/dist/container/index.js +273 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/graphql/index.js +2156 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +9694 -5047
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3411 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +795 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/llms.txt +231 -0
- package/package.json +125 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/ARCHITECTURE.md +3 -3
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +294 -232
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +37 -18
- package/src/cli/templates/database/mysql.ts +3 -3
- package/src/cli/templates/database/none.ts +2 -2
- package/src/cli/templates/database/postgresql.ts +3 -3
- package/src/cli/templates/database/sqlite.ts +3 -3
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +33 -15
- package/src/cli/templates/frontend/none.ts +2 -2
- package/src/cli/templates/frontend/react.ts +18 -18
- package/src/cli/templates/frontend/solid.ts +15 -15
- package/src/cli/templates/frontend/svelte.ts +17 -17
- package/src/cli/templates/frontend/vue.ts +15 -15
- package/src/cli/templates/generators/index.ts +29 -29
- package/src/cli/templates/generators/types.ts +21 -21
- package/src/cli/templates/index.ts +6 -6
- package/src/cli/templates/project/api.ts +37 -36
- package/src/cli/templates/project/default.ts +25 -25
- package/src/cli/templates/project/fullstack.ts +28 -26
- package/src/cli/templates/project/index.ts +55 -16
- package/src/cli/templates/project/minimal.ts +17 -12
- package/src/cli/templates/project/types.ts +10 -5
- package/src/cli/templates/project/website.ts +15 -15
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -3
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +14 -8
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +566 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/graphql/built-in-engine.ts +598 -0
- package/src/graphql/context-builder.ts +110 -0
- package/src/graphql/decorators.ts +358 -0
- package/src/graphql/execution-pipeline.ts +227 -0
- package/src/graphql/graphql-module.ts +563 -0
- package/src/graphql/index.ts +101 -0
- package/src/graphql/metadata.ts +237 -0
- package/src/graphql/schema-builder.ts +319 -0
- package/src/graphql/subscription-handler.ts +283 -0
- package/src/graphql/types.ts +324 -0
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +182 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +457 -299
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/cli.test.ts +19 -19
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/cli.test.ts +1 -1
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/graphql.test.ts +991 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- package/tsconfig.json +11 -3
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { SchemaGenerator } from '../schema-generator';
|
|
3
|
+
import { ApiProperty, ApiPropertyOptional } from '../decorators';
|
|
4
|
+
|
|
5
|
+
// ============= Test Fixtures =============
|
|
6
|
+
|
|
7
|
+
class SimpleDto {
|
|
8
|
+
@ApiProperty({ description: 'User name' })
|
|
9
|
+
name!: string;
|
|
10
|
+
|
|
11
|
+
@ApiPropertyOptional({ description: 'User email' })
|
|
12
|
+
email?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class UserDto {
|
|
16
|
+
@ApiProperty({ description: 'User ID' })
|
|
17
|
+
id!: string;
|
|
18
|
+
|
|
19
|
+
@ApiProperty({ description: 'Email address', format: 'email' })
|
|
20
|
+
email!: string;
|
|
21
|
+
|
|
22
|
+
@ApiProperty({ description: 'Full name', minLength: 2, maxLength: 100 })
|
|
23
|
+
name!: string;
|
|
24
|
+
|
|
25
|
+
@ApiPropertyOptional({ description: 'Age', minimum: 0, maximum: 150 })
|
|
26
|
+
age?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class CreateUserDto {
|
|
30
|
+
@ApiProperty({ description: 'Email address', example: 'user@example.com' })
|
|
31
|
+
email!: string;
|
|
32
|
+
|
|
33
|
+
@ApiProperty({ description: 'Password', minLength: 8 })
|
|
34
|
+
password!: string;
|
|
35
|
+
|
|
36
|
+
@ApiPropertyOptional({ description: 'Full name' })
|
|
37
|
+
name?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============= Tests =============
|
|
41
|
+
|
|
42
|
+
describe('SchemaGenerator', () => {
|
|
43
|
+
describe('Primitive type generation', () => {
|
|
44
|
+
test('should generate string schema from String type', () => {
|
|
45
|
+
const generator = new SchemaGenerator();
|
|
46
|
+
const schema = generator.generateSchema(String);
|
|
47
|
+
expect(schema.type).toBe('string');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('should generate number schema from Number type', () => {
|
|
51
|
+
const generator = new SchemaGenerator();
|
|
52
|
+
const schema = generator.generateSchema(Number);
|
|
53
|
+
expect(schema.type).toBe('number');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should generate boolean schema from Boolean type', () => {
|
|
57
|
+
const generator = new SchemaGenerator();
|
|
58
|
+
const schema = generator.generateSchema(Boolean);
|
|
59
|
+
expect(schema.type).toBe('boolean');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should generate schema from string type names', () => {
|
|
63
|
+
const generator = new SchemaGenerator();
|
|
64
|
+
|
|
65
|
+
expect(generator.generateSchema('string').type).toBe('string');
|
|
66
|
+
expect(generator.generateSchema('number').type).toBe('number');
|
|
67
|
+
expect(generator.generateSchema('integer').type).toBe('integer');
|
|
68
|
+
expect(generator.generateSchema('boolean').type).toBe('boolean');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('should generate email format for email type', () => {
|
|
72
|
+
const generator = new SchemaGenerator();
|
|
73
|
+
const schema = generator.generateSchema('email');
|
|
74
|
+
expect(schema.type).toBe('string');
|
|
75
|
+
expect(schema.format).toBe('email');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('should generate date-time format for datetime type', () => {
|
|
79
|
+
const generator = new SchemaGenerator();
|
|
80
|
+
const schema = generator.generateSchema('datetime');
|
|
81
|
+
expect(schema.type).toBe('string');
|
|
82
|
+
expect(schema.format).toBe('date-time');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('should generate uuid format for uuid type', () => {
|
|
86
|
+
const generator = new SchemaGenerator();
|
|
87
|
+
const schema = generator.generateSchema('uuid');
|
|
88
|
+
expect(schema.type).toBe('string');
|
|
89
|
+
expect(schema.format).toBe('uuid');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('should generate uri format for url type', () => {
|
|
93
|
+
const generator = new SchemaGenerator();
|
|
94
|
+
const schema = generator.generateSchema('url');
|
|
95
|
+
expect(schema.type).toBe('string');
|
|
96
|
+
expect(schema.format).toBe('uri');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('Object/DTO schema generation', () => {
|
|
101
|
+
test('should generate schema for simple DTO', () => {
|
|
102
|
+
const generator = new SchemaGenerator();
|
|
103
|
+
const schema = generator.generateSchema(SimpleDto);
|
|
104
|
+
|
|
105
|
+
expect(schema.$ref).toBeDefined();
|
|
106
|
+
expect(schema.$ref).toContain('SimpleDto');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('should cache and return reference for same DTO', () => {
|
|
110
|
+
const generator = new SchemaGenerator();
|
|
111
|
+
const schema1 = generator.generateSchema(SimpleDto);
|
|
112
|
+
const schema2 = generator.generateSchema(SimpleDto);
|
|
113
|
+
|
|
114
|
+
expect(schema1).toEqual(schema2);
|
|
115
|
+
expect(schema1.$ref).toBe(schema2.$ref);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('should generate schemas map with DTO properties', () => {
|
|
119
|
+
const generator = new SchemaGenerator();
|
|
120
|
+
generator.generateSchema(UserDto);
|
|
121
|
+
|
|
122
|
+
const schemas = generator.getSchemas();
|
|
123
|
+
expect(schemas).toHaveProperty('UserDto');
|
|
124
|
+
|
|
125
|
+
const userSchema = schemas['UserDto'];
|
|
126
|
+
expect(userSchema.type).toBe('object');
|
|
127
|
+
expect(userSchema.properties).toBeDefined();
|
|
128
|
+
expect(userSchema.properties?.['name']).toBeDefined();
|
|
129
|
+
expect(userSchema.properties?.['email']).toBeDefined();
|
|
130
|
+
expect(userSchema.properties?.['age']).toBeDefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('should respect required fields', () => {
|
|
134
|
+
const generator = new SchemaGenerator();
|
|
135
|
+
generator.generateSchema(UserDto);
|
|
136
|
+
|
|
137
|
+
const schemas = generator.getSchemas();
|
|
138
|
+
const userSchema = schemas['UserDto'];
|
|
139
|
+
|
|
140
|
+
expect(userSchema.required).toContain('name');
|
|
141
|
+
expect(userSchema.required).toContain('email');
|
|
142
|
+
expect(userSchema.required).toContain('id');
|
|
143
|
+
|
|
144
|
+
// Optional fields should not be in required
|
|
145
|
+
if (userSchema.required) {
|
|
146
|
+
expect(userSchema.required.includes('age')).toBe(false);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('should include property descriptions', () => {
|
|
151
|
+
const generator = new SchemaGenerator();
|
|
152
|
+
generator.generateSchema(UserDto);
|
|
153
|
+
|
|
154
|
+
const schemas = generator.getSchemas();
|
|
155
|
+
const userSchema = schemas['UserDto'];
|
|
156
|
+
|
|
157
|
+
expect((userSchema.properties?.['name'] as any)?.description).toBe('Full name');
|
|
158
|
+
expect((userSchema.properties?.['email'] as any)?.description).toBe('Email address');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('should include format information', () => {
|
|
162
|
+
const generator = new SchemaGenerator();
|
|
163
|
+
generator.generateSchema(UserDto);
|
|
164
|
+
|
|
165
|
+
const schemas = generator.getSchemas();
|
|
166
|
+
const userSchema = schemas['UserDto'];
|
|
167
|
+
|
|
168
|
+
expect((userSchema.properties?.['email'] as any)?.format).toBe('email');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('should include validation constraints', () => {
|
|
172
|
+
const generator = new SchemaGenerator();
|
|
173
|
+
generator.generateSchema(UserDto);
|
|
174
|
+
|
|
175
|
+
const schemas = generator.getSchemas();
|
|
176
|
+
const userSchema = schemas['UserDto'];
|
|
177
|
+
|
|
178
|
+
const nameProperty = userSchema.properties?.['name'] as any;
|
|
179
|
+
expect(nameProperty?.minLength).toBe(2);
|
|
180
|
+
expect(nameProperty?.maxLength).toBe(100);
|
|
181
|
+
|
|
182
|
+
const ageProperty = userSchema.properties?.['age'] as any;
|
|
183
|
+
expect(ageProperty?.minimum).toBe(0);
|
|
184
|
+
expect(ageProperty?.maximum).toBe(150);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('should include examples and defaults', () => {
|
|
188
|
+
const generator = new SchemaGenerator();
|
|
189
|
+
generator.generateSchema(CreateUserDto);
|
|
190
|
+
|
|
191
|
+
const schemas = generator.getSchemas();
|
|
192
|
+
const createSchema = schemas['CreateUserDto'];
|
|
193
|
+
|
|
194
|
+
const emailProperty = createSchema.properties?.['email'] as any;
|
|
195
|
+
expect(emailProperty?.example).toBe('user@example.com');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('Multiple DTO schema generation', () => {
|
|
200
|
+
test('should generate schemas for multiple DTOs', () => {
|
|
201
|
+
const generator = new SchemaGenerator();
|
|
202
|
+
generator.generateSchema(UserDto);
|
|
203
|
+
generator.generateSchema(CreateUserDto);
|
|
204
|
+
|
|
205
|
+
const schemas = generator.getSchemas();
|
|
206
|
+
expect(schemas).toHaveProperty('UserDto');
|
|
207
|
+
expect(schemas).toHaveProperty('CreateUserDto');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('should handle DTOs with same property names differently', () => {
|
|
211
|
+
const generator = new SchemaGenerator();
|
|
212
|
+
const userRef = generator.generateSchema(UserDto);
|
|
213
|
+
const createRef = generator.generateSchema(CreateUserDto);
|
|
214
|
+
|
|
215
|
+
expect(userRef.$ref).not.toBe(createRef.$ref);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('Schema clearing', () => {
|
|
220
|
+
test('should clear all cached schemas', () => {
|
|
221
|
+
const generator = new SchemaGenerator();
|
|
222
|
+
generator.generateSchema(UserDto);
|
|
223
|
+
generator.generateSchema(CreateUserDto);
|
|
224
|
+
|
|
225
|
+
let schemas = generator.getSchemas();
|
|
226
|
+
expect(Object.keys(schemas).length).toBeGreaterThan(0);
|
|
227
|
+
|
|
228
|
+
generator.clear();
|
|
229
|
+
schemas = generator.getSchemas();
|
|
230
|
+
expect(Object.keys(schemas).length).toBe(0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('should regenerate schemas after clearing', () => {
|
|
234
|
+
const generator = new SchemaGenerator();
|
|
235
|
+
generator.generateSchema(UserDto);
|
|
236
|
+
generator.clear();
|
|
237
|
+
|
|
238
|
+
const ref1 = generator.generateSchema(UserDto);
|
|
239
|
+
const ref2 = generator.generateSchema(UserDto);
|
|
240
|
+
|
|
241
|
+
expect(ref1).toEqual(ref2);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('Array type handling', () => {
|
|
246
|
+
test('should handle Array type', () => {
|
|
247
|
+
const generator = new SchemaGenerator();
|
|
248
|
+
const schema = generator.generateSchema(Array);
|
|
249
|
+
expect(schema.type).toBe('array');
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('Type name generation', () => {
|
|
254
|
+
test('should use class name for DTO', () => {
|
|
255
|
+
const generator = new SchemaGenerator();
|
|
256
|
+
generator.generateSchema(UserDto);
|
|
257
|
+
|
|
258
|
+
const schemas = generator.getSchemas();
|
|
259
|
+
expect(schemas).toHaveProperty('UserDto');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('should generate unique names for multiple generations', () => {
|
|
263
|
+
const generator = new SchemaGenerator();
|
|
264
|
+
|
|
265
|
+
// Create anonymous classes
|
|
266
|
+
const AnonClass1 = class {};
|
|
267
|
+
const AnonClass2 = class {};
|
|
268
|
+
|
|
269
|
+
const schema1 = generator.generateSchema(AnonClass1);
|
|
270
|
+
const schema2 = generator.generateSchema(AnonClass2);
|
|
271
|
+
|
|
272
|
+
expect(schema1.$ref).not.toBe(schema2.$ref);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI Decorators
|
|
3
|
+
*
|
|
4
|
+
* Decorators for documenting controllers, methods, parameters, and DTOs with OpenAPI metadata.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ApiBodyOptions,
|
|
9
|
+
ApiHeaderOptions,
|
|
10
|
+
ApiKeySecurityOptions,
|
|
11
|
+
ApiOperationOptions,
|
|
12
|
+
ApiParamOptions,
|
|
13
|
+
ApiPropertyOptions,
|
|
14
|
+
ApiQueryOptions,
|
|
15
|
+
ApiResponseOptions,
|
|
16
|
+
Constructor,
|
|
17
|
+
OpenAPISecurity,
|
|
18
|
+
SecuritySchemeOptions,
|
|
19
|
+
} from './types';
|
|
20
|
+
import {
|
|
21
|
+
getApiMetadata,
|
|
22
|
+
getApiMethodMetadata,
|
|
23
|
+
getApiPropertyMetadata,
|
|
24
|
+
setApiMetadata,
|
|
25
|
+
setApiMethodMetadata,
|
|
26
|
+
setApiPropertyMetadata,
|
|
27
|
+
} from './metadata';
|
|
28
|
+
|
|
29
|
+
// ============= Class-Level Decorators =============
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Mark one or more tags that apply to all operations in this controller
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* @Controller('/users')
|
|
37
|
+
* @ApiTags('users', 'accounts')
|
|
38
|
+
* class UserController { ... }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function ApiTags(...tags: string[]): ClassDecorator {
|
|
42
|
+
return <TFunction extends Function>(target: TFunction): TFunction => {
|
|
43
|
+
const existingTags = getApiMetadata<string[]>(target as unknown as Constructor, 'api:tags') ?? [];
|
|
44
|
+
const combined = [...new Set([...existingTags, ...tags])];
|
|
45
|
+
setApiMetadata(target as unknown as Constructor, 'api:tags', combined);
|
|
46
|
+
return target;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Mark this controller with Bearer token authentication
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* @Controller('/api')
|
|
56
|
+
* @ApiBearerAuth()
|
|
57
|
+
* class ApiController { ... }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function ApiBearerAuth(name = 'bearer', options?: SecuritySchemeOptions): ClassDecorator | MethodDecorator {
|
|
61
|
+
return function (target: unknown, propertyKey?: string | symbol) {
|
|
62
|
+
const security: OpenAPISecurity[] = [{ [name]: [] }];
|
|
63
|
+
const targetObj = propertyKey
|
|
64
|
+
? (target as object) // Method decorator
|
|
65
|
+
: (target as unknown as Constructor); // Class decorator
|
|
66
|
+
|
|
67
|
+
const store = propertyKey ? getApiMethodMetadata : getApiMetadata;
|
|
68
|
+
const setSt = propertyKey ? setApiMethodMetadata : setApiMetadata;
|
|
69
|
+
const existingSecurity = store<OpenAPISecurity[]>(
|
|
70
|
+
targetObj,
|
|
71
|
+
'api:security',
|
|
72
|
+
) ?? [];
|
|
73
|
+
|
|
74
|
+
setSt(targetObj, 'api:security', [...existingSecurity, ...security]);
|
|
75
|
+
return propertyKey ? descriptor : target;
|
|
76
|
+
} as any;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Mark this controller with Basic authentication
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* @Controller('/api')
|
|
85
|
+
* @ApiBasicAuth()
|
|
86
|
+
* class ApiController { ... }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function ApiBasicAuth(name = 'basic'): ClassDecorator | MethodDecorator {
|
|
90
|
+
return function (target: unknown, propertyKey?: string | symbol) {
|
|
91
|
+
const security: OpenAPISecurity[] = [{ [name]: [] }];
|
|
92
|
+
const targetObj = propertyKey
|
|
93
|
+
? (target as object) // Method decorator
|
|
94
|
+
: (target as unknown as Constructor); // Class decorator
|
|
95
|
+
|
|
96
|
+
const store = propertyKey ? getApiMethodMetadata : getApiMetadata;
|
|
97
|
+
const setSt = propertyKey ? setApiMethodMetadata : setApiMetadata;
|
|
98
|
+
const existingSecurity = store<OpenAPISecurity[]>(
|
|
99
|
+
targetObj,
|
|
100
|
+
'api:security',
|
|
101
|
+
) ?? [];
|
|
102
|
+
|
|
103
|
+
setSt(targetObj, 'api:security', [...existingSecurity, ...security]);
|
|
104
|
+
return propertyKey ? descriptor : target;
|
|
105
|
+
} as any;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Mark this controller with API Key authentication
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* @Controller('/api')
|
|
114
|
+
* @ApiApiKey({ in: 'header', name: 'X-API-Key' })
|
|
115
|
+
* class ApiController { ... }
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export function ApiApiKey(options: ApiKeySecurityOptions, name = 'api_key'): ClassDecorator | MethodDecorator {
|
|
119
|
+
return function (target: unknown, propertyKey?: string | symbol) {
|
|
120
|
+
const security: OpenAPISecurity[] = [{ [name]: [] }];
|
|
121
|
+
const targetObj = propertyKey
|
|
122
|
+
? (target as object) // Method decorator
|
|
123
|
+
: (target as unknown as Constructor); // Class decorator
|
|
124
|
+
|
|
125
|
+
const store = propertyKey ? getApiMethodMetadata : getApiMetadata;
|
|
126
|
+
const setSt = propertyKey ? setApiMethodMetadata : setApiMetadata;
|
|
127
|
+
const existingSecurity = store<OpenAPISecurity[]>(
|
|
128
|
+
targetObj,
|
|
129
|
+
'api:security',
|
|
130
|
+
) ?? [];
|
|
131
|
+
|
|
132
|
+
// Store security scheme metadata for document builder
|
|
133
|
+
setSt(targetObj, `api:security:scheme:${name}`, options);
|
|
134
|
+
setSt(targetObj, 'api:security', [...existingSecurity, ...security]);
|
|
135
|
+
return propertyKey ? descriptor : target;
|
|
136
|
+
} as any;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Exclude this entire controller from OpenAPI documentation
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* @Controller('/internal')
|
|
145
|
+
* @ApiExcludeController()
|
|
146
|
+
* class InternalController { ... }
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export function ApiExcludeController(): ClassDecorator {
|
|
150
|
+
return <TFunction extends Function>(target: TFunction): TFunction => {
|
|
151
|
+
setApiMetadata(target as unknown as Constructor, 'api:exclude', true);
|
|
152
|
+
return target;
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ============= Method-Level Decorators =============
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Document the operation (HTTP method) with summary, description, and other metadata
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* @Get('/:id')
|
|
164
|
+
* @ApiOperation({ summary: 'Get user by ID', description: 'Retrieve a single user' })
|
|
165
|
+
* getUser(@Param('id') id: string) { ... }
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
export function ApiOperation(options: ApiOperationOptions): MethodDecorator {
|
|
169
|
+
return (target: unknown, propertyKey: string | symbol) => {
|
|
170
|
+
setApiMethodMetadata(target as object, `api:operation:${String(propertyKey)}`, options);
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Document an HTTP response from this operation
|
|
176
|
+
* Can be used multiple times for different status codes
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* @ApiResponse({ status: 200, description: 'Success', type: UserDto })
|
|
181
|
+
* @ApiResponse({ status: 404, description: 'Not found' })
|
|
182
|
+
* getUser() { ... }
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
export function ApiResponse(options: ApiResponseOptions): MethodDecorator {
|
|
186
|
+
return (target: unknown, propertyKey: string | symbol) => {
|
|
187
|
+
const key = `api:responses:${String(propertyKey)}`;
|
|
188
|
+
const existing = getApiMethodMetadata<ApiResponseOptions[]>(target as object, key) ?? [];
|
|
189
|
+
existing.push(options);
|
|
190
|
+
setApiMethodMetadata(target as object, key, existing);
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Document a path parameter
|
|
196
|
+
* Can be used multiple times for different parameters
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```typescript
|
|
200
|
+
* @Get('/:userId/posts/:postId')
|
|
201
|
+
* @ApiParam({ name: 'userId', type: 'string', description: 'User ID' })
|
|
202
|
+
* @ApiParam({ name: 'postId', type: 'string', description: 'Post ID' })
|
|
203
|
+
* getPost(@Param('userId') userId: string, @Param('postId') postId: string) { ... }
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export function ApiParam(options: ApiParamOptions): MethodDecorator {
|
|
207
|
+
return (target: unknown, propertyKey: string | symbol) => {
|
|
208
|
+
const key = `api:params:${String(propertyKey)}`;
|
|
209
|
+
const existing = getApiMethodMetadata<ApiParamOptions[]>(target as object, key) ?? [];
|
|
210
|
+
existing.push(options);
|
|
211
|
+
setApiMethodMetadata(target as object, key, existing);
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Document a query parameter
|
|
217
|
+
* Can be used multiple times for different parameters
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```typescript
|
|
221
|
+
* @Get()
|
|
222
|
+
* @ApiQuery({ name: 'page', type: 'number', description: 'Page number', required: false })
|
|
223
|
+
* @ApiQuery({ name: 'limit', type: 'number', description: 'Items per page', required: false })
|
|
224
|
+
* getUsers(@Query('page') page?: number, @Query('limit') limit?: number) { ... }
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
export function ApiQuery(options: ApiQueryOptions): MethodDecorator {
|
|
228
|
+
return (target: unknown, propertyKey: string | symbol) => {
|
|
229
|
+
const key = `api:query:${String(propertyKey)}`;
|
|
230
|
+
const existing = getApiMethodMetadata<ApiQueryOptions[]>(target as object, key) ?? [];
|
|
231
|
+
existing.push(options);
|
|
232
|
+
setApiMethodMetadata(target as object, key, existing);
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Document an HTTP header
|
|
238
|
+
* Can be used multiple times for different headers
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```typescript
|
|
242
|
+
* @Post()
|
|
243
|
+
* @ApiHeader({ name: 'X-Request-ID', description: 'Request ID', required: true })
|
|
244
|
+
* create() { ... }
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
export function ApiHeader(options: ApiHeaderOptions): MethodDecorator {
|
|
248
|
+
return (target: unknown, propertyKey: string | symbol) => {
|
|
249
|
+
const key = `api:headers:${String(propertyKey)}`;
|
|
250
|
+
const existing = getApiMethodMetadata<ApiHeaderOptions[]>(target as object, key) ?? [];
|
|
251
|
+
existing.push(options);
|
|
252
|
+
setApiMethodMetadata(target as object, key, existing);
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Document the request body
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```typescript
|
|
261
|
+
* @Post()
|
|
262
|
+
* @ApiBody({ type: CreateUserDto, description: 'User data to create' })
|
|
263
|
+
* create(@Body() dto: CreateUserDto) { ... }
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
export function ApiBody(options: ApiBodyOptions): MethodDecorator {
|
|
267
|
+
return (target: unknown, propertyKey: string | symbol) => {
|
|
268
|
+
setApiMethodMetadata(target as object, `api:body:${String(propertyKey)}`, options);
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Exclude this endpoint from OpenAPI documentation
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* @Get()
|
|
278
|
+
* @ApiExcludeEndpoint()
|
|
279
|
+
* internalOnly() { ... }
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
export function ApiExcludeEndpoint(): MethodDecorator {
|
|
283
|
+
return (target: unknown, propertyKey: string | symbol) => {
|
|
284
|
+
setApiMethodMetadata(target as object, `api:exclude:${String(propertyKey)}`, true);
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ============= Property-Level Decorators (for DTOs) =============
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Document a DTO property with type, description, validation rules, etc.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* class CreateUserDto {
|
|
296
|
+
* @ApiProperty({ description: 'Email address', example: 'user@example.com' })
|
|
297
|
+
* email: string;
|
|
298
|
+
*
|
|
299
|
+
* @ApiProperty({ minLength: 2, maxLength: 50, description: 'Full name' })
|
|
300
|
+
* name: string;
|
|
301
|
+
* }
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
export function ApiProperty(options?: ApiPropertyOptions): PropertyDecorator {
|
|
305
|
+
return (target: object, propertyKey: string | symbol) => {
|
|
306
|
+
const opts: ApiPropertyOptions = { ...options, required: options?.required !== false };
|
|
307
|
+
setApiPropertyMetadata(target, propertyKey, opts);
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Document an optional DTO property
|
|
313
|
+
* Equivalent to ApiProperty with required: false
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* ```typescript
|
|
317
|
+
* class UserFilterDto {
|
|
318
|
+
* @ApiPropertyOptional({ description: 'Filter by name', example: 'John' })
|
|
319
|
+
* name?: string;
|
|
320
|
+
* }
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
323
|
+
export function ApiPropertyOptional(options?: Omit<ApiPropertyOptions, 'required'>): PropertyDecorator {
|
|
324
|
+
return (target: object, propertyKey: string | symbol) => {
|
|
325
|
+
const opts: ApiPropertyOptions = { ...options, required: false };
|
|
326
|
+
setApiPropertyMetadata(target, propertyKey, opts);
|
|
327
|
+
};
|
|
328
|
+
}
|