182 lines
5.2 KiB
Markdown
182 lines
5.2 KiB
Markdown
# Repository Rules (REPO-01..07)
|
|
|
|
## REPO-01: Interface Defined in Domain (CRITICAL)
|
|
|
|
Repository interfaces MUST be defined in the domain package, next to the entity they persist. This follows the Dependency Inversion Principle — the domain defines what it needs, adapters implement it.
|
|
|
|
**Correct:**
|
|
```go
|
|
// domain/hour/repository.go
|
|
package hour
|
|
|
|
type Repository interface {
|
|
GetHour(ctx context.Context, hourTime time.Time) (*Hour, error)
|
|
UpdateHour(ctx context.Context, hourTime time.Time,
|
|
updateFn func(h *Hour) (*Hour, error)) error
|
|
}
|
|
```
|
|
|
|
**Wrong:**
|
|
```go
|
|
// adapters/repository.go ← VIOLATION: interface in adapter layer
|
|
package adapters
|
|
|
|
type HourRepository interface { ... }
|
|
```
|
|
|
|
**Check:** Grep `domain/` for `type.*Repository interface`. Grep `adapters/` for the same — if found in adapters, it's a violation.
|
|
|
|
---
|
|
|
|
## REPO-02: Update Callback Pattern (WARNING)
|
|
|
|
Repository update methods SHOULD use a callback/closure pattern. The repository handles transaction lifecycle; the callback handles domain logic.
|
|
|
|
```go
|
|
// Interface
|
|
UpdateHour(ctx context.Context, hourTime time.Time,
|
|
updateFn func(h *Hour) (*Hour, error)) error
|
|
|
|
// Usage in handler
|
|
err := h.hourRepo.UpdateHour(ctx, cmd.Hour, func(h *hour.Hour) (*hour.Hour, error) {
|
|
if err := h.CancelTraining(); err != nil {
|
|
return nil, err
|
|
}
|
|
return h, nil
|
|
})
|
|
```
|
|
|
|
Benefits:
|
|
- Transaction scope is clear
|
|
- Domain logic is isolated from persistence details
|
|
- Enables optimistic locking, retries, etc. transparently
|
|
|
|
---
|
|
|
|
## REPO-03: Separate DB Model Structs (WARNING)
|
|
|
|
Adapter implementations MUST use separate structs for database representation. Domain entities should NOT have serialization tags.
|
|
|
|
**Correct:**
|
|
```go
|
|
// adapters/ — DB model
|
|
type mysqlHour struct {
|
|
ID int `db:"id"`
|
|
Hour time.Time `db:"hour"`
|
|
Availability string `db:"availability"`
|
|
}
|
|
|
|
// or for Firestore (needs exported fields for tags)
|
|
type TrainingModel struct {
|
|
UUID string `firestore:"Uuid"`
|
|
UserUUID string `firestore:"UserUuid"`
|
|
Time time.Time `firestore:"Time"`
|
|
}
|
|
|
|
// Conversion in adapter
|
|
func (r *MySQLHourRepository) toHour(m mysqlHour) (*hour.Hour, error) {
|
|
availability, err := hour.NewAvailabilityFromString(m.Availability)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return hour.UnmarshalHourFromDatabase(m.Hour, availability), nil
|
|
}
|
|
```
|
|
|
|
**Wrong:**
|
|
```go
|
|
// domain/hour/hour.go
|
|
type Hour struct {
|
|
Hour time.Time `json:"hour" db:"hour"` // VIOLATION: DB tags on domain entity
|
|
Availability string `json:"availability"` // VIOLATION: serialization concern in domain
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## REPO-04: Adapter Constructor Naming (INFO)
|
|
|
|
Repository adapter constructors follow: `New{Technology}{Entity}Repository`
|
|
|
|
```go
|
|
func NewFirestoreHourRepository(client *firestore.Client, factory hour.Factory) *FirestoreHourRepository
|
|
func NewMySQLHourRepository(db *sqlx.DB) *MySQLHourRepository
|
|
func NewMemoryHourRepository(factory hour.Factory) *MemoryHourRepository
|
|
```
|
|
|
|
---
|
|
|
|
## REPO-05: Technology Suffix Naming (INFO)
|
|
|
|
Adapter types use technology as a suffix/prefix to distinguish implementations.
|
|
|
|
```go
|
|
type FirestoreHourRepository struct { ... }
|
|
type MySQLHourRepository struct { ... }
|
|
type MemoryHourRepository struct { ... }
|
|
|
|
// For external service clients
|
|
type TrainerGrpc struct { ... }
|
|
type UsersGrpc struct { ... }
|
|
```
|
|
|
|
---
|
|
|
|
## REPO-06: Shared Test Suite (WARNING)
|
|
|
|
Repository tests SHOULD run the same test logic against ALL implementations (memory, MySQL, Firestore, etc.). This ensures behavioral consistency.
|
|
|
|
**Pattern:**
|
|
```go
|
|
func createRepositories(t *testing.T) []Repository {
|
|
return []Repository{
|
|
{Name: "Firebase", Repository: newFirebaseRepository(t)},
|
|
{Name: "MySQL", Repository: newMySQLRepository(t)},
|
|
{Name: "memory", Repository: adapters.NewMemoryHourRepository(testFactory)},
|
|
}
|
|
}
|
|
|
|
func TestRepository(t *testing.T) {
|
|
repositories := createRepositories(t)
|
|
for i := range repositories {
|
|
r := repositories[i] // capture loop variable
|
|
t.Run(r.Name, func(t *testing.T) {
|
|
t.Parallel()
|
|
testUpdateHour(t, r.Repository)
|
|
testUpdateHour_parallel(t, r.Repository)
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
**Check:** Look for test files in `adapters/` that test repository implementations. Verify they use a shared test function or table-driven approach.
|
|
|
|
---
|
|
|
|
## REPO-07: UnmarshalFromDatabase Usage (WARNING)
|
|
|
|
Adapter implementations MUST use the entity's `UnmarshalFromDatabase` function to reconstruct domain objects from persistence, not the regular constructor.
|
|
|
|
**Correct:**
|
|
```go
|
|
func (r *FirestoreHourRepository) toHour(doc *firestore.DocumentSnapshot) (*hour.Hour, error) {
|
|
var m HourModel
|
|
if err := doc.DataTo(&m); err != nil {
|
|
return nil, err
|
|
}
|
|
availability, err := hour.NewAvailabilityFromString(m.Availability)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return hour.UnmarshalHourFromDatabase(m.Hour, availability), nil
|
|
}
|
|
```
|
|
|
|
**Wrong:**
|
|
```go
|
|
func (r *FirestoreHourRepository) toHour(doc *firestore.DocumentSnapshot) (*hour.Hour, error) {
|
|
// VIOLATION: using business constructor for DB reconstruction
|
|
return hour.NewAvailableHour(m.Hour) // This re-validates and may reject valid stored data
|
|
}
|
|
```
|