Skip to content

Commit c68c074

Browse files
Fixed optional field bug; Added comments & documentation for custom api decorator.
1 parent 9dd7d3e commit c68c074

File tree

1 file changed

+180
-18
lines changed

1 file changed

+180
-18
lines changed

src/common/decorators/api.decorator.ts

Lines changed: 180 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,68 @@ import {
1616
} from '@nestjs/swagger';
1717
import { RedisAuthGuard } from '../../auth/redis-auth.guard';
1818

19+
/**
20+
* API Decorator Response Type
21+
* Alias for Swagger's ApiResponseOptions to simplify type usage
22+
*/
1923
export type ApiDecoratorResponse = ApiResponseOptions;
2024

25+
/**
26+
* API Decorator Options Interface
27+
*
28+
* This interface defines all available options for the @Api() decorator,
29+
* which provides a unified way to configure Swagger documentation for NestJS endpoints.
30+
*
31+
* @example
32+
* ```typescript
33+
* @Api({
34+
* summary: 'Get all users',
35+
* description: 'Returns a paginated list of users',
36+
* paginatedResponseType: UserDto,
37+
* queriesFrom: [PaginationArgs, UserFilterDto],
38+
* authenticationRequired: true,
39+
* envelope: true
40+
* })
41+
* ```
42+
*/
2143
export interface ApiOptions {
2244
/** Shorthand for ApiOperation -> summary */
2345
summary?: string;
46+
2447
/** Shorthand for ApiOperation -> description */
2548
description?: string;
49+
2650
/** Full ApiOperation options (overrides summary/description if provided) */
2751
apiOperationOptions?: Partial<ApiOperationOptions>;
52+
2853
/** Explicit list of responses. If provided, default Created/Unauthorized/Forbidden set is suppressed (except Unauthorized when auth required). */
2954
responses?: ApiDecoratorResponse[];
30-
/** Backwards compat (deprecated) */
55+
56+
/** @deprecated Backwards compatibility - use 'responses' instead */
3157
apiResponses?: Partial<ApiResponseOptions>[];
32-
/** When true attaches RedisAuthGuard + bearer auth + 401 response (if not explicitly supplied). */
58+
59+
/** When true, attaches RedisAuthGuard + bearer auth + 401 response (if not explicitly supplied). */
3360
authenticationRequired?: boolean;
34-
/** DTO / class for request body */
61+
62+
/** DTO / class for request body - will be automatically inferred from method signature if not provided */
3563
bodyType?: NestType<any> | (new (...args: any[]) => any);
36-
/** Path params */
64+
65+
/**
66+
* Manually defined path parameters
67+
* For automatic path param extraction, use 'pathParamsFrom' instead
68+
*/
3769
params?: Array<{
3870
name: string;
3971
description?: string;
4072
type?: any;
4173
required?: boolean;
4274
enum?: any[];
4375
}>;
44-
/** Query params */
76+
77+
/**
78+
* Manually defined query parameters
79+
* For automatic query param extraction, use 'queriesFrom' instead
80+
*/
4581
queries?: Array<{
4682
name: string;
4783
description?: string;
@@ -50,25 +86,88 @@ export interface ApiOptions {
5086
enum?: any[];
5187
example?: any;
5288
}>;
53-
/** Derive query params from one or more DTO / classes with Field decorators */
89+
90+
/**
91+
* Automatically derive query params from one or more DTOs with @Field decorators
92+
* The decorator will read metadata stored by @Field decorators (where inQuery: true)
93+
* and generate @ApiQuery decorators for Swagger documentation
94+
*
95+
* @example
96+
* queriesFrom: [PaginationArgs, JobFilterDto]
97+
*/
5498
queriesFrom?: (new (...args: any[]) => any) | Array<new (...args: any[]) => any>;
55-
/** Derive path params from one or more DTO / classes with Field decorators */
99+
100+
/**
101+
* Automatically derive path params from one or more DTOs with @Field decorators
102+
* The decorator will read metadata stored by @Field decorators (where inPath: true)
103+
* and generate @ApiParam decorators for Swagger documentation
104+
*
105+
* @example
106+
* pathParamsFrom: JobIdPathParamsDto
107+
*/
56108
pathParamsFrom?: (new (...args: any[]) => any) | Array<new (...args: any[]) => any>;
57-
/** Mark operation deprecated */
109+
110+
/** Mark operation deprecated in Swagger UI */
58111
deprecated?: boolean;
112+
59113
/** Shorthand to specify a single 200 response type */
60114
responseType?: NestType<any>;
115+
61116
/** Shorthand to specify an array 200 response type */
62117
responseArrayType?: NestType<any>;
63-
/** Shorthand to specify a paginated 200 response type (items + pageInfo) */
118+
119+
/**
120+
* Shorthand to specify a paginated 200 response type
121+
* Generates a schema with { items: [], pageInfo: {}, totalCount: number, meta?: {} }
122+
*/
64123
paginatedResponseType?: NestType<any>;
65-
/** Wrap successful 2xx response in a standard envelope { success, data, error? } */
124+
125+
/**
126+
* When true, wraps successful 2xx response in a standard envelope: { success, data, error? }
127+
* This metadata is also stored for use by interceptors
128+
*/
66129
envelope?: boolean;
67130
}
68131

132+
/**
133+
* @Api Decorator
134+
*
135+
* A powerful unified decorator for configuring Swagger/OpenAPI documentation in NestJS.
136+
* Combines multiple Swagger decorators into a single, declarative interface.
137+
*
138+
* Key Features:
139+
* - Automatic request body type inference from method signatures
140+
* - Auto-generation of query/path params from DTOs with @Field decorators
141+
* - Support for paginated, array, and enveloped responses
142+
* - Built-in authentication guard integration
143+
* - Flexible response configuration
144+
*
145+
* @param options - Configuration options for the API endpoint
146+
* @returns A method decorator that applies all necessary Swagger decorators
147+
*
148+
* @example
149+
* ```typescript
150+
* @Get()
151+
* @Api({
152+
* summary: 'Get all jobs',
153+
* description: 'Returns a paginated list of jobs with optional filters',
154+
* paginatedResponseType: JobDto,
155+
* queriesFrom: [PaginationArgs, JobFilterDto],
156+
* envelope: true
157+
* })
158+
* async findAll(@Query() pagination: PaginationArgs, @Query() filters: JobFilterDto) {
159+
* // ...
160+
* }
161+
* ```
162+
*/
69163
export function Api(options: ApiOptions): MethodDecorator {
70164
return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
71-
// Attempt body type inference if not supplied
165+
// ============================================================================
166+
// STEP 1: Automatic Body Type Inference
167+
// ============================================================================
168+
// Attempt to infer the request body type from the method's parameter types
169+
// if it hasn't been explicitly provided in the options.
170+
// This looks for DTO or Input classes in the method signature.
72171
if (!options.bodyType) {
73172
try {
74173
const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', target, propertyKey) || [];
@@ -85,29 +184,49 @@ export function Api(options: ApiOptions): MethodDecorator {
85184
}
86185
}
87186

187+
// ============================================================================
188+
// STEP 2: Build API Operation Metadata
189+
// ============================================================================
190+
// Construct the ApiOperation options with summary, description, and deprecation info
88191
const op: ApiOperationOptions = {
89192
summary: options.summary,
90193
description: options.description,
91194
deprecated: options.deprecated,
92195
...(options.apiOperationOptions || {}),
93196
} as ApiOperationOptions;
94197

198+
// ============================================================================
199+
// STEP 3: Determine Response Strategy
200+
// ============================================================================
201+
// Check if the user has provided custom responses.
202+
// If they have, we skip adding default responses (Created, Unauthorized, Forbidden)
95203
const userProvidedResponses = (options.responses || options.apiResponses || []).filter((v) =>
96204
Boolean(v)
97205
) as ApiResponseOptions[];
98206
const addDefaultSet = userProvidedResponses.length === 0; // Only add defaults when no custom responses passed
99207

208+
// ============================================================================
209+
// STEP 4: Build Decorator Chain
210+
// ============================================================================
211+
// Start building the array of decorators that will be applied to the method
100212
const decorators: any[] = [];
213+
214+
// Add authentication guards if required
101215
if (options.authenticationRequired) {
102216
decorators.push(UseGuards(RedisAuthGuard), ApiBearerAuth());
103217
}
218+
104219
decorators.push(ApiOperation(op));
105220

221+
// Add request body documentation if bodyType is specified
106222
if (options.bodyType) {
107223
decorators.push(ApiBody({ type: options.bodyType }));
108224
}
109225

110-
// Params
226+
// ============================================================================
227+
// STEP 5: Path Parameters
228+
// ============================================================================
229+
// Add manually defined path parameters
111230
(options.params || []).forEach((p) =>
112231
decorators.push(
113232
ApiParam({
@@ -120,7 +239,8 @@ export function Api(options: ApiOptions): MethodDecorator {
120239
)
121240
);
122241

123-
// Auto path params from metadata
242+
// Auto-generate path parameters from DTOs with @Field decorators
243+
// Reads metadata set by @Field(... inPath: true) decorators
124244
if (options.pathParamsFrom) {
125245
const sources = Array.isArray(options.pathParamsFrom) ? options.pathParamsFrom : [options.pathParamsFrom];
126246
sources.forEach((src) => {
@@ -142,7 +262,11 @@ export function Api(options: ApiOptions): MethodDecorator {
142262
});
143263
}
144264

145-
// Queries
265+
// ============================================================================
266+
// STEP 6: Query Parameters
267+
// ============================================================================
268+
// Add manually defined query parameters
269+
// Note: For manual queries, required defaults to false (must be explicitly true)
146270
(options.queries || []).forEach((q) =>
147271
decorators.push(
148272
ApiQuery({
@@ -156,10 +280,15 @@ export function Api(options: ApiOptions): MethodDecorator {
156280
)
157281
);
158282

283+
// Auto-generate query parameters from DTOs with @Field decorators
284+
// Reads metadata set by @Field(... inQuery: true) decorators
285+
// IMPORTANT: We explicitly set required to true or false (not undefined)
286+
// to ensure Swagger correctly displays optional fields
159287
if (options.queriesFrom) {
160288
const sources = Array.isArray(options.queriesFrom) ? options.queriesFrom : [options.queriesFrom];
161289
sources.forEach((src) => {
162290
if (!src) return;
291+
// Retrieve metadata stored by @Field decorator
163292
const qMeta = Reflect.getMetadata('cb:fieldMeta', src.prototype) || [];
164293
qMeta
165294
.filter((m: any) => m.inQuery)
@@ -168,7 +297,9 @@ export function Api(options: ApiOptions): MethodDecorator {
168297
ApiQuery({
169298
name: m.name,
170299
description: m.description,
171-
required: m.required === true,
300+
// Explicitly set required to true or false (never undefined)
301+
// This ensures Swagger properly marks optional fields as not required
302+
required: m.required === true ? true : false,
172303
enum: m.enum,
173304
type: m.type,
174305
})
@@ -177,11 +308,17 @@ export function Api(options: ApiOptions): MethodDecorator {
177308
});
178309
}
179310

311+
// ============================================================================
312+
// STEP 7: Response Documentation
313+
// ============================================================================
314+
// Generate response schemas based on the provided options
180315
// Shorthand 200 response helpers (only if user didn't explicitly define 200)
181316
const hasExplicit200 = userProvidedResponses.some((r) => r.status === 200);
182317
if (!hasExplicit200) {
318+
// Single object response
183319
if (options.responseType) {
184320
if (options.envelope) {
321+
// Wrapped in envelope: { success: true, data: {...} }
185322
decorators.push(
186323
ApiResponse({
187324
status: 200,
@@ -196,14 +333,18 @@ export function Api(options: ApiOptions): MethodDecorator {
196333
})
197334
);
198335
} else {
336+
// Direct response without envelope
199337
decorators.push(ApiResponse({ status: 200, description: 'Successful response', type: options.responseType }));
200338
}
201-
} else if (options.responseArrayType) {
339+
}
340+
// Array response
341+
else if (options.responseArrayType) {
202342
const arraySchema = {
203343
type: 'array',
204344
items: { $ref: getSchemaPath(options.responseArrayType) },
205345
};
206346
if (options.envelope) {
347+
// Wrapped in envelope: { success: true, data: [...] }
207348
decorators.push(
208349
ApiResponse({
209350
status: 200,
@@ -218,6 +359,7 @@ export function Api(options: ApiOptions): MethodDecorator {
218359
})
219360
);
220361
} else {
362+
// Direct array response
221363
decorators.push(
222364
ApiResponse({
223365
status: 200,
@@ -226,7 +368,9 @@ export function Api(options: ApiOptions): MethodDecorator {
226368
})
227369
);
228370
}
229-
} else if (options.paginatedResponseType) {
371+
}
372+
// Paginated response with items, pageInfo, totalCount, and optional meta
373+
else if (options.paginatedResponseType) {
230374
const basePaginated = {
231375
type: 'object',
232376
properties: {
@@ -253,6 +397,7 @@ export function Api(options: ApiOptions): MethodDecorator {
253397
},
254398
};
255399
if (options.envelope) {
400+
// Wrapped in envelope: { success: true, data: { items, pageInfo, totalCount, meta } }
256401
decorators.push(
257402
ApiResponse({
258403
status: 200,
@@ -267,6 +412,7 @@ export function Api(options: ApiOptions): MethodDecorator {
267412
})
268413
);
269414
} else {
415+
// Direct paginated response
270416
decorators.push(
271417
ApiResponse({
272418
status: 200,
@@ -278,23 +424,35 @@ export function Api(options: ApiOptions): MethodDecorator {
278424
}
279425
}
280426

427+
// ============================================================================
428+
// STEP 8: Default Error Responses
429+
// ============================================================================
430+
// Add default error responses (401, 201, 403) if user hasn't provided custom responses
281431
if (addDefaultSet) {
282432
decorators.push(ApiUnauthorizedResponse({ description: 'Unauthorized' }));
283433
decorators.push(ApiCreatedResponse({ description: 'The record has been successfully created.' }));
284434
decorators.push(ApiForbiddenResponse({ description: 'Forbidden.' }));
285435
} else {
436+
// If user provided custom responses, ensure 401 is still added for authenticated endpoints
286437
let has401 = userProvidedResponses.some((r) => r.status === 401);
287438
if (options.authenticationRequired && !has401) {
288439
decorators.push(ApiUnauthorizedResponse({ description: 'Unauthorized' }));
289440
has401 = true;
290441
}
291442
}
292443

444+
// ============================================================================
445+
// STEP 9: Add User-Provided Responses
446+
// ============================================================================
447+
// Add any custom responses provided by the user
293448
if (userProvidedResponses.length > 0) {
294449
userProvidedResponses.forEach((r) => decorators.push(ApiResponse(r)));
295450
}
296451

297-
// Store envelope intention for interceptor usage
452+
// ============================================================================
453+
// STEP 10: Store Metadata for Interceptors
454+
// ============================================================================
455+
// Store envelope metadata so interceptors can wrap responses appropriately
298456
if (options.envelope) {
299457
try {
300458
Reflect.defineMetadata('cb:envelope', true, descriptor.value);
@@ -303,6 +461,10 @@ export function Api(options: ApiOptions): MethodDecorator {
303461
}
304462
}
305463

464+
// ============================================================================
465+
// STEP 11: Apply All Decorators
466+
// ============================================================================
467+
// Apply all collected decorators to the target method
306468
applyDecorators(...decorators)(target, propertyKey, descriptor);
307469
};
308470
}

0 commit comments

Comments
 (0)