🔥
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# Command Handler Scaffold Template
|
||||
|
||||
Generate a single command handler file following the 4-component pattern.
|
||||
|
||||
## Placeholders
|
||||
|
||||
- `{{Name}}` — PascalCase command name (e.g., `ScheduleTraining`)
|
||||
- `{{name}}` — camelCase (e.g., `scheduleTraining`)
|
||||
- `{{module}}` — Go module path from go.mod
|
||||
- `{{entity}}` — Domain entity name, lowercase (e.g., `hour`)
|
||||
- `{{Entity}}` — Domain entity name, PascalCase (e.g., `Hour`)
|
||||
|
||||
## File: `app/command/{{name_snake}}.go`
|
||||
|
||||
```go
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"{{module}}/domain/{{entity}}"
|
||||
"{{module_common}}/decorator"
|
||||
)
|
||||
|
||||
// 1. Command struct — imperative verb + noun, plain data
|
||||
type {{Name}} struct {
|
||||
// TODO: Add command fields
|
||||
// Example:
|
||||
// UUID string
|
||||
// Hour time.Time
|
||||
}
|
||||
|
||||
// 2. Exported handler type alias
|
||||
type {{Name}}Handler decorator.CommandHandler[{{Name}}]
|
||||
|
||||
// 3. Unexported concrete handler struct
|
||||
type {{name}}Handler struct {
|
||||
{{entity}}Repo {{entity}}.Repository
|
||||
}
|
||||
|
||||
// 4. Constructor with nil-checks + decorator wrapping
|
||||
func New{{Name}}Handler(
|
||||
{{entity}}Repo {{entity}}.Repository,
|
||||
logger *logrus.Entry,
|
||||
metricsClient decorator.MetricsClient,
|
||||
) {{Name}}Handler {
|
||||
if {{entity}}Repo == nil {
|
||||
panic("nil {{entity}}Repo")
|
||||
}
|
||||
if logger == nil {
|
||||
panic("nil logger")
|
||||
}
|
||||
if metricsClient == nil {
|
||||
panic("nil metricsClient")
|
||||
}
|
||||
|
||||
return decorator.ApplyCommandDecorators[{{Name}}](
|
||||
{{name}}Handler{{"{"}}{{entity}}Repo: {{entity}}Repo},
|
||||
logger,
|
||||
metricsClient,
|
||||
)
|
||||
}
|
||||
|
||||
// Handle — orchestrates domain logic, does NOT contain business rules
|
||||
func (h {{name}}Handler) Handle(ctx context.Context, cmd {{Name}}) error {
|
||||
// TODO: Implement command handling
|
||||
//
|
||||
// Typical patterns:
|
||||
//
|
||||
// Pattern A — Update via callback:
|
||||
// return h.{{entity}}Repo.Update{{Entity}}(ctx, cmd.UUID, func(e *{{entity}}.{{Entity}}) (*{{entity}}.{{Entity}}, error) {
|
||||
// if err := e.SomeDomainAction(); err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return e, nil
|
||||
// })
|
||||
//
|
||||
// Pattern B — Create new entity:
|
||||
// entity, err := {{entity}}.New{{Entity}}(cmd.UUID, ...)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return h.{{entity}}Repo.Save(ctx, entity)
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## Update `app/app.go`
|
||||
|
||||
After creating the handler, add it to the `Commands` struct:
|
||||
|
||||
```go
|
||||
type Commands struct {
|
||||
// ... existing handlers ...
|
||||
{{Name}} command.{{Name}}Handler
|
||||
}
|
||||
```
|
||||
|
||||
## Update `service/application.go`
|
||||
|
||||
Wire the handler in the composition root:
|
||||
|
||||
```go
|
||||
Commands: app.Commands{
|
||||
// ... existing handlers ...
|
||||
{{Name}}: command.New{{Name}}Handler(
|
||||
{{entity}}Repository,
|
||||
logger,
|
||||
metricsClient,
|
||||
),
|
||||
},
|
||||
```
|
||||
@@ -0,0 +1,156 @@
|
||||
# Domain Entity Scaffold Template
|
||||
|
||||
Generate a domain entity with factory constructor, value objects, and errors.
|
||||
|
||||
## Placeholders
|
||||
|
||||
- `{{Name}}` — PascalCase entity name (e.g., `Training`, `Hour`, `Order`)
|
||||
- `{{name}}` — camelCase (e.g., `training`)
|
||||
- `{{name_lower}}` — all lowercase package name (e.g., `training`)
|
||||
- `{{name_snake}}` — snake_case (e.g., `training`)
|
||||
|
||||
## File: `domain/{{name_lower}}/{{name_snake}}.go`
|
||||
|
||||
```go
|
||||
package {{name_lower}}
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// {{Name}} is the aggregate root for the {{name_lower}} domain.
|
||||
type {{Name}} struct {
|
||||
uuid string
|
||||
createdAt time.Time
|
||||
// TODO: Add domain fields (all private)
|
||||
// status Status // value object, not raw string
|
||||
}
|
||||
|
||||
// New{{Name}} creates a new {{Name}} with validated invariants.
|
||||
func New{{Name}}(uuid string) (*{{Name}}, error) {
|
||||
if uuid == "" {
|
||||
return nil, errors.New("empty {{name_lower}} uuid")
|
||||
}
|
||||
|
||||
return &{{Name}}{
|
||||
uuid: uuid,
|
||||
createdAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Unmarshal{{Name}}FromDatabase reconstructs a {{Name}} from persistence.
|
||||
// Bypasses validation — data was valid when stored.
|
||||
func Unmarshal{{Name}}FromDatabase(
|
||||
uuid string,
|
||||
createdAt time.Time,
|
||||
// TODO: Add all persisted fields
|
||||
) *{{Name}} {
|
||||
return &{{Name}}{
|
||||
uuid: uuid,
|
||||
createdAt: createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Accessor methods — expose state without allowing mutation.
|
||||
|
||||
func (t {{Name}}) UUID() string {
|
||||
return t.uuid
|
||||
}
|
||||
|
||||
func (t {{Name}}) CreatedAt() time.Time {
|
||||
return t.createdAt
|
||||
}
|
||||
|
||||
// TODO: Add behavior methods using domain language.
|
||||
// Examples:
|
||||
//
|
||||
// func (t *{{Name}}) Approve() error {
|
||||
// if t.status != Pending {
|
||||
// return ErrNotPending
|
||||
// }
|
||||
// t.status = Approved
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// func (t *{{Name}}) Cancel() error { ... }
|
||||
// func (t *{{Name}}) Submit(details string) error { ... }
|
||||
```
|
||||
|
||||
## File: `domain/{{name_lower}}/errors.go`
|
||||
|
||||
```go
|
||||
package {{name_lower}}
|
||||
|
||||
import "errors"
|
||||
|
||||
// Sentinel errors — simple, no context needed.
|
||||
var (
|
||||
ErrNotFound = errors.New("{{name_lower}} not found")
|
||||
// TODO: Add domain-specific errors
|
||||
// ErrAlreadyCanceled = errors.New("{{name_lower}} already canceled")
|
||||
// ErrNotPending = errors.New("{{name_lower}} is not in pending state")
|
||||
)
|
||||
|
||||
// Typed errors — carry context for logging/display.
|
||||
// Example:
|
||||
//
|
||||
// type ForbiddenError struct {
|
||||
// RequestingUserUUID string
|
||||
// OwnerUUID string
|
||||
// }
|
||||
//
|
||||
// func (e ForbiddenError) Error() string {
|
||||
// return fmt.Sprintf("user %s cannot access {{name_lower}} owned by %s",
|
||||
// e.RequestingUserUUID, e.OwnerUUID)
|
||||
// }
|
||||
```
|
||||
|
||||
## File: `domain/{{name_lower}}/status.go` (Optional Value Object)
|
||||
|
||||
```go
|
||||
package {{name_lower}}
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Status is a value object — cannot be constructed with arbitrary values.
|
||||
type Status struct {
|
||||
s string
|
||||
}
|
||||
|
||||
var (
|
||||
Pending = Status{"pending"}
|
||||
Approved = Status{"approved"}
|
||||
Canceled = Status{"canceled"}
|
||||
)
|
||||
|
||||
func NewStatusFromString(s string) (Status, error) {
|
||||
switch s {
|
||||
case "pending":
|
||||
return Pending, nil
|
||||
case "approved":
|
||||
return Approved, nil
|
||||
case "canceled":
|
||||
return Canceled, nil
|
||||
default:
|
||||
return Status{}, fmt.Errorf("unknown {{name_lower}} status: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func (s Status) String() string {
|
||||
return s.s
|
||||
}
|
||||
|
||||
func (s Status) IsZero() bool {
|
||||
return s == Status{}
|
||||
}
|
||||
```
|
||||
|
||||
## Post-Creation Checklist
|
||||
|
||||
- [ ] All struct fields are private (unexported)
|
||||
- [ ] Factory constructor validates all invariants
|
||||
- [ ] UnmarshalFromDatabase accepts all persisted fields
|
||||
- [ ] Value objects are struct wrappers, not type aliases
|
||||
- [ ] Behavior methods use domain language, not CRUD
|
||||
- [ ] Errors are sentinel vars or typed structs
|
||||
@@ -0,0 +1,124 @@
|
||||
# Query Handler Scaffold Template
|
||||
|
||||
Generate a query handler file with a read model interface.
|
||||
|
||||
## Placeholders
|
||||
|
||||
- `{{Name}}` — PascalCase query name (e.g., `AvailableHours`)
|
||||
- `{{name}}` — camelCase (e.g., `availableHours`)
|
||||
- `{{name_snake}}` — snake_case (e.g., `available_hours`)
|
||||
- `{{module}}` — Go module path from go.mod
|
||||
- `{{Result}}` — Result type (e.g., `[]Date`, `*HourDetails`)
|
||||
|
||||
## File: `app/query/{{name_snake}}.go`
|
||||
|
||||
```go
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"{{module_common}}/decorator"
|
||||
)
|
||||
|
||||
// Read model — defines what data the query needs
|
||||
// Implemented by adapters (repository or dedicated read store)
|
||||
type {{Name}}ReadModel interface {
|
||||
{{Name}}(ctx context.Context /* TODO: add query params */) ({{Result}}, error)
|
||||
}
|
||||
|
||||
// 1. Query struct — noun phrase, plain data
|
||||
type {{Name}} struct {
|
||||
// TODO: Add query parameters
|
||||
// Example:
|
||||
// From time.Time
|
||||
// To time.Time
|
||||
}
|
||||
|
||||
// Result types — optimized for reading, may differ from domain entities
|
||||
// type Date struct {
|
||||
// Date time.Time
|
||||
// Hours []Hour
|
||||
// }
|
||||
|
||||
// 2. Exported handler type alias
|
||||
type {{Name}}Handler decorator.QueryHandler[{{Name}}, {{Result}}]
|
||||
|
||||
// 3. Unexported concrete handler struct
|
||||
type {{name}}Handler struct {
|
||||
readModel {{Name}}ReadModel
|
||||
}
|
||||
|
||||
// 4. Constructor with nil-checks + decorator wrapping
|
||||
func New{{Name}}Handler(
|
||||
readModel {{Name}}ReadModel,
|
||||
logger *logrus.Entry,
|
||||
metricsClient decorator.MetricsClient,
|
||||
) {{Name}}Handler {
|
||||
if readModel == nil {
|
||||
panic("nil readModel")
|
||||
}
|
||||
if logger == nil {
|
||||
panic("nil logger")
|
||||
}
|
||||
if metricsClient == nil {
|
||||
panic("nil metricsClient")
|
||||
}
|
||||
|
||||
return decorator.ApplyQueryDecorators[{{Name}}, {{Result}}](
|
||||
{{name}}Handler{readModel: readModel},
|
||||
logger,
|
||||
metricsClient,
|
||||
)
|
||||
}
|
||||
|
||||
// Handle — delegates to read model, may add input validation
|
||||
func (h {{name}}Handler) Handle(ctx context.Context, q {{Name}}) ({{Result}}, error) {
|
||||
// TODO: Add input validation if needed
|
||||
// Example:
|
||||
// if q.From.After(q.To) {
|
||||
// return nil, errors.NewIncorrectInputError("date-from-after-date-to", "date from is after date to")
|
||||
// }
|
||||
|
||||
return h.readModel.{{Name}}(ctx /* TODO: pass query params */)
|
||||
}
|
||||
```
|
||||
|
||||
## Update `app/app.go`
|
||||
|
||||
Add to the `Queries` struct:
|
||||
|
||||
```go
|
||||
type Queries struct {
|
||||
// ... existing handlers ...
|
||||
{{Name}} query.{{Name}}Handler
|
||||
}
|
||||
```
|
||||
|
||||
## Update `service/application.go`
|
||||
|
||||
Wire the handler. The read model is typically implemented by the same repository adapter or a dedicated read adapter:
|
||||
|
||||
```go
|
||||
Queries: app.Queries{
|
||||
// ... existing handlers ...
|
||||
{{Name}}: query.New{{Name}}Handler(
|
||||
{{entity}}Repository, // implements {{Name}}ReadModel
|
||||
logger,
|
||||
metricsClient,
|
||||
),
|
||||
},
|
||||
```
|
||||
|
||||
## Implement ReadModel on Adapter
|
||||
|
||||
Add the read model method to your repository adapter:
|
||||
|
||||
```go
|
||||
// In adapters/
|
||||
func (r *Memory{{Entity}}Repository) {{Name}}(ctx context.Context /* params */) ({{Result}}, error) {
|
||||
// TODO: Implement query against storage
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,212 @@
|
||||
# Repository Scaffold Template
|
||||
|
||||
Generate a repository interface in the domain package and a memory implementation in adapters.
|
||||
|
||||
## Placeholders
|
||||
|
||||
- `{{Name}}` — PascalCase entity name (e.g., `Training`)
|
||||
- `{{name}}` — camelCase (e.g., `training`)
|
||||
- `{{name_lower}}` — all lowercase package name (e.g., `training`)
|
||||
- `{{name_snake}}` — snake_case (e.g., `training`)
|
||||
- `{{module}}` — Go module path from go.mod
|
||||
|
||||
## File: `domain/{{name_lower}}/repository.go`
|
||||
|
||||
```go
|
||||
package {{name_lower}}
|
||||
|
||||
import "context"
|
||||
|
||||
// Repository defines persistence operations for {{Name}}.
|
||||
// Defined in domain — adapters implement it implicitly.
|
||||
type Repository interface {
|
||||
// Get{{Name}} retrieves a {{Name}} by its UUID.
|
||||
Get{{Name}}(ctx context.Context, uuid string) (*{{Name}}, error)
|
||||
|
||||
// Update{{Name}} loads a {{Name}}, applies the update function within a
|
||||
// transaction, and persists the result. The callback pattern ensures
|
||||
// domain logic is separated from transaction management.
|
||||
Update{{Name}}(ctx context.Context, uuid string,
|
||||
updateFn func(t *{{Name}}) (*{{Name}}, error)) error
|
||||
|
||||
// TODO: Add other methods as needed. Examples:
|
||||
// Save{{Name}}(ctx context.Context, t *{{Name}}) error
|
||||
// Delete{{Name}}(ctx context.Context, uuid string) error
|
||||
}
|
||||
```
|
||||
|
||||
## File: `adapters/memory_{{name_snake}}_repository.go`
|
||||
|
||||
```go
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"{{module}}/domain/{{name_lower}}"
|
||||
)
|
||||
|
||||
// Memory{{Name}}Repository is an in-memory implementation of {{name_lower}}.Repository.
|
||||
// Useful for tests and local development.
|
||||
type Memory{{Name}}Repository struct {
|
||||
{{name}}s map[string]{{name_lower}}.{{Name}}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewMemory{{Name}}Repository() *Memory{{Name}}Repository {
|
||||
return &Memory{{Name}}Repository{
|
||||
{{name}}s: make(map[string]{{name_lower}}.{{Name}}),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Memory{{Name}}Repository) Get{{Name}}(ctx context.Context, uuid string) (*{{name_lower}}.{{Name}}, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
t, ok := r.{{name}}s[uuid]
|
||||
if !ok {
|
||||
return nil, {{name_lower}}.ErrNotFound
|
||||
}
|
||||
|
||||
// Return a copy to prevent mutation of stored value
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (r *Memory{{Name}}Repository) Update{{Name}}(
|
||||
ctx context.Context,
|
||||
uuid string,
|
||||
updateFn func(t *{{name_lower}}.{{Name}}) (*{{name_lower}}.{{Name}}, error),
|
||||
) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
current, ok := r.{{name}}s[uuid]
|
||||
if !ok {
|
||||
return {{name_lower}}.ErrNotFound
|
||||
}
|
||||
|
||||
updated, err := updateFn(¤t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.{{name}}s[uuid] = *updated
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save{{Name}} stores a new {{Name}}. Used for initial creation.
|
||||
func (r *Memory{{Name}}Repository) Save{{Name}}(ctx context.Context, t *{{name_lower}}.{{Name}}) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.{{name}}s[t.UUID()] = *t
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## File: `adapters/memory_{{name_snake}}_repository_test.go`
|
||||
|
||||
```go
|
||||
package adapters_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"{{module}}/adapters"
|
||||
"{{module}}/domain/{{name_lower}}"
|
||||
)
|
||||
|
||||
func TestMemory{{Name}}Repository_Get(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
repo := adapters.NewMemory{{Name}}Repository()
|
||||
|
||||
// Setup: create and save a {{name_lower}}
|
||||
entity, err := {{name_lower}}.New{{Name}}("test-uuid")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.Save{{Name}}(ctx, entity)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test: retrieve it
|
||||
got, err := repo.Get{{Name}}(ctx, "test-uuid")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test-uuid", got.UUID())
|
||||
}
|
||||
|
||||
func TestMemory{{Name}}Repository_GetNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
repo := adapters.NewMemory{{Name}}Repository()
|
||||
|
||||
_, err := repo.Get{{Name}}(ctx, "nonexistent")
|
||||
assert.ErrorIs(t, err, {{name_lower}}.ErrNotFound)
|
||||
}
|
||||
|
||||
func TestMemory{{Name}}Repository_Update(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
repo := adapters.NewMemory{{Name}}Repository()
|
||||
|
||||
// Setup
|
||||
entity, err := {{name_lower}}.New{{Name}}("test-uuid")
|
||||
require.NoError(t, err)
|
||||
err = repo.Save{{Name}}(ctx, entity)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test: update via callback
|
||||
err = repo.Update{{Name}}(ctx, "test-uuid", func(t *{{name_lower}}.{{Name}}) (*{{name_lower}}.{{Name}}, error) {
|
||||
// TODO: Apply domain action
|
||||
return t, nil
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
```
|
||||
|
||||
## Extending to Production Adapters
|
||||
|
||||
When adding a real database adapter (e.g., PostgreSQL):
|
||||
|
||||
### 1. Create DB model struct
|
||||
|
||||
```go
|
||||
// adapters/postgres_{{name_snake}}_repository.go
|
||||
|
||||
type postgres{{Name}} struct {
|
||||
UUID string `db:"uuid"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
// ... map all persisted fields
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Implement conversion methods
|
||||
|
||||
```go
|
||||
func (r *Postgres{{Name}}Repository) to{{Name}}(m postgres{{Name}}) *{{name_lower}}.{{Name}} {
|
||||
return {{name_lower}}.Unmarshal{{Name}}FromDatabase(m.UUID, m.CreatedAt)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Run shared tests against all implementations
|
||||
|
||||
```go
|
||||
type TestRepository struct {
|
||||
Name string
|
||||
Repository {{name_lower}}.Repository
|
||||
}
|
||||
|
||||
func createRepositories(t *testing.T) []TestRepository {
|
||||
return []TestRepository{
|
||||
{Name: "memory", Repository: adapters.NewMemory{{Name}}Repository()},
|
||||
{Name: "postgres", Repository: newPostgresRepository(t)},
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,223 @@
|
||||
# Service Scaffold Template
|
||||
|
||||
Generate a complete service skeleton with all standard directories and stub files.
|
||||
|
||||
## Placeholders
|
||||
|
||||
- `{{Name}}` — PascalCase service/aggregate name (e.g., `Training`)
|
||||
- `{{name}}` — camelCase (e.g., `training`)
|
||||
- `{{name_snake}}` — snake_case (e.g., `training`)
|
||||
- `{{name_lower}}` — all lowercase (e.g., `training`)
|
||||
- `{{module}}` — Go module path from go.mod
|
||||
|
||||
## Files to Create
|
||||
|
||||
### 1. `domain/{{name_lower}}/{{name_snake}}.go`
|
||||
|
||||
```go
|
||||
package {{name_lower}}
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type {{Name}} struct {
|
||||
uuid string
|
||||
createdAt time.Time
|
||||
}
|
||||
|
||||
func New{{Name}}(uuid string) (*{{Name}}, error) {
|
||||
if uuid == "" {
|
||||
return nil, errors.New("empty {{name_lower}} uuid")
|
||||
}
|
||||
|
||||
return &{{Name}}{
|
||||
uuid: uuid,
|
||||
createdAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Unmarshal{{Name}}FromDatabase(uuid string, createdAt time.Time) *{{Name}} {
|
||||
return &{{Name}}{
|
||||
uuid: uuid,
|
||||
createdAt: createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (t {{Name}}) UUID() string {
|
||||
return t.uuid
|
||||
}
|
||||
|
||||
func (t {{Name}}) CreatedAt() time.Time {
|
||||
return t.createdAt
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `domain/{{name_lower}}/repository.go`
|
||||
|
||||
```go
|
||||
package {{name_lower}}
|
||||
|
||||
import "context"
|
||||
|
||||
type Repository interface {
|
||||
Get{{Name}}(ctx context.Context, uuid string) (*{{Name}}, error)
|
||||
Update{{Name}}(ctx context.Context, uuid string,
|
||||
updateFn func(t *{{Name}}) (*{{Name}}, error)) error
|
||||
}
|
||||
```
|
||||
|
||||
### 3. `domain/{{name_lower}}/errors.go`
|
||||
|
||||
```go
|
||||
package {{name_lower}}
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("{{name_lower}} not found")
|
||||
)
|
||||
```
|
||||
|
||||
### 4. `app/app.go`
|
||||
|
||||
```go
|
||||
package app
|
||||
|
||||
import (
|
||||
"{{module}}/app/command"
|
||||
"{{module}}/app/query"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
Commands Commands
|
||||
Queries Queries
|
||||
}
|
||||
|
||||
type Commands struct {
|
||||
// Add command handlers here, e.g.:
|
||||
// Create{{Name}} command.Create{{Name}}Handler
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
// Add query handlers here, e.g.:
|
||||
// {{Name}}ByUUID query.{{Name}}ByUUIDHandler
|
||||
}
|
||||
```
|
||||
|
||||
### 5. `app/command/.gitkeep`
|
||||
|
||||
Create empty directory placeholder.
|
||||
|
||||
### 6. `app/query/.gitkeep`
|
||||
|
||||
Create empty directory placeholder.
|
||||
|
||||
### 7. `ports/http.go`
|
||||
|
||||
```go
|
||||
package ports
|
||||
|
||||
import (
|
||||
"{{module}}/app"
|
||||
)
|
||||
|
||||
type HttpServer struct {
|
||||
app app.Application
|
||||
}
|
||||
|
||||
func NewHttpServer(application app.Application) HttpServer {
|
||||
return HttpServer{app: application}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. `adapters/memory_{{name_snake}}_repository.go`
|
||||
|
||||
```go
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"{{module}}/domain/{{name_lower}}"
|
||||
)
|
||||
|
||||
type Memory{{Name}}Repository struct {
|
||||
{{name_lower}}s map[string]{{name_lower}}.{{Name}}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewMemory{{Name}}Repository() *Memory{{Name}}Repository {
|
||||
return &Memory{{Name}}Repository{
|
||||
{{name_lower}}s: make(map[string]{{name_lower}}.{{Name}}),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Memory{{Name}}Repository) Get{{Name}}(ctx context.Context, uuid string) (*{{name_lower}}.{{Name}}, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
t, ok := r.{{name_lower}}s[uuid]
|
||||
if !ok {
|
||||
return nil, {{name_lower}}.ErrNotFound
|
||||
}
|
||||
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (r *Memory{{Name}}Repository) Update{{Name}}(
|
||||
ctx context.Context,
|
||||
uuid string,
|
||||
updateFn func(t *{{name_lower}}.{{Name}}) (*{{name_lower}}.{{Name}}, error),
|
||||
) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
current, ok := r.{{name_lower}}s[uuid]
|
||||
if !ok {
|
||||
return {{name_lower}}.ErrNotFound
|
||||
}
|
||||
|
||||
updated, err := updateFn(¤t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.{{name_lower}}s[uuid] = *updated
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 9. `service/application.go`
|
||||
|
||||
```go
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"{{module}}/adapters"
|
||||
"{{module}}/app"
|
||||
)
|
||||
|
||||
func NewApplication(ctx context.Context) app.Application {
|
||||
{{name_lower}}Repository := adapters.NewMemory{{Name}}Repository()
|
||||
_ = {{name_lower}}Repository // wire into handlers
|
||||
|
||||
return app.Application{
|
||||
Commands: app.Commands{},
|
||||
Queries: app.Queries{},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Post-Creation Instructions
|
||||
|
||||
After creating the service skeleton:
|
||||
|
||||
1. Add your first command with `/threedots scaffold command <ActionName>`
|
||||
2. Add your first query with `/threedots scaffold query <QueryName>`
|
||||
3. Wire them in `service/application.go`
|
||||
4. Add HTTP/gRPC handlers in `ports/`
|
||||
Reference in New Issue
Block a user