@nakedev/go-scaffold 0.1.4 → 0.3.1

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 +133 -44
  3. package/dist/commands/auth.js +116 -11
  4. package/dist/commands/create.js +13 -1
  5. package/dist/commands/generate.js +21 -11
  6. package/dist/commands/method.js +32 -3
  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 +366 -63
  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/observability-manifest.js +24 -0
  19. package/dist/templates/rbac-manifest.js +1 -0
  20. package/dist/templates/worker-manifest.js +23 -6
  21. package/dist/utils/auth-patcher.js +96 -21
  22. package/dist/utils/config.js +58 -10
  23. package/dist/utils/gocheck.js +57 -5
  24. package/dist/utils/golangci-patcher.js +73 -0
  25. package/dist/utils/gomod-patcher.js +53 -0
  26. package/dist/utils/main-patcher.js +58 -4
  27. package/dist/utils/marker-patch.js +125 -3
  28. package/dist/utils/method-patcher.js +17 -2
  29. package/dist/utils/module-location.js +37 -1
  30. package/dist/utils/naming.js +50 -2
  31. package/dist/utils/observability-patcher.js +107 -0
  32. package/dist/utils/platform-patcher.js +98 -12
  33. package/dist/utils/rbac-patcher.js +60 -10
  34. package/package.json +3 -5
  35. package/templates/add/auth/internal/app/user/errors.go.hbs +7 -0
  36. package/templates/add/auth/internal/app/user/handler.go.hbs +64 -23
  37. package/templates/add/auth/internal/app/user/jwt.go.hbs +31 -7
  38. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +39 -0
  39. package/templates/add/auth/internal/app/user/model/identity.go.hbs +3 -0
  40. package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +26 -0
  41. package/templates/add/auth/internal/app/user/model/user.go.hbs +9 -1
  42. package/templates/add/auth/internal/app/user/repository.go.hbs +64 -11
  43. package/templates/add/auth/internal/app/user/repository_test.go.hbs +192 -0
  44. package/templates/add/auth/internal/app/user/service.go.hbs +105 -21
  45. package/templates/add/auth/internal/app/user/service_test.go.hbs +81 -2
  46. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +9 -138
  47. package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +144 -0
  48. package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +147 -0
  49. package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +21 -19
  50. package/templates/add/auth/internal/shared/middleware/ratelimit_memory.go.hbs +63 -0
  51. package/templates/add/auth/internal/shared/middleware/ratelimit_redis.go.hbs +32 -0
  52. package/templates/add/auth/migrations/create_auth_tokens.down.sql.hbs +1 -0
  53. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +16 -0
  54. package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -1
  55. package/templates/add/auth/migrations/create_identities.up.sql.hbs +9 -5
  56. package/templates/add/auth/migrations/create_login_throttle.down.sql.hbs +1 -0
  57. package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +10 -0
  58. package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -1
  59. package/templates/add/auth/migrations/create_users.up.sql.hbs +13 -2
  60. package/templates/add/rbac/internal/app/role/dto.go.hbs +8 -3
  61. package/templates/add/rbac/internal/app/role/handler.go.hbs +4 -1
  62. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +3 -0
  63. package/templates/add/rbac/internal/app/role/model/role.go.hbs +4 -0
  64. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +3 -0
  65. package/templates/add/rbac/internal/app/role/repository.go.hbs +12 -11
  66. package/templates/add/rbac/internal/app/role/repository_test.go.hbs +176 -0
  67. package/templates/add/rbac/internal/app/role/service.go.hbs +7 -0
  68. package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +19 -0
  69. package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +1 -1
  70. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +5 -5
  71. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -11
  72. package/templates/add/worker/cmd/worker/main.go.hbs +30 -24
  73. package/templates/add/worker/internal/platform/mail/mail.go.hbs +21 -0
  74. package/templates/add/worker/internal/platform/mail/task.go.hbs +31 -34
  75. package/templates/add/worker/internal/platform/queue/asynq.go.hbs +140 -0
  76. package/templates/add/worker/internal/platform/queue/queue.go.hbs +87 -0
  77. package/templates/add/worker/internal/platform/queue/river.go.hbs +148 -0
  78. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +71 -12
  79. package/templates/create/base/.dockerignore.hbs +13 -0
  80. package/templates/create/base/.env.example.hbs +18 -9
  81. package/templates/create/base/.github/dependabot.yml.hbs +20 -0
  82. package/templates/create/base/.github/workflows/ci.yml.hbs +12 -2
  83. package/templates/create/base/.golangci.yml.hbs +27 -0
  84. package/templates/create/base/AGENTS.md.hbs +21 -7
  85. package/templates/create/base/Dockerfile.hbs +42 -0
  86. package/templates/create/base/Makefile.hbs +43 -13
  87. package/templates/create/base/README.md.hbs +45 -8
  88. package/templates/create/base/cmd/api/main.go.hbs +17 -130
  89. package/templates/create/base/cmd/api/wiring.go.hbs +161 -0
  90. package/templates/create/base/go.mod.hbs +4 -4
  91. package/templates/create/base/internal/platform/database/database.go.hbs +30 -11
  92. package/templates/create/base/internal/shared/config/config.go.hbs +13 -7
  93. package/templates/create/base/internal/shared/pagination/pagination.go.hbs +11 -0
  94. package/templates/create/base/internal/shared/tx/tx.go.hbs +47 -0
  95. package/templates/create/base/redocly.yaml.hbs +21 -0
  96. package/templates/create/features/docs/architecture.md.hbs +32 -11
  97. package/templates/create/features/docs/openapi.yaml.hbs +0 -4
  98. package/templates/create/features/docs/patterns.md.hbs +82 -8
  99. package/templates/create/features/docs/techstack.md.hbs +8 -3
  100. package/templates/generate/module/dto.go.hbs +8 -1
  101. package/templates/generate/module/errors.go.hbs +5 -0
  102. package/templates/generate/module/field-column.down.sql.hbs +2 -0
  103. package/templates/generate/module/field-column.up.sql.hbs +15 -0
  104. package/templates/generate/module/handler_test.go.hbs +8 -1
  105. package/templates/generate/module/migration.down.sql.hbs +3 -1
  106. package/templates/generate/module/migration.up.sql.hbs +7 -2
  107. package/templates/generate/module/minimal/dto.go.hbs +3 -1
  108. package/templates/generate/module/model/model.go.hbs +10 -1
  109. package/templates/generate/module/permission.up.sql.hbs +3 -1
  110. package/templates/generate/module/repository.go.hbs +60 -6
  111. package/templates/generate/module/repository_test.go.hbs +30 -0
  112. package/templates/generate/module/service.go.hbs +10 -1
  113. package/templates/generate/module/service_test.go.hbs +45 -0
  114. package/dist/commands/remove.js +0 -88
  115. package/scripts/smoke-test.mjs +0 -2058
  116. package/templates/add/worker/internal/platform/queue/client.go.hbs +0 -31
  117. package/templates/add/worker/internal/platform/queue/server.go.hbs +0 -68
  118. package/tests/integration/default-module.test.mjs +0 -46
  119. package/tests/integration/generator-naming.test.mjs +0 -81
  120. package/tests/integration/generator-unit-test-seams.test.mjs +0 -91
  121. package/tests/integration/legacy-method-compat.test.mjs +0 -222
  122. package/tests/integration/remove-module.test.mjs +0 -58
  123. package/tests/unit/naming.test.mjs +0 -94
  124. package/tests/unit/smoke-isolation.test.mjs +0 -35
