@nakedev/go-scaffold 0.5.5 → 0.8.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.
Files changed (48) hide show
  1. package/README.md +16 -2
  2. package/dist/commands/auth.js +6 -1
  3. package/dist/commands/check.js +5 -1
  4. package/dist/commands/method.js +27 -1
  5. package/dist/index.js +20 -6
  6. package/dist/prompts/auth-wizard.js +33 -1
  7. package/dist/templates/create-manifest.js +8 -0
  8. package/dist/utils/hexagonal-method-patcher.js +79 -20
  9. package/package.json +1 -1
  10. package/templates/add/auth/docs/login.yaml.hbs +9 -1
  11. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler.go.hbs +12 -2
  12. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +1 -0
  13. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +97 -13
  14. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository_test.go.hbs +169 -5
  15. package/templates/add/auth/internal/app/user/application/local_auth.go.hbs +44 -2
  16. package/templates/add/auth/internal/app/user/application/recovery_service.go.hbs +2 -1
  17. package/templates/add/auth/internal/app/user/application/service.go.hbs +1 -1
  18. package/templates/add/auth/internal/app/user/application/service_test.go.hbs +71 -12
  19. package/templates/add/auth/internal/app/user/application/user_query.go.hbs +5 -4
  20. package/templates/add/auth/internal/app/user/domain/errors.go.hbs +4 -0
  21. package/templates/add/auth/internal/app/user/ports/repository.go.hbs +21 -3
  22. package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +10 -0
  23. package/templates/add/rbac/docs/roles.yaml.hbs +2 -1
  24. package/templates/add/rbac/docs/users.yaml.hbs +2 -1
  25. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +6 -2
  26. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +21 -4
  27. package/templates/add/rbac/internal/app/role/application/dto.go.hbs +9 -1
  28. package/templates/add/rbac/internal/app/role/application/service.go.hbs +6 -6
  29. package/templates/add/rbac/internal/app/role/application/service_test.go.hbs +19 -1
  30. package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +13 -1
  31. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +1 -1
  32. package/templates/create/base/AGENTS.md.hbs +9 -1
  33. package/templates/create/base/README.md.hbs +2 -1
  34. package/templates/create/base/internal/shared/dbq/dbq.go.hbs +47 -0
  35. package/templates/create/base/internal/shared/dbq/dbq_test.go.hbs +66 -0
  36. package/templates/create/base/internal/shared/pagination/pagination.go.hbs +19 -2
  37. package/templates/create/features/docs/architecture.md.hbs +9 -6
  38. package/templates/create/features/docs/common/parameters.yaml.hbs +5 -0
  39. package/templates/create/features/docs/common/schemas.yaml.hbs +1 -0
  40. package/templates/create/features/docs/patterns.md.hbs +60 -0
  41. package/templates/generate/module/docs/collection.yaml.hbs +2 -1
  42. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +8 -3
  43. package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +27 -4
  44. package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +3 -3
  45. package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +3 -3
  46. package/templates/generate/module/hexagonal/application/service.crud.go.hbs +3 -3
  47. package/templates/generate/module/hexagonal/application/service_test.go.hbs +4 -3
  48. package/templates/generate/module/hexagonal/ports/repository.go.hbs +18 -2
