@nakedev/go-scaffold 0.1.4 → 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 +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 +59 -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 +8 -4
  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
@@ -11,65 +11,125 @@ const config_1 = require("../utils/config");
11
11
  const template_renderer_1 = require("../utils/template-renderer");
12
12
  const worker_manifest_1 = require("../templates/worker-manifest");
13
13
  const platform_patcher_1 = require("../utils/platform-patcher");
14
- // addWorker scaffolds the async task processing subsystem: Redis
15
- // (platform/cache), Asynq client/server (platform/queue), SMTP mail
16
- // (platform/mail, with an email:send task type as the one thing the fresh
14
+ const gocheck_1 = require("../utils/gocheck");
15
+ const gomod_patcher_1 = require("../utils/gomod-patcher");
16
+ const auth_patcher_1 = require("../utils/auth-patcher");
17
+ // addWorker scaffolds async job processing: the backend-neutral queue
18
+ // contract (platform/queue), one adapter for the chosen backing store, SMTP
19
+ // mail (platform/mail, with an email:send job as the one thing a fresh
17
20
  // worker actually handles), and cmd/worker itself. Opt-in — most projects
18
- // don't need a queue on day one, and an empty worker with no task types
19
- // registered is a stranger scaffold than just not having one.
20
- async function addWorker(projectDir = process.cwd()) {
21
+ // don't need a queue on day one, and an empty worker with no job kinds
22
+ // registered is a stranger scaffold than not having one.
23
+ async function addWorker(backend, projectDir = process.cwd()) {
21
24
  const config = (0, config_1.readConfig)(projectDir);
22
25
  const queueDir = path_1.default.join(projectDir, "internal", "platform", "queue");
23
26
  if (fs_extra_1.default.existsSync(queueDir)) {
24
27
  throw new Error(`${queueDir} already exists — worker infrastructure looks like it's already been added`);
25
28
  }
26
- await (0, template_renderer_1.applyTemplateEntries)(projectDir, worker_manifest_1.WORKER_FILES, { goModule: config.goModule });
27
- (0, platform_patcher_1.patchConfigForWorker)(path_1.default.join(projectDir, "internal", "shared", "config", "config.go"));
28
- (0, platform_patcher_1.patchMainGoForWorker)(path_1.default.join(projectDir, "cmd", "api", "main.go"), config.goModule);
29
- patchEnvExample(path_1.default.join(projectDir, ".env.example"));
30
- patchMakefile(path_1.default.join(projectDir, "Makefile"));
29
+ const parsedBefore = (0, gocheck_1.parseChecks)(projectDir);
30
+ const riverQueue = backend === "river";
31
+ await (0, template_renderer_1.applyTemplateEntries)(projectDir, (0, worker_manifest_1.workerFiles)(backend), { goModule: config.goModule, riverQueue });
32
+ (0, platform_patcher_1.patchConfigForWorker)(path_1.default.join(projectDir, "internal", "shared", "config", "config.go"), { redis: !riverQueue });
33
+ if (!riverQueue) {
34
+ (0, platform_patcher_1.patchMainGoForWorker)(path_1.default.join(projectDir, "cmd", "api", "wiring.go"), config.goModule);
35
+ (0, platform_patcher_1.patchComposeForRedis)(path_1.default.join(projectDir, "docker-compose.yml"));
36
+ (0, platform_patcher_1.patchCiForRedis)(path_1.default.join(projectDir, ".github", "workflows", "ci.yml"));
37
+ }
38
+ // pinned to what this scaffold was written against — see gomod-patcher
39
+ (0, gomod_patcher_1.patchGoModRequires)(path_1.default.join(projectDir, "go.mod"), riverQueue
40
+ ? ["github.com/riverqueue/river v0.43.0", "github.com/riverqueue/river/riverdriver/riverdatabasesql v0.43.0"]
41
+ : ["github.com/hibiken/asynq v0.26.0", "github.com/redis/go-redis/v9 v9.22.0"]);
42
+ // auth added before the worker wired a synchronous mailer — now that there
43
+ // is a queue, move it onto it
44
+ const mailerUpgraded = (0, auth_patcher_1.upgradeMailerToQueue)(path_1.default.join(projectDir, "cmd", "api", "wiring.go"), config.goModule, backend);
45
+ patchEnvExample(path_1.default.join(projectDir, ".env.example"), { redis: !riverQueue });
46
+ patchMakefile(path_1.default.join(projectDir, "Makefile"), { river: riverQueue });
31
47
  (0, template_renderer_1.gofmtTree)(projectDir);
32
- (0, config_1.writeConfig)(projectDir, { ...config, features: { ...config.features, worker: true } });
33
- console.log(picocolors_1.default.green("\nadded internal/platform/{cache,queue,mail}/ and cmd/worker/"));
34
- console.log("wired Redis into cmd/api (readyz check) — cmd/api does not enqueue anything yet");
35
- console.log(picocolors_1.default.dim("\nnext: make worker (separate terminal, or `make dev` runs both), then go build ./... to confirm"));
48
+ // parse-only: river/asynq aren't in go.mod until the user runs `go mod
49
+ // tidy`, so `go vet` can't be the gate here.
50
+ (0, gocheck_1.assertStillParses)(projectDir, parsedBefore, `added worker (${backend})`);
51
+ (0, config_1.writeConfig)(projectDir, { ...config, features: { ...config.features, worker: true, queue: backend } });
52
+ console.log(picocolors_1.default.green(`\nadded internal/platform/{queue,mail}/ and cmd/worker/ (queue backend: ${backend})`));
53
+ if (riverQueue) {
54
+ console.log("jobs are rows in your Postgres — no extra service, and an enqueue inside tx.Do commits with it");
55
+ console.log(picocolors_1.default.dim("\nnext: make river-migrate (creates River's tables), then make worker"));
56
+ }
57
+ else {
58
+ console.log(mailerUpgraded
59
+ ? "wired Redis into cmd/api (readyz check) — auth's mailer now enqueues onto it instead of blocking on SMTP"
60
+ : "wired Redis into cmd/api (readyz check) — cmd/api does not enqueue anything yet");
61
+ console.log(picocolors_1.default.yellow("note: a Redis enqueue cannot join a database transaction — see the warning on queue.Asynq"));
62
+ console.log(picocolors_1.default.dim("\nnext: make worker (separate terminal, or `make dev` runs both), then go build ./... to confirm"));
63
+ }
36
64
  }
37
- function patchEnvExample(envExamplePath) {
65
+ // needsSmtp and needsRedis are checked independently: `add auth` run without
66
+ // a worker already writes SMTP_HOST on its own (auth stands alone — see
67
+ // addAuth), so by the time a Redis-backed worker arrives after it, the old
68
+ // single `if (content.includes("SMTP_HOST")) return` guard fired on the SMTP
69
+ // block alone and quietly skipped the Redis block right along with it —
70
+ // REDIS_URL never made it into .env.example even though config.go now reads
71
+ // it. Each block gets its own guard so one being done doesn't hide the other.
72
+ function patchEnvExample(envExamplePath, opts) {
38
73
  if (!fs_extra_1.default.existsSync(envExamplePath))
39
74
  return;
40
75
  let content = fs_extra_1.default.readFileSync(envExamplePath, "utf8");
41
- if (content.includes("REDIS_URL"))
42
- return; // already added
43
- content =
44
- content.replace(/\n?$/, "\n") +
45
- "\nREDIS_URL=redis://localhost:6379/0\n" +
76
+ const needsRedis = opts.redis && !content.includes("REDIS_URL");
77
+ const needsSmtp = !content.includes("SMTP_HOST");
78
+ if (!needsRedis && !needsSmtp)
79
+ return; // both already added
80
+ content = content.replace(/\n?$/, "\n");
81
+ if (needsRedis) {
82
+ content += "\nREDIS_URL=redis://localhost:6379/0\n";
83
+ }
84
+ if (needsSmtp) {
85
+ content +=
46
86
  "\n# leave SMTP_HOST unset to log emails instead of sending them (dev default)\n" +
47
- "SMTP_HOST=\n" +
48
- "SMTP_PORT=587\n" +
49
- "SMTP_USERNAME=\n" +
50
- "SMTP_PASSWORD=\n" +
51
- "SMTP_FROM=no-reply@example.local\n";
87
+ "SMTP_HOST=\n" +
88
+ "SMTP_PORT=587\n" +
89
+ "SMTP_USERNAME=\n" +
90
+ "SMTP_PASSWORD=\n" +
91
+ "SMTP_FROM=no-reply@example.local\n";
92
+ }
52
93
  fs_extra_1.default.writeFileSync(envExamplePath, content);
53
94
  }
54
- function patchMakefile(makefilePath) {
95
+ function patchMakefile(makefilePath, opts) {
55
96
  if (!fs_extra_1.default.existsSync(makefilePath))
56
97
  return;
57
98
  let content = fs_extra_1.default.readFileSync(makefilePath, "utf8");
58
99
  if (content.includes("\nworker:\n"))
59
100
  return; // already added
60
- content = content.replace(/^\.PHONY: /m, ".PHONY: dev worker ");
101
+ content = content.replace(/^\.PHONY: /m, `.PHONY: dev worker${opts.river ? " river-migrate" : ""} `);
102
+ // River keeps its own tables, versioned by River itself rather than by this
103
+ // project's migrations/ directory — run its CLI once per database. Pinned
104
+ // by nothing on purpose: it is a one-shot setup command, not a build input.
105
+ const riverTarget = opts.river
106
+ ? "\n# create River's job tables (run once per database, and after upgrading River)\n" +
107
+ "river-migrate:\n" +
108
+ "\t@set -a; [ -f $(ENV_FILE) ] && . ./$(ENV_FILE); set +a; \\\n" +
109
+ '\tgo run github.com/riverqueue/river/cmd/river@latest migrate-up --line main --database-url "$$DB_DSN"\n'
110
+ : "";
111
+ // Both load config exactly the way Makefile.hbs says every target does:
112
+ // via $(ENV_FILE), and through the same sed that strips trailing comments.
113
+ // Without it, .env.example's own `APP_ENV=development # prod: production`
114
+ // reaches `export` as a bare `#` and prints an error on every run.
115
+ const loadEnv = "@set -a; [ -f $(ENV_FILE) ] && . ./$(ENV_FILE); set +a;";
61
116
  const targets = "\n# run both API + worker in one terminal — Ctrl+C kills both\n" +
62
117
  "dev:\n" +
63
- "\t@[ -f .env ] && export $$(grep -v '^#' .env | xargs); \\\n" +
118
+ `\t${loadEnv} \\\n` +
64
119
  "\t(trap 'kill 0' SIGINT SIGTERM; \\\n" +
65
120
  "\t go run ./cmd/api & \\\n" +
66
121
  "\t go run ./cmd/worker & \\\n" +
67
122
  "\t wait)\n" +
68
123
  "\n" +
69
- "# background worker for async task processing (email, ...) — requires Redis.\n" +
124
+ `# background worker for async job processing (email, ...) — requires ${opts.river ? "Postgres (make river-migrate first)" : "Redis"}.\n` +
70
125
  "# Use `make dev` to run both in one terminal, or run this in a separate one.\n" +
71
126
  "worker:\n" +
72
- "\t@[ -f .env ] && export $$(grep -v '^#' .env | xargs); go run ./cmd/worker\n";
73
- content = content.replace(/\nbuild:/, `${targets}\nbuild:`);
127
+ `\t${loadEnv} go run ./cmd/worker\n` +
128
+ riverTarget;
129
+ // Function replacer: targets contains literal "$$" (Make's escape for a
130
+ // shell "$") which String.replace would otherwise collapse to a single "$"
131
+ // when the replacement is a plain string — turning "$$DB_DSN" into
132
+ // "$DB_DSN" and silently handing the wrong value to every command below.
133
+ content = content.replace(/\nbuild:/, () => `${targets}\nbuild:`);
74
134
  fs_extra_1.default.writeFileSync(makefilePath, content);
75
135
  }