@nakedev/go-scaffold 0.1.2 → 0.1.3

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 (108) hide show
  1. package/README.md +75 -0
  2. package/dist/commands/auth.js +129 -0
  3. package/dist/commands/create.js +3 -2
  4. package/dist/commands/generate.js +59 -1
  5. package/dist/commands/method.js +3 -0
  6. package/dist/commands/migration.js +34 -0
  7. package/dist/commands/rbac.js +103 -0
  8. package/dist/commands/remove.js +19 -2
  9. package/dist/commands/worker.js +75 -0
  10. package/dist/index.js +66 -3
  11. package/dist/prompts/create-wizard.js +6 -1
  12. package/dist/prompts/generate-wizard.js +8 -0
  13. package/dist/templates/auth-manifest.js +19 -0
  14. package/dist/templates/create-manifest.js +25 -0
  15. package/dist/templates/rbac-manifest.js +17 -0
  16. package/dist/templates/worker-manifest.js +12 -0
  17. package/dist/utils/auth-patcher.js +96 -0
  18. package/dist/utils/gocheck.js +65 -0
  19. package/dist/utils/main-patcher.js +8 -1
  20. package/dist/utils/migrations.js +30 -8
  21. package/dist/utils/openapi-patcher.js +16 -0
  22. package/dist/utils/platform-patcher.js +59 -0
  23. package/dist/utils/rbac-patcher.js +277 -0
  24. package/dist/utils/version.js +24 -0
  25. package/package.json +2 -2
  26. package/templates/add/auth/cmd/seed/main.go.hbs +76 -0
  27. package/templates/add/auth/docs/forgot-password.yaml.hbs +19 -0
  28. package/templates/add/auth/docs/google-callback.yaml.hbs +22 -0
  29. package/templates/add/auth/docs/google-login.yaml.hbs +7 -0
  30. package/templates/add/auth/docs/login.yaml.hbs +19 -0
  31. package/templates/add/auth/docs/logout.yaml.hbs +8 -0
  32. package/templates/add/auth/docs/refresh.yaml.hbs +15 -0
  33. package/templates/add/auth/docs/register.yaml.hbs +19 -0
  34. package/templates/add/auth/docs/reset-password.yaml.hbs +16 -0
  35. package/templates/add/auth/docs/schemas.yaml.hbs +58 -0
  36. package/templates/add/auth/docs/users-me-logout-all.yaml.hbs +9 -0
  37. package/templates/add/auth/docs/users-me-resend-verification.yaml.hbs +10 -0
  38. package/templates/add/auth/docs/users-me.yaml.hbs +12 -0
  39. package/templates/add/auth/docs/verify-email.yaml.hbs +16 -0
  40. package/templates/add/auth/internal/app/user/dto.go.hbs +77 -0
  41. package/templates/add/auth/internal/app/user/errors.go.hbs +36 -0
  42. package/templates/add/auth/internal/app/user/handler.go.hbs +235 -0
  43. package/templates/add/auth/internal/app/user/jwt.go.hbs +84 -0
  44. package/templates/add/auth/internal/app/user/model/identity.go.hbs +28 -0
  45. package/templates/add/auth/internal/app/user/model/user.go.hbs +22 -0
  46. package/templates/add/auth/internal/app/user/repository.go.hbs +84 -0
  47. package/templates/add/auth/internal/app/user/service.go.hbs +447 -0
  48. package/templates/add/auth/internal/app/user/service_test.go.hbs +237 -0
  49. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +159 -0
  50. package/templates/add/auth/internal/shared/middleware/auth.go.hbs +64 -0
  51. package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +44 -0
  52. package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -0
  53. package/templates/add/auth/migrations/create_identities.up.sql.hbs +11 -0
  54. package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -0
  55. package/templates/add/auth/migrations/create_users.up.sql.hbs +9 -0
  56. package/templates/add/rbac/docs/permissions.yaml.hbs +24 -0
  57. package/templates/add/rbac/docs/role-permissions.yaml.hbs +31 -0
  58. package/templates/add/rbac/docs/role.yaml.hbs +17 -0
  59. package/templates/add/rbac/docs/roles.yaml.hbs +43 -0
  60. package/templates/add/rbac/docs/schemas.yaml.hbs +37 -0
  61. package/templates/add/rbac/docs/user-set-role.yaml.hbs +23 -0
  62. package/templates/add/rbac/docs/user.yaml.hbs +16 -0
  63. package/templates/add/rbac/docs/users.yaml.hbs +23 -0
  64. package/templates/add/rbac/internal/app/role/dto.go.hbs +40 -0
  65. package/templates/add/rbac/internal/app/role/errors.go.hbs +39 -0
  66. package/templates/add/rbac/internal/app/role/handler.go.hbs +101 -0
  67. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +9 -0
  68. package/templates/add/rbac/internal/app/role/model/role.go.hbs +18 -0
  69. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +8 -0
  70. package/templates/add/rbac/internal/app/role/repository.go.hbs +96 -0
  71. package/templates/add/rbac/internal/app/role/service.go.hbs +210 -0
  72. package/templates/add/rbac/internal/app/role/service_test.go.hbs +119 -0
  73. package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +88 -0
  74. package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +88 -0
  75. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +15 -0
  76. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +35 -0
  77. package/templates/add/worker/cmd/worker/main.go.hbs +77 -0
  78. package/templates/add/worker/internal/platform/cache/redis.go.hbs +18 -0
  79. package/templates/add/worker/internal/platform/mail/mail.go.hbs +48 -0
  80. package/templates/add/worker/internal/platform/mail/task.go.hbs +52 -0
  81. package/templates/add/worker/internal/platform/queue/client.go.hbs +31 -0
  82. package/templates/add/worker/internal/platform/queue/server.go.hbs +68 -0
  83. package/templates/create/base/.env.example.hbs +20 -0
  84. package/templates/create/base/.github/workflows/ci.yml.hbs +4 -2
  85. package/templates/create/base/.gitignore.hbs +2 -0
  86. package/templates/create/base/Makefile.hbs +30 -5
  87. package/templates/create/base/README.md.hbs +36 -8
  88. package/templates/create/base/cmd/api/main.go.hbs +26 -1
  89. package/templates/create/base/internal/platform/database/database.go.hbs +53 -0
  90. package/templates/create/base/internal/shared/config/config.go.hbs +43 -1
  91. package/templates/create/base/internal/shared/middleware/cors.go.hbs +29 -0
  92. package/templates/create/base/internal/shared/middleware/error.go.hbs +13 -1
  93. package/templates/create/base/migrations/embed.go.hbs +15 -0
  94. package/templates/create/features/docs/architecture.md.hbs +22 -0
  95. package/templates/create/features/docs/common/responses.yaml.hbs +15 -0
  96. package/templates/create/features/docs/observability/metrics.yaml.hbs +12 -0
  97. package/templates/create/features/docs/openapi.yaml.hbs +13 -0
  98. package/templates/create/features/docs/techstack.md.hbs +3 -0
  99. package/templates/create/features/observability/middleware/metrics.go.hbs +41 -0
  100. package/templates/create/features/observability/middleware/tracing.go.hbs +46 -0
  101. package/templates/create/features/observability/platform/telemetry/tracing.go.hbs +130 -0
  102. package/templates/generate/module/handler.go.hbs +20 -3
  103. package/templates/generate/module/handler_test.go.hbs +49 -6
  104. package/templates/generate/module/minimal/handler.go.hbs +21 -3
  105. package/templates/generate/module/minimal/handler_test.go.hbs +53 -6
  106. package/templates/generate/module/permission.down.sql.hbs +5 -0
  107. package/templates/generate/module/permission.up.sql.hbs +4 -0
  108. package/dist/utils/module-paths.js +0 -33
