opencode-andev-rules 1.0.0 → 1.0.2
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/LICENSE +21 -0
- package/README.md +47 -37
- package/package.json +28 -14
- package/skills/backend/nestJS/SKILL.md +255 -0
- package/skills/backend/nestJS/references/nestjs-module.md +212 -0
- package/skills/backend/nestJS/references/nestjs-testing.md +395 -0
- package/skills/infrastructure/prisma/SKILL.md +65 -0
- package/skills/infrastructure/prisma/references/prisma-queries.md +197 -0
- package/skills/infrastructure/prisma/references/prisma-schema.md +126 -0
- package/skills/registry.json +46 -4
- package/RULES.md +0 -253
- package/plugin.js +0 -96
- /package/skills/{improve-codenase-systems → enhance/improve-codenase-systems}/HTML-REPORT.md +0 -0
- /package/skills/{improve-codenase-systems → enhance/improve-codenase-systems}/SKILL.md +0 -0
- /package/skills/{improve-codenase-systems → enhance/improve-codenase-systems}/agents/openai.yaml +0 -0
- /package/skills/{improve-codenase-systems → enhance/improve-codenase-systems}/meta.json +0 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# NestJS Testing Strategies
|
|
2
|
+
|
|
3
|
+
## Test Types
|
|
4
|
+
|
|
5
|
+
### Test Pyramid
|
|
6
|
+
```
|
|
7
|
+
╱╲
|
|
8
|
+
╱ E2E ╲
|
|
9
|
+
╱────────╲
|
|
10
|
+
╱ Integration ╲
|
|
11
|
+
╱────────────────╲
|
|
12
|
+
╱ Unit Tests ╲
|
|
13
|
+
╱──────────────────╲
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Type | Speed | Scope | Confidence |
|
|
17
|
+
|------|-------|-------|------------|
|
|
18
|
+
| Unit | Fast | Single class/module | Low |
|
|
19
|
+
| Integration | Medium | Module interactions | Medium |
|
|
20
|
+
| E2E | Slow | Full system | High |
|
|
21
|
+
|
|
22
|
+
## Unit Testing
|
|
23
|
+
|
|
24
|
+
### Service Test
|
|
25
|
+
```typescript
|
|
26
|
+
import { Test } from '@nestjs/testing';
|
|
27
|
+
import { UserService } from './user.service';
|
|
28
|
+
import { UserRepository } from './user.repository';
|
|
29
|
+
import { EmailService } from './email.service';
|
|
30
|
+
|
|
31
|
+
describe('UserService', () => {
|
|
32
|
+
let service: UserService;
|
|
33
|
+
let userRepo: jest.Mocked<UserRepository>;
|
|
34
|
+
let emailService: jest.Mocked<EmailService>;
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
const module = await Test.createTestingModule({
|
|
38
|
+
providers: [
|
|
39
|
+
UserService,
|
|
40
|
+
{
|
|
41
|
+
provide: UserRepository,
|
|
42
|
+
useValue: {
|
|
43
|
+
findById: jest.fn(),
|
|
44
|
+
save: jest.fn(),
|
|
45
|
+
delete: jest.fn(),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
provide: EmailService,
|
|
50
|
+
useValue: {
|
|
51
|
+
sendWelcomeEmail: jest.fn(),
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
}).compile();
|
|
56
|
+
|
|
57
|
+
service = module.get(UserService);
|
|
58
|
+
userRepo = module.get(UserRepository);
|
|
59
|
+
emailService = module.get(EmailService);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('createUser', () => {
|
|
63
|
+
it('should create a user and send welcome email', async () => {
|
|
64
|
+
const dto = { email: 'test@example.com', name: 'Test' };
|
|
65
|
+
const expectedUser = { id: '1', ...dto };
|
|
66
|
+
|
|
67
|
+
userRepo.save.mockResolvedValue(expectedUser);
|
|
68
|
+
|
|
69
|
+
const result = await service.createUser(dto);
|
|
70
|
+
|
|
71
|
+
expect(result).toEqual(expectedUser);
|
|
72
|
+
expect(userRepo.save).toHaveBeenCalledWith(expect.objectContaining(dto));
|
|
73
|
+
expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith(expectedUser.id, dto.email);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should throw if email already exists', async () => {
|
|
77
|
+
userRepo.findByEmail.mockResolvedValue({ id: '1', email: 'exists@example.com' });
|
|
78
|
+
|
|
79
|
+
await expect(
|
|
80
|
+
service.createUser({ email: 'exists@example.com', name: 'Test' }),
|
|
81
|
+
).rejects.toThrow('Email already exists');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Controller Test
|
|
88
|
+
```typescript
|
|
89
|
+
import { Test } from '@nestjs/testing';
|
|
90
|
+
import { UserController } from './user.controller';
|
|
91
|
+
import { UserService } from './user.service';
|
|
92
|
+
|
|
93
|
+
describe('UserController', () => {
|
|
94
|
+
let controller: UserController;
|
|
95
|
+
let service: jest.Mocked<UserService>;
|
|
96
|
+
|
|
97
|
+
beforeEach(async () => {
|
|
98
|
+
const module = await Test.createTestingModule({
|
|
99
|
+
controllers: [UserController],
|
|
100
|
+
providers: [
|
|
101
|
+
{
|
|
102
|
+
provide: UserService,
|
|
103
|
+
useValue: {
|
|
104
|
+
createUser: jest.fn(),
|
|
105
|
+
findById: jest.fn(),
|
|
106
|
+
findAll: jest.fn(),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
}).compile();
|
|
111
|
+
|
|
112
|
+
controller = module.get(UserController);
|
|
113
|
+
service = module.get(UserService);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should return created user', async () => {
|
|
117
|
+
const dto = { email: 'test@example.com', name: 'Test' };
|
|
118
|
+
const expected = { id: '1', ...dto };
|
|
119
|
+
|
|
120
|
+
service.createUser.mockResolvedValue(expected);
|
|
121
|
+
|
|
122
|
+
const result = await controller.create(dto);
|
|
123
|
+
|
|
124
|
+
expect(result).toEqual(expected);
|
|
125
|
+
expect(result).not.toHaveProperty('password');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Integration Testing
|
|
131
|
+
|
|
132
|
+
### Database Integration
|
|
133
|
+
```typescript
|
|
134
|
+
import { Test } from '@nestjs/testing';
|
|
135
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
136
|
+
import { UserService } from './user.service';
|
|
137
|
+
import { User } from './user.entity';
|
|
138
|
+
import { UserModule } from './user.module';
|
|
139
|
+
import { createTestDatabase } from '../test/utils/database';
|
|
140
|
+
|
|
141
|
+
describe('UserService Integration', () => {
|
|
142
|
+
let service: UserService;
|
|
143
|
+
let module: TestingModule;
|
|
144
|
+
|
|
145
|
+
beforeAll(async () => {
|
|
146
|
+
const database = await createTestDatabase();
|
|
147
|
+
|
|
148
|
+
module = await Test.createTestingModule({
|
|
149
|
+
imports: [
|
|
150
|
+
TypeOrmModule.forRoot(database.config),
|
|
151
|
+
UserModule,
|
|
152
|
+
],
|
|
153
|
+
}).compile();
|
|
154
|
+
|
|
155
|
+
service = module.get(UserService);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
afterAll(async () => {
|
|
159
|
+
await module.close();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
beforeEach(async () => {
|
|
163
|
+
await cleanDatabase();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should persist user to database', async () => {
|
|
167
|
+
const user = await service.createUser({
|
|
168
|
+
email: 'test@example.com',
|
|
169
|
+
name: 'Test User',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const found = await service.findById(user.id);
|
|
173
|
+
expect(found).toBeDefined();
|
|
174
|
+
expect(found.email).toBe('test@example.com');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should enforce unique email constraint', async () => {
|
|
178
|
+
await service.createUser({
|
|
179
|
+
email: 'dupe@example.com',
|
|
180
|
+
name: 'First',
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await expect(
|
|
184
|
+
service.createUser({
|
|
185
|
+
email: 'dupe@example.com',
|
|
186
|
+
name: 'Second',
|
|
187
|
+
}),
|
|
188
|
+
).rejects.toThrow();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### HTTP Integration
|
|
194
|
+
```typescript
|
|
195
|
+
import * as request from 'supertest';
|
|
196
|
+
import { Test } from '@nestjs/testing';
|
|
197
|
+
import { INestApplication } from '@nestjs/common';
|
|
198
|
+
import { AppModule } from '../src/app.module';
|
|
199
|
+
|
|
200
|
+
describe('User API (integration)', () => {
|
|
201
|
+
let app: INestApplication;
|
|
202
|
+
|
|
203
|
+
beforeAll(async () => {
|
|
204
|
+
const module = await Test.createTestingModule({
|
|
205
|
+
imports: [AppModule],
|
|
206
|
+
}).compile();
|
|
207
|
+
|
|
208
|
+
app = module.createNestApplication();
|
|
209
|
+
await app.init();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
afterAll(async () => {
|
|
213
|
+
await app.close();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('POST /users', () => {
|
|
217
|
+
it('should create user', async () => {
|
|
218
|
+
const response = await request(app.getHttpServer())
|
|
219
|
+
.post('/users')
|
|
220
|
+
.send({ email: 'test@example.com', name: 'Test' })
|
|
221
|
+
.expect(201);
|
|
222
|
+
|
|
223
|
+
expect(response.body).toHaveProperty('id');
|
|
224
|
+
expect(response.body.email).toBe('test@example.com');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should reject invalid email', async () => {
|
|
228
|
+
await request(app.getHttpServer())
|
|
229
|
+
.post('/users')
|
|
230
|
+
.send({ email: 'invalid', name: 'Test' })
|
|
231
|
+
.expect(400);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## E2E Testing
|
|
238
|
+
|
|
239
|
+
### Full E2E Test
|
|
240
|
+
```typescript
|
|
241
|
+
import * as request from 'supertest';
|
|
242
|
+
import { Test } from '@nestjs/testing';
|
|
243
|
+
import { INestApplication } from '@nestjs/common';
|
|
244
|
+
import { AppModule } from '../src/app.module';
|
|
245
|
+
|
|
246
|
+
describe('User E2E', () => {
|
|
247
|
+
let app: INestApplication;
|
|
248
|
+
let authToken: string;
|
|
249
|
+
|
|
250
|
+
beforeAll(async () => {
|
|
251
|
+
const module = await Test.createTestingModule({
|
|
252
|
+
imports: [AppModule],
|
|
253
|
+
}).compile();
|
|
254
|
+
|
|
255
|
+
app = module.createNestApplication();
|
|
256
|
+
await app.init();
|
|
257
|
+
|
|
258
|
+
// Auth
|
|
259
|
+
const loginRes = await request(app.getHttpServer())
|
|
260
|
+
.post('/auth/login')
|
|
261
|
+
.send({ email: 'admin@test.com', password: 'admin123' });
|
|
262
|
+
|
|
263
|
+
authToken = loginRes.body.accessToken;
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
afterAll(async () => {
|
|
267
|
+
await app.close();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('full user lifecycle', async () => {
|
|
271
|
+
// Create
|
|
272
|
+
const createRes = await request(app.getHttpServer())
|
|
273
|
+
.post('/users')
|
|
274
|
+
.set('Authorization', `Bearer ${authToken}`)
|
|
275
|
+
.send({ email: 'new@test.com', name: 'New User' })
|
|
276
|
+
.expect(201);
|
|
277
|
+
|
|
278
|
+
const userId = createRes.body.id;
|
|
279
|
+
|
|
280
|
+
// Read
|
|
281
|
+
await request(app.getHttpServer())
|
|
282
|
+
.get(`/users/${userId}`)
|
|
283
|
+
.set('Authorization', `Bearer ${authToken}`)
|
|
284
|
+
.expect(200);
|
|
285
|
+
|
|
286
|
+
// List
|
|
287
|
+
const listRes = await request(app.getHttpServer())
|
|
288
|
+
.get('/users')
|
|
289
|
+
.set('Authorization', `Bearer ${authToken}`)
|
|
290
|
+
.expect(200);
|
|
291
|
+
|
|
292
|
+
expect(listRes.body.data.length).toBeGreaterThan(0);
|
|
293
|
+
|
|
294
|
+
// Delete
|
|
295
|
+
await request(app.getHttpServer())
|
|
296
|
+
.delete(`/users/${userId}`)
|
|
297
|
+
.set('Authorization', `Bearer ${authToken}`)
|
|
298
|
+
.expect(204);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Test Database Utilities
|
|
304
|
+
|
|
305
|
+
### Test Database Setup
|
|
306
|
+
```typescript
|
|
307
|
+
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
|
308
|
+
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
|
|
309
|
+
|
|
310
|
+
export async function createTestDatabase(): Promise<{
|
|
311
|
+
container: StartedPostgreSqlContainer;
|
|
312
|
+
config: TypeOrmModuleOptions;
|
|
313
|
+
}> {
|
|
314
|
+
const container = await new PostgreSqlContainer()
|
|
315
|
+
.withDatabase('test')
|
|
316
|
+
.withUsername('test')
|
|
317
|
+
.withPassword('test')
|
|
318
|
+
.start();
|
|
319
|
+
|
|
320
|
+
const config: TypeOrmModuleOptions = {
|
|
321
|
+
type: 'postgres',
|
|
322
|
+
host: container.getHost(),
|
|
323
|
+
port: container.getMappedPort(5432),
|
|
324
|
+
username: container.getUsername(),
|
|
325
|
+
password: container.getPassword(),
|
|
326
|
+
database: container.getDatabase(),
|
|
327
|
+
entities: ['dist/**/*.entity{.ts,.js}'],
|
|
328
|
+
synchronize: true,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
return { container, config };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function cleanDatabase(): Promise<void> {
|
|
335
|
+
const entities = getMetadataArgsStorage().tables.map((t) => t.name);
|
|
336
|
+
for (const entity of entities) {
|
|
337
|
+
await getConnection().manager.query(`TRUNCATE TABLE "${entity}" CASCADE`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Mocking Strategies
|
|
343
|
+
|
|
344
|
+
### Service Mocking
|
|
345
|
+
```typescript
|
|
346
|
+
// Mock factory
|
|
347
|
+
export const createMockUserService = (): jest.Mocked<UserService> => ({
|
|
348
|
+
createUser: jest.fn(),
|
|
349
|
+
findById: jest.fn(),
|
|
350
|
+
findAll: jest.fn(),
|
|
351
|
+
updateUser: jest.fn(),
|
|
352
|
+
deleteUser: jest.fn(),
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Custom provider
|
|
356
|
+
const mockUserService = createMockUserService();
|
|
357
|
+
mockUserService.findById.mockResolvedValue(testUser);
|
|
358
|
+
|
|
359
|
+
const module = await Test.createTestingModule({
|
|
360
|
+
providers: [
|
|
361
|
+
{ provide: UserService, useValue: mockUserService },
|
|
362
|
+
],
|
|
363
|
+
}).compile();
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Guard Mocking
|
|
367
|
+
```typescript
|
|
368
|
+
import { ExecutionContext } from '@nestjs/common';
|
|
369
|
+
|
|
370
|
+
export const mockAuthGuard = {
|
|
371
|
+
canActivate: jest.fn((context: ExecutionContext) => {
|
|
372
|
+
const req = context.switchToHttp().getRequest();
|
|
373
|
+
req.user = { id: 'test-user-id', roles: ['admin'] };
|
|
374
|
+
return true;
|
|
375
|
+
}),
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Override guard in test
|
|
379
|
+
const module = await Test.createTestingModule({
|
|
380
|
+
imports: [UserModule],
|
|
381
|
+
})
|
|
382
|
+
.overrideGuard(AuthGuard('jwt'))
|
|
383
|
+
.useValue(mockAuthGuard)
|
|
384
|
+
.compile();
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Key Points
|
|
388
|
+
- Unit tests mock all dependencies and test single classes in isolation
|
|
389
|
+
- Integration tests use real database (TestContainers) and verify module interactions
|
|
390
|
+
- E2E tests exercise the full HTTP stack including guards, pipes, interceptors
|
|
391
|
+
- TestContainers provides disposable databases for integration testing
|
|
392
|
+
- Clean database between tests to avoid state pollution
|
|
393
|
+
- Guard mocking enables testing authenticated endpoints without tokens
|
|
394
|
+
- Service mocking with jest.Mocked<T> provides type-safe mocks
|
|
395
|
+
- Override providers and guards for controlled test scenarios
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: prisma
|
|
3
|
+
description: Prisma ORM rules — client setup, queries, errors, transactions, migrations. Use when building or reviewing Prisma data access.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Prisma Skill
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
- One PrismaClient singleton for the whole app — never `new PrismaClient()` per request or per service.
|
|
11
|
+
- Provide it once (global module / DI) — services inject it, never instantiate it.
|
|
12
|
+
- Datasource URL from env only — never hardcode connection strings, never log them.
|
|
13
|
+
- Size the pool via `connection_limit`/`pool_timeout` — serverless or hot-reload dev must reuse the singleton or use an external pooler.
|
|
14
|
+
|
|
15
|
+
## Relations: FK/PK decision
|
|
16
|
+
|
|
17
|
+
- Always ask the user before modeling relations: database-enforced foreign keys vs Prisma-emulated relations (`relationMode = "prisma"`).
|
|
18
|
+
- Without DB-level FKs, Prisma can still join via `include` — but nothing enforces referential integrity in the database.
|
|
19
|
+
- With DB FKs: integrity enforced by the database; `onDelete` acts at the DB level; migrations carry the constraints.
|
|
20
|
+
- Without DB FKs: integrity must be enforced in application code (existence checks + transactions); no `onDelete` in the database; fits providers without FK support.
|
|
21
|
+
- Never assume one mode — the choice changes schema, migrations, and error handling; confirm it first.
|
|
22
|
+
|
|
23
|
+
## Queries
|
|
24
|
+
|
|
25
|
+
- Use `select` when only a subset of fields is required — don't over-fetch.
|
|
26
|
+
- Avoid N+1 queries — use `include` or joins, not loops.
|
|
27
|
+
- Avoid `include` for large relation collections — paginate or `select` instead.
|
|
28
|
+
- Prisma fetches all scalar columns by default — with wide tables or heavy columns (text, JSON), always narrow it down.
|
|
29
|
+
- Never blanket `include` — it pulls every column of the related table; nest `select` inside instead.
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// Good — narrow scalars, nested select for relations
|
|
33
|
+
await prisma.user.findMany({
|
|
34
|
+
select: { id: true, name: true, posts: { select: { id: true, title: true } } },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Or drop heavy fields (Prisma 6.2+)
|
|
38
|
+
await prisma.post.findMany({ omit: { content: true } });
|
|
39
|
+
```
|
|
40
|
+
- Use `findUnique` when the field is unique — `findFirst` only otherwise.
|
|
41
|
+
- Paginate `findMany` — never return unbounded results; cursor pagination for large datasets, offset for small.
|
|
42
|
+
- Always `orderBy` deterministic fields for pagination.
|
|
43
|
+
- Run independent queries in parallel — don't `await` sequentially.
|
|
44
|
+
- Never raw SQL unless performance-critical (comment why) — never concatenate values into queries, use parameterized inputs.
|
|
45
|
+
|
|
46
|
+
## Errors
|
|
47
|
+
|
|
48
|
+
- Map Prisma errors to domain exceptions in one place (`P2002` → conflict, `P2025` → not found, ...) — never leak raw Prisma errors to API responses.
|
|
49
|
+
|
|
50
|
+
## Transactions
|
|
51
|
+
|
|
52
|
+
- Multi-step writes go in `$transaction` — no partial writes.
|
|
53
|
+
- Keep transaction scope small — no external calls inside transactions.
|
|
54
|
+
- Race conditions on read-modify-write — use transactions or locks.
|
|
55
|
+
|
|
56
|
+
## Migrations
|
|
57
|
+
|
|
58
|
+
- Keep migrations in `prisma/migrations` and commit them — never edit an applied migration, create a new one instead.
|
|
59
|
+
- Deploy with `migrate deploy` — never `migrate dev` against prod.
|
|
60
|
+
- Review destructive changes (drop column/table, type narrowing) before applying — backfill via a data migration when rows exist.
|
|
61
|
+
|
|
62
|
+
## References
|
|
63
|
+
|
|
64
|
+
- Schema layout, models, indexes, relations, enums: `references/prisma-schema.md`.
|
|
65
|
+
- CLI, CRUD, optimization catalog: `references/prisma-queries.md`.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Prisma Queries & Operations
|
|
2
|
+
|
|
3
|
+
Samples assume the global singleton `prisma` (see `SKILL.md` Setup) — never `new PrismaClient()` per call.
|
|
4
|
+
|
|
5
|
+
## CLI
|
|
6
|
+
|
|
7
|
+
| Command | Use |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `prisma generate` | Regenerate Prisma Client |
|
|
10
|
+
| `prisma migrate dev` | Create and apply migrations in dev |
|
|
11
|
+
| `prisma migrate deploy` | Apply migrations in production/CI |
|
|
12
|
+
| `prisma db push` | Push schema straight to DB, no migration (prototypes only) |
|
|
13
|
+
| `prisma db pull` | Introspect an existing DB into the schema |
|
|
14
|
+
| `prisma studio` | Open the GUI |
|
|
15
|
+
|
|
16
|
+
## CRUD
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// Create
|
|
20
|
+
await prisma.user.create({ data: { email: 'a@b.com', name: 'An' } });
|
|
21
|
+
|
|
22
|
+
// Read
|
|
23
|
+
await prisma.user.findUnique({ where: { email: 'a@b.com' } });
|
|
24
|
+
await prisma.user.findMany({
|
|
25
|
+
where: { name: { contains: 'An' } },
|
|
26
|
+
orderBy: { id: 'desc' },
|
|
27
|
+
take: 10,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Update / Delete
|
|
31
|
+
await prisma.user.update({ where: { id: 'user_01' }, data: { name: 'Bình' } });
|
|
32
|
+
await prisma.user.delete({ where: { id: 'user_01' } });
|
|
33
|
+
|
|
34
|
+
// Relations — narrow, never blanket include
|
|
35
|
+
await prisma.user.findMany({
|
|
36
|
+
select: { id: true, name: true, posts: { select: { id: true, title: true } } },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Transaction (independent writes)
|
|
40
|
+
await prisma.$transaction([
|
|
41
|
+
prisma.user.create({ data: { email: 'x@y.com', name: 'X' } }),
|
|
42
|
+
prisma.post.create({ data: { title: 'Hi', authorId: 'user_01' } }),
|
|
43
|
+
]);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Optimization
|
|
47
|
+
|
|
48
|
+
### 1. Select only what you need (`select` / `omit`)
|
|
49
|
+
|
|
50
|
+
- Same rule as `SKILL.md` Queries — full example there.
|
|
51
|
+
- Never blanket `include` — it pulls every column of the related table.
|
|
52
|
+
|
|
53
|
+
### 2. Avoid N+1 queries
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// Wrong (N+1)
|
|
57
|
+
const users = await prisma.user.findMany();
|
|
58
|
+
for (const u of users) {
|
|
59
|
+
u.posts = await prisma.post.findMany({ where: { authorId: u.id } });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Right — one round trip, narrowed
|
|
63
|
+
const users = await prisma.user.findMany({
|
|
64
|
+
select: { id: true, name: true, posts: { select: { id: true, title: true } } },
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- `relationLoadStrategy: 'join'` lets Prisma use one JOIN instead of separate queries — check preview/GA status for your Prisma version.
|
|
69
|
+
- Simultaneous `findUnique` calls in the same tick are auto-batched (dataloader) — useful for GraphQL resolvers.
|
|
70
|
+
|
|
71
|
+
### 3. Index the right columns
|
|
72
|
+
|
|
73
|
+
```prisma
|
|
74
|
+
model Post {
|
|
75
|
+
// ...
|
|
76
|
+
@@index([authorId]) // FK
|
|
77
|
+
@@index([published, createdAt]) // composite for filter + sort
|
|
78
|
+
@@unique([authorId, slug])
|
|
79
|
+
|
|
80
|
+
@@map("posts")
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- Index columns used in `where`, `orderBy`, joins — column order matters in composite indexes.
|
|
85
|
+
- Never over-index — indexes slow down writes.
|
|
86
|
+
|
|
87
|
+
### 4. Paginate efficiently
|
|
88
|
+
|
|
89
|
+
- Offset (`skip` + `take`) slows down as `skip` grows — the DB still scans skipped rows.
|
|
90
|
+
- Cursor pagination is stable and fast for large datasets:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
await prisma.post.findMany({
|
|
94
|
+
take: 20,
|
|
95
|
+
skip: 1,
|
|
96
|
+
cursor: { id: lastId },
|
|
97
|
+
orderBy: { id: 'asc' },
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 5. Batch writes
|
|
102
|
+
|
|
103
|
+
- Never loop single-row writes — use batch operations:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
await prisma.user.createMany({ data: users, skipDuplicates: true });
|
|
107
|
+
await prisma.post.updateMany({ where: { authorId: 'user_01' }, data: { published: true } });
|
|
108
|
+
await prisma.post.deleteMany({ where: { published: false } });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 6. Connection pooling
|
|
112
|
+
|
|
113
|
+
- Reuse the singleton everywhere — hot-reload dev and serverless must guard it, never create clients per invocation:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
|
|
117
|
+
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
|
|
118
|
+
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- Size the pool via connection string: `?connection_limit=10&pool_timeout=20`.
|
|
122
|
+
- Serverless (Vercel, Lambda): use an external pooler (PgBouncer, Supabase pooler, Prisma Accelerate) — never exhaust direct connections.
|
|
123
|
+
|
|
124
|
+
### 7. Aggregate in the database
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
await prisma.post.groupBy({ by: ['authorId'], _count: { _all: true } });
|
|
128
|
+
await prisma.order.aggregate({ _sum: { total: true }, where: { status: 'PAID' } });
|
|
129
|
+
await prisma.user.findMany({ include: { _count: { select: { posts: true } } } });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- Filter, sort, and count in the DB — never pull everything and `.filter()` in code.
|
|
133
|
+
|
|
134
|
+
### 8. Keep transactions lean
|
|
135
|
+
|
|
136
|
+
- `$transaction([...])` batch form for independent operations.
|
|
137
|
+
- Interactive form only when logic runs between steps — keep it short:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
await prisma.$transaction(
|
|
141
|
+
async (tx) => {
|
|
142
|
+
const acc = await tx.account.update({
|
|
143
|
+
where: { id },
|
|
144
|
+
data: { balance: { decrement: 100 } },
|
|
145
|
+
});
|
|
146
|
+
if (acc.balance < 0) throw new Error('Insufficient balance');
|
|
147
|
+
},
|
|
148
|
+
{ timeout: 5000, maxWait: 2000 },
|
|
149
|
+
);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- No external API calls, no heavy work inside transactions — they hold connections and locks.
|
|
153
|
+
|
|
154
|
+
### 9. Raw queries for complex cases
|
|
155
|
+
|
|
156
|
+
- Window functions, CTEs, advanced full-text search: use raw queries.
|
|
157
|
+
- Always the tagged template form — never string-concatenate user input:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const rows = await prisma.$queryRaw`
|
|
161
|
+
SELECT author_id, COUNT(*) FROM "posts" WHERE published = true GROUP BY author_id
|
|
162
|
+
`;
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- Never `$queryRawUnsafe` with user input — use TypedSQL for typed raw queries.
|
|
166
|
+
|
|
167
|
+
### 10. Measure before optimizing
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const prisma = new PrismaClient({ log: ['query', 'warn', 'error'] });
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- Enable query logging in dev (on the singleton) to catch N+1 and redundant queries.
|
|
174
|
+
- Run `EXPLAIN ANALYZE` on generated SQL to confirm index usage.
|
|
175
|
+
- Trace per-query time with OpenTelemetry or Prisma `$extends`.
|
|
176
|
+
|
|
177
|
+
### 11. Cache
|
|
178
|
+
|
|
179
|
+
- Cache heavy-read, rarely-changed results with Redis or in-memory.
|
|
180
|
+
- Prisma Accelerate supports `cacheStrategy: { ttl: 60, swr: 120 }`.
|
|
181
|
+
|
|
182
|
+
### 12. Cold start / bundle
|
|
183
|
+
|
|
184
|
+
- Edge/serverless: consider driver adapters (Neon, PlanetScale, D1, ...) to cut size and startup latency — check your Prisma version.
|
|
185
|
+
- Generate only the needed binary targets on deploy (`binaryTargets`).
|
|
186
|
+
|
|
187
|
+
## Checklist
|
|
188
|
+
|
|
189
|
+
- One PrismaClient singleton
|
|
190
|
+
- Always `select`/`omit` on wide tables
|
|
191
|
+
- No queries in loops — use `include`, `in`, `createMany`
|
|
192
|
+
- Indexes on FKs and filter/sort columns
|
|
193
|
+
- Cursor pagination for large lists
|
|
194
|
+
- Short transactions
|
|
195
|
+
- Query logging in dev to catch N+1
|
|
196
|
+
- Pooler for serverless
|
|
197
|
+
- Raw SQL for complex queries (safe tagged-template form)
|