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.
@@ -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)