@c0x12c/ai-toolkit 1.15.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-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +12 -0
- package/README.md +439 -0
- package/VERSION +1 -0
- package/agents/design-critic.md +127 -0
- package/agents/idea-killer.md +72 -0
- package/agents/infrastructure-expert.md +49 -0
- package/agents/micronaut-backend-expert.md +45 -0
- package/agents/phase-reviewer.md +150 -0
- package/agents/research-planner.md +70 -0
- package/agents/solution-architect-cto.md +49 -0
- package/agents/sre-architect.md +49 -0
- package/agents/team-coordinator.md +111 -0
- package/bin/cli.js +780 -0
- package/claude-md/00-header.md +39 -0
- package/claude-md/01-core.md +105 -0
- package/claude-md/05-database.md +20 -0
- package/claude-md/11-backend-micronaut.md +19 -0
- package/claude-md/20-frontend-react.md +44 -0
- package/claude-md/25-ux-design.md +56 -0
- package/claude-md/30-infrastructure.md +24 -0
- package/claude-md/30-project-mgmt.md +119 -0
- package/claude-md/40-product.md +39 -0
- package/claude-md/50-ops.md +34 -0
- package/claude-md/60-research.md +27 -0
- package/claude-md/90-footer.md +21 -0
- package/commands/spartan/brainstorm.md +134 -0
- package/commands/spartan/brownfield.md +157 -0
- package/commands/spartan/build.md +435 -0
- package/commands/spartan/careful.md +94 -0
- package/commands/spartan/commit-message.md +112 -0
- package/commands/spartan/content.md +17 -0
- package/commands/spartan/context-save.md +161 -0
- package/commands/spartan/contribute.md +140 -0
- package/commands/spartan/daily.md +42 -0
- package/commands/spartan/debug.md +308 -0
- package/commands/spartan/deep-dive.md +55 -0
- package/commands/spartan/deploy.md +207 -0
- package/commands/spartan/e2e.md +264 -0
- package/commands/spartan/env-setup.md +166 -0
- package/commands/spartan/epic.md +199 -0
- package/commands/spartan/fe-review.md +181 -0
- package/commands/spartan/figma-to-code.md +260 -0
- package/commands/spartan/forensics.md +46 -0
- package/commands/spartan/freeze.md +84 -0
- package/commands/spartan/fundraise.md +53 -0
- package/commands/spartan/gate-review.md +229 -0
- package/commands/spartan/gsd-upgrade.md +376 -0
- package/commands/spartan/guard.md +42 -0
- package/commands/spartan/init-project.md +178 -0
- package/commands/spartan/init-rules.md +298 -0
- package/commands/spartan/interview.md +154 -0
- package/commands/spartan/kickoff.md +73 -0
- package/commands/spartan/kotlin-service.md +109 -0
- package/commands/spartan/lean-canvas.md +222 -0
- package/commands/spartan/lint-rules.md +122 -0
- package/commands/spartan/map-codebase.md +124 -0
- package/commands/spartan/migration.md +82 -0
- package/commands/spartan/next-app.md +317 -0
- package/commands/spartan/next-feature.md +212 -0
- package/commands/spartan/onboard.md +326 -0
- package/commands/spartan/outreach.md +16 -0
- package/commands/spartan/phase.md +142 -0
- package/commands/spartan/pitch.md +18 -0
- package/commands/spartan/plan.md +210 -0
- package/commands/spartan/pr-ready.md +202 -0
- package/commands/spartan/project.md +106 -0
- package/commands/spartan/qa.md +222 -0
- package/commands/spartan/research.md +254 -0
- package/commands/spartan/review.md +132 -0
- package/commands/spartan/scan-rules.md +173 -0
- package/commands/spartan/sessions.md +143 -0
- package/commands/spartan/spec.md +131 -0
- package/commands/spartan/startup.md +257 -0
- package/commands/spartan/team.md +570 -0
- package/commands/spartan/teardown.md +161 -0
- package/commands/spartan/testcontainer.md +97 -0
- package/commands/spartan/tf-cost.md +123 -0
- package/commands/spartan/tf-deploy.md +116 -0
- package/commands/spartan/tf-drift.md +100 -0
- package/commands/spartan/tf-import.md +107 -0
- package/commands/spartan/tf-module.md +121 -0
- package/commands/spartan/tf-plan.md +100 -0
- package/commands/spartan/tf-review.md +106 -0
- package/commands/spartan/tf-scaffold.md +109 -0
- package/commands/spartan/tf-security.md +147 -0
- package/commands/spartan/think.md +221 -0
- package/commands/spartan/unfreeze.md +13 -0
- package/commands/spartan/update.md +134 -0
- package/commands/spartan/ux.md +1233 -0
- package/commands/spartan/validate.md +193 -0
- package/commands/spartan/web-to-prd.md +706 -0
- package/commands/spartan/workstreams.md +109 -0
- package/commands/spartan/write.md +16 -0
- package/commands/spartan.md +386 -0
- package/frameworks/00-framework-comparison-guide.md +317 -0
- package/frameworks/01-lean-canvas.md +196 -0
- package/frameworks/02-design-sprint.md +304 -0
- package/frameworks/03-foundation-sprint.md +337 -0
- package/frameworks/04-business-model-canvas.md +391 -0
- package/frameworks/05-customer-development.md +426 -0
- package/frameworks/06-jobs-to-be-done.md +358 -0
- package/frameworks/07-mom-test.md +392 -0
- package/frameworks/08-value-proposition-canvas.md +488 -0
- package/frameworks/09-javelin-board.md +428 -0
- package/frameworks/10-build-measure-learn.md +467 -0
- package/frameworks/11-mvp-approaches.md +533 -0
- package/frameworks/think-before-build.md +593 -0
- package/lib/assembler.js +197 -0
- package/lib/assembler.test.js +159 -0
- package/lib/detector.js +166 -0
- package/lib/detector.test.js +221 -0
- package/lib/packs.js +16 -0
- package/lib/resolver.js +272 -0
- package/lib/resolver.test.js +298 -0
- package/lib/worktree.sh +104 -0
- package/package.json +50 -0
- package/packs/backend-micronaut.yaml +35 -0
- package/packs/backend-nodejs.yaml +15 -0
- package/packs/backend-python.yaml +15 -0
- package/packs/core.yaml +37 -0
- package/packs/database.yaml +21 -0
- package/packs/frontend-react.yaml +24 -0
- package/packs/infrastructure.yaml +40 -0
- package/packs/ops.yaml +16 -0
- package/packs/packs.compiled.json +371 -0
- package/packs/product.yaml +22 -0
- package/packs/project-mgmt.yaml +24 -0
- package/packs/research.yaml +39 -0
- package/packs/shared-backend.yaml +14 -0
- package/packs/ux-design.yaml +21 -0
- package/rules/backend-micronaut/API_DESIGN.md +313 -0
- package/rules/backend-micronaut/BATCH_PROCESSING.md +92 -0
- package/rules/backend-micronaut/CONTROLLERS.md +388 -0
- package/rules/backend-micronaut/KOTLIN.md +414 -0
- package/rules/backend-micronaut/RETROFIT_PLACEMENT.md +290 -0
- package/rules/backend-micronaut/SERVICES_AND_BEANS.md +325 -0
- package/rules/core/NAMING_CONVENTIONS.md +208 -0
- package/rules/core/SKILL_AUTHORING.md +174 -0
- package/rules/core/TIMEZONE.md +316 -0
- package/rules/database/ORM_AND_REPO.md +289 -0
- package/rules/database/SCHEMA.md +146 -0
- package/rules/database/TRANSACTIONS.md +311 -0
- package/rules/frontend-react/FRONTEND.md +344 -0
- package/rules/infrastructure/MODULES.md +260 -0
- package/rules/infrastructure/NAMING.md +196 -0
- package/rules/infrastructure/PROVIDERS.md +309 -0
- package/rules/infrastructure/SECURITY.md +310 -0
- package/rules/infrastructure/STATE_AND_BACKEND.md +237 -0
- package/rules/infrastructure/STRUCTURE.md +234 -0
- package/rules/infrastructure/VARIABLES.md +285 -0
- package/rules/shared-backend/ARCHITECTURE.md +46 -0
- package/rules/ux-design/DESIGN_PROCESS.md +176 -0
- package/skills/api-endpoint-creator/SKILL.md +455 -0
- package/skills/api-endpoint-creator/error-handling-guide.md +244 -0
- package/skills/api-endpoint-creator/examples.md +522 -0
- package/skills/api-endpoint-creator/testing-patterns.md +302 -0
- package/skills/article-writing/SKILL.md +109 -0
- package/skills/article-writing/examples.md +59 -0
- package/skills/backend-api-design/SKILL.md +84 -0
- package/skills/backend-api-design/code-patterns.md +138 -0
- package/skills/brainstorm/SKILL.md +95 -0
- package/skills/browser-qa/SKILL.md +87 -0
- package/skills/browser-qa/playwright-snippets.md +110 -0
- package/skills/ci-cd-patterns/SKILL.md +108 -0
- package/skills/ci-cd-patterns/workflows.md +149 -0
- package/skills/competitive-teardown/SKILL.md +93 -0
- package/skills/competitive-teardown/example-analysis.md +50 -0
- package/skills/content-engine/SKILL.md +131 -0
- package/skills/content-engine/examples.md +72 -0
- package/skills/database-patterns/SKILL.md +72 -0
- package/skills/database-patterns/code-templates.md +114 -0
- package/skills/database-table-creator/SKILL.md +141 -0
- package/skills/database-table-creator/examples.md +552 -0
- package/skills/database-table-creator/kotlin-templates.md +400 -0
- package/skills/database-table-creator/migration-template.sql +68 -0
- package/skills/database-table-creator/validation-checklist.md +337 -0
- package/skills/deep-research/SKILL.md +80 -0
- package/skills/design-intelligence/SKILL.md +268 -0
- package/skills/design-workflow/SKILL.md +127 -0
- package/skills/design-workflow/checklists.md +45 -0
- package/skills/idea-validation/SKILL.md +129 -0
- package/skills/idea-validation/example-report.md +50 -0
- package/skills/investor-materials/SKILL.md +122 -0
- package/skills/investor-materials/example-outline.md +70 -0
- package/skills/investor-outreach/SKILL.md +112 -0
- package/skills/investor-outreach/examples.md +76 -0
- package/skills/kotlin-best-practices/SKILL.md +58 -0
- package/skills/kotlin-best-practices/code-patterns.md +132 -0
- package/skills/market-research/SKILL.md +99 -0
- package/skills/security-checklist/SKILL.md +65 -0
- package/skills/security-checklist/audit-reference.md +95 -0
- package/skills/service-debugging/SKILL.md +116 -0
- package/skills/service-debugging/common-issues.md +65 -0
- package/skills/startup-pipeline/SKILL.md +152 -0
- package/skills/terraform-best-practices/SKILL.md +244 -0
- package/skills/terraform-module-creator/SKILL.md +284 -0
- package/skills/terraform-review/SKILL.md +222 -0
- package/skills/terraform-security-audit/SKILL.md +280 -0
- package/skills/terraform-service-scaffold/SKILL.md +574 -0
- package/skills/testing-strategies/SKILL.md +116 -0
- package/skills/testing-strategies/examples.md +103 -0
- package/skills/testing-strategies/integration-test-setup.md +71 -0
- package/skills/ui-ux-pro-max/SKILL.md +238 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/python-setup.md +146 -0
- package/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/skills/web-to-prd/SKILL.md +478 -0
- package/templates/build-config.yaml +44 -0
- package/templates/commands-config.yaml +55 -0
- package/templates/competitor-analysis.md +60 -0
- package/templates/content/AGENT_TEMPLATE.md +47 -0
- package/templates/content/COMMAND_TEMPLATE.md +27 -0
- package/templates/content/RULE_TEMPLATE.md +40 -0
- package/templates/content/SKILL_TEMPLATE.md +41 -0
- package/templates/design-config.md +105 -0
- package/templates/design-doc.md +207 -0
- package/templates/epic.md +100 -0
- package/templates/feature-spec.md +181 -0
- package/templates/idea-canvas.md +47 -0
- package/templates/implementation-plan.md +159 -0
- package/templates/prd-template.md +86 -0
- package/templates/preamble.md +89 -0
- package/templates/project-readme.md +35 -0
- package/templates/quality-gates.md +230 -0
- package/templates/spartan-config.yaml +164 -0
- package/templates/user-interview.md +69 -0
- package/templates/validation-checklist.md +108 -0
- package/templates/workflow-backend-micronaut.md +409 -0
- package/templates/workflow-frontend-react.md +233 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# API Endpoint Testing Patterns
|
|
2
|
+
|
|
3
|
+
How to write integration tests for API endpoints in the platform.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Test Structure
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Integration Test (Controller)
|
|
11
|
+
-> Retrofit HTTP Client
|
|
12
|
+
-> Actual HTTP Request
|
|
13
|
+
-> Controller -> Manager -> Repository -> Database
|
|
14
|
+
-> HTTP Response
|
|
15
|
+
-> Assertions
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Integration tests check the full stack works together.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Base Test Class
|
|
23
|
+
|
|
24
|
+
All controller tests extend `AbstractControllerTest`:
|
|
25
|
+
|
|
26
|
+
```kotlin
|
|
27
|
+
@MicronautTest(environments = ["test"], transactional = false)
|
|
28
|
+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
|
29
|
+
class ProjectControllerTest : AbstractControllerTest() {
|
|
30
|
+
|
|
31
|
+
private lateinit var projectClient: ProjectClient
|
|
32
|
+
|
|
33
|
+
@BeforeAll
|
|
34
|
+
override fun beforeAll() {
|
|
35
|
+
val url = embeddedServer.url.toString()
|
|
36
|
+
val jackson = ObjectMapper().configured()
|
|
37
|
+
val retrofit = Retrofits
|
|
38
|
+
.newBuilder(url = url.toHttpUrl(), jackson = jackson)
|
|
39
|
+
.build()
|
|
40
|
+
|
|
41
|
+
projectClient = retrofit.create(ProjectClient::class.java)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@AfterEach
|
|
45
|
+
fun cleanupAfterEach() {
|
|
46
|
+
database.primary.truncateAllTables()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Key Points**:
|
|
52
|
+
- `@MicronautTest(environments = ["test"])` - Use test environment
|
|
53
|
+
- `@TestInstance(Lifecycle.PER_CLASS)` - Reuse test instance
|
|
54
|
+
- Extend `AbstractControllerTest` for utilities
|
|
55
|
+
- Create Retrofit client in `@BeforeAll`
|
|
56
|
+
- Clean database in `@AfterEach`
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Retrofit Client
|
|
61
|
+
|
|
62
|
+
**File**: `app/module-client/src/main/kotlin/com/yourcompany/client/ProjectClient.kt`
|
|
63
|
+
|
|
64
|
+
```kotlin
|
|
65
|
+
package com.yourcompany.client
|
|
66
|
+
|
|
67
|
+
import com.yourcompany.client.request.{domain}.CreateProjectRequest
|
|
68
|
+
import com.yourcompany.client.response.{domain}.ProjectResponse
|
|
69
|
+
import com.yourcompany.client.response.{domain}.ProjectListResponse
|
|
70
|
+
import retrofit2.http.*
|
|
71
|
+
import java.util.UUID
|
|
72
|
+
|
|
73
|
+
interface ProjectClient {
|
|
74
|
+
|
|
75
|
+
@GET("/api/v1/admin/projects")
|
|
76
|
+
suspend fun listProjects(
|
|
77
|
+
@Header("Authorization") authorization: String,
|
|
78
|
+
@Query("page") page: Int? = null,
|
|
79
|
+
@Query("limit") limit: Int? = null
|
|
80
|
+
): ProjectListResponse
|
|
81
|
+
|
|
82
|
+
@GET("/api/v1/admin/project")
|
|
83
|
+
suspend fun getProject(
|
|
84
|
+
@Header("Authorization") authorization: String,
|
|
85
|
+
@Query("id") id: UUID
|
|
86
|
+
): ProjectResponse
|
|
87
|
+
|
|
88
|
+
@POST("/api/v1/admin/project")
|
|
89
|
+
suspend fun createProject(
|
|
90
|
+
@Header("Authorization") authorization: String,
|
|
91
|
+
@Body request: CreateProjectRequest
|
|
92
|
+
): ProjectResponse
|
|
93
|
+
|
|
94
|
+
@POST("/api/v1/admin/project/delete")
|
|
95
|
+
suspend fun deleteProject(
|
|
96
|
+
@Header("Authorization") authorization: String,
|
|
97
|
+
@Query("id") id: UUID
|
|
98
|
+
): Boolean
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Test Pattern 1: Happy Path
|
|
105
|
+
|
|
106
|
+
```kotlin
|
|
107
|
+
@Test
|
|
108
|
+
fun `getProject - returns project when exists`() = runBlocking {
|
|
109
|
+
val project = createTestProject()
|
|
110
|
+
val token = accessToken(prepareUser(), UserRole.ADMIN)
|
|
111
|
+
|
|
112
|
+
val response = projectClient.getProject(
|
|
113
|
+
authorization = token,
|
|
114
|
+
id = project.id
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
assertThat(response.id).isEqualTo(project.id)
|
|
118
|
+
assertThat(response.name).isEqualTo(project.name)
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Test Pattern 2: Not Found (404)
|
|
123
|
+
|
|
124
|
+
```kotlin
|
|
125
|
+
@Test
|
|
126
|
+
fun `getProject - returns 404 when not found`() = runBlocking {
|
|
127
|
+
val token = accessToken(prepareUser(), UserRole.ADMIN)
|
|
128
|
+
|
|
129
|
+
val exception = assertThrows<HttpException> {
|
|
130
|
+
runBlocking {
|
|
131
|
+
projectClient.getProject(
|
|
132
|
+
authorization = token,
|
|
133
|
+
id = UUID.randomUUID()
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
assertThat(exception.code()).isEqualTo(404)
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Test Pattern 3: Authentication (401)
|
|
143
|
+
|
|
144
|
+
```kotlin
|
|
145
|
+
@Test
|
|
146
|
+
fun `should return 401 when not authenticated`() = runBlocking {
|
|
147
|
+
val exception = assertThrows<HttpException> {
|
|
148
|
+
runBlocking {
|
|
149
|
+
projectClient.getProject(
|
|
150
|
+
authorization = "",
|
|
151
|
+
id = UUID.randomUUID()
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
assertThat(exception.code()).isEqualTo(401)
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Test Pattern 4: Create and Verify
|
|
161
|
+
|
|
162
|
+
```kotlin
|
|
163
|
+
@Test
|
|
164
|
+
fun `createProject - creates project successfully`() = runBlocking {
|
|
165
|
+
val token = accessToken(prepareUser(), UserRole.ADMIN)
|
|
166
|
+
val request = CreateProjectRequest(
|
|
167
|
+
name = "Test Project",
|
|
168
|
+
description = "A test project"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
val response = projectClient.createProject(
|
|
172
|
+
authorization = token,
|
|
173
|
+
request = request
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
assertThat(response.name).isEqualTo("Test Project")
|
|
177
|
+
assertThat(response.description).isEqualTo("A test project")
|
|
178
|
+
|
|
179
|
+
// Verify in database
|
|
180
|
+
val saved = projectRepository.byId(response.id)
|
|
181
|
+
assertThat(saved).isNotNull
|
|
182
|
+
assertThat(saved?.name).isEqualTo("Test Project")
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Test Pattern 5: List with Pagination
|
|
187
|
+
|
|
188
|
+
```kotlin
|
|
189
|
+
@Test
|
|
190
|
+
fun `listProjects - returns paginated results`() = runBlocking {
|
|
191
|
+
createTestProject(name = "Project A")
|
|
192
|
+
createTestProject(name = "Project B")
|
|
193
|
+
createTestProject(name = "Project C")
|
|
194
|
+
|
|
195
|
+
val token = accessToken(prepareUser(), UserRole.ADMIN)
|
|
196
|
+
|
|
197
|
+
val response = projectClient.listProjects(
|
|
198
|
+
authorization = token,
|
|
199
|
+
page = 1,
|
|
200
|
+
limit = 2
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
assertThat(response.items).hasSize(2)
|
|
204
|
+
assertThat(response.total).isEqualTo(3)
|
|
205
|
+
assertThat(response.hasMore).isTrue()
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Test Pattern 6: Soft Delete
|
|
210
|
+
|
|
211
|
+
```kotlin
|
|
212
|
+
@Test
|
|
213
|
+
fun `deleteProject - soft deletes project`() = runBlocking {
|
|
214
|
+
val project = createTestProject()
|
|
215
|
+
val token = accessToken(prepareUser(), UserRole.ADMIN)
|
|
216
|
+
|
|
217
|
+
val result = projectClient.deleteProject(
|
|
218
|
+
authorization = token,
|
|
219
|
+
id = project.id
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
assertThat(result).isTrue()
|
|
223
|
+
|
|
224
|
+
// Verify not returned by queries
|
|
225
|
+
val exception = assertThrows<HttpException> {
|
|
226
|
+
runBlocking {
|
|
227
|
+
projectClient.getProject(authorization = token, id = project.id)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
assertThat(exception.code()).isEqualTo(404)
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Test Helper Patterns
|
|
237
|
+
|
|
238
|
+
```kotlin
|
|
239
|
+
private val projectRepository: ProjectRepository by lazy {
|
|
240
|
+
DefaultProjectRepository(database)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private fun createTestProject(
|
|
244
|
+
name: String = "Test Project ${UUID.randomUUID()}",
|
|
245
|
+
description: String? = "Test description",
|
|
246
|
+
status: String = "active"
|
|
247
|
+
): ProjectEntity {
|
|
248
|
+
return projectRepository.insert(
|
|
249
|
+
ProjectEntity(
|
|
250
|
+
name = name,
|
|
251
|
+
description = description,
|
|
252
|
+
status = status
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Utility methods from `AbstractControllerTest`**:
|
|
259
|
+
- `prepareUser(email, displayName, role)` - Create test user
|
|
260
|
+
- `accessToken(user, role)` - Generate JWT token
|
|
261
|
+
- `database` - DatabaseContext for direct DB access
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Test Scenarios Checklist
|
|
266
|
+
|
|
267
|
+
For each endpoint, test:
|
|
268
|
+
|
|
269
|
+
### Create
|
|
270
|
+
- [ ] Happy path succeeds
|
|
271
|
+
- [ ] Returns created resource
|
|
272
|
+
- [ ] Resource exists in database
|
|
273
|
+
- [ ] Requires authentication (401)
|
|
274
|
+
|
|
275
|
+
### Read (Get by ID)
|
|
276
|
+
- [ ] Returns resource when exists (200)
|
|
277
|
+
- [ ] Returns 404 when not found
|
|
278
|
+
- [ ] Requires authentication (401)
|
|
279
|
+
|
|
280
|
+
### List
|
|
281
|
+
- [ ] Returns paginated results
|
|
282
|
+
- [ ] Pagination works (page/limit)
|
|
283
|
+
- [ ] Returns empty list when no results
|
|
284
|
+
- [ ] Requires authentication (401)
|
|
285
|
+
|
|
286
|
+
### Delete
|
|
287
|
+
- [ ] Soft deletes successfully
|
|
288
|
+
- [ ] Resource not returned after deletion
|
|
289
|
+
- [ ] Returns 404 when not found
|
|
290
|
+
- [ ] Requires authentication (401)
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Best Practices
|
|
295
|
+
|
|
296
|
+
1. **Isolation**: Clean database between tests (`@AfterEach`)
|
|
297
|
+
2. **Random data**: Use UUIDs in names to avoid conflicts
|
|
298
|
+
3. **AssertJ**: Use `assertThat` for readable assertions
|
|
299
|
+
4. **Given/When/Then**: Structure tests clearly
|
|
300
|
+
5. **Verify database**: Don't just check the response
|
|
301
|
+
6. **Auth tests**: Always test with and without auth
|
|
302
|
+
7. **Use Retrofit clients**: Never raw HttpRequest for API tests
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: article-writing
|
|
3
|
+
description: Write blog posts, guides, tutorials, and long-form content. Sounds like a real person, not AI. Use when the user wants polished written content.
|
|
4
|
+
allowed_tools:
|
|
5
|
+
- Read
|
|
6
|
+
- Write
|
|
7
|
+
- WebSearch
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Article Writing
|
|
11
|
+
|
|
12
|
+
Write long-form content that sounds like a person wrote it.
|
|
13
|
+
|
|
14
|
+
## When to Use
|
|
15
|
+
|
|
16
|
+
- Blog posts, essays, guides, tutorials
|
|
17
|
+
- Turning notes or research into an article
|
|
18
|
+
- Matching an existing voice from examples
|
|
19
|
+
- Cleaning up and tightening existing writing
|
|
20
|
+
|
|
21
|
+
> See `examples.md` for good vs bad writing examples that show what "sounds human" actually means.
|
|
22
|
+
|
|
23
|
+
## Rules
|
|
24
|
+
|
|
25
|
+
1. Start with something concrete: example, number, story, or code block.
|
|
26
|
+
2. Explain after the example, not before.
|
|
27
|
+
3. Short, direct sentences.
|
|
28
|
+
4. Use real numbers when you have them.
|
|
29
|
+
5. Never make up facts, company data, or quotes.
|
|
30
|
+
|
|
31
|
+
## Banned Patterns
|
|
32
|
+
|
|
33
|
+
Delete and rewrite any of these:
|
|
34
|
+
- "In today's rapidly evolving landscape"
|
|
35
|
+
- "Moreover", "Furthermore", "Additionally"
|
|
36
|
+
- "game-changer", "cutting-edge", "revolutionary"
|
|
37
|
+
- Vague claims with no proof
|
|
38
|
+
- Bio claims you can't back up
|
|
39
|
+
|
|
40
|
+
## Writing Process
|
|
41
|
+
|
|
42
|
+
1. Know the audience and goal
|
|
43
|
+
2. Outline: one purpose per section
|
|
44
|
+
3. Start each section with evidence or example
|
|
45
|
+
4. Only keep sentences that earn their spot
|
|
46
|
+
5. Cut anything that sounds like a template
|
|
47
|
+
|
|
48
|
+
## Structure Tips
|
|
49
|
+
|
|
50
|
+
### Technical Guides
|
|
51
|
+
- Open with what the reader gets
|
|
52
|
+
- Code or terminal examples in every section
|
|
53
|
+
- End with real takeaways, not soft summary
|
|
54
|
+
|
|
55
|
+
### Essays / Opinion
|
|
56
|
+
- Start with tension or a sharp point
|
|
57
|
+
- One argument per section
|
|
58
|
+
- Back opinions with examples
|
|
59
|
+
|
|
60
|
+
### Newsletters
|
|
61
|
+
- First screen must be strong
|
|
62
|
+
- Mix insight with updates
|
|
63
|
+
- Clear section labels, easy to skim
|
|
64
|
+
|
|
65
|
+
## Interaction Style
|
|
66
|
+
|
|
67
|
+
**No BS. Honest feedback only.**
|
|
68
|
+
|
|
69
|
+
This is a two-way talk:
|
|
70
|
+
- I ask you questions → you answer
|
|
71
|
+
- You ask me questions → I think hard, give you options, then answer
|
|
72
|
+
|
|
73
|
+
**When I ask you a question, I always:**
|
|
74
|
+
1. Think about it first
|
|
75
|
+
2. Give you 2-3 options with my honest take on each
|
|
76
|
+
3. Tell you which one I'd pick and why
|
|
77
|
+
4. Then ask what you think
|
|
78
|
+
|
|
79
|
+
**When you ask me something:**
|
|
80
|
+
- I give you a straight answer
|
|
81
|
+
- I tell you if a section is boring, weak, or filler
|
|
82
|
+
- I push for real examples over vague claims
|
|
83
|
+
|
|
84
|
+
**Never:**
|
|
85
|
+
- Ask a question without giving options
|
|
86
|
+
- Let filler paragraphs stay in the draft
|
|
87
|
+
- Say "it depends" without picking a side
|
|
88
|
+
- Skip the "this part is weak" feedback
|
|
89
|
+
- Write AI-sounding content and call it done
|
|
90
|
+
|
|
91
|
+
## Gotchas
|
|
92
|
+
|
|
93
|
+
- **The intro is where most articles die.** If the first paragraph starts with "In this article, we'll explore..." — delete it and start with a story, stat, or code block.
|
|
94
|
+
- **AI-written articles all sound the same.** They hedge ("it's important to note"), use transition words nobody says out loud ("Moreover"), and avoid strong opinions. Cut all of that.
|
|
95
|
+
- **Don't explain the obvious.** If your audience is developers, don't explain what an API is. Write for the person, not the lowest common denominator.
|
|
96
|
+
- **Long ≠ thorough.** A 3,000-word article with 1,500 words of filler is worse than a 1,500-word article where every sentence earns its spot.
|
|
97
|
+
- **Every section needs evidence.** A claim without a number, example, or code block is just an opinion. Back it up or cut it.
|
|
98
|
+
|
|
99
|
+
## Before You Deliver
|
|
100
|
+
|
|
101
|
+
- Facts match sources
|
|
102
|
+
- No filler or corporate talk
|
|
103
|
+
- Voice matches examples (if given)
|
|
104
|
+
- Every section adds something new
|
|
105
|
+
- Format fits the platform
|
|
106
|
+
|
|
107
|
+
## Output
|
|
108
|
+
|
|
109
|
+
Save to the project's research or build folder depending on context.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Article Writing — Good vs Bad Examples
|
|
2
|
+
|
|
3
|
+
> Read these examples to calibrate your writing style. The "bad" versions are typical AI output. The "good" versions sound like a person wrote them.
|
|
4
|
+
|
|
5
|
+
## Intro Paragraphs
|
|
6
|
+
|
|
7
|
+
### Bad (AI-style)
|
|
8
|
+
> In today's rapidly evolving technological landscape, the importance of effective error handling cannot be overstated. This comprehensive guide will walk you through the various aspects of implementing robust error handling strategies in your Kotlin applications, enabling you to build more resilient and maintainable software systems.
|
|
9
|
+
|
|
10
|
+
### Good (human-style)
|
|
11
|
+
> Last Tuesday, our payment service threw 4,000 unhandled exceptions in 20 minutes. The fix was 3 lines of code. Here's what went wrong and how to make sure it doesn't happen to you.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Explaining a Concept
|
|
16
|
+
|
|
17
|
+
### Bad (AI-style)
|
|
18
|
+
> Dependency injection is a crucial software design pattern that facilitates the development of loosely coupled, maintainable, and testable code. By leveraging this paradigm, developers can effectively manage the complex dependencies between various components of their application architecture.
|
|
19
|
+
|
|
20
|
+
### Good (human-style)
|
|
21
|
+
> Dependency injection means your class doesn't create its own dependencies — someone hands them in. Instead of `val db = Database()` inside your class, you write `class UserRepo(val db: Database)` and let the framework wire it up. That's it. The rest is details.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Technical Guide Section
|
|
26
|
+
|
|
27
|
+
### Bad (AI-style)
|
|
28
|
+
> To implement this feature, you'll want to start by carefully considering the architectural implications. First, create a new service class that will handle the business logic. Then, implement the necessary repository methods. Finally, wire everything together in the controller layer. This approach ensures a clean separation of concerns.
|
|
29
|
+
|
|
30
|
+
### Good (human-style)
|
|
31
|
+
> Three files to create:
|
|
32
|
+
> 1. `PaymentManager.kt` — validates the amount, calls Stripe, saves the record
|
|
33
|
+
> 2. `PaymentRepository.kt` — insert and soft-delete methods
|
|
34
|
+
> 3. `PaymentController.kt` — one POST endpoint, delegates to the manager
|
|
35
|
+
>
|
|
36
|
+
> Start with the manager. The other two are boilerplate.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Transitions Between Sections
|
|
41
|
+
|
|
42
|
+
### Bad (AI-style)
|
|
43
|
+
> Now that we've explored the fundamentals of error handling, let's delve into the more advanced aspects of this topic. In the following section, we'll examine how to implement custom error types that can provide more granular control over your application's error handling mechanisms.
|
|
44
|
+
|
|
45
|
+
### Good (human-style)
|
|
46
|
+
> The basics work for 80% of cases. But when you need different error responses for different callers (API vs webhook vs internal), you need custom error types.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Conclusions
|
|
51
|
+
|
|
52
|
+
### Bad (AI-style)
|
|
53
|
+
> In conclusion, we've covered a comprehensive overview of error handling patterns in Kotlin. By implementing these strategies, you'll be well-equipped to build robust, maintainable applications. Remember that error handling is not just about catching exceptions — it's about creating a resilient system that gracefully handles the unexpected.
|
|
54
|
+
|
|
55
|
+
### Good (human-style)
|
|
56
|
+
> Three things to remember:
|
|
57
|
+
> 1. Return `Either`, don't throw. Your callers will thank you.
|
|
58
|
+
> 2. Log the actual error, not a generic message.
|
|
59
|
+
> 3. Test the error paths. They run more often than you think.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backend-api-design
|
|
3
|
+
description: Design RPC-style APIs with layered architecture (Controller → Manager → Repository). Use when creating new API endpoints, designing API contracts, or reviewing API patterns.
|
|
4
|
+
allowed_tools:
|
|
5
|
+
- Read
|
|
6
|
+
- Write
|
|
7
|
+
- Edit
|
|
8
|
+
- Glob
|
|
9
|
+
- Grep
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Backend API Design — Quick Reference
|
|
13
|
+
|
|
14
|
+
## URL Patterns
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
GET /api/v1/employees # List (plural)
|
|
18
|
+
GET /api/v1/employee # Get one (?id=xxx)
|
|
19
|
+
POST /api/v1/employee # Create
|
|
20
|
+
POST /api/v1/employee/update # Update (?id=xxx)
|
|
21
|
+
POST /api/v1/employee/delete # Soft delete (?id=xxx)
|
|
22
|
+
POST /api/v1/employee/restore # Restore (?id=xxx)
|
|
23
|
+
POST /api/v1/sync/employees # Action
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Hard Rules
|
|
27
|
+
|
|
28
|
+
- **NO path params** — always `@QueryValue`, never `@PathVariable`
|
|
29
|
+
- **Singular for single resource** — `/employee` not `/employees/{id}`
|
|
30
|
+
- **Plural for collections** — `/employees`
|
|
31
|
+
- **Verb sub-paths for actions** — `/delete`, `/restore`, `/sync`
|
|
32
|
+
|
|
33
|
+
## Layered Architecture
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Controller → thin, just delegates
|
|
37
|
+
↓
|
|
38
|
+
Manager → business logic, transactions, Either returns
|
|
39
|
+
↓
|
|
40
|
+
Repository → data access only, no business logic
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Controller: Thin Wrapper
|
|
44
|
+
- Parse query params with defaults
|
|
45
|
+
- Delegate to manager
|
|
46
|
+
- Unwrap Either with `.throwOrValue()`
|
|
47
|
+
- NO business logic, NO repository access
|
|
48
|
+
|
|
49
|
+
### Manager: Business Logic
|
|
50
|
+
- Returns `Either<ClientException, T>`
|
|
51
|
+
- Wraps DB ops in `transaction(db.primary) { }`
|
|
52
|
+
- Orchestrates multiple repositories
|
|
53
|
+
- Validates business rules
|
|
54
|
+
|
|
55
|
+
### Repository: Data Access
|
|
56
|
+
- Returns entities or null
|
|
57
|
+
- `db.replica` for reads, `db.primary` for writes
|
|
58
|
+
- Always checks `deletedAt.isNull()`
|
|
59
|
+
|
|
60
|
+
## Quick Code Reference
|
|
61
|
+
|
|
62
|
+
The core controller delegation pattern:
|
|
63
|
+
|
|
64
|
+
```kotlin
|
|
65
|
+
@Get("/employee")
|
|
66
|
+
suspend fun getEmployee(@QueryValue id: UUID): EmployeeResponse {
|
|
67
|
+
return employeeManager.findById(id).throwOrValue()
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- **Response models** — `companion object { fun from(entity) }` in `module-client/response/{domain}/`
|
|
72
|
+
- **Pagination** — offset-based, manager returns `EmployeeListResponse` with `items`, `total`, `page`, `limit`, `hasMore`
|
|
73
|
+
- **Errors** — return `ClientError.NOT_FOUND.asException().left()` from managers, never throw
|
|
74
|
+
- **Factory beans** — `@Factory` class with `@Singleton` method, wire repos + db into manager
|
|
75
|
+
|
|
76
|
+
> See code-patterns.md for complete controller, response model, pagination, error handling, and factory bean templates.
|
|
77
|
+
|
|
78
|
+
## Gotchas
|
|
79
|
+
|
|
80
|
+
- **Multi-word `@QueryValue` params MUST have explicit snake_case names.** The frontend axios interceptor sends `project_id` but Micronaut matches the literal param name. Write `@QueryValue("project_id") projectId: UUID`, not bare `@QueryValue projectId: UUID`.
|
|
81
|
+
- **Don't use `@Put`, `@Delete`, or `@Patch`.** This is RPC-style — all mutations are `@Post`. The only `@Get` is for reads.
|
|
82
|
+
- **Controllers that inject repositories are a code smell.** If you see `private val fooRepository: FooRepository` in a controller, move it to the manager.
|
|
83
|
+
- **`andWhere {}` not second `.where {}`.** Calling `.where {}` twice replaces the first condition. Use `.andWhere {}` to chain.
|
|
84
|
+
- **Don't forget `@ExecuteOn(TaskExecutors.IO)`.** Without it, suspend functions may hang or run on the wrong thread pool. Every controller needs it.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# API Design — Code Patterns
|
|
2
|
+
|
|
3
|
+
> This file is referenced by SKILL.md. Read it when implementing actual endpoints.
|
|
4
|
+
|
|
5
|
+
## Controller Template
|
|
6
|
+
|
|
7
|
+
```kotlin
|
|
8
|
+
@ExecuteOn(TaskExecutors.IO) // REQUIRED for suspend
|
|
9
|
+
@Validated
|
|
10
|
+
@Controller("/api/v1/admin")
|
|
11
|
+
@Secured(OAuthSecurityRule.ADMIN)
|
|
12
|
+
class EmployeeController(
|
|
13
|
+
private val employeeManager: EmployeeManager // Managers ONLY
|
|
14
|
+
) {
|
|
15
|
+
@Get("/employee")
|
|
16
|
+
suspend fun getEmployee(@QueryValue id: UUID): EmployeeResponse {
|
|
17
|
+
return employeeManager.findById(id).throwOrValue()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Get("/employees")
|
|
21
|
+
suspend fun listEmployees(
|
|
22
|
+
@QueryValue page: Int?,
|
|
23
|
+
@QueryValue limit: Int?,
|
|
24
|
+
@QueryValue status: String?
|
|
25
|
+
): EmployeeListResponse {
|
|
26
|
+
return employeeManager.list(
|
|
27
|
+
page = page ?: 1,
|
|
28
|
+
limit = (limit ?: 20).coerceAtMost(100),
|
|
29
|
+
status = status
|
|
30
|
+
).throwOrValue()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Response Models
|
|
36
|
+
|
|
37
|
+
All in `module-client/response/{domain}/`:
|
|
38
|
+
|
|
39
|
+
```kotlin
|
|
40
|
+
data class EmployeeResponse(
|
|
41
|
+
val id: UUID,
|
|
42
|
+
val name: String,
|
|
43
|
+
val email: String,
|
|
44
|
+
val status: String,
|
|
45
|
+
val createdAt: Instant
|
|
46
|
+
) {
|
|
47
|
+
companion object {
|
|
48
|
+
fun from(entity: EmployeeEntity) = EmployeeResponse(
|
|
49
|
+
id = entity.id,
|
|
50
|
+
name = entity.name,
|
|
51
|
+
email = entity.email,
|
|
52
|
+
status = entity.status,
|
|
53
|
+
createdAt = entity.createdAt
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
data class EmployeeListResponse(
|
|
59
|
+
val items: List<EmployeeResponse>,
|
|
60
|
+
val total: Int,
|
|
61
|
+
val page: Int,
|
|
62
|
+
val limit: Int,
|
|
63
|
+
val hasMore: Boolean
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Pagination Pattern
|
|
68
|
+
|
|
69
|
+
```kotlin
|
|
70
|
+
override suspend fun list(
|
|
71
|
+
page: Int,
|
|
72
|
+
limit: Int,
|
|
73
|
+
status: String?
|
|
74
|
+
): Either<ClientException, EmployeeListResponse> {
|
|
75
|
+
val offset = (page - 1) * limit
|
|
76
|
+
|
|
77
|
+
val (items, total) = transaction(db.replica) {
|
|
78
|
+
val query = EmployeesTable
|
|
79
|
+
.selectAll()
|
|
80
|
+
.where { EmployeesTable.deletedAt.isNull() }
|
|
81
|
+
|
|
82
|
+
if (status != null) {
|
|
83
|
+
query.andWhere { EmployeesTable.status eq status }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
val total = query.count().toInt()
|
|
87
|
+
val items = query
|
|
88
|
+
.orderBy(EmployeesTable.createdAt to SortOrder.DESC)
|
|
89
|
+
.limit(limit)
|
|
90
|
+
.offset(offset.toLong())
|
|
91
|
+
.map { convert(it) }
|
|
92
|
+
|
|
93
|
+
items to total
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return EmployeeListResponse(
|
|
97
|
+
items = items.map { EmployeeResponse.from(it) },
|
|
98
|
+
total = total,
|
|
99
|
+
page = page,
|
|
100
|
+
limit = limit,
|
|
101
|
+
hasMore = (page * limit) < total
|
|
102
|
+
).right()
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Error Pattern
|
|
107
|
+
|
|
108
|
+
```kotlin
|
|
109
|
+
// Not found
|
|
110
|
+
val entity = repository.byId(id)
|
|
111
|
+
?: return ClientError.NOT_FOUND.asException().left()
|
|
112
|
+
|
|
113
|
+
// Already exists
|
|
114
|
+
val existing = repository.byEmail(email)
|
|
115
|
+
if (existing != null) {
|
|
116
|
+
return ClientError.ALREADY_EXISTS.asException().left()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Validation
|
|
120
|
+
if (request.name.isBlank()) {
|
|
121
|
+
return ClientError.INVALID_INPUT.asException("Name is required").left()
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Factory Bean
|
|
126
|
+
|
|
127
|
+
```kotlin
|
|
128
|
+
@Factory
|
|
129
|
+
class EmployeeManagerFactory {
|
|
130
|
+
@Singleton
|
|
131
|
+
fun provideEmployeeManager(
|
|
132
|
+
employeeRepository: EmployeeRepository,
|
|
133
|
+
db: DatabaseContext
|
|
134
|
+
): EmployeeManager {
|
|
135
|
+
return DefaultEmployeeManager(employeeRepository, db)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|