一键导入
go-add-entity
Add a full CRUD vertical slice (domain, DTO, repository, service, handler, migration, factory wiring) to an existing GOB Go microservice
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Add a full CRUD vertical slice (domain, DTO, repository, service, handler, migration, factory wiring) to an existing GOB Go microservice
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Internal POSIX runtime helpers for agente-00c/feature-00c orchestrators (state, lock, validation, hashes, secrets filter). NOT user-invocable.
Task status / backlog progress report; identifies tasks ready to start. Triggers: "revisar tarefas", "status das tarefas", "progresso do projeto", "review tasks". Skip for executing (execute-task) or creating tasks (create-tasks).
GLOBAL feature portfolio dashboard — compare progress, suggest archive/abandon/prioritize. Triggers: "status global", "portfolio de features", "dashboard de features", "comparar features". Cross-feature; for single feature deep-dive use review-task.
Convert a natural-language feature description into SDD spec (user stories, FRs, success criteria). Triggers: "specify", "criar spec", "nova feature", "feature spec". Skip for refining an existing spec (clarify).
Audit a GOB Go microservice against all project conventions and patterns
Requirements quality gate ("unit tests for English") by domain (ux/api/security/performance/a11y). Triggers: "checklist", "validar requisitos", "quality gate". Validates REQUIREMENT quality, not code.
| name | go-add-entity |
| description | Add a full CRUD vertical slice (domain, DTO, repository, service, handler, migration, factory wiring) to an existing GOB Go microservice |
| allowed-tools | ["Read","Write","Edit","Glob","Grep","Bash"] |
Generate a complete CRUD vertical slice for a new entity in an existing GOB Go microservice. This creates 8-10 files following all project conventions.
"add entity", "new entity", "nova entidade", "add CRUD", "adicionar recurso", "add resource"
$ARGUMENTS should specify:
gob-member-service) — requiredServiceGroup, Topic) — required, in PascalCaseBefore generating ANY code, read these files from the target service (in parallel):
go.mod — module path (e.g., github.com/gob/gob-process-service)internal/factory/factory.go — existing repos, factory pattern in useinternal/repository/repository.go — existing interfaces, import pathsmigrations/ (ls directory) — next migration numbercmd/api/main.go — wiring point, existing services/handlers, route groupsinternal/dto/dto.go — existing shared types (ErrorResponse, Response, Pagination)internal/domain/ (ls directory) — existing domain models for referenceAlso determine:
ServiceGroup → service_groups)internal/domain/{entity_snake}.gopackage domain
import (
"time"
"github.com/google/uuid"
)
// {Entity} represents a {description}.
type {Entity} struct {
ID uuid.UUID `json:"id" db:"id"`
// ... fields with both json and db tags ...
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty" db:"updated_at"`
}
Rules:
json:"" and db:"" tags on every fieldjson tags: snake_case, omitempty on optional/nullable fieldsdb tags: snake_case matching exact DB column names*string, *time.Time, *uuid.UUID)uuid.UUID from github.com/google/uuidjson.RawMessage with omitemptytype {Entity}Status string with constantsinternal/dto/{entity_snake}.gopackage dto
import "github.com/google/uuid"
// Create{Entity}Request is the input for creating a new {entity}.
type Create{Entity}Request struct {
// Fields without ID, CreatedAt, UpdatedAt
// Use json tags in snake_case matching backend expectation
Name string `json:"name" validate:"required"`
Description string `json:"description,omitempty"`
ParentID *uuid.UUID `json:"parent_id,omitempty"`
}
// Update{Entity}Request is the input for updating an existing {entity}.
type Update{Entity}Request struct {
// Only updatable fields, all optional (pointers)
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
}
Rules:
ErrorResponse, Response, Pagination — they exist in dto.gojson tags in snake_caseNew{Entity}Response() constructorvalidate tags for required fields if the service uses a validatorinternal/repository/repository.go// {Entity}Repository defines persistence operations for {entities}.
type {Entity}Repository interface {
FindByID(ctx context.Context, id uuid.UUID) (*domain.{Entity}, error)
List(ctx context.Context, filter *{Entity}Filter) ([]*domain.{Entity}, error)
Count(ctx context.Context, filter *{Entity}Filter) (int64, error)
Create(ctx context.Context, entity *domain.{Entity}) error
Update(ctx context.Context, entity *domain.{Entity}) error
Delete(ctx context.Context, id uuid.UUID) error
}
// {Entity}Filter contains filtering and pagination options.
type {Entity}Filter struct {
Search string
Status string
Limit int
Offset int
SortBy string
SortOrder string
}
Rules:
context.Context is ALWAYS the first parameterFindByID returns (*domain.{Entity}, error) — nil/nil when not foundinternal/repository/postgres/{entity_snake}_repository.gopackage postgres
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/gob/gob-go-commons/pkg/logger"
"github.com/gob/gob-{service}/internal/domain"
"github.com/gob/gob-{service}/internal/repository"
)
type {Entity}Repository struct {
db *sqlx.DB
log *logger.Logger
}
func New{Entity}Repository(db *sqlx.DB, log *logger.Logger) *{Entity}Repository {
return &{Entity}Repository{db: db, log: log}
}
// Internal row struct for DB scanning
type {entity}Row struct {
ID uuid.UUID `db:"id"`
Name string `db:"name"`
Desc sql.NullString `db:"description"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt *time.Time `db:"updated_at"`
}
func (r *{entity}Row) toDomain() *domain.{Entity} {
return &domain.{Entity}{
ID: r.ID,
Name: r.Name,
Description: r.Desc.String,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
}
}
func (r *{Entity}Repository) FindByID(ctx context.Context, id uuid.UUID) (*domain.{Entity}, error) {
start := time.Now()
r.log.WithField("entity", "{entity}").
WithField("operation", "find_by_id").
WithField("entity_id", id.String()).
Debug("Querying database")
var row {entity}Row
query := `SELECT * FROM {schema}.{table} WHERE id = $1`
if err := r.db.GetContext(ctx, &row, query, id); err != nil {
if err == sql.ErrNoRows {
r.log.WithField("entity", "{entity}").
WithField("operation", "find_by_id").
WithField("duration_ms", time.Since(start).Milliseconds()).
Debug("Entity not found")
return nil, nil
}
r.log.WithError(err).
WithField("entity", "{entity}").
WithField("operation", "find_by_id").
WithField("duration_ms", time.Since(start).Milliseconds()).
Error("Database query failed")
return nil, err
}
r.log.WithField("entity", "{entity}").
WithField("operation", "find_by_id").
WithField("duration_ms", time.Since(start).Milliseconds()).
Debug("Query completed")
return row.toDomain(), nil
}
func (r *{Entity}Repository) List(ctx context.Context, filter *repository.{Entity}Filter) ([]*domain.{Entity}, error) {
start := time.Now()
r.log.WithField("entity", "{entity}").
WithField("operation", "list").
Debug("Querying database")
var rows []*{entity}Row
query := `SELECT * FROM {schema}.{table} WHERE 1=1`
args := []any{}
argIdx := 1
if filter.Search != "" {
query += fmt.Sprintf(` AND (name ILIKE $%d)`, argIdx)
args = append(args, "%"+filter.Search+"%")
argIdx++
}
if filter.Status != "" {
query += fmt.Sprintf(` AND status = $%d`, argIdx)
args = append(args, filter.Status)
argIdx++
}
// Sorting
sortBy := "created_at"
sortOrder := "DESC"
if filter.SortBy != "" {
sortBy = filter.SortBy
}
if filter.SortOrder != "" {
sortOrder = strings.ToUpper(filter.SortOrder)
}
query += fmt.Sprintf(` ORDER BY %s %s`, sortBy, sortOrder)
// Pagination
if filter.Limit > 0 {
query += fmt.Sprintf(` LIMIT $%d`, argIdx)
args = append(args, filter.Limit)
argIdx++
}
if filter.Offset > 0 {
query += fmt.Sprintf(` OFFSET $%d`, argIdx)
args = append(args, filter.Offset)
argIdx++
}
if err := r.db.SelectContext(ctx, &rows, query, args...); err != nil {
r.log.WithError(err).
WithField("entity", "{entity}").
WithField("operation", "list").
WithField("duration_ms", time.Since(start).Milliseconds()).
Error("Database query failed")
return nil, err
}
result := make([]*domain.{Entity}, len(rows))
for i, row := range rows {
result[i] = row.toDomain()
}
r.log.WithField("entity", "{entity}").
WithField("operation", "list").
WithField("result_count", len(result)).
WithField("duration_ms", time.Since(start).Milliseconds()).
Debug("Query completed")
return result, nil
}
func (r *{Entity}Repository) Count(ctx context.Context, filter *repository.{Entity}Filter) (int64, error) {
start := time.Now()
query := `SELECT COUNT(*) FROM {schema}.{table} WHERE 1=1`
args := []any{}
argIdx := 1
if filter.Search != "" {
query += fmt.Sprintf(` AND (name ILIKE $%d)`, argIdx)
args = append(args, "%"+filter.Search+"%")
argIdx++
}
if filter.Status != "" {
query += fmt.Sprintf(` AND status = $%d`, argIdx)
args = append(args, filter.Status)
argIdx++
}
var count int64
if err := r.db.GetContext(ctx, &count, query, args...); err != nil {
r.log.WithError(err).
WithField("entity", "{entity}").
WithField("operation", "count").
WithField("duration_ms", time.Since(start).Milliseconds()).
Error("Database count query failed")
return 0, err
}
r.log.WithField("entity", "{entity}").
WithField("operation", "count").
WithField("result_count", count).
WithField("duration_ms", time.Since(start).Milliseconds()).
Debug("Count query completed")
return count, nil
}
func (r *{Entity}Repository) Create(ctx context.Context, entity *domain.{Entity}) error {
start := time.Now()
r.log.WithField("entity", "{entity}").
WithField("operation", "create").
WithField("entity_id", entity.ID.String()).
Debug("Inserting into database")
query := `
INSERT INTO {schema}.{table} (id, name, description, created_at)
VALUES ($1, $2, $3, $4)`
_, err := r.db.ExecContext(ctx, query,
entity.ID, entity.Name, entity.Description, entity.CreatedAt)
if err != nil {
r.log.WithError(err).
WithField("entity", "{entity}").
WithField("operation", "create").
WithField("duration_ms", time.Since(start).Milliseconds()).
Error("Database insert failed")
return err
}
r.log.WithField("entity", "{entity}").
WithField("operation", "create").
WithField("entity_id", entity.ID.String()).
WithField("duration_ms", time.Since(start).Milliseconds()).
Debug("Insert completed")
return nil
}
func (r *{Entity}Repository) Update(ctx context.Context, entity *domain.{Entity}) error {
start := time.Now()
now := time.Now()
entity.UpdatedAt = &now
query := `
UPDATE {schema}.{table}
SET name = $2, description = $3, updated_at = $4
WHERE id = $1`
_, err := r.db.ExecContext(ctx, query,
entity.ID, entity.Name, entity.Description, entity.UpdatedAt)
if err != nil {
r.log.WithError(err).
WithField("entity", "{entity}").
WithField("operation", "update").
WithField("entity_id", entity.ID.String()).
WithField("duration_ms", time.Since(start).Milliseconds()).
Error("Database update failed")
return err
}
r.log.WithField("entity", "{entity}").
WithField("operation", "update").
WithField("entity_id", entity.ID.String()).
WithField("duration_ms", time.Since(start).Milliseconds()).
Debug("Update completed")
return nil
}
func (r *{Entity}Repository) Delete(ctx context.Context, id uuid.UUID) error {
start := time.Now()
query := `DELETE FROM {schema}.{table} WHERE id = $1`
_, err := r.db.ExecContext(ctx, query, id)
if err != nil {
r.log.WithError(err).
WithField("entity", "{entity}").
WithField("operation", "delete").
WithField("entity_id", id.String()).
WithField("duration_ms", time.Since(start).Milliseconds()).
Error("Database delete failed")
return err
}
r.log.WithField("entity", "{entity}").
WithField("operation", "delete").
WithField("entity_id", id.String()).
WithField("duration_ms", time.Since(start).Milliseconds()).
Debug("Delete completed")
return nil
}
Rules:
{entity}Row struct for DB scanning (use sql.NullString, sql.NullInt64, []byte for JSON)toDomain() method on row struct to convert to domain modelGetContext for single row, SelectContext for multiple rowssql.ErrNoRows → return nil, nil (NOT an error){schema}.{table} (e.g., process.service_groups)$1, $2, $3... (NOT ?)argIdx counter for filter queriesinternal/service/{entity_snake}_service.gopackage service
import (
"context"
"errors"
"time"
"github.com/google/uuid"
"github.com/gob/gob-go-commons/pkg/logger"
"github.com/gob/gob-{service}/internal/domain"
"github.com/gob/gob-{service}/internal/repository"
)
var (
Err{Entity}NotFound = errors.New("{entity} not found")
)
type {Entity}Service struct {
repo repository.{Entity}Repository
log *logger.Logger
}
func New{Entity}Service(repo repository.{Entity}Repository, log *logger.Logger) *{Entity}Service {
return &{Entity}Service{repo: repo, log: log}
}
func (s *{Entity}Service) GetByID(ctx context.Context, id uuid.UUID) (*domain.{Entity}, error) {
entity, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, err
}
if entity == nil {
return nil, Err{Entity}NotFound
}
return entity, nil
}
func (s *{Entity}Service) List(ctx context.Context, filter *repository.{Entity}Filter) ([]*domain.{Entity}, int64, error) {
entities, err := s.repo.List(ctx, filter)
if err != nil {
return nil, 0, err
}
total, err := s.repo.Count(ctx, filter)
if err != nil {
return nil, 0, err
}
return entities, total, nil
}
func (s *{Entity}Service) Create(ctx context.Context, input *Create{Entity}Input) (*domain.{Entity}, error) {
entity := &domain.{Entity}{
ID: uuid.New(),
Name: input.Name,
CreatedAt: time.Now(),
}
if err := s.repo.Create(ctx, entity); err != nil {
return nil, err
}
return entity, nil
}
func (s *{Entity}Service) Update(ctx context.Context, id uuid.UUID, input *Update{Entity}Input) (*domain.{Entity}, error) {
entity, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, err
}
if entity == nil {
return nil, Err{Entity}NotFound
}
// Apply partial updates
if input.Name != nil {
entity.Name = *input.Name
}
if err := s.repo.Update(ctx, entity); err != nil {
return nil, err
}
return entity, nil
}
func (s *{Entity}Service) Delete(ctx context.Context, id uuid.UUID) error {
entity, err := s.repo.FindByID(ctx, id)
if err != nil {
return err
}
if entity == nil {
return Err{Entity}NotFound
}
return s.repo.Delete(ctx, id)
}
// Input types for service methods
type Create{Entity}Input struct {
Name string
Description string
}
type Update{Entity}Input struct {
Name *string
Description *string
}
Rules:
var Err{Entity}NotFound = errors.New("...")FindByID returns nil → wrap as sentinel error in serviceinternal/handler/{entity_snake}_handler.gopackage handler
import (
"errors"
"net/http"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/gob/gob-go-commons/pkg/middleware"
"github.com/gob/gob-{service}/internal/dto"
"github.com/gob/gob-{service}/internal/repository"
"github.com/gob/gob-{service}/internal/service"
)
type {Entity}Handler struct {
svc *service.{Entity}Service
log *logger.Logger
}
func New{Entity}Handler(svc *service.{Entity}Service, log *logger.Logger) *{Entity}Handler {
return &{Entity}Handler{svc: svc, log: log}
}
func (h *{Entity}Handler) RegisterRoutes(router fiber.Router) {
// STATIC routes FIRST
router.Get("/", h.List)
router.Post("/", h.Create)
// PARAMETERIZED routes LAST
router.Get("/:id", h.Get)
router.Put("/:id", h.Update)
router.Delete("/:id", h.Delete)
}
func (h *{Entity}Handler) List(c *fiber.Ctx) error {
filter := &repository.{Entity}Filter{
Search: c.Query("search"),
Status: c.Query("status"),
Limit: c.QueryInt("limit", 20),
Offset: c.QueryInt("offset", 0),
SortBy: c.Query("sort_by", "created_at"),
SortOrder: c.Query("sort_order", "desc"),
}
entities, total, err := h.svc.List(c.Context(), filter)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(dto.ErrorResponse{
Error: "internal_error",
Message: "Failed to list {entities}",
})
}
return c.JSON(dto.Response{
Data: entities,
Total: total,
})
}
func (h *{Entity}Handler) Get(c *fiber.Ctx) error {
id, err := uuid.Parse(c.Params("id"))
if err != nil {
return c.Status(http.StatusBadRequest).JSON(dto.ErrorResponse{
Error: "invalid_id",
Message: "Invalid {entity} ID",
})
}
entity, err := h.svc.GetByID(c.Context(), id)
if err != nil {
if errors.Is(err, service.Err{Entity}NotFound) {
return c.Status(http.StatusNotFound).JSON(dto.ErrorResponse{
Error: "not_found",
Message: "{Entity} not found",
})
}
return c.Status(http.StatusInternalServerError).JSON(dto.ErrorResponse{
Error: "internal_error",
Message: "Failed to get {entity}",
})
}
return c.JSON(entity)
}
func (h *{Entity}Handler) Create(c *fiber.Ctx) error {
var req dto.Create{Entity}Request
if err := c.BodyParser(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(dto.ErrorResponse{
Error: "invalid_request",
Message: "Invalid request body",
})
}
input := &service.Create{Entity}Input{
Name: req.Name,
Description: req.Description,
}
entity, err := h.svc.Create(c.Context(), input)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(dto.ErrorResponse{
Error: "internal_error",
Message: "Failed to create {entity}",
})
}
return c.Status(http.StatusCreated).JSON(entity)
}
func (h *{Entity}Handler) Update(c *fiber.Ctx) error {
id, err := uuid.Parse(c.Params("id"))
if err != nil {
return c.Status(http.StatusBadRequest).JSON(dto.ErrorResponse{
Error: "invalid_id",
Message: "Invalid {entity} ID",
})
}
var req dto.Update{Entity}Request
if err := c.BodyParser(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(dto.ErrorResponse{
Error: "invalid_request",
Message: "Invalid request body",
})
}
input := &service.Update{Entity}Input{
Name: req.Name,
Description: req.Description,
}
entity, err := h.svc.Update(c.Context(), id, input)
if err != nil {
if errors.Is(err, service.Err{Entity}NotFound) {
return c.Status(http.StatusNotFound).JSON(dto.ErrorResponse{
Error: "not_found",
Message: "{Entity} not found",
})
}
return c.Status(http.StatusInternalServerError).JSON(dto.ErrorResponse{
Error: "internal_error",
Message: "Failed to update {entity}",
})
}
return c.JSON(entity)
}
func (h *{Entity}Handler) Delete(c *fiber.Ctx) error {
id, err := uuid.Parse(c.Params("id"))
if err != nil {
return c.Status(http.StatusBadRequest).JSON(dto.ErrorResponse{
Error: "invalid_id",
Message: "Invalid {entity} ID",
})
}
if err := h.svc.Delete(c.Context(), id); err != nil {
if errors.Is(err, service.Err{Entity}NotFound) {
return c.Status(http.StatusNotFound).JSON(dto.ErrorResponse{
Error: "not_found",
Message: "{Entity} not found",
})
}
return c.Status(http.StatusInternalServerError).JSON(dto.ErrorResponse{
Error: "internal_error",
Message: "Failed to delete {entity}",
})
}
return c.SendStatus(http.StatusNoContent)
}
Rules:
RegisterRoutes() method — static routes BEFORE parameterized /:id/:id catch-all (Fiber trie conflict)errors.Is() for sentinel error dispatchdto.ErrorResponse for all error responsesdto.Response with Data + Total for list endpointsc.BodyParser() for request bodyc.Params("id") + uuid.Parse() for path paramsc.Query() / c.QueryInt() for query paramsmiddleware.GetUserID(c), middleware.GetClaims(c)Generate using the same conventions as the go-add-migration skill:
gen_random_uuid() for PKsTIMESTAMP NOT NULL DEFAULT NOW() for created_atDROP TABLE IF EXISTS ... CASCADEinternal/factory/factory.goAdd to the Repositories struct:
{Entity} repository.{Entity}Repository
Add to NewRepositories():
{Entity}: postgres.New{Entity}Repository(db, log),
Add to NewDryRunRepositories() if it exists:
{Entity}: postgres.New{Entity}Repository(db, log), // read-only uses real repo
Add getter method:
func (r *Repositories) Get{Entity}Repository() repository.{Entity}Repository {
return r.{Entity}
}
cmd/api/main.goAdd after existing services/handlers (find the wiring section):
// {Entity}
{entity}Svc := service.New{Entity}Service(repos.Get{Entity}Repository(), log)
{entity}Handler := handler.New{Entity}Handler({entity}Svc, log)
Register route group. CRITICAL: if there's an existing /:id handler at the same router level, the new group must be registered BEFORE it:
{entity}Group := api.Group("/{entities}")
{entity}Handler.RegisterRoutes({entity}Group)
After all files are created:
go mod tidy in the service directorygo build ./... to verify compilationmake migrate-up SERVICE={service-name}Fiber trie conflict: NEVER register a sub-group (e.g., /groups) after a /:id catch-all at the same level. Static paths MUST come first. This is the #1 cause of mysterious 404s.
sql.ErrNoRows: In repository, return nil, nil. In service, check nil and return sentinel error. NEVER let sql.ErrNoRows propagate as an unhandled error.
JSONB columns: Use json.RawMessage in domain, []byte in row struct, convert in toDomain(). For writes, may need jsonbParam() helper.
Service returns domain, NOT DTOs: The handler is responsible for converting domain → DTO/response.
Import cycle prevention: Domain → nothing. DTO → domain (if needed). Repository → domain. Service → repository + domain. Handler → service + dto + domain. Factory → repository + postgres.
go mod tidy: ALWAYS run after adding new imports. The service may not have all dependencies yet.
Schema prefix: EVERY SQL query must use {schema}.{table} format. Unqualified table names will hit the wrong schema or fail.
Portuguese accents: Seed data and user-facing messages must use proper accents (e, a, c, o, i).