@nakedev/go-scaffold 0.1.3 → 0.3.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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -50
  3. package/dist/commands/auth.js +116 -11
  4. package/dist/commands/create.js +13 -1
  5. package/dist/commands/generate.js +23 -12
  6. package/dist/commands/method.js +94 -17
  7. package/dist/commands/observability.js +114 -0
  8. package/dist/commands/rbac.js +19 -2
  9. package/dist/commands/undo.js +331 -0
  10. package/dist/commands/worker.js +92 -32
  11. package/dist/index.js +368 -64
  12. package/dist/prompts/auth-wizard.js +29 -0
  13. package/dist/prompts/create-wizard.js +5 -2
  14. package/dist/prompts/generate-wizard.js +57 -0
  15. package/dist/prompts/worker-wizard.js +25 -0
  16. package/dist/templates/auth-manifest.js +20 -3
  17. package/dist/templates/create-manifest.js +13 -20
  18. package/dist/templates/module-manifest.js +2 -0
  19. package/dist/templates/observability-manifest.js +24 -0
  20. package/dist/templates/rbac-manifest.js +1 -0
  21. package/dist/templates/worker-manifest.js +23 -6
  22. package/dist/utils/auth-patcher.js +96 -21
  23. package/dist/utils/config.js +58 -10
  24. package/dist/utils/gocheck.js +57 -5
  25. package/dist/utils/golangci-patcher.js +73 -0
  26. package/dist/utils/gomod-patcher.js +53 -0
  27. package/dist/utils/main-patcher.js +58 -4
  28. package/dist/utils/marker-patch.js +125 -3
  29. package/dist/utils/method-patcher.js +97 -18
  30. package/dist/utils/module-location.js +58 -0
  31. package/dist/utils/naming.js +125 -14
  32. package/dist/utils/observability-patcher.js +107 -0
  33. package/dist/utils/openapi-patcher.js +19 -1
  34. package/dist/utils/platform-patcher.js +98 -12
  35. package/dist/utils/rbac-patcher.js +60 -10
  36. package/dist/utils/smoke-run.js +31 -0
  37. package/package.json +11 -4
  38. package/templates/add/auth/internal/app/user/errors.go.hbs +7 -0
  39. package/templates/add/auth/internal/app/user/handler.go.hbs +64 -23
  40. package/templates/add/auth/internal/app/user/jwt.go.hbs +31 -7
  41. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +39 -0
  42. package/templates/add/auth/internal/app/user/model/identity.go.hbs +3 -0
  43. package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +26 -0
  44. package/templates/add/auth/internal/app/user/model/user.go.hbs +9 -1
  45. package/templates/add/auth/internal/app/user/repository.go.hbs +64 -11
  46. package/templates/add/auth/internal/app/user/repository_test.go.hbs +192 -0
  47. package/templates/add/auth/internal/app/user/service.go.hbs +105 -21
  48. package/templates/add/auth/internal/app/user/service_test.go.hbs +81 -2
  49. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +9 -138
  50. package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +144 -0
  51. package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +147 -0
  52. package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +21 -19
  53. package/templates/add/auth/internal/shared/middleware/ratelimit_memory.go.hbs +63 -0
  54. package/templates/add/auth/internal/shared/middleware/ratelimit_redis.go.hbs +32 -0
  55. package/templates/add/auth/migrations/create_auth_tokens.down.sql.hbs +1 -0
  56. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +16 -0
  57. package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -1
  58. package/templates/add/auth/migrations/create_identities.up.sql.hbs +9 -5
  59. package/templates/add/auth/migrations/create_login_throttle.down.sql.hbs +1 -0
  60. package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +10 -0
  61. package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -1
  62. package/templates/add/auth/migrations/create_users.up.sql.hbs +13 -2
  63. package/templates/add/rbac/internal/app/role/dto.go.hbs +8 -3
  64. package/templates/add/rbac/internal/app/role/handler.go.hbs +4 -1
  65. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +3 -0
  66. package/templates/add/rbac/internal/app/role/model/role.go.hbs +4 -0
  67. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +3 -0
  68. package/templates/add/rbac/internal/app/role/repository.go.hbs +12 -11
  69. package/templates/add/rbac/internal/app/role/repository_test.go.hbs +176 -0
  70. package/templates/add/rbac/internal/app/role/service.go.hbs +7 -0
  71. package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +19 -0
  72. package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +1 -1
  73. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +5 -5
  74. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -11
  75. package/templates/add/worker/cmd/worker/main.go.hbs +30 -24
  76. package/templates/add/worker/internal/platform/mail/mail.go.hbs +21 -0
  77. package/templates/add/worker/internal/platform/mail/task.go.hbs +31 -34
  78. package/templates/add/worker/internal/platform/queue/asynq.go.hbs +140 -0
  79. package/templates/add/worker/internal/platform/queue/queue.go.hbs +87 -0
  80. package/templates/add/worker/internal/platform/queue/river.go.hbs +148 -0
  81. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +59 -12
  82. package/templates/create/base/.dockerignore.hbs +13 -0
  83. package/templates/create/base/.env.example.hbs +18 -9
  84. package/templates/create/base/.github/dependabot.yml.hbs +20 -0
  85. package/templates/create/base/.github/workflows/ci.yml.hbs +29 -6
  86. package/templates/create/base/.golangci.yml.hbs +27 -0
  87. package/templates/create/base/AGENTS.md.hbs +14 -12
  88. package/templates/create/base/Dockerfile.hbs +42 -0
  89. package/templates/create/base/Makefile.hbs +52 -16
  90. package/templates/create/base/README.md.hbs +61 -12
  91. package/templates/create/base/cmd/api/main.go.hbs +17 -130
  92. package/templates/create/base/cmd/api/wiring.go.hbs +161 -0
  93. package/templates/create/base/go.mod.hbs +4 -4
  94. package/templates/create/base/internal/platform/database/database.go.hbs +30 -11
  95. package/templates/create/base/internal/shared/config/config.go.hbs +13 -7
  96. package/templates/create/base/internal/shared/pagination/pagination.go.hbs +11 -0
  97. package/templates/create/base/internal/shared/tx/tx.go.hbs +47 -0
  98. package/templates/create/base/redocly.yaml.hbs +21 -0
  99. package/templates/create/features/docs/architecture.md.hbs +32 -11
  100. package/templates/create/features/docs/openapi.yaml.hbs +4 -9
  101. package/templates/create/features/docs/patterns.md.hbs +91 -14
  102. package/templates/create/features/docs/techstack.md.hbs +8 -3
  103. package/templates/generate/module/docs/item.yaml.hbs +3 -3
  104. package/templates/generate/module/dto.go.hbs +8 -1
  105. package/templates/generate/module/errors.go.hbs +5 -0
  106. package/templates/generate/module/field-column.down.sql.hbs +2 -0
  107. package/templates/generate/module/field-column.up.sql.hbs +15 -0
  108. package/templates/generate/module/handler.go.hbs +18 -4
  109. package/templates/generate/module/handler_test.go.hbs +88 -62
  110. package/templates/generate/module/migration.down.sql.hbs +3 -1
  111. package/templates/generate/module/migration.up.sql.hbs +7 -2
  112. package/templates/generate/module/minimal/dto.go.hbs +3 -1
  113. package/templates/generate/module/minimal/handler.go.hbs +8 -2
  114. package/templates/generate/module/minimal/handler_test.go.hbs +5 -112
  115. package/templates/generate/module/minimal/service_test.go.hbs +39 -16
  116. package/templates/generate/module/model/model.go.hbs +16 -0
  117. package/templates/generate/module/permission.up.sql.hbs +3 -1
  118. package/templates/generate/module/repository.go.hbs +60 -6
  119. package/templates/generate/module/repository_test.go.hbs +109 -0
  120. package/templates/generate/module/service.go.hbs +15 -4
  121. package/templates/generate/module/service_test.go.hbs +113 -17
  122. package/dist/commands/remove.js +0 -89
  123. package/templates/add/worker/internal/platform/queue/client.go.hbs +0 -31
  124. package/templates/add/worker/internal/platform/queue/server.go.hbs +0 -68
