@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 +22 -8
- package/configs/_shared/rules/conventions/interaction.md +38 -0
- package/configs/_shared/rules/domain/backend/api-design.md +58 -2
- package/configs/adonisjs/rules/auth.md +192 -0
- package/configs/adonisjs/rules/controllers.md +111 -0
- package/configs/adonisjs/rules/core.md +95 -0
- package/configs/adonisjs/rules/database/lucid.md +167 -0
- package/configs/adonisjs/rules/middleware.md +140 -0
- package/configs/adonisjs/rules/services.md +154 -0
- package/configs/adonisjs/rules/testing.md +170 -0
- package/configs/adonisjs/rules/validation.md +130 -0
- package/configs/adonisjs/settings.json +34 -0
- package/package.json +1 -1
- package/src/tech-config.json +4 -0
package/README.md
CHANGED
|
@@ -41,14 +41,15 @@ npx @malamute/ai-rules <command>
|
|
|
41
41
|
|
|
42
42
|
## Supported Technologies
|
|
43
43
|
|
|
44
|
-
| Technology
|
|
45
|
-
|
|
|
46
|
-
| **Angular**
|
|
47
|
-
| **Next.js**
|
|
48
|
-
| **NestJS**
|
|
49
|
-
|
|
|
50
|
-
| **
|
|
51
|
-
| **
|
|
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
|
-
###
|
|
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