@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,552 @@
|
|
|
1
|
+
# Database Table Creator - Code Examples
|
|
2
|
+
|
|
3
|
+
This file contains actual code patterns extracted from the codebase. **Follow these patterns exactly**.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Kotlin Table Object Pattern
|
|
8
|
+
|
|
9
|
+
**File**: `app/module-repository/src/main/kotlin/com/yourcompany/postgresql/table/UserTable.kt`
|
|
10
|
+
|
|
11
|
+
```kotlin
|
|
12
|
+
package com.yourcompany.postgresql.table
|
|
13
|
+
|
|
14
|
+
import org.jetbrains.exposed.dao.id.UUIDTable
|
|
15
|
+
import org.jetbrains.exposed.sql.javatime.timestamp
|
|
16
|
+
import java.time.Instant
|
|
17
|
+
|
|
18
|
+
object UserTable : UUIDTable("users") { // ✓ MUST extend UUIDTable
|
|
19
|
+
// Business columns
|
|
20
|
+
val email = text("email").nullable() // ✓ Use text() not varchar()
|
|
21
|
+
val emailVerified = bool("email_verified").default(false)
|
|
22
|
+
val displayName = text("display_name").nullable()
|
|
23
|
+
val avatarUrl = text("avatar_url").nullable()
|
|
24
|
+
val authId = text("auth_id").nullable()
|
|
25
|
+
val provider = text("provider").nullable()
|
|
26
|
+
val status = text("status").default("active")
|
|
27
|
+
val referralSource = text("referral_source").default("OTHER")
|
|
28
|
+
val password = text("password").nullable()
|
|
29
|
+
val userRoleType = text("user_role_type").nullable()
|
|
30
|
+
val userType = text("user_type").nullable()
|
|
31
|
+
val userStatusType = text("user_status_type").nullable()
|
|
32
|
+
val companyId = uuid("company_id").nullable()
|
|
33
|
+
|
|
34
|
+
// Required timestamp columns
|
|
35
|
+
val createdAt = timestamp("created_at").clientDefault { Instant.now() }
|
|
36
|
+
val updatedAt = timestamp("updated_at").nullable()
|
|
37
|
+
val deletedAt = timestamp("deleted_at").nullable()
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Key points**:
|
|
42
|
+
- Extends `UUIDTable` (provides UUID primary key)
|
|
43
|
+
- Uses `text()` for all string columns (matches SQL TEXT type)
|
|
44
|
+
- Uses `.nullable()` for optional fields
|
|
45
|
+
- Standard timestamps: createdAt (non-null), updatedAt (nullable), deletedAt (nullable)
|
|
46
|
+
- Column names match SQL exactly (snake_case)
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Entity Data Class Pattern
|
|
51
|
+
|
|
52
|
+
**File**: `app/module-repository/src/main/kotlin/com/yourcompany/postgresql/entity/UserEntity.kt`
|
|
53
|
+
|
|
54
|
+
```kotlin
|
|
55
|
+
package com.yourcompany.postgresql.entity
|
|
56
|
+
|
|
57
|
+
import com.yourcompany.postgresql.constant.ReferralSource
|
|
58
|
+
import com.yourcompany.postgresql.constant.UserRoleType
|
|
59
|
+
import com.yourcompany.postgresql.constant.UserStatus
|
|
60
|
+
import com.yourcompany.postgresql.constant.UserStatusType
|
|
61
|
+
import com.yourcompany.postgresql.constant.UserType
|
|
62
|
+
import java.time.Instant
|
|
63
|
+
import java.util.UUID
|
|
64
|
+
|
|
65
|
+
data class UserEntity( // ✓ Use data class
|
|
66
|
+
override val id: UUID = UUID.randomUUID(), // ✓ Override from Entity
|
|
67
|
+
val email: String? = null,
|
|
68
|
+
val emailVerified: Boolean = false,
|
|
69
|
+
val displayName: String? = null,
|
|
70
|
+
val avatarUrl: String? = null,
|
|
71
|
+
val authId: String? = null,
|
|
72
|
+
val provider: String? = null,
|
|
73
|
+
val status: UserStatus? = null, // ✓ Use enum types when appropriate
|
|
74
|
+
val referralSource: ReferralSource? = null,
|
|
75
|
+
val password: String? = null,
|
|
76
|
+
val role: UserRoleType? = null,
|
|
77
|
+
val type: UserType? = null,
|
|
78
|
+
val businessStatus: UserStatusType? = null,
|
|
79
|
+
val companyId: UUID? = null,
|
|
80
|
+
override val createdAt: Instant = Instant.now(), // ✓ Override from Entity
|
|
81
|
+
override val updatedAt: Instant? = null, // ✓ Override from Entity
|
|
82
|
+
override val deletedAt: Instant? = null // ✓ Override from Entity
|
|
83
|
+
) : Entity<Instant> // ✓ MUST implement Entity<Instant>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Key points**:
|
|
87
|
+
- Must be a `data class`
|
|
88
|
+
- Must implement `Entity<Instant>`
|
|
89
|
+
- Must override: id, createdAt, updatedAt, deletedAt
|
|
90
|
+
- Use `String` for TEXT columns
|
|
91
|
+
- Use `UUID` for UUID columns
|
|
92
|
+
- Use `Instant` for TIMESTAMP columns
|
|
93
|
+
- Use enums for status/type columns
|
|
94
|
+
- Nullable fields have `? = null` defaults
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Repository Interface Pattern
|
|
99
|
+
|
|
100
|
+
```kotlin
|
|
101
|
+
package com.yourcompany.postgresql.repository
|
|
102
|
+
|
|
103
|
+
import com.yourcompany.postgresql.entity.UserEntity
|
|
104
|
+
import java.util.UUID
|
|
105
|
+
|
|
106
|
+
interface UserRepository {
|
|
107
|
+
// Standard CRUD operations
|
|
108
|
+
fun insert(entity: UserEntity): UserEntity
|
|
109
|
+
fun update(
|
|
110
|
+
id: UUID,
|
|
111
|
+
email: String? = null,
|
|
112
|
+
displayName: String? = null,
|
|
113
|
+
// ... other fields
|
|
114
|
+
): UserEntity?
|
|
115
|
+
|
|
116
|
+
fun byId(id: UUID): UserEntity?
|
|
117
|
+
fun byIds(ids: List<UUID>): List<UserEntity>
|
|
118
|
+
|
|
119
|
+
// Custom query methods
|
|
120
|
+
fun byEmail(email: String): UserEntity?
|
|
121
|
+
fun byAuthId(authId: String): UserEntity?
|
|
122
|
+
|
|
123
|
+
// Soft delete operations
|
|
124
|
+
fun deleteById(id: UUID): UserEntity?
|
|
125
|
+
fun restoreById(id: UUID): UserEntity?
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Key points**:
|
|
130
|
+
- Use domain-specific method names (`byEmail`, not `findByEmail`)
|
|
131
|
+
- Return entity or null (don't throw exceptions)
|
|
132
|
+
- Update accepts optional fields
|
|
133
|
+
- Include soft delete and restore methods
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Repository Implementation Pattern
|
|
138
|
+
|
|
139
|
+
```kotlin
|
|
140
|
+
package com.yourcompany.postgresql.repository
|
|
141
|
+
|
|
142
|
+
import com.yourcompany.database.DatabaseContext
|
|
143
|
+
import com.yourcompany.postgresql.entity.UserEntity
|
|
144
|
+
import com.yourcompany.postgresql.table.UserTable
|
|
145
|
+
import org.jetbrains.exposed.sql.*
|
|
146
|
+
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
|
147
|
+
import org.jetbrains.exposed.sql.transactions.transaction
|
|
148
|
+
import java.time.Instant
|
|
149
|
+
import java.util.UUID
|
|
150
|
+
|
|
151
|
+
class DefaultUserRepository(
|
|
152
|
+
private val db: DatabaseContext // ✓ Constructor injection
|
|
153
|
+
) : UserRepository {
|
|
154
|
+
|
|
155
|
+
override fun insert(entity: UserEntity): UserEntity {
|
|
156
|
+
return transaction(db.primary) { // ✓ Use db.primary for writes
|
|
157
|
+
UserTable.insert {
|
|
158
|
+
it[id] = entity.id
|
|
159
|
+
it[email] = entity.email
|
|
160
|
+
it[emailVerified] = entity.emailVerified
|
|
161
|
+
it[displayName] = entity.displayName
|
|
162
|
+
// ... map all fields
|
|
163
|
+
it[createdAt] = entity.createdAt
|
|
164
|
+
it[updatedAt] = entity.updatedAt
|
|
165
|
+
it[deletedAt] = entity.deletedAt
|
|
166
|
+
}
|
|
167
|
+
entity
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
override fun update(
|
|
172
|
+
id: UUID,
|
|
173
|
+
email: String?,
|
|
174
|
+
displayName: String?
|
|
175
|
+
): UserEntity? {
|
|
176
|
+
return transaction(db.primary) {
|
|
177
|
+
val updated = UserTable.update(
|
|
178
|
+
where = {
|
|
179
|
+
UserTable.id eq id and // ✓ Filter by id
|
|
180
|
+
UserTable.deletedAt.isNull() // ✓ CRITICAL: Filter soft deletes
|
|
181
|
+
}
|
|
182
|
+
) {
|
|
183
|
+
email?.let { value -> it[UserTable.email] = value }
|
|
184
|
+
displayName?.let { value -> it[UserTable.displayName] = value }
|
|
185
|
+
// updatedAt automatically set by database trigger
|
|
186
|
+
}
|
|
187
|
+
if (updated > 0) byId(id) else null // ✓ Return updated entity or null
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
override fun byId(id: UUID): UserEntity? {
|
|
192
|
+
return transaction(db.replica) { // ✓ Use db.replica for reads
|
|
193
|
+
UserTable
|
|
194
|
+
.selectAll()
|
|
195
|
+
.where { UserTable.id eq id }
|
|
196
|
+
.andWhere { UserTable.deletedAt.isNull() } // ✓ CRITICAL: Filter soft deletes
|
|
197
|
+
.map { convert(it) }
|
|
198
|
+
.singleOrNull() // ✓ Return null if not found
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
override fun byEmail(email: String): UserEntity? {
|
|
203
|
+
return transaction(db.replica) {
|
|
204
|
+
UserTable
|
|
205
|
+
.selectAll()
|
|
206
|
+
.where { UserTable.email eq email }
|
|
207
|
+
.andWhere { UserTable.deletedAt.isNull() } // ✓ ALWAYS filter soft deletes
|
|
208
|
+
.map { convert(it) }
|
|
209
|
+
.singleOrNull()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
override fun deleteById(id: UUID): UserEntity? {
|
|
214
|
+
return transaction(db.primary) {
|
|
215
|
+
val updated = UserTable.update(
|
|
216
|
+
where = {
|
|
217
|
+
UserTable.id eq id and
|
|
218
|
+
UserTable.deletedAt.isNull() // ✓ Only delete active records
|
|
219
|
+
}
|
|
220
|
+
) {
|
|
221
|
+
it[deletedAt] = Instant.now() // ✓ Soft delete: set timestamp
|
|
222
|
+
}
|
|
223
|
+
if (updated > 0) {
|
|
224
|
+
// Return the soft-deleted entity (need to query without deletedAt filter)
|
|
225
|
+
UserTable
|
|
226
|
+
.selectAll()
|
|
227
|
+
.where { UserTable.id eq id }
|
|
228
|
+
.map { convert(it) }
|
|
229
|
+
.singleOrNull()
|
|
230
|
+
} else null
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
override fun restoreById(id: UUID): UserEntity? {
|
|
235
|
+
return transaction(db.primary) {
|
|
236
|
+
val updated = UserTable.update(
|
|
237
|
+
where = {
|
|
238
|
+
UserTable.id eq id and
|
|
239
|
+
UserTable.deletedAt.isNotNull() // ✓ Only restore deleted records
|
|
240
|
+
}
|
|
241
|
+
) {
|
|
242
|
+
it[deletedAt] = null // ✓ Restore: clear timestamp
|
|
243
|
+
}
|
|
244
|
+
if (updated > 0) byId(id) else null
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ✓ Private helper for mapping
|
|
249
|
+
private fun convert(row: ResultRow): UserEntity {
|
|
250
|
+
return UserEntity(
|
|
251
|
+
id = row[UserTable.id].value, // ✓ UUID field needs .value
|
|
252
|
+
email = row[UserTable.email],
|
|
253
|
+
emailVerified = row[UserTable.emailVerified],
|
|
254
|
+
displayName = row[UserTable.displayName],
|
|
255
|
+
avatarUrl = row[UserTable.avatarUrl],
|
|
256
|
+
authId = row[UserTable.authId],
|
|
257
|
+
provider = row[UserTable.provider],
|
|
258
|
+
// Map enums properly
|
|
259
|
+
status = row[UserTable.status]?.let { UserStatus.valueOf(it) },
|
|
260
|
+
referralSource = row[UserTable.referralSource]?.let { ReferralSource.valueOf(it) },
|
|
261
|
+
password = row[UserTable.password],
|
|
262
|
+
role = row[UserTable.userRoleType]?.let { UserRoleType.valueOf(it) },
|
|
263
|
+
type = row[UserTable.userType]?.let { UserType.valueOf(it) },
|
|
264
|
+
businessStatus = row[UserTable.userStatusType]?.let { UserStatusType.valueOf(it) },
|
|
265
|
+
companyId = row[UserTable.companyId],
|
|
266
|
+
createdAt = row[UserTable.createdAt],
|
|
267
|
+
updatedAt = row[UserTable.updatedAt],
|
|
268
|
+
deletedAt = row[UserTable.deletedAt]
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Critical patterns**:
|
|
275
|
+
- Use `transaction(db.primary)` for all writes (insert, update, delete)
|
|
276
|
+
- Use `transaction(db.replica)` for all reads
|
|
277
|
+
- **ALWAYS** include `.andWhere { deletedAt.isNull() }` in queries
|
|
278
|
+
- Soft delete sets `deletedAt = Instant.now()`
|
|
279
|
+
- Return null when record not found (don't throw)
|
|
280
|
+
- Private `convert()` method for ResultRow → Entity mapping
|
|
281
|
+
- NO `!!` operators anywhere
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Repository Factory Bean Pattern
|
|
286
|
+
|
|
287
|
+
**File**: `app/module-repository/src/main/kotlin/com/yourcompany/runtime/factory/RepositoryFactory.kt`
|
|
288
|
+
|
|
289
|
+
```kotlin
|
|
290
|
+
package com.yourcompany.runtime.factory
|
|
291
|
+
|
|
292
|
+
import com.yourcompany.database.DatabaseContext
|
|
293
|
+
import com.yourcompany.postgresql.repository.*
|
|
294
|
+
import io.micronaut.context.annotation.Factory
|
|
295
|
+
import jakarta.inject.Singleton
|
|
296
|
+
|
|
297
|
+
@Factory
|
|
298
|
+
class RepositoryFactory {
|
|
299
|
+
|
|
300
|
+
@Singleton
|
|
301
|
+
fun provideUserRepository(db: DatabaseContext): UserRepository {
|
|
302
|
+
return DefaultUserRepository(db)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
@Singleton
|
|
306
|
+
fun provideContactRepository(db: DatabaseContext): ContactRepository {
|
|
307
|
+
return DefaultContactRepository(db)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Add new repository beans here
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Key points**:
|
|
315
|
+
- Use `@Factory` class annotation
|
|
316
|
+
- Use `@Singleton` method annotation
|
|
317
|
+
- Return interface type, not implementation
|
|
318
|
+
- Accept `DatabaseContext` parameter
|
|
319
|
+
- Method name: `provide{TableName}Repository`
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Repository Test Pattern
|
|
324
|
+
|
|
325
|
+
```kotlin
|
|
326
|
+
package com.yourcompany.postgresql.repository
|
|
327
|
+
|
|
328
|
+
import assertk.assertThat
|
|
329
|
+
import assertk.assertions.*
|
|
330
|
+
import com.yourcompany.postgresql.entity.UserEntity
|
|
331
|
+
import com.yourcompany.postgresql.constant.UserStatus
|
|
332
|
+
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
|
|
333
|
+
import jakarta.inject.Inject
|
|
334
|
+
import org.junit.jupiter.api.BeforeEach
|
|
335
|
+
import org.junit.jupiter.api.Test
|
|
336
|
+
import java.util.UUID
|
|
337
|
+
|
|
338
|
+
@MicronautTest(environments = ["test"]) // ✓ Test environment
|
|
339
|
+
class DefaultUserRepositoryTest : AbstractRepositoryTest() { // ✓ Extend base class
|
|
340
|
+
|
|
341
|
+
@Inject
|
|
342
|
+
lateinit var repository: UserRepository // ✓ Inject interface
|
|
343
|
+
|
|
344
|
+
@BeforeEach
|
|
345
|
+
fun setup() {
|
|
346
|
+
database.primary.truncateAllTables() // ✓ Clean state
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
@Test
|
|
350
|
+
fun `insert - creates user successfully`() {
|
|
351
|
+
val entity = dummyUserEntity()
|
|
352
|
+
|
|
353
|
+
val result = repository.insert(entity)
|
|
354
|
+
|
|
355
|
+
assertThat(result.id).isEqualTo(entity.id)
|
|
356
|
+
assertThat(result.email).isEqualTo(entity.email)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
@Test
|
|
360
|
+
fun `update - updates selected fields only`() {
|
|
361
|
+
val entity = repository.insert(dummyUserEntity())
|
|
362
|
+
val newEmail = "newemail@example.com"
|
|
363
|
+
|
|
364
|
+
val updated = repository.update(
|
|
365
|
+
id = entity.id,
|
|
366
|
+
email = newEmail
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
assertThat(updated).isNotNull()
|
|
370
|
+
assertThat(updated?.email).isEqualTo(newEmail)
|
|
371
|
+
assertThat(updated?.displayName).isEqualTo(entity.displayName) // Unchanged
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
@Test
|
|
375
|
+
fun `byId - returns entity when exists`() {
|
|
376
|
+
val entity = repository.insert(dummyUserEntity())
|
|
377
|
+
|
|
378
|
+
val result = repository.byId(entity.id)
|
|
379
|
+
|
|
380
|
+
assertThat(result).isNotNull()
|
|
381
|
+
assertThat(result?.id).isEqualTo(entity.id)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@Test
|
|
385
|
+
fun `byId - returns null when not exists`() {
|
|
386
|
+
val randomId = UUID.randomUUID()
|
|
387
|
+
|
|
388
|
+
val result = repository.byId(randomId)
|
|
389
|
+
|
|
390
|
+
assertThat(result).isNull()
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
@Test
|
|
394
|
+
fun `byId - returns null when soft deleted`() {
|
|
395
|
+
val entity = repository.insert(dummyUserEntity())
|
|
396
|
+
repository.deleteById(entity.id) // Soft delete
|
|
397
|
+
|
|
398
|
+
val result = repository.byId(entity.id)
|
|
399
|
+
|
|
400
|
+
assertThat(result).isNull() // ✓ Soft deleted records not returned
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
@Test
|
|
404
|
+
fun `deleteById - soft deletes entity`() {
|
|
405
|
+
val entity = repository.insert(dummyUserEntity())
|
|
406
|
+
|
|
407
|
+
val deleted = repository.deleteById(entity.id)
|
|
408
|
+
|
|
409
|
+
assertThat(deleted).isNotNull()
|
|
410
|
+
assertThat(deleted?.deletedAt).isNotNull() // ✓ Has timestamp
|
|
411
|
+
assertThat(repository.byId(entity.id)).isNull() // ✓ Not found in queries
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
@Test
|
|
415
|
+
fun `restoreById - restores soft deleted entity`() {
|
|
416
|
+
val entity = repository.insert(dummyUserEntity())
|
|
417
|
+
repository.deleteById(entity.id)
|
|
418
|
+
|
|
419
|
+
val restored = repository.restoreById(entity.id)
|
|
420
|
+
|
|
421
|
+
assertThat(restored).isNotNull()
|
|
422
|
+
assertThat(restored?.deletedAt).isNull() // ✓ Timestamp cleared
|
|
423
|
+
assertThat(repository.byId(entity.id)).isNotNull() // ✓ Found in queries
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ✓ Test helper with defaults
|
|
427
|
+
private fun dummyUserEntity(
|
|
428
|
+
id: UUID = UUID.randomUUID(),
|
|
429
|
+
email: String = "${UUID.randomUUID()}@test.com"
|
|
430
|
+
) = UserEntity(
|
|
431
|
+
id = id,
|
|
432
|
+
email = email,
|
|
433
|
+
emailVerified = false,
|
|
434
|
+
displayName = "Test User",
|
|
435
|
+
status = UserStatus.ACTIVE
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Required tests** (minimum):
|
|
441
|
+
1. Insert happy path
|
|
442
|
+
2. Update modifies fields correctly
|
|
443
|
+
3. byId returns entity when exists
|
|
444
|
+
4. byId returns null when not exists
|
|
445
|
+
5. byId returns null when soft deleted ← **Critical for soft delete verification**
|
|
446
|
+
6. deleteById soft deletes entity
|
|
447
|
+
7. restoreById restores soft deleted entity
|
|
448
|
+
|
|
449
|
+
**Test patterns**:
|
|
450
|
+
- Extend `AbstractRepositoryTest`
|
|
451
|
+
- Use `@MicronautTest(environments = ["test"])`
|
|
452
|
+
- Clean database in `@BeforeEach`
|
|
453
|
+
- Create dummy entity helpers with random data
|
|
454
|
+
- Use AssertJ assertions (`assertk.assertThat`)
|
|
455
|
+
- Test soft delete behavior thoroughly
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Anti-Patterns to NEVER Use
|
|
460
|
+
|
|
461
|
+
❌ **Wrong: Using `Table` instead of `UUIDTable`**
|
|
462
|
+
```kotlin
|
|
463
|
+
object UserTable : Table("users") // WRONG - missing UUID primary key
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
✓ **Correct: Use `UUIDTable`**
|
|
467
|
+
```kotlin
|
|
468
|
+
object UserTable : UUIDTable("users") // CORRECT
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
❌ **Wrong: Forgetting soft delete filter**
|
|
474
|
+
```kotlin
|
|
475
|
+
fun byId(id: UUID): UserEntity? {
|
|
476
|
+
return transaction(db.replica) {
|
|
477
|
+
UserTable
|
|
478
|
+
.selectAll()
|
|
479
|
+
.where { UserTable.id eq id } // WRONG - includes soft deleted
|
|
480
|
+
.map { convert(it) }
|
|
481
|
+
.singleOrNull()
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
✓ **Correct: Always filter soft deletes**
|
|
487
|
+
```kotlin
|
|
488
|
+
fun byId(id: UUID): UserEntity? {
|
|
489
|
+
return transaction(db.replica) {
|
|
490
|
+
UserTable
|
|
491
|
+
.selectAll()
|
|
492
|
+
.where { UserTable.id eq id }
|
|
493
|
+
.andWhere { UserTable.deletedAt.isNull() } // CORRECT
|
|
494
|
+
.map { convert(it) }
|
|
495
|
+
.singleOrNull()
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
❌ **Wrong: Using `!!` operator**
|
|
503
|
+
```kotlin
|
|
504
|
+
val user = repository.byId(id)!! // FORBIDDEN - will fail pre-commit hook
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
✓ **Correct: Use safe calls**
|
|
508
|
+
```kotlin
|
|
509
|
+
val user = repository.byId(id)
|
|
510
|
+
?: return ClientError.USER_NOT_FOUND.asException().left()
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
❌ **Wrong: Hard delete**
|
|
516
|
+
```kotlin
|
|
517
|
+
fun deleteById(id: UUID): Boolean {
|
|
518
|
+
return transaction(db.primary) {
|
|
519
|
+
UserTable.deleteWhere { id eq userId } > 0 // WRONG - permanent deletion
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
✓ **Correct: Soft delete**
|
|
525
|
+
```kotlin
|
|
526
|
+
fun deleteById(id: UUID): UserEntity? {
|
|
527
|
+
return transaction(db.primary) {
|
|
528
|
+
UserTable.update({ id eq userId and deletedAt.isNull() }) {
|
|
529
|
+
it[deletedAt] = Instant.now() // CORRECT - soft delete
|
|
530
|
+
}
|
|
531
|
+
if (updated > 0) byId(id) else null
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## Summary
|
|
539
|
+
|
|
540
|
+
**Always follow these patterns exactly**:
|
|
541
|
+
1. Table extends `UUIDTable`
|
|
542
|
+
2. Entity implements `Entity<Instant>`
|
|
543
|
+
3. Use `text()` not `varchar()`
|
|
544
|
+
4. Filter `deletedAt.isNull()` in ALL queries
|
|
545
|
+
5. Use `transaction(db.primary)` for writes
|
|
546
|
+
6. Use `transaction(db.replica)` for reads
|
|
547
|
+
7. Never use `!!` operator
|
|
548
|
+
8. Soft delete only (set deletedAt timestamp)
|
|
549
|
+
9. Return null when not found
|
|
550
|
+
10. Test soft delete behavior thoroughly
|
|
551
|
+
|
|
552
|
+
**These patterns are non-negotiable and must be followed exactly.**
|