autoworkflow 3.1.4 → 3.5.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/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +174 -11
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# TypeORM Skill
|
|
2
|
+
|
|
3
|
+
## Data Source Configuration
|
|
4
|
+
\`\`\`typescript
|
|
5
|
+
// src/data-source.ts
|
|
6
|
+
import { DataSource } from 'typeorm';
|
|
7
|
+
import { User } from './entities/User';
|
|
8
|
+
import { Post } from './entities/Post';
|
|
9
|
+
|
|
10
|
+
export const AppDataSource = new DataSource({
|
|
11
|
+
type: 'postgres',
|
|
12
|
+
host: process.env.DB_HOST,
|
|
13
|
+
port: parseInt(process.env.DB_PORT || '5432'),
|
|
14
|
+
username: process.env.DB_USER,
|
|
15
|
+
password: process.env.DB_PASSWORD,
|
|
16
|
+
database: process.env.DB_NAME,
|
|
17
|
+
synchronize: false, // Use migrations in production!
|
|
18
|
+
logging: process.env.NODE_ENV === 'development',
|
|
19
|
+
entities: [User, Post],
|
|
20
|
+
migrations: ['src/migrations/*.ts'],
|
|
21
|
+
subscribers: ['src/subscribers/*.ts'],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Initialize
|
|
25
|
+
await AppDataSource.initialize();
|
|
26
|
+
\`\`\`
|
|
27
|
+
|
|
28
|
+
## Entity Definition
|
|
29
|
+
\`\`\`typescript
|
|
30
|
+
import {
|
|
31
|
+
Entity, PrimaryGeneratedColumn, Column, CreateDateColumn,
|
|
32
|
+
UpdateDateColumn, DeleteDateColumn, OneToMany, ManyToOne,
|
|
33
|
+
ManyToMany, JoinTable, Index, BeforeInsert, AfterLoad
|
|
34
|
+
} from 'typeorm';
|
|
35
|
+
|
|
36
|
+
@Entity('users')
|
|
37
|
+
@Index(['email'])
|
|
38
|
+
@Index(['createdAt'])
|
|
39
|
+
export class User {
|
|
40
|
+
@PrimaryGeneratedColumn('uuid')
|
|
41
|
+
id: string;
|
|
42
|
+
|
|
43
|
+
@Column({ unique: true })
|
|
44
|
+
email: string;
|
|
45
|
+
|
|
46
|
+
@Column()
|
|
47
|
+
name: string;
|
|
48
|
+
|
|
49
|
+
@Column({ select: false }) // Excluded from default selects
|
|
50
|
+
password: string;
|
|
51
|
+
|
|
52
|
+
@Column({ type: 'enum', enum: ['user', 'admin'], default: 'user' })
|
|
53
|
+
role: 'user' | 'admin';
|
|
54
|
+
|
|
55
|
+
@Column({ type: 'jsonb', nullable: true })
|
|
56
|
+
settings: Record<string, unknown>;
|
|
57
|
+
|
|
58
|
+
@OneToMany(() => Post, (post) => post.author)
|
|
59
|
+
posts: Post[];
|
|
60
|
+
|
|
61
|
+
@ManyToMany(() => Role, (role) => role.users)
|
|
62
|
+
@JoinTable({ name: 'user_roles' })
|
|
63
|
+
roles: Role[];
|
|
64
|
+
|
|
65
|
+
@CreateDateColumn()
|
|
66
|
+
createdAt: Date;
|
|
67
|
+
|
|
68
|
+
@UpdateDateColumn()
|
|
69
|
+
updatedAt: Date;
|
|
70
|
+
|
|
71
|
+
@DeleteDateColumn() // Soft delete
|
|
72
|
+
deletedAt: Date | null;
|
|
73
|
+
|
|
74
|
+
// Lifecycle hooks
|
|
75
|
+
@BeforeInsert()
|
|
76
|
+
async hashPassword() {
|
|
77
|
+
if (this.password) {
|
|
78
|
+
this.password = await bcrypt.hash(this.password, 10);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Computed property (not persisted)
|
|
83
|
+
fullName: string;
|
|
84
|
+
|
|
85
|
+
@AfterLoad()
|
|
86
|
+
computeFullName() {
|
|
87
|
+
this.fullName = \`\${this.firstName} \${this.lastName}\`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Entity('posts')
|
|
92
|
+
export class Post {
|
|
93
|
+
@PrimaryGeneratedColumn('uuid')
|
|
94
|
+
id: string;
|
|
95
|
+
|
|
96
|
+
@Column()
|
|
97
|
+
title: string;
|
|
98
|
+
|
|
99
|
+
@Column({ type: 'text', nullable: true })
|
|
100
|
+
content: string;
|
|
101
|
+
|
|
102
|
+
@Column({ default: false })
|
|
103
|
+
published: boolean;
|
|
104
|
+
|
|
105
|
+
@ManyToOne(() => User, (user) => user.posts, { onDelete: 'CASCADE' })
|
|
106
|
+
author: User;
|
|
107
|
+
|
|
108
|
+
@Column()
|
|
109
|
+
authorId: string;
|
|
110
|
+
|
|
111
|
+
@CreateDateColumn()
|
|
112
|
+
createdAt: Date;
|
|
113
|
+
}
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
## Repository Patterns
|
|
117
|
+
\`\`\`typescript
|
|
118
|
+
// Get repository
|
|
119
|
+
const userRepository = AppDataSource.getRepository(User);
|
|
120
|
+
|
|
121
|
+
// Find operations
|
|
122
|
+
const user = await userRepository.findOne({
|
|
123
|
+
where: { id },
|
|
124
|
+
relations: ['posts', 'roles'],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const user = await userRepository.findOneOrFail({ where: { id } }); // Throws if not found
|
|
128
|
+
|
|
129
|
+
const users = await userRepository.find({
|
|
130
|
+
where: { role: 'user' },
|
|
131
|
+
relations: ['posts'],
|
|
132
|
+
order: { createdAt: 'DESC' },
|
|
133
|
+
skip: 0,
|
|
134
|
+
take: 20,
|
|
135
|
+
select: ['id', 'email', 'name', 'createdAt'],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Complex where conditions
|
|
139
|
+
const users = await userRepository.find({
|
|
140
|
+
where: [
|
|
141
|
+
{ role: 'admin' },
|
|
142
|
+
{ email: Like('%@company.com') },
|
|
143
|
+
], // OR condition
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const users = await userRepository.find({
|
|
147
|
+
where: {
|
|
148
|
+
role: 'user',
|
|
149
|
+
createdAt: MoreThan(new Date('2024-01-01')),
|
|
150
|
+
}, // AND condition
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Create & Save
|
|
154
|
+
const user = userRepository.create({
|
|
155
|
+
email: 'user@example.com',
|
|
156
|
+
name: 'John',
|
|
157
|
+
});
|
|
158
|
+
await userRepository.save(user);
|
|
159
|
+
|
|
160
|
+
// Or in one step
|
|
161
|
+
const user = await userRepository.save({
|
|
162
|
+
email: 'user@example.com',
|
|
163
|
+
name: 'John',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Update
|
|
167
|
+
await userRepository.update({ id }, { name: 'Jane' });
|
|
168
|
+
|
|
169
|
+
// Soft delete (with @DeleteDateColumn)
|
|
170
|
+
await userRepository.softDelete({ id });
|
|
171
|
+
await userRepository.restore({ id }); // Restore soft-deleted
|
|
172
|
+
|
|
173
|
+
// Hard delete
|
|
174
|
+
await userRepository.delete({ id });
|
|
175
|
+
\`\`\`
|
|
176
|
+
|
|
177
|
+
## QueryBuilder
|
|
178
|
+
\`\`\`typescript
|
|
179
|
+
// Select query builder
|
|
180
|
+
const users = await userRepository
|
|
181
|
+
.createQueryBuilder('user')
|
|
182
|
+
.leftJoinAndSelect('user.posts', 'post')
|
|
183
|
+
.where('user.role = :role', { role: 'user' })
|
|
184
|
+
.andWhere('post.published = :published', { published: true })
|
|
185
|
+
.orderBy('user.createdAt', 'DESC')
|
|
186
|
+
.skip(0)
|
|
187
|
+
.take(20)
|
|
188
|
+
.getMany();
|
|
189
|
+
|
|
190
|
+
// With subquery
|
|
191
|
+
const usersWithPostCount = await userRepository
|
|
192
|
+
.createQueryBuilder('user')
|
|
193
|
+
.loadRelationCountAndMap('user.postCount', 'user.posts')
|
|
194
|
+
.getMany();
|
|
195
|
+
|
|
196
|
+
// Select specific fields
|
|
197
|
+
const userEmails = await userRepository
|
|
198
|
+
.createQueryBuilder('user')
|
|
199
|
+
.select(['user.id', 'user.email'])
|
|
200
|
+
.where('user.role = :role', { role: 'admin' })
|
|
201
|
+
.getRawMany();
|
|
202
|
+
|
|
203
|
+
// Aggregations
|
|
204
|
+
const stats = await userRepository
|
|
205
|
+
.createQueryBuilder('user')
|
|
206
|
+
.select('user.role', 'role')
|
|
207
|
+
.addSelect('COUNT(*)', 'count')
|
|
208
|
+
.groupBy('user.role')
|
|
209
|
+
.getRawMany();
|
|
210
|
+
|
|
211
|
+
// Update with query builder
|
|
212
|
+
await userRepository
|
|
213
|
+
.createQueryBuilder()
|
|
214
|
+
.update(User)
|
|
215
|
+
.set({ lastLoginAt: new Date() })
|
|
216
|
+
.where('id = :id', { id })
|
|
217
|
+
.execute();
|
|
218
|
+
|
|
219
|
+
// Delete with query builder
|
|
220
|
+
await userRepository
|
|
221
|
+
.createQueryBuilder()
|
|
222
|
+
.delete()
|
|
223
|
+
.from(User)
|
|
224
|
+
.where('deletedAt IS NOT NULL')
|
|
225
|
+
.andWhere('deletedAt < :date', { date: thirtyDaysAgo })
|
|
226
|
+
.execute();
|
|
227
|
+
\`\`\`
|
|
228
|
+
|
|
229
|
+
## Transactions
|
|
230
|
+
\`\`\`typescript
|
|
231
|
+
// Using transaction manager
|
|
232
|
+
await AppDataSource.transaction(async (manager) => {
|
|
233
|
+
const userRepo = manager.getRepository(User);
|
|
234
|
+
const postRepo = manager.getRepository(Post);
|
|
235
|
+
|
|
236
|
+
const user = await userRepo.save({
|
|
237
|
+
email: 'test@example.com',
|
|
238
|
+
name: 'Test',
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await postRepo.save({
|
|
242
|
+
title: 'First Post',
|
|
243
|
+
authorId: user.id,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// If any operation fails, all are rolled back
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Using query runner (more control)
|
|
250
|
+
const queryRunner = AppDataSource.createQueryRunner();
|
|
251
|
+
await queryRunner.connect();
|
|
252
|
+
await queryRunner.startTransaction();
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
await queryRunner.manager.save(User, userData);
|
|
256
|
+
await queryRunner.manager.save(Post, postData);
|
|
257
|
+
await queryRunner.commitTransaction();
|
|
258
|
+
} catch (err) {
|
|
259
|
+
await queryRunner.rollbackTransaction();
|
|
260
|
+
throw err;
|
|
261
|
+
} finally {
|
|
262
|
+
await queryRunner.release();
|
|
263
|
+
}
|
|
264
|
+
\`\`\`
|
|
265
|
+
|
|
266
|
+
## Custom Repositories
|
|
267
|
+
\`\`\`typescript
|
|
268
|
+
// Custom repository with DataSource
|
|
269
|
+
export const UserRepository = AppDataSource.getRepository(User).extend({
|
|
270
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
271
|
+
return this.findOne({ where: { email } });
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
async findActiveWithPosts(): Promise<User[]> {
|
|
275
|
+
return this.find({
|
|
276
|
+
where: { deletedAt: IsNull() },
|
|
277
|
+
relations: ['posts'],
|
|
278
|
+
order: { createdAt: 'DESC' },
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
async softDeleteOldUsers(days: number): Promise<number> {
|
|
283
|
+
const date = new Date();
|
|
284
|
+
date.setDate(date.getDate() - days);
|
|
285
|
+
|
|
286
|
+
const result = await this.softDelete({
|
|
287
|
+
lastLoginAt: LessThan(date),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return result.affected || 0;
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Usage
|
|
295
|
+
const user = await UserRepository.findByEmail('test@example.com');
|
|
296
|
+
\`\`\`
|
|
297
|
+
|
|
298
|
+
## Migrations
|
|
299
|
+
\`\`\`bash
|
|
300
|
+
# Generate migration from entity changes
|
|
301
|
+
npx typeorm migration:generate src/migrations/AddUserRole -d src/data-source.ts
|
|
302
|
+
|
|
303
|
+
# Create empty migration
|
|
304
|
+
npx typeorm migration:create src/migrations/SeedData
|
|
305
|
+
|
|
306
|
+
# Run migrations
|
|
307
|
+
npx typeorm migration:run -d src/data-source.ts
|
|
308
|
+
|
|
309
|
+
# Revert last migration
|
|
310
|
+
npx typeorm migration:revert -d src/data-source.ts
|
|
311
|
+
\`\`\`
|
|
312
|
+
|
|
313
|
+
\`\`\`typescript
|
|
314
|
+
// Migration file example
|
|
315
|
+
export class AddUserRole1234567890 implements MigrationInterface {
|
|
316
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
317
|
+
await queryRunner.query(\`
|
|
318
|
+
ALTER TABLE "users" ADD "role" varchar DEFAULT 'user'
|
|
319
|
+
\`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
323
|
+
await queryRunner.query(\`
|
|
324
|
+
ALTER TABLE "users" DROP COLUMN "role"
|
|
325
|
+
\`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
\`\`\`
|
|
329
|
+
|
|
330
|
+
## Entity Subscribers
|
|
331
|
+
\`\`\`typescript
|
|
332
|
+
@EventSubscriber()
|
|
333
|
+
export class UserSubscriber implements EntitySubscriberInterface<User> {
|
|
334
|
+
listenTo() {
|
|
335
|
+
return User;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
beforeInsert(event: InsertEvent<User>) {
|
|
339
|
+
console.log('Before insert:', event.entity);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
afterInsert(event: InsertEvent<User>) {
|
|
343
|
+
// Send welcome email, log audit, etc.
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
afterUpdate(event: UpdateEvent<User>) {
|
|
347
|
+
if (event.updatedColumns.find(c => c.propertyName === 'email')) {
|
|
348
|
+
// Email changed, send verification
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
\`\`\`
|
|
353
|
+
|
|
354
|
+
## ❌ DON'T
|
|
355
|
+
- Use \`synchronize: true\` in production
|
|
356
|
+
- Forget to handle N+1 queries (use relations or QueryBuilder joins)
|
|
357
|
+
- Use \`find()\` without limits on large tables
|
|
358
|
+
- Put business logic in entities
|
|
359
|
+
- Use raw queries without parameter binding
|
|
360
|
+
|
|
361
|
+
## ✅ DO
|
|
362
|
+
- Use migrations for schema changes
|
|
363
|
+
- Use QueryBuilder for complex queries
|
|
364
|
+
- Use transactions for multi-step operations
|
|
365
|
+
- Use soft deletes for important data
|
|
366
|
+
- Use \`select: false\` for sensitive fields
|
|
367
|
+
- Use entity subscribers for side effects
|
|
368
|
+
- Create custom repository methods for reusable queries
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# Vitest Skill
|
|
2
|
+
|
|
3
|
+
## Test Structure
|
|
4
|
+
\`\`\`typescript
|
|
5
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
|
|
7
|
+
describe('UserService', () => {
|
|
8
|
+
let service: UserService;
|
|
9
|
+
let mockDb: { findUser: ReturnType<typeof vi.fn> };
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockDb = { findUser: vi.fn() };
|
|
13
|
+
service = new UserService(mockDb);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('getUser', () => {
|
|
21
|
+
it('should return user when found', async () => {
|
|
22
|
+
// Arrange
|
|
23
|
+
mockDb.findUser.mockResolvedValue({ id: '1', name: 'Test' });
|
|
24
|
+
|
|
25
|
+
// Act
|
|
26
|
+
const result = await service.getUser('1');
|
|
27
|
+
|
|
28
|
+
// Assert
|
|
29
|
+
expect(result).toEqual({ id: '1', name: 'Test' });
|
|
30
|
+
expect(mockDb.findUser).toHaveBeenCalledWith('1');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should throw when user not found', async () => {
|
|
34
|
+
mockDb.findUser.mockResolvedValue(null);
|
|
35
|
+
|
|
36
|
+
await expect(service.getUser('1')).rejects.toThrow('Not found');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
\`\`\`
|
|
41
|
+
|
|
42
|
+
## Mocking
|
|
43
|
+
|
|
44
|
+
### Function Mocks
|
|
45
|
+
\`\`\`typescript
|
|
46
|
+
import { vi, expect } from 'vitest';
|
|
47
|
+
|
|
48
|
+
// Basic mock
|
|
49
|
+
const mockFn = vi.fn();
|
|
50
|
+
mockFn.mockReturnValue('default');
|
|
51
|
+
mockFn.mockReturnValueOnce('first call');
|
|
52
|
+
|
|
53
|
+
// Async mocks
|
|
54
|
+
const mockAsync = vi.fn();
|
|
55
|
+
mockAsync.mockResolvedValue({ data: 'success' });
|
|
56
|
+
mockAsync.mockRejectedValue(new Error('Failed'));
|
|
57
|
+
|
|
58
|
+
// Implementation mock
|
|
59
|
+
const mockImpl = vi.fn((x: number) => x * 2);
|
|
60
|
+
|
|
61
|
+
// Assertions
|
|
62
|
+
expect(mockFn).toHaveBeenCalled();
|
|
63
|
+
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
|
|
64
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
65
|
+
expect(mockFn).toHaveBeenLastCalledWith('lastArg');
|
|
66
|
+
expect(mockFn).toHaveReturnedWith('value');
|
|
67
|
+
|
|
68
|
+
// Reset mock
|
|
69
|
+
mockFn.mockClear(); // Clear call history
|
|
70
|
+
mockFn.mockReset(); // Clear history + implementation
|
|
71
|
+
mockFn.mockRestore(); // Restore original (for spies)
|
|
72
|
+
\`\`\`
|
|
73
|
+
|
|
74
|
+
### Module Mocks
|
|
75
|
+
\`\`\`typescript
|
|
76
|
+
// Mock entire module (hoisted to top)
|
|
77
|
+
vi.mock('./userService', () => ({
|
|
78
|
+
UserService: vi.fn().mockImplementation(() => ({
|
|
79
|
+
getUser: vi.fn().mockResolvedValue({ id: '1' }),
|
|
80
|
+
})),
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
// Mock with factory
|
|
84
|
+
vi.mock('./api', () => ({
|
|
85
|
+
fetchUser: vi.fn().mockResolvedValue({ id: '1', name: 'Test' }),
|
|
86
|
+
fetchPosts: vi.fn(),
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
// Partial mock (keep some real implementations)
|
|
90
|
+
vi.mock('./utils', async () => {
|
|
91
|
+
const actual = await vi.importActual('./utils');
|
|
92
|
+
return {
|
|
93
|
+
...actual,
|
|
94
|
+
formatDate: vi.fn().mockReturnValue('2024-01-01'),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Auto-mock all exports
|
|
99
|
+
vi.mock('./service'); // All exports become vi.fn()
|
|
100
|
+
|
|
101
|
+
// Get mocked module
|
|
102
|
+
import { fetchUser } from './api';
|
|
103
|
+
const mockedFetchUser = vi.mocked(fetchUser);
|
|
104
|
+
\`\`\`
|
|
105
|
+
|
|
106
|
+
### Spy on Methods
|
|
107
|
+
\`\`\`typescript
|
|
108
|
+
// Spy on object method
|
|
109
|
+
const spy = vi.spyOn(console, 'log');
|
|
110
|
+
spy.mockImplementation(() => {}); // Suppress output
|
|
111
|
+
|
|
112
|
+
// Spy on prototype
|
|
113
|
+
const saveSpy = vi.spyOn(UserService.prototype, 'save');
|
|
114
|
+
saveSpy.mockResolvedValue({ id: '1' });
|
|
115
|
+
|
|
116
|
+
// Spy on getter/setter
|
|
117
|
+
vi.spyOn(object, 'property', 'get').mockReturnValue('mocked');
|
|
118
|
+
|
|
119
|
+
// Restore original
|
|
120
|
+
spy.mockRestore();
|
|
121
|
+
\`\`\`
|
|
122
|
+
|
|
123
|
+
### Global Mocks
|
|
124
|
+
\`\`\`typescript
|
|
125
|
+
// Mock global function
|
|
126
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
127
|
+
ok: true,
|
|
128
|
+
json: () => Promise.resolve({ data: 'test' }),
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
// Mock global object
|
|
132
|
+
vi.stubGlobal('localStorage', {
|
|
133
|
+
getItem: vi.fn(),
|
|
134
|
+
setItem: vi.fn(),
|
|
135
|
+
clear: vi.fn(),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Mock environment variables
|
|
139
|
+
vi.stubEnv('API_URL', 'http://test.com');
|
|
140
|
+
|
|
141
|
+
// Unstub
|
|
142
|
+
vi.unstubAllGlobals();
|
|
143
|
+
vi.unstubAllEnvs();
|
|
144
|
+
\`\`\`
|
|
145
|
+
|
|
146
|
+
## Timer Mocks
|
|
147
|
+
\`\`\`typescript
|
|
148
|
+
import { vi, beforeEach, afterEach, it, expect } from 'vitest';
|
|
149
|
+
|
|
150
|
+
beforeEach(() => {
|
|
151
|
+
vi.useFakeTimers();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
vi.useRealTimers();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should debounce calls', () => {
|
|
159
|
+
const callback = vi.fn();
|
|
160
|
+
const debounced = debounce(callback, 1000);
|
|
161
|
+
|
|
162
|
+
debounced();
|
|
163
|
+
debounced();
|
|
164
|
+
debounced();
|
|
165
|
+
|
|
166
|
+
expect(callback).not.toHaveBeenCalled();
|
|
167
|
+
|
|
168
|
+
vi.advanceTimersByTime(1000);
|
|
169
|
+
|
|
170
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should handle async timers', async () => {
|
|
174
|
+
const callback = vi.fn();
|
|
175
|
+
setTimeout(callback, 1000);
|
|
176
|
+
|
|
177
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
178
|
+
expect(callback).toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Mock Date
|
|
182
|
+
vi.setSystemTime(new Date('2024-01-15'));
|
|
183
|
+
expect(new Date().toISOString()).toBe('2024-01-15T00:00:00.000Z');
|
|
184
|
+
|
|
185
|
+
// Run all timers
|
|
186
|
+
vi.runAllTimers();
|
|
187
|
+
vi.runOnlyPendingTimers(); // Avoid infinite loops with recursive timers
|
|
188
|
+
\`\`\`
|
|
189
|
+
|
|
190
|
+
## Snapshot Testing
|
|
191
|
+
\`\`\`typescript
|
|
192
|
+
import { expect, it } from 'vitest';
|
|
193
|
+
|
|
194
|
+
// File snapshot
|
|
195
|
+
it('should match snapshot', () => {
|
|
196
|
+
const result = formatUser({ name: 'John', email: 'john@example.com' });
|
|
197
|
+
expect(result).toMatchSnapshot();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Inline snapshot
|
|
201
|
+
it('should match inline snapshot', () => {
|
|
202
|
+
expect(formatUser(user)).toMatchInlineSnapshot(\`
|
|
203
|
+
{
|
|
204
|
+
"name": "John Doe",
|
|
205
|
+
"email": "john@example.com"
|
|
206
|
+
}
|
|
207
|
+
\`);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Update snapshots: vitest --update or vitest -u
|
|
211
|
+
\`\`\`
|
|
212
|
+
|
|
213
|
+
## Test Utilities
|
|
214
|
+
\`\`\`typescript
|
|
215
|
+
// Parameterized tests
|
|
216
|
+
import { describe, it, expect } from 'vitest';
|
|
217
|
+
|
|
218
|
+
describe.each([
|
|
219
|
+
{ input: 1, expected: 2 },
|
|
220
|
+
{ input: 2, expected: 4 },
|
|
221
|
+
{ input: 3, expected: 6 },
|
|
222
|
+
])('double($input)', ({ input, expected }) => {
|
|
223
|
+
it(\`should return \${expected}\`, () => {
|
|
224
|
+
expect(double(input)).toBe(expected);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Skip and focus
|
|
229
|
+
describe.skip('skipped suite', () => {});
|
|
230
|
+
it.skip('skipped test', () => {});
|
|
231
|
+
describe.only('focused suite', () => {}); // Only run this
|
|
232
|
+
it.only('focused test', () => {}); // Only run this
|
|
233
|
+
|
|
234
|
+
// Todo tests
|
|
235
|
+
it.todo('should implement this feature');
|
|
236
|
+
|
|
237
|
+
// Concurrent tests (run in parallel)
|
|
238
|
+
describe.concurrent('parallel tests', () => {
|
|
239
|
+
it('test 1', async () => {});
|
|
240
|
+
it('test 2', async () => {});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Retry flaky tests
|
|
244
|
+
it('flaky test', { retry: 3 }, async () => {});
|
|
245
|
+
|
|
246
|
+
// Timeout
|
|
247
|
+
it('slow test', { timeout: 10000 }, async () => {});
|
|
248
|
+
\`\`\`
|
|
249
|
+
|
|
250
|
+
## Type Testing
|
|
251
|
+
\`\`\`typescript
|
|
252
|
+
import { expectTypeOf, assertType } from 'vitest';
|
|
253
|
+
|
|
254
|
+
it('should have correct types', () => {
|
|
255
|
+
// Check type equality
|
|
256
|
+
expectTypeOf(getUser).toBeFunction();
|
|
257
|
+
expectTypeOf(getUser).parameter(0).toBeString();
|
|
258
|
+
expectTypeOf(getUser).returns.resolves.toMatchTypeOf<User>();
|
|
259
|
+
|
|
260
|
+
// Check assignability
|
|
261
|
+
expectTypeOf<string>().toMatchTypeOf<string | number>();
|
|
262
|
+
|
|
263
|
+
// Assert type (compile-time only)
|
|
264
|
+
assertType<string>('hello');
|
|
265
|
+
});
|
|
266
|
+
\`\`\`
|
|
267
|
+
|
|
268
|
+
## In-Source Testing
|
|
269
|
+
\`\`\`typescript
|
|
270
|
+
// src/utils.ts - tests in same file
|
|
271
|
+
export function add(a: number, b: number): number {
|
|
272
|
+
return a + b;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Only included when running tests
|
|
276
|
+
if (import.meta.vitest) {
|
|
277
|
+
const { it, expect } = import.meta.vitest;
|
|
278
|
+
|
|
279
|
+
it('add', () => {
|
|
280
|
+
expect(add(1, 2)).toBe(3);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
\`\`\`
|
|
284
|
+
|
|
285
|
+
## Configuration (vitest.config.ts)
|
|
286
|
+
\`\`\`typescript
|
|
287
|
+
import { defineConfig } from 'vitest/config';
|
|
288
|
+
|
|
289
|
+
export default defineConfig({
|
|
290
|
+
test: {
|
|
291
|
+
globals: true, // Use describe, it, expect without imports
|
|
292
|
+
environment: 'node', // or 'jsdom', 'happy-dom'
|
|
293
|
+
include: ['**/*.{test,spec}.{ts,tsx}'],
|
|
294
|
+
exclude: ['node_modules', 'dist'],
|
|
295
|
+
coverage: {
|
|
296
|
+
provider: 'v8', // or 'istanbul'
|
|
297
|
+
reporter: ['text', 'html', 'lcov'],
|
|
298
|
+
exclude: ['**/*.d.ts', '**/*.test.ts'],
|
|
299
|
+
thresholds: {
|
|
300
|
+
branches: 80,
|
|
301
|
+
functions: 80,
|
|
302
|
+
lines: 80,
|
|
303
|
+
statements: 80,
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
setupFiles: ['./vitest.setup.ts'],
|
|
307
|
+
alias: {
|
|
308
|
+
'@': './src',
|
|
309
|
+
},
|
|
310
|
+
mockReset: true, // Reset mocks between tests
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
\`\`\`
|
|
314
|
+
|
|
315
|
+
## ❌ DON'T
|
|
316
|
+
- Test implementation details
|
|
317
|
+
- Share state between tests
|
|
318
|
+
- Use \`.only\` in committed code
|
|
319
|
+
- Write flaky tests (timing-dependent)
|
|
320
|
+
- Forget to restore mocks
|
|
321
|
+
- Mock everything (test integrations too)
|
|
322
|
+
|
|
323
|
+
## ✅ DO
|
|
324
|
+
- Use vi.mock for module mocking
|
|
325
|
+
- Clear mocks between tests (mockReset: true)
|
|
326
|
+
- Use vi.mocked() for type-safe mocked imports
|
|
327
|
+
- Use fake timers for time-dependent code
|
|
328
|
+
- Use concurrent tests for performance
|
|
329
|
+
- Leverage in-source testing for utilities
|
|
330
|
+
- Use type testing for public APIs
|