@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.
- package/README.md +75 -0
- package/dist/commands/auth.js +129 -0
- package/dist/commands/create.js +3 -2
- package/dist/commands/generate.js +59 -1
- package/dist/commands/method.js +3 -0
- package/dist/commands/migration.js +34 -0
- package/dist/commands/rbac.js +103 -0
- package/dist/commands/remove.js +19 -2
- package/dist/commands/worker.js +75 -0
- package/dist/index.js +66 -3
- package/dist/prompts/create-wizard.js +6 -1
- package/dist/prompts/generate-wizard.js +8 -0
- package/dist/templates/auth-manifest.js +19 -0
- package/dist/templates/create-manifest.js +25 -0
- package/dist/templates/rbac-manifest.js +17 -0
- package/dist/templates/worker-manifest.js +12 -0
- package/dist/utils/auth-patcher.js +96 -0
- package/dist/utils/gocheck.js +65 -0
- package/dist/utils/main-patcher.js +8 -1
- package/dist/utils/migrations.js +30 -8
- package/dist/utils/openapi-patcher.js +16 -0
- package/dist/utils/platform-patcher.js +59 -0
- package/dist/utils/rbac-patcher.js +277 -0
- package/dist/utils/version.js +24 -0
- package/package.json +2 -2
- package/templates/add/auth/cmd/seed/main.go.hbs +76 -0
- package/templates/add/auth/docs/forgot-password.yaml.hbs +19 -0
- package/templates/add/auth/docs/google-callback.yaml.hbs +22 -0
- package/templates/add/auth/docs/google-login.yaml.hbs +7 -0
- package/templates/add/auth/docs/login.yaml.hbs +19 -0
- package/templates/add/auth/docs/logout.yaml.hbs +8 -0
- package/templates/add/auth/docs/refresh.yaml.hbs +15 -0
- package/templates/add/auth/docs/register.yaml.hbs +19 -0
- package/templates/add/auth/docs/reset-password.yaml.hbs +16 -0
- package/templates/add/auth/docs/schemas.yaml.hbs +58 -0
- package/templates/add/auth/docs/users-me-logout-all.yaml.hbs +9 -0
- package/templates/add/auth/docs/users-me-resend-verification.yaml.hbs +10 -0
- package/templates/add/auth/docs/users-me.yaml.hbs +12 -0
- package/templates/add/auth/docs/verify-email.yaml.hbs +16 -0
- package/templates/add/auth/internal/app/user/dto.go.hbs +77 -0
- package/templates/add/auth/internal/app/user/errors.go.hbs +36 -0
- package/templates/add/auth/internal/app/user/handler.go.hbs +235 -0
- package/templates/add/auth/internal/app/user/jwt.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +28 -0
- package/templates/add/auth/internal/app/user/model/user.go.hbs +22 -0
- package/templates/add/auth/internal/app/user/repository.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/service.go.hbs +447 -0
- package/templates/add/auth/internal/app/user/service_test.go.hbs +237 -0
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +159 -0
- package/templates/add/auth/internal/shared/middleware/auth.go.hbs +64 -0
- package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +44 -0
- package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_identities.up.sql.hbs +11 -0
- package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_users.up.sql.hbs +9 -0
- package/templates/add/rbac/docs/permissions.yaml.hbs +24 -0
- package/templates/add/rbac/docs/role-permissions.yaml.hbs +31 -0
- package/templates/add/rbac/docs/role.yaml.hbs +17 -0
- package/templates/add/rbac/docs/roles.yaml.hbs +43 -0
- package/templates/add/rbac/docs/schemas.yaml.hbs +37 -0
- package/templates/add/rbac/docs/user-set-role.yaml.hbs +23 -0
- package/templates/add/rbac/docs/user.yaml.hbs +16 -0
- package/templates/add/rbac/docs/users.yaml.hbs +23 -0
- package/templates/add/rbac/internal/app/role/dto.go.hbs +40 -0
- package/templates/add/rbac/internal/app/role/errors.go.hbs +39 -0
- package/templates/add/rbac/internal/app/role/handler.go.hbs +101 -0
- package/templates/add/rbac/internal/app/role/model/permission.go.hbs +9 -0
- package/templates/add/rbac/internal/app/role/model/role.go.hbs +18 -0
- package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +8 -0
- package/templates/add/rbac/internal/app/role/repository.go.hbs +96 -0
- package/templates/add/rbac/internal/app/role/service.go.hbs +210 -0
- package/templates/add/rbac/internal/app/role/service_test.go.hbs +119 -0
- package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +88 -0
- package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +88 -0
- package/templates/add/rbac/migrations/add_roles.down.sql.hbs +15 -0
- package/templates/add/rbac/migrations/add_roles.up.sql.hbs +35 -0
- package/templates/add/worker/cmd/worker/main.go.hbs +77 -0
- package/templates/add/worker/internal/platform/cache/redis.go.hbs +18 -0
- package/templates/add/worker/internal/platform/mail/mail.go.hbs +48 -0
- package/templates/add/worker/internal/platform/mail/task.go.hbs +52 -0
- package/templates/add/worker/internal/platform/queue/client.go.hbs +31 -0
- package/templates/add/worker/internal/platform/queue/server.go.hbs +68 -0
- package/templates/create/base/.env.example.hbs +20 -0
- package/templates/create/base/.github/workflows/ci.yml.hbs +4 -2
- package/templates/create/base/.gitignore.hbs +2 -0
- package/templates/create/base/Makefile.hbs +30 -5
- package/templates/create/base/README.md.hbs +36 -8
- package/templates/create/base/cmd/api/main.go.hbs +26 -1
- package/templates/create/base/internal/platform/database/database.go.hbs +53 -0
- package/templates/create/base/internal/shared/config/config.go.hbs +43 -1
- package/templates/create/base/internal/shared/middleware/cors.go.hbs +29 -0
- package/templates/create/base/internal/shared/middleware/error.go.hbs +13 -1
- package/templates/create/base/migrations/embed.go.hbs +15 -0
- package/templates/create/features/docs/architecture.md.hbs +22 -0
- package/templates/create/features/docs/common/responses.yaml.hbs +15 -0
- package/templates/create/features/docs/observability/metrics.yaml.hbs +12 -0
- package/templates/create/features/docs/openapi.yaml.hbs +13 -0
- package/templates/create/features/docs/techstack.md.hbs +3 -0
- package/templates/create/features/observability/middleware/metrics.go.hbs +41 -0
- package/templates/create/features/observability/middleware/tracing.go.hbs +46 -0
- package/templates/create/features/observability/platform/telemetry/tracing.go.hbs +130 -0
- package/templates/generate/module/handler.go.hbs +20 -3
- package/templates/generate/module/handler_test.go.hbs +49 -6
- package/templates/generate/module/minimal/handler.go.hbs +21 -3
- package/templates/generate/module/minimal/handler_test.go.hbs +53 -6
- package/templates/generate/module/permission.down.sql.hbs +5 -0
- package/templates/generate/module/permission.up.sql.hbs +4 -0
- package/dist/utils/module-paths.js +0 -33
package/dist/index.js
CHANGED
|
@@ -10,12 +10,17 @@ const picocolors_1 = __importDefault(require("picocolors"));
|
|
|
10
10
|
const create_1 = require("./commands/create");
|
|
11
11
|
const generate_1 = require("./commands/generate");
|
|
12
12
|
const method_1 = require("./commands/method");
|
|
13
|
+
const migration_1 = require("./commands/migration");
|
|
13
14
|
const remove_1 = require("./commands/remove");
|
|
15
|
+
const version_1 = require("./utils/version");
|
|
16
|
+
const worker_1 = require("./commands/worker");
|
|
17
|
+
const auth_1 = require("./commands/auth");
|
|
18
|
+
const rbac_1 = require("./commands/rbac");
|
|
14
19
|
const program = new commander_1.Command();
|
|
15
20
|
program
|
|
16
21
|
.name("go-scaffold")
|
|
17
22
|
.description("Scaffold Gin + GORM + Postgres Go backend projects with a consistent domain-module standard")
|
|
18
|
-
.version(
|
|
23
|
+
.version((0, version_1.cliVersion)());
|
|
19
24
|
program
|
|
20
25
|
.command("create [name]")
|
|
21
26
|
.alias("c")
|
|
@@ -23,6 +28,7 @@ program
|
|
|
23
28
|
.option("--defaults", "skip the wizard, use defaults (for CI/scripting)")
|
|
24
29
|
.option("--no-docker", "skip docker-compose.yml (only applies with --defaults)")
|
|
25
30
|
.option("--no-openapi-docs", "skip docs/openapi.yaml (only applies with --defaults)")
|
|
31
|
+
.option("--observability", "add Prometheus /metrics + OpenTelemetry tracing (only applies with --defaults; off by default)")
|
|
26
32
|
.option("--api-prefix <prefix>", 'URL prefix every route is grouped under (default "v1"; pass "" for none)')
|
|
27
33
|
.action(async (name, opts) => {
|
|
28
34
|
try {
|
|
@@ -30,6 +36,7 @@ program
|
|
|
30
36
|
defaults: opts.defaults,
|
|
31
37
|
docker: opts.docker,
|
|
32
38
|
openapiDocs: opts.openapiDocs,
|
|
39
|
+
observability: opts.observability,
|
|
33
40
|
apiPrefix: opts.apiPrefix,
|
|
34
41
|
});
|
|
35
42
|
}
|
|
@@ -51,14 +58,18 @@ const generate = program
|
|
|
51
58
|
choices: [
|
|
52
59
|
{ name: "Module (full CRUD domain)", value: "module" },
|
|
53
60
|
{ name: "Method (add one endpoint to an existing module)", value: "method" },
|
|
61
|
+
{ name: "Migration (reserve a timestamped up/down SQL file pair)", value: "migration" },
|
|
54
62
|
],
|
|
55
63
|
});
|
|
56
64
|
if (target === "module") {
|
|
57
65
|
await (0, generate_1.generateModule)(undefined, { full: true });
|
|
58
66
|
}
|
|
59
|
-
else {
|
|
67
|
+
else if (target === "method") {
|
|
60
68
|
await (0, method_1.generateMethod)(undefined, undefined, {});
|
|
61
69
|
}
|
|
70
|
+
else {
|
|
71
|
+
await (0, migration_1.generateMigration)(undefined);
|
|
72
|
+
}
|
|
62
73
|
}
|
|
63
74
|
catch (err) {
|
|
64
75
|
console.error(picocolors_1.default.red(err.message));
|
|
@@ -70,9 +81,11 @@ generate
|
|
|
70
81
|
.alias("m")
|
|
71
82
|
.description("scaffold a domain module — full CRUD by default, or a bare skeleton with --no-full")
|
|
72
83
|
.option("--no-full", "minimal skeleton (model/errors/repository, no default CRUD) — add endpoints one at a time with `generate method`")
|
|
84
|
+
.option("--auth", "require a valid access token for this module's routes (needs `add auth`)")
|
|
85
|
+
.option("--permission <code>", "also require this permission via authz.Require (needs `add rbac`; implies --auth)")
|
|
73
86
|
.action(async (name, opts) => {
|
|
74
87
|
try {
|
|
75
|
-
await (0, generate_1.generateModule)(name, { full: opts.full });
|
|
88
|
+
await (0, generate_1.generateModule)(name, { full: opts.full, auth: opts.auth, permission: opts.permission });
|
|
76
89
|
}
|
|
77
90
|
catch (err) {
|
|
78
91
|
console.error(picocolors_1.default.red(err.message));
|
|
@@ -107,6 +120,56 @@ generate
|
|
|
107
120
|
process.exitCode = 1;
|
|
108
121
|
}
|
|
109
122
|
});
|
|
123
|
+
generate
|
|
124
|
+
.command("migration [name]")
|
|
125
|
+
.alias("mig")
|
|
126
|
+
.description("reserve a timestamped migrations/<version>_<name>.{up,down}.sql pair (stubs only — you write the SQL)")
|
|
127
|
+
.action(async (name) => {
|
|
128
|
+
try {
|
|
129
|
+
await (0, migration_1.generateMigration)(name);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
console.error(picocolors_1.default.red(err.message));
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
const add = program.command("add").description("add opt-in infrastructure to an existing go-scaffold project");
|
|
137
|
+
add
|
|
138
|
+
.command("worker")
|
|
139
|
+
.description("add Redis, an Asynq task queue, SMTP mail, and cmd/worker (opt-in — most projects don't need this on day one)")
|
|
140
|
+
.action(async () => {
|
|
141
|
+
try {
|
|
142
|
+
await (0, worker_1.addWorker)();
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
console.error(picocolors_1.default.red(err.message));
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
add
|
|
150
|
+
.command("auth")
|
|
151
|
+
.description("add email/password auth: JWT access tokens, Redis-backed refresh token rotation, register/login/refresh/logout/me (requires `add worker` first)")
|
|
152
|
+
.action(async () => {
|
|
153
|
+
try {
|
|
154
|
+
await (0, auth_1.addAuth)();
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
console.error(picocolors_1.default.red(err.message));
|
|
158
|
+
process.exitCode = 1;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
add
|
|
162
|
+
.command("rbac")
|
|
163
|
+
.description("add role-based access control: roles/permissions admin API, cached Authz middleware, PATCH /users/:id/set-role (requires `add auth` first)")
|
|
164
|
+
.action(async () => {
|
|
165
|
+
try {
|
|
166
|
+
await (0, rbac_1.addRbac)();
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
console.error(picocolors_1.default.red(err.message));
|
|
170
|
+
process.exitCode = 1;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
110
173
|
const remove = program
|
|
111
174
|
.command("remove")
|
|
112
175
|
.alias("rm")
|
|
@@ -25,6 +25,10 @@ async function runCreateWizard() {
|
|
|
25
25
|
message: "Include hand-written OpenAPI docs (docs/openapi.yaml, whole docs/ tree served at /docs)?",
|
|
26
26
|
default: true,
|
|
27
27
|
});
|
|
28
|
+
const observability = await (0, prompts_1.confirm)({
|
|
29
|
+
message: "Add metrics + tracing (Prometheus /metrics, OpenTelemetry over OTLP/HTTP for Gin + GORM)?",
|
|
30
|
+
default: false,
|
|
31
|
+
});
|
|
28
32
|
const apiPrefixRaw = await (0, prompts_1.input)({
|
|
29
33
|
message: "API route prefix (e.g. v1, api/v1; leave blank for none):",
|
|
30
34
|
default: "v1",
|
|
@@ -34,10 +38,11 @@ async function runCreateWizard() {
|
|
|
34
38
|
console.log("\nSummary:");
|
|
35
39
|
console.log(` Docker + PostgreSQL: ${docker ? "yes" : "no"}`);
|
|
36
40
|
console.log(` OpenAPI docs: ${openapiDocs ? "yes" : "no"}`);
|
|
41
|
+
console.log(` Metrics + tracing: ${observability ? "yes" : "no"}`);
|
|
37
42
|
console.log(` Route prefix: ${apiPrefix ? `/${apiPrefix}` : "(none)"}`);
|
|
38
43
|
const proceed = await (0, prompts_1.confirm)({ message: "\nCreate project with these settings?", default: true });
|
|
39
44
|
if (!proceed) {
|
|
40
45
|
throw new Error("project creation cancelled");
|
|
41
46
|
}
|
|
42
|
-
return { features: { docker, openapiDocs }, apiPrefix };
|
|
47
|
+
return { features: { docker, openapiDocs, observability }, apiPrefix };
|
|
43
48
|
}
|
|
@@ -4,6 +4,7 @@ exports.promptModuleName = promptModuleName;
|
|
|
4
4
|
exports.promptMethodName = promptMethodName;
|
|
5
5
|
exports.promptMethodType = promptMethodType;
|
|
6
6
|
exports.promptGetMode = promptGetMode;
|
|
7
|
+
exports.promptMigrationName = promptMigrationName;
|
|
7
8
|
exports.promptLookupField = promptLookupField;
|
|
8
9
|
const prompts_1 = require("@inquirer/prompts");
|
|
9
10
|
const naming_1 = require("../utils/naming");
|
|
@@ -53,6 +54,13 @@ async function promptGetMode() {
|
|
|
53
54
|
],
|
|
54
55
|
});
|
|
55
56
|
}
|
|
57
|
+
async function promptMigrationName() {
|
|
58
|
+
const name = await (0, prompts_1.input)({
|
|
59
|
+
message: "Migration name (e.g. add_status_to_orders):",
|
|
60
|
+
validate: (value) => (value.trim() ? true : "migration name is required"),
|
|
61
|
+
});
|
|
62
|
+
return name.trim();
|
|
63
|
+
}
|
|
56
64
|
async function promptLookupField() {
|
|
57
65
|
const field = await (0, prompts_1.input)({
|
|
58
66
|
message: "Lookup field (e.g. email, status, slug):",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AUTH_FILES = void 0;
|
|
4
|
+
// output paths are relative to the project root
|
|
5
|
+
exports.AUTH_FILES = [
|
|
6
|
+
{ template: "add/auth/internal/shared/middleware/auth.go.hbs", output: "internal/shared/middleware/auth.go" },
|
|
7
|
+
{ template: "add/auth/internal/shared/middleware/ratelimit.go.hbs", output: "internal/shared/middleware/ratelimit.go" },
|
|
8
|
+
{ template: "add/auth/internal/app/user/model/user.go.hbs", output: "internal/app/user/model/user.go" },
|
|
9
|
+
{ template: "add/auth/internal/app/user/model/identity.go.hbs", output: "internal/app/user/model/identity.go" },
|
|
10
|
+
{ template: "add/auth/internal/app/user/dto.go.hbs", output: "internal/app/user/dto.go" },
|
|
11
|
+
{ template: "add/auth/internal/app/user/errors.go.hbs", output: "internal/app/user/errors.go" },
|
|
12
|
+
{ template: "add/auth/internal/app/user/jwt.go.hbs", output: "internal/app/user/jwt.go" },
|
|
13
|
+
{ template: "add/auth/internal/app/user/tokenstore.go.hbs", output: "internal/app/user/tokenstore.go" },
|
|
14
|
+
{ template: "add/auth/internal/app/user/repository.go.hbs", output: "internal/app/user/repository.go" },
|
|
15
|
+
{ template: "add/auth/internal/app/user/service.go.hbs", output: "internal/app/user/service.go" },
|
|
16
|
+
{ template: "add/auth/internal/app/user/service_test.go.hbs", output: "internal/app/user/service_test.go" },
|
|
17
|
+
{ template: "add/auth/internal/app/user/handler.go.hbs", output: "internal/app/user/handler.go" },
|
|
18
|
+
{ template: "add/auth/cmd/seed/main.go.hbs", output: "cmd/seed/main.go" },
|
|
19
|
+
];
|
|
@@ -45,6 +45,10 @@ exports.CREATE_MANIFEST = [
|
|
|
45
45
|
template: "create/base/internal/shared/pagination/pagination.go.hbs",
|
|
46
46
|
output: "internal/shared/pagination/pagination.go",
|
|
47
47
|
},
|
|
48
|
+
{
|
|
49
|
+
template: "create/base/internal/shared/middleware/cors.go.hbs",
|
|
50
|
+
output: "internal/shared/middleware/cors.go",
|
|
51
|
+
},
|
|
48
52
|
{
|
|
49
53
|
template: "create/base/internal/shared/middleware/error.go.hbs",
|
|
50
54
|
output: "internal/shared/middleware/error.go",
|
|
@@ -58,6 +62,7 @@ exports.CREATE_MANIFEST = [
|
|
|
58
62
|
output: "internal/shared/middleware/requestid.go",
|
|
59
63
|
},
|
|
60
64
|
{ template: "create/base/migrations/.gitkeep.hbs", output: "migrations/.gitkeep" },
|
|
65
|
+
{ template: "create/base/migrations/embed.go.hbs", output: "migrations/embed.go" },
|
|
61
66
|
// architecture standards docs — always included, this is the point of the CLI
|
|
62
67
|
{
|
|
63
68
|
template: "create/features/docs/architecture.md.hbs",
|
|
@@ -107,4 +112,24 @@ exports.CREATE_MANIFEST = [
|
|
|
107
112
|
output: "docs/health/health-readyz.yaml",
|
|
108
113
|
when: (ctx) => ctx.openapiDocs,
|
|
109
114
|
},
|
|
115
|
+
{
|
|
116
|
+
template: "create/features/docs/observability/metrics.yaml.hbs",
|
|
117
|
+
output: "docs/observability/metrics.yaml",
|
|
118
|
+
when: (ctx) => ctx.openapiDocs && ctx.observability,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
template: "create/features/observability/middleware/metrics.go.hbs",
|
|
122
|
+
output: "internal/shared/middleware/metrics.go",
|
|
123
|
+
when: (ctx) => ctx.observability,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
template: "create/features/observability/middleware/tracing.go.hbs",
|
|
127
|
+
output: "internal/shared/middleware/tracing.go",
|
|
128
|
+
when: (ctx) => ctx.observability,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
template: "create/features/observability/platform/telemetry/tracing.go.hbs",
|
|
132
|
+
output: "internal/platform/telemetry/tracing.go",
|
|
133
|
+
when: (ctx) => ctx.observability,
|
|
134
|
+
},
|
|
110
135
|
];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RBAC_FILES = void 0;
|
|
4
|
+
// output paths are relative to the project root
|
|
5
|
+
exports.RBAC_FILES = [
|
|
6
|
+
{ template: "add/rbac/internal/shared/middleware/authz.go.hbs", output: "internal/shared/middleware/authz.go" },
|
|
7
|
+
{ template: "add/rbac/internal/shared/middleware/authz_test.go.hbs", output: "internal/shared/middleware/authz_test.go" },
|
|
8
|
+
{ template: "add/rbac/internal/app/role/model/role.go.hbs", output: "internal/app/role/model/role.go" },
|
|
9
|
+
{ template: "add/rbac/internal/app/role/model/permission.go.hbs", output: "internal/app/role/model/permission.go" },
|
|
10
|
+
{ template: "add/rbac/internal/app/role/model/role_permission.go.hbs", output: "internal/app/role/model/role_permission.go" },
|
|
11
|
+
{ template: "add/rbac/internal/app/role/repository.go.hbs", output: "internal/app/role/repository.go" },
|
|
12
|
+
{ template: "add/rbac/internal/app/role/service.go.hbs", output: "internal/app/role/service.go" },
|
|
13
|
+
{ template: "add/rbac/internal/app/role/service_test.go.hbs", output: "internal/app/role/service_test.go" },
|
|
14
|
+
{ template: "add/rbac/internal/app/role/handler.go.hbs", output: "internal/app/role/handler.go" },
|
|
15
|
+
{ template: "add/rbac/internal/app/role/dto.go.hbs", output: "internal/app/role/dto.go" },
|
|
16
|
+
{ template: "add/rbac/internal/app/role/errors.go.hbs", output: "internal/app/role/errors.go" },
|
|
17
|
+
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WORKER_FILES = void 0;
|
|
4
|
+
// output paths are relative to the project root
|
|
5
|
+
exports.WORKER_FILES = [
|
|
6
|
+
{ template: "add/worker/internal/platform/cache/redis.go.hbs", output: "internal/platform/cache/redis.go" },
|
|
7
|
+
{ template: "add/worker/internal/platform/queue/client.go.hbs", output: "internal/platform/queue/client.go" },
|
|
8
|
+
{ template: "add/worker/internal/platform/queue/server.go.hbs", output: "internal/platform/queue/server.go" },
|
|
9
|
+
{ template: "add/worker/internal/platform/mail/mail.go.hbs", output: "internal/platform/mail/mail.go" },
|
|
10
|
+
{ template: "add/worker/internal/platform/mail/task.go.hbs", output: "internal/platform/mail/task.go" },
|
|
11
|
+
{ template: "add/worker/cmd/worker/main.go.hbs", output: "cmd/worker/main.go" },
|
|
12
|
+
];
|
|
@@ -0,0 +1,96 @@
|
|
|
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.patchConfigForAuth = patchConfigForAuth;
|
|
7
|
+
exports.patchMainGoForAuth = patchMainGoForAuth;
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const marker_patch_1 = require("./marker-patch");
|
|
10
|
+
const IMPORT_MARKER = "// go-scaffold:imports";
|
|
11
|
+
const CONFIG_FIELDS_MARKER = "// go-scaffold:config-fields";
|
|
12
|
+
const CONFIG_LOAD_MARKER = "// go-scaffold:config-load";
|
|
13
|
+
const CONFIG_CHECKS_MARKER = "// go-scaffold:config-checks";
|
|
14
|
+
const PLATFORM_INIT_MARKER = "// go-scaffold:platform-init";
|
|
15
|
+
const MODEL_MARKER = "// go-scaffold:models";
|
|
16
|
+
const ROUTE_MARKER = "// go-scaffold:routes";
|
|
17
|
+
const SHUTDOWN_MARKER = "// go-scaffold:shutdown";
|
|
18
|
+
// patchConfigForAuth adds JWT/cookie/password-reset/Google OAuth fields to
|
|
19
|
+
// Config, the same marker-based text insertion patchConfigForWorker uses
|
|
20
|
+
// (config.go.hbs is only rendered once, at `create` — everything after that
|
|
21
|
+
// is a real file a human may have already edited).
|
|
22
|
+
function patchConfigForAuth(configGoPath) {
|
|
23
|
+
let content = fs_extra_1.default.readFileSync(configGoPath, "utf8");
|
|
24
|
+
const fieldsBlock = [
|
|
25
|
+
"JWTSecret string",
|
|
26
|
+
"JWTAccessTTL time.Duration",
|
|
27
|
+
"JWTRefreshTTL time.Duration",
|
|
28
|
+
"CookieSecure bool",
|
|
29
|
+
"",
|
|
30
|
+
"PasswordResetTTL time.Duration",
|
|
31
|
+
"PasswordResetURL string",
|
|
32
|
+
"",
|
|
33
|
+
"EmailVerifyTTL time.Duration",
|
|
34
|
+
"EmailVerifyURL string",
|
|
35
|
+
"",
|
|
36
|
+
"GoogleClientID string",
|
|
37
|
+
"GoogleClientSecret string",
|
|
38
|
+
"GoogleRedirectURL string",
|
|
39
|
+
].join("\n");
|
|
40
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_FIELDS_MARKER, fieldsBlock, "JWTSecret string");
|
|
41
|
+
const loadBlock = [
|
|
42
|
+
'JWTSecret: env("JWT_SECRET", "dev-secret-change-me"),',
|
|
43
|
+
'JWTAccessTTL: time.Duration(envInt("JWT_ACCESS_TTL_MIN", 15)) * time.Minute,',
|
|
44
|
+
'JWTRefreshTTL: time.Duration(envInt("JWT_REFRESH_TTL_MIN", 43200)) * time.Minute,',
|
|
45
|
+
'CookieSecure: env("COOKIE_SECURE", "false") == "true",',
|
|
46
|
+
"",
|
|
47
|
+
'PasswordResetTTL: time.Duration(envInt("PASSWORD_RESET_TTL_MIN", 30)) * time.Minute,',
|
|
48
|
+
'PasswordResetURL: env("PASSWORD_RESET_URL", "http://localhost:3000/reset-password"),',
|
|
49
|
+
"",
|
|
50
|
+
'EmailVerifyTTL: time.Duration(envInt("EMAIL_VERIFY_TTL_MIN", 1440)) * time.Minute,',
|
|
51
|
+
'EmailVerifyURL: env("EMAIL_VERIFY_URL", "http://localhost:3000/verify-email"),',
|
|
52
|
+
"",
|
|
53
|
+
'GoogleClientID: env("GOOGLE_CLIENT_ID", ""),',
|
|
54
|
+
'GoogleClientSecret: env("GOOGLE_CLIENT_SECRET", ""),',
|
|
55
|
+
'GoogleRedirectURL: env("GOOGLE_REDIRECT_URL", ""),',
|
|
56
|
+
].join("\n");
|
|
57
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_LOAD_MARKER, loadBlock, 'JWTSecret: env("JWT_SECRET"');
|
|
58
|
+
fs_extra_1.default.writeFileSync(configGoPath, content);
|
|
59
|
+
}
|
|
60
|
+
// patchMainGoForAuth wires the user domain into cmd/api: its import, a
|
|
61
|
+
// queue.Client (needed for the forgot-password email — cmd/api itself never
|
|
62
|
+
// enqueued anything before this), its two models in the AutoMigrate call, a
|
|
63
|
+
// prod guard against the still-default JWT secret, and its route
|
|
64
|
+
// registration (the domain's own Handler.Register splits /auth public vs
|
|
65
|
+
// /users protected — main.go doesn't need to know that split, same
|
|
66
|
+
// convention as every other module).
|
|
67
|
+
function patchMainGoForAuth(mainGoPath, goModule) {
|
|
68
|
+
let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
|
|
69
|
+
const importLine = `"${goModule}/internal/app/user"`;
|
|
70
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, importLine, importLine);
|
|
71
|
+
const modelImportLine = `usermodel "${goModule}/internal/app/user/model"`;
|
|
72
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, modelImportLine, modelImportLine);
|
|
73
|
+
const queueImportLine = `"${goModule}/internal/platform/queue"`;
|
|
74
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, queueImportLine, queueImportLine);
|
|
75
|
+
const mailImportLine = `"${goModule}/internal/platform/mail"`;
|
|
76
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, mailImportLine, mailImportLine);
|
|
77
|
+
const checkBlock = [
|
|
78
|
+
'if cfg.IsProd() && cfg.JWTSecret == "dev-secret-change-me" {',
|
|
79
|
+
'\tlogger.Error("JWT_SECRET is still the dev default — set a real secret before deploying with APP_ENV=production")',
|
|
80
|
+
"\tos.Exit(1)",
|
|
81
|
+
"}",
|
|
82
|
+
].join("\n");
|
|
83
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_CHECKS_MARKER, checkBlock, "JWT_SECRET is still the dev default");
|
|
84
|
+
const queueInitBlock = ["q, err := queue.NewClient(cfg.RedisURL)", "if err != nil {", '\tlogger.Error("open queue", "error", err)', "\tos.Exit(1)", "}"].join("\n");
|
|
85
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PLATFORM_INIT_MARKER, queueInitBlock, "q, err := queue.NewClient(cfg.RedisURL)");
|
|
86
|
+
const migrateLine1 = "&usermodel.User{},";
|
|
87
|
+
const migrateLine2 = "&usermodel.Identity{},";
|
|
88
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, MODEL_MARKER, migrateLine1, migrateLine1);
|
|
89
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, MODEL_MARKER, migrateLine2, migrateLine2);
|
|
90
|
+
const routeLine = "user.NewHandler(user.NewService(user.NewRepository(db), user.NewRedisTokenStore(rdb), mail.NewAsyncClient(q), cfg), cfg.JWTSecret, cfg.JWTRefreshTTL, cfg.CookieSecure, rdb).Register(api)";
|
|
91
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, ROUTE_MARKER, routeLine, routeLine);
|
|
92
|
+
content = content.replace(/\n\t_ = api \/\/ dropped once `generate module` registers the first route\n/, "\n");
|
|
93
|
+
const shutdownBlock = ["if err := q.Close(); err != nil {", '\tlogger.Error("close queue", "error", err)', "}"].join("\n");
|
|
94
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SHUTDOWN_MARKER, shutdownBlock, "if err := q.Close()");
|
|
95
|
+
fs_extra_1.default.writeFileSync(mainGoPath, content);
|
|
96
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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.typeChecks = typeChecks;
|
|
7
|
+
exports.assertNoDrift = assertNoDrift;
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
const version_1 = require("./version");
|
|
11
|
+
// typeChecks runs `go vet ./...` in projectRoot. Returns null when there's no
|
|
12
|
+
// Go toolchain on PATH — the caller can't conclude anything either way then,
|
|
13
|
+
// the same graceful skip as gofmtTree.
|
|
14
|
+
//
|
|
15
|
+
// `go vet` rather than `go build`: build skips _test.go files entirely, and the
|
|
16
|
+
// generated handler_test.go is exactly where a shared/ signature change lands
|
|
17
|
+
// first (it constructs the middleware chain by hand). vet type-checks tests
|
|
18
|
+
// too, so it sees what build would miss.
|
|
19
|
+
function typeChecks(projectRoot) {
|
|
20
|
+
try {
|
|
21
|
+
(0, child_process_1.execFileSync)("go", ["version"], { stdio: "ignore" });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
(0, child_process_1.execFileSync)("go", ["vet", "./..."], { cwd: projectRoot, stdio: ["ignore", "pipe", "pipe"] });
|
|
28
|
+
return { ok: true, output: "" };
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
const e = err;
|
|
32
|
+
return { ok: false, output: `${e.stderr?.toString() ?? ""}${e.stdout?.toString() ?? ""}`.trim() };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// assertNoDrift is the second half of a before/after pair: given what
|
|
36
|
+
// typeChecks said *before* the files were written, it re-checks and fails
|
|
37
|
+
// loudly if generating is what broke the project.
|
|
38
|
+
//
|
|
39
|
+
// Why compare instead of just checking afterwards: a project can be mid-refactor
|
|
40
|
+
// (or simply not have run `go mod tidy` yet), and blaming the generator for a
|
|
41
|
+
// break it didn't cause is worse than staying quiet. Only the
|
|
42
|
+
// passed-before → broken-after transition is unambiguously ours.
|
|
43
|
+
//
|
|
44
|
+
// The failure this exists for: `generate` renders templates pinned to the
|
|
45
|
+
// shared/ layer that `create` emits, so once a project edits that layer — which
|
|
46
|
+
// is normal, expected work — the generated code stops compiling against it.
|
|
47
|
+
// Without this check that lands as a mystery build error some time later, in
|
|
48
|
+
// files the user never wrote.
|
|
49
|
+
function assertNoDrift(projectRoot, before, config) {
|
|
50
|
+
if (before === null || !before.ok)
|
|
51
|
+
return; // no Go here, or already broken — not ours to judge
|
|
52
|
+
const after = typeChecks(projectRoot);
|
|
53
|
+
if (after === null || after.ok)
|
|
54
|
+
return;
|
|
55
|
+
const scaffoldedWith = config.scaffoldVersion ?? "unknown (predates version stamping)";
|
|
56
|
+
throw new Error(`${picocolors_1.default.red("the generated code doesn't compile, but this project was fine a moment ago.")}\n\n` +
|
|
57
|
+
`The most likely cause is drift: this project's internal/shared layer has been edited\n` +
|
|
58
|
+
`since it was scaffolded, so the templates this CLI emits no longer match it.\n\n` +
|
|
59
|
+
` scaffolded with: go-scaffold ${scaffoldedWith}\n` +
|
|
60
|
+
` this CLI: go-scaffold ${(0, version_1.cliVersion)()}\n\n` +
|
|
61
|
+
`${picocolors_1.default.dim("go vet ./... says:")}\n${after.output}\n\n` +
|
|
62
|
+
`The generated files were left in place — reconcile them with your shared/ layer by\n` +
|
|
63
|
+
`hand, or undo (\`go-scaffold remove module <name>\` for a module) and generate again\n` +
|
|
64
|
+
`with a CLI version that matches this project.`);
|
|
65
|
+
}
|
|
@@ -17,6 +17,11 @@ const UNUSED_API_LINE = "_ = api // dropped once `generate module` registers the
|
|
|
17
17
|
// unpatchMainGo removes precisely what patch added.
|
|
18
18
|
function mainGoLines(patch) {
|
|
19
19
|
const modelAlias = `${patch.pkg}model`; // every domain's model subpackage is named "model"
|
|
20
|
+
const handlerArgs = [`${patch.pkg}.NewService(${patch.pkg}.NewRepository(db))`];
|
|
21
|
+
if (patch.auth)
|
|
22
|
+
handlerArgs.push("cfg.JWTSecret");
|
|
23
|
+
if (patch.permission)
|
|
24
|
+
handlerArgs.push("authz");
|
|
20
25
|
return {
|
|
21
26
|
importLine: `"${patch.goModule}/internal/app/${patch.modulePath}"`,
|
|
22
27
|
modelImportLine: `${modelAlias} "${patch.goModule}/internal/app/${patch.modulePath}/model"`,
|
|
@@ -24,7 +29,9 @@ function mainGoLines(patch) {
|
|
|
24
29
|
// `api` is the one route group declared by main.go.hbs, prefixed with
|
|
25
30
|
// whatever apiPrefix the project chose at create time (e.g. /v1, /api,
|
|
26
31
|
// or none) — every module registers on it, there is no per-module choice.
|
|
27
|
-
|
|
32
|
+
// `authz` only exists in main.go once `add rbac` has run — patch.permission
|
|
33
|
+
// is only ever set once that's already been verified by the caller.
|
|
34
|
+
routeLine: `${patch.pkg}.NewHandler(${handlerArgs.join(", ")}).Register(api)`,
|
|
28
35
|
};
|
|
29
36
|
}
|
|
30
37
|
// patchMainGo wires a newly generated module into cmd/api/main.go: its
|
package/dist/utils/migrations.js
CHANGED
|
@@ -3,15 +3,37 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.newMigrationVersion = newMigrationVersion;
|
|
7
7
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
-
// golang-migrate
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
// golang-migrate timestamp numbering — the same convention as the CLI's own
|
|
9
|
+
// `migrate create -ext sql -dir migrations -seq=false <name>`: a 14-digit
|
|
10
|
+
// UTC timestamp (YYYYMMDDHHMMSS). Two people branching from the same base and
|
|
11
|
+
// each adding a migration get different filenames instead of both claiming
|
|
12
|
+
// the next sequential number and colliding on merge. golang-migrate orders
|
|
13
|
+
// by the numeric prefix either way, and a 14-digit timestamp always sorts
|
|
14
|
+
// after any existing 6-digit sequential number, so a project with old-style
|
|
15
|
+
// numbers already in migrations/ is safe to keep generating into.
|
|
16
|
+
function newMigrationVersion(migrationsDir) {
|
|
17
|
+
const existing = new Set((fs_extra_1.default.existsSync(migrationsDir) ? fs_extra_1.default.readdirSync(migrationsDir) : [])
|
|
12
18
|
.map((f) => f.match(/^(\d+)_/))
|
|
13
19
|
.filter((m) => m !== null)
|
|
14
|
-
.map((m) =>
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
.map((m) => m[1]));
|
|
21
|
+
let d = new Date();
|
|
22
|
+
let version = formatVersion(d);
|
|
23
|
+
// Vanishingly unlikely in normal (human-driven) use, but guard the
|
|
24
|
+
// same-second edge case rather than silently overwrite a sibling file.
|
|
25
|
+
while (existing.has(version)) {
|
|
26
|
+
d = new Date(d.getTime() + 1000);
|
|
27
|
+
version = formatVersion(d);
|
|
28
|
+
}
|
|
29
|
+
return version;
|
|
30
|
+
}
|
|
31
|
+
function formatVersion(d) {
|
|
32
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
33
|
+
return (String(d.getUTCFullYear()) +
|
|
34
|
+
pad(d.getUTCMonth() + 1) +
|
|
35
|
+
pad(d.getUTCDate()) +
|
|
36
|
+
pad(d.getUTCHours()) +
|
|
37
|
+
pad(d.getUTCMinutes()) +
|
|
38
|
+
pad(d.getUTCSeconds()));
|
|
17
39
|
}
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.patchOpenapiIndex = patchOpenapiIndex;
|
|
7
7
|
exports.unpatchOpenapiIndex = unpatchOpenapiIndex;
|
|
8
|
+
exports.patchOpenapiIndexRaw = patchOpenapiIndexRaw;
|
|
8
9
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
10
|
const marker_patch_1 = require("./marker-patch");
|
|
10
11
|
const PATHS_MARKER = "# go-scaffold:paths";
|
|
@@ -45,3 +46,18 @@ function unpatchOpenapiIndex(openapiPath, naming, apiPrefix) {
|
|
|
45
46
|
const { paths, schemas } = openapiLines(naming, apiPrefix);
|
|
46
47
|
fs_extra_1.default.writeFileSync(openapiPath, (0, marker_patch_1.removeLines)(content, [...paths, ...schemas]));
|
|
47
48
|
}
|
|
49
|
+
// patchOpenapiIndexRaw wires hand-written path docs into the index — used by
|
|
50
|
+
// `add auth`/`add rbac`, whose endpoints aren't a single CRUD resource so
|
|
51
|
+
// there's no ModuleNaming to derive lines from. Each entry is patched with
|
|
52
|
+
// its own sentinel (the path key) rather than one block for all of them, so
|
|
53
|
+
// re-running `add rbac` after a partial failure doesn't skip entries that
|
|
54
|
+
// never made it in.
|
|
55
|
+
function patchOpenapiIndexRaw(openapiPath, apiPrefix, entries) {
|
|
56
|
+
let content = fs_extra_1.default.readFileSync(openapiPath, "utf8");
|
|
57
|
+
for (const { urlPath, file } of entries) {
|
|
58
|
+
const key = `${apiPrefix ? `/${apiPrefix}` : ""}${urlPath}:`;
|
|
59
|
+
const block = `${key}\n $ref: '${file}'`;
|
|
60
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PATHS_MARKER, block, key);
|
|
61
|
+
}
|
|
62
|
+
fs_extra_1.default.writeFileSync(openapiPath, content);
|
|
63
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
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.patchConfigForWorker = patchConfigForWorker;
|
|
7
|
+
exports.patchMainGoForWorker = patchMainGoForWorker;
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const marker_patch_1 = require("./marker-patch");
|
|
10
|
+
const IMPORT_MARKER = "// go-scaffold:imports";
|
|
11
|
+
const CONFIG_FIELDS_MARKER = "// go-scaffold:config-fields";
|
|
12
|
+
const CONFIG_LOAD_MARKER = "// go-scaffold:config-load";
|
|
13
|
+
const PLATFORM_INIT_MARKER = "// go-scaffold:platform-init";
|
|
14
|
+
const READYZ_MARKER = "// go-scaffold:readyz-checks";
|
|
15
|
+
const SHUTDOWN_MARKER = "// go-scaffold:shutdown";
|
|
16
|
+
// patchConfigForWorker adds RedisURL + SMTP_* fields to Config and their
|
|
17
|
+
// env() loads to Load() — via marker comments, the same text-insertion
|
|
18
|
+
// approach as patchMainGo, since config.go.hbs is only rendered once (at
|
|
19
|
+
// `create`) and everything after that is a real file a human may have
|
|
20
|
+
// already edited.
|
|
21
|
+
function patchConfigForWorker(configGoPath) {
|
|
22
|
+
let content = fs_extra_1.default.readFileSync(configGoPath, "utf8");
|
|
23
|
+
const fieldsBlock = ["RedisURL string", "", "SMTPHost string", "SMTPPort string", "SMTPUsername string", "SMTPPassword string", "SMTPFrom string"].join("\n");
|
|
24
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_FIELDS_MARKER, fieldsBlock, "RedisURL string");
|
|
25
|
+
const loadBlock = [
|
|
26
|
+
'RedisURL: env("REDIS_URL", "redis://localhost:6379/0"),',
|
|
27
|
+
"",
|
|
28
|
+
'SMTPHost: env("SMTP_HOST", ""),',
|
|
29
|
+
'SMTPPort: env("SMTP_PORT", "587"),',
|
|
30
|
+
'SMTPUsername: env("SMTP_USERNAME", ""),',
|
|
31
|
+
'SMTPPassword: env("SMTP_PASSWORD", ""),',
|
|
32
|
+
'SMTPFrom: env("SMTP_FROM", "no-reply@example.local"),',
|
|
33
|
+
].join("\n");
|
|
34
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_LOAD_MARKER, loadBlock, 'RedisURL: env("REDIS_URL"');
|
|
35
|
+
fs_extra_1.default.writeFileSync(configGoPath, content);
|
|
36
|
+
}
|
|
37
|
+
// patchMainGoForWorker wires Redis into cmd/api: opened alongside the DB, and
|
|
38
|
+
// pinged as part of /readyz (so a Redis outage is caught the same way a DB
|
|
39
|
+
// outage already is). It does not create a queue.Client — nothing in cmd/api
|
|
40
|
+
// enqueues a task until some domain actually needs to (e.g. a future `add
|
|
41
|
+
// auth`'s forgot-password flow); an unused *queue.Client sitting in main()
|
|
42
|
+
// would just be dead weight until then.
|
|
43
|
+
function patchMainGoForWorker(mainGoPath, goModule) {
|
|
44
|
+
let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
|
|
45
|
+
const cacheImport = `"${goModule}/internal/platform/cache"`;
|
|
46
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, cacheImport, cacheImport);
|
|
47
|
+
const initBlock = ["rdb, err := cache.Open(cfg)", "if err != nil {", '\tlogger.Error("open redis", "error", err)', "\tos.Exit(1)", "}"].join("\n");
|
|
48
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PLATFORM_INIT_MARKER, initBlock, "rdb, err := cache.Open(cfg)");
|
|
49
|
+
const readyzBlock = [
|
|
50
|
+
"if err := rdb.Ping(c.Request.Context()).Err(); err != nil {",
|
|
51
|
+
'\tc.JSON(http.StatusServiceUnavailable, gin.H{"status": "unavailable"})',
|
|
52
|
+
"\treturn",
|
|
53
|
+
"}",
|
|
54
|
+
].join("\n");
|
|
55
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, READYZ_MARKER, readyzBlock, "if err := rdb.Ping(");
|
|
56
|
+
const shutdownBlock = ["if err := rdb.Close(); err != nil {", '\tlogger.Error("close redis", "error", err)', "}"].join("\n");
|
|
57
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SHUTDOWN_MARKER, shutdownBlock, "if err := rdb.Close()");
|
|
58
|
+
fs_extra_1.default.writeFileSync(mainGoPath, content);
|
|
59
|
+
}
|