autoworkflow 3.1.5 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# MCP Slack Skill
|
|
2
|
+
|
|
3
|
+
## Server Configuration
|
|
4
|
+
\`\`\`json
|
|
5
|
+
// claude_desktop_config.json or .claude/settings.json
|
|
6
|
+
{
|
|
7
|
+
"mcpServers": {
|
|
8
|
+
"slack": {
|
|
9
|
+
"command": "npx",
|
|
10
|
+
"args": ["-y", "@modelcontextprotocol/server-slack"],
|
|
11
|
+
"env": {
|
|
12
|
+
"SLACK_BOT_TOKEN": "xoxb-xxxxxxxxxxxx",
|
|
13
|
+
"SLACK_TEAM_ID": "T0XXXXXXX"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
\`\`\`
|
|
19
|
+
|
|
20
|
+
## Authentication Setup
|
|
21
|
+
\`\`\`bash
|
|
22
|
+
# 1. Create a Slack App at https://api.slack.com/apps
|
|
23
|
+
# 2. Add Bot Token Scopes under OAuth & Permissions:
|
|
24
|
+
# - channels:history (read public channel messages)
|
|
25
|
+
# - channels:read (list public channels)
|
|
26
|
+
# - chat:write (post messages)
|
|
27
|
+
# - files:read (access files)
|
|
28
|
+
# - files:write (upload files)
|
|
29
|
+
# - groups:history (read private channel messages)
|
|
30
|
+
# - groups:read (list private channels)
|
|
31
|
+
# - im:history (read DMs)
|
|
32
|
+
# - im:read (list DMs)
|
|
33
|
+
# - users:read (list users)
|
|
34
|
+
# - reactions:read (access reactions)
|
|
35
|
+
# - reactions:write (add reactions)
|
|
36
|
+
# 3. Install app to workspace
|
|
37
|
+
# 4. Copy Bot User OAuth Token (xoxb-...)
|
|
38
|
+
\`\`\`
|
|
39
|
+
|
|
40
|
+
## Available Tools
|
|
41
|
+
|
|
42
|
+
### Channel Operations
|
|
43
|
+
\`\`\`
|
|
44
|
+
list_channels
|
|
45
|
+
- types?: 'public_channel' | 'private_channel' | 'mpim' | 'im'
|
|
46
|
+
- limit?: number (default 100)
|
|
47
|
+
- Returns: array of channels with id, name, topic
|
|
48
|
+
|
|
49
|
+
get_channel_info
|
|
50
|
+
- channel: string (channel ID)
|
|
51
|
+
- Returns: channel details
|
|
52
|
+
|
|
53
|
+
get_channel_history
|
|
54
|
+
- channel: string (channel ID)
|
|
55
|
+
- limit?: number (default 100)
|
|
56
|
+
- oldest?: string (timestamp)
|
|
57
|
+
- latest?: string (timestamp)
|
|
58
|
+
- Returns: array of messages
|
|
59
|
+
|
|
60
|
+
join_channel
|
|
61
|
+
- channel: string (channel ID)
|
|
62
|
+
- Returns: success status
|
|
63
|
+
\`\`\`
|
|
64
|
+
|
|
65
|
+
### Message Operations
|
|
66
|
+
\`\`\`
|
|
67
|
+
post_message
|
|
68
|
+
- channel: string (channel ID or name)
|
|
69
|
+
- text: string (message content)
|
|
70
|
+
- thread_ts?: string (reply to thread)
|
|
71
|
+
- unfurl_links?: boolean
|
|
72
|
+
- unfurl_media?: boolean
|
|
73
|
+
- Returns: message timestamp
|
|
74
|
+
|
|
75
|
+
update_message
|
|
76
|
+
- channel: string
|
|
77
|
+
- ts: string (message timestamp)
|
|
78
|
+
- text: string (new content)
|
|
79
|
+
- Returns: updated message
|
|
80
|
+
|
|
81
|
+
delete_message
|
|
82
|
+
- channel: string
|
|
83
|
+
- ts: string (message timestamp)
|
|
84
|
+
- Returns: success status
|
|
85
|
+
|
|
86
|
+
reply_to_thread
|
|
87
|
+
- channel: string
|
|
88
|
+
- thread_ts: string (parent message timestamp)
|
|
89
|
+
- text: string
|
|
90
|
+
- Returns: message timestamp
|
|
91
|
+
|
|
92
|
+
get_thread_replies
|
|
93
|
+
- channel: string
|
|
94
|
+
- ts: string (thread parent timestamp)
|
|
95
|
+
- Returns: array of reply messages
|
|
96
|
+
\`\`\`
|
|
97
|
+
|
|
98
|
+
### Reaction Operations
|
|
99
|
+
\`\`\`
|
|
100
|
+
add_reaction
|
|
101
|
+
- channel: string
|
|
102
|
+
- timestamp: string (message ts)
|
|
103
|
+
- name: string (emoji name without colons)
|
|
104
|
+
|
|
105
|
+
remove_reaction
|
|
106
|
+
- channel: string
|
|
107
|
+
- timestamp: string
|
|
108
|
+
- name: string
|
|
109
|
+
|
|
110
|
+
get_reactions
|
|
111
|
+
- channel: string
|
|
112
|
+
- timestamp: string
|
|
113
|
+
- Returns: array of reactions
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
### User Operations
|
|
117
|
+
\`\`\`
|
|
118
|
+
list_users
|
|
119
|
+
- limit?: number
|
|
120
|
+
- Returns: array of users with id, name, real_name, email
|
|
121
|
+
|
|
122
|
+
get_user_info
|
|
123
|
+
- user: string (user ID)
|
|
124
|
+
- Returns: user profile details
|
|
125
|
+
|
|
126
|
+
get_user_by_email
|
|
127
|
+
- email: string
|
|
128
|
+
- Returns: user information
|
|
129
|
+
\`\`\`
|
|
130
|
+
|
|
131
|
+
### File Operations
|
|
132
|
+
\`\`\`
|
|
133
|
+
upload_file
|
|
134
|
+
- channels: string (comma-separated channel IDs)
|
|
135
|
+
- content?: string (file content)
|
|
136
|
+
- filename: string
|
|
137
|
+
- filetype?: string
|
|
138
|
+
- title?: string
|
|
139
|
+
- initial_comment?: string
|
|
140
|
+
- Returns: file info
|
|
141
|
+
|
|
142
|
+
list_files
|
|
143
|
+
- channel?: string
|
|
144
|
+
- user?: string
|
|
145
|
+
- types?: string (e.g., 'images', 'pdfs')
|
|
146
|
+
- Returns: array of files
|
|
147
|
+
\`\`\`
|
|
148
|
+
|
|
149
|
+
## Message Formatting
|
|
150
|
+
|
|
151
|
+
### Block Kit Basics
|
|
152
|
+
\`\`\`json
|
|
153
|
+
{
|
|
154
|
+
"blocks": [
|
|
155
|
+
{
|
|
156
|
+
"type": "header",
|
|
157
|
+
"text": {
|
|
158
|
+
"type": "plain_text",
|
|
159
|
+
"text": "Deployment Complete"
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"type": "section",
|
|
164
|
+
"text": {
|
|
165
|
+
"type": "mrkdwn",
|
|
166
|
+
"text": "*Status:* :white_check_mark: Success\\n*Environment:* Production"
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"type": "divider"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"type": "section",
|
|
174
|
+
"fields": [
|
|
175
|
+
{ "type": "mrkdwn", "text": "*Version:* v2.3.1" },
|
|
176
|
+
{ "type": "mrkdwn", "text": "*Duration:* 3m 42s" }
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"type": "actions",
|
|
181
|
+
"elements": [
|
|
182
|
+
{
|
|
183
|
+
"type": "button",
|
|
184
|
+
"text": { "type": "plain_text", "text": "View Logs" },
|
|
185
|
+
"url": "https://logs.example.com"
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
\`\`\`
|
|
192
|
+
|
|
193
|
+
### Markdown Formatting
|
|
194
|
+
\`\`\`
|
|
195
|
+
*bold*
|
|
196
|
+
_italic_
|
|
197
|
+
~strikethrough~
|
|
198
|
+
\`code\`
|
|
199
|
+
\`\`\`code block\`\`\`
|
|
200
|
+
> quote
|
|
201
|
+
<https://example.com|Link Text>
|
|
202
|
+
<@U12345> (mention user)
|
|
203
|
+
<#C12345> (mention channel)
|
|
204
|
+
<!here> <!channel> <!everyone> (special mentions)
|
|
205
|
+
:emoji_name: (emoji)
|
|
206
|
+
\`\`\`
|
|
207
|
+
|
|
208
|
+
## Common Workflows
|
|
209
|
+
|
|
210
|
+
### Post Build Notification
|
|
211
|
+
\`\`\`
|
|
212
|
+
1. Use list_channels to find #deployments channel
|
|
213
|
+
2. Use post_message with Block Kit:
|
|
214
|
+
- Header: "Build #123 Complete"
|
|
215
|
+
- Status, branch, commit info
|
|
216
|
+
- Link to build logs
|
|
217
|
+
- Actions: View Diff, Rollback
|
|
218
|
+
\`\`\`
|
|
219
|
+
|
|
220
|
+
### Daily Standup Reminder
|
|
221
|
+
\`\`\`
|
|
222
|
+
1. Get channel history from yesterday
|
|
223
|
+
2. Summarize activity
|
|
224
|
+
3. Post formatted standup prompt
|
|
225
|
+
4. Add reaction for acknowledgment tracking
|
|
226
|
+
\`\`\`
|
|
227
|
+
|
|
228
|
+
### Error Alert Thread
|
|
229
|
+
\`\`\`
|
|
230
|
+
1. Post main alert message
|
|
231
|
+
2. Store thread_ts from response
|
|
232
|
+
3. Add error details as thread replies
|
|
233
|
+
4. Add reactions for status tracking
|
|
234
|
+
- :eyes: (investigating)
|
|
235
|
+
- :white_check_mark: (resolved)
|
|
236
|
+
\`\`\`
|
|
237
|
+
|
|
238
|
+
## Channel ID Lookup
|
|
239
|
+
\`\`\`bash
|
|
240
|
+
# Channel IDs start with C (public) or G (private)
|
|
241
|
+
# Find channel ID:
|
|
242
|
+
# 1. Right-click channel > View channel details
|
|
243
|
+
# 2. Copy channel ID from bottom of modal
|
|
244
|
+
|
|
245
|
+
# Or use list_channels tool to search by name
|
|
246
|
+
\`\`\`
|
|
247
|
+
|
|
248
|
+
## ❌ DON'T
|
|
249
|
+
- Post sensitive data (tokens, passwords) in channels
|
|
250
|
+
- Spam channels with excessive notifications
|
|
251
|
+
- Use @channel/@here for non-urgent messages
|
|
252
|
+
- Store bot tokens in code
|
|
253
|
+
- Ignore rate limits (Tier 1: 1 req/sec)
|
|
254
|
+
|
|
255
|
+
## ✅ DO
|
|
256
|
+
- Use threads to organize related messages
|
|
257
|
+
- Format messages with Block Kit for readability
|
|
258
|
+
- Use appropriate emoji reactions for status
|
|
259
|
+
- Batch notifications where possible
|
|
260
|
+
- Respect rate limits with backoff
|
|
261
|
+
- Use channel-specific tokens when needed
|
|
262
|
+
- Archive notifications after resolution
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# Micronaut Skill
|
|
2
|
+
|
|
3
|
+
## Project Setup
|
|
4
|
+
\`\`\`bash
|
|
5
|
+
# Create new project
|
|
6
|
+
mn create-app com.example.myapp --features=data-jdbc,postgres,security-jwt
|
|
7
|
+
|
|
8
|
+
# Run with hot reload
|
|
9
|
+
./gradlew run --continuous
|
|
10
|
+
|
|
11
|
+
# Build native image
|
|
12
|
+
./gradlew nativeCompile
|
|
13
|
+
\`\`\`
|
|
14
|
+
|
|
15
|
+
## Controller
|
|
16
|
+
\`\`\`java
|
|
17
|
+
@Controller("/api/v1/users")
|
|
18
|
+
public class UserController {
|
|
19
|
+
|
|
20
|
+
private final UserService userService;
|
|
21
|
+
|
|
22
|
+
public UserController(UserService userService) {
|
|
23
|
+
this.userService = userService;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Get
|
|
27
|
+
public Page<UserResponse> listUsers(
|
|
28
|
+
@QueryValue(defaultValue = "0") int page,
|
|
29
|
+
@QueryValue(defaultValue = "20") int size) {
|
|
30
|
+
return userService.findAll(Pageable.from(page, size));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Get("/{id}")
|
|
34
|
+
public HttpResponse<UserResponse> getUser(@PathVariable String id) {
|
|
35
|
+
return userService.findById(id)
|
|
36
|
+
.map(HttpResponse::ok)
|
|
37
|
+
.orElse(HttpResponse.notFound());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Post
|
|
41
|
+
public HttpResponse<UserResponse> createUser(@Valid @Body CreateUserRequest request) {
|
|
42
|
+
UserResponse user = userService.create(request);
|
|
43
|
+
return HttpResponse.created(user)
|
|
44
|
+
.header("Location", "/api/v1/users/" + user.id());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Put("/{id}")
|
|
48
|
+
public HttpResponse<UserResponse> updateUser(
|
|
49
|
+
@PathVariable String id,
|
|
50
|
+
@Valid @Body UpdateUserRequest request) {
|
|
51
|
+
return HttpResponse.ok(userService.update(id, request));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Delete("/{id}")
|
|
55
|
+
public HttpResponse<Void> deleteUser(@PathVariable String id) {
|
|
56
|
+
userService.delete(id);
|
|
57
|
+
return HttpResponse.noContent();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
\`\`\`
|
|
61
|
+
|
|
62
|
+
## Reactive Controller
|
|
63
|
+
\`\`\`java
|
|
64
|
+
@Controller("/api/v1/users")
|
|
65
|
+
public class UserController {
|
|
66
|
+
|
|
67
|
+
private final UserService userService;
|
|
68
|
+
|
|
69
|
+
public UserController(UserService userService) {
|
|
70
|
+
this.userService = userService;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Get
|
|
74
|
+
public Flux<UserResponse> listUsers() {
|
|
75
|
+
return userService.findAllReactive();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Get("/{id}")
|
|
79
|
+
public Mono<HttpResponse<UserResponse>> getUser(@PathVariable String id) {
|
|
80
|
+
return userService.findByIdReactive(id)
|
|
81
|
+
.map(HttpResponse::ok)
|
|
82
|
+
.defaultIfEmpty(HttpResponse.notFound());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Post
|
|
86
|
+
public Mono<HttpResponse<UserResponse>> createUser(@Valid @Body CreateUserRequest request) {
|
|
87
|
+
return userService.createReactive(request)
|
|
88
|
+
.map(user -> HttpResponse.created(user));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
\`\`\`
|
|
92
|
+
|
|
93
|
+
## Data Access with Micronaut Data
|
|
94
|
+
\`\`\`java
|
|
95
|
+
// Entity
|
|
96
|
+
@MappedEntity("users")
|
|
97
|
+
public class User {
|
|
98
|
+
|
|
99
|
+
@Id
|
|
100
|
+
@GeneratedValue(GeneratedValue.Type.UUID)
|
|
101
|
+
private String id;
|
|
102
|
+
|
|
103
|
+
@Column("email")
|
|
104
|
+
private String email;
|
|
105
|
+
|
|
106
|
+
@Column("name")
|
|
107
|
+
private String name;
|
|
108
|
+
|
|
109
|
+
@Column("password_hash")
|
|
110
|
+
private String passwordHash;
|
|
111
|
+
|
|
112
|
+
@Column("is_active")
|
|
113
|
+
private boolean isActive = true;
|
|
114
|
+
|
|
115
|
+
@DateCreated
|
|
116
|
+
@Column("created_at")
|
|
117
|
+
private Instant createdAt;
|
|
118
|
+
|
|
119
|
+
@DateUpdated
|
|
120
|
+
@Column("updated_at")
|
|
121
|
+
private Instant updatedAt;
|
|
122
|
+
|
|
123
|
+
// Getters and setters...
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Repository
|
|
127
|
+
@JdbcRepository(dialect = Dialect.POSTGRES)
|
|
128
|
+
public interface UserRepository extends PageableRepository<User, String> {
|
|
129
|
+
|
|
130
|
+
Optional<User> findByEmail(String email);
|
|
131
|
+
|
|
132
|
+
List<User> findByIsActiveTrue();
|
|
133
|
+
|
|
134
|
+
@Query("SELECT * FROM users WHERE is_active = true ORDER BY created_at DESC")
|
|
135
|
+
List<User> findActiveUsers();
|
|
136
|
+
|
|
137
|
+
boolean existsByEmail(String email);
|
|
138
|
+
|
|
139
|
+
void updateName(@Id String id, String name);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Reactive Repository
|
|
143
|
+
@R2dbcRepository(dialect = Dialect.POSTGRES)
|
|
144
|
+
public interface UserRepository extends ReactiveStreamsCrudRepository<User, String> {
|
|
145
|
+
|
|
146
|
+
Mono<User> findByEmail(String email);
|
|
147
|
+
|
|
148
|
+
Flux<User> findByIsActiveTrue();
|
|
149
|
+
}
|
|
150
|
+
\`\`\`
|
|
151
|
+
|
|
152
|
+
## Service Layer
|
|
153
|
+
\`\`\`java
|
|
154
|
+
@Singleton
|
|
155
|
+
public class UserService {
|
|
156
|
+
|
|
157
|
+
private final UserRepository userRepository;
|
|
158
|
+
private final PasswordEncoder passwordEncoder;
|
|
159
|
+
|
|
160
|
+
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
|
|
161
|
+
this.userRepository = userRepository;
|
|
162
|
+
this.passwordEncoder = passwordEncoder;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public Page<UserResponse> findAll(Pageable pageable) {
|
|
166
|
+
return userRepository.findAll(pageable)
|
|
167
|
+
.map(UserResponse::from);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public Optional<UserResponse> findById(String id) {
|
|
171
|
+
return userRepository.findById(id)
|
|
172
|
+
.map(UserResponse::from);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@Transactional
|
|
176
|
+
public UserResponse create(CreateUserRequest request) {
|
|
177
|
+
if (userRepository.existsByEmail(request.email())) {
|
|
178
|
+
throw new ConflictException("Email already exists");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
User user = new User();
|
|
182
|
+
user.setEmail(request.email());
|
|
183
|
+
user.setName(request.name());
|
|
184
|
+
user.setPasswordHash(passwordEncoder.encode(request.password()));
|
|
185
|
+
|
|
186
|
+
return UserResponse.from(userRepository.save(user));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@Transactional
|
|
190
|
+
public UserResponse update(String id, UpdateUserRequest request) {
|
|
191
|
+
User user = userRepository.findById(id)
|
|
192
|
+
.orElseThrow(() -> new NotFoundException("User not found: " + id));
|
|
193
|
+
|
|
194
|
+
if (request.name() != null) {
|
|
195
|
+
user.setName(request.name());
|
|
196
|
+
}
|
|
197
|
+
if (request.email() != null) {
|
|
198
|
+
user.setEmail(request.email());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return UserResponse.from(userRepository.update(user));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@Transactional
|
|
205
|
+
public void delete(String id) {
|
|
206
|
+
if (!userRepository.existsById(id)) {
|
|
207
|
+
throw new NotFoundException("User not found: " + id);
|
|
208
|
+
}
|
|
209
|
+
userRepository.deleteById(id);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
\`\`\`
|
|
213
|
+
|
|
214
|
+
## Exception Handling
|
|
215
|
+
\`\`\`java
|
|
216
|
+
@Produces
|
|
217
|
+
@Singleton
|
|
218
|
+
public class GlobalExceptionHandler {
|
|
219
|
+
|
|
220
|
+
@Error(global = true)
|
|
221
|
+
public HttpResponse<ErrorResponse> handleNotFound(
|
|
222
|
+
HttpRequest<?> request,
|
|
223
|
+
NotFoundException exception) {
|
|
224
|
+
return HttpResponse.notFound()
|
|
225
|
+
.body(new ErrorResponse("NOT_FOUND", exception.getMessage()));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@Error(global = true)
|
|
229
|
+
public HttpResponse<ErrorResponse> handleConflict(
|
|
230
|
+
HttpRequest<?> request,
|
|
231
|
+
ConflictException exception) {
|
|
232
|
+
return HttpResponse.status(HttpStatus.CONFLICT)
|
|
233
|
+
.body(new ErrorResponse("CONFLICT", exception.getMessage()));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@Error(global = true)
|
|
237
|
+
public HttpResponse<ValidationErrorResponse> handleValidation(
|
|
238
|
+
HttpRequest<?> request,
|
|
239
|
+
ConstraintViolationException exception) {
|
|
240
|
+
Map<String, String> errors = exception.getConstraintViolations().stream()
|
|
241
|
+
.collect(Collectors.toMap(
|
|
242
|
+
v -> v.getPropertyPath().toString(),
|
|
243
|
+
ConstraintViolation::getMessage
|
|
244
|
+
));
|
|
245
|
+
return HttpResponse.badRequest()
|
|
246
|
+
.body(new ValidationErrorResponse("VALIDATION_ERROR", errors));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
public record ErrorResponse(String code, String message) {}
|
|
251
|
+
public record ValidationErrorResponse(String code, Map<String, String> errors) {}
|
|
252
|
+
\`\`\`
|
|
253
|
+
|
|
254
|
+
## Configuration
|
|
255
|
+
\`\`\`yaml
|
|
256
|
+
# application.yml
|
|
257
|
+
micronaut:
|
|
258
|
+
application:
|
|
259
|
+
name: myapp
|
|
260
|
+
server:
|
|
261
|
+
port: 8080
|
|
262
|
+
|
|
263
|
+
datasources:
|
|
264
|
+
default:
|
|
265
|
+
url: \${DATABASE_URL}
|
|
266
|
+
username: \${DATABASE_USER}
|
|
267
|
+
password: \${DATABASE_PASSWORD}
|
|
268
|
+
dialect: POSTGRES
|
|
269
|
+
|
|
270
|
+
jpa:
|
|
271
|
+
default:
|
|
272
|
+
properties:
|
|
273
|
+
hibernate:
|
|
274
|
+
hbm2ddl:
|
|
275
|
+
auto: validate
|
|
276
|
+
|
|
277
|
+
micronaut:
|
|
278
|
+
security:
|
|
279
|
+
authentication: bearer
|
|
280
|
+
token:
|
|
281
|
+
jwt:
|
|
282
|
+
signatures:
|
|
283
|
+
secret:
|
|
284
|
+
generator:
|
|
285
|
+
secret: \${JWT_SECRET}
|
|
286
|
+
\`\`\`
|
|
287
|
+
|
|
288
|
+
## Security
|
|
289
|
+
\`\`\`java
|
|
290
|
+
@Singleton
|
|
291
|
+
public class AuthenticationProviderUserPassword implements AuthenticationProvider<HttpRequest<?>> {
|
|
292
|
+
|
|
293
|
+
private final UserRepository userRepository;
|
|
294
|
+
private final PasswordEncoder passwordEncoder;
|
|
295
|
+
|
|
296
|
+
public AuthenticationProviderUserPassword(
|
|
297
|
+
UserRepository userRepository,
|
|
298
|
+
PasswordEncoder passwordEncoder) {
|
|
299
|
+
this.userRepository = userRepository;
|
|
300
|
+
this.passwordEncoder = passwordEncoder;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
@Override
|
|
304
|
+
public Publisher<AuthenticationResponse> authenticate(
|
|
305
|
+
HttpRequest<?> httpRequest,
|
|
306
|
+
AuthenticationRequest<?, ?> authRequest) {
|
|
307
|
+
return Flux.just(authRequest)
|
|
308
|
+
.flatMap(req -> {
|
|
309
|
+
String email = (String) req.getIdentity();
|
|
310
|
+
String password = (String) req.getSecret();
|
|
311
|
+
|
|
312
|
+
return userRepository.findByEmail(email)
|
|
313
|
+
.filter(user -> passwordEncoder.matches(password, user.getPasswordHash()))
|
|
314
|
+
.map(user -> AuthenticationResponse.success(user.getId()))
|
|
315
|
+
.switchIfEmpty(Mono.just(AuthenticationResponse.failure()));
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Secured endpoint
|
|
321
|
+
@Controller("/api/v1/admin")
|
|
322
|
+
@Secured(SecurityRule.IS_AUTHENTICATED)
|
|
323
|
+
public class AdminController {
|
|
324
|
+
|
|
325
|
+
@Get("/users")
|
|
326
|
+
@Secured({"ADMIN"})
|
|
327
|
+
public List<UserResponse> listAllUsers() {
|
|
328
|
+
// ...
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
\`\`\`
|
|
332
|
+
|
|
333
|
+
## Testing
|
|
334
|
+
\`\`\`java
|
|
335
|
+
@MicronautTest
|
|
336
|
+
class UserControllerTest {
|
|
337
|
+
|
|
338
|
+
@Inject
|
|
339
|
+
@Client("/")
|
|
340
|
+
HttpClient client;
|
|
341
|
+
|
|
342
|
+
@Inject
|
|
343
|
+
UserRepository userRepository;
|
|
344
|
+
|
|
345
|
+
@Test
|
|
346
|
+
void testGetUser() {
|
|
347
|
+
HttpResponse<UserResponse> response = client.toBlocking()
|
|
348
|
+
.exchange(HttpRequest.GET("/api/v1/users/1"), UserResponse.class);
|
|
349
|
+
|
|
350
|
+
assertEquals(HttpStatus.OK, response.getStatus());
|
|
351
|
+
assertNotNull(response.body());
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
@Test
|
|
355
|
+
void testCreateUser() {
|
|
356
|
+
var request = new CreateUserRequest("test@example.com", "Test", "password123");
|
|
357
|
+
|
|
358
|
+
HttpResponse<UserResponse> response = client.toBlocking()
|
|
359
|
+
.exchange(HttpRequest.POST("/api/v1/users", request), UserResponse.class);
|
|
360
|
+
|
|
361
|
+
assertEquals(HttpStatus.CREATED, response.getStatus());
|
|
362
|
+
assertTrue(response.header("Location").contains("/api/v1/users/"));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@Test
|
|
366
|
+
void testGetUserNotFound() {
|
|
367
|
+
HttpClientResponseException exception = assertThrows(
|
|
368
|
+
HttpClientResponseException.class,
|
|
369
|
+
() -> client.toBlocking().exchange(HttpRequest.GET("/api/v1/users/999"))
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
assertEquals(HttpStatus.NOT_FOUND, exception.getStatus());
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
\`\`\`
|
|
376
|
+
|
|
377
|
+
## ✅ DO
|
|
378
|
+
- Use constructor injection (Micronaut creates at compile time)
|
|
379
|
+
- Use \`@Singleton\` for services (default scope)
|
|
380
|
+
- Use Micronaut Data for repositories
|
|
381
|
+
- Use \`@Error\` handlers for exception mapping
|
|
382
|
+
- Return \`HttpResponse<T>\` for explicit status control
|
|
383
|
+
|
|
384
|
+
## ❌ DON'T
|
|
385
|
+
- Don't use field injection
|
|
386
|
+
- Don't return null - use Optional or throw exceptions
|
|
387
|
+
- Don't forget \`@Transactional\` for write operations
|
|
388
|
+
- Don't use reflection-based libraries (breaks native image)
|