@@ -0,0 +1,109 @@
1
+ package {{pkg}}
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "os"
7
+ "testing"
8
+
9
+ "{{goModule}}/internal/app/{{modulePath}}/model"
10
+
11
+ "github.com/google/uuid"
12
+ "gorm.io/driver/postgres"
13
+ "gorm.io/gorm"
14
+ )
15
+
16
+ // repositoryDBForTest expects the test database schema to come from the same
17
+ // versioned SQL migrations used in production. Unit tests stay database-free;
18
+ // CI sets REQUIRE_TEST_DB=true so an unavailable/unmigrated database fails
19
+ // instead of becoming a false-green skip.
20
+ func repositoryDBForTest(t *testing.T) *gorm.DB {
21
+ t.Helper()
22
+ dsn := os.Getenv("TEST_DB_DSN")
23
+ if dsn == "" {
24
+ if os.Getenv("REQUIRE_TEST_DB") == "true" {
25
+ t.Fatal("TEST_DB_DSN is required when REQUIRE_TEST_DB=true")
26
+ }
27
+ t.Skip("repository integration test skipped: set TEST_DB_DSN to a migrated PostgreSQL database")
28
+ }
29
+
30
+ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{TranslateError: true})
31
+ if err != nil {
32
+ if os.Getenv("REQUIRE_TEST_DB") == "true" {
33
+ t.Fatalf("open required test database: %v", err)
34
+ }
35
+ t.Skipf("repository integration test skipped: %v", err)
36
+ }
37
+
38
+ sqlDB, err := db.DB()
39
+ if err != nil {
40
+ t.Fatalf("get SQL database handle: %v", err)
41
+ }
42
+ if err := sqlDB.Ping(); err != nil {
43
+ if os.Getenv("REQUIRE_TEST_DB") == "true" {
44
+ t.Fatalf("ping required test database: %v", err)
45
+ }
46
+ t.Skipf("repository integration test skipped: %v", err)
47
+ }
48
+
49
+ tx := db.Begin()
50
+ if tx.Error != nil {
51
+ t.Fatalf("begin test transaction: %v", tx.Error)
52
+ }
53
+ t.Cleanup(func() {
54
+ if err := tx.Rollback().Error; err != nil {
55
+ t.Errorf("rollback test transaction: %v", err)
56
+ }
57
+ })
58
+ return tx
59
+ }
60
+
61
+ func TestRepository_CreateFindDelete(t *testing.T) {
62
+ repo := NewRepository(repositoryDBForTest(t))
63
+ ctx := context.Background()
64
+ id := uuid.New()
65
+ item := &model.{{pascalName}}{ID: id}
66
+
67
+ if err := repo.Create(ctx, item); err != nil {
68
+ t.Fatalf("create: %v", err)
69
+ }
70
+ found, err := repo.FindByID(ctx, id)
71
+ if err != nil {
72
+ t.Fatalf("find by id: %v", err)
73
+ }
74
+ if found.ID != id {
75
+ t.Fatalf("want id %s, got %s", id, found.ID)
76
+ }
77
+ if err := repo.Delete(ctx, id); err != nil {
78
+ t.Fatalf("delete: %v", err)
79
+ }
80
+ }
81
+
82
+ // Two clients both hold version 1; the second save must be refused instead of
83
+ // overwriting the first. Runs against the real database because the guard is
84
+ // a WHERE clause — a stub would only be testing itself.
85
+ func TestRepository_Update_RefusesStaleVersion(t *testing.T) {
86
+ repo := NewRepository(repositoryDBForTest(t))
87
+ ctx := context.Background()
88
+ item := &model.{{pascalName}}{ID: uuid.New()}
89
+
90
+ if err := repo.Create(ctx, item); err != nil {
91
+ t.Fatalf("create: %v", err)
92
+ }
93
+
94
+ first := &model.{{pascalName}}{ID: item.ID, Version: item.Version}
95
+ if err := repo.Update(ctx, first); err != nil {
96
+ t.Fatalf("first update: %v", err)
97
+ }
98
+ if first.Version != item.Version+1 {
99
+ t.Fatalf("want version bumped to %d, got %d", item.Version+1, first.Version)
100
+ }
101
+
102
+ stale := &model.{{pascalName}}{ID: item.ID, Version: item.Version}
103
+ if err := repo.Update(ctx, stale); !errors.Is(err, ErrStaleVersion) {
104
+ t.Fatalf("want ErrStaleVersion for the second writer, got %v", err)
105
+ }
106
+ if stale.Version != item.Version {
107
+ t.Fatalf("a refused update must leave the caller's version alone, got %d", stale.Version)
108
+ }
109
+ }
@@ -33,8 +33,9 @@ func NewService(repo repository) *Service {
33
33
  return &Service{repo: repo}
34
34
  }