@@ -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
- }
@@ -1,46 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { execFileSync } from "node:child_process";
3
- import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import path from "node:path";
6
- import test from "node:test";
7
- import { fileURLToPath } from "node:url";
8
-
9
- const ROOT = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
10
- const CLI = path.join(ROOT, "bin", "go-scaffold.js");
11
-
12
- function runCLI(cwd, ...args) {
13
- return execFileSync("node", [CLI, ...args], {
14
- cwd,
15
- encoding: "utf8",
16
- stdio: ["ignore", "pipe", "pipe"],
17
- });
18
- }
19
-
20
- test("generate module defaults to a safe minimal module", () => {
21
- const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-default-module-"));
22
- try {
23
- runCLI(scratch, "create", "sample", "--defaults", "--no-docker");
24
- const project = path.join(scratch, "sample");
25
-
26
- const output = runCLI(project, "generate", "module", "orders");
27
- const handler = readFileSync(
28
- path.join(project, "internal", "app", "order", "handler.go"),
29
- "utf8"
30
- );
31
-
32
- assert.match(output, /registered empty route group/);
33
- assert.doesNotMatch(handler, /g\.POST\(/);
34
- assert.equal(existsSync(path.join(project, "docs", "orders")), false);
35
-
36
- const legacyOutput = runCLI(project, "generate", "module", "widgets", "--no-full");
37
- const legacyHandler = readFileSync(
38
- path.join(project, "internal", "app", "widget", "handler.go"),
39
- "utf8"
40
- );
41
- assert.match(legacyOutput, /registered empty route group/);
42
- assert.doesNotMatch(legacyHandler, /g\.POST\(/);
43
- } finally {
44
- rmSync(scratch, { recursive: true, force: true });
45
- }
46
- });
@@ -1,81 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { execFileSync } from "node:child_process";
3
- import {
4
- mkdtempSync,
5
- readFileSync,
6
- readdirSync,
7
- rmSync,
8
- } from "node:fs";
9
- import { tmpdir } from "node:os";
10
- import path from "node:path";
11
- import test from "node:test";
12
- import { fileURLToPath } from "node:url";
13
-
14
- const ROOT = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
15
- const CLI = path.join(ROOT, "bin", "go-scaffold.js");
16
-
17
- function runCLI(cwd, ...args) {
18
- return execFileSync("node", [CLI, ...args], {
19
- cwd,
20
- encoding: "utf8",
21
- stdio: ["ignore", "pipe", "pipe"],
22
- });
23
- }
24
-
25
- function read(root, relativePath) {
26
- return readFileSync(path.join(root, relativePath), "utf8");
27
- }
28
-
29
- function migration(root, suffix) {
30
- const filename = readdirSync(path.join(root, "migrations")).find((entry) =>
31
- entry.endsWith(suffix)
32
- );
33
- assert.ok(filename, `missing migration ending with ${suffix}`);
34
- return read(root, path.join("migrations", filename));
35
- }
36
-
37
- test("plural module input produces a singular entity with one explicit table name", () => {
38
- const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-naming-"));
39
- try {
40
- runCLI(scratch, "create", "sample", "--defaults", "--no-docker");
41
- const project = path.join(scratch, "sample");
42
- runCLI(project, "generate", "module", "orders");
43
-
44
- const model = read(project, "internal/app/order/model/model.go");
45
- assert.match(model, /type Order struct/);
46
- assert.match(
47
- model,
48
- /func \(Order\) TableName\(\) string \{\s*return "orders"\s*\}/
49
- );
50
- assert.match(migration(project, "_create_orders.up.sql"), /CREATE TABLE orders/);
51
- assert.match(read(project, "internal/app/order/handler.go"), /Group\("\/orders"/);
52
- } finally {
53
- rmSync(scratch, { recursive: true, force: true });
54
- }
55
- });
56
-
57
- test("multi-word module keeps URL words and uses snake_case SQL identifiers", () => {
58
- const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-multiword-"));
59
- try {
60
- runCLI(scratch, "create", "sample", "--defaults", "--no-docker");
61
- const project = path.join(scratch, "sample");
62
- runCLI(project, "generate", "module", "order-items");
63
-
64
- const model = read(project, "internal/app/orderitem/model/model.go");
65
- assert.match(model, /type OrderItem struct/);
66
- assert.match(
67
- model,
68
- /func \(OrderItem\) TableName\(\) string \{\s*return "order_items"\s*\}/
69
- );
70
- assert.match(
71
- migration(project, "_create_order-items.up.sql"),
72
- /CREATE TABLE order_items/
73
- );
74
- assert.match(
75
- read(project, "internal/app/orderitem/handler.go"),
76
- /Group\("\/order-items"/
77
- );
78
- } finally {
79
- rmSync(scratch, { recursive: true, force: true });
80
- }
81
- });
@@ -1,91 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { execFileSync } from "node:child_process";
3
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import path from "node:path";
6
- import test from "node:test";
7
- import { fileURLToPath } from "node:url";
8
-
9
- const ROOT = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
10
- const CLI = path.join(ROOT, "bin", "go-scaffold.js");
11
-
12
- function run(command, args, cwd) {
13
- return execFileSync(command, args, {
14
- cwd,
15
- encoding: "utf8",
16
- stdio: ["ignore", "pipe", "pipe"],
17
- });
18
- }
19
-
20
- function read(project, relativePath) {
21
- return readFileSync(path.join(project, relativePath), "utf8");
22
- }
23
-
24
- test("generated modules have scalable service and handler unit-test seams", () => {
25
- const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-unit-seams-"));
26
- try {
27
- run("node", [CLI, "create", "sample", "--defaults", "--no-docker"], scratch);
28
- const project = path.join(scratch, "sample");
29
- run("node", [CLI, "generate", "module", "orders", "--full"], project);
30
-
31
- const handler = read(project, "internal/app/order/handler.go");
32
- assert.match(handler, /type service interface \{/);
33
- assert.match(handler, /svc service/);
34
- assert.doesNotMatch(handler, /svc \*Service/);
35
- assert.match(handler, /Create\(context\.Context, createInput\)/);
36
- assert.match(handler, /Update\(context\.Context, uuid\.UUID, updateInput\)/);
37
-
38
- const service = read(project, "internal/app/order/service.go");
39
- assert.match(service, /Create\(ctx context\.Context, in createInput\)/);
40
- assert.match(service, /Update\(ctx context\.Context, id uuid\.UUID, in updateInput\)/);
41
-
42
- const serviceTest = read(project, "internal/app/order/service_test.go");
43
- assert.match(serviceTest, /type repositoryStub struct/);
44
- assert.match(serviceTest, /createFn\s+func/);
45
- assert.match(serviceTest, /findByIDFn\s+func/);
46
- assert.match(serviceTest, /updateFn\s+func/);
47
- assert.doesNotMatch(serviceTest, /type fakeRepo struct/);
48
-
49
- const handlerTest = read(project, "internal/app/order/handler_test.go");
50
- assert.match(handlerTest, /type serviceStub struct/);
51
- assert.match(handlerTest, /createFn\s+func/);
52
- assert.doesNotMatch(handlerTest, /gorm\.io\/driver\/postgres/);
53
- assert.doesNotMatch(handlerTest, /TEST_DB_DSN/);
54
-
55
- const repositoryTest = read(project, "internal/app/order/repository_test.go");
56
- assert.match(repositoryTest, /REQUIRE_TEST_DB/);
57
- assert.match(repositoryTest, /TEST_DB_DSN/);
58
- assert.doesNotMatch(repositoryTest, /AutoMigrate/);
59
- assert.doesNotMatch(repositoryTest, /DropTable/);
60
-
61
- const ci = read(project, ".github/workflows/ci.yml");
62
- assert.match(ci, /migrate -path migrations/);
63
- assert.match(ci, /REQUIRE_TEST_DB: "true"/);
64
- assert.match(ci, /TEST_DB_DSN:/);
65
-
66
- const itemDocs = read(project, "docs/orders/item.yaml");
67
- const deleteContract = itemDocs.slice(itemDocs.indexOf("delete:"));
68
- assert.doesNotMatch(deleteContract, /"404"/);
69
-
70
- run("node", [CLI, "generate", "method", "orders", "approve", "--type", "patch"], project);
71
- const openapi = read(project, "docs/openapi.yaml");
72
- assert.match(openapi, /\/v1\/orders\/\{id\}\/approve:/);
73
- const approveDocs = read(project, "docs/orders/methods/approve.yaml");
74
- assert.match(approveDocs, /^parameters:/);
75
- assert.match(approveDocs, /^patch:/m);
76
- assert.match(approveDocs, /operationId: approveOrder/);
77
- assert.doesNotMatch(approveDocs, /requestBody:/);
78
-
79
- run("node", [CLI, "generate", "method", "orders", "submit", "--type", "post"], project);
80
- const submitDocs = read(project, "docs/orders/methods/submit.yaml");
81
- assert.doesNotMatch(submitDocs, /^parameters:/);
82
- assert.doesNotMatch(submitDocs, /in: path/);
83
- assert.match(submitDocs, /^post:/m);
84
- assert.match(submitDocs, /requestBody:/);
85
-
86
- run("go", ["mod", "tidy"], project);
87
- run("go", ["test", "./..."], project);
88
- } finally {
89
- rmSync(scratch, { recursive: true, force: true });
90
- }
91
- });
@@ -1,222 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { execFileSync } from "node:child_process";
3
- import {
4
- existsSync,
5
- mkdirSync,
6
- mkdtempSync,
7
- readFileSync,
8
- readdirSync,
9
- renameSync,
10
- rmSync,
11
- writeFileSync,
12
- } from "node:fs";
13
- import { tmpdir } from "node:os";
14
- import path from "node:path";
15
- import test from "node:test";
16
- import { fileURLToPath } from "node:url";
17
-
18
- const ROOT = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
19
- const CLI = path.join(ROOT, "bin", "go-scaffold.js");
20
-
21
- function run(command, args, cwd) {
22
- return execFileSync(command, args, {
23
- cwd,
24
- encoding: "utf8",
25
- stdio: ["ignore", "pipe", "pipe"],
26
- });
27
- }
28
-
29
- function rewriteGoTree(dir, rewrite) {
30
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
31
- const target = path.join(dir, entry.name);
32
- if (entry.isDirectory()) rewriteGoTree(target, rewrite);
33
- else if (entry.name.endsWith(".go")) {
34
- writeFileSync(target, rewrite(readFileSync(target, "utf8")));
35
- }
36
- }
37
- }
38
-
39
- function convertToLegacyPluralModule(project) {
40
- const canonicalDir = path.join(project, "internal", "app", "order");
41
- const legacyDir = path.join(project, "internal", "app", "orders");
42
- renameSync(canonicalDir, legacyDir);
43
- rewriteGoTree(legacyDir, (source) =>
44
- source
45
- .replace(/^package order$/m, "package orders")
46
- .replaceAll("sample/internal/app/order/model", "sample/internal/app/orders/model")
47
- .replaceAll("model.Order", "model.Orders")
48
- .replaceAll("ErrOrder", "ErrOrders")
49
- .replace("type Order struct", "type Orders struct")
50
- .replaceAll("func (Order)", "func (Orders)")
51
- .replace('return "orders"', 'return "orderses"')
52
- .replace('Group("/orders"', 'Group("/orderses"')
53
- );
54
-
55
- const mainPath = path.join(project, "cmd", "api", "main.go");
56
- const legacyMain = readFileSync(mainPath, "utf8")
57
- .replaceAll("sample/internal/app/order", "sample/internal/app/orders")
58
- .replaceAll("ordermodel.Order", "ordersmodel.Orders")
59
- .replaceAll("ordermodel", "ordersmodel")
60
- .replaceAll("order.New", "orders.New")
61
- .replaceAll("orderRepo", "ordersRepo")
62
- .replaceAll("orderService", "ordersService")
63
- .replaceAll("orderHandler", "ordersHandler");
64
- writeFileSync(mainPath, legacyMain);
65
-
66
- const docsDir = path.join(project, "docs");
67
- const canonicalDocs = path.join(docsDir, "orders");
68
- const legacyDocs = path.join(docsDir, "orderses");
69
- if (existsSync(canonicalDocs)) {
70
- renameSync(canonicalDocs, legacyDocs);
71
- for (const name of readdirSync(legacyDocs)) {
72
- const target = path.join(legacyDocs, name);
73
- if (!name.endsWith(".yaml")) continue;
74
- writeFileSync(target, readFileSync(target, "utf8").replaceAll("Order", "Orders"));
75
- }
76
- }
77
- const openapiPath = path.join(docsDir, "openapi.yaml");
78
- if (existsSync(openapiPath)) {
79
- writeFileSync(
80
- openapiPath,
81
- readFileSync(openapiPath, "utf8")
82
- .replaceAll("/v1/orders", "/v1/orderses")
83
- .replaceAll("./orders/", "./orderses/")
84
- .replaceAll("OrderCreate", "OrdersCreate")
85
- .replaceAll("OrderUpdate", "OrdersUpdate")
86
- .replaceAll("OrderResponse", "OrdersResponse")
87
- );
88
- }
89
-
90
- const migrationsDir = path.join(project, "migrations");
91
- for (const name of readdirSync(migrationsDir)) {
92
- if (!name.includes("_create_orders.")) continue;
93
- const source = path.join(migrationsDir, name);
94
- const target = path.join(migrationsDir, name.replace("_create_orders.", "_create_orderses."));
95
- writeFileSync(target, readFileSync(source, "utf8").replaceAll("orders", "orderses"));
96
- rmSync(source);
97
- }
98
- return legacyDir;
99
- }
100
-
101
- test("generate module refuses to duplicate a legacy plural package", () => {
102
- const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-legacy-duplicate-"));
103
- try {
104
- run("node", [CLI, "create", "sample", "--defaults", "--no-docker"], scratch);
105
- const project = path.join(scratch, "sample");
106
- mkdirSync(path.join(project, "internal", "app", "orders"), { recursive: true });
107
- writeFileSync(path.join(project, "internal", "app", "orders", "legacy.go"), "package orders\n");
108
-
109
- assert.throws(
110
- () => run("node", [CLI, "generate", "module", "orders", "--full"], project),
111
- /internal\/app\/orders.*already exists/
112
- );
113
- assert.equal(existsSync(path.join(project, "internal", "app", "order")), false);
114
- } finally {
115
- rmSync(scratch, { recursive: true, force: true });
116
- }
117
- });
118
-
119
- test("remove module locates legacy plural packages and preserves their migrations", () => {
120
- const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-legacy-remove-"));
121
- try {
122
- run("node", [CLI, "create", "sample", "--defaults", "--no-docker"], scratch);
123
- const project = path.join(scratch, "sample");
124
- run("node", [CLI, "generate", "module", "orders", "--full"], project);
125
- const legacyDir = convertToLegacyPluralModule(project);
126
- const migrationsDir = path.join(project, "migrations");
127
- const migrationsBefore = readdirSync(migrationsDir).filter((name) => name.includes("_create_orderses."));
128
-
129
- const output = run("node", [CLI, "remove", "module", "orders", "--yes"], project);
130
-
131
- assert.equal(existsSync(legacyDir), false);
132
- for (const name of migrationsBefore) assert.equal(existsSync(path.join(migrationsDir, name)), true);
133
- assert.doesNotMatch(readFileSync(path.join(project, "cmd", "api", "main.go"), "utf8"), /internal\/app\/orders/);
134
- assert.doesNotMatch(readFileSync(path.join(project, "docs", "openapi.yaml"), "utf8"), /orderses/);
135
- assert.match(output, /preserved migration history/);
136
- } finally {
137
- rmSync(scratch, { recursive: true, force: true });
138
- }
139
- });
140
-
141
- test("generate method locates legacy plural packages and concrete handlers", () => {
142
- const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-legacy-method-"));
143
- try {
144
- run("node", [CLI, "create", "sample", "--defaults", "--no-docker"], scratch);
145
- const project = path.join(scratch, "sample");
146
- run("node", [CLI, "generate", "module", "orders", "--full"], project);
147
-
148
- const legacyDir = convertToLegacyPluralModule(project);
149
-
150
- const handlerPath = path.join(legacyDir, "handler.go");
151
- const modernHandler = readFileSync(handlerPath, "utf8");
152
- const legacyHandler = modernHandler
153
- .replace(/type service interface \{[\s\S]*?\/\/ go-scaffold:service-interface\n\}\n\n/, "")
154
- .replace('\t"context"\n', "")
155
- .replace('\t"sample/internal/app/orders/model"\n', "")
156
- .replace('\t"github.com/google/uuid"\n', "")
157
- .replace("svc service", "svc *Service")
158
- .replace("func NewHandler(svc service", "func NewHandler(svc *Service");
159
- writeFileSync(handlerPath, legacyHandler);
160
- writeFileSync(path.join(legacyDir, "handler_test.go"), "package orders\n");
161
-
162
- const serviceTestPath = path.join(legacyDir, "service_test.go");
163
- writeFileSync(
164
- serviceTestPath,
165
- `package orders
166
-
167
- import (
168
- "context"
169
- "testing"
170
-
171
- "sample/internal/app/orders/model"
172
-
173
- "github.com/google/uuid"
174
- )
175
-
176
- type fakeRepo struct {
177
- err error
178
- m *model.Orders
179
- }
180
-
181
- func (f *fakeRepo) Create(context.Context, *model.Orders) error { return f.err }
182
- func (f *fakeRepo) FindAll(context.Context, int, int) ([]model.Orders, error) { return nil, f.err }
183
- func (f *fakeRepo) FindByID(context.Context, uuid.UUID) (*model.Orders, error) {
184
- if f.err != nil {
185
- return nil, f.err
186
- }
187
- return f.m, nil
188
- }
189
- func (f *fakeRepo) Update(context.Context, *model.Orders) error { return f.err }
190
- func (f *fakeRepo) Delete(context.Context, uuid.UUID) error { return f.err }
191
-
192
- // go-scaffold:fake-repo-methods
193
-
194
- func TestGeneratedLegacyFindByStatusReturnsFixture(t *testing.T) {
195
- want := &model.Orders{ID: uuid.New()}
196
- svc := NewService(&fakeRepo{m: want})
197
- got, err := svc.FindByStatus(context.Background(), "active")
198
- if err != nil {
199
- t.Fatalf("unexpected error: %v", err)
200
- }
201
- if got != want {
202
- t.Fatalf("expected legacy fake fixture %p, got %p", want, got)
203
- }
204
- }
205
- `
206
- );
207
-
208
- run(
209
- "node",
210
- [CLI, "generate", "method", "order", "findByStatus", "--type", "get", "--get-mode", "one", "--field", "status"],
211
- project
212
- );
213
- assert.match(
214
- readFileSync(serviceTestPath, "utf8"),
215
- /\/\/nolint:unused\nfunc \(f \*fakeRepo\) FindByStatus/
216
- );
217
- run("go", ["mod", "tidy"], project);
218
- run("go", ["test", "./..."], project);
219
- } finally {
220
- rmSync(scratch, { recursive: true, force: true });
221
- }
222
- });
@@ -1,58 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { execFileSync } from "node:child_process";
3
- import {
4
- existsSync,
5
- mkdtempSync,
6
- readFileSync,
7
- readdirSync,
8
- rmSync,
9
- } from "node:fs";
10
- import { tmpdir } from "node:os";
11
- import path from "node:path";
12
- import test from "node:test";
13
- import { fileURLToPath } from "node:url";
14
-
15
- const ROOT = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
16
- const CLI = path.join(ROOT, "bin", "go-scaffold.js");
17
-
18
- function runCLI(cwd, ...args) {
19
- return execFileSync("node", [CLI, ...args], {
20
- cwd,
21
- encoding: "utf8",
22
- stdio: ["ignore", "pipe", "pipe"],
23
- });
24
- }
25
-
26
- function createMigrations(project, table) {
27
- return readdirSync(path.join(project, "migrations")).filter((entry) =>
28
- entry.includes(`_create_${table}.`)
29
- );
30
- }
31
-
32
- test("remove module preserves immutable migrations and re-add reuses them", () => {
33
- const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-remove-"));
34
- try {
35
- runCLI(scratch, "create", "sample", "--defaults", "--no-docker");
36
- const project = path.join(scratch, "sample");
37
- runCLI(project, "generate", "module", "widgets");
38
- runCLI(project, "generate", "method", "widgets", "approve", "--type", "patch");
39
-
40
- const before = createMigrations(project, "widgets");
41
- assert.equal(before.length, 2);
42
- const openapiPath = path.join(project, "docs", "openapi.yaml");
43
- assert.match(readFileSync(openapiPath, "utf8"), /\/v1\/widgets\/\{id\}\/approve:/);
44
-
45
- const output = runCLI(project, "remove", "module", "widgets", "--yes");
46
-
47
- assert.equal(existsSync(path.join(project, "internal", "app", "widget")), false);
48
- assert.deepEqual(createMigrations(project, "widgets"), before);
49
- assert.match(output, /preserved migration history/);
50
- assert.match(output, /generate migration drop_widgets/);
51
- assert.doesNotMatch(readFileSync(openapiPath, "utf8"), /\.\/widgets\//);
52
-
53
- runCLI(project, "generate", "module", "widgets");
54
- assert.deepEqual(createMigrations(project, "widgets"), before);
55
- } finally {
56
- rmSync(scratch, { recursive: true, force: true });
57
- }
58
- });