@malamute/ai-rules 1.4.1 → 1.5.1

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 CHANGED
@@ -41,14 +41,15 @@ npx @malamute/ai-rules <command>
41
41
 
42
42
  ## Supported Technologies
43
43
 
44
- | Technology | Stack | Version |
45
- | ----------- | ----------------------------------------- | ------- |
46
- | **Angular** | Nx + NgRx + Signals + Vitest | 21+ |
47
- | **Next.js** | App Router + React 19 + Server Components | 15+ |
48
- | **NestJS** | Prisma/TypeORM + Passport + Vitest | 11+ |
49
- | **.NET** | Clean Architecture + MediatR + EF Core | 9+ |
50
- | **FastAPI** | Pydantic v2 + SQLAlchemy 2.0 + pytest | 0.115+ |
51
- | **Flask** | Marshmallow + SQLAlchemy 2.0 + pytest | 3.0+ |
44
+ | Technology | Stack | Version |
45
+ | ------------ | ----------------------------------------- | ------- |
46
+ | **Angular** | Nx + NgRx + Signals + Vitest | 21+ |
47
+ | **Next.js** | App Router + React 19 + Server Components | 15+ |
48
+ | **NestJS** | Prisma/TypeORM + Passport + Vitest | 11+ |
49
+ | **AdonisJS** | Lucid ORM + VineJS + Japa | 6+ |
50
+ | **.NET** | Clean Architecture + MediatR + EF Core | 9+ |
51
+ | **FastAPI** | Pydantic v2 + SQLAlchemy 2.0 + pytest | 0.115+ |
52
+ | **Flask** | Marshmallow + SQLAlchemy 2.0 + pytest | 3.0+ |
52
53
 
53
54
  ## Commands
54
55
 
@@ -255,6 +256,19 @@ ai-rules update
255
256
 
256
257
  </details>
257
258
 
259
+ <details>
260
+ <summary><strong>AdonisJS</strong></summary>
261
+
262
+ | Aspect | Convention |
263
+ | ------------ | ----------------------------------- |
264
+ | Architecture | MVC with Services layer |
265
+ | Validation | VineJS |
266
+ | ORM | Lucid (Active Record) |
267
+ | Auth | Access Tokens / Session-based |
268
+ | Tests | Japa |
269
+
270
+ </details>
271
+
258
272
  <details>
259
273
  <summary><strong>.NET</strong></summary>
260
274
 
@@ -0,0 +1,38 @@
1
+ ---
2
+ paths:
3
+ - "**/*"
4
+ ---
5
+
6
+ # Interaction Rules
7
+
8
+ ## Rules Are Absolute
9
+
10
+ 1. **Rules can NEVER be violated. Tasks can fail.**
11
+ 2. If a task requires violating a rule, the task fails - not the rule.
12
+ 3. If a task is blocked, explain the problem and ask how to proceed.
13
+
14
+ ## Protected Changes
15
+
16
+ Never modify without explaining WHY and asking permission:
17
+ - Package manager config (yarn, npm, pnpm)
18
+ - Infrastructure (docker, CI/CD, deployment)
19
+ - Project structure
20
+ - Build config
21
+
22
+ ## Questions vs Actions
23
+
24
+ - **Question** ("what is...", "why...", "how does...") → Answer only, no code
25
+ - **Explicit request** ("create", "implement", "fix", "add") → Action with code
26
+
27
+ When the user asks a question, answer it. Do not start coding or running commands.
28
+
29
+ ## Confirmation Before Action
30
+
31
+ For non-trivial changes, confirm approach before implementing:
32
+ 1. Explain what will be done
33
+ 2. Wait for user approval
34
+ 3. Then execute
35
+
36
+ ## Language
37
+
38
+ Match the user's language. If they write in French, respond in French.
@@ -151,21 +151,77 @@ GET /api/v1/users?sort=lastName:asc,firstName:asc
151
151
  GET /api/v1/users?fields=id,name,email
152
152
  ```
153
153
 
154
- ## Versioning
154
+ ## API Versioning
155
155
 
156
- ### URL Path (recommended)
156
+ ### When to Version (Breaking Changes)
157
+
158
+ - Removing or renaming a field
159
+ - Changing field type (string → number)
160
+ - Removing an endpoint
161
+ - Changing authentication method
162
+ - Modifying error response structure
163
+
164
+ ### NOT Breaking (no version bump)
165
+
166
+ - Adding new optional fields
167
+ - Adding new endpoints
168
+ - Adding new query parameters
169
+ - Performance improvements
170
+
171
+ ### Strategy: URL Path (recommended)
157
172
 
158
173
  ```
159
174
  /api/v1/users
160
175
  /api/v2/users
161
176
  ```
162
177
 
178
+ - Simple, explicit, cacheable
179
+ - Easy to route at load balancer level
180
+ - Version visible in logs
181
+
163
182
  ### Header-based (alternative)
164
183
 
165
184
  ```
166
185
  Accept: application/vnd.api+json; version=1