35
35
 
36
- func (s *Service) Create(ctx context.Context) (*model.{{pascalName}}, error) {
37
- // TODO: accept and set real fields from createInput
36
+ func (s *Service) Create(ctx context.Context, in createInput) (*model.{{pascalName}}, error) {
37
+ // TODO: set real fields from in
38
+ _ = in
38
39
  m := &model.{{pascalName}}{ID: id.New()}
39
40
  if err := s.repo.Create(ctx, m); err != nil {
40
41
  if dberr.IsDuplicate(err) {
@@ -61,13 +62,20 @@ func (s *Service) Get(ctx context.Context, id uuid.UUID) (*model.{{pascalName}},
61
62
  return m, nil
62
63
  }
63
64
 
64
- func (s *Service) Update(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
65
+ func (s *Service) Update(ctx context.Context, id uuid.UUID, in updateInput) (*model.{{pascalName}}, error) {
65
66
  m, err := s.repo.FindByID(ctx, id)
66
67
  if err != nil {
67
68
  return nil, wrapFindErr(err)
68
69
  }
69
- // TODO: apply real fields from updateInput before saving
70
+ // TODO: apply real fields from in before saving
71
+ // The version the client read comes from the request, not from the row we
72
+ // just loaded — comparing the row against itself would always succeed and
73
+ // defeat the check.
74
+ m.Version = in.Version
70
75
  if err := s.repo.Update(ctx, m); err != nil {
76
+ if errors.Is(err, ErrStaleVersion) {
77
+ return nil, errStale()
78
+ }
71
79
  if dberr.IsDuplicate(err) {
72
80
  return nil, errConflict()
73
81
  }
@@ -78,6 +86,9 @@ func (s *Service) Update(ctx context.Context, id uuid.UUID) (*model.{{pascalName
78
86
 
79
87
  func (s *Service) Delete(ctx context.Context, id uuid.UUID) error {
80
88
  if err := s.repo.Delete(ctx, id); err != nil {
89
+ if errors.Is(err, gorm.ErrRecordNotFound) {
90
+ return errNotFound()
91
+ }
81
92
  // deleting a {{pkg}} that's still referenced elsewhere → FK RESTRICT trips = 409, not 500
82
93
  if dberr.IsForeignKey(err) {
83
94
  return errHasReferences()
@@ -12,24 +12,54 @@ import (
12
12
  "gorm.io/gorm"
13
13
  )
14
14
 
15
- // fakeRepo = mock of the repository interface, so the service can be tested without a DB
16
- type fakeRepo struct {
17
- err error
18
- m *model.{{pascalName}}
15
+ // repositoryStub exposes one function per dependency operation. Tests configure
16
+ // only the calls they expect; an unexpected call panics instead of silently
17
+ // returning a shared zero value/error that can hide broken orchestration.
18
+ type repositoryStub struct {
19
+ createFn func(context.Context, *model.{{pascalName}}) error
20
+ findAllFn func(context.Context, int, int) ([]model.{{pascalName}}, error)
21
+ findByIDFn func(context.Context, uuid.UUID) (*model.{{pascalName}}, error)
22
+ updateFn func(context.Context, *model.{{pascalName}}) error
23
+ deleteFn func(context.Context, uuid.UUID) error
24
+ // go-scaffold:repository-stub-fields
19
25
  }
20
26
 
21
- func (f *fakeRepo) Create(context.Context, *model.{{pascalName}}) error { return f.err }
22
- func (f *fakeRepo) FindAll(context.Context, int, int) ([]model.{{pascalName}}, error) { return nil, f.err }
23
- func (f *fakeRepo) FindByID(context.Context, uuid.UUID) (*model.{{pascalName}}, error) {
24
- if f.err != nil {
25
- return nil, f.err
27
+ func (s *repositoryStub) Create(ctx context.Context, m *model.{{pascalName}}) error {
28
+ if s.createFn == nil {
29
+ panic("unexpected repository.Create call")
26
30
  }
27
- return f.m, nil
31
+ return s.createFn(ctx, m)
28
32
  }
29
- func (f *fakeRepo) Update(context.Context, *model.{{pascalName}}) error { return f.err }
30
- func (f *fakeRepo) Delete(context.Context, uuid.UUID) error { return f.err }
31
33
 
32
- // go-scaffold:fake-repo-methods
34
+ func (s *repositoryStub) FindAll(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error) {
35
+ if s.findAllFn == nil {
36
+ panic("unexpected repository.FindAll call")
37
+ }
38
+ return s.findAllFn(ctx, limit, offset)
39
+ }
40
+
41
+ func (s *repositoryStub) FindByID(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
42
+ if s.findByIDFn == nil {
43
+ panic("unexpected repository.FindByID call")
44
+ }
45
+ return s.findByIDFn(ctx, id)
46
+ }
47
+
48
+ func (s *repositoryStub) Update(ctx context.Context, m *model.{{pascalName}}) error {
49
+ if s.updateFn == nil {
50
+ panic("unexpected repository.Update call")
51
+ }
52
+ return s.updateFn(ctx, m)
53
+ }
54
+
55
+ func (s *repositoryStub) Delete(ctx context.Context, id uuid.UUID) error {
56
+ if s.deleteFn == nil {
57
+ panic("unexpected repository.Delete call")
58
+ }
59
+ return s.deleteFn(ctx, id)
60
+ }
61
+
62
+ // go-scaffold:repository-stub-methods
33
63
 
34
64
  func status(t *testing.T, err error) int {
35
65
  t.Helper()
@@ -41,25 +71,91 @@ func status(t *testing.T, err error) int {
41
71
  }
42
72
 
43
73
  func TestService_Create_Duplicate(t *testing.T) {
44
- svc := NewService(&fakeRepo{err: gorm.ErrDuplicatedKey})
45
- _, err := svc.Create(context.Background())
74
+ repo := &repositoryStub{
75
+ createFn: func(context.Context, *model.{{pascalName}}) error {
76
+ return gorm.ErrDuplicatedKey
77
+ },
78
+ }
79
+ svc := NewService(repo)
80
+
81
+ _, err := svc.Create(context.Background(), createInput{})
82
+
46
83
  if got := status(t, err); got != 409 {
47
84
  t.Fatalf("want 409 conflict, got %d", got)
48
85
  }
49
86
  }
50
87
 
51
88
  func TestService_Get_NotFound(t *testing.T) {
52
- svc := NewService(&fakeRepo{err: gorm.ErrRecordNotFound})
89
+ repo := &repositoryStub{
90
+ findByIDFn: func(context.Context, uuid.UUID) (*model.{{pascalName}}, error) {
91
+ return nil, gorm.ErrRecordNotFound
92
+ },
93
+ }
94
+ svc := NewService(repo)
95
+
53
96
  _, err := svc.Get(context.Background(), uuid.New())
97
+
54
98
  if got := status(t, err); got != 404 {
55
99
  t.Fatalf("want 404, got %d", got)
56
100
  }
57
101
  }
58
102
 
59
103
  func TestService_List_DBError_Becomes500(t *testing.T) {
60
- svc := NewService(&fakeRepo{err: errors.New("connection refused")})
104
+ repo := &repositoryStub{
105
+ findAllFn: func(context.Context, int, int) ([]model.{{pascalName}}, error) {
106
+ return nil, errors.New("connection refused")
107
+ },
108
+ }
109
+ svc := NewService(repo)
110
+
61
111
  _, err := svc.List(context.Background(), 20, 0)
112
+
62
113
  if got := status(t, err); got != 500 {
63
114
  t.Fatalf("want 500, got %d", got)
64
115
  }
65
116
  }
117
+
118
+ // The check that stops one user's save from silently erasing another's: the
119
+ // repository reports the row moved on, the client must see 409 rather than a
120
+ // 500 or, worse, a success.
121
+ func TestService_Update_StaleVersion(t *testing.T) {
122
+ repo := &repositoryStub{
123
+ findByIDFn: func(_ context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
124
+ return &model.{{pascalName}}{ID: id, Version: 7}, nil
125
+ },
126
+ updateFn: func(context.Context, *model.{{pascalName}}) error {
127
+ return ErrStaleVersion
128
+ },
129
+ }
130
+ svc := NewService(repo)
131
+
132
+ _, err := svc.Update(context.Background(), uuid.New(), updateInput{Version: 3})
133
+
134
+ if got := status(t, err); got != 409 {
135
+ t.Fatalf("want 409 stale, got %d", got)
136
+ }
137
+ }
138
+
139
+ // The version compared against the database has to be the one the client sent,
140
+ // not the one just read back — comparing the row against itself would always
141
+ // match and the guard would never fire.
142
+ func TestService_Update_SendsClientVersion(t *testing.T) {
143
+ var seen int
144
+ repo := &repositoryStub{
145
+ findByIDFn: func(_ context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
146
+ return &model.{{pascalName}}{ID: id, Version: 7}, nil
147
+ },
148
+ updateFn: func(_ context.Context, m *model.{{pascalName}}) error {
149
+ seen = m.Version
150
+ return nil
151
+ },
152
+ }
153
+ svc := NewService(repo)
154
+
155
+ if _, err := svc.Update(context.Background(), uuid.New(), updateInput{Version: 3}); err != nil {
156
+ t.Fatalf("update: %v", err)
157
+ }
158
+ if seen != 3 {
159
+ t.Fatalf("want the client's version 3 to reach the repository, got %d", seen)
160
+ }
161
+ }
@@ -1,89 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.removeModule = removeModule;
7
- const path_1 = __importDefault(require("path"));
8
- const fs_extra_1 = __importDefault(require("fs-extra"));
9
- const picocolors_1 = __importDefault(require("picocolors"));
10
- const prompts_1 = require("@inquirer/prompts");
11
- const config_1 = require("../utils/config");
12
- const naming_1 = require("../utils/naming");
13
- const main_patcher_1 = require("../utils/main-patcher");
14
- const openapi_patcher_1 = require("../utils/openapi-patcher");
15
- const template_renderer_1 = require("../utils/template-renderer");
16
- const generate_wizard_1 = require("../prompts/generate-wizard");
17
- // removeModule is the inverse of generateModule: deletes the domain package and
18
- // pulls its wiring back out of main.go / openapi.yaml / migrations, so dropping
19
- // a domain is one command instead of hand-editing 3+ files (the error-prone
20
- // path that produced the duplicate-registration bug in the first place).
21
- async function removeModule(rawName, opts, projectDir = process.cwd()) {
22
- const config = (0, config_1.readConfig)(projectDir);
23
- const naming = (0, naming_1.resolveModuleNaming)(rawName ?? (await (0, generate_wizard_1.promptModuleName)()));
24
- const modulePath = naming.pkg;
25
- const moduleDir = path_1.default.join(projectDir, "internal", "app", modulePath);
26
- if (!fs_extra_1.default.existsSync(moduleDir)) {
27
- throw new Error(`module "${naming.pkg}" not found at internal/app/${modulePath} — nothing to remove`);
28
- }
29
- if (!opts.yes) {
30
- const ok = await (0, prompts_1.confirm)({
31
- message: `Remove module "${naming.pkg}"? Deletes internal/app/${modulePath}/, its migration, and un-wires main.go/openapi.yaml`,
32
- default: false,
33
- });
34
- if (!ok)
35
- throw new Error("removal cancelled");
36
- }
37
- // detect --auth/--permission from the generated handler.go itself (not
38
- // stored anywhere else) — unpatchMainGo needs the exact same flags used at
39
- // generate-time to reconstruct the identical line it's removing.
40
- const handlerGoPath = path_1.default.join(moduleDir, "handler.go");
41
- let auth;
42
- let permission;
43
- if (fs_extra_1.default.existsSync(handlerGoPath)) {
44
- const handlerContent = fs_extra_1.default.readFileSync(handlerGoPath, "utf8");
45
- auth = /jwtSecret\s+string/.test(handlerContent);
46
- permission = handlerContent.match(/h\.authz\.Require\("([^"]+)"\)/)?.[1];
47
- }
48
- // 1. the domain package
49
- fs_extra_1.default.removeSync(moduleDir);
50
- // 2. main.go wiring
51
- (0, main_patcher_1.unpatchMainGo)(path_1.default.join(projectDir, "cmd", "api", "main.go"), {
52
- goModule: config.goModule,
53
- modulePath,
54
- pkg: naming.pkg,
55
- pascalName: naming.pascalName,
56
- auth,
57
- permission,
58
- });
59
- // 3. openapi index + per-module docs
60
- const openapiPath = path_1.default.join(projectDir, "docs", "openapi.yaml");
61
- if (fs_extra_1.default.existsSync(openapiPath)) {
62
- (0, openapi_patcher_1.unpatchOpenapiIndex)(openapiPath, naming, config.apiPrefix);
63
- fs_extra_1.default.removeSync(path_1.default.join(projectDir, "docs", naming.plural));
64
- }
65
- // 4. migration files (up + down) — including the --permission seed
66
- // migration, if this module was generated with one.
67
- const migrationsDir = path_1.default.join(projectDir, "migrations");
68
- const removedMigrations = [];
69
- if (fs_extra_1.default.existsSync(migrationsDir)) {
70
- for (const f of fs_extra_1.default.readdirSync(migrationsDir)) {
71
- if (f.endsWith(`_create_${naming.plural}.up.sql`) ||
72
- f.endsWith(`_create_${naming.plural}.down.sql`) ||
73
- f.endsWith(`_add_${naming.plural}_permission.up.sql`) ||
74
- f.endsWith(`_add_${naming.plural}_permission.down.sql`)) {
75
- fs_extra_1.default.removeSync(path_1.default.join(migrationsDir, f));
76
- removedMigrations.push(f);
77
- }
78
- }
79
- }
80
- (0, template_renderer_1.gofmtTree)(projectDir);
81
- console.log(picocolors_1.default.green(`\nremoved module "${naming.pkg}"`));
82
- console.log(` deleted internal/app/${modulePath}/`);
83
- console.log(` un-wired cmd/api/main.go`);
84
- if (fs_extra_1.default.existsSync(openapiPath))
85
- console.log(` un-wired docs/openapi.yaml + deleted docs/${naming.plural}/`);
86
- if (removedMigrations.length)
87
- console.log(` deleted ${removedMigrations.join(", ")}`);
88
- console.log(picocolors_1.default.yellow(`\nnote: the ${naming.plural} table (if migrated) is untouched — drop it yourself, or add a down migration`));
89
- }
@@ -1,31 +0,0 @@
1
- package queue
2
-
3
- import (
4
- "github.com/hibiken/asynq"
5
- )
6
-
7
- // Client enqueues tasks to Redis. Created by cmd/api; each domain or platform
8
- // package defines its own task types and calls Client.Enqueue.
9
- type Client struct {
10
- inner *asynq.Client
11
- }
12
-
13
- func NewClient(redisURL string) (*Client, error) {
14
- opt, err := asynq.ParseRedisURI(redisURL)
15
- if err != nil {
16
- return nil, err
17
- }
18
- return &Client{inner: asynq.NewClient(opt)}, nil
19
- }
20
-
21
- func (c *Client) Enqueue(task *asynq.Task, opts ...asynq.Option) (string, error) {
22
- info, err := c.inner.Enqueue(task, opts...)
23
- if err != nil {
24
- return "", err
25
- }
26
- return info.ID, nil
27
- }
28
-
29
- func (c *Client) Close() error {
30
- return c.inner.Close()
31
- }
@@ -1,68 +0,0 @@
1
- package queue
2
-
3
- import (
4
- "context"
5
- "fmt"
6
- "log/slog"
7
-
8
- "github.com/hibiken/asynq"
9
- )
10
-
11
- // Server runs the background worker. Created by cmd/worker; handlers register
12
- // their task types on the mux before Start is called.
13
- type Server struct {
14
- inner *asynq.Server
15
- mux *asynq.ServeMux
16
- }
17
-
18
- // slogAdapter wraps *slog.Logger to implement asynq.Logger. Asynq calls these
19
- // like the stdlib `log` package (a plain message, sometimes several args
20
- // meant to be concatenated) — fmt.Sprint joins them into one message string
21
- // rather than passing them as slog key-value pairs, which would otherwise
22
- // misinterpret a lone message argument as a key with no value.
23
- type slogAdapter struct {
24
- inner *slog.Logger
25
- }
26
-
27
- func (a *slogAdapter) Debug(args ...interface{}) { a.inner.Debug(fmt.Sprint(args...)) }
28
- func (a *slogAdapter) Info(args ...interface{}) { a.inner.Info(fmt.Sprint(args...)) }
29
- func (a *slogAdapter) Warn(args ...interface{}) { a.inner.Warn(fmt.Sprint(args...)) }
30
- func (a *slogAdapter) Error(args ...interface{}) { a.inner.Error(fmt.Sprint(args...)) }
31
- func (a *slogAdapter) Fatal(args ...interface{}) { a.inner.Error(fmt.Sprint(args...)) }
32
-
33
- func NewServer(redisURL string) (*Server, error) {
34
- opt, err := asynq.ParseRedisURI(redisURL)
35
- if err != nil {
36
- return nil, err
37
- }
38
- srv := &Server{
39
- inner: asynq.NewServer(opt, asynq.Config{
40
- Concurrency: 10,
41
- Logger: &slogAdapter{inner: slog.Default()},
42
- }),
43
- mux: asynq.NewServeMux(),
44
- }
45
- // Global error handler for tasks that panic or return a non-Retryable error.
46
- srv.mux.Use(func(next asynq.Handler) asynq.Handler {
47
- return asynq.HandlerFunc(func(ctx context.Context, task *asynq.Task) error {
48
- err := next.ProcessTask(ctx, task)
49
- if err != nil {
50
- slog.Error("task failed", "type", task.Type(), "error", err)
51
- }
52
- return err
53
- })
54
- })
55
- return srv, nil
56
- }
57
-
58
- func (s *Server) Handle(pattern string, handler asynq.Handler) {
59
- s.mux.Handle(pattern, handler)
60
- }
61
-
62
- func (s *Server) Start() error {
63
- return s.inner.Start(s.mux)
64
- }
65
-
66
- func (s *Server) Shutdown() {
67
- s.inner.Shutdown()
68
- }