autoworkflow 3.1.5 → 3.6.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 +26 -0
- 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/post-edit.sh +190 -17
- package/.claude/hooks/pre-edit.sh +221 -0
- package/.claude/hooks/session-check.sh +90 -0
- package/.claude/settings.json +56 -6
- package/.claude/settings.local.json +5 -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 +163 -52
- package/package.json +1 -1
- package/system/triggers.md +256 -17
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# Entity Framework Core Skill
|
|
2
|
+
|
|
3
|
+
## DbContext Configuration
|
|
4
|
+
\`\`\`csharp
|
|
5
|
+
public class AppDbContext : DbContext
|
|
6
|
+
{
|
|
7
|
+
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
|
8
|
+
|
|
9
|
+
public DbSet<User> Users => Set<User>();
|
|
10
|
+
public DbSet<Post> Posts => Set<Post>();
|
|
11
|
+
public DbSet<Comment> Comments => Set<Comment>();
|
|
12
|
+
|
|
13
|
+
protected override void OnModelCreating(ModelBuilder builder)
|
|
14
|
+
{
|
|
15
|
+
// Apply all configurations from assembly
|
|
16
|
+
builder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Override SaveChanges for auditing
|
|
20
|
+
public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
|
|
21
|
+
{
|
|
22
|
+
foreach (var entry in ChangeTracker.Entries<IAuditable>())
|
|
23
|
+
{
|
|
24
|
+
if (entry.State == EntityState.Added)
|
|
25
|
+
{
|
|
26
|
+
entry.Entity.CreatedAt = DateTime.UtcNow;
|
|
27
|
+
}
|
|
28
|
+
entry.Entity.UpdatedAt = DateTime.UtcNow;
|
|
29
|
+
}
|
|
30
|
+
return await base.SaveChangesAsync(ct);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Registration in Program.cs
|
|
35
|
+
builder.Services.AddDbContext<AppDbContext>(options =>
|
|
36
|
+
options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))
|
|
37
|
+
.EnableSensitiveDataLogging(builder.Environment.IsDevelopment())
|
|
38
|
+
.EnableDetailedErrors(builder.Environment.IsDevelopment()));
|
|
39
|
+
\`\`\`
|
|
40
|
+
|
|
41
|
+
## Entity Configuration (Fluent API)
|
|
42
|
+
\`\`\`csharp
|
|
43
|
+
// User entity
|
|
44
|
+
public class User
|
|
45
|
+
{
|
|
46
|
+
public string Id { get; set; } = Guid.NewGuid().ToString();
|
|
47
|
+
public string Email { get; set; } = null!;
|
|
48
|
+
public string Name { get; set; } = null!;
|
|
49
|
+
public string PasswordHash { get; set; } = null!;
|
|
50
|
+
public bool IsActive { get; set; } = true;
|
|
51
|
+
public DateTime CreatedAt { get; set; }
|
|
52
|
+
public DateTime UpdatedAt { get; set; }
|
|
53
|
+
|
|
54
|
+
// Navigation properties
|
|
55
|
+
public Profile? Profile { get; set; }
|
|
56
|
+
public ICollection<Post> Posts { get; set; } = new List<Post>();
|
|
57
|
+
public ICollection<Role> Roles { get; set; } = new List<Role>();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Configuration class
|
|
61
|
+
public class UserConfiguration : IEntityTypeConfiguration<User>
|
|
62
|
+
{
|
|
63
|
+
public void Configure(EntityTypeBuilder<User> builder)
|
|
64
|
+
{
|
|
65
|
+
builder.ToTable("users");
|
|
66
|
+
|
|
67
|
+
builder.HasKey(u => u.Id);
|
|
68
|
+
|
|
69
|
+
builder.Property(u => u.Email)
|
|
70
|
+
.HasMaxLength(255)
|
|
71
|
+
.IsRequired();
|
|
72
|
+
|
|
73
|
+
builder.HasIndex(u => u.Email)
|
|
74
|
+
.IsUnique();
|
|
75
|
+
|
|
76
|
+
builder.Property(u => u.Name)
|
|
77
|
+
.HasMaxLength(100)
|
|
78
|
+
.IsRequired();
|
|
79
|
+
|
|
80
|
+
builder.Property(u => u.PasswordHash)
|
|
81
|
+
.HasColumnName("password_hash")
|
|
82
|
+
.IsRequired();
|
|
83
|
+
|
|
84
|
+
// One-to-one
|
|
85
|
+
builder.HasOne(u => u.Profile)
|
|
86
|
+
.WithOne(p => p.User)
|
|
87
|
+
.HasForeignKey<Profile>(p => p.UserId)
|
|
88
|
+
.OnDelete(DeleteBehavior.Cascade);
|
|
89
|
+
|
|
90
|
+
// One-to-many
|
|
91
|
+
builder.HasMany(u => u.Posts)
|
|
92
|
+
.WithOne(p => p.Author)
|
|
93
|
+
.HasForeignKey(p => p.AuthorId)
|
|
94
|
+
.OnDelete(DeleteBehavior.Cascade);
|
|
95
|
+
|
|
96
|
+
// Many-to-many
|
|
97
|
+
builder.HasMany(u => u.Roles)
|
|
98
|
+
.WithMany(r => r.Users)
|
|
99
|
+
.UsingEntity<Dictionary<string, object>>(
|
|
100
|
+
"user_roles",
|
|
101
|
+
j => j.HasOne<Role>().WithMany().HasForeignKey("role_id"),
|
|
102
|
+
j => j.HasOne<User>().WithMany().HasForeignKey("user_id"));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
\`\`\`
|
|
106
|
+
|
|
107
|
+
## Migrations
|
|
108
|
+
\`\`\`bash
|
|
109
|
+
# Add migration
|
|
110
|
+
dotnet ef migrations add CreateUsersTable
|
|
111
|
+
|
|
112
|
+
# Update database
|
|
113
|
+
dotnet ef database update
|
|
114
|
+
|
|
115
|
+
# Generate SQL script
|
|
116
|
+
dotnet ef migrations script -o migrations.sql
|
|
117
|
+
|
|
118
|
+
# Revert to specific migration
|
|
119
|
+
dotnet ef database update PreviousMigration
|
|
120
|
+
|
|
121
|
+
# Remove last migration (if not applied)
|
|
122
|
+
dotnet ef migrations remove
|
|
123
|
+
\`\`\`
|
|
124
|
+
|
|
125
|
+
## Queries
|
|
126
|
+
\`\`\`csharp
|
|
127
|
+
public class UserRepository : IUserRepository
|
|
128
|
+
{
|
|
129
|
+
private readonly AppDbContext _context;
|
|
130
|
+
|
|
131
|
+
public UserRepository(AppDbContext context)
|
|
132
|
+
{
|
|
133
|
+
_context = context;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Basic queries
|
|
137
|
+
public async Task<User?> GetByIdAsync(string id, CancellationToken ct = default)
|
|
138
|
+
{
|
|
139
|
+
return await _context.Users.FindAsync(new object[] { id }, ct);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public async Task<User?> GetByEmailAsync(string email, CancellationToken ct = default)
|
|
143
|
+
{
|
|
144
|
+
return await _context.Users
|
|
145
|
+
.FirstOrDefaultAsync(u => u.Email == email, ct);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Eager loading
|
|
149
|
+
public async Task<User?> GetWithPostsAsync(string id, CancellationToken ct = default)
|
|
150
|
+
{
|
|
151
|
+
return await _context.Users
|
|
152
|
+
.Include(u => u.Posts)
|
|
153
|
+
.Include(u => u.Profile)
|
|
154
|
+
.FirstOrDefaultAsync(u => u.Id == id, ct);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Filtered include
|
|
158
|
+
public async Task<User?> GetWithActivePostsAsync(string id, CancellationToken ct = default)
|
|
159
|
+
{
|
|
160
|
+
return await _context.Users
|
|
161
|
+
.Include(u => u.Posts.Where(p => p.Published))
|
|
162
|
+
.FirstOrDefaultAsync(u => u.Id == id, ct);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Pagination
|
|
166
|
+
public async Task<(List<User> Users, int Total)> GetPagedAsync(
|
|
167
|
+
int page, int pageSize, CancellationToken ct = default)
|
|
168
|
+
{
|
|
169
|
+
var query = _context.Users.Where(u => u.IsActive);
|
|
170
|
+
|
|
171
|
+
var total = await query.CountAsync(ct);
|
|
172
|
+
var users = await query
|
|
173
|
+
.OrderByDescending(u => u.CreatedAt)
|
|
174
|
+
.Skip((page - 1) * pageSize)
|
|
175
|
+
.Take(pageSize)
|
|
176
|
+
.ToListAsync(ct);
|
|
177
|
+
|
|
178
|
+
return (users, total);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Projection
|
|
182
|
+
public async Task<List<UserSummary>> GetSummariesAsync(CancellationToken ct = default)
|
|
183
|
+
{
|
|
184
|
+
return await _context.Users
|
|
185
|
+
.Where(u => u.IsActive)
|
|
186
|
+
.Select(u => new UserSummary(u.Id, u.Email, u.Name))
|
|
187
|
+
.ToListAsync(ct);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Raw SQL
|
|
191
|
+
public async Task<List<User>> SearchAsync(string query, CancellationToken ct = default)
|
|
192
|
+
{
|
|
193
|
+
return await _context.Users
|
|
194
|
+
.FromSqlInterpolated($"SELECT * FROM users WHERE name ILIKE {'%' + query + '%'}")
|
|
195
|
+
.ToListAsync(ct);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public record UserSummary(string Id, string Email, string Name);
|
|
200
|
+
\`\`\`
|
|
201
|
+
|
|
202
|
+
## CRUD Operations
|
|
203
|
+
\`\`\`csharp
|
|
204
|
+
// Create
|
|
205
|
+
public async Task<User> CreateAsync(CreateUserRequest request, CancellationToken ct = default)
|
|
206
|
+
{
|
|
207
|
+
var user = new User
|
|
208
|
+
{
|
|
209
|
+
Email = request.Email,
|
|
210
|
+
Name = request.Name,
|
|
211
|
+
PasswordHash = HashPassword(request.Password)
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
_context.Users.Add(user);
|
|
215
|
+
await _context.SaveChangesAsync(ct);
|
|
216
|
+
|
|
217
|
+
return user;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Update
|
|
221
|
+
public async Task<User?> UpdateAsync(
|
|
222
|
+
string id, UpdateUserRequest request, CancellationToken ct = default)
|
|
223
|
+
{
|
|
224
|
+
var user = await _context.Users.FindAsync(new object[] { id }, ct);
|
|
225
|
+
if (user is null) return null;
|
|
226
|
+
|
|
227
|
+
if (request.Name is not null)
|
|
228
|
+
user.Name = request.Name;
|
|
229
|
+
if (request.Email is not null)
|
|
230
|
+
user.Email = request.Email;
|
|
231
|
+
|
|
232
|
+
await _context.SaveChangesAsync(ct);
|
|
233
|
+
return user;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Delete
|
|
237
|
+
public async Task<bool> DeleteAsync(string id, CancellationToken ct = default)
|
|
238
|
+
{
|
|
239
|
+
var user = await _context.Users.FindAsync(new object[] { id }, ct);
|
|
240
|
+
if (user is null) return false;
|
|
241
|
+
|
|
242
|
+
_context.Users.Remove(user);
|
|
243
|
+
await _context.SaveChangesAsync(ct);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Bulk update
|
|
248
|
+
public async Task DeactivateOldUsersAsync(DateTime before, CancellationToken ct = default)
|
|
249
|
+
{
|
|
250
|
+
await _context.Users
|
|
251
|
+
.Where(u => u.CreatedAt < before && u.IsActive)
|
|
252
|
+
.ExecuteUpdateAsync(s => s.SetProperty(u => u.IsActive, false), ct);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Bulk delete
|
|
256
|
+
public async Task DeleteInactiveAsync(CancellationToken ct = default)
|
|
257
|
+
{
|
|
258
|
+
await _context.Users
|
|
259
|
+
.Where(u => !u.IsActive)
|
|
260
|
+
.ExecuteDeleteAsync(ct);
|
|
261
|
+
}
|
|
262
|
+
\`\`\`
|
|
263
|
+
|
|
264
|
+
## Transactions
|
|
265
|
+
\`\`\`csharp
|
|
266
|
+
public async Task CreateUserWithProfileAsync(
|
|
267
|
+
CreateUserRequest request, CreateProfileRequest profileRequest, CancellationToken ct = default)
|
|
268
|
+
{
|
|
269
|
+
await using var transaction = await _context.Database.BeginTransactionAsync(ct);
|
|
270
|
+
|
|
271
|
+
try
|
|
272
|
+
{
|
|
273
|
+
var user = new User
|
|
274
|
+
{
|
|
275
|
+
Email = request.Email,
|
|
276
|
+
Name = request.Name,
|
|
277
|
+
PasswordHash = HashPassword(request.Password)
|
|
278
|
+
};
|
|
279
|
+
_context.Users.Add(user);
|
|
280
|
+
await _context.SaveChangesAsync(ct);
|
|
281
|
+
|
|
282
|
+
var profile = new Profile
|
|
283
|
+
{
|
|
284
|
+
UserId = user.Id,
|
|
285
|
+
Bio = profileRequest.Bio
|
|
286
|
+
};
|
|
287
|
+
_context.Profiles.Add(profile);
|
|
288
|
+
await _context.SaveChangesAsync(ct);
|
|
289
|
+
|
|
290
|
+
await transaction.CommitAsync(ct);
|
|
291
|
+
}
|
|
292
|
+
catch
|
|
293
|
+
{
|
|
294
|
+
await transaction.RollbackAsync(ct);
|
|
295
|
+
throw;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// With execution strategy (for retries)
|
|
300
|
+
public async Task CreateWithRetryAsync(User user, CancellationToken ct = default)
|
|
301
|
+
{
|
|
302
|
+
var strategy = _context.Database.CreateExecutionStrategy();
|
|
303
|
+
|
|
304
|
+
await strategy.ExecuteAsync(async () =>
|
|
305
|
+
{
|
|
306
|
+
await using var transaction = await _context.Database.BeginTransactionAsync(ct);
|
|
307
|
+
|
|
308
|
+
_context.Users.Add(user);
|
|
309
|
+
await _context.SaveChangesAsync(ct);
|
|
310
|
+
// More operations...
|
|
311
|
+
|
|
312
|
+
await transaction.CommitAsync(ct);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
\`\`\`
|
|
316
|
+
|
|
317
|
+
## Change Tracking
|
|
318
|
+
\`\`\`csharp
|
|
319
|
+
// No-tracking queries (read-only, better performance)
|
|
320
|
+
public async Task<List<User>> GetAllReadOnlyAsync(CancellationToken ct = default)
|
|
321
|
+
{
|
|
322
|
+
return await _context.Users
|
|
323
|
+
.AsNoTracking()
|
|
324
|
+
.ToListAsync(ct);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Disable tracking globally for read-heavy scenarios
|
|
328
|
+
services.AddDbContext<AppDbContext>(options =>
|
|
329
|
+
options.UseNpgsql(connectionString)
|
|
330
|
+
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
|
|
331
|
+
|
|
332
|
+
// Check entity state
|
|
333
|
+
var entry = _context.Entry(user);
|
|
334
|
+
var state = entry.State; // Added, Modified, Deleted, Unchanged, Detached
|
|
335
|
+
|
|
336
|
+
// Explicit state change
|
|
337
|
+
_context.Entry(user).State = EntityState.Modified;
|
|
338
|
+
|
|
339
|
+
// Attach detached entity
|
|
340
|
+
_context.Users.Attach(user);
|
|
341
|
+
\`\`\`
|
|
342
|
+
|
|
343
|
+
## Compiled Queries
|
|
344
|
+
\`\`\`csharp
|
|
345
|
+
// For frequently executed queries
|
|
346
|
+
private static readonly Func<AppDbContext, string, Task<User?>> GetByEmailQuery =
|
|
347
|
+
EF.CompileAsyncQuery((AppDbContext ctx, string email) =>
|
|
348
|
+
ctx.Users.FirstOrDefault(u => u.Email == email));
|
|
349
|
+
|
|
350
|
+
public async Task<User?> GetByEmailOptimizedAsync(string email)
|
|
351
|
+
{
|
|
352
|
+
return await GetByEmailQuery(_context, email);
|
|
353
|
+
}
|
|
354
|
+
\`\`\`
|
|
355
|
+
|
|
356
|
+
## ✅ DO
|
|
357
|
+
- Use \`IEntityTypeConfiguration<T>\` for entity configuration
|
|
358
|
+
- Use \`AsNoTracking()\` for read-only queries
|
|
359
|
+
- Use projections (\`Select\`) to fetch only needed fields
|
|
360
|
+
- Use \`Include\` for eager loading to avoid N+1
|
|
361
|
+
- Use \`ExecuteUpdateAsync\`/\`ExecuteDeleteAsync\` for bulk operations
|
|
362
|
+
- Pass \`CancellationToken\` to all async methods
|
|
363
|
+
|
|
364
|
+
## ❌ DON'T
|
|
365
|
+
- Don't call \`ToList()\` before \`Where()\` (fetches all rows)
|
|
366
|
+
- Don't use lazy loading (enabled by default in some versions)
|
|
367
|
+
- Don't forget \`AsNoTracking()\` for read-only queries
|
|
368
|
+
- Don't use \`Find()\` with includes (use \`FirstOrDefault\`)
|
|
369
|
+
- Don't expose \`DbContext\` outside repository layer
|
|
370
|
+
- Don't ignore migration conflicts - resolve them properly
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# Express.js Skill
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
\`\`\`
|
|
5
|
+
src/
|
|
6
|
+
├── index.ts # App entry
|
|
7
|
+
├── app.ts # Express app setup
|
|
8
|
+
├── routes/
|
|
9
|
+
│ ├── index.ts # Route aggregator
|
|
10
|
+
│ └── users.ts # User routes
|
|
11
|
+
├── controllers/
|
|
12
|
+
│ └── users.ts # Route handlers
|
|
13
|
+
├── middleware/
|
|
14
|
+
│ ├── auth.ts # Authentication
|
|
15
|
+
│ ├── validate.ts # Validation
|
|
16
|
+
│ └── errorHandler.ts # Error handling
|
|
17
|
+
├── services/
|
|
18
|
+
│ └── users.ts # Business logic
|
|
19
|
+
└── types/
|
|
20
|
+
└── express.d.ts # Type extensions
|
|
21
|
+
\`\`\`
|
|
22
|
+
|
|
23
|
+
## App Setup
|
|
24
|
+
\`\`\`typescript
|
|
25
|
+
import express from 'express';
|
|
26
|
+
import cors from 'cors';
|
|
27
|
+
import helmet from 'helmet';
|
|
28
|
+
import { router } from './routes';
|
|
29
|
+
import { errorHandler } from './middleware/errorHandler';
|
|
30
|
+
|
|
31
|
+
const app = express();
|
|
32
|
+
|
|
33
|
+
// Security middleware
|
|
34
|
+
app.use(helmet());
|
|
35
|
+
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
|
|
36
|
+
|
|
37
|
+
// Body parsing
|
|
38
|
+
app.use(express.json({ limit: '10kb' }));
|
|
39
|
+
app.use(express.urlencoded({ extended: true }));
|
|
40
|
+
|
|
41
|
+
// Request logging
|
|
42
|
+
app.use((req, res, next) => {
|
|
43
|
+
console.log(\`\${req.method} \${req.path}\`);
|
|
44
|
+
next();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Routes
|
|
48
|
+
app.use('/api', router);
|
|
49
|
+
|
|
50
|
+
// 404 handler
|
|
51
|
+
app.use((req, res) => {
|
|
52
|
+
res.status(404).json({ error: 'Not found' });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Error handler (must be last)
|
|
56
|
+
app.use(errorHandler);
|
|
57
|
+
|
|
58
|
+
export { app };
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
## Router Pattern
|
|
62
|
+
\`\`\`typescript
|
|
63
|
+
// routes/users.ts
|
|
64
|
+
import { Router } from 'express';
|
|
65
|
+
import { auth } from '../middleware/auth';
|
|
66
|
+
import { validate } from '../middleware/validate';
|
|
67
|
+
import { createUserSchema, updateUserSchema } from '../schemas/user';
|
|
68
|
+
import * as userController from '../controllers/users';
|
|
69
|
+
|
|
70
|
+
const router = Router();
|
|
71
|
+
|
|
72
|
+
router.get('/', userController.getUsers);
|
|
73
|
+
router.get('/:id', userController.getUser);
|
|
74
|
+
router.post('/', validate(createUserSchema), userController.createUser);
|
|
75
|
+
router.put('/:id', auth, validate(updateUserSchema), userController.updateUser);
|
|
76
|
+
router.delete('/:id', auth, userController.deleteUser);
|
|
77
|
+
|
|
78
|
+
export { router as userRouter };
|
|
79
|
+
|
|
80
|
+
// routes/index.ts
|
|
81
|
+
import { Router } from 'express';
|
|
82
|
+
import { userRouter } from './users';
|
|
83
|
+
|
|
84
|
+
const router = Router();
|
|
85
|
+
|
|
86
|
+
router.use('/users', userRouter);
|
|
87
|
+
|
|
88
|
+
export { router };
|
|
89
|
+
\`\`\`
|
|
90
|
+
|
|
91
|
+
## Controller Pattern
|
|
92
|
+
\`\`\`typescript
|
|
93
|
+
// controllers/users.ts
|
|
94
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
95
|
+
import { userService } from '../services/users';
|
|
96
|
+
|
|
97
|
+
export const getUsers = async (req: Request, res: Response, next: NextFunction) => {
|
|
98
|
+
try {
|
|
99
|
+
const { page = 1, limit = 10 } = req.query;
|
|
100
|
+
const users = await userService.findAll({ page: +page, limit: +limit });
|
|
101
|
+
res.json(users);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
next(error);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const getUser = async (req: Request, res: Response, next: NextFunction) => {
|
|
108
|
+
try {
|
|
109
|
+
const user = await userService.findById(req.params.id);
|
|
110
|
+
if (!user) {
|
|
111
|
+
return res.status(404).json({ error: 'User not found' });
|
|
112
|
+
}
|
|
113
|
+
res.json(user);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
next(error);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const createUser = async (req: Request, res: Response, next: NextFunction) => {
|
|
120
|
+
try {
|
|
121
|
+
const user = await userService.create(req.body);
|
|
122
|
+
res.status(201).json(user);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
next(error);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
\`\`\`
|
|
128
|
+
|
|
129
|
+
## Validation Middleware
|
|
130
|
+
\`\`\`typescript
|
|
131
|
+
// middleware/validate.ts
|
|
132
|
+
import { z, ZodSchema } from 'zod';
|
|
133
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
134
|
+
|
|
135
|
+
export const validate = (schema: ZodSchema) => {
|
|
136
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
137
|
+
const result = schema.safeParse({
|
|
138
|
+
body: req.body,
|
|
139
|
+
query: req.query,
|
|
140
|
+
params: req.params,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!result.success) {
|
|
144
|
+
return res.status(400).json({
|
|
145
|
+
error: 'Validation failed',
|
|
146
|
+
details: result.error.flatten(),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
req.body = result.data.body;
|
|
151
|
+
next();
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// schemas/user.ts
|
|
156
|
+
export const createUserSchema = z.object({
|
|
157
|
+
body: z.object({
|
|
158
|
+
email: z.string().email(),
|
|
159
|
+
name: z.string().min(1).max(100),
|
|
160
|
+
password: z.string().min(8),
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
163
|
+
\`\`\`
|
|
164
|
+
|
|
165
|
+
## Authentication Middleware
|
|
166
|
+
\`\`\`typescript
|
|
167
|
+
// middleware/auth.ts
|
|
168
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
169
|
+
import jwt from 'jsonwebtoken';
|
|
170
|
+
|
|
171
|
+
// Extend Express Request type
|
|
172
|
+
declare global {
|
|
173
|
+
namespace Express {
|
|
174
|
+
interface Request {
|
|
175
|
+
user?: { id: string; email: string };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const auth = async (req: Request, res: Response, next: NextFunction) => {
|
|
181
|
+
const authHeader = req.headers.authorization;
|
|
182
|
+
|
|
183
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
184
|
+
return res.status(401).json({ error: 'No token provided' });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const token = authHeader.split(' ')[1];
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const payload = jwt.verify(token, process.env.JWT_SECRET!) as { id: string; email: string };
|
|
191
|
+
req.user = payload;
|
|
192
|
+
next();
|
|
193
|
+
} catch {
|
|
194
|
+
return res.status(401).json({ error: 'Invalid token' });
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Optional auth (doesn't fail if no token)
|
|
199
|
+
export const optionalAuth = async (req: Request, res: Response, next: NextFunction) => {
|
|
200
|
+
const authHeader = req.headers.authorization;
|
|
201
|
+
|
|
202
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
203
|
+
try {
|
|
204
|
+
const token = authHeader.split(' ')[1];
|
|
205
|
+
req.user = jwt.verify(token, process.env.JWT_SECRET!) as any;
|
|
206
|
+
} catch {
|
|
207
|
+
// Ignore invalid tokens for optional auth
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
next();
|
|
212
|
+
};
|
|
213
|
+
\`\`\`
|
|
214
|
+
|
|
215
|
+
## Error Handler
|
|
216
|
+
\`\`\`typescript
|
|
217
|
+
// middleware/errorHandler.ts
|
|
218
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
219
|
+
|
|
220
|
+
class AppError extends Error {
|
|
221
|
+
constructor(
|
|
222
|
+
public statusCode: number,
|
|
223
|
+
public message: string,
|
|
224
|
+
public isOperational = true
|
|
225
|
+
) {
|
|
226
|
+
super(message);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const errorHandler = (
|
|
231
|
+
err: Error,
|
|
232
|
+
req: Request,
|
|
233
|
+
res: Response,
|
|
234
|
+
next: NextFunction
|
|
235
|
+
) => {
|
|
236
|
+
// Log error
|
|
237
|
+
console.error(err);
|
|
238
|
+
|
|
239
|
+
if (err instanceof AppError) {
|
|
240
|
+
return res.status(err.statusCode).json({
|
|
241
|
+
error: err.message,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Don't leak error details in production
|
|
246
|
+
res.status(500).json({
|
|
247
|
+
error: process.env.NODE_ENV === 'production'
|
|
248
|
+
? 'Internal server error'
|
|
249
|
+
: err.message,
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export { AppError };
|
|
254
|
+
\`\`\`
|
|
255
|
+
|
|
256
|
+
## Async Handler Wrapper
|
|
257
|
+
\`\`\`typescript
|
|
258
|
+
// utils/asyncHandler.ts
|
|
259
|
+
import type { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
260
|
+
|
|
261
|
+
type AsyncHandler = (
|
|
262
|
+
req: Request,
|
|
263
|
+
res: Response,
|
|
264
|
+
next: NextFunction
|
|
265
|
+
) => Promise<any>;
|
|
266
|
+
|
|
267
|
+
export const asyncHandler = (fn: AsyncHandler): RequestHandler => {
|
|
268
|
+
return (req, res, next) => {
|
|
269
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Usage
|
|
274
|
+
router.get('/users', asyncHandler(async (req, res) => {
|
|
275
|
+
const users = await userService.findAll();
|
|
276
|
+
res.json(users);
|
|
277
|
+
}));
|
|
278
|
+
\`\`\`
|
|
279
|
+
|
|
280
|
+
## File Upload
|
|
281
|
+
\`\`\`typescript
|
|
282
|
+
import multer from 'multer';
|
|
283
|
+
|
|
284
|
+
const upload = multer({
|
|
285
|
+
storage: multer.memoryStorage(),
|
|
286
|
+
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
|
|
287
|
+
fileFilter: (req, file, cb) => {
|
|
288
|
+
if (file.mimetype.startsWith('image/')) {
|
|
289
|
+
cb(null, true);
|
|
290
|
+
} else {
|
|
291
|
+
cb(new Error('Only images allowed'));
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
router.post('/upload', upload.single('image'), async (req, res) => {
|
|
297
|
+
const file = req.file;
|
|
298
|
+
// Process file...
|
|
299
|
+
res.json({ url: uploadedUrl });
|
|
300
|
+
});
|
|
301
|
+
\`\`\`
|
|
302
|
+
|
|
303
|
+
## ❌ DON'T
|
|
304
|
+
- Use synchronous error handling (use async/await + next)
|
|
305
|
+
- Forget to call next(error) in catch blocks
|
|
306
|
+
- Put business logic in controllers
|
|
307
|
+
- Trust req.body without validation
|
|
308
|
+
- Expose stack traces in production
|
|
309
|
+
|
|
310
|
+
## ✅ DO
|
|
311
|
+
- Use middleware for cross-cutting concerns
|
|
312
|
+
- Validate all request bodies with Zod
|
|
313
|
+
- Handle errors centrally with error middleware
|
|
314
|
+
- Use async wrapper or try/catch with next(error)
|
|
315
|
+
- Extend Request type for custom properties
|
|
316
|
+
- Separate routes, controllers, and services
|