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.
Files changed (159) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +21 -0
  3. package/README.md +620 -0
  4. package/bin/cli.js +224 -0
  5. package/index.js +12 -0
  6. package/package.json +74 -0
  7. package/src/constants.js +65 -0
  8. package/src/generator.js +271 -0
  9. package/src/index.js +13 -0
  10. package/src/moduleGenerator.js +586 -0
  11. package/src/postSetup.js +365 -0
  12. package/src/prompts.js +189 -0
  13. package/src/utils.js +112 -0
  14. package/templates/README.md +81 -0
  15. package/templates/base/eslint.config.mjs +34 -0
  16. package/templates/base/gitignore +78 -0
  17. package/templates/base/nest-cli.json +8 -0
  18. package/templates/base/src/common/constants/cookie.config.ts +20 -0
  19. package/templates/base/src/common/decorators/get-user.decorator.ts +13 -0
  20. package/templates/base/src/common/decorators/public.decorator.ts +4 -0
  21. package/templates/base/src/common/decorators/roles.decorator.ts +4 -0
  22. package/templates/base/src/common/dtos/pagination.dto.ts +29 -0
  23. package/templates/base/src/common/filters/http-exception.filter.ts +108 -0
  24. package/templates/base/src/common/guards/auth.guard.ts +51 -0
  25. package/templates/base/src/common/guards/refresh-token.guard.ts +39 -0
  26. package/templates/base/src/common/guards/roles.guard.ts +45 -0
  27. package/templates/base/src/common/interceptors/response.interceptor.ts +55 -0
  28. package/templates/base/src/common/interfaces/api-response.interface.ts +26 -0
  29. package/templates/base/src/common/middleware/correlation-id.middleware.ts +20 -0
  30. package/templates/base/src/common/validators/password.validator.ts +37 -0
  31. package/templates/base/src/config/config.module.ts +14 -0
  32. package/templates/base/src/config/logger.config.ts +109 -0
  33. package/templates/base/src/main.ts +84 -0
  34. package/templates/base/src/modules/auth/auth.controller.ts +133 -0
  35. package/templates/base/src/modules/auth/dtos/login.dto.ts +11 -0
  36. package/templates/base/src/modules/auth/dtos/signup.dto.ts +8 -0
  37. package/templates/base/src/modules/health/health.controller.ts +20 -0
  38. package/templates/base/src/modules/health/health.module.ts +7 -0
  39. package/templates/base/src/modules/users/dtos/update-profile.dto.ts +12 -0
  40. package/templates/base/src/modules/users/dtos/update-user.dto.ts +13 -0
  41. package/templates/base/src/modules/users/users.controller.ts +65 -0
  42. package/templates/base/test/app.e2e-spec.ts +24 -0
  43. package/templates/base/test/jest-e2e.json +9 -0
  44. package/templates/base/tsconfig.build.json +4 -0
  45. package/templates/base/tsconfig.json +24 -0
  46. package/templates/base-crud/CRUD_README.md +385 -0
  47. package/templates/base-crud/src/common/base/base.controller.ts +321 -0
  48. package/templates/base-crud/src/common/base/base.service.ts +192 -0
  49. package/templates/base-crud/src/common/base/index.ts +20 -0
  50. package/templates/base-crud/src/common/base/swagger/api-response.dto.ts +82 -0
  51. package/templates/base-crud/src/common/base/swagger/paginated.dto.ts +102 -0
  52. package/templates/base-crud/src/modules/products/dto/create-product.dto.ts +44 -0
  53. package/templates/base-crud/src/modules/products/dto/product.dto.ts +38 -0
  54. package/templates/base-crud/src/modules/products/dto/update-product.dto.ts +12 -0
  55. package/templates/base-crud/src/modules/products/products.controller.ts +94 -0
  56. package/templates/base-crud-drizzle/src/modules/products/products.controller.ts +79 -0
  57. package/templates/base-crud-drizzle/src/modules/products/products.module.ts +24 -0
  58. package/templates/base-crud-drizzle/src/modules/products/products.service.ts +140 -0
  59. package/templates/base-crud-drizzle/src/modules/products/schema/products.schema.ts +39 -0
  60. package/templates/base-crud-mongoose/src/modules/products/products.controller.ts +79 -0
  61. package/templates/base-crud-mongoose/src/modules/products/products.module.ts +26 -0
  62. package/templates/base-crud-mongoose/src/modules/products/products.service.ts +133 -0
  63. package/templates/base-crud-mongoose/src/modules/products/schemas/product.schema.ts +67 -0
  64. package/templates/base-crud-prisma/src/modules/products/products.module.ts +12 -0
  65. package/templates/base-crud-prisma/src/modules/products/products.service.ts +100 -0
  66. package/templates/base-crud-typeorm/src/modules/products/entities/product.entity.ts +58 -0
  67. package/templates/base-crud-typeorm/src/modules/products/products.controller.ts +79 -0
  68. package/templates/base-crud-typeorm/src/modules/products/products.module.ts +25 -0
  69. package/templates/base-crud-typeorm/src/modules/products/products.service.ts +102 -0
  70. package/templates/database/mongodb/.env.example +22 -0
  71. package/templates/database/mysql/.env.example +24 -0
  72. package/templates/database/mysql/drizzle.config.ts +13 -0
  73. package/templates/database/mysql/package.json +5 -0
  74. package/templates/database/mysql/prisma/schema.prisma +55 -0
  75. package/templates/database/mysql/src/database/drizzle.ts +13 -0
  76. package/templates/database/mysql/src/database/schema.ts +58 -0
  77. package/templates/database/postgres/.env.example +24 -0
  78. package/templates/database/postgres/drizzle.config.ts +13 -0
  79. package/templates/database/postgres/package.json +8 -0
  80. package/templates/database/postgres/prisma/schema.prisma +55 -0
  81. package/templates/database/sqlite/.env.example +24 -0
  82. package/templates/database/sqlite/drizzle.config.ts +13 -0
  83. package/templates/database/sqlite/package.json +8 -0
  84. package/templates/database/sqlite/prisma/schema.prisma +48 -0
  85. package/templates/database/sqlite/src/database/drizzle.ts +11 -0
  86. package/templates/database/sqlite/src/database/schema.ts +52 -0
  87. package/templates/orm/drizzle/drizzle.config.ts +13 -0
  88. package/templates/orm/drizzle/package.json +90 -0
  89. package/templates/orm/drizzle/src/app.module.ts +73 -0
  90. package/templates/orm/drizzle/src/config/env.validation.ts +55 -0
  91. package/templates/orm/drizzle/src/database/database.module.ts +25 -0
  92. package/templates/orm/drizzle/src/database/drizzle.ts +13 -0
  93. package/templates/orm/drizzle/src/database/schema.ts +60 -0
  94. package/templates/orm/drizzle/src/database/seed.ts +87 -0
  95. package/templates/orm/drizzle/src/modules/auth/auth.module.ts +12 -0
  96. package/templates/orm/drizzle/src/modules/auth/auth.service.ts +298 -0
  97. package/templates/orm/drizzle/src/modules/health/health.controller.ts +34 -0
  98. package/templates/orm/drizzle/src/modules/health/health.module.ts +7 -0
  99. package/templates/orm/drizzle/src/modules/users/users.module.ts +10 -0
  100. package/templates/orm/drizzle/src/modules/users/users.service.ts +152 -0
  101. package/templates/orm/mongoose/.env.example +22 -0
  102. package/templates/orm/mongoose/package.json +86 -0
  103. package/templates/orm/mongoose/src/app.module.ts +73 -0
  104. package/templates/orm/mongoose/src/config/env.validation.ts +55 -0
  105. package/templates/orm/mongoose/src/database/database.module.ts +19 -0
  106. package/templates/orm/mongoose/src/database/seed.ts +107 -0
  107. package/templates/orm/mongoose/src/modules/auth/auth.module.ts +21 -0
  108. package/templates/orm/mongoose/src/modules/auth/auth.service.ts +272 -0
  109. package/templates/orm/mongoose/src/modules/auth/dtos/login.dto.ts +10 -0
  110. package/templates/orm/mongoose/src/modules/auth/dtos/signup.dto.ts +8 -0
  111. package/templates/orm/mongoose/src/modules/health/health.controller.ts +26 -0
  112. package/templates/orm/mongoose/src/modules/health/health.module.ts +7 -0
  113. package/templates/orm/mongoose/src/modules/users/dtos/update-profile.dto.ts +24 -0
  114. package/templates/orm/mongoose/src/modules/users/dtos/update-user.dto.ts +13 -0
  115. package/templates/orm/mongoose/src/modules/users/users.module.ts +19 -0
  116. package/templates/orm/mongoose/src/modules/users/users.service.ts +179 -0
  117. package/templates/orm/mongoose/src/schemas/index.ts +2 -0
  118. package/templates/orm/mongoose/src/schemas/refresh-token.schema.ts +55 -0
  119. package/templates/orm/mongoose/src/schemas/user.schema.ts +53 -0
  120. package/templates/orm/prisma/package.json +102 -0
  121. package/templates/orm/prisma/prisma/schema.prisma +60 -0
  122. package/templates/orm/prisma/prisma/seed.ts +69 -0
  123. package/templates/orm/prisma/src/app.module.ts +55 -0
  124. package/templates/orm/prisma/src/config/env.validation.ts +56 -0
  125. package/templates/orm/prisma/src/modules/auth/auth.module.ts +17 -0
  126. package/templates/orm/prisma/src/modules/auth/auth.service.ts +308 -0
  127. package/templates/orm/prisma/src/modules/health/health.controller.ts +26 -0
  128. package/templates/orm/prisma/src/modules/health/health.module.ts +10 -0
  129. package/templates/orm/prisma/src/modules/users/users.module.ts +11 -0
  130. package/templates/orm/prisma/src/modules/users/users.service.ts +105 -0
  131. package/templates/orm/prisma/src/prisma/prisma.module.ts +9 -0
  132. package/templates/orm/prisma/src/prisma/prisma.service.ts +16 -0
  133. package/templates/orm/typeorm/package.json +100 -0
  134. package/templates/orm/typeorm/src/app.module.ts +55 -0
  135. package/templates/orm/typeorm/src/config/env.validation.ts +58 -0
  136. package/templates/orm/typeorm/src/database/data-source.ts +19 -0
  137. package/templates/orm/typeorm/src/database/database.module.ts +27 -0
  138. package/templates/orm/typeorm/src/database/seed.ts +76 -0
  139. package/templates/orm/typeorm/src/entities/index.ts +2 -0
  140. package/templates/orm/typeorm/src/entities/refresh-token.entity.ts +45 -0
  141. package/templates/orm/typeorm/src/entities/user.entity.ts +51 -0
  142. package/templates/orm/typeorm/src/modules/auth/auth.module.ts +19 -0
  143. package/templates/orm/typeorm/src/modules/auth/auth.service.ts +286 -0
  144. package/templates/orm/typeorm/src/modules/health/health.controller.ts +24 -0
  145. package/templates/orm/typeorm/src/modules/health/health.module.ts +9 -0
  146. package/templates/orm/typeorm/src/modules/users/users.module.ts +13 -0
  147. package/templates/orm/typeorm/src/modules/users/users.service.ts +108 -0
  148. package/templates/swagger/src/common/dtos/pagination.dto.ts +43 -0
  149. package/templates/swagger/src/main.ts +116 -0
  150. package/templates/swagger/src/modules/auth/auth.controller.ts +235 -0
  151. package/templates/swagger/src/modules/auth/dtos/login.dto.ts +20 -0
  152. package/templates/swagger/src/modules/auth/dtos/signup.dto.ts +14 -0
  153. package/templates/swagger/src/modules/users/dtos/update-profile.dto.ts +19 -0
  154. package/templates/swagger/src/modules/users/dtos/update-user.dto.ts +23 -0
  155. package/templates/swagger/src/modules/users/users.controller.ts +214 -0
  156. package/templates/swagger-mongoose/src/modules/auth/dtos/login.dto.ts +20 -0
  157. package/templates/swagger-mongoose/src/modules/auth/dtos/signup.dto.ts +14 -0
  158. package/templates/swagger-mongoose/src/modules/users/dtos/update-profile.dto.ts +40 -0
  159. package/templates/swagger-mongoose/src/modules/users/dtos/update-user.dto.ts +23 -0
