@nakedev/go-scaffold 0.1.2 → 0.1.4
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 +93 -14
- package/dist/commands/auth.js +129 -0
- package/dist/commands/create.js +3 -2
- package/dist/commands/generate.js +61 -2
- package/dist/commands/method.js +66 -15
- package/dist/commands/migration.js +34 -0
- package/dist/commands/rbac.js +103 -0
- package/dist/commands/remove.js +36 -20
- package/dist/commands/worker.js +75 -0
- package/dist/index.js +71 -7
- 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/module-manifest.js +2 -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/method-patcher.js +80 -16
- package/dist/utils/migrations.js +30 -8
- package/dist/utils/module-location.js +22 -0
- package/dist/utils/naming.js +75 -12
- package/dist/utils/openapi-patcher.js +35 -1
- package/dist/utils/platform-patcher.js +59 -0
- package/dist/utils/rbac-patcher.js +277 -0
- package/dist/utils/smoke-run.js +31 -0
- package/dist/utils/version.js +24 -0
- package/package.json +15 -6
- package/scripts/smoke-test.mjs +2058 -0
- 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 +19 -4
- package/templates/create/base/.gitignore.hbs +2 -0
- package/templates/create/base/AGENTS.md.hbs +10 -12
- package/templates/create/base/Makefile.hbs +39 -8
- package/templates/create/base/README.md.hbs +52 -12
- 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 +17 -5
- package/templates/create/features/docs/patterns.md.hbs +9 -6
- 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/docs/item.yaml.hbs +3 -3
- package/templates/generate/module/handler.go.hbs +37 -6
- package/templates/generate/module/handler_test.go.hbs +113 -51
- package/templates/generate/module/migration.down.sql.hbs +1 -1
- package/templates/generate/module/migration.up.sql.hbs +1 -1
- package/templates/generate/module/minimal/handler.go.hbs +28 -4
- package/templates/generate/module/minimal/handler_test.go.hbs +5 -65
- package/templates/generate/module/minimal/service_test.go.hbs +39 -16
- package/templates/generate/module/model/model.go.hbs +7 -0
- package/templates/generate/module/permission.down.sql.hbs +5 -0
- package/templates/generate/module/permission.up.sql.hbs +4 -0
- package/templates/generate/module/repository_test.go.hbs +79 -0
- package/templates/generate/module/service.go.hbs +6 -4
- package/templates/generate/module/service_test.go.hbs +68 -17
- package/tests/integration/default-module.test.mjs +46 -0
- package/tests/integration/generator-naming.test.mjs +81 -0
- package/tests/integration/generator-unit-test-seams.test.mjs +91 -0
- package/tests/integration/legacy-method-compat.test.mjs +222 -0
- package/tests/integration/remove-module.test.mjs +58 -0
- package/tests/unit/naming.test.mjs +94 -0
- package/tests/unit/smoke-isolation.test.mjs +35 -0
- package/dist/utils/module-paths.js +0 -33
|
@@ -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
|
|
@@ -12,9 +12,12 @@ const DTO_MARKER = "// go-scaffold:dto";
|
|
|
12
12
|
const REPO_INTERFACE_MARKER = "// go-scaffold:repository-interface";
|
|
13
13
|
const REPO_IMPL_MARKER = "// go-scaffold:repository-methods";
|
|
14
14
|
const SERVICE_MARKER = "// go-scaffold:service-methods";
|
|
15
|
+
const SERVICE_INTERFACE_MARKER = "// go-scaffold:service-interface";
|
|
15
16
|
const HANDLER_ROUTES_MARKER = "// go-scaffold:handler-routes";
|
|
16
17
|
const HANDLER_FUNCS_MARKER = "// go-scaffold:handler-funcs";
|
|
17
|
-
const
|
|
18
|
+
const REPOSITORY_STUB_FIELDS_MARKER = "// go-scaffold:repository-stub-fields";
|
|
19
|
+
const REPOSITORY_STUB_METHODS_MARKER = "// go-scaffold:repository-stub-methods";
|
|
20
|
+
const LEGACY_FAKE_REPO_METHODS_MARKER = "// go-scaffold:fake-repo-methods";
|
|
18
21
|
const UNUSED_G_LINE = "\t_ = g\n";
|
|
19
22
|
// writeHandler ensures whatever packages the new handler code references are
|
|
20
23
|
// imported (a minimal module starts with only "gin" imported) and drops the
|
|
@@ -62,6 +65,48 @@ function patchMethod(paths, naming, method, opts, goModule) {
|
|
|
62
65
|
else {
|
|
63
66
|
patchDelete(paths, method, goModule);
|
|
64
67
|
}
|
|
68
|
+
patchHandlerServiceInterface(paths.handlerPath, naming, method, opts, goModule);
|
|
69
|
+
}
|
|
70
|
+
function patchHandlerServiceInterface(handlerPath, naming, method, opts, goModule) {
|
|
71
|
+
let signature;
|
|
72
|
+
let needsModel = true;
|
|
73
|
+
let needsUUID = false;
|
|
74
|
+
if (opts.type === "get" && opts.getMode === "all") {
|
|
75
|
+
signature = `${method.pascalName}(context.Context, int, int) ([]model.${naming.pascalName}, error)`;
|
|
76
|
+
}
|
|
77
|
+
else if (opts.type === "get") {
|
|
78
|
+
signature = `${method.pascalName}(context.Context, string) (*model.${naming.pascalName}, error)`;
|
|
79
|
+
}
|
|
80
|
+
else if (opts.type === "post") {
|
|
81
|
+
signature = `${method.pascalName}(context.Context, ${method.pascalName}Input) (*model.${naming.pascalName}, error)`;
|
|
82
|
+
}
|
|
83
|
+
else if (opts.type === "put" || opts.type === "patch") {
|
|
84
|
+
signature = `${method.pascalName}(context.Context, uuid.UUID) (*model.${naming.pascalName}, error)`;
|
|
85
|
+
needsUUID = true;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
signature = `${method.pascalName}(context.Context, uuid.UUID) error`;
|
|
89
|
+
needsModel = false;
|
|
90
|
+
needsUUID = true;
|
|
91
|
+
}
|
|
92
|
+
let handler = fs_extra_1.default.readFileSync(handlerPath, "utf8");
|
|
93
|
+
if (!(0, marker_patch_1.hasMarker)(handler, SERVICE_INTERFACE_MARKER)) {
|
|
94
|
+
// Projects scaffolded before the narrow service interface stored *Service
|
|
95
|
+
// directly. The concrete type already exposes generated methods, so there
|
|
96
|
+
// is no interface declaration to patch and no migration is required.
|
|
97
|
+
if (handler.includes("svc *Service"))
|
|
98
|
+
return;
|
|
99
|
+
throw new Error(`marker "${SERVICE_INTERFACE_MARKER}" not found and Handler does not use legacy *Service wiring`);
|
|
100
|
+
}
|
|
101
|
+
handler = (0, marker_patch_1.insertBeforeMarker)(handler, SERVICE_INTERFACE_MARKER, signature);
|
|
102
|
+
handler = (0, marker_patch_1.ensureImport)(handler, "context");
|
|
103
|
+
if (needsModel) {
|
|
104
|
+
handler = (0, marker_patch_1.ensureImport)(handler, `${goModule}/internal/app/${naming.pkg}/model`);
|
|
105
|
+
}
|
|
106
|
+
if (needsUUID) {
|
|
107
|
+
handler = (0, marker_patch_1.ensureImport)(handler, "github.com/google/uuid");
|
|
108
|
+
}
|
|
109
|
+
fs_extra_1.default.writeFileSync(handlerPath, handler);
|
|
65
110
|
}
|
|
66
111
|
function patchGetAll(paths, naming, method, goModule) {
|
|
67
112
|
let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
|
|
@@ -117,22 +162,40 @@ function patchGetOne(paths, naming, method, rawField, goModule) {
|
|
|
117
162
|
fs_extra_1.default.writeFileSync(paths.repositoryPath, repo);
|
|
118
163
|
let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
|
|
119
164
|
service = (0, marker_patch_1.insertBeforeMarker)(service, REPO_INTERFACE_MARKER, `FindBy${fieldPascal}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error)`);
|
|
120
|
-
//
|
|
121
|
-
//
|
|
165
|
+
// The repository interface just grew, so the test double needs a matching
|
|
166
|
+
// method or focused service tests stop compiling. New projects use a
|
|
167
|
+
// function-backed stub; projects scaffolded before that refactor retain the
|
|
168
|
+
// old fakeRepo marker and error behavior.
|
|
122
169
|
let serviceTest = fs_extra_1.default.readFileSync(paths.serviceTestPath, "utf8");
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
170
|
+
if ((0, marker_patch_1.hasMarker)(serviceTest, REPOSITORY_STUB_FIELDS_MARKER) &&
|
|
171
|
+
(0, marker_patch_1.hasMarker)(serviceTest, REPOSITORY_STUB_METHODS_MARKER)) {
|
|
172
|
+
serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, REPOSITORY_STUB_FIELDS_MARKER, `findBy${fieldPascal}Fn func(context.Context, string) (*model.${naming.pascalName}, error)`);
|
|
173
|
+
serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, REPOSITORY_STUB_METHODS_MARKER, [
|
|
174
|
+
`//nolint:unused`,
|
|
175
|
+
`func (s *repositoryStub) FindBy${fieldPascal}(ctx context.Context, value string) (*model.${naming.pascalName}, error) {`,
|
|
176
|
+
`\tif s.findBy${fieldPascal}Fn == nil {`,
|
|
177
|
+
`\t\tpanic("unexpected repository.FindBy${fieldPascal} call")`,
|
|
178
|
+
`\t}`,
|
|
179
|
+
`\treturn s.findBy${fieldPascal}Fn(ctx, value)`,
|
|
180
|
+
`}`,
|
|
181
|
+
``,
|
|
182
|
+
].join("\n"));
|
|
183
|
+
}
|
|
184
|
+
else if ((0, marker_patch_1.hasMarker)(serviceTest, LEGACY_FAKE_REPO_METHODS_MARKER)) {
|
|
185
|
+
serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, LEGACY_FAKE_REPO_METHODS_MARKER, [
|
|
186
|
+
`//nolint:unused`,
|
|
187
|
+
`func (f *fakeRepo) FindBy${fieldPascal}(context.Context, string) (*model.${naming.pascalName}, error) {`,
|
|
188
|
+
`\tif f.err != nil {`,
|
|
189
|
+
`\t\treturn nil, f.err`,
|
|
190
|
+
`\t}`,
|
|
191
|
+
`\treturn f.m, nil`,
|
|
192
|
+
`}`,
|
|
193
|
+
``,
|
|
194
|
+
].join("\n"));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
throw new Error(`neither current repository stub markers nor legacy "${LEGACY_FAKE_REPO_METHODS_MARKER}" found in ${paths.serviceTestPath}`);
|
|
198
|
+
}
|
|
136
199
|
fs_extra_1.default.writeFileSync(paths.serviceTestPath, serviceTest);
|
|
137
200
|
service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
|
|
138
201
|
`func (s *Service) ${method.pascalName}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error) {`,
|
|
@@ -266,6 +329,7 @@ function markersPresent(handlerPath, servicePath) {
|
|
|
266
329
|
const service = fs_extra_1.default.readFileSync(servicePath, "utf8");
|
|
267
330
|
return ((0, marker_patch_1.hasMarker)(handler, HANDLER_ROUTES_MARKER) &&
|
|
268
331
|
(0, marker_patch_1.hasMarker)(handler, HANDLER_FUNCS_MARKER) &&
|
|
332
|
+
((0, marker_patch_1.hasMarker)(handler, SERVICE_INTERFACE_MARKER) || handler.includes("svc *Service")) &&
|
|
269
333
|
(0, marker_patch_1.hasMarker)(service, SERVICE_MARKER) &&
|
|
270
334
|
(0, marker_patch_1.hasMarker)(service, REPO_INTERFACE_MARKER));
|
|
271
335
|
}
|
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
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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.existingModulePackages = existingModulePackages;
|
|
7
|
+
exports.resolveProjectModuleNaming = resolveProjectModuleNaming;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
const naming_1 = require("./naming");
|
|
11
|
+
function existingModulePackages(projectDir) {
|
|
12
|
+
const appDir = path_1.default.join(projectDir, "internal", "app");
|
|
13
|
+
if (!fs_extra_1.default.existsSync(appDir))
|
|
14
|
+
return [];
|
|
15
|
+
return fs_extra_1.default
|
|
16
|
+
.readdirSync(appDir, { withFileTypes: true })
|
|
17
|
+
.filter((entry) => entry.isDirectory())
|
|
18
|
+
.map((entry) => entry.name);
|
|
19
|
+
}
|
|
20
|
+
function resolveProjectModuleNaming(projectDir, rawName) {
|
|
21
|
+
return (0, naming_1.resolveExistingModuleNaming)(rawName, existingModulePackages(projectDir));
|
|
22
|
+
}
|
package/dist/utils/naming.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.pluralize = pluralize;
|
|
4
7
|
exports.toPascalCase = toPascalCase;
|
|
@@ -13,18 +16,14 @@ exports.validateModuleName = validateModuleName;
|
|
|
13
16
|
exports.normalizeApiPrefix = normalizeApiPrefix;
|
|
14
17
|
exports.validateApiPrefix = validateApiPrefix;
|
|
15
18
|
exports.resolveModuleNaming = resolveModuleNaming;
|
|
19
|
+
exports.resolveExistingModuleNaming = resolveExistingModuleNaming;
|
|
16
20
|
exports.resolveMethodNaming = resolveMethodNaming;
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
21
|
+
const pluralize_1 = __importDefault(require("pluralize"));
|
|
22
|
+
// Pluralization must be idempotent because module names may come from the
|
|
23
|
+
// interactive prompt (singular) or a documented/scripted command (often
|
|
24
|
+
// plural). Keep this wrapper exported for callers that only need inflection.
|
|
20
25
|
function pluralize(word) {
|
|
21
|
-
|
|
22
|
-
return word + "es";
|
|
23
|
-
if (/[^aeiou]y$/.test(word))
|
|
24
|
-
return word.slice(0, -1) + "ies";
|
|
25
|
-
if (word.endsWith("s"))
|
|
26
|
-
return word;
|
|
27
|
-
return word + "s";
|
|
26
|
+
return pluralize_1.default.plural(word);
|
|
28
27
|
}
|
|
29
28
|
function toPascalCase(value) {
|
|
30
29
|
return value
|
|
@@ -98,7 +97,7 @@ function assertNotGoKeyword(ident, role) {
|
|
|
98
97
|
// shadowing the builtin in main.go). Returns true|message for inquirer, and
|
|
99
98
|
// backs the assert in resolveModuleNaming — one source of truth for both.
|
|
100
99
|
function validateModuleName(rawName) {
|
|
101
|
-
const pkg = toPackageName(rawName);
|
|
100
|
+
const pkg = toPackageName(pluralize_1.default.singular(toKebabCase(rawName)));
|
|
102
101
|
if (!pkg)
|
|
103
102
|
return `invalid module name: "${rawName}" (must contain letters/numbers)`;
|
|
104
103
|
if (/^[0-9]/.test(pkg)) {
|
|
@@ -134,15 +133,79 @@ function resolveModuleNaming(rawName) {
|
|
|
134
133
|
const check = validateModuleName(rawName);
|
|
135
134
|
if (check !== true)
|
|
136
135
|
throw new Error(check);
|
|
136
|
+
const singular = pluralize_1.default.singular(toKebabCase(rawName));
|
|
137
|
+
const pkg = toPackageName(singular);
|
|
138
|
+
const plural = pluralize_1.default.plural(singular);
|
|
139
|
+
return {
|
|
140
|
+
name: singular,
|
|
141
|
+
pkg,
|
|
142
|
+
pascalName: toPascalCase(singular),
|
|
143
|
+
plural,
|
|
144
|
+
tableName: toDbName(plural),
|
|
145
|
+
errorPrefix: toDbName(singular).toUpperCase(),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function legacyPluralize(word) {
|
|
149
|
+
if (/[sxz]$/.test(word) || /[^aeiou](ch|sh)$/.test(word))
|
|
150
|
+
return word + "es";
|
|
151
|
+
if (/[^aeiou]y$/.test(word))
|
|
152
|
+
return word.slice(0, -1) + "ies";
|
|
153
|
+
if (word.endsWith("s"))
|
|
154
|
+
return word;
|
|
155
|
+
return word + "s";
|
|
156
|
+
}
|
|
157
|
+
function resolveLegacyModuleNaming(rawName) {
|
|
137
158
|
const pkg = toPackageName(rawName);
|
|
159
|
+
const plural = legacyPluralize(pkg);
|
|
138
160
|
return {
|
|
139
161
|
name: pkg,
|
|
140
162
|
pkg,
|
|
141
163
|
pascalName: toPascalCase(pkg),
|
|
142
|
-
plural
|
|
164
|
+
plural,
|
|
165
|
+
tableName: toDbName(plural),
|
|
143
166
|
errorPrefix: pkg.toUpperCase(),
|
|
144
167
|
};
|
|
145
168
|
}
|
|
169
|
+
// Projects generated before canonical inflection used the raw input as the Go
|
|
170
|
+
// package name. Prefer the canonical package when present, but keep locating
|
|
171
|
+
// legacy plural packages so upgrade does not make method/remove commands lose
|
|
172
|
+
// sight of existing code. Two matches are unsafe: silently choosing one can
|
|
173
|
+
// patch or delete the wrong module.
|
|
174
|
+
function resolveExistingModuleNaming(rawName, existingPackages) {
|
|
175
|
+
const requestedLegacy = resolveLegacyModuleNaming(rawName);
|
|
176
|
+
const existing = new Set(existingPackages);
|
|
177
|
+
let canonical;
|
|
178
|
+
try {
|
|
179
|
+
canonical = resolveModuleNaming(rawName);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (existing.has(requestedLegacy.pkg))
|
|
183
|
+
return requestedLegacy;
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
const aliases = existingPackages.filter((pkg) => {
|
|
187
|
+
if (pkg === canonical.pkg)
|
|
188
|
+
return false;
|
|
189
|
+
try {
|
|
190
|
+
return resolveModuleNaming(pkg).pkg === canonical.pkg;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
const matches = [
|
|
197
|
+
...(existing.has(canonical.pkg) ? [canonical.pkg] : []),
|
|
198
|
+
...aliases,
|
|
199
|
+
];
|
|
200
|
+
if (matches.length > 1) {
|
|
201
|
+
throw new Error(`ambiguous module "${rawName}": ${matches.map((pkg) => `internal/app/${pkg}`).join(" and ")} exist`);
|
|
202
|
+
}
|
|
203
|
+
if (matches[0] === canonical.pkg)
|
|
204
|
+
return canonical;
|
|
205
|
+
if (matches[0])
|
|
206
|
+
return resolveLegacyModuleNaming(matches[0]);
|
|
207
|
+
return canonical;
|
|
208
|
+
}
|
|
146
209
|
function resolveMethodNaming(rawName) {
|
|
147
210
|
const cleaned = rawName.trim();
|
|
148
211
|
const pascalName = toPascalCase(cleaned);
|
|
@@ -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";
|
|
@@ -43,5 +44,38 @@ function patchOpenapiIndex(openapiPath, naming, apiPrefix) {
|
|
|
43
44
|
function unpatchOpenapiIndex(openapiPath, naming, apiPrefix) {
|
|
44
45
|
const content = fs_extra_1.default.readFileSync(openapiPath, "utf8");
|
|
45
46
|
const { paths, schemas } = openapiLines(naming, apiPrefix);
|
|
46
|
-
|
|
47
|
+
const withoutKnownEntries = (0, marker_patch_1.removeLines)(content, [...paths, ...schemas]);
|
|
48
|
+
// `generate method` can add any number of module-specific path documents.
|
|
49
|
+
// Remove every two-line path/$ref block owned by this module, otherwise the
|
|
50
|
+
// index keeps dangling references after the docs folder is deleted.
|
|
51
|
+
const lines = withoutKnownEntries.split("\n");
|
|
52
|
+
const moduleRefPrefix = `./${naming.plural}/`;
|
|
53
|
+
const kept = [];
|
|
54
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
55
|
+
const current = lines[i];
|
|
56
|
+
const next = lines[i + 1];
|
|
57
|
+
const isPathKey = /^\s*\/[^:]+:\s*$/.test(current);
|
|
58
|
+
const refMatch = next?.match(/^\s*\$ref:\s*['"]([^'"]+)['"]\s*$/);
|
|
59
|
+
if (isPathKey && refMatch?.[1].startsWith(moduleRefPrefix)) {
|
|
60
|
+
i += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
kept.push(current);
|
|
64
|
+
}
|
|
65
|
+
fs_extra_1.default.writeFileSync(openapiPath, kept.join("\n"));
|
|
66
|
+
}
|
|
67
|
+
// patchOpenapiIndexRaw wires hand-written path docs into the index — used by
|
|
68
|
+
// `add auth`/`add rbac`, whose endpoints aren't a single CRUD resource so
|
|
69
|
+
// there's no ModuleNaming to derive lines from. Each entry is patched with
|
|
70
|
+
// its own sentinel (the path key) rather than one block for all of them, so
|
|
71
|
+
// re-running `add rbac` after a partial failure doesn't skip entries that
|
|
72
|
+
// never made it in.
|
|
73
|
+
function patchOpenapiIndexRaw(openapiPath, apiPrefix, entries) {
|
|
74
|
+
let content = fs_extra_1.default.readFileSync(openapiPath, "utf8");
|
|
75
|
+
for (const { urlPath, file } of entries) {
|
|
76
|
+
const key = `${apiPrefix ? `/${apiPrefix}` : ""}${urlPath}:`;
|
|
77
|
+
const block = `${key}\n $ref: '${file}'`;
|
|
78
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PATHS_MARKER, block, key);
|
|
79
|
+
}
|
|
80
|
+
fs_extra_1.default.writeFileSync(openapiPath, content);
|
|
47
81
|
}
|
|
@@ -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
|
+
}
|