@malamute/ai-rules 1.0.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/README.md +174 -0
- package/bin/cli.js +5 -0
- package/configs/_shared/.claude/commands/fix-issue.md +38 -0
- package/configs/_shared/.claude/commands/generate-tests.md +49 -0
- package/configs/_shared/.claude/commands/review-pr.md +77 -0
- package/configs/_shared/.claude/rules/accessibility.md +270 -0
- package/configs/_shared/.claude/rules/performance.md +226 -0
- package/configs/_shared/.claude/rules/security.md +188 -0
- package/configs/_shared/.claude/skills/debug/SKILL.md +118 -0
- package/configs/_shared/.claude/skills/learning/SKILL.md +224 -0
- package/configs/_shared/.claude/skills/review/SKILL.md +86 -0
- package/configs/_shared/.claude/skills/spec/SKILL.md +112 -0
- package/configs/_shared/CLAUDE.md +174 -0
- package/configs/angular/.claude/rules/components.md +257 -0
- package/configs/angular/.claude/rules/state.md +250 -0
- package/configs/angular/.claude/rules/testing.md +422 -0
- package/configs/angular/.claude/settings.json +31 -0
- package/configs/angular/CLAUDE.md +251 -0
- package/configs/dotnet/.claude/rules/api.md +370 -0
- package/configs/dotnet/.claude/rules/architecture.md +199 -0
- package/configs/dotnet/.claude/rules/database/efcore.md +408 -0
- package/configs/dotnet/.claude/rules/testing.md +389 -0
- package/configs/dotnet/.claude/settings.json +9 -0
- package/configs/dotnet/CLAUDE.md +319 -0
- package/configs/nestjs/.claude/rules/auth.md +321 -0
- package/configs/nestjs/.claude/rules/database/prisma.md +305 -0
- package/configs/nestjs/.claude/rules/database/typeorm.md +379 -0
- package/configs/nestjs/.claude/rules/modules.md +215 -0
- package/configs/nestjs/.claude/rules/testing.md +315 -0
- package/configs/nestjs/.claude/rules/validation.md +279 -0
- package/configs/nestjs/.claude/settings.json +15 -0
- package/configs/nestjs/CLAUDE.md +263 -0
- package/configs/nextjs/.claude/rules/components.md +211 -0
- package/configs/nextjs/.claude/rules/state/redux-toolkit.md +429 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +299 -0
- package/configs/nextjs/.claude/rules/testing.md +315 -0
- package/configs/nextjs/.claude/settings.json +29 -0
- package/configs/nextjs/CLAUDE.md +376 -0
- package/configs/python/.claude/rules/database/sqlalchemy.md +355 -0
- package/configs/python/.claude/rules/fastapi.md +272 -0
- package/configs/python/.claude/rules/flask.md +332 -0
- package/configs/python/.claude/rules/testing.md +374 -0
- package/configs/python/.claude/settings.json +18 -0
- package/configs/python/CLAUDE.md +273 -0
- package/package.json +41 -0
- package/src/install.js +315 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/*.spec.ts"
|
|
4
|
+
- "test/**/*.e2e-spec.ts"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# NestJS Testing
|
|
8
|
+
|
|
9
|
+
## Unit Tests
|
|
10
|
+
|
|
11
|
+
### Test Services in Isolation
|
|
12
|
+
|
|
13
|
+
Mock all dependencies. Focus on business logic.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
17
|
+
import { UsersService } from './users.service';
|
|
18
|
+
import { UsersRepository } from './users.repository';
|
|
19
|
+
import { NotFoundException } from '@nestjs/common';
|
|
20
|
+
|
|
21
|
+
describe('UsersService', () => {
|
|
22
|
+
let service: UsersService;
|
|
23
|
+
let repository: jest.Mocked<UsersRepository>;
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
27
|
+
providers: [
|
|
28
|
+
UsersService,
|
|
29
|
+
{
|
|
30
|
+
provide: UsersRepository,
|
|
31
|
+
useValue: {
|
|
32
|
+
findById: jest.fn(),
|
|
33
|
+
create: jest.fn(),
|
|
34
|
+
update: jest.fn(),
|
|
35
|
+
delete: jest.fn(),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
}).compile();
|
|
40
|
+
|
|
41
|
+
service = module.get<UsersService>(UsersService);
|
|
42
|
+
repository = module.get(UsersRepository);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('findOne', () => {
|
|
46
|
+
it('should return user when found', async () => {
|
|
47
|
+
const user = { id: '1', email: 'test@example.com' };
|
|
48
|
+
repository.findById.mockResolvedValue(user);
|
|
49
|
+
|
|
50
|
+
const result = await service.findOne('1');
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual(user);
|
|
53
|
+
expect(repository.findById).toHaveBeenCalledWith('1');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should throw NotFoundException when user not found', async () => {
|
|
57
|
+
repository.findById.mockResolvedValue(null);
|
|
58
|
+
|
|
59
|
+
await expect(service.findOne('1')).rejects.toThrow(NotFoundException);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Test Controllers
|
|
66
|
+
|
|
67
|
+
Test request handling, pipes, guards.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
71
|
+
import { UsersController } from './users.controller';
|
|
72
|
+
import { UsersService } from './users.service';
|
|
73
|
+
|
|
74
|
+
describe('UsersController', () => {
|
|
75
|
+
let controller: UsersController;
|
|
76
|
+
let service: jest.Mocked<UsersService>;
|
|
77
|
+
|
|
78
|
+
beforeEach(async () => {
|
|
79
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
80
|
+
controllers: [UsersController],
|
|
81
|
+
providers: [
|
|
82
|
+
{
|
|
83
|
+
provide: UsersService,
|
|
84
|
+
useValue: {
|
|
85
|
+
findOne: jest.fn(),
|
|
86
|
+
create: jest.fn(),
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
}).compile();
|
|
91
|
+
|
|
92
|
+
controller = module.get<UsersController>(UsersController);
|
|
93
|
+
service = module.get(UsersService);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('findOne', () => {
|
|
97
|
+
it('should return user from service', async () => {
|
|
98
|
+
const user = { id: '1', email: 'test@example.com' };
|
|
99
|
+
service.findOne.mockResolvedValue(user);
|
|
100
|
+
|
|
101
|
+
const result = await controller.findOne('1');
|
|
102
|
+
|
|
103
|
+
expect(result).toEqual(user);
|
|
104
|
+
expect(service.findOne).toHaveBeenCalledWith('1');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Mock Repository Pattern
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// Create reusable mock factory
|
|
114
|
+
export const createMockRepository = <T = any>() => ({
|
|
115
|
+
find: jest.fn(),
|
|
116
|
+
findOne: jest.fn(),
|
|
117
|
+
create: jest.fn(),
|
|
118
|
+
save: jest.fn(),
|
|
119
|
+
update: jest.fn(),
|
|
120
|
+
delete: jest.fn(),
|
|
121
|
+
createQueryBuilder: jest.fn(() => ({
|
|
122
|
+
where: jest.fn().mockReturnThis(),
|
|
123
|
+
andWhere: jest.fn().mockReturnThis(),
|
|
124
|
+
orderBy: jest.fn().mockReturnThis(),
|
|
125
|
+
skip: jest.fn().mockReturnThis(),
|
|
126
|
+
take: jest.fn().mockReturnThis(),
|
|
127
|
+
getMany: jest.fn(),
|
|
128
|
+
getOne: jest.fn(),
|
|
129
|
+
})),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Usage
|
|
133
|
+
const module = await Test.createTestingModule({
|
|
134
|
+
providers: [
|
|
135
|
+
UsersService,
|
|
136
|
+
{
|
|
137
|
+
provide: getRepositoryToken(User),
|
|
138
|
+
useValue: createMockRepository(),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
}).compile();
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## E2E Tests
|
|
145
|
+
|
|
146
|
+
### Setup Test Application
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// test/app.e2e-spec.ts
|
|
150
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
151
|
+
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
|
152
|
+
import * as request from 'supertest';
|
|
153
|
+
import { AppModule } from '../src/app.module';
|
|
154
|
+
|
|
155
|
+
describe('AppController (e2e)', () => {
|
|
156
|
+
let app: INestApplication;
|
|
157
|
+
|
|
158
|
+
beforeAll(async () => {
|
|
159
|
+
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
160
|
+
imports: [AppModule],
|
|
161
|
+
}).compile();
|
|
162
|
+
|
|
163
|
+
app = moduleFixture.createNestApplication();
|
|
164
|
+
|
|
165
|
+
// Apply same pipes as production
|
|
166
|
+
app.useGlobalPipes(
|
|
167
|
+
new ValidationPipe({
|
|
168
|
+
whitelist: true,
|
|
169
|
+
forbidNonWhitelisted: true,
|
|
170
|
+
transform: true,
|
|
171
|
+
}),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
await app.init();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
afterAll(async () => {
|
|
178
|
+
await app.close();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('/users', () => {
|
|
182
|
+
it('POST /users - should create user', () => {
|
|
183
|
+
return request(app.getHttpServer())
|
|
184
|
+
.post('/users')
|
|
185
|
+
.send({
|
|
186
|
+
email: 'test@example.com',
|
|
187
|
+
password: 'password123',
|
|
188
|
+
})
|
|
189
|
+
.expect(201)
|
|
190
|
+
.expect((res) => {
|
|
191
|
+
expect(res.body).toHaveProperty('id');
|
|
192
|
+
expect(res.body.email).toBe('test@example.com');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('POST /users - should reject invalid email', () => {
|
|
197
|
+
return request(app.getHttpServer())
|
|
198
|
+
.post('/users')
|
|
199
|
+
.send({
|
|
200
|
+
email: 'invalid-email',
|
|
201
|
+
password: 'password123',
|
|
202
|
+
})
|
|
203
|
+
.expect(400);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('GET /users/:id - should return user', () => {
|
|
207
|
+
return request(app.getHttpServer())
|
|
208
|
+
.get('/users/existing-id')
|
|
209
|
+
.expect(200)
|
|
210
|
+
.expect((res) => {
|
|
211
|
+
expect(res.body).toHaveProperty('email');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('GET /users/:id - should return 404 for unknown user', () => {
|
|
216
|
+
return request(app.getHttpServer())
|
|
217
|
+
.get('/users/unknown-id')
|
|
218
|
+
.expect(404);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Test with Authentication
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
describe('Protected Routes (e2e)', () => {
|
|
228
|
+
let app: INestApplication;
|
|
229
|
+
let authToken: string;
|
|
230
|
+
|
|
231
|
+
beforeAll(async () => {
|
|
232
|
+
// ... setup app
|
|
233
|
+
|
|
234
|
+
// Get auth token
|
|
235
|
+
const response = await request(app.getHttpServer())
|
|
236
|
+
.post('/auth/login')
|
|
237
|
+
.send({ email: 'test@example.com', password: 'password' });
|
|
238
|
+
|
|
239
|
+
authToken = response.body.accessToken;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('GET /profile - should return user profile with token', () => {
|
|
243
|
+
return request(app.getHttpServer())
|
|
244
|
+
.get('/profile')
|
|
245
|
+
.set('Authorization', `Bearer ${authToken}`)
|
|
246
|
+
.expect(200);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('GET /profile - should return 401 without token', () => {
|
|
250
|
+
return request(app.getHttpServer())
|
|
251
|
+
.get('/profile')
|
|
252
|
+
.expect(401);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Test Database Isolation
|
|
258
|
+
|
|
259
|
+
Use a test database and clean between tests:
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import { DataSource } from 'typeorm';
|
|
263
|
+
|
|
264
|
+
describe('Users E2E', () => {
|
|
265
|
+
let app: INestApplication;
|
|
266
|
+
let dataSource: DataSource;
|
|
267
|
+
|
|
268
|
+
beforeAll(async () => {
|
|
269
|
+
const module = await Test.createTestingModule({
|
|
270
|
+
imports: [AppModule],
|
|
271
|
+
}).compile();
|
|
272
|
+
|
|
273
|
+
app = module.createNestApplication();
|
|
274
|
+
await app.init();
|
|
275
|
+
|
|
276
|
+
dataSource = module.get(DataSource);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
beforeEach(async () => {
|
|
280
|
+
// Clean database before each test
|
|
281
|
+
await dataSource.synchronize(true);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
afterAll(async () => {
|
|
285
|
+
await dataSource.destroy();
|
|
286
|
+
await app.close();
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Test Coverage Goals
|
|
292
|
+
|
|
293
|
+
- **Services**: 90%+ (business logic)
|
|
294
|
+
- **Controllers**: 80%+ (happy paths + error cases)
|
|
295
|
+
- **Guards/Pipes**: 80%+
|
|
296
|
+
- **E2E**: Cover all API endpoints
|
|
297
|
+
|
|
298
|
+
## Testing Commands
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
# Unit tests
|
|
302
|
+
npm run test
|
|
303
|
+
|
|
304
|
+
# Watch mode
|
|
305
|
+
npm run test:watch
|
|
306
|
+
|
|
307
|
+
# Coverage report
|
|
308
|
+
npm run test:cov
|
|
309
|
+
|
|
310
|
+
# E2E tests
|
|
311
|
+
npm run test:e2e
|
|
312
|
+
|
|
313
|
+
# Single file
|
|
314
|
+
npm run test -- users.service.spec.ts
|
|
315
|
+
```
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/*.dto.ts"
|
|
4
|
+
- "src/**/dto/*.ts"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# NestJS Validation & DTOs
|
|
8
|
+
|
|
9
|
+
## DTO Rules
|
|
10
|
+
|
|
11
|
+
### Always Use class-validator
|
|
12
|
+
|
|
13
|
+
Every DTO property must have validation decorators.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import {
|
|
17
|
+
IsEmail,
|
|
18
|
+
IsString,
|
|
19
|
+
IsOptional,
|
|
20
|
+
MinLength,
|
|
21
|
+
MaxLength,
|
|
22
|
+
IsUUID,
|
|
23
|
+
IsEnum,
|
|
24
|
+
IsArray,
|
|
25
|
+
ValidateNested,
|
|
26
|
+
IsInt,
|
|
27
|
+
Min,
|
|
28
|
+
Max,
|
|
29
|
+
} from 'class-validator';
|
|
30
|
+
import { Type } from 'class-transformer';
|
|
31
|
+
|
|
32
|
+
export class CreateUserDto {
|
|
33
|
+
@IsEmail()
|
|
34
|
+
email: string;
|
|
35
|
+
|
|
36
|
+
@IsString()
|
|
37
|
+
@MinLength(8)
|
|
38
|
+
@MaxLength(100)
|
|
39
|
+
password: string;
|
|
40
|
+
|
|
41
|
+
@IsString()
|
|
42
|
+
@IsOptional()
|
|
43
|
+
@MaxLength(50)
|
|
44
|
+
name?: string;
|
|
45
|
+
|
|
46
|
+
@IsEnum(UserRole)
|
|
47
|
+
@IsOptional()
|
|
48
|
+
role?: UserRole;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Use Mapped Types for Variants
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { PartialType, PickType, OmitType, IntersectionType } from '@nestjs/mapped-types';
|
|
56
|
+
|
|
57
|
+
// All fields optional
|
|
58
|
+
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
|
59
|
+
|
|
60
|
+
// Only specific fields
|
|
61
|
+
export class LoginDto extends PickType(CreateUserDto, ['email', 'password']) {}
|
|
62
|
+
|
|
63
|
+
// Exclude fields
|
|
64
|
+
export class PublicUserDto extends OmitType(CreateUserDto, ['password']) {}
|
|
65
|
+
|
|
66
|
+
// Combine DTOs
|
|
67
|
+
export class CreateUserWithAddressDto extends IntersectionType(
|
|
68
|
+
CreateUserDto,
|
|
69
|
+
CreateAddressDto,
|
|
70
|
+
) {}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Validate Nested Objects
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
export class CreateOrderDto {
|
|
77
|
+
@IsUUID()
|
|
78
|
+
userId: string;
|
|
79
|
+
|
|
80
|
+
@IsArray()
|
|
81
|
+
@ValidateNested({ each: true })
|
|
82
|
+
@Type(() => OrderItemDto)
|
|
83
|
+
items: OrderItemDto[];
|
|
84
|
+
|
|
85
|
+
@ValidateNested()
|
|
86
|
+
@Type(() => AddressDto)
|
|
87
|
+
shippingAddress: AddressDto;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class OrderItemDto {
|
|
91
|
+
@IsUUID()
|
|
92
|
+
productId: string;
|
|
93
|
+
|
|
94
|
+
@IsInt()
|
|
95
|
+
@Min(1)
|
|
96
|
+
@Max(100)
|
|
97
|
+
quantity: number;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Transform Input Data
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { Transform, Type } from 'class-transformer';
|
|
105
|
+
|
|
106
|
+
export class QueryDto {
|
|
107
|
+
@IsOptional()
|
|
108
|
+
@Type(() => Number)
|
|
109
|
+
@IsInt()
|
|
110
|
+
@Min(1)
|
|
111
|
+
page?: number = 1;
|
|
112
|
+
|
|
113
|
+
@IsOptional()
|
|
114
|
+
@Type(() => Number)
|
|
115
|
+
@IsInt()
|
|
116
|
+
@Min(1)
|
|
117
|
+
@Max(100)
|
|
118
|
+
limit?: number = 20;
|
|
119
|
+
|
|
120
|
+
@IsOptional()
|
|
121
|
+
@Transform(({ value }) => value?.toLowerCase().trim())
|
|
122
|
+
@IsString()
|
|
123
|
+
search?: string;
|
|
124
|
+
|
|
125
|
+
@IsOptional()
|
|
126
|
+
@Transform(({ value }) => value === 'true' || value === true)
|
|
127
|
+
@IsBoolean()
|
|
128
|
+
active?: boolean;
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Validation Groups
|
|
133
|
+
|
|
134
|
+
Use groups for conditional validation:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
export class UserDto {
|
|
138
|
+
@IsUUID({ groups: ['update'] })
|
|
139
|
+
@IsOptional({ groups: ['create'] })
|
|
140
|
+
id?: string;
|
|
141
|
+
|
|
142
|
+
@IsEmail({ groups: ['create', 'update'] })
|
|
143
|
+
email: string;
|
|
144
|
+
|
|
145
|
+
@IsString({ groups: ['create'] })
|
|
146
|
+
@MinLength(8, { groups: ['create'] })
|
|
147
|
+
password: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Controller usage
|
|
151
|
+
@Post()
|
|
152
|
+
create(
|
|
153
|
+
@Body(new ValidationPipe({ groups: ['create'] }))
|
|
154
|
+
dto: UserDto,
|
|
155
|
+
) {}
|
|
156
|
+
|
|
157
|
+
@Patch(':id')
|
|
158
|
+
update(
|
|
159
|
+
@Body(new ValidationPipe({ groups: ['update'] }))
|
|
160
|
+
dto: UserDto,
|
|
161
|
+
) {}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Custom Validators
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import {
|
|
168
|
+
ValidatorConstraint,
|
|
169
|
+
ValidatorConstraintInterface,
|
|
170
|
+
ValidationArguments,
|
|
171
|
+
registerDecorator,
|
|
172
|
+
} from 'class-validator';
|
|
173
|
+
|
|
174
|
+
@ValidatorConstraint({ async: true })
|
|
175
|
+
export class IsUniqueEmailConstraint implements ValidatorConstraintInterface {
|
|
176
|
+
constructor(private readonly usersService: UsersService) {}
|
|
177
|
+
|
|
178
|
+
async validate(email: string): Promise<boolean> {
|
|
179
|
+
const user = await this.usersService.findByEmail(email);
|
|
180
|
+
return !user;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
defaultMessage(args: ValidationArguments): string {
|
|
184
|
+
return 'Email already exists';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Decorator factory
|
|
189
|
+
export function IsUniqueEmail(validationOptions?: ValidationOptions) {
|
|
190
|
+
return function (object: object, propertyName: string) {
|
|
191
|
+
registerDecorator({
|
|
192
|
+
target: object.constructor,
|
|
193
|
+
propertyName,
|
|
194
|
+
options: validationOptions,
|
|
195
|
+
constraints: [],
|
|
196
|
+
validator: IsUniqueEmailConstraint,
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Usage
|
|
202
|
+
export class CreateUserDto {
|
|
203
|
+
@IsEmail()
|
|
204
|
+
@IsUniqueEmail()
|
|
205
|
+
email: string;
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## API Documentation with Swagger
|
|
210
|
+
|
|
211
|
+
Combine validation with OpenAPI documentation:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
215
|
+
|
|
216
|
+
export class CreateUserDto {
|
|
217
|
+
@ApiProperty({
|
|
218
|
+
example: 'user@example.com',
|
|
219
|
+
description: 'User email address',
|
|
220
|
+
})
|
|
221
|
+
@IsEmail()
|
|
222
|
+
email: string;
|
|
223
|
+
|
|
224
|
+
@ApiProperty({
|
|
225
|
+
minLength: 8,
|
|
226
|
+
description: 'User password (min 8 characters)',
|
|
227
|
+
})
|
|
228
|
+
@IsString()
|
|
229
|
+
@MinLength(8)
|
|
230
|
+
password: string;
|
|
231
|
+
|
|
232
|
+
@ApiPropertyOptional({
|
|
233
|
+
example: 'John Doe',
|
|
234
|
+
maxLength: 50,
|
|
235
|
+
})
|
|
236
|
+
@IsString()
|
|
237
|
+
@IsOptional()
|
|
238
|
+
@MaxLength(50)
|
|
239
|
+
name?: string;
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Response DTOs
|
|
244
|
+
|
|
245
|
+
Use separate DTOs for responses to control exposed data:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { Exclude, Expose, Type } from 'class-transformer';
|
|
249
|
+
|
|
250
|
+
export class UserResponseDto {
|
|
251
|
+
@Expose()
|
|
252
|
+
id: string;
|
|
253
|
+
|
|
254
|
+
@Expose()
|
|
255
|
+
email: string;
|
|
256
|
+
|
|
257
|
+
@Expose()
|
|
258
|
+
name: string;
|
|
259
|
+
|
|
260
|
+
@Exclude()
|
|
261
|
+
password: string;
|
|
262
|
+
|
|
263
|
+
@Expose()
|
|
264
|
+
@Type(() => Date)
|
|
265
|
+
createdAt: Date;
|
|
266
|
+
|
|
267
|
+
constructor(partial: Partial<UserResponseDto>) {
|
|
268
|
+
Object.assign(this, partial);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Service usage with ClassSerializerInterceptor
|
|
273
|
+
@UseInterceptors(ClassSerializerInterceptor)
|
|
274
|
+
@Get(':id')
|
|
275
|
+
async findOne(@Param('id') id: string): Promise<UserResponseDto> {
|
|
276
|
+
const user = await this.usersService.findOne(id);
|
|
277
|
+
return new UserResponseDto(user);
|
|
278
|
+
}
|
|
279
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(npm run start:dev)",
|
|
5
|
+
"Bash(npm run build)",
|
|
6
|
+
"Bash(npm run test*)",
|
|
7
|
+
"Bash(npm run lint*)",
|
|
8
|
+
"Bash(npm run format*)",
|
|
9
|
+
"Bash(npx prisma *)",
|
|
10
|
+
"Bash(npx typeorm *)",
|
|
11
|
+
"Bash(npm install *)"
|
|
12
|
+
],
|
|
13
|
+
"deny": []
|
|
14
|
+
}
|
|
15
|
+
}
|