@@ -0,0 +1,24 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { ProductsController } from './products.controller';
3
+ import { ProductsService } from './products.service';
4
+ import { DatabaseModule } from '../../database/database.module';
5
+
6
+ /**
7
+ * Products Module (Drizzle)
8
+ *
9
+ * DatabaseModule is @Global() so DRIZZLE token is available app-wide,
10
+ * but we import it here explicitly for documentation clarity.
11
+ *
12
+ * @example
13
+ * // In app.module.ts:
14
+ * import { ProductsModule } from './modules/products/products.module';
15
+ * @Module({ imports: [ProductsModule] })
16
+ * export class AppModule {}
17
+ */
18
+ @Module({
19
+ imports: [DatabaseModule],
20
+ controllers: [ProductsController],
21
+ providers: [ProductsService],
22
+ exports: [ProductsService],
23
+ })
24
+ export class ProductsModule {}
@@ -0,0 +1,140 @@
1
+ import { Injectable, Inject, NotFoundException } from '@nestjs/common';
2
+ import { eq, isNull, and, sql } from 'drizzle-orm';
3
+ import { NodePgDatabase } from 'drizzle-orm/node-postgres';
4
+ import { DRIZZLE } from '../../database/database.module';
5
+ import { BaseService, IBaseRepository, PaginationQueryDto } from '../../common/base';
6
+ import { CreateProductDto } from './dto/create-product.dto';
7
+ import { UpdateProductDto } from './dto/update-product.dto';
8
+ import { products, Product } from './schema/products.schema';
9
+
10
+ /** Uniform entity alias used by the shared ProductsController */
11
+ export type ProductEntity = Product;
12
+
13
+ // ─────────────────────────────────────────────────────────────
14
+ // Drizzle Product Repository
15
+ // ─────────────────────────────────────────────────────────────
16
+ class DrizzleProductRepository
17
+ implements IBaseRepository<Product, CreateProductDto, UpdateProductDto>
18
+ {
19
+ constructor(private readonly db: NodePgDatabase) {}
20
+
21
+ async create(dto: CreateProductDto): Promise<Product> {
22
+ const [product] = await this.db
23
+ .insert(products)
24
+ .values({
25
+ name: dto.name,
26
+ description: dto.description,
27
+ sku: dto.sku,
28
+ price: String(dto.price), // Drizzle numeric → string
29
+ stock: dto.stock,
30
+ category: dto.category,
31
+ status: dto.status as Product['status'],
32
+ })
33
+ .returning();
34
+ return product;
35
+ }
36
+
37
+ async findAll(
38
+ pagination: PaginationQueryDto,
39
+ ): Promise<{ data: Product[]; total: number }> {
40
+ const { page = 1, limit = 10 } = pagination;
41
+ const offset = (page - 1) * limit;
42
+
43
+ const [data, [{ count }]] = await Promise.all([
44
+ this.db
45
+ .select()
46
+ .from(products)
47
+ .where(isNull(products.deletedAt))
48
+ .orderBy(sql`${products.createdAt} DESC`)
49
+ .limit(limit)
50
+ .offset(offset),
51
+ this.db
52
+ .select({ count: sql<number>`count(*)::int` })
53
+ .from(products)
54
+ .where(isNull(products.deletedAt)),
55
+ ]);
56
+
57
+ return { data, total: count };
58
+ }
59
+
60
+ async findOne(id: string): Promise<Product | null> {
61
+ const [product] = await this.db
62
+ .select()
63
+ .from(products)
64
+ .where(and(eq(products.id, id), isNull(products.deletedAt)));
65
+ return product ?? null;
66
+ }
67
+
68
+ async update(id: string, dto: UpdateProductDto): Promise<Product> {
69
+ const updateData: Partial<typeof products.$inferInsert> = {};
70
+ if (dto.name !== undefined) updateData.name = dto.name;
71
+ if (dto.description !== undefined) updateData.description = dto.description;
72
+ if (dto.sku !== undefined) updateData.sku = dto.sku;
73
+ if (dto.price !== undefined) updateData.price = String(dto.price);
74
+ if (dto.stock !== undefined) updateData.stock = dto.stock;
75
+ if (dto.category !== undefined) updateData.category = dto.category;
76
+ if (dto.status !== undefined) updateData.status = dto.status as Product['status'];
77
+ updateData.updatedAt = new Date();
78
+
79
+ const [updated] = await this.db
80
+ .update(products)
81
+ .set(updateData)
82
+ .where(eq(products.id, id))
83
+ .returning();
84
+ return updated;
85
+ }
86
+
87
+ async remove(id: string): Promise<Product> {
88
+ const [deleted] = await this.db
89
+ .update(products)
90
+ .set({ deletedAt: new Date() })
91
+ .where(eq(products.id, id))
92
+ .returning();
93
+ return deleted;
94
+ }
95
+ }
96
+
97
+ // ─────────────────────────────────────────────────────────────
98
+ // Products Service (Drizzle)
99
+ // ─────────────────────────────────────────────────────────────
100
+ @Injectable()
101
+ export class ProductsService extends BaseService<
102
+ Product,
103
+ CreateProductDto,
104
+ UpdateProductDto
105
+ > {
106
+ private readonly repository: DrizzleProductRepository;
107
+
108
+ constructor(@Inject(DRIZZLE) private readonly db: NodePgDatabase) {
109
+ super();
110
+ this.repository = new DrizzleProductRepository(this.db);
111
+ }
112
+
113
+ protected getRepository(): IBaseRepository<Product, CreateProductDto, UpdateProductDto> {
114
+ return this.repository;
115
+ }
116
+
117
+ /** Find by SKU */
118
+ async findBySku(sku: string): Promise<Product> {
119
+ const [product] = await this.db
120
+ .select()
121
+ .from(products)
122
+ .where(and(eq(products.sku, sku), isNull(products.deletedAt)));
123
+
124
+ if (!product) {
125
+ throw new NotFoundException(`Product with SKU "${sku}" was not found`);
126
+ }
127
+ return product;
128
+ }
129
+
130
+ /** Adjust stock */
131
+ async adjustStock(id: string, delta: number): Promise<Product> {
132
+ await this.findOne(id);
133
+ const [updated] = await this.db
134
+ .update(products)
135
+ .set({ stock: sql`${products.stock} + ${delta}` })
136
+ .where(eq(products.id, id))
137
+ .returning();
138
+ return updated;
139
+ }
140
+ }
@@ -0,0 +1,39 @@
1
+ import {
2
+ pgTable,
3
+ varchar,
4
+ text,
5
+ timestamp,
6
+ integer,
7
+ numeric,
8
+ pgEnum,
9
+ uuid,
10
+ } from 'drizzle-orm/pg-core';
11
+
12
+ // ProductStatus enum
13
+ export const productStatusEnum = pgEnum('product_status', [
14
+ 'ACTIVE',
15
+ 'INACTIVE',
16
+ 'OUT_OF_STOCK',
17
+ 'DISCONTINUED',
18
+ ]);
19
+
20
+ // Products table
21
+ export const products = pgTable('products', {
22
+ id: uuid('id').defaultRandom().primaryKey(),
23
+ name: varchar('name', { length: 150 }).notNull(),
24
+ description: text('description'),
25
+ sku: varchar('sku', { length: 50 }).notNull().unique(),
26
+ price: numeric('price', { precision: 10, scale: 2 }).notNull(),
27
+ stock: integer('stock').default(0).notNull(),
28
+ category: varchar('category', { length: 100 }),
29
+ status: productStatusEnum('status').default('ACTIVE').notNull(),
30
+ createdAt: timestamp('created_at').defaultNow().notNull(),
31
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
32
+ deletedAt: timestamp('deleted_at'),
33
+ });
34
+
35
+ // Inferred types
36
+ export type Product = typeof products.$inferSelect;
37
+ export type NewProduct = typeof products.$inferInsert;
38
+
39
+ export type ProductStatus = 'ACTIVE' | 'INACTIVE' | 'OUT_OF_STOCK' | 'DISCONTINUED';
@@ -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
+
@@ -0,0 +1,26 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { MongooseModule } from '@nestjs/mongoose';
3
+ import { ProductsController } from './products.controller';
4
+ import { ProductsService } from './products.service';
5
+ import { Product, ProductSchema } from './schemas/product.schema';
6
+
7
+ /**
8
+ * Products Module (Mongoose)
9
+ *
10
+ * Registers Product schema with MongooseModule.forFeature.
11
+ *
12
+ * @example
13
+ * // In app.module.ts:
14
+ * import { ProductsModule } from './modules/products/products.module';
15
+ * @Module({ imports: [ProductsModule] })
16
+ * export class AppModule {}
17
+ */
18
+ @Module({
19
+ imports: [
20
+ MongooseModule.forFeature([{ name: Product.name, schema: ProductSchema }]),
21
+ ],
22
+ controllers: [ProductsController],
23
+ providers: [ProductsService],
24
+ exports: [ProductsService],
25
+ })
26
+ export class ProductsModule {}
@@ -0,0 +1,133 @@
1
+ import { Injectable, NotFoundException } from '@nestjs/common';
2
+ import { InjectModel } from '@nestjs/mongoose';
3
+ import { Model } from 'mongoose';
4
+ import { BaseService, IBaseRepository, PaginationQueryDto } from '../../common/base';
5
+ import { CreateProductDto } from './dto/create-product.dto';
6
+ import { UpdateProductDto } from './dto/update-product.dto';
7
+ import { Product, ProductDocument, ProductStatus } from './schemas/product.schema';
8
+
9
+ /** Lean plain-object type returned from Mongoose .lean() queries */
10
+ export type LeanProduct = {
11
+ id: string;
12
+ name: string;
13
+ description?: string;
14
+ sku: string;
15
+ price: number;
16
+ stock: number;
17
+ category?: string;
18
+ status: ProductStatus;
19
+ createdAt: Date;
20
+ updatedAt: Date;
21
+ deletedAt?: Date | null;
22
+ };
23
+
24
+ /** Uniform entity alias used by the shared ProductsController */
25
+ export type ProductEntity = LeanProduct;
26
+
27
+ // ─────────────────────────────────────────────────────────────
28
+ // Mongoose Product Repository
29
+ // ─────────────────────────────────────────────────────────────
30
+ class MongooseProductRepository
31
+ implements IBaseRepository<LeanProduct, CreateProductDto, UpdateProductDto>
32
+ {
33
+ constructor(private readonly model: Model<ProductDocument>) {}
34
+
35
+ async create(dto: CreateProductDto): Promise<LeanProduct> {
36
+ const created = await this.model.create(dto);
37
+ return created.toJSON() as LeanProduct;
38
+ }
39
+
40
+ async findAll(
41
+ pagination: PaginationQueryDto,
42
+ ): Promise<{ data: LeanProduct[]; total: number }> {
43
+ const { page = 1, limit = 10 } = pagination;
44
+ const skip = (page - 1) * limit;
45
+ const filter = { deletedAt: null };
46
+
47
+ const [data, total] = await Promise.all([
48
+ this.model
49
+ .find(filter)
50
+ .sort({ createdAt: -1 })
51
+ .skip(skip)
52
+ .limit(limit)
53
+ .lean<LeanProduct[]>({ virtuals: true }),
54
+ this.model.countDocuments(filter),
55
+ ]);
56
+
57
+ return { data, total };
58
+ }
59
+
60
+ async findOne(id: string): Promise<LeanProduct | null> {
61
+ return this.model
62
+ .findOne({ _id: id, deletedAt: null })
63
+ .lean<LeanProduct>({ virtuals: true });
64
+ }
65
+
66
+ async update(id: string, dto: UpdateProductDto): Promise<LeanProduct> {
67
+ return this.model
68
+ .findByIdAndUpdate(
69
+ id,
70
+ { $set: dto },
71
+ { new: true, runValidators: true },
72
+ )
73
+ .lean<LeanProduct>({ virtuals: true }) as Promise<LeanProduct>;
74
+ }
75
+
76
+ async remove(id: string): Promise<LeanProduct> {
77
+ return this.model
78
+ .findByIdAndUpdate(
79
+ id,
80
+ { $set: { deletedAt: new Date() } },
81
+ { new: true },
82
+ )
83
+ .lean<LeanProduct>({ virtuals: true }) as Promise<LeanProduct>;
84
+ }
85
+ }
86
+
87
+ // ─────────────────────────────────────────────────────────────
88
+ // Products Service (Mongoose)
89
+ // ─────────────────────────────────────────────────────────────
90
+ @Injectable()
91
+ export class ProductsService extends BaseService<
92
+ LeanProduct,
93
+ CreateProductDto,
94
+ UpdateProductDto
95
+ > {
96
+ private readonly repository: MongooseProductRepository;
97
+
98
+ constructor(
99
+ @InjectModel(Product.name)
100
+ private readonly productModel: Model<ProductDocument>,
101
+ ) {
102
+ super();
103
+ this.repository = new MongooseProductRepository(this.productModel);
104
+ }
105
+
106
+ protected getRepository(): IBaseRepository<LeanProduct, CreateProductDto, UpdateProductDto> {
107
+ return this.repository;
108
+ }
109
+
110
+ /** Find by SKU */
111
+ async findBySku(sku: string): Promise<LeanProduct> {
112
+ const product = await this.productModel
113
+ .findOne({ sku, deletedAt: null })
114
+ .lean<LeanProduct>({ virtuals: true });
115
+
116
+ if (!product) {
117
+ throw new NotFoundException(`Product with SKU "${sku}" was not found`);
118
+ }
119
+ return product;
120
+ }
121
+
122
+ /** Adjust stock */
123
+ async adjustStock(id: string, delta: number): Promise<LeanProduct> {
124
+ await this.findOne(id);
125
+ return this.productModel
126
+ .findByIdAndUpdate(
127
+ id,
128
+ { $inc: { stock: delta } },
129
+ { new: true },
130
+ )
131
+ .lean<LeanProduct>({ virtuals: true }) as Promise<LeanProduct>;
132
+ }
133
+ }
@@ -0,0 +1,67 @@
1
+ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
2
+ import { Document, HydratedDocument } from 'mongoose';
3
+
4
+ export type ProductDocument = HydratedDocument<Product>;
5
+
6
+ export enum ProductStatus {
7
+ ACTIVE = 'ACTIVE',
8
+ INACTIVE = 'INACTIVE',
9
+ OUT_OF_STOCK = 'OUT_OF_STOCK',
10
+ DISCONTINUED = 'DISCONTINUED',
11
+ }
12
+
13
+ @Schema({
14
+ timestamps: true,
15
+ collection: 'products',
16
+ toJSON: {
17
+ virtuals: true,
18
+ transform: (_, ret: Record<string, unknown>) => {
19
+ ret['id'] = ret['_id'];
20
+ delete ret['_id'];
21
+ delete ret['__v'];
22
+ return ret;
23
+ },
24
+ },
25
+ })
26
+ export class Product {
27
+ // Virtual id field (mapped from _id)
28
+ id: string;
29
+
30
+ @Prop({ required: true, maxlength: 150, trim: true })
31
+ name: string;
32
+
33
+ @Prop({ maxlength: 1000 })
34
+ description?: string;
35
+
36
+ @Prop({ required: true, unique: true, uppercase: true, trim: true })
37
+ sku: string;
38
+
39
+ @Prop({ required: true, min: 0 })
40
+ price: number;
41
+
42
+ @Prop({ required: true, min: 0, default: 0 })
43
+ stock: number;
44
+
45
+ @Prop({ maxlength: 100 })
46
+ category?: string;
47
+
48
+ @Prop({
49
+ type: String,
50
+ enum: Object.values(ProductStatus),
51
+ default: ProductStatus.ACTIVE,
52
+ })
53
+ status: ProductStatus;
54
+
55
+ /** Soft-delete timestamp — null/undefined means record is active */
56
+ @Prop({ type: Date, default: null, index: true })
57
+ deletedAt?: Date | null;
58
+
59
+ // Timestamps added automatically by { timestamps: true }
60
+ createdAt: Date;
61
+ updatedAt: Date;
62
+ }
63
+
64
+ export const ProductSchema = SchemaFactory.createForClass(Product);
65
+
66
+ // Partial index: only index active documents
67
+ ProductSchema.index({ sku: 1 }, { unique: true, partialFilterExpression: { deletedAt: null } });
@@ -0,0 +1,12 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { ProductsController } from './products.controller';
3
+ import { ProductsService } from './products.service';
4
+ import { PrismaModule } from '../../prisma/prisma.module';
5
+
6
+ @Module({
7
+ imports: [PrismaModule],
8
+ controllers: [ProductsController],
9
+ providers: [ProductsService],
10
+ exports: [ProductsService],
11
+ })
12
+ export class ProductsModule {}
@@ -0,0 +1,100 @@
1
+ import { Injectable, NotFoundException } from '@nestjs/common';
2
+ import { PrismaService } from '../../prisma/prisma.service';
3
+ import { BaseService, IBaseRepository, PaginationQueryDto } from '../../common/base';
4
+ import { CreateProductDto } from './dto/create-product.dto';
5
+ import { UpdateProductDto } from './dto/update-product.dto';
6
+
7
+ // ─────────────────────────────────────────────────────────────
8
+ // Product Entity Interface
9
+ //
10
+ // TODO: Once you add the Product model to prisma/schema.prisma
11
+ // and run `npx prisma migrate dev`, replace this interface
12
+ // with the generated Prisma type:
13
+ // import { Product } from '@prisma/client';
14
+ // export type ProductEntity = Product;
15
+ //
16
+ // Prisma schema to add:
17
+ // model Product {
18
+ // id String @id @default(uuid())
19
+ // name String
20
+ // description String?
21
+ // sku String @unique
22
+ // price Float
23
+ // stock Int @default(0)
24
+ // category String?
25
+ // status String @default("ACTIVE")
26
+ // createdAt DateTime @default(now())
27
+ // updatedAt DateTime @updatedAt
28
+ // deletedAt DateTime?
29
+ // }
30
+ // ─────────────────────────────────────────────────────────────
31
+ export interface ProductEntity {
32
+ id: string;
33
+ name: string;
34
+ description?: string | null;
35
+ sku: string;
36
+ price: number;
37
+ stock: number;
38
+ category?: string | null;
39
+ status: string;
40
+ createdAt: Date;
41
+ updatedAt: Date;
42
+ deletedAt?: Date | null;
43
+ }
44
+
45
+ class PrismaProductRepository
46
+ implements IBaseRepository<ProductEntity, CreateProductDto, UpdateProductDto>
47
+ {
48
+ constructor(private readonly prisma: PrismaService) {}
49
+
50
+ async create(dto: CreateProductDto): Promise<ProductEntity> {
51
+ return (this.prisma as any).product.create({ data: dto }) as Promise<ProductEntity>;
52
+ }
53
+
54
+ async findAll(p: PaginationQueryDto): Promise<{ data: ProductEntity[]; total: number }> {
55
+ const { page = 1, limit = 10 } = p;
56
+ const skip = (page - 1) * limit;
57
+ const [data, total] = await this.prisma.$transaction([
58
+ (this.prisma as any).product.findMany({ where: { deletedAt: null }, skip, take: limit, orderBy: { createdAt: 'desc' } }),
59
+ (this.prisma as any).product.count({ where: { deletedAt: null } }),
60
+ ]);
61
+ return { data: data as ProductEntity[], total };
62
+ }
63
+
64
+ async findOne(id: string): Promise<ProductEntity | null> {
65
+ return (this.prisma as any).product.findFirst({ where: { id, deletedAt: null } }) as Promise<ProductEntity | null>;
66
+ }
67
+
68
+ async update(id: string, dto: UpdateProductDto): Promise<ProductEntity> {
69
+ return (this.prisma as any).product.update({ where: { id }, data: dto }) as Promise<ProductEntity>;
70
+ }
71
+
72
+ async remove(id: string): Promise<ProductEntity> {
73
+ return (this.prisma as any).product.update({ where: { id }, data: { deletedAt: new Date() } }) as Promise<ProductEntity>;
74
+ }
75
+ }
76
+
77
+ @Injectable()
78
+ export class ProductsService extends BaseService<ProductEntity, CreateProductDto, UpdateProductDto> {
79
+ private readonly repository: PrismaProductRepository;
80
+
81
+ constructor(private readonly prisma: PrismaService) {
82
+ super();
83
+ this.repository = new PrismaProductRepository(this.prisma);
84
+ }
85
+
86
+ protected getRepository(): IBaseRepository<ProductEntity, CreateProductDto, UpdateProductDto> {
87
+ return this.repository;
88
+ }
89
+
90
+ async findBySku(sku: string): Promise<ProductEntity> {
91
+ const p = await (this.prisma as any).product.findFirst({ where: { sku, deletedAt: null } });
92
+ if (!p) throw new NotFoundException(`Product with SKU "${sku}" was not found`);
93
+ return p as ProductEntity;
94
+ }
95
+
96
+ async adjustStock(id: string, delta: number): Promise<ProductEntity> {
97
+ await this.findOne(id);
98
+ return (this.prisma as any).product.update({ where: { id }, data: { stock: { increment: delta } } }) as Promise<ProductEntity>;
99
+ }
100
+ }
@@ -0,0 +1,58 @@
1
+ import {
2
+ Entity,
3
+ PrimaryGeneratedColumn,
4
+ Column,
5
+ CreateDateColumn,
6
+ UpdateDateColumn,
7
+ DeleteDateColumn,
8
+ Index,
9
+ } from 'typeorm';
10
+
11
+ export enum ProductStatus {
12
+ ACTIVE = 'ACTIVE',
13
+ INACTIVE = 'INACTIVE',
14
+ OUT_OF_STOCK = 'OUT_OF_STOCK',
15
+ DISCONTINUED = 'DISCONTINUED',
16
+ }
17
+
18
+ @Entity('products')
19
+ export class Product {
20
+ @PrimaryGeneratedColumn('uuid')
21
+ id: string;
22
+
23
+ @Column({ length: 150 })
24
+ name: string;
25
+
26
+ @Column({ type: 'text', nullable: true })
27
+ description: string | null;
28
+
29
+ @Column({ length: 50, unique: true })
30
+ @Index()
31
+ sku: string;
32
+
33
+ @Column({ type: 'decimal', precision: 10, scale: 2 })
34
+ price: number;
35
+
36
+ @Column({ default: 0 })
37
+ stock: number;
38
+
39
+ @Column({ length: 100, nullable: true })
40
+ category: string | null;
41
+
42
+ @Column({
43
+ type: 'enum',
44
+ enum: ProductStatus,
45
+ default: ProductStatus.ACTIVE,
46
+ })
47
+ status: ProductStatus;
48
+
49
+ @CreateDateColumn({ name: 'created_at' })
50
+ createdAt: Date;
51
+
52
+ @UpdateDateColumn({ name: 'updated_at' })
53
+ updatedAt: Date;
54
+
55
+ /** Soft-delete column — null means the record is active */
56
+ @DeleteDateColumn({ name: 'deleted_at', nullable: true })
57
+ deletedAt: Date | null;
58
+ }