@malamute/ai-rules 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.
- package/README.md +174 -0
- package/bin/cli.js +5 -0
- package/configs/_shared/.claude/commands/fix-issue.md +38 -0
- package/configs/_shared/.claude/commands/generate-tests.md +49 -0
- package/configs/_shared/.claude/commands/review-pr.md +77 -0
- package/configs/_shared/.claude/rules/accessibility.md +270 -0
- package/configs/_shared/.claude/rules/performance.md +226 -0
- package/configs/_shared/.claude/rules/security.md +188 -0
- package/configs/_shared/.claude/skills/debug/SKILL.md +118 -0
- package/configs/_shared/.claude/skills/learning/SKILL.md +224 -0
- package/configs/_shared/.claude/skills/review/SKILL.md +86 -0
- package/configs/_shared/.claude/skills/spec/SKILL.md +112 -0
- package/configs/_shared/CLAUDE.md +174 -0
- package/configs/angular/.claude/rules/components.md +257 -0
- package/configs/angular/.claude/rules/state.md +250 -0
- package/configs/angular/.claude/rules/testing.md +422 -0
- package/configs/angular/.claude/settings.json +31 -0
- package/configs/angular/CLAUDE.md +251 -0
- package/configs/dotnet/.claude/rules/api.md +370 -0
- package/configs/dotnet/.claude/rules/architecture.md +199 -0
- package/configs/dotnet/.claude/rules/database/efcore.md +408 -0
- package/configs/dotnet/.claude/rules/testing.md +389 -0
- package/configs/dotnet/.claude/settings.json +9 -0
- package/configs/dotnet/CLAUDE.md +319 -0
- package/configs/nestjs/.claude/rules/auth.md +321 -0
- package/configs/nestjs/.claude/rules/database/prisma.md +305 -0
- package/configs/nestjs/.claude/rules/database/typeorm.md +379 -0
- package/configs/nestjs/.claude/rules/modules.md +215 -0
- package/configs/nestjs/.claude/rules/testing.md +315 -0
- package/configs/nestjs/.claude/rules/validation.md +279 -0
- package/configs/nestjs/.claude/settings.json +15 -0
- package/configs/nestjs/CLAUDE.md +263 -0
- package/configs/nextjs/.claude/rules/components.md +211 -0
- package/configs/nextjs/.claude/rules/state/redux-toolkit.md +429 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +299 -0
- package/configs/nextjs/.claude/rules/testing.md +315 -0
- package/configs/nextjs/.claude/settings.json +29 -0
- package/configs/nextjs/CLAUDE.md +376 -0
- package/configs/python/.claude/rules/database/sqlalchemy.md +355 -0
- package/configs/python/.claude/rules/fastapi.md +272 -0
- package/configs/python/.claude/rules/flask.md +332 -0
- package/configs/python/.claude/rules/testing.md +374 -0
- package/configs/python/.claude/settings.json +18 -0
- package/configs/python/CLAUDE.md +273 -0
- package/package.json +41 -0
- package/src/install.js +315 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/Infrastructure/**/*.cs"
|
|
4
|
+
- "**/*DbContext*.cs"
|
|
5
|
+
- "**/*Repository*.cs"
|
|
6
|
+
- "**/Configurations/**/*.cs"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Entity Framework Core Rules
|
|
10
|
+
|
|
11
|
+
## DbContext Setup
|
|
12
|
+
|
|
13
|
+
```csharp
|
|
14
|
+
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
|
15
|
+
: DbContext(options), IApplicationDbContext
|
|
16
|
+
{
|
|
17
|
+
public DbSet<User> Users => Set<User>();
|
|
18
|
+
public DbSet<Post> Posts => Set<Post>();
|
|
19
|
+
|
|
20
|
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
21
|
+
{
|
|
22
|
+
// Apply all configurations from assembly
|
|
23
|
+
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
|
|
24
|
+
|
|
25
|
+
base.OnModelCreating(modelBuilder);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
|
29
|
+
{
|
|
30
|
+
// Audit timestamps
|
|
31
|
+
foreach (var entry in ChangeTracker.Entries<BaseEntity>())
|
|
32
|
+
{
|
|
33
|
+
switch (entry.State)
|
|
34
|
+
{
|
|
35
|
+
case EntityState.Added:
|
|
36
|
+
entry.Entity.CreatedAt = DateTime.UtcNow;
|
|
37
|
+
break;
|
|
38
|
+
case EntityState.Modified:
|
|
39
|
+
entry.Entity.UpdatedAt = DateTime.UtcNow;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return await base.SaveChangesAsync(cancellationToken);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Entity Configuration
|
|
50
|
+
|
|
51
|
+
### Fluent Configuration (Preferred)
|
|
52
|
+
|
|
53
|
+
```csharp
|
|
54
|
+
// Configurations/UserConfiguration.cs
|
|
55
|
+
public class UserConfiguration : IEntityTypeConfiguration<User>
|
|
56
|
+
{
|
|
57
|
+
public void Configure(EntityTypeBuilder<User> builder)
|
|
58
|
+
{
|
|
59
|
+
builder.ToTable("users");
|
|
60
|
+
|
|
61
|
+
builder.HasKey(u => u.Id);
|
|
62
|
+
|
|
63
|
+
builder.Property(u => u.Id)
|
|
64
|
+
.HasColumnName("id")
|
|
65
|
+
.ValueGeneratedNever(); // Use app-generated GUIDs
|
|
66
|
+
|
|
67
|
+
builder.Property(u => u.Email)
|
|
68
|
+
.HasColumnName("email")
|
|
69
|
+
.HasMaxLength(256)
|
|
70
|
+
.IsRequired();
|
|
71
|
+
|
|
72
|
+
builder.HasIndex(u => u.Email)
|
|
73
|
+
.IsUnique();
|
|
74
|
+
|
|
75
|
+
builder.Property(u => u.PasswordHash)
|
|
76
|
+
.HasColumnName("password_hash")
|
|
77
|
+
.HasMaxLength(256)
|
|
78
|
+
.IsRequired();
|
|
79
|
+
|
|
80
|
+
builder.Property(u => u.Name)
|
|
81
|
+
.HasColumnName("name")
|
|
82
|
+
.HasMaxLength(100);
|
|
83
|
+
|
|
84
|
+
builder.Property(u => u.Role)
|
|
85
|
+
.HasColumnName("role")
|
|
86
|
+
.HasConversion<string>()
|
|
87
|
+
.HasMaxLength(50);
|
|
88
|
+
|
|
89
|
+
builder.Property(u => u.CreatedAt)
|
|
90
|
+
.HasColumnName("created_at")
|
|
91
|
+
.HasDefaultValueSql("GETUTCDATE()");
|
|
92
|
+
|
|
93
|
+
builder.Property(u => u.UpdatedAt)
|
|
94
|
+
.HasColumnName("updated_at");
|
|
95
|
+
|
|
96
|
+
// Relationships
|
|
97
|
+
builder.HasMany(u => u.Posts)
|
|
98
|
+
.WithOne(p => p.Author)
|
|
99
|
+
.HasForeignKey(p => p.AuthorId)
|
|
100
|
+
.OnDelete(DeleteBehavior.Cascade);
|
|
101
|
+
|
|
102
|
+
// Value Object
|
|
103
|
+
builder.OwnsOne(u => u.Address, address =>
|
|
104
|
+
{
|
|
105
|
+
address.Property(a => a.Street).HasColumnName("address_street");
|
|
106
|
+
address.Property(a => a.City).HasColumnName("address_city");
|
|
107
|
+
address.Property(a => a.ZipCode).HasColumnName("address_zip");
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Naming Conventions
|
|
114
|
+
|
|
115
|
+
| C# | Database |
|
|
116
|
+
|----|----------|
|
|
117
|
+
| PascalCase properties | snake_case columns |
|
|
118
|
+
| PascalCase entities | snake_case tables |
|
|
119
|
+
|
|
120
|
+
```csharp
|
|
121
|
+
// Global snake_case convention
|
|
122
|
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
123
|
+
{
|
|
124
|
+
foreach (var entity in modelBuilder.Model.GetEntityTypes())
|
|
125
|
+
{
|
|
126
|
+
// Table name
|
|
127
|
+
entity.SetTableName(ToSnakeCase(entity.GetTableName()!));
|
|
128
|
+
|
|
129
|
+
// Column names
|
|
130
|
+
foreach (var property in entity.GetProperties())
|
|
131
|
+
{
|
|
132
|
+
property.SetColumnName(ToSnakeCase(property.Name));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Foreign keys
|
|
136
|
+
foreach (var key in entity.GetForeignKeys())
|
|
137
|
+
{
|
|
138
|
+
key.SetConstraintName(ToSnakeCase(key.GetConstraintName()!));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private static string ToSnakeCase(string name)
|
|
144
|
+
{
|
|
145
|
+
return string.Concat(name.Select((c, i) =>
|
|
146
|
+
i > 0 && char.IsUpper(c) ? "_" + c : c.ToString()))
|
|
147
|
+
.ToLower();
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Repository Pattern
|
|
152
|
+
|
|
153
|
+
```csharp
|
|
154
|
+
// Domain/Interfaces/IRepository.cs
|
|
155
|
+
public interface IRepository<T> where T : class
|
|
156
|
+
{
|
|
157
|
+
Task<T?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
|
158
|
+
Task<IReadOnlyList<T>> GetAllAsync(CancellationToken cancellationToken = default);
|
|
159
|
+
Task AddAsync(T entity, CancellationToken cancellationToken = default);
|
|
160
|
+
void Update(T entity);
|
|
161
|
+
void Remove(T entity);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Infrastructure/Repositories/Repository.cs
|
|
165
|
+
public class Repository<T>(ApplicationDbContext context) : IRepository<T>
|
|
166
|
+
where T : class
|
|
167
|
+
{
|
|
168
|
+
protected readonly DbSet<T> DbSet = context.Set<T>();
|
|
169
|
+
|
|
170
|
+
public virtual async Task<T?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
|
171
|
+
{
|
|
172
|
+
return await DbSet.FindAsync([id], cancellationToken);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public virtual async Task<IReadOnlyList<T>> GetAllAsync(CancellationToken cancellationToken = default)
|
|
176
|
+
{
|
|
177
|
+
return await DbSet.ToListAsync(cancellationToken);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public async Task AddAsync(T entity, CancellationToken cancellationToken = default)
|
|
181
|
+
{
|
|
182
|
+
await DbSet.AddAsync(entity, cancellationToken);
|
|
183
|
+
await context.SaveChangesAsync(cancellationToken);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
public void Update(T entity)
|
|
187
|
+
{
|
|
188
|
+
DbSet.Update(entity);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
public void Remove(T entity)
|
|
192
|
+
{
|
|
193
|
+
DbSet.Remove(entity);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Query Patterns
|
|
199
|
+
|
|
200
|
+
### Specification Pattern
|
|
201
|
+
|
|
202
|
+
```csharp
|
|
203
|
+
// Base specification
|
|
204
|
+
public abstract class Specification<T>
|
|
205
|
+
{
|
|
206
|
+
public abstract Expression<Func<T, bool>> ToExpression();
|
|
207
|
+
|
|
208
|
+
public bool IsSatisfiedBy(T entity)
|
|
209
|
+
{
|
|
210
|
+
return ToExpression().Compile()(entity);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Concrete specification
|
|
215
|
+
public class ActiveUsersSpecification : Specification<User>
|
|
216
|
+
{
|
|
217
|
+
public override Expression<Func<User, bool>> ToExpression()
|
|
218
|
+
{
|
|
219
|
+
return user => user.IsActive && user.EmailVerified;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Usage in repository
|
|
224
|
+
public async Task<IReadOnlyList<User>> GetBySpecificationAsync(
|
|
225
|
+
Specification<User> spec,
|
|
226
|
+
CancellationToken cancellationToken = default)
|
|
227
|
+
{
|
|
228
|
+
return await DbSet
|
|
229
|
+
.Where(spec.ToExpression())
|
|
230
|
+
.ToListAsync(cancellationToken);
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Pagination
|
|
235
|
+
|
|
236
|
+
```csharp
|
|
237
|
+
public async Task<PaginatedList<User>> GetPaginatedAsync(
|
|
238
|
+
int page,
|
|
239
|
+
int pageSize,
|
|
240
|
+
CancellationToken cancellationToken = default)
|
|
241
|
+
{
|
|
242
|
+
var query = DbSet.AsNoTracking();
|
|
243
|
+
|
|
244
|
+
var totalCount = await query.CountAsync(cancellationToken);
|
|
245
|
+
|
|
246
|
+
var items = await query
|
|
247
|
+
.OrderBy(u => u.CreatedAt)
|
|
248
|
+
.Skip((page - 1) * pageSize)
|
|
249
|
+
.Take(pageSize)
|
|
250
|
+
.ToListAsync(cancellationToken);
|
|
251
|
+
|
|
252
|
+
return new PaginatedList<User>(items, totalCount, page, pageSize);
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Efficient Queries
|
|
257
|
+
|
|
258
|
+
```csharp
|
|
259
|
+
// Use AsNoTracking for read-only queries
|
|
260
|
+
var users = await context.Users
|
|
261
|
+
.AsNoTracking()
|
|
262
|
+
.Where(u => u.IsActive)
|
|
263
|
+
.ToListAsync();
|
|
264
|
+
|
|
265
|
+
// Project to DTO directly (avoid loading full entity)
|
|
266
|
+
var userDtos = await context.Users
|
|
267
|
+
.AsNoTracking()
|
|
268
|
+
.Where(u => u.IsActive)
|
|
269
|
+
.Select(u => new UserDto(u.Id, u.Email, u.Name))
|
|
270
|
+
.ToListAsync();
|
|
271
|
+
|
|
272
|
+
// Explicit loading for related data
|
|
273
|
+
var user = await context.Users.FindAsync(id);
|
|
274
|
+
await context.Entry(user)
|
|
275
|
+
.Collection(u => u.Posts)
|
|
276
|
+
.LoadAsync();
|
|
277
|
+
|
|
278
|
+
// Split queries for large includes
|
|
279
|
+
var users = await context.Users
|
|
280
|
+
.Include(u => u.Posts)
|
|
281
|
+
.Include(u => u.Comments)
|
|
282
|
+
.AsSplitQuery()
|
|
283
|
+
.ToListAsync();
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Migrations
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
# Create migration
|
|
290
|
+
dotnet ef migrations add AddUsersTable \
|
|
291
|
+
-p src/Infrastructure \
|
|
292
|
+
-s src/WebApi \
|
|
293
|
+
-o Data/Migrations
|
|
294
|
+
|
|
295
|
+
# Apply migrations
|
|
296
|
+
dotnet ef database update -p src/Infrastructure -s src/WebApi
|
|
297
|
+
|
|
298
|
+
# Generate SQL script
|
|
299
|
+
dotnet ef migrations script -p src/Infrastructure -s src/WebApi -o migration.sql
|
|
300
|
+
|
|
301
|
+
# Revert last migration
|
|
302
|
+
dotnet ef migrations remove -p src/Infrastructure -s src/WebApi
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Migration Best Practices
|
|
306
|
+
|
|
307
|
+
```csharp
|
|
308
|
+
public partial class AddUsersTable : Migration
|
|
309
|
+
{
|
|
310
|
+
protected override void Up(MigrationBuilder migrationBuilder)
|
|
311
|
+
{
|
|
312
|
+
migrationBuilder.CreateTable(
|
|
313
|
+
name: "users",
|
|
314
|
+
columns: table => new
|
|
315
|
+
{
|
|
316
|
+
id = table.Column<Guid>(nullable: false),
|
|
317
|
+
email = table.Column<string>(maxLength: 256, nullable: false),
|
|
318
|
+
created_at = table.Column<DateTime>(nullable: false, defaultValueSql: "GETUTCDATE()")
|
|
319
|
+
},
|
|
320
|
+
constraints: table =>
|
|
321
|
+
{
|
|
322
|
+
table.PrimaryKey("pk_users", x => x.id);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
migrationBuilder.CreateIndex(
|
|
326
|
+
name: "ix_users_email",
|
|
327
|
+
table: "users",
|
|
328
|
+
column: "email",
|
|
329
|
+
unique: true);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
protected override void Down(MigrationBuilder migrationBuilder)
|
|
333
|
+
{
|
|
334
|
+
migrationBuilder.DropTable(name: "users");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## Transactions
|
|
340
|
+
|
|
341
|
+
```csharp
|
|
342
|
+
// Unit of Work pattern
|
|
343
|
+
public interface IUnitOfWork
|
|
344
|
+
{
|
|
345
|
+
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
|
346
|
+
Task BeginTransactionAsync(CancellationToken cancellationToken = default);
|
|
347
|
+
Task CommitTransactionAsync(CancellationToken cancellationToken = default);
|
|
348
|
+
Task RollbackTransactionAsync(CancellationToken cancellationToken = default);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Explicit transaction
|
|
352
|
+
await using var transaction = await context.Database.BeginTransactionAsync();
|
|
353
|
+
try
|
|
354
|
+
{
|
|
355
|
+
await context.Users.AddAsync(user);
|
|
356
|
+
await context.SaveChangesAsync();
|
|
357
|
+
|
|
358
|
+
await context.AuditLogs.AddAsync(auditLog);
|
|
359
|
+
await context.SaveChangesAsync();
|
|
360
|
+
|
|
361
|
+
await transaction.CommitAsync();
|
|
362
|
+
}
|
|
363
|
+
catch
|
|
364
|
+
{
|
|
365
|
+
await transaction.RollbackAsync();
|
|
366
|
+
throw;
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Soft Delete
|
|
371
|
+
|
|
372
|
+
```csharp
|
|
373
|
+
// Base entity with soft delete
|
|
374
|
+
public abstract class SoftDeletableEntity
|
|
375
|
+
{
|
|
376
|
+
public DateTime? DeletedAt { get; set; }
|
|
377
|
+
public bool IsDeleted => DeletedAt.HasValue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Global query filter
|
|
381
|
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
382
|
+
{
|
|
383
|
+
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
|
384
|
+
{
|
|
385
|
+
if (typeof(SoftDeletableEntity).IsAssignableFrom(entityType.ClrType))
|
|
386
|
+
{
|
|
387
|
+
var parameter = Expression.Parameter(entityType.ClrType, "e");
|
|
388
|
+
var property = Expression.Property(parameter, nameof(SoftDeletableEntity.DeletedAt));
|
|
389
|
+
var filter = Expression.Lambda(
|
|
390
|
+
Expression.Equal(property, Expression.Constant(null, typeof(DateTime?))),
|
|
391
|
+
parameter);
|
|
392
|
+
|
|
393
|
+
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filter);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Soft delete instead of hard delete
|
|
399
|
+
public void SoftDelete(SoftDeletableEntity entity)
|
|
400
|
+
{
|
|
401
|
+
entity.DeletedAt = DateTime.UtcNow;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Include deleted records when needed
|
|
405
|
+
var allUsers = await context.Users
|
|
406
|
+
.IgnoreQueryFilters()
|
|
407
|
+
.ToListAsync();
|
|
408
|
+
```
|