@@ -7,6 +7,7 @@ import (
7
7
 
8
8
  "{{goModule}}/internal/app/{{modulePath}}/application"
9
9
  "{{goModule}}/internal/app/{{modulePath}}/domain"
10
+ "{{goModule}}/internal/app/{{modulePath}}/ports"
10
11
  "{{goModule}}/internal/shared/apperror"
11
12
  "{{goModule}}/internal/shared/httpx"
12
13
  {{#if auth}} "{{goModule}}/internal/shared/middleware"
@@ -107,10 +108,14 @@ func (h *Handler) create(c *gin.Context) {
107
108
 
108
109
  func (h *Handler) list(c *gin.Context) {
109
110
  p := pagination.Parse(c)
111
+ // One struct all the way down, so this module's own filters are read off
112
+ // the query string here and added as fields on ports.ListFilter — never as
113
+ // extra arguments on the three signatures below.
114
+ filter := ports.ListFilter{Search: p.Search, Limit: p.Limit, Offset: p.Offset}
110
115
  {{#if cqrs}}
111
- items, err := h.queries.List(c.Request.Context(), p.Limit, p.Offset)
116
+ items, total, err := h.queries.List(c.Request.Context(), filter)
112
117
  {{else}}
113
- items, err := h.svc.List(c.Request.Context(), p.Limit, p.Offset)
118
+ items, total, err := h.svc.List(c.Request.Context(), filter)
114
119
  {{/if}}
115
120
  if err != nil {
116
121
  c.Error(appError(err))
@@ -120,7 +125,7 @@ func (h *Handler) list(c *gin.Context) {
120
125
  for i := range items {
121
126
  out[i] = toResponse(application.ToResponse(&items[i]))
122
127
  }
123
- c.JSON(http.StatusOK, p.Response(out))
128
+ c.JSON(http.StatusOK, p.ResponseWithTotal(out, total))
124
129
  }
125
130
 
126
131
  func (h *Handler) get(c *gin.Context) {
@@ -6,6 +6,7 @@ import (
6
6
 
7
7
  "{{goModule}}/internal/app/{{modulePath}}/domain"
8
8
  "{{goModule}}/internal/app/{{modulePath}}/ports"
9
+ "{{goModule}}/internal/shared/dbq"
9
10
  "{{goModule}}/internal/shared/dberr"
10
11
  "{{goModule}}/internal/shared/tx"
11
12
 
@@ -30,16 +31,38 @@ func (r *Repository) Create(ctx context.Context, m *domain.{{pascalName}}) error
30
31
  return nil
31
32
  }
32
33
 
33
- func (r *Repository) FindAll(ctx context.Context, limit, offset int) ([]domain.{{pascalName}}, error) {
34
+ // FindAll answers one page of the filter and how many rows it matched
35
+ // altogether. Two queries, because a count over a LIMITed query would only
36
+ // ever count the page.
37
+ func (r *Repository) FindAll(ctx context.Context, filter ports.ListFilter) ([]domain.{{pascalName}}, int64, error) {
38
+ // A closure rather than one *gorm.DB reused twice: conditions accumulate
39
+ // on the value, so the count would silently inherit the page's LIMIT.
40
+ matching := func() *gorm.DB {
41
+ q := tx.From(ctx, r.db).WithContext(ctx).Model(&{{pascalName}}Model{})
42
+ // TODO: name the columns this list is searched by. dbq.Search is a
43
+ // no-op until it has at least one, so ?q= is accepted and ignored
44
+ // until then. Add this module's own filters beside it.
45
+ q = dbq.Search(q, filter.Search /*, "name", "code" */)
46
+ return q
47
+ }
48
+
49
+ var total int64
50
+ if err := matching().Count(&total).Error; err != nil {
51
+ return nil, 0, mapDatabaseError(err)
52
+ }
53
+
34
54
  var rows []{{pascalName}}Model
35
- if err := tx.From(ctx, r.db).WithContext(ctx).Order("id desc").Limit(limit).Offset(offset).Find(&rows).Error; err != nil {
36
- return nil, mapDatabaseError(err)
55
+ // created_at orders it, id breaks the ties: without the tiebreaker two
56
+ // rows written in the same instant can swap between page 1 and page 2,
57
+ // showing one twice and hiding the other.
58
+ if err := matching().Order("created_at desc, id").Limit(filter.Limit).Offset(filter.Offset).Find(&rows).Error; err != nil {
59
+ return nil, 0, mapDatabaseError(err)
37
60
  }
38
61
  items := make([]domain.{{pascalName}}, len(rows))
39
62
  for i := range rows {
40
63
  items[i] = *toDomain(&rows[i])
41
64
  }
42
- return items, nil
65
+ return items, total, nil
43
66
  }
44
67
 
45
68
  func (r *Repository) FindByID(ctx context.Context, id uuid.UUID) (*domain.{{pascalName}}, error) {
@@ -15,7 +15,7 @@ import (
15
15
  // which side of the CQRS boundary was called.
16
16
  type repositoryStub struct {
17
17
  createFn func(context.Context, *domain.{{pascalName}}) error
18
- findAllFn func(context.Context, int, int) ([]domain.{{pascalName}}, error)
18
+ findAllFn func(context.Context, ports.ListFilter) ([]domain.{{pascalName}}, int64, error)
19
19
  findByIDFn func(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
20
20
  updateFn func(context.Context, *domain.{{pascalName}}) error
21
21
  deleteFn func(context.Context, uuid.UUID) error
@@ -28,11 +28,11 @@ func (s *repositoryStub) Create(ctx context.Context, m *domain.{{pascalName}}) e
28
28
  }
29
29
  return s.createFn(ctx, m)
30
30
  }
31
- func (s *repositoryStub) FindAll(ctx context.Context, limit, offset int) ([]domain.{{pascalName}}, error) {
31
+ func (s *repositoryStub) FindAll(ctx context.Context, filter ports.ListFilter) ([]domain.{{pascalName}}, int64, error) {
32
32
  if s.findAllFn == nil {
33
33
  panic("unexpected repository.FindAll call")
34
34
  }
35
- return s.findAllFn(ctx, limit, offset)
35
+ return s.findAllFn(ctx, filter)
36
36
  }
37
37
  func (s *repositoryStub) FindByID(ctx context.Context, id uuid.UUID) (*domain.{{pascalName}}, error) {
38
38
  if s.findByIDFn == nil {
@@ -9,7 +9,7 @@ import (
9
9
  )
10
10
 
11
11
  type QueryPort interface {
12
- List(context.Context, int, int) ([]domain.{{pascalName}}, error)
12
+ List(context.Context, ports.ListFilter) ([]domain.{{pascalName}}, int64, error)
13
13
  Get(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
14
14
  // go-scaffold:query-interface
15
15
  }
@@ -22,8 +22,8 @@ func NewQueryHandler(repo ports.QueryRepository) *QueryHandler {
22
22
  return &QueryHandler{repo: repo}
23
23
  }
24
24
 
25
- func (h *QueryHandler) List(ctx context.Context, limit, offset int) ([]domain.{{pascalName}}, error) {
26
- return h.repo.FindAll(ctx, limit, offset)
25
+ func (h *QueryHandler) List(ctx context.Context, filter ports.ListFilter) ([]domain.{{pascalName}}, int64, error) {
26
+ return h.repo.FindAll(ctx, filter)
27
27
  }
28
28
 
29
29
  func (h *QueryHandler) Get(ctx context.Context, id uuid.UUID) (*domain.{{pascalName}}, error) {
@@ -13,7 +13,7 @@ import (
13
13
 
14
14
  type ServicePort interface {
15
15
  Create(context.Context, CreateInput) (*domain.{{pascalName}}, error)
16
- List(context.Context, int, int) ([]domain.{{pascalName}}, error)
16
+ List(context.Context, ports.ListFilter) ([]domain.{{pascalName}}, int64, error)
17
17
  Get(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
18
18
  Update(context.Context, uuid.UUID, UpdateInput) (*domain.{{pascalName}}, error)
19
19
  Delete(context.Context, uuid.UUID) error
@@ -38,8 +38,8 @@ func (s *Service) Create(ctx context.Context, in CreateInput) (*domain.{{pascalN
38
38
  return m, nil
39
39
  }
40
40
 
41
- func (s *Service) List(ctx context.Context, limit, offset int) ([]domain.{{pascalName}}, error) {
42
- return s.repo.FindAll(ctx, limit, offset)
41
+ func (s *Service) List(ctx context.Context, filter ports.ListFilter) ([]domain.{{pascalName}}, int64, error) {
42
+ return s.repo.FindAll(ctx, filter)
43
43
  }
44
44
 
45
45
  func (s *Service) Get(ctx context.Context, id uuid.UUID) (*domain.{{pascalName}}, error) {
@@ -5,6 +5,7 @@ import (
5
5
  "testing"
6
6
 
7
7
  "{{goModule}}/internal/app/{{modulePath}}/domain"
8
+ "{{goModule}}/internal/app/{{modulePath}}/ports"
8
9
 
9
10
  "github.com/google/uuid"
10
11
  )
@@ -15,7 +16,7 @@ import (
15
16
  // orchestration.
16
17
  type repositoryStub struct {
17
18
  createFn func(context.Context, *domain.{{pascalName}}) error
18
- findAllFn func(context.Context, int, int) ([]domain.{{pascalName}}, error)
19
+ findAllFn func(context.Context, ports.ListFilter) ([]domain.{{pascalName}}, int64, error)
19
20
  findByIDFn func(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
20
21
  updateFn func(context.Context, *domain.{{pascalName}}) error
21
22
  deleteFn func(context.Context, uuid.UUID) error
@@ -28,11 +29,11 @@ func (s *repositoryStub) Create(ctx context.Context, m *domain.{{pascalName}}) e
28
29
  }
29
30
  return s.createFn(ctx, m)
30
31
  }
31
- func (s *repositoryStub) FindAll(ctx context.Context, limit, offset int) ([]domain.{{pascalName}}, error) {
32
+ func (s *repositoryStub) FindAll(ctx context.Context, filter ports.ListFilter) ([]domain.{{pascalName}}, int64, error) {
32
33
  if s.findAllFn == nil {
33
34
  panic("unexpected repository.FindAll call")
34
35
  }
35
- return s.findAllFn(ctx, limit, offset)
36
+ return s.findAllFn(ctx, filter)
36
37
  }
37
38
  func (s *repositoryStub) FindByID(ctx context.Context, id uuid.UUID) (*domain.{{pascalName}}, error) {
38
39
  if s.findByIDFn == nil {
@@ -11,10 +11,26 @@ import (
11
11
  "github.com/google/uuid"
12
12
  )
13
13
 
14
+ // ListFilter is the whole question the list endpoint asks. The zero value is
15
+ // "the first page of everything", so a caller only sets what it narrows by.
16
+ //
17
+ // Put this module's own filters here as fields rather than as arguments: one
18
+ // struct travels handler -> application -> repository, so adding a filter
19
+ // later is a field and not a fourth parameter on three signatures.
20
+ type ListFilter struct {
21
+ // Search is ?q= as shared/pagination parsed it, empty when the caller
22
+ // sent none.
23
+ Search string
24
+ Limit int
25
+ Offset int
26
+ }
27
+
14
28
  // Repository is the complete persistence port for service-style modules.
15
29
  type Repository interface {
16
30
  Create(context.Context, *domain.{{pascalName}}) error
17
- FindAll(context.Context, int, int) ([]domain.{{pascalName}}, error)
31
+ // FindAll answers one page and the total the filter matched, which is what
32
+ // a client needs to draw "page 3 of 12".
33
+ FindAll(context.Context, ListFilter) ([]domain.{{pascalName}}, int64, error)
18
34
  FindByID(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
19
35
  Update(context.Context, *domain.{{pascalName}}) error
20
36
  Delete(context.Context, uuid.UUID) error
@@ -32,7 +48,7 @@ type CommandRepository interface {
32
48
  }
33
49
 
34
50
  type QueryRepository interface {
35
- FindAll(context.Context, int, int) ([]domain.{{pascalName}}, error)
51
+ FindAll(context.Context, ListFilter) ([]domain.{{pascalName}}, int64, error)
36
52
  FindByID(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
37
53
  // go-scaffold:query-repository-interface
38
54
  }