@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,321 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/auth/**/*.ts"
|
|
4
|
+
- "src/**/*.guard.ts"
|
|
5
|
+
- "src/**/*.strategy.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# NestJS Authentication
|
|
9
|
+
|
|
10
|
+
## Passport + JWT Pattern
|
|
11
|
+
|
|
12
|
+
### Module Structure
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
src/modules/auth/
|
|
16
|
+
├── auth.module.ts
|
|
17
|
+
├── auth.controller.ts
|
|
18
|
+
├── auth.service.ts
|
|
19
|
+
├── strategies/
|
|
20
|
+
│ ├── jwt.strategy.ts
|
|
21
|
+
│ └── local.strategy.ts
|
|
22
|
+
├── guards/
|
|
23
|
+
│ ├── jwt-auth.guard.ts
|
|
24
|
+
│ └── local-auth.guard.ts
|
|
25
|
+
├── decorators/
|
|
26
|
+
│ ├── current-user.decorator.ts
|
|
27
|
+
│ └── public.decorator.ts
|
|
28
|
+
└── dto/
|
|
29
|
+
├── login.dto.ts
|
|
30
|
+
└── register.dto.ts
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### JWT Strategy
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { Injectable } from '@nestjs/common';
|
|
37
|
+
import { PassportStrategy } from '@nestjs/passport';
|
|
38
|
+
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
39
|
+
import { ConfigService } from '@nestjs/config';
|
|
40
|
+
|
|
41
|
+
export interface JwtPayload {
|
|
42
|
+
sub: string;
|
|
43
|
+
email: string;
|
|
44
|
+
role: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Injectable()
|
|
48
|
+
export class JwtStrategy extends PassportStrategy(Strategy) {
|
|
49
|
+
constructor(private readonly configService: ConfigService) {
|
|
50
|
+
super({
|
|
51
|
+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
52
|
+
ignoreExpiration: false,
|
|
53
|
+
secretOrKey: configService.getOrThrow<string>('JWT_SECRET'),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
validate(payload: JwtPayload) {
|
|
58
|
+
// Returned value is attached to request.user
|
|
59
|
+
return {
|
|
60
|
+
id: payload.sub,
|
|
61
|
+
email: payload.email,
|
|
62
|
+
role: payload.role,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Local Strategy (Login)
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
72
|
+
import { PassportStrategy } from '@nestjs/passport';
|
|
73
|
+
import { Strategy } from 'passport-local';
|
|
74
|
+
import { AuthService } from '../auth.service';
|
|
75
|
+
|
|
76
|
+
@Injectable()
|
|
77
|
+
export class LocalStrategy extends PassportStrategy(Strategy) {
|
|
78
|
+
constructor(private readonly authService: AuthService) {
|
|
79
|
+
super({
|
|
80
|
+
usernameField: 'email', // Use email instead of username
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async validate(email: string, password: string) {
|
|
85
|
+
const user = await this.authService.validateUser(email, password);
|
|
86
|
+
if (!user) {
|
|
87
|
+
throw new UnauthorizedException('Invalid credentials');
|
|
88
|
+
}
|
|
89
|
+
return user;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Auth Service
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
98
|
+
import { JwtService } from '@nestjs/jwt';
|
|
99
|
+
import { UsersService } from '../users/users.service';
|
|
100
|
+
import * as bcrypt from 'bcrypt';
|
|
101
|
+
|
|
102
|
+
@Injectable()
|
|
103
|
+
export class AuthService {
|
|
104
|
+
constructor(
|
|
105
|
+
private readonly usersService: UsersService,
|
|
106
|
+
private readonly jwtService: JwtService,
|
|
107
|
+
) {}
|
|
108
|
+
|
|
109
|
+
async validateUser(email: string, password: string) {
|
|
110
|
+
const user = await this.usersService.findByEmail(email);
|
|
111
|
+
if (!user) return null;
|
|
112
|
+
|
|
113
|
+
const isValid = await bcrypt.compare(password, user.password);
|
|
114
|
+
if (!isValid) return null;
|
|
115
|
+
|
|
116
|
+
const { password: _, ...result } = user;
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async login(user: { id: string; email: string; role: string }) {
|
|
121
|
+
const payload: JwtPayload = {
|
|
122
|
+
sub: user.id,
|
|
123
|
+
email: user.email,
|
|
124
|
+
role: user.role,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
accessToken: this.jwtService.sign(payload),
|
|
129
|
+
user: {
|
|
130
|
+
id: user.id,
|
|
131
|
+
email: user.email,
|
|
132
|
+
role: user.role,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async register(dto: RegisterDto) {
|
|
138
|
+
const hashedPassword = await bcrypt.hash(dto.password, 10);
|
|
139
|
+
const user = await this.usersService.create({
|
|
140
|
+
...dto,
|
|
141
|
+
password: hashedPassword,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return this.login(user);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Guards
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// jwt-auth.guard.ts
|
|
153
|
+
import { Injectable, ExecutionContext } from '@nestjs/common';
|
|
154
|
+
import { AuthGuard } from '@nestjs/passport';
|
|
155
|
+
import { Reflector } from '@nestjs/core';
|
|
156
|
+
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
|
157
|
+
|
|
158
|
+
@Injectable()
|
|
159
|
+
export class JwtAuthGuard extends AuthGuard('jwt') {
|
|
160
|
+
constructor(private reflector: Reflector) {
|
|
161
|
+
super();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
canActivate(context: ExecutionContext) {
|
|
165
|
+
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
|
166
|
+
context.getHandler(),
|
|
167
|
+
context.getClass(),
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
if (isPublic) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return super.canActivate(context);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// local-auth.guard.ts
|
|
179
|
+
import { Injectable } from '@nestjs/common';
|
|
180
|
+
import { AuthGuard } from '@nestjs/passport';
|
|
181
|
+
|
|
182
|
+
@Injectable()
|
|
183
|
+
export class LocalAuthGuard extends AuthGuard('local') {}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Custom Decorators
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// public.decorator.ts
|
|
190
|
+
import { SetMetadata } from '@nestjs/common';
|
|
191
|
+
|
|
192
|
+
export const IS_PUBLIC_KEY = 'isPublic';
|
|
193
|
+
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
|
194
|
+
|
|
195
|
+
// current-user.decorator.ts
|
|
196
|
+
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
|
197
|
+
|
|
198
|
+
export const CurrentUser = createParamDecorator(
|
|
199
|
+
(data: string | undefined, ctx: ExecutionContext) => {
|
|
200
|
+
const request = ctx.switchToHttp().getRequest();
|
|
201
|
+
const user = request.user;
|
|
202
|
+
return data ? user?.[data] : user;
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// roles.decorator.ts
|
|
207
|
+
import { SetMetadata } from '@nestjs/common';
|
|
208
|
+
|
|
209
|
+
export const ROLES_KEY = 'roles';
|
|
210
|
+
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Roles Guard
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
|
217
|
+
import { Reflector } from '@nestjs/core';
|
|
218
|
+
import { ROLES_KEY } from '../decorators/roles.decorator';
|
|
219
|
+
|
|
220
|
+
@Injectable()
|
|
221
|
+
export class RolesGuard implements CanActivate {
|
|
222
|
+
constructor(private reflector: Reflector) {}
|
|
223
|
+
|
|
224
|
+
canActivate(context: ExecutionContext): boolean {
|
|
225
|
+
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
|
|
226
|
+
ROLES_KEY,
|
|
227
|
+
[context.getHandler(), context.getClass()],
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (!requiredRoles) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const { user } = context.switchToHttp().getRequest();
|
|
235
|
+
return requiredRoles.includes(user.role);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Controller Usage
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
@Controller('auth')
|
|
244
|
+
export class AuthController {
|
|
245
|
+
constructor(private readonly authService: AuthService) {}
|
|
246
|
+
|
|
247
|
+
@Public()
|
|
248
|
+
@UseGuards(LocalAuthGuard)
|
|
249
|
+
@Post('login')
|
|
250
|
+
login(@CurrentUser() user) {
|
|
251
|
+
return this.authService.login(user);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@Public()
|
|
255
|
+
@Post('register')
|
|
256
|
+
register(@Body() dto: RegisterDto) {
|
|
257
|
+
return this.authService.register(dto);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@Get('profile')
|
|
261
|
+
getProfile(@CurrentUser() user) {
|
|
262
|
+
return user;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@Roles('admin')
|
|
266
|
+
@UseGuards(RolesGuard)
|
|
267
|
+
@Get('admin')
|
|
268
|
+
adminOnly(@CurrentUser() user) {
|
|
269
|
+
return { message: 'Admin access granted', user };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Module Configuration
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
@Module({
|
|
278
|
+
imports: [
|
|
279
|
+
UsersModule,
|
|
280
|
+
PassportModule,
|
|
281
|
+
JwtModule.registerAsync({
|
|
282
|
+
inject: [ConfigService],
|
|
283
|
+
useFactory: (config: ConfigService) => ({
|
|
284
|
+
secret: config.getOrThrow('JWT_SECRET'),
|
|
285
|
+
signOptions: {
|
|
286
|
+
expiresIn: config.get('JWT_EXPIRES_IN', '1d'),
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
}),
|
|
290
|
+
],
|
|
291
|
+
controllers: [AuthController],
|
|
292
|
+
providers: [AuthService, JwtStrategy, LocalStrategy],
|
|
293
|
+
exports: [AuthService],
|
|
294
|
+
})
|
|
295
|
+
export class AuthModule {}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Global Guard Setup (app.module.ts)
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
302
|
+
|
|
303
|
+
@Module({
|
|
304
|
+
providers: [
|
|
305
|
+
{
|
|
306
|
+
provide: APP_GUARD,
|
|
307
|
+
useClass: JwtAuthGuard,
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
})
|
|
311
|
+
export class AppModule {}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Security Best Practices
|
|
315
|
+
|
|
316
|
+
- Never store plain passwords - always use bcrypt
|
|
317
|
+
- Use environment variables for secrets
|
|
318
|
+
- Set appropriate JWT expiration times
|
|
319
|
+
- Implement refresh token rotation for long sessions
|
|
320
|
+
- Rate limit authentication endpoints
|
|
321
|
+
- Log failed authentication attempts
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "prisma/**/*.prisma"
|
|
4
|
+
- "src/**/*.repository.ts"
|
|
5
|
+
- "src/**/prisma*.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# NestJS with Prisma
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
### Prisma Module
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// prisma/prisma.module.ts
|
|
16
|
+
import { Global, Module } from '@nestjs/common';
|
|
17
|
+
import { PrismaService } from './prisma.service';
|
|
18
|
+
|
|
19
|
+
@Global()
|
|
20
|
+
@Module({
|
|
21
|
+
providers: [PrismaService],
|
|
22
|
+
exports: [PrismaService],
|
|
23
|
+
})
|
|
24
|
+
export class PrismaModule {}
|
|
25
|
+
|
|
26
|
+
// prisma/prisma.service.ts
|
|
27
|
+
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
|
28
|
+
import { PrismaClient } from '@prisma/client';
|
|
29
|
+
|
|
30
|
+
@Injectable()
|
|
31
|
+
export class PrismaService
|
|
32
|
+
extends PrismaClient
|
|
33
|
+
implements OnModuleInit, OnModuleDestroy
|
|
34
|
+
{
|
|
35
|
+
async onModuleInit() {
|
|
36
|
+
await this.$connect();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async onModuleDestroy() {
|
|
40
|
+
await this.$disconnect();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Schema Design
|
|
46
|
+
|
|
47
|
+
### Model Conventions
|
|
48
|
+
|
|
49
|
+
```prisma
|
|
50
|
+
// prisma/schema.prisma
|
|
51
|
+
|
|
52
|
+
generator client {
|
|
53
|
+
provider = "prisma-client-js"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
datasource db {
|
|
57
|
+
provider = "postgresql"
|
|
58
|
+
url = env("DATABASE_URL")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
model User {
|
|
62
|
+
id String @id @default(uuid())
|
|
63
|
+
email String @unique
|
|
64
|
+
password String
|
|
65
|
+
name String?
|
|
66
|
+
role Role @default(USER)
|
|
67
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
68
|
+
updatedAt DateTime @updatedAt @map("updated_at")
|
|
69
|
+
|
|
70
|
+
posts Post[]
|
|
71
|
+
profile Profile?
|
|
72
|
+
|
|
73
|
+
@@map("users")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
model Post {
|
|
77
|
+
id String @id @default(uuid())
|
|
78
|
+
title String
|
|
79
|
+
content String?
|
|
80
|
+
published Boolean @default(false)
|
|
81
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
82
|
+
updatedAt DateTime @updatedAt @map("updated_at")
|
|
83
|
+
|
|
84
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
85
|
+
authorId String @map("author_id")
|
|
86
|
+
|
|
87
|
+
categories Category[]
|
|
88
|
+
|
|
89
|
+
@@index([authorId])
|
|
90
|
+
@@map("posts")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
enum Role {
|
|
94
|
+
USER
|
|
95
|
+
ADMIN
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Naming Conventions
|
|
100
|
+
|
|
101
|
+
- Models: PascalCase (`User`, `BlogPost`)
|
|
102
|
+
- Fields: camelCase (`createdAt`, `authorId`)
|
|
103
|
+
- Database tables: snake_case via `@@map("users")`
|
|
104
|
+
- Database columns: snake_case via `@map("created_at")`
|
|
105
|
+
|
|
106
|
+
## Repository Pattern
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// users/users.repository.ts
|
|
110
|
+
import { Injectable } from '@nestjs/common';
|
|
111
|
+
import { PrismaService } from '../prisma/prisma.service';
|
|
112
|
+
import { Prisma, User } from '@prisma/client';
|
|
113
|
+
|
|
114
|
+
@Injectable()
|
|
115
|
+
export class UsersRepository {
|
|
116
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
117
|
+
|
|
118
|
+
async findById(id: string): Promise<User | null> {
|
|
119
|
+
return this.prisma.user.findUnique({
|
|
120
|
+
where: { id },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
125
|
+
return this.prisma.user.findUnique({
|
|
126
|
+
where: { email },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async findMany(params: {
|
|
131
|
+
skip?: number;
|
|
132
|
+
take?: number;
|
|
133
|
+
where?: Prisma.UserWhereInput;
|
|
134
|
+
orderBy?: Prisma.UserOrderByWithRelationInput;
|
|
135
|
+
}): Promise<User[]> {
|
|
136
|
+
const { skip, take, where, orderBy } = params;
|
|
137
|
+
return this.prisma.user.findMany({
|
|
138
|
+
skip,
|
|
139
|
+
take,
|
|
140
|
+
where,
|
|
141
|
+
orderBy,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async create(data: Prisma.UserCreateInput): Promise<User> {
|
|
146
|
+
return this.prisma.user.create({ data });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async update(id: string, data: Prisma.UserUpdateInput): Promise<User> {
|
|
150
|
+
return this.prisma.user.update({
|
|
151
|
+
where: { id },
|
|
152
|
+
data,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async delete(id: string): Promise<void> {
|
|
157
|
+
await this.prisma.user.delete({ where: { id } });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Query Patterns
|
|
163
|
+
|
|
164
|
+
### Select Specific Fields
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// Return only needed fields
|
|
168
|
+
const user = await this.prisma.user.findUnique({
|
|
169
|
+
where: { id },
|
|
170
|
+
select: {
|
|
171
|
+
id: true,
|
|
172
|
+
email: true,
|
|
173
|
+
name: true,
|
|
174
|
+
// password excluded
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Include Relations
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
const userWithPosts = await this.prisma.user.findUnique({
|
|
183
|
+
where: { id },
|
|
184
|
+
include: {
|
|
185
|
+
posts: {
|
|
186
|
+
where: { published: true },
|
|
187
|
+
orderBy: { createdAt: 'desc' },
|
|
188
|
+
take: 10,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Pagination
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
async findPaginated(page: number, limit: number) {
|
|
198
|
+
const [data, total] = await Promise.all([
|
|
199
|
+
this.prisma.user.findMany({
|
|
200
|
+
skip: (page - 1) * limit,
|
|
201
|
+
take: limit,
|
|
202
|
+
orderBy: { createdAt: 'desc' },
|
|
203
|
+
}),
|
|
204
|
+
this.prisma.user.count(),
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
data,
|
|
209
|
+
meta: {
|
|
210
|
+
total,
|
|
211
|
+
page,
|
|
212
|
+
limit,
|
|
213
|
+
totalPages: Math.ceil(total / limit),
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Transactions
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
async transferCredits(fromId: string, toId: string, amount: number) {
|
|
223
|
+
return this.prisma.$transaction(async (tx) => {
|
|
224
|
+
const sender = await tx.user.update({
|
|
225
|
+
where: { id: fromId },
|
|
226
|
+
data: { credits: { decrement: amount } },
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (sender.credits < 0) {
|
|
230
|
+
throw new Error('Insufficient credits');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
await tx.user.update({
|
|
234
|
+
where: { id: toId },
|
|
235
|
+
data: { credits: { increment: amount } },
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return { success: true };
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Soft Delete Pattern
|
|
244
|
+
|
|
245
|
+
```prisma
|
|
246
|
+
model User {
|
|
247
|
+
id String @id @default(uuid())
|
|
248
|
+
deletedAt DateTime? @map("deleted_at")
|
|
249
|
+
// ...
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// Middleware approach
|
|
255
|
+
this.prisma.$use(async (params, next) => {
|
|
256
|
+
if (params.model === 'User') {
|
|
257
|
+
if (params.action === 'delete') {
|
|
258
|
+
params.action = 'update';
|
|
259
|
+
params.args['data'] = { deletedAt: new Date() };
|
|
260
|
+
}
|
|
261
|
+
if (params.action === 'findMany' || params.action === 'findFirst') {
|
|
262
|
+
params.args['where'] = {
|
|
263
|
+
...params.args['where'],
|
|
264
|
+
deletedAt: null,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return next(params);
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Migrations
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
# Create migration
|
|
276
|
+
npx prisma migrate dev --name add_users_table
|
|
277
|
+
|
|
278
|
+
# Apply migrations (production)
|
|
279
|
+
npx prisma migrate deploy
|
|
280
|
+
|
|
281
|
+
# Reset database (dev only)
|
|
282
|
+
npx prisma migrate reset
|
|
283
|
+
|
|
284
|
+
# Generate client after schema change
|
|
285
|
+
npx prisma generate
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Testing with Prisma
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
// Mock PrismaService
|
|
292
|
+
const mockPrismaService = {
|
|
293
|
+
user: {
|
|
294
|
+
findUnique: jest.fn(),
|
|
295
|
+
findMany: jest.fn(),
|
|
296
|
+
create: jest.fn(),
|
|
297
|
+
update: jest.fn(),
|
|
298
|
+
delete: jest.fn(),
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// E2E: Use test database
|
|
303
|
+
// .env.test
|
|
304
|
+
DATABASE_URL="postgresql://localhost:5432/myapp_test"
|
|
305
|
+
```
|