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,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
|
+
}
|