167
186
  ```
168
187
 
188
+ ### Deprecation Policy
189
+
190
+ 1. Announce deprecation (minimum 6 months before sunset)
191
+ 2. Add `Deprecation` header to responses
192
+ 3. Document migration path
193
+ 4. Monitor usage, notify active consumers
194
+ 5. Sunset old version
195
+
196
+ ```
197
+ Deprecation: true
198
+ Sunset: Sat, 01 Jun 2025 00:00:00 GMT
199
+ Link: <https://api.example.com/docs/migration-v2>; rel="deprecation"
200
+ ```
201
+
202
+ ### Version Lifecycle
203
+
204
+ | Status | Description |
205
+ |--------|-------------|
206
+ | **Current** | Latest stable, recommended |
207
+ | **Supported** | Still maintained, receives security fixes |
208
+ | **Deprecated** | Works but scheduled for removal |
209
+ | **Sunset** | No longer available |
210
+
211
+ ### Code Organization
212
+
213
+ ```
214
+ project/
215
+ ├── src/
216
+ │ ├── v1/
217
+ │ │ ├── controllers/
218
+ │ │ └── dto/
219
+ │ ├── v2/
220
+ │ │ ├── controllers/
221
+ │ │ └── dto/
222
+ │ └── shared/ # Version-agnostic (services, repositories)
223
+ ```
224
+
169
225
  ## Rate Limiting
170
226
 
171
227
  Include headers in response:
@@ -0,0 +1,192 @@
1
+ ---
2
+ paths:
3
+ - "app/controllers/auth/**/*.ts"
4
+ - "app/middleware/**/*.ts"
5
+ - "config/auth.ts"
6
+ ---
7
+
8
+ # AdonisJS Authentication
9
+
10
+ ## Access Tokens (API)
11
+
12
+ ### Configuration
13
+
14
+ ```typescript
15
+ // config/auth.ts
16
+ import { defineConfig } from '@adonisjs/auth'
17
+ import { tokensGuard, tokensUserProvider } from '@adonisjs/auth/access_tokens'
18
+
19
+ export default defineConfig({
20
+ default: 'api',
21
+ guards: {
22
+ api: tokensGuard({
23
+ provider: tokensUserProvider({
24
+ tokens: 'accessTokens',
25
+ model: () => import('#models/user'),
26
+ }),
27
+ }),
28
+ },
29
+ })
30
+ ```
31
+
32
+ ### User Model Setup
33
+
34
+ ```typescript
35
+ import { DbAccessTokensProvider } from '@adonisjs/auth/access_tokens'
36
+
37
+ export default class User extends BaseModel {
38
+ // ... other columns
39
+
40
+ static accessTokens = DbAccessTokensProvider.forModel(User)
41
+ }
42
+ ```
43
+
44
+ ### Auth Controller
45
+
46
+ ```typescript
47
+ import type { HttpContext } from '@adonisjs/core/http'
48
+ import User from '#models/user'
49
+ import hash from '@adonisjs/core/services/hash'
50
+ import { loginValidator, registerValidator } from '#validators/auth'
51
+
52
+ export default class AuthController {
53
+ async register({ request, response }: HttpContext) {
54
+ const payload = await request.validateUsing(registerValidator)
55
+ const user = await User.create(payload)
56
+ const token = await User.accessTokens.create(user)
57
+
58
+ return response.created({
59
+ user,
60
+ token: token.value!.release(),
61
+ })
62
+ }
63
+
64
+ async login({ request, response }: HttpContext) {
65
+ const { email, password } = await request.validateUsing(loginValidator)
66
+
67
+ const user = await User.findBy('email', email)
68
+ if (!user) {
69
+ return response.unauthorized({ message: 'Invalid credentials' })
70
+ }
71
+
72
+ const isValid = await hash.verify(user.password, password)
73
+ if (!isValid) {
74
+ return response.unauthorized({ message: 'Invalid credentials' })
75
+ }
76
+
77
+ const token = await User.accessTokens.create(user)
78
+
79
+ return response.ok({
80
+ user,
81
+ token: token.value!.release(),
82
+ })
83
+ }
84
+
85
+ async logout({ auth, response }: HttpContext) {
86
+ const user = auth.user!
87
+ await User.accessTokens.delete(user, user.currentAccessToken.identifier)
88
+ return response.noContent()
89
+ }
90
+
91
+ async me({ auth, response }: HttpContext) {
92
+ return response.ok(auth.user)
93
+ }
94
+ }
95
+ ```
96
+
97
+ ### Routes
98
+
99
+ ```typescript
100
+ // start/routes.ts
101
+ import router from '@adonisjs/core/services/router'
102
+ import { middleware } from '#start/kernel'
103
+
104
+ const AuthController = () => import('#controllers/auth_controller')
105
+
106
+ router.group(() => {
107
+ router.post('register', [AuthController, 'register'])
108
+ router.post('login', [AuthController, 'login'])
109
+
110
+ router.group(() => {
111
+ router.delete('logout', [AuthController, 'logout'])
112
+ router.get('me', [AuthController, 'me'])
113
+ }).use(middleware.auth())
114
+ }).prefix('auth')
115
+ ```
116
+
117
+ ## Middleware
118
+
119
+ ### Auth Middleware
120
+
121
+ ```typescript
122
+ // Protect routes
123
+ router.get('profile', [ProfileController, 'show']).use(middleware.auth())
124
+
125
+ // In controller, access user
126
+ async show({ auth }: HttpContext) {
127
+ const user = auth.user! // Typed as User
128
+ }
129
+ ```
130
+
131
+ ### Custom Middleware
132
+
133
+ ```typescript
134
+ // app/middleware/admin_middleware.ts
135
+ import type { HttpContext } from '@adonisjs/core/http'
136
+ import type { NextFn } from '@adonisjs/core/types/http'
137
+
138
+ export default class AdminMiddleware {
139
+ async handle({ auth, response }: HttpContext, next: NextFn) {
140
+ if (auth.user?.role !== 'admin') {
141
+ return response.forbidden({ message: 'Admin access required' })
142
+ }
143
+ await next()
144
+ }
145
+ }
146
+ ```
147
+
148
+ ### Register Middleware
149
+
150
+ ```typescript
151
+ // start/kernel.ts
152
+ import router from '@adonisjs/core/services/router'
153
+
154
+ router.named({
155
+ auth: () => import('#middleware/auth_middleware'),
156
+ admin: () => import('#middleware/admin_middleware'),
157
+ })
158
+ ```
159
+
160
+ ## Password Hashing
161
+
162
+ ```typescript
163
+ import hash from '@adonisjs/core/services/hash'
164
+
165
+ // Hash password
166
+ const hashed = await hash.make('password')
167
+
168
+ // Verify password
169
+ const isValid = await hash.verify(hashed, 'password')
170
+ ```
171
+
172
+ ## Auth Validators
173
+
174
+ ```typescript
175
+ // app/validators/auth.ts
176
+ import vine from '@vinejs/vine'
177
+
178
+ export const registerValidator = vine.compile(
179
+ vine.object({
180
+ email: vine.string().email().normalizeEmail(),
181
+ password: vine.string().minLength(8).confirmed(),
182
+ name: vine.string().minLength(2),
183
+ })
184
+ )
185
+
186
+ export const loginValidator = vine.compile(
187
+ vine.object({
188
+ email: vine.string().email(),
189
+ password: vine.string(),
190
+ })
191
+ )
192
+ ```
@@ -0,0 +1,111 @@
1
+ ---
2
+ paths:
3
+ - "app/controllers/**/*.ts"
4
+ ---
5
+
6
+ # AdonisJS Controllers
7
+
8
+ ## Structure
9
+
10
+ Controllers handle HTTP concerns only. Delegate business logic to services.
11
+
12
+ ```typescript
13
+ import type { HttpContext } from '@adonisjs/core/http'
14
+ import { inject } from '@adonisjs/core'
15
+ import UserService from '#services/user_service'
16
+ import { createUserValidator, updateUserValidator } from '#validators/user'
17
+
18
+ @inject()
19
+ export default class UsersController {
20
+ constructor(private userService: UserService) {}
21
+
22
+ async index({ response }: HttpContext) {
23
+ const users = await this.userService.getAll()
24
+ return response.ok(users)
25
+ }
26
+
27
+ async store({ request, response }: HttpContext) {
28
+ const payload = await request.validateUsing(createUserValidator)
29
+ const user = await this.userService.create(payload)
30
+ return response.created(user)
31
+ }
32
+
33
+ async show({ params, response }: HttpContext) {
34
+ const user = await this.userService.findOrFail(params.id)
35
+ return response.ok(user)
36
+ }
37
+
38
+ async update({ params, request, response }: HttpContext) {
39
+ const payload = await request.validateUsing(updateUserValidator)
40
+ const user = await this.userService.update(params.id, payload)
41
+ return response.ok(user)
42
+ }
43
+
44
+ async destroy({ params, response }: HttpContext) {
45
+ await this.userService.delete(params.id)
46
+ return response.noContent()
47
+ }
48
+ }
49
+ ```
50
+
51
+ ## Best Practices
52
+
53
+ ### Use Dependency Injection
54
+
55
+ ```typescript
56
+ // Good
57
+ @inject()
58
+ export default class OrdersController {
59
+ constructor(
60
+ private orderService: OrderService,
61
+ private notificationService: NotificationService
62
+ ) {}
63
+ }
64
+
65
+ // Avoid: instantiating services manually
66
+ export default class OrdersController {
67
+ private orderService = new OrderService() // Hard to test
68
+ }
69
+ ```
70
+
71
+ ### Validate Input
72
+
73
+ Always validate using VineJS validators:
74
+
75
+ ```typescript
76
+ async store({ request }: HttpContext) {
77
+ // Validates and returns typed payload
78
+ const payload = await request.validateUsing(createOrderValidator)
79
+ // payload is now typed and validated
80
+ }
81
+ ```
82
+
83
+ ### Use Response Helpers
84
+
85
+ ```typescript
86
+ response.ok(data) // 200
87
+ response.created(data) // 201
88
+ response.noContent() // 204
89
+ response.badRequest(error) // 400
90
+ response.unauthorized() // 401
91
+ response.forbidden() // 403
92
+ response.notFound() // 404
93
+ ```
94
+
95
+ ### Resource Routes
96
+
97
+ ```typescript
98
+ // start/routes.ts
99
+ import router from '@adonisjs/core/services/router'
100
+
101
+ const UsersController = () => import('#controllers/users_controller')
102
+
103
+ router.resource('users', UsersController).apiOnly()
104
+
105
+ // Generates:
106
+ // GET /users → index
107
+ // POST /users → store
108
+ // GET /users/:id → show
109
+ // PUT /users/:id → update
110
+ // DELETE /users/:id → destroy
111
+ ```
@@ -0,0 +1,95 @@
1
+ ---
2
+ description: "AdonisJS 6+ project conventions and architecture"
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # AdonisJS Project Guidelines
7
+
8
+ ## Stack
9
+
10
+ - AdonisJS 6+
11
+ - TypeScript strict mode
12
+ - Node.js 20+
13
+ - Japa (test framework)
14
+ - Lucid ORM
15
+
16
+ ## Architecture
17
+
18
+ ```
19
+ ├── app/
20
+ │ ├── controllers/
21
+ │ ├── models/
22
+ │ ├── services/
23
+ │ ├── validators/
24
+ │ ├── middleware/
25
+ │ ├── exceptions/
26
+ │ └── mails/
27
+ ├── config/
28
+ ├── database/
29
+ │ ├── migrations/
30
+ │ ├── seeders/
31
+ │ └── factories/
32
+ ├── resources/
33
+ │ └── views/
34
+ ├── start/
35
+ │ ├── routes.ts
36
+ │ ├── kernel.ts
37
+ │ └── events.ts
38
+ ├── tests/
39
+ └── adonisrc.ts
40
+ ```
41
+
42
+ ## Request Lifecycle
43
+
44
+ ```
45
+ Request → Global Middleware → Route Middleware → Controller → Response
46
+ ```
47
+
48
+ ## Core Principles
49
+
50
+ ### Layer Responsibilities
51
+
52
+ | Layer | Responsibility |
53
+ |-------|---------------|
54
+ | Controller | HTTP handling, delegates to services |
55
+ | Service | Business logic |
56
+ | Model | Data structure, relationships, hooks |
57
+ | Validator | Input validation with VineJS |
58
+
59
+ ### Naming Conventions
60
+
61
+ - Controllers: `UsersController` (plural)
62
+ - Models: `User` (singular)
63
+ - Validators: `CreateUserValidator`
64
+ - Migrations: `TIMESTAMP_create_users_table`
65
+
66
+ ### Dependency Injection
67
+
68
+ Use the IoC container:
69
+ ```typescript
70
+ @inject()
71
+ export default class UsersController {
72
+ constructor(private userService: UserService) {}
73
+ }
74
+ ```
75
+
76
+ ## Commands
77
+
78
+ ```bash
79
+ node ace serve --watch # Dev server
80
+ node ace build # Production build
81
+ node ace test # Run tests
82
+ node ace make:controller # Generate controller
83
+ node ace make:model # Generate model
84
+ node ace make:validator # Generate validator
85
+ node ace make:service # Generate service
86
+ node ace migration:run # Run migrations
87
+ node ace migration:rollback
88
+ ```
89
+
90
+ ## Code Style
91
+
92
+ - Use `@inject()` decorator for DI
93
+ - Async methods return `Promise<T>`
94
+ - Use `HttpContext` for request/response
95
+ - Validators use VineJS schema
@@ -0,0 +1,167 @@
1
+ ---
2
+ paths:
3
+ - "app/models/**/*.ts"
4
+ - "database/migrations/**/*.ts"
5
+ - "database/seeders/**/*.ts"
6
+ - "database/factories/**/*.ts"
7
+ ---
8
+
9
+ # Lucid ORM
10
+
11
+ ## Models
12
+
13
+ ```typescript
14
+ import { DateTime } from 'luxon'
15
+ import { BaseModel, column, hasMany, belongsTo } from '@adonisjs/lucid/orm'
16
+ import type { HasMany, BelongsTo } from '@adonisjs/lucid/types/relations'
17
+ import Post from '#models/post'
18
+ import Role from '#models/role'
19
+
20
+ export default class User extends BaseModel {
21
+ @column({ isPrimary: true })
22
+ declare id: number
23
+
24
+ @column()
25
+ declare email: string
26
+
27
+ @column({ serializeAs: null })
28
+ declare password: string
29
+
30
+ @column()
31
+ declare roleId: number
32
+
33
+ @column.dateTime({ autoCreate: true })
34
+ declare createdAt: DateTime
35
+
36
+ @column.dateTime({ autoCreate: true, autoUpdate: true })
37
+ declare updatedAt: DateTime
38
+
39
+ // Relationships
40
+ @hasMany(() => Post)
41
+ declare posts: HasMany<typeof Post>
42
+
43
+ @belongsTo(() => Role)
44
+ declare role: BelongsTo<typeof Role>
45
+ }
46
+ ```
47
+
48
+ ## Relationships
49
+
50
+ ```typescript
51
+ // One to Many
52
+ @hasMany(() => Post)
53
+ declare posts: HasMany<typeof Post>
54
+
55
+ // Belongs To
56
+ @belongsTo(() => User)
57
+ declare user: BelongsTo<typeof User>
58
+
59
+ // Many to Many
60
+ @manyToMany(() => Tag, {
61
+ pivotTable: 'post_tags',
62
+ pivotTimestamps: true,
63
+ })
64
+ declare tags: ManyToMany<typeof Tag>
65
+
66
+ // Has One
67
+ @hasOne(() => Profile)
68
+ declare profile: HasOne<typeof Profile>
69
+ ```
70
+
71
+ ## Queries
72
+
73
+ ```typescript
74
+ // Find
75
+ const user = await User.find(1)
76
+ const user = await User.findOrFail(1)
77
+ const user = await User.findBy('email', 'user@example.com')
78
+
79
+ // Query builder
80
+ const users = await User.query()
81
+ .where('isActive', true)
82
+ .whereNotNull('verifiedAt')
83
+ .orderBy('createdAt', 'desc')
84
+ .limit(10)
85
+
86
+ // With relationships
87
+ const user = await User.query()
88
+ .where('id', 1)
89
+ .preload('posts', (query) => {
90
+ query.where('isPublished', true)
91
+ })
92
+ .firstOrFail()
93
+
94
+ // Aggregates
95
+ const count = await User.query().count('* as total')
96
+ ```
97
+
98
+ ## Migrations
99
+
100
+ ```typescript
101
+ import { BaseSchema } from '@adonisjs/lucid/schema'
102
+
103
+ export default class extends BaseSchema {
104
+ protected tableName = 'users'
105
+
106
+ async up() {
107
+ this.schema.createTable(this.tableName, (table) => {
108
+ table.increments('id')
109
+ table.string('email').notNullable().unique()
110
+ table.string('password').notNullable()
111
+ table.integer('role_id').unsigned().references('roles.id').onDelete('CASCADE')
112
+ table.timestamp('created_at')
113
+ table.timestamp('updated_at')
114
+ })
115
+ }
116
+
117
+ async down() {
118
+ this.schema.dropTable(this.tableName)
119
+ }
120
+ }
121
+ ```
122
+
123
+ ## Factories
124
+
125
+ ```typescript
126
+ import Factory from '@adonisjs/lucid/factories'
127
+ import User from '#models/user'
128
+
129
+ export const UserFactory = Factory.define(User, ({ faker }) => ({
130
+ email: faker.internet.email(),
131
+ password: faker.internet.password(),
132
+ }))
133
+ .relation('posts', () => PostFactory)
134
+ .build()
135
+
136
+ // Usage
137
+ const user = await UserFactory.create()
138
+ const users = await UserFactory.createMany(5)
139
+ const userWithPosts = await UserFactory.with('posts', 3).create()
140
+ ```
141
+
142
+ ## Hooks
143
+
144
+ ```typescript
145
+ import { beforeSave } from '@adonisjs/lucid/orm'
146
+ import hash from '@adonisjs/core/services/hash'
147
+
148
+ export default class User extends BaseModel {
149
+ @beforeSave()
150
+ static async hashPassword(user: User) {
151
+ if (user.$dirty.password) {
152
+ user.password = await hash.make(user.password)
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
158
+ ## Transactions
159
+
160
+ ```typescript
161
+ import db from '@adonisjs/lucid/services/db'
162
+
163
+ await db.transaction(async (trx) => {
164
+ const user = await User.create({ email, password }, { client: trx })
165
+ await Profile.create({ userId: user.id }, { client: trx })
166
+ })
167
+ ```
@@ -0,0 +1,140 @@
1
+ ---
2
+ paths:
3
+ - "app/middleware/**/*.ts"
4
+ - "start/kernel.ts"
5
+ ---
6
+
7
+ # AdonisJS Middleware
8
+
9
+ ## Creating Middleware
10
+
11
+ ```typescript
12
+ // app/middleware/admin_middleware.ts
13
+ import type { HttpContext } from '@adonisjs/core/http'
14
+ import type { NextFn } from '@adonisjs/core/types/http'
15
+
16
+ export default class AdminMiddleware {
17
+ async handle({ auth, response }: HttpContext, next: NextFn) {
18
+ if (auth.user?.role !== 'admin') {
19
+ return response.forbidden({ message: 'Admin access required' })
20
+ }
21
+
22
+ await next()
23
+ }
24
+ }
25
+ ```
26
+
27
+ ## Register Middleware
28
+
29
+ ```typescript
30
+ // start/kernel.ts
31
+ import router from '@adonisjs/core/services/router'
32
+
33
+ // Named middleware (use on specific routes)
34
+ router.named({
35
+ auth: () => import('#middleware/auth_middleware'),
36
+ admin: () => import('#middleware/admin_middleware'),
37
+ verified: () => import('#middleware/verified_middleware'),
38
+ })
39
+ ```
40
+
41
+ ## Using Middleware
42
+
43
+ ```typescript
44
+ // start/routes.ts
45
+ import router from '@adonisjs/core/services/router'
46
+ import { middleware } from '#start/kernel'
47
+
48
+ // Single middleware
49
+ router.get('dashboard', [DashboardController, 'index'])
50
+ .use(middleware.auth())
51
+
52
+ // Multiple middleware
53
+ router.get('admin', [AdminController, 'index'])
54
+ .use([middleware.auth(), middleware.admin()])
55
+
56
+ // Group middleware
57
+ router.group(() => {
58
+ router.get('profile', [ProfileController, 'show'])
59
+ router.put('profile', [ProfileController, 'update'])
60
+ }).use(middleware.auth())
61
+ ```
62
+
63
+ ## Middleware with Parameters
64
+
65
+ ```typescript
66
+ // app/middleware/role_middleware.ts
67
+ import type { HttpContext } from '@adonisjs/core/http'
68
+ import type { NextFn } from '@adonisjs/core/types/http'
69
+
70
+ export default class RoleMiddleware {
71
+ async handle({ auth, response }: HttpContext, next: NextFn, options: { roles: string[] }) {
72
+ if (!options.roles.includes(auth.user!.role)) {
73
+ return response.forbidden({ message: 'Insufficient permissions' })
74
+ }
75
+
76
+ await next()
77
+ }
78
+ }
79
+
80
+ // Usage
81
+ router.get('reports', [ReportsController, 'index'])
82
+ .use(middleware.role({ roles: ['admin', 'manager'] }))
83
+ ```
84
+
85
+ ## Request Logging Middleware
86
+
87
+ ```typescript
88
+ import type { HttpContext } from '@adonisjs/core/http'
89
+ import type { NextFn } from '@adonisjs/core/types/http'
90
+ import logger from '@adonisjs/core/services/logger'
91
+
92
+ export default class RequestLoggerMiddleware {
93
+ async handle({ request }: HttpContext, next: NextFn) {
94
+ const start = Date.now()
95
+
96
+ await next()
97
+
98
+ const duration = Date.now() - start
99
+ logger.info(`${request.method()} ${request.url()} - ${duration}ms`)
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## Global Middleware
105
+
106
+ ```typescript
107
+ // start/kernel.ts
108
+ import server from '@adonisjs/core/services/server'
109
+
110
+ // Server middleware (runs for every request)
111
+ server.use([
112
+ () => import('#middleware/container_bindings_middleware'),
113
+ () => import('#middleware/force_json_response_middleware'),
114
+ ])
115
+
116
+ // Router middleware (runs for routes only)
117
+ router.use([
118
+ () => import('@adonisjs/core/bodyparser_middleware'),
119
+ () => import('#middleware/request_logger_middleware'),
120
+ ])
121
+ ```
122
+
123
+ ## Terminate Hook
124
+
125
+ ```typescript
126
+ export default class AnalyticsMiddleware {
127
+ async handle(ctx: HttpContext, next: NextFn) {
128
+ await next()
129
+ }
130
+
131
+ // Runs after response is sent
132
+ async terminate(ctx: HttpContext) {
133
+ await Analytics.track({
134
+ path: ctx.request.url(),
135
+ userId: ctx.auth.user?.id,
136
+ duration: ctx.response.getResponseTime(),
137
+ })
138
+ }
139
+ }
140
+ ```
@@ -0,0 +1,154 @@
1
+ ---
2
+ paths:
3
+ - "app/services/**/*.ts"
4
+ ---
5
+
6
+ # AdonisJS Services
7
+
8
+ ## Structure
9
+
10
+ Services contain business logic. Controllers delegate to services.
11
+
12
+ ```typescript
13
+ // app/services/user_service.ts
14
+ import { inject } from '@adonisjs/core'
15
+ import User from '#models/user'
16
+ import hash from '@adonisjs/core/services/hash'
17
+
18
+ @inject()
19
+ export default class UserService {
20
+ async getAll() {
21
+ return User.query().orderBy('createdAt', 'desc')
22
+ }
23
+
24
+ async findOrFail(id: number) {
25
+ return User.findOrFail(id)
26
+ }
27
+
28
+ async create(data: { email: string; password: string; name: string }) {
29
+ return User.create({
30
+ ...data,
31
+ password: await hash.make(data.password),
32
+ })
33
+ }
34
+
35
+ async update(id: number, data: Partial<{ email: string; name: string }>) {
36
+ const user = await User.findOrFail(id)
37
+ user.merge(data)
38
+ await user.save()
39
+ return user
40
+ }
41
+
42
+ async delete(id: number) {
43
+ const user = await User.findOrFail(id)
44
+ await user.delete()
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## Service with Dependencies
50
+
51
+ ```typescript
52
+ import { inject } from '@adonisjs/core'
53
+ import mail from '@adonisjs/mail/services/main'
54
+ import User from '#models/user'
55
+ import NotificationService from '#services/notification_service'
56
+
57
+ @inject()
58
+ export default class OrderService {
59
+ constructor(private notificationService: NotificationService) {}
60
+
61
+ async create(userId: number, items: OrderItem[]) {
62
+ const user = await User.findOrFail(userId)
63
+
64
+ const order = await Order.create({
65
+ userId,
66
+ total: this.calculateTotal(items),
67
+ })
68
+
69
+ await order.related('items').createMany(items)
70
+
71
+ // Use injected service
72
+ await this.notificationService.sendOrderConfirmation(user, order)
73
+
74
+ return order
75
+ }
76
+
77
+ private calculateTotal(items: OrderItem[]): number {
78
+ return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## Register in Container (optional)
84
+
85
+ For complex setup or interfaces:
86
+
87
+ ```typescript
88
+ // providers/app_provider.ts
89
+ import type { ApplicationService } from '@adonisjs/core/types'
90
+
91
+ export default class AppProvider {
92
+ constructor(protected app: ApplicationService) {}
93
+
94
+ async boot() {
95
+ const { default: PaymentService } = await import('#services/payment_service')
96
+
97
+ this.app.container.singleton('payment', () => {
98
+ return new PaymentService(env.get('STRIPE_KEY'))
99
+ })
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## Error Handling
105
+
106
+ ```typescript
107
+ import { Exception } from '@adonisjs/core/exceptions'
108
+
109
+ export class InsufficientFundsException extends Exception {
110
+ static status = 422
111
+ static code = 'E_INSUFFICIENT_FUNDS'
112
+ }
113
+
114
+ export default class WalletService {
115
+ async withdraw(userId: number, amount: number) {
116
+ const wallet = await Wallet.findByOrFail('userId', userId)
117
+
118
+ if (wallet.balance < amount) {
119
+ throw new InsufficientFundsException('Insufficient funds')
120
+ }
121
+
122
+ wallet.balance -= amount
123
+ await wallet.save()
124
+ return wallet
125
+ }
126
+ }
127
+ ```
128
+
129
+ ## Testing Services
130
+
131
+ ```typescript
132
+ import { test } from '@japa/runner'
133
+ import UserService from '#services/user_service'
134
+ import { UserFactory } from '#database/factories/user_factory'
135
+
136
+ test.group('UserService', (group) => {
137
+ group.each.setup(async () => {
138
+ await Database.beginGlobalTransaction()
139
+ return () => Database.rollbackGlobalTransaction()
140
+ })
141
+
142
+ test('creates user with hashed password', async ({ assert }) => {
143
+ const service = new UserService()
144
+
145
+ const user = await service.create({
146
+ email: 'test@example.com',
147
+ password: 'plaintext',
148
+ name: 'Test',
149
+ })
150
+
151
+ assert.notEqual(user.password, 'plaintext')
152
+ })
153
+ })
154
+ ```
@@ -0,0 +1,170 @@
1
+ ---
2
+ paths:
3
+ - "tests/**/*.ts"
4
+ ---
5
+
6
+ # AdonisJS Testing (Japa)
7
+
8
+ ## Test Structure
9
+
10
+ ```
11
+ tests/
12
+ ├── bootstrap.ts # Test setup
13
+ ├── unit/
14
+ │ └── services/
15
+ ├── functional/
16
+ │ └── controllers/
17
+ └── integration/
18
+ ```
19
+
20
+ ## Unit Test
21
+
22
+ ```typescript
23
+ import { test } from '@japa/runner'
24
+ import UserService from '#services/user_service'
25
+
26
+ test.group('UserService', () => {
27
+ test('creates a user with valid data', async ({ assert }) => {
28
+ const service = new UserService()
29
+ const user = await service.create({
30
+ email: 'test@example.com',
31
+ password: 'password123',
32
+ })
33
+
34
+ assert.exists(user.id)
35
+ assert.equal(user.email, 'test@example.com')
36
+ })
37
+ })
38
+ ```
39
+
40
+ ## Functional Test (HTTP)
41
+
42
+ ```typescript
43
+ import { test } from '@japa/runner'
44
+ import { UserFactory } from '#database/factories/user_factory'
45
+
46
+ test.group('Users Controller', (group) => {
47
+ group.each.setup(async () => {
48
+ // Runs before each test
49
+ await Database.beginGlobalTransaction()
50
+ return () => Database.rollbackGlobalTransaction()
51
+ })
52
+
53
+ test('list all users', async ({ client, assert }) => {
54
+ await UserFactory.createMany(3)
55
+
56
+ const response = await client.get('/users')
57
+
58
+ response.assertStatus(200)
59
+ assert.lengthOf(response.body(), 3)
60
+ })
61
+
62
+ test('create a user', async ({ client }) => {
63
+ const response = await client.post('/users').json({
64
+ email: 'new@example.com',
65
+ password: 'password123',
66
+ name: 'New User',
67
+ })
68
+
69
+ response.assertStatus(201)
70
+ response.assertBodyContains({ email: 'new@example.com' })
71
+ })
72
+
73
+ test('requires authentication', async ({ client }) => {
74
+ const response = await client.get('/profile')
75
+ response.assertStatus(401)
76
+ })
77
+ })
78
+ ```
79
+
80
+ ## Authenticated Requests
81
+
82
+ ```typescript
83
+ test('authenticated user can view profile', async ({ client }) => {
84
+ const user = await UserFactory.create()
85
+
86
+ const response = await client
87
+ .get('/profile')
88
+ .loginAs(user)
89
+
90
+ response.assertStatus(200)
91
+ response.assertBodyContains({ id: user.id })
92
+ })
93
+ ```
94
+
95
+ ## Database Helpers
96
+
97
+ ```typescript
98
+ import Database from '@adonisjs/lucid/services/db'
99
+ import { test } from '@japa/runner'
100
+
101
+ test.group('Database tests', (group) => {
102
+ // Transaction per test (auto rollback)
103
+ group.each.setup(async () => {
104
+ await Database.beginGlobalTransaction()
105
+ return () => Database.rollbackGlobalTransaction()
106
+ })
107
+
108
+ // Or truncate tables
109
+ group.each.setup(async () => {
110
+ await Database.truncate('users')
111
+ })
112
+ })
113
+ ```
114
+
115
+ ## Assertions
116
+
117
+ ```typescript
118
+ test('response assertions', async ({ client, assert }) => {
119
+ const response = await client.get('/users/1')
120
+
121
+ // Status
122
+ response.assertStatus(200)
123
+ response.assertStatus(201)
124
+ response.assertStatus(404)
125
+
126
+ // Body
127
+ response.assertBody({ id: 1, name: 'John' })
128
+ response.assertBodyContains({ name: 'John' })
129
+
130
+ // Headers
131
+ response.assertHeader('content-type', 'application/json')
132
+
133
+ // Custom assertions
134
+ assert.equal(response.body().email, 'test@example.com')
135
+ assert.lengthOf(response.body().users, 3)
136
+ assert.isTrue(response.body().isActive)
137
+ })
138
+ ```
139
+
140
+ ## Mocking
141
+
142
+ ```typescript
143
+ import { test } from '@japa/runner'
144
+ import mail from '@adonisjs/mail/services/main'
145
+
146
+ test('sends welcome email on registration', async ({ client, assert }) => {
147
+ const { mails } = mail.fake()
148
+
149
+ await client.post('/auth/register').json({
150
+ email: 'test@example.com',
151
+ password: 'password123',
152
+ })
153
+
154
+ assert.lengthOf(mails.sent, 1)
155
+ mails.assertSent(WelcomeMail, (mail) => {
156
+ return mail.to[0].address === 'test@example.com'
157
+ })
158
+
159
+ mail.restore()
160
+ })
161
+ ```
162
+
163
+ ## Running Tests
164
+
165
+ ```bash
166
+ node ace test # All tests
167
+ node ace test --files="tests/functional/**"
168
+ node ace test --tags="@slow"
169
+ node ace test --watch # Watch mode
170
+ ```
@@ -0,0 +1,130 @@
1
+ ---
2
+ paths:
3
+ - "app/validators/**/*.ts"
4
+ ---
5
+
6
+ # VineJS Validation
7
+
8
+ ## Basic Validator
9
+
10
+ ```typescript
11
+ import vine from '@vinejs/vine'
12
+
13
+ export const createUserValidator = vine.compile(
14
+ vine.object({
15
+ email: vine.string().email().normalizeEmail(),
16
+ password: vine.string().minLength(8),
17
+ name: vine.string().minLength(2).maxLength(100),
18
+ })
19
+ )
20
+
21
+ export const updateUserValidator = vine.compile(
22
+ vine.object({
23
+ email: vine.string().email().normalizeEmail().optional(),
24
+ name: vine.string().minLength(2).maxLength(100).optional(),
25
+ })
26
+ )
27
+ ```
28
+
29
+ ## Common Rules
30
+
31
+ ```typescript
32
+ vine.object({
33
+ // Strings
34
+ name: vine.string().minLength(2).maxLength(100),
35
+ email: vine.string().email(),
36
+ url: vine.string().url(),
37
+ slug: vine.string().regex(/^[a-z0-9-]+$/),
38
+
39
+ // Numbers
40
+ age: vine.number().min(0).max(150),
41
+ price: vine.number().positive().decimal(2),
42
+
43
+ // Booleans
44
+ isActive: vine.boolean(),
45
+
46
+ // Dates
47
+ birthDate: vine.date({ formats: ['YYYY-MM-DD'] }),
48
+
49
+ // Arrays
50
+ tags: vine.array(vine.string()).minLength(1).maxLength(10),
51
+
52
+ // Enums
53
+ status: vine.enum(['draft', 'published', 'archived']),
54
+
55
+ // Optional & Nullable
56
+ bio: vine.string().optional(),
57
+ deletedAt: vine.date().nullable(),
58
+ })
59
+ ```
60
+
61
+ ## Custom Error Messages
62
+
63
+ ```typescript
64
+ export const createUserValidator = vine.compile(
65
+ vine.object({
66
+ email: vine.string().email(),
67
+ password: vine.string().minLength(8),
68
+ })
69
+ )
70
+
71
+ createUserValidator.messagesProvider = new SimpleMessagesProvider({
72
+ 'email.required': 'Email is required',
73
+ 'email.email': 'Invalid email format',
74
+ 'password.minLength': 'Password must be at least 8 characters',
75
+ })
76
+ ```
77
+
78
+ ## Unique Validation
79
+
80
+ ```typescript
81
+ import vine from '@vinejs/vine'
82
+ import { FieldContext } from '@vinejs/vine/types'
83
+ import User from '#models/user'
84
+
85
+ const uniqueEmail = vine.createRule(async (value: unknown, _options: undefined, field: FieldContext) => {
86
+ if (typeof value !== 'string') return
87
+
88
+ const user = await User.findBy('email', value)
89
+ if (user) {
90
+ field.report('Email already exists', 'unique', field)
91
+ }
92
+ })
93
+
94
+ export const createUserValidator = vine.compile(
95
+ vine.object({
96
+ email: vine.string().email().use(uniqueEmail()),
97
+ })
98
+ )
99
+ ```
100
+
101
+ ## Conditional Validation
102
+
103
+ ```typescript
104
+ vine.object({
105
+ accountType: vine.enum(['personal', 'business']),
106
+ companyName: vine
107
+ .string()
108
+ .minLength(2)
109
+ .requiredWhen('accountType', '=', 'business'),
110
+ taxId: vine
111
+ .string()
112
+ .optional()
113
+ .requiredWhen('accountType', '=', 'business'),
114
+ })
115
+ ```
116
+
117
+ ## Usage in Controller
118
+
119
+ ```typescript
120
+ import { createUserValidator } from '#validators/user'
121
+
122
+ export default class UsersController {
123
+ async store({ request, response }: HttpContext) {
124
+ const payload = await request.validateUsing(createUserValidator)
125
+ // payload is fully typed: { email: string, password: string, name: string }
126
+ const user = await User.create(payload)
127
+ return response.created(user)
128
+ }
129
+ }
130
+ ```
@@ -0,0 +1,34 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(node ace serve --watch)",
5
+ "Bash(node ace build)",
6
+ "Bash(node ace test*)",
7
+ "Bash(node ace make:*)",
8
+ "Bash(node ace migration:*)",
9
+ "Bash(node ace db:*)",
10
+ "Bash(node ace generate:*)",
11
+ "Bash(node ace list)",
12
+ "Bash(npm run dev)",
13
+ "Bash(npm run build)",
14
+ "Bash(npm run test*)",
15
+ "Bash(npm run lint*)",
16
+ "Bash(npm install *)",
17
+ "Bash(npm ci)",
18
+ "Read",
19
+ "Edit",
20
+ "Write"
21
+ ],
22
+ "deny": [
23
+ "Bash(git push *)",
24
+ "Bash(git push)",
25
+ "Bash(rm -rf *)",
26
+ "Read(.env)",
27
+ "Read(.env.*)",
28
+ "Read(**/secrets/**)"
29
+ ]
30
+ },
31
+ "env": {
32
+ "NODE_ENV": "development"
33
+ }
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malamute/ai-rules",
3
- "version": "1.4.1",
3
+ "version": "1.5.1",
4
4
  "description": "Claude Code configuration boilerplates for Angular, Next.js, NestJS, .NET, Python and more",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -23,6 +23,10 @@
23
23
  "flask": {
24
24
  "type": "backend",
25
25
  "language": "python"
26
+ },
27
+ "adonisjs": {
28
+ "type": "backend",
29
+ "language": "typescript"
26
30
  }
27
31
  },
28
32
  "ruleMapping": {