@nakedev/go-scaffold 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +143 -50
- package/dist/commands/auth.js +116 -11
- package/dist/commands/create.js +13 -1
- package/dist/commands/generate.js +23 -12
- package/dist/commands/method.js +94 -17
- package/dist/commands/observability.js +114 -0
- package/dist/commands/rbac.js +19 -2
- package/dist/commands/undo.js +331 -0
- package/dist/commands/worker.js +92 -32
- package/dist/index.js +368 -64
- package/dist/prompts/auth-wizard.js +29 -0
- package/dist/prompts/create-wizard.js +5 -2
- package/dist/prompts/generate-wizard.js +57 -0
- package/dist/prompts/worker-wizard.js +25 -0
- package/dist/templates/auth-manifest.js +20 -3
- package/dist/templates/create-manifest.js +13 -20
- package/dist/templates/module-manifest.js +2 -0
- package/dist/templates/observability-manifest.js +24 -0
- package/dist/templates/rbac-manifest.js +1 -0
- package/dist/templates/worker-manifest.js +23 -6
- package/dist/utils/auth-patcher.js +96 -21
- package/dist/utils/config.js +58 -10
- package/dist/utils/gocheck.js +57 -5
- package/dist/utils/golangci-patcher.js +73 -0
- package/dist/utils/gomod-patcher.js +53 -0
- package/dist/utils/main-patcher.js +58 -4
- package/dist/utils/marker-patch.js +125 -3
- package/dist/utils/method-patcher.js +97 -18
- package/dist/utils/module-location.js +58 -0
- package/dist/utils/naming.js +125 -14
- package/dist/utils/observability-patcher.js +107 -0
- package/dist/utils/openapi-patcher.js +19 -1
- package/dist/utils/platform-patcher.js +98 -12
- package/dist/utils/rbac-patcher.js +60 -10
- package/dist/utils/smoke-run.js +31 -0
- package/package.json +11 -4
- package/templates/add/auth/internal/app/user/errors.go.hbs +7 -0
- package/templates/add/auth/internal/app/user/handler.go.hbs +64 -23
- package/templates/add/auth/internal/app/user/jwt.go.hbs +31 -7
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +39 -0
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +3 -0
- package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +26 -0
- package/templates/add/auth/internal/app/user/model/user.go.hbs +9 -1
- package/templates/add/auth/internal/app/user/repository.go.hbs +64 -11
- package/templates/add/auth/internal/app/user/repository_test.go.hbs +192 -0
- package/templates/add/auth/internal/app/user/service.go.hbs +105 -21
- package/templates/add/auth/internal/app/user/service_test.go.hbs +81 -2
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +9 -138
- package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +144 -0
- package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +147 -0
- package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +21 -19
- package/templates/add/auth/internal/shared/middleware/ratelimit_memory.go.hbs +63 -0
- package/templates/add/auth/internal/shared/middleware/ratelimit_redis.go.hbs +32 -0
- package/templates/add/auth/migrations/create_auth_tokens.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +16 -0
- package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_identities.up.sql.hbs +9 -5
- package/templates/add/auth/migrations/create_login_throttle.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +10 -0
- package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_users.up.sql.hbs +13 -2
- package/templates/add/rbac/internal/app/role/dto.go.hbs +8 -3
- package/templates/add/rbac/internal/app/role/handler.go.hbs +4 -1
- package/templates/add/rbac/internal/app/role/model/permission.go.hbs +3 -0
- package/templates/add/rbac/internal/app/role/model/role.go.hbs +4 -0
- package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +3 -0
- package/templates/add/rbac/internal/app/role/repository.go.hbs +12 -11
- package/templates/add/rbac/internal/app/role/repository_test.go.hbs +176 -0
- package/templates/add/rbac/internal/app/role/service.go.hbs +7 -0
- package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +19 -0
- package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +1 -1
- package/templates/add/rbac/migrations/add_roles.down.sql.hbs +5 -5
- package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -11
- package/templates/add/worker/cmd/worker/main.go.hbs +30 -24
- package/templates/add/worker/internal/platform/mail/mail.go.hbs +21 -0
- package/templates/add/worker/internal/platform/mail/task.go.hbs +31 -34
- package/templates/add/worker/internal/platform/queue/asynq.go.hbs +140 -0
- package/templates/add/worker/internal/platform/queue/queue.go.hbs +87 -0
- package/templates/add/worker/internal/platform/queue/river.go.hbs +148 -0
- package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +59 -12
- package/templates/create/base/.dockerignore.hbs +13 -0
- package/templates/create/base/.env.example.hbs +18 -9
- package/templates/create/base/.github/dependabot.yml.hbs +20 -0
- package/templates/create/base/.github/workflows/ci.yml.hbs +29 -6
- package/templates/create/base/.golangci.yml.hbs +27 -0
- package/templates/create/base/AGENTS.md.hbs +14 -12
- package/templates/create/base/Dockerfile.hbs +42 -0
- package/templates/create/base/Makefile.hbs +52 -16
- package/templates/create/base/README.md.hbs +61 -12
- package/templates/create/base/cmd/api/main.go.hbs +17 -130
- package/templates/create/base/cmd/api/wiring.go.hbs +161 -0
- package/templates/create/base/go.mod.hbs +4 -4
- package/templates/create/base/internal/platform/database/database.go.hbs +30 -11
- package/templates/create/base/internal/shared/config/config.go.hbs +13 -7
- package/templates/create/base/internal/shared/pagination/pagination.go.hbs +11 -0
- package/templates/create/base/internal/shared/tx/tx.go.hbs +47 -0
- package/templates/create/base/redocly.yaml.hbs +21 -0
- package/templates/create/features/docs/architecture.md.hbs +32 -11
- package/templates/create/features/docs/openapi.yaml.hbs +4 -9
- package/templates/create/features/docs/patterns.md.hbs +91 -14
- package/templates/create/features/docs/techstack.md.hbs +8 -3
- package/templates/generate/module/docs/item.yaml.hbs +3 -3
- package/templates/generate/module/dto.go.hbs +8 -1
- package/templates/generate/module/errors.go.hbs +5 -0
- package/templates/generate/module/field-column.down.sql.hbs +2 -0
- package/templates/generate/module/field-column.up.sql.hbs +15 -0
- package/templates/generate/module/handler.go.hbs +18 -4
- package/templates/generate/module/handler_test.go.hbs +88 -62
- package/templates/generate/module/migration.down.sql.hbs +3 -1
- package/templates/generate/module/migration.up.sql.hbs +7 -2
- package/templates/generate/module/minimal/dto.go.hbs +3 -1
- package/templates/generate/module/minimal/handler.go.hbs +8 -2
- package/templates/generate/module/minimal/handler_test.go.hbs +5 -112
- package/templates/generate/module/minimal/service_test.go.hbs +39 -16
- package/templates/generate/module/model/model.go.hbs +16 -0
- package/templates/generate/module/permission.up.sql.hbs +3 -1
- package/templates/generate/module/repository.go.hbs +60 -6
- package/templates/generate/module/repository_test.go.hbs +109 -0
- package/templates/generate/module/service.go.hbs +15 -4
- package/templates/generate/module/service_test.go.hbs +113 -17
- package/dist/commands/remove.js +0 -89
- package/templates/add/worker/internal/platform/queue/client.go.hbs +0 -31
- package/templates/add/worker/internal/platform/queue/server.go.hbs +0 -68
|
@@ -0,0 +1,73 @@
|
|
|
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.patchGolangciForModule = patchGolangciForModule;
|
|
7
|
+
exports.unpatchGolangciForModule = unpatchGolangciForModule;
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const marker_patch_1 = require("./marker-patch");
|
|
10
|
+
const DEPGUARD_MARKER = "# go-scaffold:depguard-rules";
|
|
11
|
+
function ruleName(pkg) {
|
|
12
|
+
return `domain-isolation-${pkg}`;
|
|
13
|
+
}
|
|
14
|
+
// depguardRule is the per-domain boundary rule. It has to be per-domain
|
|
15
|
+
// because depguard matches import paths as static prefixes with no idea which
|
|
16
|
+
// domain a file belongs to: a single `deny: internal/app` rule would also
|
|
17
|
+
// reject `order` importing its own `order/model`. Scoping with `files:` and
|
|
18
|
+
// allowing exactly this domain's own path gives "may import myself, may not
|
|
19
|
+
// import a sibling".
|
|
20
|
+
//
|
|
21
|
+
// Written with no leading indentation — insertBeforeMarker re-indents the
|
|
22
|
+
// whole block to match the marker's own column.
|
|
23
|
+
function depguardRule(goModule, pkg) {
|
|
24
|
+
return [
|
|
25
|
+
`${ruleName(pkg)}:`,
|
|
26
|
+
` list-mode: lax`,
|
|
27
|
+
` files:`,
|
|
28
|
+
` - "**/internal/app/${pkg}/**"`,
|
|
29
|
+
` allow:`,
|
|
30
|
+
` - "${goModule}/internal/app/${pkg}"`,
|
|
31
|
+
` deny:`,
|
|
32
|
+
` - pkg: "${goModule}/internal/app"`,
|
|
33
|
+
` desc: >-`,
|
|
34
|
+
` a domain must not import another domain directly — declare a`,
|
|
35
|
+
` consumer-side interface for what you need and let`,
|
|
36
|
+
` cmd/api/wiring.go wire the concrete service in`,
|
|
37
|
+
` (docs/architect/patterns.md)`,
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
// patchGolangciForModule adds this domain's boundary rule. No-op on a project
|
|
41
|
+
// whose .golangci.yml predates the marker (or was replaced wholesale) — a
|
|
42
|
+
// missing lint rule must never fail code generation.
|
|
43
|
+
function patchGolangciForModule(golangciPath, goModule, pkg) {
|
|
44
|
+
if (!fs_extra_1.default.existsSync(golangciPath))
|
|
45
|
+
return;
|
|
46
|
+
const content = fs_extra_1.default.readFileSync(golangciPath, "utf8");
|
|
47
|
+
if (!content.includes(DEPGUARD_MARKER))
|
|
48
|
+
return;
|
|
49
|
+
fs_extra_1.default.writeFileSync(golangciPath, (0, marker_patch_1.insertBeforeMarkerOnce)(content, DEPGUARD_MARKER, depguardRule(goModule, pkg), `${ruleName(pkg)}:`));
|
|
50
|
+
}
|
|
51
|
+
// unpatchGolangciForModule drops the rule again, matched by its heading and
|
|
52
|
+
// everything indented under it — the inverse of patchGolangciForModule.
|
|
53
|
+
function unpatchGolangciForModule(golangciPath, pkg) {
|
|
54
|
+
if (!fs_extra_1.default.existsSync(golangciPath))
|
|
55
|
+
return;
|
|
56
|
+
const lines = fs_extra_1.default.readFileSync(golangciPath, "utf8").split("\n");
|
|
57
|
+
const heading = `${ruleName(pkg)}:`;
|
|
58
|
+
const start = lines.findIndex((l) => l.trim() === heading);
|
|
59
|
+
if (start === -1)
|
|
60
|
+
return;
|
|
61
|
+
const indent = lines[start].length - lines[start].trimStart().length;
|
|
62
|
+
let end = start + 1;
|
|
63
|
+
while (end < lines.length) {
|
|
64
|
+
const line = lines[end];
|
|
65
|
+
// blank lines belong to the block only if something indented follows
|
|
66
|
+
const deeper = line.trim() === "" || line.length - line.trimStart().length > indent;
|
|
67
|
+
if (!deeper)
|
|
68
|
+
break;
|
|
69
|
+
end++;
|
|
70
|
+
}
|
|
71
|
+
lines.splice(start, end - start);
|
|
72
|
+
fs_extra_1.default.writeFileSync(golangciPath, lines.join("\n"));
|
|
73
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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.patchGoModRequires = patchGoModRequires;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
// Every `add` command pulls in third-party packages, and none of them used to
|
|
9
|
+
// touch go.mod — the imports just appeared and `go mod tidy` was left to
|
|
10
|
+
// resolve them. Tidy picks the latest release of anything it doesn't already
|
|
11
|
+
// have a version for, so two people running `add auth` a month apart got
|
|
12
|
+
// different builds from the same CLI, and nothing recorded which versions the
|
|
13
|
+
// scaffold was actually written and tested against.
|
|
14
|
+
//
|
|
15
|
+
// Pinning here fixes that: tidy honours a version that's already in go.mod
|
|
16
|
+
// rather than reaching for latest. It's a floor, not a ceiling — if another
|
|
17
|
+
// feature needs something newer, tidy still resolves upward, which is why
|
|
18
|
+
// golang.org/x/crypto can be pinned by `add auth` and later raised by
|
|
19
|
+
// `add observability`.
|
|
20
|
+
//
|
|
21
|
+
// Users still need to run `go mod tidy` (every command says so): these lines
|
|
22
|
+
// have no go.sum entries, and only tidy can add those.
|
|
23
|
+
function patchGoModRequires(goModPath, requires) {
|
|
24
|
+
if (!fs_extra_1.default.existsSync(goModPath))
|
|
25
|
+
return;
|
|
26
|
+
let content = fs_extra_1.default.readFileSync(goModPath, "utf8");
|
|
27
|
+
// already-present modules are left at whatever version they're on: a
|
|
28
|
+
// re-run, or a version the user deliberately bumped, is not ours to reset
|
|
29
|
+
const missing = requires.filter((line) => !hasModule(content, modulePathOf(line)));
|
|
30
|
+
if (missing.length === 0)
|
|
31
|
+
return;
|
|
32
|
+
const block = content.match(/^require \(\n(?:.*\n)*?\)$/m);
|
|
33
|
+
if (!block) {
|
|
34
|
+
// no grouped require block (hand-edited, or single-line `require x y`
|
|
35
|
+
// form) — append one rather than trying to rewrite what's there
|
|
36
|
+
content = content.replace(/\n?$/, "\n") + `\nrequire (\n${missing.map((l) => `\t${l}`).join("\n")}\n)\n`;
|
|
37
|
+
fs_extra_1.default.writeFileSync(goModPath, content);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const updated = block[0].replace(/\n\)$/, `\n${missing.map((l) => `\t${l}`).join("\n")}\n)`);
|
|
41
|
+
// function replacer: a version string can't contain $ today, but this is the
|
|
42
|
+
// same trap ensureImport documents and costs nothing to avoid
|
|
43
|
+
fs_extra_1.default.writeFileSync(goModPath, content.replace(block[0], () => updated));
|
|
44
|
+
}
|
|
45
|
+
function modulePathOf(requireLine) {
|
|
46
|
+
return requireLine.trim().split(/\s+/)[0];
|
|
47
|
+
}
|
|
48
|
+
// Word-boundary match on the module path, so `go.opentelemetry.io/otel`
|
|
49
|
+
// doesn't count as already-present when only `go.opentelemetry.io/otel/sdk`
|
|
50
|
+
// is there.
|
|
51
|
+
function hasModule(goMod, modulePath) {
|
|
52
|
+
return goMod.split("\n").some((line) => modulePathOf(line) === modulePath);
|
|
53
|
+
}
|
|
@@ -3,11 +3,13 @@ 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.assertMainGoPatchable = assertMainGoPatchable;
|
|
6
7
|
exports.patchMainGo = patchMainGo;
|
|
7
8
|
exports.unpatchMainGo = unpatchMainGo;
|
|
8
9
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
10
|
const marker_patch_1 = require("./marker-patch");
|
|
10
11
|
const IMPORT_MARKER = "// go-scaffold:imports";
|
|
12
|
+
const SCHEMA_MARKER = "// go-scaffold:schemas";
|
|
11
13
|
const MODEL_MARKER = "// go-scaffold:models";
|
|
12
14
|
const ROUTE_MARKER = "// go-scaffold:routes";
|
|
13
15
|
// no leading tab: insertBeforeMarker re-indents, and removeLines matches by
|
|
@@ -15,9 +17,19 @@ const ROUTE_MARKER = "// go-scaffold:routes";
|
|
|
15
17
|
const UNUSED_API_LINE = "_ = api // dropped once `generate module` registers the first route";
|
|
16
18
|
// the exact lines patchMainGo inserts for a module — one source of truth so
|
|
17
19
|
// unpatchMainGo removes precisely what patch added.
|
|
20
|
+
//
|
|
21
|
+
// The service gets its own named variable rather than being constructed
|
|
22
|
+
// inline inside NewHandler(...). docs/architect/patterns.md tells you to wire
|
|
23
|
+
// one domain's service into another's constructor when it needs behaviour
|
|
24
|
+
// from it, which means editing this block by hand — and an inline expression
|
|
25
|
+
// has nowhere to hold the result. With a named variable the edit is "add an
|
|
26
|
+
// argument to the end of line one", and unpatchMainGo can still find the line
|
|
27
|
+
// afterwards because it matches on the `<pkg>Svc :=` prefix, not the whole
|
|
28
|
+
// text.
|
|
18
29
|
function mainGoLines(patch) {
|
|
19
30
|
const modelAlias = `${patch.pkg}model`; // every domain's model subpackage is named "model"
|
|
20
|
-
const
|
|
31
|
+
const svcVar = `${patch.pkg}Svc`;
|
|
32
|
+
const handlerArgs = [svcVar];
|
|
21
33
|
if (patch.auth)
|
|
22
34
|
handlerArgs.push("cfg.JWTSecret");
|
|
23
35
|
if (patch.permission)
|
|
@@ -25,7 +37,20 @@ function mainGoLines(patch) {
|
|
|
25
37
|
return {
|
|
26
38
|
importLine: `"${patch.goModule}/internal/app/${patch.modulePath}"`,
|
|
27
39
|
modelImportLine: `${modelAlias} "${patch.goModule}/internal/app/${patch.modulePath}/model"`,
|
|
40
|
+
// AutoMigrate (dev) creates tables but not the schema they live in — see
|
|
41
|
+
// the comment on go-scaffold:schemas in main.go.hbs. One Exec per schema,
|
|
42
|
+
// guarded by its own sentinel so two modules sharing a schema name only
|
|
43
|
+
// ever produce one line (not expected today, but cheap to keep safe).
|
|
44
|
+
schemaLines: [
|
|
45
|
+
`if err := db.Exec("CREATE SCHEMA IF NOT EXISTS ${patch.schemaName}").Error; err != nil {`,
|
|
46
|
+
`\treturn fmt.Errorf("create schema ${patch.schemaName}: %w", err)`,
|
|
47
|
+
`}`,
|
|
48
|
+
].join("\n"),
|
|
49
|
+
schemaSentinel: `CREATE SCHEMA IF NOT EXISTS ${patch.schemaName}`,
|
|
28
50
|
migrateLine: `&${modelAlias}.${patch.pascalName}{},`,
|
|
51
|
+
serviceLine: `${svcVar} := ${patch.pkg}.NewService(${patch.pkg}.NewRepository(db))`,
|
|
52
|
+
// matches the service line however the user has since extended it
|
|
53
|
+
servicePrefix: `${svcVar} :=`,
|
|
29
54
|
// `api` is the one route group declared by main.go.hbs, prefixed with
|
|
30
55
|
// whatever apiPrefix the project chose at create time (e.g. /v1, /api,
|
|
31
56
|
// or none) — every module registers on it, there is no per-module choice.
|
|
@@ -34,20 +59,43 @@ function mainGoLines(patch) {
|
|
|
34
59
|
routeLine: `${patch.pkg}.NewHandler(${handlerArgs.join(", ")}).Register(api)`,
|
|
35
60
|
};
|
|
36
61
|
}
|
|
37
|
-
//
|
|
62
|
+
// assertMainGoPatchable is the pre-flight for it: every marker patchMainGo
|
|
63
|
+
// needs, checked before the caller writes anything. Without it a missing
|
|
64
|
+
// marker surfaced only once the module files and its migration were already
|
|
65
|
+
// on disk, and the retry then hit "internal/app/<pkg> already exists" — a
|
|
66
|
+
// dead end that pointed nowhere useful.
|
|
67
|
+
function assertMainGoPatchable(mainGoPath) {
|
|
68
|
+
if (!fs_extra_1.default.existsSync(mainGoPath)) {
|
|
69
|
+
throw new Error(`${mainGoPath} not found — this doesn't look like a go-scaffold project`);
|
|
70
|
+
}
|
|
71
|
+
const content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
|
|
72
|
+
const missing = [IMPORT_MARKER, SCHEMA_MARKER, MODEL_MARKER, ROUTE_MARKER].filter((m) => !(0, marker_patch_1.hasMarker)(content, m));
|
|
73
|
+
if (missing.length) {
|
|
74
|
+
throw new Error(`cmd/api/wiring.go is missing the marker comment${missing.length > 1 ? "s" : ""} this command patches at:\n` +
|
|
75
|
+
missing.map((m) => ` ${m}`).join("\n") +
|
|
76
|
+
`\n\nEither the file was hand-edited, or it was scaffolded by an older go-scaffold that\n` +
|
|
77
|
+
`didn't emit ${missing.length > 1 ? "them" : "it"} yet. Add the marker${missing.length > 1 ? "s" : ""} back where the generated code should go, or\n` +
|
|
78
|
+
`wire this module into main.go by hand.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// patchMainGo wires a newly generated module into cmd/api/wiring.go: its
|
|
38
82
|
// import, its model in the AutoMigrate call, and its route registration —
|
|
39
83
|
// via marker comments rather than a Go AST rewrite (ponytail: text insertion
|
|
40
84
|
// at a fixed marker is enough here; reach for go/ast if main.go ever needs
|
|
41
85
|
// edits markers can't express).
|
|
42
86
|
function patchMainGo(mainGoPath, patch) {
|
|
43
87
|
let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
|
|
44
|
-
const { importLine, modelImportLine, migrateLine, routeLine } = mainGoLines(patch);
|
|
88
|
+
const { importLine, modelImportLine, schemaLines, schemaSentinel, migrateLine, serviceLine, servicePrefix, routeLine } = mainGoLines(patch);
|
|
45
89
|
// each guarded by its own sentinel so re-running after only the module
|
|
46
90
|
// folder was deleted (main.go still wired) is a no-op, not a dup that
|
|
47
91
|
// panics gin at startup.
|
|
48
92
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, importLine, importLine);
|
|
49
93
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, modelImportLine, modelImportLine);
|
|
94
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SCHEMA_MARKER, schemaLines, schemaSentinel);
|
|
50
95
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, MODEL_MARKER, migrateLine, migrateLine);
|
|
96
|
+
// sentinel is the prefix, not the whole line: a re-run must not add a
|
|
97
|
+
// second service line just because the first one gained a dependency.
|
|
98
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, ROUTE_MARKER, serviceLine, servicePrefix);
|
|
51
99
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, ROUTE_MARKER, routeLine, routeLine);
|
|
52
100
|
content = (0, marker_patch_1.removeLines)(content, [UNUSED_API_LINE]);
|
|
53
101
|
fs_extra_1.default.writeFileSync(mainGoPath, content);
|
|
@@ -57,8 +105,14 @@ function patchMainGo(mainGoPath, patch) {
|
|
|
57
105
|
// main.go still compiles (api would otherwise be declared-and-unused).
|
|
58
106
|
function unpatchMainGo(mainGoPath, patch) {
|
|
59
107
|
let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
|
|
60
|
-
const { importLine, modelImportLine, migrateLine, routeLine } = mainGoLines(patch);
|
|
108
|
+
const { importLine, modelImportLine, schemaLines, migrateLine, servicePrefix, routeLine } = mainGoLines(patch);
|
|
61
109
|
content = (0, marker_patch_1.removeLines)(content, [importLine, modelImportLine, migrateLine, routeLine]);
|
|
110
|
+
// by prefix: the service line may have grown arguments since it was written
|
|
111
|
+
content = (0, marker_patch_1.removeLinesByPrefix)(content, [servicePrefix]);
|
|
112
|
+
// by contiguous block, not removeLines: every module's schema block is
|
|
113
|
+
// identical except the schema name, so a line-by-line removal would also
|
|
114
|
+
// strip another module's matching lines.
|
|
115
|
+
content = (0, marker_patch_1.removeBlock)(content, schemaLines);
|
|
62
116
|
if (!content.includes(".Register(api)") && !content.includes(UNUSED_API_LINE)) {
|
|
63
117
|
content = (0, marker_patch_1.insertBeforeMarker)(content, ROUTE_MARKER, UNUSED_API_LINE);
|
|
64
118
|
}
|
|
@@ -10,6 +10,8 @@ exports.hasMarker = hasMarker;
|
|
|
10
10
|
exports.ensureImport = ensureImport;
|
|
11
11
|
exports.insertBeforeMarkerOnce = insertBeforeMarkerOnce;
|
|
12
12
|
exports.removeLines = removeLines;
|
|
13
|
+
exports.removeBlock = removeBlock;
|
|
14
|
+
exports.removeLinesByPrefix = removeLinesByPrefix;
|
|
13
15
|
exports.insertBeforeMarker = insertBeforeMarker;
|
|
14
16
|
function hasMarker(content, marker) {
|
|
15
17
|
return content.split("\n").some((l) => l.trim() === marker);
|
|
@@ -24,7 +26,12 @@ function ensureImport(content, importPath) {
|
|
|
24
26
|
const importLine = `"${importPath}"`;
|
|
25
27
|
if (content.includes(importLine))
|
|
26
28
|
return content;
|
|
27
|
-
|
|
29
|
+
// A function replacer, not a string one: String.replace treats "$$", "$&",
|
|
30
|
+
// digit-groups etc. in a *string* replacement as special patterns even when
|
|
31
|
+
// the search argument is a plain string, not a regex — collapsing any "$$"
|
|
32
|
+
// that happens to appear in importLine. A function's return value is spliced
|
|
33
|
+
// in literally, so this holds regardless of what importPath contains.
|
|
34
|
+
return content.replace(/import \(\n/, () => `import (\n\t${importLine}\n`);
|
|
28
35
|
}
|
|
29
36
|
// insertBeforeMarkerOnce: like insertBeforeMarker but a no-op if `sentinel`
|
|
30
37
|
// already appears in the file. Makes module wiring idempotent — re-running
|
|
@@ -38,7 +45,7 @@ function insertBeforeMarkerOnce(content, marker, block, sentinel) {
|
|
|
38
45
|
return insertBeforeMarker(content, marker, block);
|
|
39
46
|
}
|
|
40
47
|
// removeLines drops every line whose trimmed text exactly equals one of the
|
|
41
|
-
// given lines — the inverse of insertBeforeMarker for `
|
|
48
|
+
// given lines — the inverse of insertBeforeMarker for `undo module`, which
|
|
42
49
|
// needs to pull a module's import/route/path entries back out. Exact-trim
|
|
43
50
|
// match so it can't clip an unrelated line that merely contains the text.
|
|
44
51
|
function removeLines(content, trimmedLines) {
|
|
@@ -48,6 +55,116 @@ function removeLines(content, trimmedLines) {
|
|
|
48
55
|
.filter((l) => !drop.has(l.trim()))
|
|
49
56
|
.join("\n");
|
|
50
57
|
}
|
|
58
|
+
// removeBlock removes a multi-line block inserted by insertBeforeMarker,
|
|
59
|
+
// matched as one contiguous run (each line compared trimmed) rather than
|
|
60
|
+
// line-by-line like removeLines. Needed when a block's individual lines
|
|
61
|
+
// aren't unique on their own — two modules' schema-creation blocks in
|
|
62
|
+
// main.go are identical except for the schema name itself, and removeLines
|
|
63
|
+
// would delete the shared lines from both when asked to remove just one.
|
|
64
|
+
function removeBlock(content, block) {
|
|
65
|
+
const blockLines = block.split("\n").map((l) => l.trim());
|
|
66
|
+
const lines = content.split("\n");
|
|
67
|
+
const out = [];
|
|
68
|
+
for (let i = 0; i < lines.length;) {
|
|
69
|
+
const matches = blockLines.every((bl, j) => lines[i + j]?.trim() === bl);
|
|
70
|
+
if (matches) {
|
|
71
|
+
i += blockLines.length;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
out.push(lines[i]);
|
|
75
|
+
i += 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out.join("\n");
|
|
79
|
+
}
|
|
80
|
+
// removeLinesByPrefix is removeLines for a line the user is expected to edit
|
|
81
|
+
// after it was generated — a service constructor that has since gained a
|
|
82
|
+
// dependency, say. Matching the whole line would miss it and leave the
|
|
83
|
+
// project un-compilable after `undo module`; matching a distinctive prefix
|
|
84
|
+
// (`orderSvc :=`) still finds it. Only use it where the prefix is unique.
|
|
85
|
+
//
|
|
86
|
+
// A match consumes lines until its brackets balance, not just the first one:
|
|
87
|
+
// docs/architect/patterns.md tells you to expand that single wiring line into
|
|
88
|
+
// a multi-line adapter literal, and dropping only line one leaves an orphaned
|
|
89
|
+
// func body behind — a main.go that no longer parses, reported by the Go
|
|
90
|
+
// compiler long after `undo module` has printed success.
|
|
91
|
+
function removeLinesByPrefix(content, prefixes) {
|
|
92
|
+
const lines = content.split("\n");
|
|
93
|
+
const out = [];
|
|
94
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
95
|
+
if (!prefixes.some((p) => lines[i].trim().startsWith(p))) {
|
|
96
|
+
out.push(lines[i]);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const start = i;
|
|
100
|
+
let depth = 0;
|
|
101
|
+
let overran = false;
|
|
102
|
+
do {
|
|
103
|
+
// A marker comment is a structural anchor — no wiring statement can
|
|
104
|
+
// contain one. Reaching it with brackets still open means the count is
|
|
105
|
+
// wrong (a malformed line above), and carrying on would swallow the
|
|
106
|
+
// marker plus every module wired after it. Stop at the boundary.
|
|
107
|
+
if (i > start && lines[i].trim().startsWith("// go-scaffold:")) {
|
|
108
|
+
overran = true;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
depth += bracketDelta(lines[i]);
|
|
112
|
+
i += 1;
|
|
113
|
+
} while (depth > 0 && i < lines.length);
|
|
114
|
+
// Either we hit a marker or ran out of file with brackets still open.
|
|
115
|
+
// Consuming anyway deletes everything in between — in main.go that's the
|
|
116
|
+
// other modules' wiring and the closing brace, from a command whose whole
|
|
117
|
+
// job is to touch one module. Refuse: undo un-wires before it deletes
|
|
118
|
+
// anything, so throwing here costs the user nothing.
|
|
119
|
+
if (overran || depth > 0) {
|
|
120
|
+
throw new Error(`the statement starting at line ${start + 1} never closes its brackets:\n` +
|
|
121
|
+
` ${lines[start].trim()}\n\n` +
|
|
122
|
+
`Refusing to guess where it ends — removing through to where the count finally balances\n` +
|
|
123
|
+
`would take the lines in between with it. Fix the brackets, or remove it by hand.`);
|
|
124
|
+
}
|
|
125
|
+
i -= 1; // the for's own increment steps past the last consumed line
|
|
126
|
+
}
|
|
127
|
+
return out.join("\n");
|
|
128
|
+
}
|
|
129
|
+
// bracketDelta counts on code only. Brackets inside a string literal or a
|
|
130
|
+
// trailing comment are text, not structure — `order.WithPrefix("(")` on the
|
|
131
|
+
// wiring line would otherwise leave the count permanently open.
|
|
132
|
+
//
|
|
133
|
+
// ponytail: a raw string that spans lines is still counted wrong, since this
|
|
134
|
+
// works a line at a time. Nothing generated produces one; reach for go/ast if
|
|
135
|
+
// that ever changes.
|
|
136
|
+
function bracketDelta(line) {
|
|
137
|
+
let delta = 0;
|
|
138
|
+
for (const ch of stripLiteralsAndComments(line)) {
|
|
139
|
+
if (ch === "(" || ch === "{" || ch === "[")
|
|
140
|
+
delta += 1;
|
|
141
|
+
else if (ch === ")" || ch === "}" || ch === "]")
|
|
142
|
+
delta -= 1;
|
|
143
|
+
}
|
|
144
|
+
return delta;
|
|
145
|
+
}
|
|
146
|
+
function stripLiteralsAndComments(line) {
|
|
147
|
+
let out = "";
|
|
148
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
149
|
+
const ch = line[i];
|
|
150
|
+
if (ch === "/" && line[i + 1] === "/")
|
|
151
|
+
break; // rest of the line is a comment
|
|
152
|
+
if (ch !== '"' && ch !== "'" && ch !== "`") {
|
|
153
|
+
out += ch;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
// skip to the closing quote; \" doesn't close an interpreted string, but
|
|
157
|
+
// a backslash is literal inside a raw one
|
|
158
|
+
const quote = ch;
|
|
159
|
+
i += 1;
|
|
160
|
+
while (i < line.length && line[i] !== quote) {
|
|
161
|
+
if (quote !== "`" && line[i] === "\\")
|
|
162
|
+
i += 1;
|
|
163
|
+
i += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
51
168
|
function insertBeforeMarker(content, marker, block) {
|
|
52
169
|
const lines = content.split("\n");
|
|
53
170
|
const markerLine = lines.find((l) => l.trim() === marker);
|
|
@@ -59,5 +176,10 @@ function insertBeforeMarker(content, marker, block) {
|
|
|
59
176
|
.split("\n")
|
|
60
177
|
.map((line) => (line ? `${indent}${line}` : line))
|
|
61
178
|
.join("\n");
|
|
62
|
-
|
|
179
|
+
// Function replacer, not a string one — see the comment on ensureImport.
|
|
180
|
+
// Every caller of this — main.go/patterns.md route and import wiring,
|
|
181
|
+
// depguard rules, rbac's main.go patches — funnels through here, so fixing
|
|
182
|
+
// it here is what actually closes the bug off, rather than auditing every
|
|
183
|
+
// block a caller happens to pass in today.
|
|
184
|
+
return content.replace(markerLine, () => `${indentedBlock}\n${markerLine}`);
|
|
63
185
|
}
|
|
@@ -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");
|
|
@@ -106,8 +151,13 @@ function patchGetOne(paths, naming, method, rawField, goModule) {
|
|
|
106
151
|
repo = (0, marker_patch_1.insertBeforeMarker)(repo, REPO_IMPL_MARKER, [
|
|
107
152
|
`func (r *Repository) FindBy${fieldPascal}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error) {`,
|
|
108
153
|
`\tvar m model.${naming.pascalName}`,
|
|
109
|
-
`\t// TODO:
|
|
110
|
-
`\
|
|
154
|
+
`\t// TODO: add the "${fieldColumn}" column and an index on it — this query`,
|
|
155
|
+
`\t// compiles against any table and only fails when it runs.`,
|
|
156
|
+
`\t//`,
|
|
157
|
+
`\t// tx.From, like every other method here: a lookup that reached for r.db`,
|
|
158
|
+
`\t// directly would read outside a transaction its caller had opened, and`,
|
|
159
|
+
`\t// so miss that transaction's own uncommitted writes.`,
|
|
160
|
+
`\tif err := tx.From(ctx, r.db).WithContext(ctx).First(&m, "${fieldColumn} = ?", ${fieldParam}).Error; err != nil {`,
|
|
111
161
|
`\t\treturn nil, err`,
|
|
112
162
|
`\t}`,
|
|
113
163
|
`\treturn &m, nil`,
|
|
@@ -117,22 +167,40 @@ function patchGetOne(paths, naming, method, rawField, goModule) {
|
|
|
117
167
|
fs_extra_1.default.writeFileSync(paths.repositoryPath, repo);
|
|
118
168
|
let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
|
|
119
169
|
service = (0, marker_patch_1.insertBeforeMarker)(service, REPO_INTERFACE_MARKER, `FindBy${fieldPascal}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error)`);
|
|
120
|
-
//
|
|
121
|
-
//
|
|
170
|
+
// The repository interface just grew, so the test double needs a matching
|
|
171
|
+
// method or focused service tests stop compiling. New projects use a
|
|
172
|
+
// function-backed stub; projects scaffolded before that refactor retain the
|
|
173
|
+
// old fakeRepo marker and error behavior.
|
|
122
174
|
let serviceTest = fs_extra_1.default.readFileSync(paths.serviceTestPath, "utf8");
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
175
|
+
if ((0, marker_patch_1.hasMarker)(serviceTest, REPOSITORY_STUB_FIELDS_MARKER) &&
|
|
176
|
+
(0, marker_patch_1.hasMarker)(serviceTest, REPOSITORY_STUB_METHODS_MARKER)) {
|
|
177
|
+
serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, REPOSITORY_STUB_FIELDS_MARKER, `findBy${fieldPascal}Fn func(context.Context, string) (*model.${naming.pascalName}, error)`);
|
|
178
|
+
serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, REPOSITORY_STUB_METHODS_MARKER, [
|
|
179
|
+
`//nolint:unused`,
|
|
180
|
+
`func (s *repositoryStub) FindBy${fieldPascal}(ctx context.Context, value string) (*model.${naming.pascalName}, error) {`,
|
|
181
|
+
`\tif s.findBy${fieldPascal}Fn == nil {`,
|
|
182
|
+
`\t\tpanic("unexpected repository.FindBy${fieldPascal} call")`,
|
|
183
|
+
`\t}`,
|
|
184
|
+
`\treturn s.findBy${fieldPascal}Fn(ctx, value)`,
|
|
185
|
+
`}`,
|
|
186
|
+
``,
|
|
187
|
+
].join("\n"));
|
|
188
|
+
}
|
|
189
|
+
else if ((0, marker_patch_1.hasMarker)(serviceTest, LEGACY_FAKE_REPO_METHODS_MARKER)) {
|
|
190
|
+
serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, LEGACY_FAKE_REPO_METHODS_MARKER, [
|
|
191
|
+
`//nolint:unused`,
|
|
192
|
+
`func (f *fakeRepo) FindBy${fieldPascal}(context.Context, string) (*model.${naming.pascalName}, error) {`,
|
|
193
|
+
`\tif f.err != nil {`,
|
|
194
|
+
`\t\treturn nil, f.err`,
|
|
195
|
+
`\t}`,
|
|
196
|
+
`\treturn f.m, nil`,
|
|
197
|
+
`}`,
|
|
198
|
+
``,
|
|
199
|
+
].join("\n"));
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
throw new Error(`neither current repository stub markers nor legacy "${LEGACY_FAKE_REPO_METHODS_MARKER}" found in ${paths.serviceTestPath}`);
|
|
203
|
+
}
|
|
136
204
|
fs_extra_1.default.writeFileSync(paths.serviceTestPath, serviceTest);
|
|
137
205
|
service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
|
|
138
206
|
`func (s *Service) ${method.pascalName}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error) {`,
|
|
@@ -224,7 +292,17 @@ function patchResourceAction(paths, naming, method, type, goModule) {
|
|
|
224
292
|
`\t\treturn nil, wrapFindErr(err)`,
|
|
225
293
|
`\t}`,
|
|
226
294
|
`\t// TODO: implement "${method.name}" — currently a no-op save`,
|
|
295
|
+
`\t// The version compared here is the one just read, so this only`,
|
|
296
|
+
`\t// catches a writer that lands between the read above and this write.`,
|
|
297
|
+
`\t// It is NOT the check Update does: that one compares against the version`,
|
|
298
|
+
`\t// the *client* last saw, which is what catches someone submitting a`,
|
|
299
|
+
`\t// form built from a copy that has since gone stale. If this endpoint`,
|
|
300
|
+
`\t// needs that, give it an input struct carrying Version and assign it`,
|
|
301
|
+
`\t// to m.Version here, the way service.go's Update does.`,
|
|
227
302
|
`\tif err := s.repo.Update(ctx, m); err != nil {`,
|
|
303
|
+
`\t\tif errors.Is(err, ErrStaleVersion) {`,
|
|
304
|
+
`\t\t\treturn nil, errStale()`,
|
|
305
|
+
`\t\t}`,
|
|
228
306
|
`\t\treturn nil, apperror.NewInternal()`,
|
|
229
307
|
`\t}`,
|
|
230
308
|
`\treturn m, nil`,
|
|
@@ -266,6 +344,7 @@ function markersPresent(handlerPath, servicePath) {
|
|
|
266
344
|
const service = fs_extra_1.default.readFileSync(servicePath, "utf8");
|
|
267
345
|
return ((0, marker_patch_1.hasMarker)(handler, HANDLER_ROUTES_MARKER) &&
|
|
268
346
|
(0, marker_patch_1.hasMarker)(handler, HANDLER_FUNCS_MARKER) &&
|
|
347
|
+
((0, marker_patch_1.hasMarker)(handler, SERVICE_INTERFACE_MARKER) || handler.includes("svc *Service")) &&
|
|
269
348
|
(0, marker_patch_1.hasMarker)(service, SERVICE_MARKER) &&
|
|
270
349
|
(0, marker_patch_1.hasMarker)(service, REPO_INTERFACE_MARKER));
|
|
271
350
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
// A Go package name has no word boundaries: "orderitem" cannot tell you it
|
|
21
|
+
// came from "order-items". Everything else a command derives — the model type
|
|
22
|
+
// (OrderItem), the route (/order-items), the table (order_items), the
|
|
23
|
+
// migration filename — does depend on those boundaries, so re-deriving them
|
|
24
|
+
// from the package name yields Orderitem/orderitems and code that doesn't
|
|
25
|
+
// compile.
|
|
26
|
+
//
|
|
27
|
+
// That mattered because the package name is the one form of the name the user
|
|
28
|
+
// actually sees: it's the folder on disk, and it's what `generate module`
|
|
29
|
+
// prints as the next command to run. Following that instruction produced
|
|
30
|
+
// `undefined: model.Orderitem (but have OrderItem)`.
|
|
31
|
+
//
|
|
32
|
+
// model/model.go is where the boundaries survive — the struct name is
|
|
33
|
+
// PascalCase, so kebab-casing it recovers the exact string `generate module`
|
|
34
|
+
// was originally given.
|
|
35
|
+
function pascalNameOnDisk(projectDir, pkg) {
|
|
36
|
+
const modelPath = path_1.default.join(projectDir, "internal", "app", pkg, "model", "model.go");
|
|
37
|
+
if (!fs_extra_1.default.existsSync(modelPath))
|
|
38
|
+
return null;
|
|
39
|
+
const match = fs_extra_1.default.readFileSync(modelPath, "utf8").match(/^type\s+([A-Z]\w*)\s+struct\b/m);
|
|
40
|
+
return match ? match[1] : null;
|
|
41
|
+
}
|
|
42
|
+
function resolveProjectModuleNaming(projectDir, rawName) {
|
|
43
|
+
const located = (0, naming_1.resolveExistingModuleNaming)(rawName, existingModulePackages(projectDir));
|
|
44
|
+
const pascal = pascalNameOnDisk(projectDir, located.pkg);
|
|
45
|
+
if (!pascal || pascal === located.pascalName)
|
|
46
|
+
return located;
|
|
47
|
+
let fromDisk;
|
|
48
|
+
try {
|
|
49
|
+
fromDisk = (0, naming_1.resolveModuleNaming)((0, naming_1.toKebabCase)(pascal));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return located; // model.go holds something we can't derive a module name from
|
|
53
|
+
}
|
|
54
|
+
// Only trust the correction when it still points at the same package.
|
|
55
|
+
// A hand-renamed struct, or a legacy module whose type never matched its
|
|
56
|
+
// folder, would otherwise silently retarget the command at another module.
|
|
57
|
+
return fromDisk.pkg === located.pkg ? fromDisk : located;
|
|
58
|
+
}
|