speedrun-cli 2.6.10 → 2.7.0
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 +22 -0
- package/package.json +7 -7
- package/src/generator.js +1 -16
- package/src/moduleGenerator.js +597 -121
- package/src/postSetup.js +423 -402
- package/src/prompts.js +25 -3
- package/templates/orm/drizzle/package.json +2 -1
- package/templates/orm/mongoose/package.json +2 -1
- package/templates/orm/prisma/package.json +1 -0
- package/templates/base-crud/src/modules/products/dto/create-product.dto.ts +0 -34
- package/templates/base-crud/src/modules/products/dto/product.dto.ts +0 -36
- package/templates/base-crud/src/modules/products/dto/update-product.dto.ts +0 -4
- package/templates/base-crud/src/modules/products/products.controller.ts +0 -78
- package/templates/base-crud-drizzle/src/modules/products/dto/create-product.dto.ts +0 -34
- package/templates/base-crud-drizzle/src/modules/products/dto/product.dto.ts +0 -36
- package/templates/base-crud-drizzle/src/modules/products/dto/update-product.dto.ts +0 -4
- package/templates/base-crud-drizzle/src/modules/products/products.controller.ts +0 -78
- package/templates/base-crud-drizzle/src/modules/products/products.module.ts +0 -24
- package/templates/base-crud-drizzle/src/modules/products/products.service.ts +0 -140
- package/templates/base-crud-drizzle/src/modules/products/schema/products.schema.ts +0 -39
- package/templates/base-crud-mongoose/src/modules/products/dto/create-product.dto.ts +0 -34
- package/templates/base-crud-mongoose/src/modules/products/dto/product.dto.ts +0 -36
- package/templates/base-crud-mongoose/src/modules/products/dto/update-product.dto.ts +0 -4
- package/templates/base-crud-mongoose/src/modules/products/products.controller.ts +0 -78
- package/templates/base-crud-mongoose/src/modules/products/products.module.ts +0 -26
- package/templates/base-crud-mongoose/src/modules/products/products.service.ts +0 -133
- package/templates/base-crud-mongoose/src/modules/products/schemas/product.schema.ts +0 -67
- package/templates/base-crud-prisma/src/modules/products/dto/create-product.dto.ts +0 -34
- package/templates/base-crud-prisma/src/modules/products/dto/product.dto.ts +0 -36
- package/templates/base-crud-prisma/src/modules/products/dto/update-product.dto.ts +0 -4
- package/templates/base-crud-prisma/src/modules/products/products.controller.ts +0 -78
- package/templates/base-crud-prisma/src/modules/products/products.module.ts +0 -12
- package/templates/base-crud-prisma/src/modules/products/products.service.ts +0 -100
- package/templates/base-crud-typeorm/src/modules/products/dto/create-product.dto.ts +0 -34
- package/templates/base-crud-typeorm/src/modules/products/dto/product.dto.ts +0 -36
- package/templates/base-crud-typeorm/src/modules/products/dto/update-product.dto.ts +0 -4
- package/templates/base-crud-typeorm/src/modules/products/entities/product.entity.ts +0 -58
- package/templates/base-crud-typeorm/src/modules/products/products.controller.ts +0 -78
- package/templates/base-crud-typeorm/src/modules/products/products.module.ts +0 -25
- package/templates/base-crud-typeorm/src/modules/products/products.service.ts +0 -102
package/src/prompts.js
CHANGED
|
@@ -101,13 +101,33 @@ async function promptForProjectDetails(providedAppName, options) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// Base CRUD Architecture prompt
|
|
104
|
-
if (
|
|
104
|
+
if (options.baseCrud === undefined && !options.yes) {
|
|
105
105
|
questions.push({
|
|
106
106
|
type: 'confirm',
|
|
107
107
|
name: 'baseCrud',
|
|
108
108
|
message:
|
|
109
109
|
'Enable Base CRUD Architecture? (generates abstract BaseService, BaseController & Swagger helpers in src/common/base)',
|
|
110
|
-
default:
|
|
110
|
+
default: true,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Generate First CRUD Module prompt
|
|
115
|
+
if (!options.yes) {
|
|
116
|
+
questions.push({
|
|
117
|
+
type: 'confirm',
|
|
118
|
+
name: 'generateFirstCrud',
|
|
119
|
+
message: 'Do you want to generate your first CRUD module now?',
|
|
120
|
+
default: true,
|
|
121
|
+
when: (answers) => (options.baseCrud !== false && (answers.baseCrud !== false)),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
questions.push({
|
|
125
|
+
type: 'input',
|
|
126
|
+
name: 'firstModuleName',
|
|
127
|
+
message: 'What module do you want to generate? (e.g., orders, products)',
|
|
128
|
+
default: 'orders',
|
|
129
|
+
when: (answers) => answers.generateFirstCrud,
|
|
130
|
+
validate: (input) => (input && input.trim() ? true : 'Module name is required'),
|
|
111
131
|
});
|
|
112
132
|
}
|
|
113
133
|
|
|
@@ -127,7 +147,9 @@ async function promptForProjectDetails(providedAppName, options) {
|
|
|
127
147
|
installDependencies: options.skipInstall ? false : answers.installDependencies !== false,
|
|
128
148
|
initializeGit: options.skipGit ? false : answers.initializeGit !== false,
|
|
129
149
|
swagger: options.swagger || answers.swagger || false,
|
|
130
|
-
baseCrud: options.baseCrud
|
|
150
|
+
baseCrud: options.baseCrud !== undefined ? options.baseCrud : (answers.baseCrud !== false),
|
|
151
|
+
generateFirstCrud: answers.generateFirstCrud || false,
|
|
152
|
+
firstModuleName: answers.firstModuleName || undefined,
|
|
131
153
|
};
|
|
132
154
|
}
|
|
133
155
|
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"db:migrate": "drizzle-kit migrate",
|
|
23
23
|
"db:push": "drizzle-kit push",
|
|
24
24
|
"db:studio": "drizzle-kit studio",
|
|
25
|
-
"db:seed": "ts-node src/database/seed.ts"
|
|
25
|
+
"db:seed": "ts-node src/database/seed.ts",
|
|
26
|
+
"seed": "ts-node src/database/seed.ts"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
29
|
"@nestjs/common": "^11.0.0",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"test:cov": "jest --coverage",
|
|
19
19
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
|
20
20
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
|
21
|
-
"db:seed": "ts-node src/database/seed.ts"
|
|
21
|
+
"db:seed": "ts-node src/database/seed.ts",
|
|
22
|
+
"seed": "ts-node src/database/seed.ts"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"@nestjs/common": "^11.0.0",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"prisma:migrate:deploy": "prisma migrate deploy",
|
|
25
25
|
"prisma:migrate:status": "prisma migrate status",
|
|
26
26
|
"prisma:seed": "ts-node prisma/seed.ts",
|
|
27
|
+
"seed": "ts-node prisma/seed.ts",
|
|
27
28
|
"prisma:studio": "prisma studio",
|
|
28
29
|
"prisma:reset": "prisma migrate reset --force"
|
|
29
30
|
},
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
-
import { IsString, IsNotEmpty, IsNumber, IsOptional, Min } from 'class-validator';
|
|
3
|
-
|
|
4
|
-
export class CreateProductDto {
|
|
5
|
-
@ApiProperty({ example: 'Wireless Mouse' })
|
|
6
|
-
@IsString()
|
|
7
|
-
@IsNotEmpty()
|
|
8
|
-
name: string;
|
|
9
|
-
|
|
10
|
-
@ApiPropertyOptional({ example: 'Ergonomic wireless mouse' })
|
|
11
|
-
@IsString()
|
|
12
|
-
@IsOptional()
|
|
13
|
-
description?: string;
|
|
14
|
-
|
|
15
|
-
@ApiProperty({ example: 'PROD-001' })
|
|
16
|
-
@IsString()
|
|
17
|
-
@IsNotEmpty()
|
|
18
|
-
sku: string;
|
|
19
|
-
|
|
20
|
-
@ApiProperty({ example: 29.99 })
|
|
21
|
-
@IsNumber()
|
|
22
|
-
@Min(0)
|
|
23
|
-
price: number;
|
|
24
|
-
|
|
25
|
-
@ApiProperty({ example: 100 })
|
|
26
|
-
@IsNumber()
|
|
27
|
-
@Min(0)
|
|
28
|
-
stock: number;
|
|
29
|
-
|
|
30
|
-
@ApiPropertyOptional({ example: 'Electronics' })
|
|
31
|
-
@IsString()
|
|
32
|
-
@IsOptional()
|
|
33
|
-
category?: string;
|
|
34
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
-
|
|
3
|
-
export class ProductDto {
|
|
4
|
-
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000', format: 'uuid' })
|
|
5
|
-
id: string;
|
|
6
|
-
|
|
7
|
-
@ApiProperty({ example: 'Wireless Mouse' })
|
|
8
|
-
name: string;
|
|
9
|
-
|
|
10
|
-
@ApiPropertyOptional({ example: 'Ergonomic wireless mouse' })
|
|
11
|
-
description?: string;
|
|
12
|
-
|
|
13
|
-
@ApiProperty({ example: 'PROD-001' })
|
|
14
|
-
sku: string;
|
|
15
|
-
|
|
16
|
-
@ApiProperty({ example: 29.99 })
|
|
17
|
-
price: number;
|
|
18
|
-
|
|
19
|
-
@ApiProperty({ example: 100 })
|
|
20
|
-
stock: number;
|
|
21
|
-
|
|
22
|
-
@ApiPropertyOptional({ example: 'Electronics' })
|
|
23
|
-
category?: string;
|
|
24
|
-
|
|
25
|
-
@ApiProperty({ example: 'ACTIVE' })
|
|
26
|
-
status: string;
|
|
27
|
-
|
|
28
|
-
@ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
29
|
-
createdAt: Date;
|
|
30
|
-
|
|
31
|
-
@ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
32
|
-
updatedAt: Date;
|
|
33
|
-
|
|
34
|
-
@ApiPropertyOptional({ nullable: true, example: null })
|
|
35
|
-
deletedAt?: Date | null;
|
|
36
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
-
import { IsString, IsNotEmpty, IsNumber, IsOptional, Min } from 'class-validator';
|
|
3
|
-
|
|
4
|
-
export class CreateProductDto {
|
|
5
|
-
@ApiProperty({ example: 'Wireless Mouse' })
|
|
6
|
-
@IsString()
|
|
7
|
-
@IsNotEmpty()
|
|
8
|
-
name: string;
|
|
9
|
-
|
|
10
|
-
@ApiPropertyOptional({ example: 'Ergonomic wireless mouse' })
|
|
11
|
-
@IsString()
|
|
12
|
-
@IsOptional()
|
|
13
|
-
description?: string;
|
|
14
|
-
|
|
15
|
-
@ApiProperty({ example: 'PROD-001' })
|
|
16
|
-
@IsString()
|
|
17
|
-
@IsNotEmpty()
|
|
18
|
-
sku: string;
|
|
19
|
-
|
|
20
|
-
@ApiProperty({ example: 29.99 })
|
|
21
|
-
@IsNumber()
|
|
22
|
-
@Min(0)
|
|
23
|
-
price: number;
|
|
24
|
-
|
|
25
|
-
@ApiProperty({ example: 100 })
|
|
26
|
-
@IsNumber()
|
|
27
|
-
@Min(0)
|
|
28
|
-
stock: number;
|
|
29
|
-
|
|
30
|
-
@ApiPropertyOptional({ example: 'Electronics' })
|
|
31
|
-
@IsString()
|
|
32
|
-
@IsOptional()
|
|
33
|
-
category?: string;
|
|
34
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
-
|
|
3
|
-
export class ProductDto {
|
|
4
|
-
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000', format: 'uuid' })
|
|
5
|
-
id: string;
|
|
6
|
-
|
|
7
|
-
@ApiProperty({ example: 'Wireless Mouse' })
|
|
8
|
-
name: string;
|
|
9
|
-
|
|
10
|
-
@ApiPropertyOptional({ example: 'Ergonomic wireless mouse' })
|
|
11
|
-
description?: string;
|
|
12
|
-
|
|
13
|
-
@ApiProperty({ example: 'PROD-001' })
|
|
14
|
-
sku: string;
|
|
15
|
-
|
|
16
|
-
@ApiProperty({ example: 29.99 })
|
|
17
|
-
price: number;
|
|
18
|
-
|
|
19
|
-
@ApiProperty({ example: 100 })
|
|
20
|
-
stock: number;
|
|
21
|
-
|
|
22
|
-
@ApiPropertyOptional({ example: 'Electronics' })
|
|
23
|
-
category?: string;
|
|
24
|
-
|
|
25
|
-
@ApiProperty({ example: 'ACTIVE' })
|
|
26
|
-
status: string;
|
|
27
|
-
|
|
28
|
-
@ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
29
|
-
createdAt: Date;
|
|
30
|
-
|
|
31
|
-
@ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
32
|
-
updatedAt: Date;
|
|
33
|
-
|
|
34
|
-
@ApiPropertyOptional({ nullable: true, example: null })
|
|
35
|
-
deletedAt?: Date | null;
|
|
36
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
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 {}
|
|
@@ -1,140 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
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';
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
-
import { IsString, IsNotEmpty, IsNumber, IsOptional, Min } from 'class-validator';
|
|
3
|
-
|
|
4
|
-
export class CreateProductDto {
|
|
5
|
-
@ApiProperty({ example: 'Wireless Mouse' })
|
|
6
|
-
@IsString()
|
|
7
|
-
@IsNotEmpty()
|
|
8
|
-
name: string;
|
|
9
|
-
|
|
10
|
-
@ApiPropertyOptional({ example: 'Ergonomic wireless mouse' })
|
|
11
|
-
@IsString()
|
|
12
|
-
@IsOptional()
|
|
13
|
-
description?: string;
|
|
14
|
-
|
|
15
|
-
@ApiProperty({ example: 'PROD-001' })
|
|
16
|
-
@IsString()
|
|
17
|
-
@IsNotEmpty()
|
|
18
|
-
sku: string;
|
|
19
|
-
|
|
20
|
-
@ApiProperty({ example: 29.99 })
|
|
21
|
-
@IsNumber()
|
|
22
|
-
@Min(0)
|
|
23
|
-
price: number;
|
|
24
|
-
|
|
25
|
-
@ApiProperty({ example: 100 })
|
|
26
|
-
@IsNumber()
|
|
27
|
-
@Min(0)
|
|
28
|
-
stock: number;
|
|
29
|
-
|
|
30
|
-
@ApiPropertyOptional({ example: 'Electronics' })
|
|
31
|
-
@IsString()
|
|
32
|
-
@IsOptional()
|
|
33
|
-
category?: string;
|
|
34
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
-
|
|
3
|
-
export class ProductDto {
|
|
4
|
-
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000', format: 'uuid' })
|
|
5
|
-
id: string;
|
|
6
|
-
|
|
7
|
-
@ApiProperty({ example: 'Wireless Mouse' })
|
|
8
|
-
name: string;
|
|
9
|
-
|
|
10
|
-
@ApiPropertyOptional({ example: 'Ergonomic wireless mouse' })
|
|
11
|
-
description?: string;
|
|
12
|
-
|
|
13
|
-
@ApiProperty({ example: 'PROD-001' })
|
|
14
|
-
sku: string;
|
|
15
|
-
|
|
16
|
-
@ApiProperty({ example: 29.99 })
|
|
17
|
-
price: number;
|
|
18
|
-
|
|
19
|
-
@ApiProperty({ example: 100 })
|
|
20
|
-
stock: number;
|
|
21
|
-
|
|
22
|
-
@ApiPropertyOptional({ example: 'Electronics' })
|
|
23
|
-
category?: string;
|
|
24
|
-
|
|
25
|
-
@ApiProperty({ example: 'ACTIVE' })
|
|
26
|
-
status: string;
|
|
27
|
-
|
|
28
|
-
@ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
29
|
-
createdAt: Date;
|
|
30
|
-
|
|
31
|
-
@ApiProperty({ example: '2025-01-01T00:00:00.000Z' })
|
|
32
|
-
updatedAt: Date;
|
|
33
|
-
|
|
34
|
-
@ApiPropertyOptional({ nullable: true, example: null })
|
|
35
|
-
deletedAt?: Date | null;
|
|
36
|
-
}
|