@nakedev/go-scaffold 0.1.1 → 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 +88 -9
- 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 +6 -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
|
@@ -0,0 +1,75 @@
|
|
|
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.addWorker = addWorker;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
const config_1 = require("../utils/config");
|
|
11
|
+
const template_renderer_1 = require("../utils/template-renderer");
|
|
12
|
+
const worker_manifest_1 = require("../templates/worker-manifest");
|
|
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
|
|
17
|
+
// 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
|
+
const config = (0, config_1.readConfig)(projectDir);
|
|
22
|
+
const queueDir = path_1.default.join(projectDir, "internal", "platform", "queue");
|
|
23
|
+
if (fs_extra_1.default.existsSync(queueDir)) {
|
|
24
|
+
throw new Error(`${queueDir} already exists — worker infrastructure looks like it's already been added`);
|
|
25
|
+
}
|
|
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"));
|
|
31
|
+
(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"));
|
|
36
|
+
}
|
|
37
|
+
function patchEnvExample(envExamplePath) {
|
|
38
|
+
if (!fs_extra_1.default.existsSync(envExamplePath))
|
|
39
|
+
return;
|
|
40
|
+
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" +
|
|
46
|
+
"\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";
|
|
52
|
+
fs_extra_1.default.writeFileSync(envExamplePath, content);
|
|
53
|
+
}
|
|
54
|
+
function patchMakefile(makefilePath) {
|
|
55
|
+
if (!fs_extra_1.default.existsSync(makefilePath))
|
|
56
|
+
return;
|
|
57
|
+
let content = fs_extra_1.default.readFileSync(makefilePath, "utf8");
|
|
58
|
+
if (content.includes("\nworker:\n"))
|
|
59
|
+
return; // already added
|
|
60
|
+
content = content.replace(/^\.PHONY: /m, ".PHONY: dev worker ");
|
|
61
|
+
const targets = "\n# run both API + worker in one terminal — Ctrl+C kills both\n" +
|
|
62
|
+
"dev:\n" +
|
|
63
|
+
"\t@[ -f .env ] && export $$(grep -v '^#' .env | xargs); \\\n" +
|
|
64
|
+
"\t(trap 'kill 0' SIGINT SIGTERM; \\\n" +
|
|
65
|
+
"\t go run ./cmd/api & \\\n" +
|
|
66
|
+
"\t go run ./cmd/worker & \\\n" +
|
|
67
|
+
"\t wait)\n" +
|
|
68
|
+
"\n" +
|
|
69
|
+
"# background worker for async task processing (email, ...) — requires Redis.\n" +
|
|
70
|
+
"# Use `make dev` to run both in one terminal, or run this in a separate one.\n" +
|
|
71
|
+
"worker:\n" +
|
|
72
|
+
"\t@[ -f .env ] && export $$(grep -v '^#' .env | xargs); go run ./cmd/worker\n";
|
|
73
|
+
content = content.replace(/\nbuild:/, `${targets}\nbuild:`);
|
|
74
|
+
fs_extra_1.default.writeFileSync(makefilePath, content);
|
|
75
|
+
}
|
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
|
+
}
|