@malamute/ai-rules 1.4.0 → 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 +25 -9
- package/configs/_shared/rules/conventions/interaction.md +38 -0
- package/configs/_shared/rules/domain/backend/api-design.md +58 -2
- package/configs/_shared/skills/analysis/sudden-death/SKILL.md +148 -0
- 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
|
@@ -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
|
+
```
|