@@ -2,24 +2,37 @@ package {{pkg}}
2
2
 
3
3
  import (
4
4
  "bytes"
5
+ {{#if permission}}
6
+ "context"
7
+ {{/if}}
5
8
  "encoding/json"
6
9
  "net/http"
7
10
  "net/http/httptest"
8
11
  "os"
9
12
  "sync"
10
13
  "testing"
14
+ {{#if auth}}
15
+ "time"
16
+ {{/if}}
11
17
 
12
18
  "{{goModule}}/internal/app/{{modulePath}}/model"
13
19
  "{{goModule}}/internal/shared/middleware"
14
20
 
15
21
  "github.com/gin-gonic/gin"
22
+ {{#if auth}}
23
+ "github.com/golang-jwt/jwt/v5"
24
+ {{/if}}
16
25
  "github.com/google/uuid"
17
26
  "gorm.io/driver/postgres"
18
27
  "gorm.io/gorm"
19
28
  )
20
29
 
21
- // integration test backed by real Postgres (same engine as prod, no sqlite) — skips if the DB isn't reachable
22
- // start the DB: docker compose up -d (override with TEST_DB_DSN)
30
+ // integration test backed by real Postgres (same engine as prod, no sqlite) — skips if the DB isn't reachable.
31
+ // Runs against its own {{dbName}}_test database, never the one DB_DSN points at: the
32
+ // harness does DropTable+AutoMigrate on every run, which would otherwise wipe the
33
+ // schema `make migrate-up` built in your dev DB (FK constraints, seed data and all).
34
+ // Create it once with `make db-create DB_NAME={{dbName}}_test`, or point TEST_DB_DSN
35
+ // somewhere else.
23
36
  var (
24
37
  testDBOnce sync.Once
25
38
  testDB *gorm.DB
@@ -31,7 +44,7 @@ func dbForTest(t *testing.T) *gorm.DB {
31
44
  testDBOnce.Do(func() {
32
45
  dsn := os.Getenv("TEST_DB_DSN")
33
46
  if dsn == "" {
34
- dsn = "postgres://postgres:postgres@localhost:5432/{{dbName}}?sslmode=disable"
47
+ dsn = "postgres://postgres:postgres@localhost:5432/{{dbName}}_test?sslmode=disable"
35
48
  }
36
49
  if testDB, testDBErr = gorm.Open(postgres.Open(dsn), &gorm.Config{TranslateError: true}); testDBErr == nil {
37
50
  // drop first — AutoMigrate can't change an existing column's type, always start from a fresh schema
@@ -40,7 +53,7 @@ func dbForTest(t *testing.T) *gorm.DB {
40
53
  }
41
54
  })
42
55
  if testDBErr != nil {
43
- t.Skipf("postgres not ready (docker compose up -d, or set TEST_DB_DSN): %v", testDBErr)
56
+ t.Skipf("postgres not ready (make db-create DB_NAME={{dbName}}_test, or set TEST_DB_DSN): %v", testDBErr)
44
57
  }
45
58
  return testDB
46
59
  }
@@ -52,14 +65,44 @@ func setup(t *testing.T) *gin.Engine {
52
65
  tx := dbForTest(t).Begin()
53
66
  t.Cleanup(func() { tx.Rollback() })
54
67
  r := gin.New()
55
- r.Use(middleware.RequestID(), middleware.Error())
56
- NewHandler(NewService(NewRepository(tx))).Register(r)
68
+ r.Use(middleware.RequestID(), middleware.Error(true))
69
+ {{#if permission}}
70
+ // stub authz: always grants "{{permission}}" — this test is about the
71
+ // CRUD handler, not re-proving RBAC (that's covered by the role
72
+ // package's own tests), so the resolver doesn't need a real DB lookup.
73
+ authz := middleware.NewAuthz(func(_ context.Context, _ string) (map[string]struct{}, error) {
74
+ return map[string]struct{}{"{{permission}}": {}}, nil
75
+ }, time.Minute)
76
+ {{/if}}
77
+ NewHandler(NewService(NewRepository(tx)){{#if auth}}, testJWTSecret{{/if}}{{#if permission}}, authz{{/if}}).Register(r)
57
78
  return r
58
79
  }
59
80
 
81
+ {{#if auth}}
82
+ const testJWTSecret = "test-secret"
83
+
84
+ // authHeader mints a valid access token signed with testJWTSecret — the
85
+ // route's own RequireAuth(testJWTSecret) (wired in via NewHandler above)
86
+ // verifies against the same secret, so this round-trips for real.
87
+ func authHeader() string {
88
+ claims := jwt.MapClaims{
89
+ "typ": "access",
90
+ "sub": uuid.NewString(),
91
+ "role": "staff",
92
+ "exp": time.Now().Add(time.Hour).Unix(),
93
+ "iat": time.Now().Unix(),
94
+ }
95
+ tok, _ := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(testJWTSecret))
96
+ return "Bearer " + tok
97
+ }
98
+
99
+ {{/if}}
60
100
  func do(r *gin.Engine, method, path, body string) *httptest.ResponseRecorder {
61
101
  req := httptest.NewRequest(method, path, bytes.NewBufferString(body))
62
102
  req.Header.Set("Content-Type", "application/json")
103
+ {{#if auth}}
104
+ req.Header.Set("Authorization", authHeader())
105
+ {{/if}}
63
106
  w := httptest.NewRecorder()
64
107
  r.ServeHTTP(w, req)
65
108
  return w
@@ -1,22 +1,40 @@
1
1
  package {{pkg}}
2
2
 
3
3
  import (
4
+ {{#if auth}}
5
+ "{{goModule}}/internal/shared/middleware"
6
+
7
+ {{/if}}
4
8
  "github.com/gin-gonic/gin"
5
9
  )
6
10
 
7
11
  // Handler = delivery for {{pkg}} (parses HTTP, calls the service, attaches errors for the middleware to render)
8
12
  type Handler struct {
9
13
  svc *Service
14
+ {{#if auth}}
15
+ jwtSecret string
16
+ {{/if}}
17
+ {{#if permission}}
18
+ authz *middleware.Authz
19
+ {{/if}}
10
20
  }
11
21
 
12
- func NewHandler(svc *Service) *Handler {
13
- return &Handler{svc: svc}
22
+ func NewHandler(svc *Service{{#if auth}}, jwtSecret string{{/if}}{{#if permission}}, authz *middleware.Authz{{/if}}) *Handler {
23
+ return &Handler{
24
+ svc: svc,
25
+ {{#if auth}}
26
+ jwtSecret: jwtSecret,
27
+ {{/if}}
28
+ {{#if permission}}
29
+ authz: authz,
30
+ {{/if}}
31
+ }
14
32
  }
15
33
 
16
34
  // Register wires {{pkg}}'s routes onto the router group (takes an IRouter so it can be nested under /v1)
17
35
  // minimal module: no routes yet — add them with `go-scaffold generate method {{pkg}} <name> --type ...`
18
36
  func (h *Handler) Register(rg gin.IRouter) {
19
- g := rg.Group("/{{plural}}")
37
+ g := rg.Group("/{{plural}}"{{#if auth}}, middleware.RequireAuth(h.jwtSecret){{/if}}{{#if permission}}, h.authz.Require("{{permission}}"){{/if}})
20
38
  _ = g
21
39
  // go-scaffold:handler-routes
22
40
  }
@@ -2,10 +2,16 @@ package {{pkg}}
2
2
 
3
3
  import (
4
4
  "bytes"
5
+ {{#if permission}}
6
+ "context"
7
+ {{/if}}
5
8
  "net/http/httptest"
6
9
  "os"
7
10
  "sync"
8
11
  "testing"
12
+ {{#if auth}}
13
+ "time"
14
+ {{/if}}
9
15
 
10
16
  "{{goModule}}/internal/app/{{modulePath}}/model"
11
17
  "{{goModule}}/internal/shared/middleware"
@@ -13,13 +19,21 @@ import (
13
19
  "gorm.io/gorm"
14
20
 
15
21
  "github.com/gin-gonic/gin"
22
+ {{#if auth}}
23
+ "github.com/golang-jwt/jwt/v5"
24
+ "github.com/google/uuid"
25
+ {{/if}}
16
26
  "gorm.io/driver/postgres"
17
27
  )
18
28
 
19
29
  // integration test harness, backed by real Postgres (same engine as prod, no sqlite) — skips if
20
30
  // the DB isn't reachable. minimal module: no routes yet, so no tests reference this yet — kept
21
- // ready for once `generate method` adds endpoints (start the DB: docker compose up -d, override
22
- // with TEST_DB_DSN)
31
+ // ready for once `generate method` adds endpoints.
32
+ // Runs against its own {{dbName}}_test database, never the one DB_DSN points at: the
33
+ // harness does DropTable+AutoMigrate on every run, which would otherwise wipe the
34
+ // schema `make migrate-up` built in your dev DB (FK constraints, seed data and all).
35
+ // Create it once with `make db-create DB_NAME={{dbName}}_test`, or point TEST_DB_DSN
36
+ // somewhere else.
23
37
  //nolint:unused
24
38
  var (
25
39
  testDBOnce sync.Once
@@ -33,7 +47,7 @@ func dbForTest(t *testing.T) *gorm.DB {
33
47
  testDBOnce.Do(func() {
34
48
  dsn := os.Getenv("TEST_DB_DSN")
35
49
  if dsn == "" {
36
- dsn = "postgres://postgres:postgres@localhost:5432/{{dbName}}?sslmode=disable"
50
+ dsn = "postgres://postgres:postgres@localhost:5432/{{dbName}}_test?sslmode=disable"
37
51
  }
38
52
  if testDB, testDBErr = gorm.Open(postgres.Open(dsn), &gorm.Config{TranslateError: true}); testDBErr == nil {
39
53
  _ = testDB.Migrator().DropTable(&model.{{pascalName}}{})
@@ -41,7 +55,7 @@ func dbForTest(t *testing.T) *gorm.DB {
41
55
  }
42
56
  })
43
57
  if testDBErr != nil {
44
- t.Skipf("postgres not ready (docker compose up -d, or set TEST_DB_DSN): %v", testDBErr)
58
+ t.Skipf("postgres not ready (make db-create DB_NAME={{dbName}}_test, or set TEST_DB_DSN): %v", testDBErr)
45
59
  }
46
60
  return testDB
47
61
  }
@@ -55,15 +69,48 @@ func setup(t *testing.T) *gin.Engine {
55
69
  tx := dbForTest(t).Begin()
56
70
  t.Cleanup(func() { tx.Rollback() })
57
71
  r := gin.New()
58
- r.Use(middleware.RequestID(), middleware.Error())
59
- NewHandler(NewService(NewRepository(tx))).Register(r)
72
+ r.Use(middleware.RequestID(), middleware.Error(true))
73
+ {{#if permission}}
74
+ // stub authz: always grants "{{permission}}" — this test is about the
75
+ // CRUD handler, not re-proving RBAC (that's covered by the role
76
+ // package's own tests), so the resolver doesn't need a real DB lookup.
77
+ authz := middleware.NewAuthz(func(_ context.Context, _ string) (map[string]struct{}, error) {
78
+ return map[string]struct{}{"{{permission}}": {}}, nil
79
+ }, time.Minute)
80
+ {{/if}}
81
+ NewHandler(NewService(NewRepository(tx)){{#if auth}}, testJWTSecret{{/if}}{{#if permission}}, authz{{/if}}).Register(r)
60
82
  return r
61
83
  }
62
84
 
85
+ {{#if auth}}
86
+ //nolint:unused
87
+ const testJWTSecret = "test-secret"
88
+
89
+ // authHeader mints a valid access token signed with testJWTSecret — the
90
+ // route's own RequireAuth(testJWTSecret) (wired in via NewHandler above)
91
+ // verifies against the same secret, so this round-trips for real.
92
+ //
93
+ //nolint:unused
94
+ func authHeader() string {
95
+ claims := jwt.MapClaims{
96
+ "typ": "access",
97
+ "sub": uuid.NewString(),
98
+ "role": "staff",
99
+ "exp": time.Now().Add(time.Hour).Unix(),
100
+ "iat": time.Now().Unix(),
101
+ }
102
+ tok, _ := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(testJWTSecret))
103
+ return "Bearer " + tok
104
+ }
105
+
106
+ {{/if}}
63
107
  //nolint:unused
64
108
  func do(r *gin.Engine, method, path, body string) *httptest.ResponseRecorder {
65
109
  req := httptest.NewRequest(method, path, bytes.NewBufferString(body))
66
110
  req.Header.Set("Content-Type", "application/json")
111
+ {{#if auth}}
112
+ req.Header.Set("Authorization", authHeader())
113
+ {{/if}}
67
114
  w := httptest.NewRecorder()
68
115
  r.ServeHTTP(w, req)
69
116
  return w
@@ -0,0 +1,5 @@
1
+ -- intentionally not deleting the permission row: another module generated
2
+ -- later may have reused this same code (the up migration's ON CONFLICT
3
+ -- assumes exactly that), and this file can't know whether that happened.
4
+ -- An unused permission row with no route checking it is harmless — delete
5
+ -- it by hand if you're sure nothing else references '{{permission}}'.
@@ -0,0 +1,4 @@
1
+ -- ON CONFLICT DO NOTHING: safe to re-generate a module reusing a permission
2
+ -- code another module already inserted (e.g. a shared "orders:manage").
3
+ INSERT INTO permissions (code, description) VALUES ('{{permission}}', 'Manage {{plural}}')
4
+ ON CONFLICT (code) DO NOTHING;
@@ -1,33 +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.listVersionFolders = listVersionFolders;
7
- exports.versionsContainingModule = versionsContainingModule;
8
- exports.nextVersionName = nextVersionName;
9
- const fs_extra_1 = __importDefault(require("fs-extra"));
10
- const path_1 = __importDefault(require("path"));
11
- // folder-based domain versioning lays modules out as internal/app/v<n>/<pkg>.
12
- // listVersionFolders returns the existing v<n> dirs, sorted ascending.
13
- function listVersionFolders(projectDir) {
14
- const appDir = path_1.default.join(projectDir, "internal", "app");
15
- if (!fs_extra_1.default.existsSync(appDir))
16
- return [];
17
- return fs_extra_1.default
18
- .readdirSync(appDir, { withFileTypes: true })
19
- .filter((e) => e.isDirectory() && /^v\d+$/.test(e.name))
20
- .map((e) => e.name)
21
- .sort((a, b) => parseInt(a.slice(1), 10) - parseInt(b.slice(1), 10));
22
- }
23
- // version folders that actually contain the given module (identified by its
24
- // handler.go) — a module can legitimately live in more than one version at
25
- // once (that's the point of versioning), so callers that need exactly one
26
- // (generate method, remove module) must pick among the matches.
27
- function versionsContainingModule(projectDir, pkg) {
28
- return listVersionFolders(projectDir).filter((v) => fs_extra_1.default.existsSync(path_1.default.join(projectDir, "internal", "app", v, pkg, "handler.go")));
29
- }
30
- function nextVersionName(versions) {
31
- const max = versions.reduce((m, v) => Math.max(m, parseInt(v.slice(1), 10) || 0), 0);
32
- return `v${max + 1}`;
33
- }