@oalacea/demon 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.
@@ -0,0 +1,425 @@
1
+ # Database Integration Test Guide
2
+
3
+ This prompt is included by EXECUTE.md. It provides detailed guidance for database integration testing.
4
+
5
+ ---
6
+
7
+ ## Transaction Rollback Pattern
8
+
9
+ **CRITICAL**: Always use transaction rollback to prevent test data pollution.
10
+
11
+ ### Setup
12
+
13
+ ```typescript
14
+ // tests/db/setup.ts
15
+ import { PrismaClient } from '@prisma/client';
16
+
17
+ const prisma = new PrismaClient();
18
+
19
+ export const db = {
20
+ prisma,
21
+ transaction: null as PrismaClient | null,
22
+
23
+ async begin() {
24
+ // Start a transaction
25
+ this.transaction = await prisma.$transaction(
26
+ async (tx) => tx,
27
+ {
28
+ maxWait: 5000,
29
+ timeout: 10000,
30
+ }
31
+ ) as PrismaClient;
32
+ },
33
+
34
+ async rollback() {
35
+ if (this.transaction) {
36
+ await this.transaction.$disconnect();
37
+ this.transaction = null;
38
+ }
39
+ },
40
+
41
+ get user() {
42
+ return this.transaction?.$extends(prisma.user) || prisma.user;
43
+ }
44
+ };
45
+
46
+ // Seed data helpers
47
+ export async function seedUsers(count = 5) {
48
+ const users = Array.from({ length: count }, (_, i) => ({
49
+ email: `user${i}@test.com`,
50
+ name: `User ${i}`
51
+ }));
52
+
53
+ if (db.transaction) {
54
+ return await db.transaction.user.createMany({ data: users });
55
+ }
56
+ return await prisma.user.createMany({ data: users });
57
+ }
58
+ ```
59
+
60
+ ---
61
+
62
+ ## CRUD Test Templates
63
+
64
+ ### Create Tests
65
+
66
+ ```typescript
67
+ describe('User Creation', () => {
68
+ beforeEach(async () => {
69
+ await db.begin();
70
+ });
71
+
72
+ afterEach(async () => {
73
+ await db.rollback();
74
+ });
75
+
76
+ it('should create user with valid data', async () => {
77
+ const user = await db.user.create({
78
+ data: {
79
+ email: 'test@example.com',
80
+ name: 'Test User',
81
+ age: 25
82
+ }
83
+ });
84
+
85
+ expect(user).toMatchObject({
86
+ email: 'test@example.com',
87
+ name: 'Test User',
88
+ age: 25
89
+ });
90
+ expect(user.id).toBeDefined();
91
+ });
92
+
93
+ it('should enforce unique email', async () => {
94
+ await db.user.create({
95
+ data: { email: 'test@example.com', name: 'User 1' }
96
+ });
97
+
98
+ await expect(
99
+ db.user.create({
100
+ data: { email: 'test@example.com', name: 'User 2' }
101
+ })
102
+ ).rejects.toThrow(/unique/i);
103
+ });
104
+
105
+ it('should validate required fields', async () => {
106
+ await expect(
107
+ db.user.create({ data: { name: 'User' } }) // Missing email
108
+ ).rejects.toThrow();
109
+ });
110
+ });
111
+ ```
112
+
113
+ ### Read Tests
114
+
115
+ ```typescript
116
+ describe('User Reading', () => {
117
+ beforeEach(async () => {
118
+ await db.begin();
119
+ await seedUsers(10);
120
+ });
121
+
122
+ afterEach(async () => {
123
+ await db.rollback();
124
+ });
125
+
126
+ it('should find user by id', async () => {
127
+ const created = await db.user.findFirst();
128
+ const found = await db.user.findUnique({
129
+ where: { id: created!.id }
130
+ });
131
+
132
+ expect(found).toEqual(created);
133
+ });
134
+
135
+ it('should find user by email', async () => {
136
+ const found = await db.user.findUnique({
137
+ where: { email: 'user0@test.com' }
138
+ });
139
+
140
+ expect(found).toBeDefined();
141
+ expect(found?.email).toBe('user0@test.com');
142
+ });
143
+
144
+ it('should return null for non-existent user', async () => {
145
+ const found = await db.user.findUnique({
146
+ where: { id: 'non-existent-id' }
147
+ });
148
+
149
+ expect(found).toBeNull();
150
+ });
151
+
152
+ it('should paginate results', async () => {
153
+ const page1 = await db.user.findMany({
154
+ take: 5,
155
+ skip: 0,
156
+ orderBy: { email: 'asc' }
157
+ });
158
+
159
+ const page2 = await db.user.findMany({
160
+ take: 5,
161
+ skip: 5,
162
+ orderBy: { email: 'asc' }
163
+ });
164
+
165
+ expect(page1).toHaveLength(5);
166
+ expect(page2).toHaveLength(5);
167
+ expect(page1[0].email).not.toBe(page2[0].email);
168
+ });
169
+ });
170
+ ```
171
+
172
+ ### Update Tests
173
+
174
+ ```typescript
175
+ describe('User Updates', () => {
176
+ let user: any;
177
+
178
+ beforeEach(async () => {
179
+ await db.begin();
180
+ user = await db.user.create({
181
+ data: { email: 'test@example.com', name: 'Test' }
182
+ });
183
+ });
184
+
185
+ afterEach(async () => {
186
+ await db.rollback();
187
+ });
188
+
189
+ it('should update user fields', async () => {
190
+ const updated = await db.user.update({
191
+ where: { id: user.id },
192
+ data: { name: 'Updated Name' }
193
+ });
194
+
195
+ expect(updated.name).toBe('Updated Name');
196
+ expect(updated.email).toBe(user.email); // Unchanged
197
+ });
198
+
199
+ it('should handle concurrent updates', async () => {
200
+ const update1 = db.user.update({
201
+ where: { id: user.id },
202
+ data: { name: 'Name 1' }
203
+ });
204
+
205
+ const update2 = db.user.update({
206
+ where: { id: user.id },
207
+ data: { name: 'Name 2' }
208
+ });
209
+
210
+ const [result1, result2] = await Promise.allSettled([update1, update2]);
211
+
212
+ // Both should succeed, last write wins
213
+ expect(result1.status).toBe('fulfilled');
214
+ expect(result2.status).toBe('fulfilled');
215
+ });
216
+ });
217
+ ```
218
+
219
+ ### Delete Tests
220
+
221
+ ```typescript
222
+ describe('User Deletion', () => {
223
+ let user: any;
224
+
225
+ beforeEach(async () => {
226
+ await db.begin();
227
+ user = await db.user.create({
228
+ data: { email: 'test@example.com', name: 'Test' }
229
+ });
230
+ });
231
+
232
+ afterEach(async () => {
233
+ await db.rollback();
234
+ });
235
+
236
+ it('should delete user', async () => {
237
+ await db.user.delete({ where: { id: user.id } });
238
+
239
+ const found = await db.user.findUnique({
240
+ where: { id: user.id }
241
+ });
242
+
243
+ expect(found).toBeNull();
244
+ });
245
+
246
+ it('should handle delete of non-existent user', async () => {
247
+ await expect(
248
+ db.user.delete({ where: { id: 'non-existent' } })
249
+ ).rejects.toThrow();
250
+ });
251
+ });
252
+ ```
253
+
254
+ ---
255
+
256
+ ## Relationship Tests
257
+
258
+ ```typescript
259
+ describe('User-Posts Relationship', () => {
260
+ beforeEach(async () => {
261
+ await db.begin();
262
+ });
263
+
264
+ afterEach(async () => {
265
+ await db.rollback();
266
+ });
267
+
268
+ it('should create user with posts', async () => {
269
+ const user = await db.user.create({
270
+ data: {
271
+ email: 'test@example.com',
272
+ name: 'Test',
273
+ posts: {
274
+ create: [
275
+ { title: 'Post 1', content: 'Content 1' },
276
+ { title: 'Post 2', content: 'Content 2' }
277
+ ]
278
+ }
279
+ },
280
+ include: { posts: true }
281
+ });
282
+
283
+ expect(user.posts).toHaveLength(2);
284
+ });
285
+
286
+ it('should eager load posts', async () => {
287
+ const user = await db.user.create({
288
+ data: {
289
+ email: 'test@example.com',
290
+ name: 'Test',
291
+ posts: {
292
+ create: [{ title: 'Post 1', content: 'Content' }]
293
+ }
294
+ }
295
+ });
296
+
297
+ const withPosts = await db.user.findUnique({
298
+ where: { id: user.id },
299
+ include: { posts: true }
300
+ });
301
+
302
+ expect(withPosts?.posts).toHaveLength(1);
303
+ });
304
+
305
+ it('should prevent orphan posts', async () => {
306
+ const user = await db.user.create({
307
+ data: {
308
+ email: 'test@example.com',
309
+ name: 'Test',
310
+ posts: {
311
+ create: [{ title: 'Post 1', content: 'Content' }]
312
+ }
313
+ }
314
+ });
315
+
316
+ await db.user.delete({ where: { id: user.id } });
317
+
318
+ const posts = await db.post.findMany({
319
+ where: { userId: user.id }
320
+ });
321
+
322
+ expect(posts).toHaveLength(0); // Cascade delete
323
+ });
324
+ });
325
+ ```
326
+
327
+ ---
328
+
329
+ ## Performance Tests
330
+
331
+ ```typescript
332
+ describe('Query Performance', () => {
333
+ beforeEach(async () => {
334
+ await db.begin();
335
+ // Seed 1000 users
336
+ await db.user.createMany({
337
+ data: Array.from({ length: 1000 }, (_, i) => ({
338
+ email: `user${i}@test.com`,
339
+ name: `User ${i}`
340
+ }))
341
+ });
342
+ });
343
+
344
+ afterEach(async () => {
345
+ await db.rollback();
346
+ });
347
+
348
+ it('should use index for email lookup', async () => {
349
+ const start = Date.now();
350
+ await db.user.findUnique({
351
+ where: { email: 'user500@test.com' }
352
+ });
353
+ const duration = Date.now() - start;
354
+
355
+ expect(duration).toBeLessThan(50); // Should be fast with index
356
+ });
357
+
358
+ it('should not have N+1 query', async () => {
359
+ // Add posts to users
360
+ for (let i = 0; i < 10; i++) {
361
+ await db.user.update({
362
+ where: { email: `user${i}@test.com` },
363
+ data: {
364
+ posts: {
365
+ create: [{ title: `Post ${i}`, content: `Content ${i}` }]
366
+ }
367
+ }
368
+ });
369
+ }
370
+
371
+ // Good: Eager loading
372
+ const usersWithPosts = await db.user.findMany({
373
+ take: 10,
374
+ include: { posts: true }
375
+ });
376
+
377
+ // Verify this doesn't trigger N+1
378
+ // (in real test, use query logging)
379
+ expect(usersWithPosts).toHaveLength(10);
380
+ });
381
+ });
382
+ ```
383
+
384
+ ---
385
+
386
+ ## Database-Specific Patterns
387
+
388
+ ### Prisma with SQLite (Test Database)
389
+
390
+ ```typescript
391
+ // tests/db/prisma-test.ts
392
+ import { PrismaClient } from '@prisma/client';
393
+
394
+ let prisma: PrismaClient;
395
+
396
+ beforeAll(async () => {
397
+ // Use in-memory SQLite for tests
398
+ process.env.DATABASE_URL = 'file:./test.db';
399
+ execSync('npx prisma migrate reset --force');
400
+ prisma = new PrismaClient();
401
+ });
402
+
403
+ afterAll(async () => {
404
+ await prisma.$disconnect();
405
+ execSync('rm test.db');
406
+ });
407
+ ```
408
+
409
+ ### Neon/Supabase (Direct Connection)
410
+
411
+ ```typescript
412
+ // Use a test schema for Neon/Supabase
413
+ beforeEach(async () => {
414
+ await prisma.$executeRaw`
415
+ CREATE SCHEMA IF NOT EXISTS test_schema;
416
+ SET search_path TO test_schema;
417
+ `;
418
+ });
419
+
420
+ afterEach(async () => {
421
+ await prisma.$executeRaw`
422
+ DROP SCHEMA test_schema CASCADE;
423
+ `;
424
+ });
425
+ ```