speedrun-cli 2.6.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/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/bin/cli.js +224 -0
- package/index.js +12 -0
- package/package.json +74 -0
- package/src/constants.js +65 -0
- package/src/generator.js +271 -0
- package/src/index.js +13 -0
- package/src/moduleGenerator.js +586 -0
- package/src/postSetup.js +365 -0
- package/src/prompts.js +189 -0
- package/src/utils.js +112 -0
- package/templates/README.md +81 -0
- package/templates/base/eslint.config.mjs +34 -0
- package/templates/base/gitignore +78 -0
- package/templates/base/nest-cli.json +8 -0
- package/templates/base/src/common/constants/cookie.config.ts +20 -0
- package/templates/base/src/common/decorators/get-user.decorator.ts +13 -0
- package/templates/base/src/common/decorators/public.decorator.ts +4 -0
- package/templates/base/src/common/decorators/roles.decorator.ts +4 -0
- package/templates/base/src/common/dtos/pagination.dto.ts +29 -0
- package/templates/base/src/common/filters/http-exception.filter.ts +108 -0
- package/templates/base/src/common/guards/auth.guard.ts +51 -0
- package/templates/base/src/common/guards/refresh-token.guard.ts +39 -0
- package/templates/base/src/common/guards/roles.guard.ts +45 -0
- package/templates/base/src/common/interceptors/response.interceptor.ts +55 -0
- package/templates/base/src/common/interfaces/api-response.interface.ts +26 -0
- package/templates/base/src/common/middleware/correlation-id.middleware.ts +20 -0
- package/templates/base/src/common/validators/password.validator.ts +37 -0
- package/templates/base/src/config/config.module.ts +14 -0
- package/templates/base/src/config/logger.config.ts +109 -0
- package/templates/base/src/main.ts +84 -0
- package/templates/base/src/modules/auth/auth.controller.ts +133 -0
- package/templates/base/src/modules/auth/dtos/login.dto.ts +11 -0
- package/templates/base/src/modules/auth/dtos/signup.dto.ts +8 -0
- package/templates/base/src/modules/health/health.controller.ts +20 -0
- package/templates/base/src/modules/health/health.module.ts +7 -0
- package/templates/base/src/modules/users/dtos/update-profile.dto.ts +12 -0
- package/templates/base/src/modules/users/dtos/update-user.dto.ts +13 -0
- package/templates/base/src/modules/users/users.controller.ts +65 -0
- package/templates/base/test/app.e2e-spec.ts +24 -0
- package/templates/base/test/jest-e2e.json +9 -0
- package/templates/base/tsconfig.build.json +4 -0
- package/templates/base/tsconfig.json +24 -0
- package/templates/base-crud/CRUD_README.md +385 -0
- package/templates/base-crud/src/common/base/base.controller.ts +321 -0
- package/templates/base-crud/src/common/base/base.service.ts +192 -0
- package/templates/base-crud/src/common/base/index.ts +20 -0
- package/templates/base-crud/src/common/base/swagger/api-response.dto.ts +82 -0
- package/templates/base-crud/src/common/base/swagger/paginated.dto.ts +102 -0
- package/templates/base-crud/src/modules/products/dto/create-product.dto.ts +44 -0
- package/templates/base-crud/src/modules/products/dto/product.dto.ts +38 -0
- package/templates/base-crud/src/modules/products/dto/update-product.dto.ts +12 -0
- package/templates/base-crud/src/modules/products/products.controller.ts +94 -0
- package/templates/base-crud-drizzle/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-drizzle/src/modules/products/products.module.ts +24 -0
- package/templates/base-crud-drizzle/src/modules/products/products.service.ts +140 -0
- package/templates/base-crud-drizzle/src/modules/products/schema/products.schema.ts +39 -0
- package/templates/base-crud-mongoose/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-mongoose/src/modules/products/products.module.ts +26 -0
- package/templates/base-crud-mongoose/src/modules/products/products.service.ts +133 -0
- package/templates/base-crud-mongoose/src/modules/products/schemas/product.schema.ts +67 -0
- package/templates/base-crud-prisma/src/modules/products/products.module.ts +12 -0
- package/templates/base-crud-prisma/src/modules/products/products.service.ts +100 -0
- package/templates/base-crud-typeorm/src/modules/products/entities/product.entity.ts +58 -0
- package/templates/base-crud-typeorm/src/modules/products/products.controller.ts +79 -0
- package/templates/base-crud-typeorm/src/modules/products/products.module.ts +25 -0
- package/templates/base-crud-typeorm/src/modules/products/products.service.ts +102 -0
- package/templates/database/mongodb/.env.example +22 -0
- package/templates/database/mysql/.env.example +24 -0
- package/templates/database/mysql/drizzle.config.ts +13 -0
- package/templates/database/mysql/package.json +5 -0
- package/templates/database/mysql/prisma/schema.prisma +55 -0
- package/templates/database/mysql/src/database/drizzle.ts +13 -0
- package/templates/database/mysql/src/database/schema.ts +58 -0
- package/templates/database/postgres/.env.example +24 -0
- package/templates/database/postgres/drizzle.config.ts +13 -0
- package/templates/database/postgres/package.json +8 -0
- package/templates/database/postgres/prisma/schema.prisma +55 -0
- package/templates/database/sqlite/.env.example +24 -0
- package/templates/database/sqlite/drizzle.config.ts +13 -0
- package/templates/database/sqlite/package.json +8 -0
- package/templates/database/sqlite/prisma/schema.prisma +48 -0
- package/templates/database/sqlite/src/database/drizzle.ts +11 -0
- package/templates/database/sqlite/src/database/schema.ts +52 -0
- package/templates/orm/drizzle/drizzle.config.ts +13 -0
- package/templates/orm/drizzle/package.json +90 -0
- package/templates/orm/drizzle/src/app.module.ts +73 -0
- package/templates/orm/drizzle/src/config/env.validation.ts +55 -0
- package/templates/orm/drizzle/src/database/database.module.ts +25 -0
- package/templates/orm/drizzle/src/database/drizzle.ts +13 -0
- package/templates/orm/drizzle/src/database/schema.ts +60 -0
- package/templates/orm/drizzle/src/database/seed.ts +87 -0
- package/templates/orm/drizzle/src/modules/auth/auth.module.ts +12 -0
- package/templates/orm/drizzle/src/modules/auth/auth.service.ts +298 -0
- package/templates/orm/drizzle/src/modules/health/health.controller.ts +34 -0
- package/templates/orm/drizzle/src/modules/health/health.module.ts +7 -0
- package/templates/orm/drizzle/src/modules/users/users.module.ts +10 -0
- package/templates/orm/drizzle/src/modules/users/users.service.ts +152 -0
- package/templates/orm/mongoose/.env.example +22 -0
- package/templates/orm/mongoose/package.json +86 -0
- package/templates/orm/mongoose/src/app.module.ts +73 -0
- package/templates/orm/mongoose/src/config/env.validation.ts +55 -0
- package/templates/orm/mongoose/src/database/database.module.ts +19 -0
- package/templates/orm/mongoose/src/database/seed.ts +107 -0
- package/templates/orm/mongoose/src/modules/auth/auth.module.ts +21 -0
- package/templates/orm/mongoose/src/modules/auth/auth.service.ts +272 -0
- package/templates/orm/mongoose/src/modules/auth/dtos/login.dto.ts +10 -0
- package/templates/orm/mongoose/src/modules/auth/dtos/signup.dto.ts +8 -0
- package/templates/orm/mongoose/src/modules/health/health.controller.ts +26 -0
- package/templates/orm/mongoose/src/modules/health/health.module.ts +7 -0
- package/templates/orm/mongoose/src/modules/users/dtos/update-profile.dto.ts +24 -0
- package/templates/orm/mongoose/src/modules/users/dtos/update-user.dto.ts +13 -0
- package/templates/orm/mongoose/src/modules/users/users.module.ts +19 -0
- package/templates/orm/mongoose/src/modules/users/users.service.ts +179 -0
- package/templates/orm/mongoose/src/schemas/index.ts +2 -0
- package/templates/orm/mongoose/src/schemas/refresh-token.schema.ts +55 -0
- package/templates/orm/mongoose/src/schemas/user.schema.ts +53 -0
- package/templates/orm/prisma/package.json +102 -0
- package/templates/orm/prisma/prisma/schema.prisma +60 -0
- package/templates/orm/prisma/prisma/seed.ts +69 -0
- package/templates/orm/prisma/src/app.module.ts +55 -0
- package/templates/orm/prisma/src/config/env.validation.ts +56 -0
- package/templates/orm/prisma/src/modules/auth/auth.module.ts +17 -0
- package/templates/orm/prisma/src/modules/auth/auth.service.ts +308 -0
- package/templates/orm/prisma/src/modules/health/health.controller.ts +26 -0
- package/templates/orm/prisma/src/modules/health/health.module.ts +10 -0
- package/templates/orm/prisma/src/modules/users/users.module.ts +11 -0
- package/templates/orm/prisma/src/modules/users/users.service.ts +105 -0
- package/templates/orm/prisma/src/prisma/prisma.module.ts +9 -0
- package/templates/orm/prisma/src/prisma/prisma.service.ts +16 -0
- package/templates/orm/typeorm/package.json +100 -0
- package/templates/orm/typeorm/src/app.module.ts +55 -0
- package/templates/orm/typeorm/src/config/env.validation.ts +58 -0
- package/templates/orm/typeorm/src/database/data-source.ts +19 -0
- package/templates/orm/typeorm/src/database/database.module.ts +27 -0
- package/templates/orm/typeorm/src/database/seed.ts +76 -0
- package/templates/orm/typeorm/src/entities/index.ts +2 -0
- package/templates/orm/typeorm/src/entities/refresh-token.entity.ts +45 -0
- package/templates/orm/typeorm/src/entities/user.entity.ts +51 -0
- package/templates/orm/typeorm/src/modules/auth/auth.module.ts +19 -0
- package/templates/orm/typeorm/src/modules/auth/auth.service.ts +286 -0
- package/templates/orm/typeorm/src/modules/health/health.controller.ts +24 -0
- package/templates/orm/typeorm/src/modules/health/health.module.ts +9 -0
- package/templates/orm/typeorm/src/modules/users/users.module.ts +13 -0
- package/templates/orm/typeorm/src/modules/users/users.service.ts +108 -0
- package/templates/swagger/src/common/dtos/pagination.dto.ts +43 -0
- package/templates/swagger/src/main.ts +116 -0
- package/templates/swagger/src/modules/auth/auth.controller.ts +235 -0
- package/templates/swagger/src/modules/auth/dtos/login.dto.ts +20 -0
- package/templates/swagger/src/modules/auth/dtos/signup.dto.ts +14 -0
- package/templates/swagger/src/modules/users/dtos/update-profile.dto.ts +19 -0
- package/templates/swagger/src/modules/users/dtos/update-user.dto.ts +23 -0
- package/templates/swagger/src/modules/users/users.controller.ts +214 -0
- package/templates/swagger-mongoose/src/modules/auth/dtos/login.dto.ts +20 -0
- package/templates/swagger-mongoose/src/modules/auth/dtos/signup.dto.ts +14 -0
- package/templates/swagger-mongoose/src/modules/users/dtos/update-profile.dto.ts +40 -0
- package/templates/swagger-mongoose/src/modules/users/dtos/update-user.dto.ts +23 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Injectable,
|
|
3
|
+
NotFoundException,
|
|
4
|
+
BadRequestException,
|
|
5
|
+
Logger,
|
|
6
|
+
Type,
|
|
7
|
+
} from '@nestjs/common';
|
|
8
|
+
import { PaginationQueryDto, PaginatedResponseDto } from './swagger/paginated.dto';
|
|
9
|
+
|
|
10
|
+
// ─────────────────────────────────────────────────────────────
|
|
11
|
+
// Repository Contract Interface
|
|
12
|
+
// Implement this interface in your concrete repository/service
|
|
13
|
+
// to fulfill the abstract contract required by BaseService.
|
|
14
|
+
// ─────────────────────────────────────────────────────────────
|
|
15
|
+
export interface IBaseRepository<T, CreateDto, UpdateDto> {
|
|
16
|
+
create(dto: CreateDto): Promise<T>;
|
|
17
|
+
findAll(pagination: PaginationQueryDto): Promise<{ data: T[]; total: number }>;
|
|
18
|
+
findOne(id: string | number): Promise<T | null>;
|
|
19
|
+
update(id: string | number, dto: UpdateDto): Promise<T>;
|
|
20
|
+
remove(id: string | number): Promise<T>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────────────────────
|
|
24
|
+
// Abstract Base Service
|
|
25
|
+
//
|
|
26
|
+
// Generic parameters:
|
|
27
|
+
// T — Entity / Document type (e.g. Product, User)
|
|
28
|
+
// CreateDto — DTO for creation (e.g. CreateProductDto)
|
|
29
|
+
// UpdateDto — DTO for updates (e.g. UpdateProductDto)
|
|
30
|
+
//
|
|
31
|
+
// How to extend:
|
|
32
|
+
// @Injectable()
|
|
33
|
+
// export class ProductsService extends BaseService<Product, CreateProductDto, UpdateProductDto> {
|
|
34
|
+
// constructor(private readonly prisma: PrismaService) { super(); }
|
|
35
|
+
// protected getRepository(): IBaseRepository<...> { return new ProductRepository(this.prisma); }
|
|
36
|
+
// }
|
|
37
|
+
// ─────────────────────────────────────────────────────────────
|
|
38
|
+
@Injectable()
|
|
39
|
+
export abstract class BaseService<T, CreateDto, UpdateDto> {
|
|
40
|
+
protected readonly logger: Logger;
|
|
41
|
+
|
|
42
|
+
constructor() {
|
|
43
|
+
// Logger uses the concrete class name for traceability
|
|
44
|
+
this.logger = new Logger(this.constructor.name);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Subclasses MUST implement this to provide the data-access repository.
|
|
49
|
+
* This decouples the base service from any specific ORM/database driver.
|
|
50
|
+
*/
|
|
51
|
+
protected abstract getRepository(): IBaseRepository<T, CreateDto, UpdateDto>;
|
|
52
|
+
|
|
53
|
+
// ─────────────────────────────────────────────────────────
|
|
54
|
+
// CREATE
|
|
55
|
+
// ─────────────────────────────────────────────────────────
|
|
56
|
+
/**
|
|
57
|
+
* Creates a new resource.
|
|
58
|
+
* The DTO is already validated and whitelisted by the global ValidationPipe.
|
|
59
|
+
*
|
|
60
|
+
* @param createDto - Validated creation payload
|
|
61
|
+
* @returns The newly created entity
|
|
62
|
+
* @throws BadRequestException on constraint violations (propagated from repo)
|
|
63
|
+
*/
|
|
64
|
+
async create(createDto: CreateDto): Promise<T> {
|
|
65
|
+
try {
|
|
66
|
+
this.logger.debug(`Creating new ${this.constructor.name.replace('Service', '')} record`);
|
|
67
|
+
const entity = await this.getRepository().create(createDto);
|
|
68
|
+
this.logger.log(`Created record successfully`);
|
|
69
|
+
return entity;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error instanceof BadRequestException || error instanceof NotFoundException) {
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
this.logger.error(`Failed to create record: ${(error as Error).message}`, (error as Error).stack);
|
|
75
|
+
throw new BadRequestException('Failed to create resource. Please check your input and try again.');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─────────────────────────────────────────────────────────
|
|
80
|
+
// READ ALL (Paginated)
|
|
81
|
+
// ─────────────────────────────────────────────────────────
|
|
82
|
+
/**
|
|
83
|
+
* Retrieves a paginated list of resources.
|
|
84
|
+
*
|
|
85
|
+
* @param pagination - Validated pagination query params (page, limit)
|
|
86
|
+
* @returns PaginatedResponseDto wrapping the result array and meta
|
|
87
|
+
*/
|
|
88
|
+
async findAll(pagination: PaginationQueryDto): Promise<PaginatedResponseDto<T>> {
|
|
89
|
+
const { page = 1, limit = 10 } = pagination;
|
|
90
|
+
this.logger.debug(`Fetching records: page=${page}, limit=${limit}`);
|
|
91
|
+
|
|
92
|
+
const { data, total } = await this.getRepository().findAll(pagination);
|
|
93
|
+
|
|
94
|
+
const totalPages = Math.ceil(total / limit);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
success: true,
|
|
98
|
+
data,
|
|
99
|
+
meta: {
|
|
100
|
+
total,
|
|
101
|
+
page,
|
|
102
|
+
limit,
|
|
103
|
+
totalPages,
|
|
104
|
+
hasNext: page < totalPages,
|
|
105
|
+
hasPrevious: page > 1,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─────────────────────────────────────────────────────────
|
|
111
|
+
// READ ONE
|
|
112
|
+
// ─────────────────────────────────────────────────────────
|
|
113
|
+
/**
|
|
114
|
+
* Retrieves a single resource by its unique identifier.
|
|
115
|
+
*
|
|
116
|
+
* @param id - Entity ID (UUID string or integer, validated by pipe in controller)
|
|
117
|
+
* @returns The found entity
|
|
118
|
+
* @throws NotFoundException if no record exists with the given ID
|
|
119
|
+
*/
|
|
120
|
+
async findOne(id: string | number): Promise<T> {
|
|
121
|
+
this.logger.debug(`Fetching record with id=${id}`);
|
|
122
|
+
const entity = await this.getRepository().findOne(id);
|
|
123
|
+
|
|
124
|
+
if (!entity) {
|
|
125
|
+
this.logger.warn(`Record not found: id=${id}`);
|
|
126
|
+
throw new NotFoundException(`Resource with id "${id}" was not found`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return entity;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─────────────────────────────────────────────────────────
|
|
133
|
+
// UPDATE
|
|
134
|
+
// ─────────────────────────────────────────────────────────
|
|
135
|
+
/**
|
|
136
|
+
* Updates an existing resource (partial update — PATCH semantics).
|
|
137
|
+
* Validates existence before applying update.
|
|
138
|
+
*
|
|
139
|
+
* @param id - Entity ID
|
|
140
|
+
* @param updateDto - Validated partial update payload
|
|
141
|
+
* @returns The updated entity
|
|
142
|
+
* @throws NotFoundException if no record exists with the given ID
|
|
143
|
+
* @throws BadRequestException on constraint violations
|
|
144
|
+
*/
|
|
145
|
+
async update(id: string | number, updateDto: UpdateDto): Promise<T> {
|
|
146
|
+
// Validate existence first (throws 404 if not found)
|
|
147
|
+
await this.findOne(id);
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
this.logger.debug(`Updating record: id=${id}`);
|
|
151
|
+
const updated = await this.getRepository().update(id, updateDto);
|
|
152
|
+
this.logger.log(`Updated record: id=${id}`);
|
|
153
|
+
return updated;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof BadRequestException || error instanceof NotFoundException) {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
this.logger.error(`Failed to update record id=${id}: ${(error as Error).message}`, (error as Error).stack);
|
|
159
|
+
throw new BadRequestException('Failed to update resource. Please check your input and try again.');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─────────────────────────────────────────────────────────
|
|
164
|
+
// REMOVE (Soft Delete)
|
|
165
|
+
// ─────────────────────────────────────────────────────────
|
|
166
|
+
/**
|
|
167
|
+
* Soft-deletes a resource by setting `deletedAt` timestamp via the repository.
|
|
168
|
+
* Validates existence before deletion.
|
|
169
|
+
*
|
|
170
|
+
* NOTE: Your ORM entity MUST have a `deletedAt?: Date` field for soft-delete.
|
|
171
|
+
* For hard-delete, override this method in the concrete service.
|
|
172
|
+
*
|
|
173
|
+
* @param id - Entity ID
|
|
174
|
+
* @returns The soft-deleted entity snapshot
|
|
175
|
+
* @throws NotFoundException if no record exists with the given ID
|
|
176
|
+
*/
|
|
177
|
+
async remove(id: string | number): Promise<T> {
|
|
178
|
+
// Validate existence first (throws 404 if not found)
|
|
179
|
+
await this.findOne(id);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
this.logger.debug(`Soft-deleting record: id=${id}`);
|
|
183
|
+
const deleted = await this.getRepository().remove(id);
|
|
184
|
+
this.logger.log(`Soft-deleted record: id=${id}`);
|
|
185
|
+
return deleted;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error instanceof NotFoundException) throw error;
|
|
188
|
+
this.logger.error(`Failed to remove record id=${id}: ${(error as Error).message}`, (error as Error).stack);
|
|
189
|
+
throw new BadRequestException('Failed to delete resource.');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base CRUD Architecture
|
|
3
|
+
* Barrel export for src/common/base
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* import { BaseService, BaseController, PaginationQueryDto } from '../common/base';
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { BaseService } from './base.service';
|
|
10
|
+
export type { IBaseRepository } from './base.service';
|
|
11
|
+
export { BaseController } from './base.controller';
|
|
12
|
+
|
|
13
|
+
// Swagger DTOs
|
|
14
|
+
export { ApiResponseDto, ApiMetaDto, ApiResponseSchema, ApiResponseArraySchema } from './swagger/api-response.dto';
|
|
15
|
+
export {
|
|
16
|
+
PaginationQueryDto,
|
|
17
|
+
PaginationMetaDto,
|
|
18
|
+
PaginatedResponseDto,
|
|
19
|
+
PaginatedResponseSchema,
|
|
20
|
+
} from './swagger/paginated.dto';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
// Meta Information DTO
|
|
5
|
+
// ─────────────────────────────────────────────────────────────
|
|
6
|
+
export class ApiMetaDto {
|
|
7
|
+
@ApiProperty({ description: 'Request correlation ID', example: 'abc-123' })
|
|
8
|
+
correlationId: string;
|
|
9
|
+
|
|
10
|
+
@ApiProperty({ description: 'Response timestamp (ISO 8601)', example: '2025-01-01T00:00:00.000Z' })
|
|
11
|
+
timestamp: string;
|
|
12
|
+
|
|
13
|
+
@ApiPropertyOptional({ description: 'Optional message about the operation', example: 'Product created successfully' })
|
|
14
|
+
message?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ─────────────────────────────────────────────────────────────
|
|
18
|
+
// Generic API Response DTO Wrapper
|
|
19
|
+
// Used with @ApiExtraModels + $ref for proper Swagger generic rendering
|
|
20
|
+
// ─────────────────────────────────────────────────────────────
|
|
21
|
+
export class ApiResponseDto<T = unknown> {
|
|
22
|
+
@ApiProperty({ description: 'Indicates if the request was successful', example: true })
|
|
23
|
+
success: boolean;
|
|
24
|
+
|
|
25
|
+
@ApiPropertyOptional({ description: 'Response payload (varies by endpoint)' })
|
|
26
|
+
data?: T;
|
|
27
|
+
|
|
28
|
+
@ApiPropertyOptional({ description: 'Error message (only present on failure)', example: 'Resource not found' })
|
|
29
|
+
message?: string;
|
|
30
|
+
|
|
31
|
+
@ApiProperty({ type: () => ApiMetaDto })
|
|
32
|
+
meta: ApiMetaDto;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────────────────────────
|
|
36
|
+
// Factory Helper for Swagger Schema Injection
|
|
37
|
+
// Usage: @ApiOkResponse(ApiResponseSchema(ProductDto))
|
|
38
|
+
// ─────────────────────────────────────────────────────────────
|
|
39
|
+
import { getSchemaPath } from '@nestjs/swagger';
|
|
40
|
+
import { Type } from '@nestjs/common';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generates an inline Swagger schema for ApiResponseDto<T>.
|
|
44
|
+
* Use this with @ApiOkResponse({ schema: ApiResponseSchema(YourDto) }).
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* \@ApiOkResponse({ schema: ApiResponseSchema(ProductDto) })
|
|
48
|
+
*/
|
|
49
|
+
export function ApiResponseSchema<T>(dataType: Type<T>) {
|
|
50
|
+
return {
|
|
51
|
+
allOf: [
|
|
52
|
+
{ $ref: getSchemaPath(ApiResponseDto) },
|
|
53
|
+
{
|
|
54
|
+
properties: {
|
|
55
|
+
data: { $ref: getSchemaPath(dataType) },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generates an inline Swagger schema for ApiResponseDto<T[]> (array response).
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* \@ApiOkResponse({ schema: ApiResponseArraySchema(ProductDto) })
|
|
67
|
+
*/
|
|
68
|
+
export function ApiResponseArraySchema<T>(dataType: Type<T>) {
|
|
69
|
+
return {
|
|
70
|
+
allOf: [
|
|
71
|
+
{ $ref: getSchemaPath(ApiResponseDto) },
|
|
72
|
+
{
|
|
73
|
+
properties: {
|
|
74
|
+
data: {
|
|
75
|
+
type: 'array',
|
|
76
|
+
items: { $ref: getSchemaPath(dataType) },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
import { IsOptional, IsInt, Min, Max } from 'class-validator';
|
|
3
|
+
import { Type } from 'class-transformer';
|
|
4
|
+
import { getSchemaPath } from '@nestjs/swagger';
|
|
5
|
+
import { Type as NestType } from '@nestjs/common';
|
|
6
|
+
|
|
7
|
+
// ─────────────────────────────────────────────────────────────
|
|
8
|
+
// Pagination Query DTO (Request)
|
|
9
|
+
// Validates & transforms query params: ?page=1&limit=10
|
|
10
|
+
// ─────────────────────────────────────────────────────────────
|
|
11
|
+
export class PaginationQueryDto {
|
|
12
|
+
@ApiPropertyOptional({
|
|
13
|
+
description: 'Page number (1-indexed)',
|
|
14
|
+
example: 1,
|
|
15
|
+
minimum: 1,
|
|
16
|
+
default: 1,
|
|
17
|
+
})
|
|
18
|
+
@IsOptional()
|
|
19
|
+
@Type(() => Number)
|
|
20
|
+
@IsInt({ message: 'page must be an integer' })
|
|
21
|
+
@Min(1, { message: 'page must be at least 1' })
|
|
22
|
+
page?: number = 1;
|
|
23
|
+
|
|
24
|
+
@ApiPropertyOptional({
|
|
25
|
+
description: 'Number of items per page',
|
|
26
|
+
example: 10,
|
|
27
|
+
minimum: 1,
|
|
28
|
+
maximum: 100,
|
|
29
|
+
default: 10,
|
|
30
|
+
})
|
|
31
|
+
@IsOptional()
|
|
32
|
+
@Type(() => Number)
|
|
33
|
+
@IsInt({ message: 'limit must be an integer' })
|
|
34
|
+
@Min(1, { message: 'limit must be at least 1' })
|
|
35
|
+
@Max(100, { message: 'limit must not exceed 100' })
|
|
36
|
+
limit?: number = 10;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─────────────────────────────────────────────────────────────
|
|
40
|
+
// Pagination Meta DTO (Response)
|
|
41
|
+
// ─────────────────────────────────────────────────────────────
|
|
42
|
+
export class PaginationMetaDto {
|
|
43
|
+
@ApiProperty({ description: 'Total number of records', example: 250 })
|
|
44
|
+
total: number;
|
|
45
|
+
|
|
46
|
+
@ApiProperty({ description: 'Current page number', example: 1 })
|
|
47
|
+
page: number;
|
|
48
|
+
|
|
49
|
+
@ApiProperty({ description: 'Number of items per page', example: 10 })
|
|
50
|
+
limit: number;
|
|
51
|
+
|
|
52
|
+
@ApiProperty({ description: 'Total number of pages', example: 25 })
|
|
53
|
+
totalPages: number;
|
|
54
|
+
|
|
55
|
+
@ApiProperty({ description: 'Whether a next page exists', example: true })
|
|
56
|
+
hasNext: boolean;
|
|
57
|
+
|
|
58
|
+
@ApiProperty({ description: 'Whether a previous page exists', example: false })
|
|
59
|
+
hasPrevious: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─────────────────────────────────────────────────────────────
|
|
63
|
+
// Generic Paginated Response DTO Wrapper
|
|
64
|
+
// ─────────────────────────────────────────────────────────────
|
|
65
|
+
export class PaginatedResponseDto<T = unknown> {
|
|
66
|
+
@ApiPropertyOptional({ description: 'Array of paginated items (type varies by endpoint)' })
|
|
67
|
+
data: T[];
|
|
68
|
+
|
|
69
|
+
@ApiProperty({ type: () => PaginationMetaDto })
|
|
70
|
+
meta: PaginationMetaDto;
|
|
71
|
+
|
|
72
|
+
@ApiProperty({ description: 'Request was successful', example: true })
|
|
73
|
+
success: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─────────────────────────────────────────────────────────────
|
|
77
|
+
// Factory Helper for Swagger Schema Injection
|
|
78
|
+
// Usage: @ApiOkResponse({ schema: PaginatedResponseSchema(ProductDto) })
|
|
79
|
+
// ─────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Generates an inline Swagger schema for PaginatedResponseDto<T>.
|
|
83
|
+
* Attach with @ApiOkResponse({ schema: PaginatedResponseSchema(YourDto) }).
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* \@ApiOkResponse({ schema: PaginatedResponseSchema(ProductDto) })
|
|
87
|
+
*/
|
|
88
|
+
export function PaginatedResponseSchema<T>(dataType: NestType<T>) {
|
|
89
|
+
return {
|
|
90
|
+
allOf: [
|
|
91
|
+
{ $ref: getSchemaPath(PaginatedResponseDto) },
|
|
92
|
+
{
|
|
93
|
+
properties: {
|
|
94
|
+
data: {
|
|
95
|
+
type: 'array',
|
|
96
|
+
items: { $ref: getSchemaPath(dataType) },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
import {
|
|
3
|
+
IsString, IsNotEmpty, IsNumber, IsPositive, IsOptional,
|
|
4
|
+
IsEnum, MinLength, MaxLength, Min, IsInt, Matches,
|
|
5
|
+
} from 'class-validator';
|
|
6
|
+
import { Type } from 'class-transformer';
|
|
7
|
+
|
|
8
|
+
export enum ProductStatus {
|
|
9
|
+
ACTIVE = 'ACTIVE',
|
|
10
|
+
INACTIVE = 'INACTIVE',
|
|
11
|
+
OUT_OF_STOCK = 'OUT_OF_STOCK',
|
|
12
|
+
DISCONTINUED = 'DISCONTINUED',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class CreateProductDto {
|
|
16
|
+
@ApiProperty({ description: 'Product name', example: 'Wireless Mechanical Keyboard', minLength: 3, maxLength: 150 })
|
|
17
|
+
@IsString() @IsNotEmpty() @MinLength(3) @MaxLength(150)
|
|
18
|
+
name: string;
|
|
19
|
+
|
|
20
|
+
@ApiPropertyOptional({ description: 'Product description', example: 'Compact TKL layout with RGB', maxLength: 1000 })
|
|
21
|
+
@IsOptional() @IsString() @MaxLength(1000)
|
|
22
|
+
description?: string;
|
|
23
|
+
|
|
24
|
+
@ApiProperty({ description: 'Unique SKU (uppercase, numbers, hyphens)', example: 'KB-WL-MEC-001', pattern: '^[A-Z0-9-]+$' })
|
|
25
|
+
@IsString() @IsNotEmpty()
|
|
26
|
+
@Matches(/^[A-Z0-9-]+$/, { message: 'sku must contain only uppercase letters, numbers, and hyphens' })
|
|
27
|
+
sku: string;
|
|
28
|
+
|
|
29
|
+
@ApiProperty({ description: 'Price in smallest currency unit (cents)', example: 149999, minimum: 0 })
|
|
30
|
+
@Type(() => Number) @IsNumber() @IsPositive()
|
|
31
|
+
price: number;
|
|
32
|
+
|
|
33
|
+
@ApiProperty({ description: 'Available stock quantity', example: 250, minimum: 0 })
|
|
34
|
+
@Type(() => Number) @IsInt() @Min(0)
|
|
35
|
+
stock: number;
|
|
36
|
+
|
|
37
|
+
@ApiPropertyOptional({ description: 'Product category', example: 'Peripherals', maxLength: 100 })
|
|
38
|
+
@IsOptional() @IsString() @MaxLength(100)
|
|
39
|
+
category?: string;
|
|
40
|
+
|
|
41
|
+
@ApiPropertyOptional({ enum: ProductStatus, default: ProductStatus.ACTIVE, example: ProductStatus.ACTIVE })
|
|
42
|
+
@IsOptional() @IsEnum(ProductStatus)
|
|
43
|
+
status?: ProductStatus = ProductStatus.ACTIVE;
|
|
44
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
import { ProductStatus } from './create-product.dto';
|
|
3
|
+
|
|
4
|
+
/** Product response DTO — shape returned from the API, used by Swagger @ApiExtraModels */
|
|
5
|
+
export class ProductDto {
|
|
6
|
+
@ApiProperty({ description: 'Product UUID v4', example: '123e4567-e89b-12d3-a456-426614174000', format: 'uuid' })
|
|
7
|
+
id: string;
|
|
8
|
+
|
|
9
|
+
@ApiProperty({ example: 'Wireless Mechanical Keyboard' })
|
|
10
|
+
name: string;
|
|
11
|
+
|
|
12
|
+
@ApiPropertyOptional({ example: 'Compact TKL layout with RGB' })
|
|
13
|
+
description?: string;
|
|
14
|
+
|
|
15
|
+
@ApiProperty({ example: 'KB-WL-MEC-001' })
|
|
16
|
+
sku: string;
|
|
17
|
+
|
|
18
|
+
@ApiProperty({ description: 'Price in cents', example: 149999 })
|
|
19
|
+
price: number;
|
|
20
|
+
|
|
21
|
+
@ApiProperty({ example: 250 })
|
|
22
|
+
stock: number;
|
|
23
|
+
|
|
24
|
+
@ApiPropertyOptional({ example: 'Peripherals' })
|
|
25
|
+
category?: string;
|
|
26
|
+
|
|
27
|
+
@ApiProperty({ enum: ProductStatus, example: ProductStatus.ACTIVE })
|
|
28
|
+
status: ProductStatus;
|
|
29
|
+
|
|
30
|
+
@ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
31
|
+
createdAt: Date;
|
|
32
|
+
|
|
33
|
+
@ApiProperty({ example: '2025-01-15T08:30:00.000Z' })
|
|
34
|
+
updatedAt: Date;
|
|
35
|
+
|
|
36
|
+
@ApiPropertyOptional({ nullable: true, example: null })
|
|
37
|
+
deletedAt?: Date | null;
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PartialType } from '@nestjs/swagger';
|
|
2
|
+
import { CreateProductDto } from './create-product.dto';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Update Product DTO
|
|
6
|
+
*
|
|
7
|
+
* Uses PartialType from @nestjs/swagger (NOT @nestjs/mapped-types) to:
|
|
8
|
+
* 1. Make all fields optional (PATCH semantics)
|
|
9
|
+
* 2. Preserve @ApiProperty decorators for Swagger schema rendering
|
|
10
|
+
* 3. Keep all class-validator rules active on provided fields
|
|
11
|
+
*/
|
|
12
|
+
export class UpdateProductDto extends PartialType(CreateProductDto) {}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Controller,
|
|
3
|
+
Get,
|
|
4
|
+
Param,
|
|
5
|
+
Query,
|
|
6
|
+
ParseUUIDPipe,
|
|
7
|
+
HttpStatus,
|
|
8
|
+
Type,
|
|
9
|
+
} from '@nestjs/common';
|
|
10
|
+
import {
|
|
11
|
+
ApiTags,
|
|
12
|
+
ApiOperation,
|
|
13
|
+
ApiResponse,
|
|
14
|
+
ApiBearerAuth,
|
|
15
|
+
ApiExtraModels,
|
|
16
|
+
ApiParam,
|
|
17
|
+
} from '@nestjs/swagger';
|
|
18
|
+
import { BaseController } from '../../common/base/base.controller';
|
|
19
|
+
import {
|
|
20
|
+
ApiResponseDto,
|
|
21
|
+
ApiResponseSchema,
|
|
22
|
+
PaginatedResponseDto,
|
|
23
|
+
PaginatedResponseSchema,
|
|
24
|
+
PaginationQueryDto,
|
|
25
|
+
} from '../../common/base';
|
|
26
|
+
import { ProductsService, ProductEntity } from './products.service';
|
|
27
|
+
import { CreateProductDto } from './dto/create-product.dto';
|
|
28
|
+
import { UpdateProductDto } from './dto/update-product.dto';
|
|
29
|
+
import { ProductDto } from './dto/product.dto';
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────────────
|
|
32
|
+
// Products Controller
|
|
33
|
+
//
|
|
34
|
+
// Extends BaseController which provides (via ORM-specific service):
|
|
35
|
+
// POST /products → create()
|
|
36
|
+
// GET /products → findAll()
|
|
37
|
+
// GET /products/:id → findOne()
|
|
38
|
+
// PUT /products/:id → update()
|
|
39
|
+
// DELETE /products/:id → remove()
|
|
40
|
+
//
|
|
41
|
+
// @ApiExtraModels registers DTOs so Swagger renders
|
|
42
|
+
// generic ApiResponseDto<ProductDto> and PaginatedResponseDto<ProductDto>.
|
|
43
|
+
// ─────────────────────────────────────────────────────────────
|
|
44
|
+
@ApiTags('Products')
|
|
45
|
+
@ApiBearerAuth('bearer')
|
|
46
|
+
@ApiExtraModels(ApiResponseDto, PaginatedResponseDto, ProductDto)
|
|
47
|
+
@Controller('products')
|
|
48
|
+
export class ProductsController extends BaseController<
|
|
49
|
+
ProductEntity,
|
|
50
|
+
CreateProductDto,
|
|
51
|
+
UpdateProductDto
|
|
52
|
+
> {
|
|
53
|
+
constructor(private readonly productsService: ProductsService) {
|
|
54
|
+
super(productsService);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
protected getDtoClass(): Type<ProductEntity> {
|
|
58
|
+
return ProductDto as unknown as Type<ProductEntity>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Overrides to inject Swagger response schemas ────────────
|
|
62
|
+
|
|
63
|
+
@Get()
|
|
64
|
+
@ApiOperation({ summary: 'Get all products (paginated)' })
|
|
65
|
+
@ApiResponse({ status: HttpStatus.OK, description: 'Paginated products', schema: PaginatedResponseSchema(ProductDto) })
|
|
66
|
+
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized' })
|
|
67
|
+
override async findAll(@Query() pagination: PaginationQueryDto): Promise<PaginatedResponseDto<ProductEntity>> {
|
|
68
|
+
return super.findAll(pagination);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Get('by-sku/:sku')
|
|
72
|
+
@ApiOperation({ summary: 'Get a product by SKU' })
|
|
73
|
+
@ApiParam({ name: 'sku', example: 'KB-WL-MEC-001', description: 'Unique Stock Keeping Unit' })
|
|
74
|
+
@ApiResponse({ status: HttpStatus.OK, description: 'Product found by SKU', schema: ApiResponseSchema(ProductDto) })
|
|
75
|
+
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Product not found' })
|
|
76
|
+
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized' })
|
|
77
|
+
async findBySku(@Param('sku') sku: string): Promise<ApiResponseDto<ProductEntity>> {
|
|
78
|
+
const data = await this.productsService.findBySku(sku);
|
|
79
|
+
return { success: true, data, meta: { correlationId: '', timestamp: new Date().toISOString() } };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Get(':id')
|
|
83
|
+
@ApiOperation({ summary: 'Get a product by UUID' })
|
|
84
|
+
@ApiParam({ name: 'id', format: 'uuid', example: '123e4567-e89b-12d3-a456-426614174000' })
|
|
85
|
+
@ApiResponse({ status: HttpStatus.OK, description: 'Product found', schema: ApiResponseSchema(ProductDto) })
|
|
86
|
+
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Product not found' })
|
|
87
|
+
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid UUID format' })
|
|
88
|
+
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized' })
|
|
89
|
+
override async findOne(
|
|
90
|
+
@Param('id', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST })) id: string,
|
|
91
|
+
): Promise<ApiResponseDto<ProductEntity>> {
|
|
92
|
+
return super.findOne(id);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Controller,
|
|
3
|
+
Get,
|
|
4
|
+
Param,
|
|
5
|
+
Query,
|
|
6
|
+
ParseUUIDPipe,
|
|
7
|
+
HttpStatus,
|
|
8
|
+
Type,
|
|
9
|
+
} from '@nestjs/common';
|
|
10
|
+
import {
|
|
11
|
+
ApiTags,
|
|
12
|
+
ApiOperation,
|
|
13
|
+
ApiResponse,
|
|
14
|
+
ApiBearerAuth,
|
|
15
|
+
ApiExtraModels,
|
|
16
|
+
ApiParam,
|
|
17
|
+
} from '@nestjs/swagger';
|
|
18
|
+
import { BaseController } from '../../common/base/base.controller';
|
|
19
|
+
import {
|
|
20
|
+
ApiResponseDto,
|
|
21
|
+
ApiResponseSchema,
|
|
22
|
+
PaginatedResponseDto,
|
|
23
|
+
PaginatedResponseSchema,
|
|
24
|
+
PaginationQueryDto,
|
|
25
|
+
} from '../../common/base';
|
|
26
|
+
import { ProductsService, ProductEntity } from './products.service';
|
|
27
|
+
import { CreateProductDto } from './dto/create-product.dto';
|
|
28
|
+
import { UpdateProductDto } from './dto/update-product.dto';
|
|
29
|
+
import { ProductDto } from './dto/product.dto';
|
|
30
|
+
|
|
31
|
+
@ApiTags('Products')
|
|
32
|
+
@ApiBearerAuth('bearer')
|
|
33
|
+
@ApiExtraModels(ApiResponseDto, PaginatedResponseDto, ProductDto)
|
|
34
|
+
@Controller('products')
|
|
35
|
+
export class ProductsController extends BaseController<
|
|
36
|
+
ProductEntity,
|
|
37
|
+
CreateProductDto,
|
|
38
|
+
UpdateProductDto
|
|
39
|
+
> {
|
|
40
|
+
constructor(private readonly productsService: ProductsService) {
|
|
41
|
+
super(productsService);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
protected getDtoClass(): Type<ProductEntity> {
|
|
45
|
+
return ProductDto as unknown as Type<ProductEntity>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Get()
|
|
49
|
+
@ApiOperation({ summary: 'Get all products (paginated)' })
|
|
50
|
+
@ApiResponse({ status: HttpStatus.OK, schema: PaginatedResponseSchema(ProductDto) })
|
|
51
|
+
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized' })
|
|
52
|
+
override async findAll(@Query() pagination: PaginationQueryDto): Promise<PaginatedResponseDto<ProductEntity>> {
|
|
53
|
+
return super.findAll(pagination);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Get('by-sku/:sku')
|
|
57
|
+
@ApiOperation({ summary: 'Get a product by SKU' })
|
|
58
|
+
@ApiParam({ name: 'sku', example: 'KB-WL-MEC-001' })
|
|
59
|
+
@ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(ProductDto) })
|
|
60
|
+
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Product not found' })
|
|
61
|
+
async findBySku(@Param('sku') sku: string): Promise<ApiResponseDto<ProductEntity>> {
|
|
62
|
+
const data = await this.productsService.findBySku(sku);
|
|
63
|
+
return { success: true, data, meta: { correlationId: '', timestamp: new Date().toISOString() } };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Get(':id')
|
|
67
|
+
@ApiOperation({ summary: 'Get a product by ID' })
|
|
68
|
+
@ApiParam({ name: 'id', format: 'uuid', example: '123e4567-e89b-12d3-a456-426614174000' })
|
|
69
|
+
@ApiResponse({ status: HttpStatus.OK, schema: ApiResponseSchema(ProductDto) })
|
|
70
|
+
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Product not found' })
|
|
71
|
+
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Invalid UUID' })
|
|
72
|
+
override async findOne(
|
|
73
|
+
@Param('id', new ParseUUIDPipe({ version: '4', errorHttpStatusCode: HttpStatus.BAD_REQUEST }))
|
|
74
|
+
id: string,
|
|
75
|
+
): Promise<ApiResponseDto<ProductEntity>> {
|
|
76
|
+
return super.findOne(id);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|