@nakedev/go-scaffold 0.4.0 → 0.5.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/README.md +598 -306
- package/dist/commands/auth.js +65 -23
- package/dist/commands/check.js +281 -0
- package/dist/commands/config.js +50 -0
- package/dist/commands/create.js +33 -2
- package/dist/commands/generate.js +29 -3
- package/dist/commands/method.js +74 -63
- package/dist/commands/migration.js +2 -2
- package/dist/commands/observability.js +4 -53
- package/dist/commands/rbac.js +21 -10
- package/dist/commands/undo.js +11 -3
- package/dist/commands/worker.js +15 -5
- package/dist/index.js +198 -59
- package/dist/prompts/auth-wizard.js +40 -6
- package/dist/prompts/create-wizard.js +42 -1
- package/dist/prompts/generate-wizard.js +89 -9
- package/dist/templates/auth-manifest.js +50 -19
- package/dist/templates/create-manifest.js +8 -0
- package/dist/templates/module-manifest.js +84 -26
- package/dist/templates/rbac-manifest.js +16 -11
- package/dist/templates/worker-manifest.js +4 -1
- package/dist/types.js +8 -0
- package/dist/utils/auth-patcher.js +124 -33
- package/dist/utils/config.js +167 -4
- package/dist/utils/docs-patcher.js +68 -0
- package/dist/utils/hexagonal-method-patcher.js +334 -0
- package/dist/utils/main-patcher.js +32 -30
- package/dist/utils/marker-patch.js +7 -1
- package/dist/utils/module-location.js +17 -11
- package/dist/utils/module-profile.js +32 -0
- package/dist/utils/platform-patcher.js +56 -7
- package/dist/utils/rbac-patcher.js +89 -210
- package/package.json +7 -2
- package/templates/add/auth/cmd/seed/main.go.hbs +15 -3
- package/templates/add/auth/docs/login.yaml.hbs +11 -1
- package/templates/add/auth/docs/mfa-verify.yaml.hbs +19 -0
- package/templates/add/auth/docs/provider-exchange.yaml.hbs +40 -0
- package/templates/add/auth/docs/provider-login.yaml.hbs +31 -0
- package/templates/add/auth/docs/refresh.yaml.hbs +7 -0
- package/templates/add/auth/docs/register.yaml.hbs +7 -0
- package/templates/add/auth/docs/reset-password.yaml.hbs +1 -1
- package/templates/add/auth/docs/schemas.yaml.hbs +59 -1
- package/templates/add/auth/docs/users-me-mfa-confirm.yaml.hbs +19 -0
- package/templates/add/auth/docs/users-me-mfa-disable.yaml.hbs +15 -0
- package/templates/add/auth/docs/users-me-mfa-setup.yaml.hbs +14 -0
- package/templates/add/auth/docs/users-me-mfa.yaml.hbs +12 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/browser_policy.go.hbs +98 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler.go.hbs +228 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_local.go.hbs +76 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_mfa.go.hbs +83 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_oauth.go.hbs +70 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_recovery.go.hbs +49 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_test.go.hbs +311 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_user.go.hbs +41 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/session_cookie.go.hbs +35 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt.go.hbs +35 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt_test.go.hbs +20 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store.go.hbs +129 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store_test.go.hbs +174 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +211 -0
- package/templates/add/auth/internal/app/user/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +18 -19
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg.go.hbs +213 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg_test.go.hbs +103 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_recovery.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore.go.hbs +228 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore_test.go.hbs +196 -0
- package/templates/add/auth/internal/app/user/application/contracts.go.hbs +52 -0
- package/templates/add/auth/internal/app/user/application/dto.go.hbs +75 -0
- package/templates/add/auth/internal/app/user/application/errors.go.hbs +62 -0
- package/templates/add/auth/internal/app/user/application/external_login.go.hbs +198 -0
- package/templates/add/auth/internal/app/user/application/jwt.go.hbs +58 -0
- package/templates/add/auth/internal/app/user/application/local_auth.go.hbs +96 -0
- package/templates/add/auth/internal/app/user/application/mfa_service.go.hbs +449 -0
- package/templates/add/auth/internal/app/user/application/mfa_service_test.go.hbs +200 -0
- package/templates/add/auth/internal/app/user/application/oauth.go.hbs +132 -0
- package/templates/add/auth/internal/app/user/application/provider_test.go.hbs +285 -0
- package/templates/add/auth/internal/app/user/application/recovery.go.hbs +82 -0
- package/templates/add/auth/internal/app/user/application/recovery_service.go.hbs +112 -0
- package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
- package/templates/add/auth/internal/app/user/application/service_test.go.hbs +891 -0
- package/templates/add/auth/internal/app/user/application/sessions.go.hbs +99 -0
- package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
- package/templates/add/auth/internal/app/user/application/user_query.go.hbs +65 -0
- package/templates/add/auth/internal/app/user/composition.go.hbs +168 -0
- package/templates/add/auth/internal/app/user/domain/entity.go.hbs +41 -0
- package/templates/add/auth/internal/app/user/domain/errors.go.hbs +32 -0
- package/templates/add/auth/internal/app/user/ports/password.go.hbs +9 -0
- package/templates/add/auth/internal/app/user/ports/repository.go.hbs +90 -0
- package/templates/add/auth/internal/platform/authprovider/google/google.go.hbs +389 -0
- package/templates/add/auth/internal/platform/authprovider/google/google_test.go.hbs +312 -0
- package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +10 -5
- package/templates/add/auth/migrations/create_identities.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_mfa.down.sql.hbs +3 -0
- package/templates/add/auth/migrations/create_mfa.up.sql.hbs +29 -0
- package/templates/add/auth/migrations/create_users.up.sql.hbs +4 -3
- package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +142 -0
- package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler_test.go.hbs +19 -0
- package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/model.go.hbs +48 -0
- package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +127 -0
- package/templates/add/rbac/internal/app/role/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +8 -8
- package/templates/add/rbac/internal/app/role/application/dto.go.hbs +47 -0
- package/templates/add/rbac/internal/app/role/application/errors.go.hbs +19 -0
- package/templates/add/rbac/internal/app/role/application/service.go.hbs +157 -0
- package/templates/add/rbac/internal/app/role/{service_test.go.hbs → application/service_test.go.hbs} +26 -19
- package/templates/add/rbac/internal/app/role/composition.go.hbs +48 -0
- package/templates/add/rbac/internal/app/role/domain/entity.go.hbs +23 -0
- package/templates/add/rbac/internal/app/role/domain/errors.go.hbs +26 -0
- package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +25 -0
- package/templates/add/rbac/migrations/add_roles.down.sql.hbs +3 -11
- package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -6
- package/templates/add/worker/internal/platform/queue/river_test.go.hbs +84 -0
- package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +358 -121
- package/templates/create/base/.env.example.hbs +0 -1
- package/templates/create/base/.golangci.yml.hbs +2 -2
- package/templates/create/base/AGENTS.md.hbs +279 -67
- package/templates/create/base/Makefile.hbs +2 -1
- package/templates/create/base/README.md.hbs +115 -32
- package/templates/create/base/cmd/api/wiring.go.hbs +13 -9
- package/templates/create/base/internal/composition/doc.go.hbs +7 -0
- package/templates/create/base/internal/platform/database/database.go.hbs +3 -3
- package/templates/create/base/internal/shared/apperror/apperror.go.hbs +15 -2
- package/templates/create/base/internal/shared/config/config.go.hbs +0 -8
- package/templates/create/base/internal/shared/middleware/cors_test.go.hbs +40 -0
- package/templates/create/base/internal/shared/middleware/error.go.hbs +15 -5
- package/templates/create/features/docs/architecture.md.hbs +92 -32
- package/templates/create/features/docs/patterns.md.hbs +137 -91
- package/templates/create/features/docs/techstack.md.hbs +18 -3
- package/templates/generate/module/hexagonal/adapters/inbound/http/dto.go.hbs +45 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/dto.minimal.go.hbs +28 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +182 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler.minimal.go.hbs +83 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler_crud_test.go.hbs +18 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler_test.go.hbs +30 -0
- package/templates/generate/module/hexagonal/adapters/outbound/postgres/model.go.hbs +37 -0
- package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +95 -0
- package/templates/generate/module/{repository_test.go.hbs → hexagonal/adapters/outbound/postgres/repository_test.go.hbs} +8 -8
- package/templates/generate/module/hexagonal/application/commands.crud.go.hbs +54 -0
- package/templates/generate/module/hexagonal/application/commands.go.hbs +25 -0
- package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +66 -0
- package/templates/generate/module/hexagonal/application/dto.go.hbs +35 -0
- package/templates/generate/module/hexagonal/application/dto.minimal.go.hbs +25 -0
- package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +33 -0
- package/templates/generate/module/hexagonal/application/queries.go.hbs +25 -0
- package/templates/generate/module/hexagonal/application/service.crud.go.hbs +73 -0
- package/templates/generate/module/hexagonal/application/service.go.hbs +29 -0
- package/templates/generate/module/hexagonal/application/service_test.go.hbs +62 -0
- package/templates/generate/module/hexagonal/composition.go.hbs +27 -0
- package/templates/generate/module/hexagonal/domain/entity.go.hbs +20 -0
- package/templates/generate/module/hexagonal/domain/errors.go.hbs +11 -0
- package/templates/generate/module/hexagonal/ports/repository.go.hbs +38 -0
- package/templates/generate/module/migration.up.sql.hbs +1 -1
- package/dist/utils/method-patcher.js +0 -357
- package/templates/add/auth/docs/google-callback.yaml.hbs +0 -22
- package/templates/add/auth/docs/google-login.yaml.hbs +0 -7
- package/templates/add/auth/internal/app/user/dto.go.hbs +0 -77
- package/templates/add/auth/internal/app/user/errors.go.hbs +0 -43
- package/templates/add/auth/internal/app/user/handler.go.hbs +0 -276
- package/templates/add/auth/internal/app/user/jwt.go.hbs +0 -108
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -39
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -31
- package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
- package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -30
- package/templates/add/auth/internal/app/user/repository.go.hbs +0 -137
- package/templates/add/auth/internal/app/user/service.go.hbs +0 -531
- package/templates/add/auth/internal/app/user/service_test.go.hbs +0 -316
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -30
- package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +0 -144
- package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +0 -147
- package/templates/add/rbac/internal/app/role/dto.go.hbs +0 -45
- package/templates/add/rbac/internal/app/role/errors.go.hbs +0 -39
- package/templates/add/rbac/internal/app/role/handler.go.hbs +0 -104
- package/templates/add/rbac/internal/app/role/model/permission.go.hbs +0 -12
- package/templates/add/rbac/internal/app/role/model/role.go.hbs +0 -22
- package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +0 -11
- package/templates/add/rbac/internal/app/role/repository.go.hbs +0 -97
- package/templates/add/rbac/internal/app/role/service.go.hbs +0 -217
- package/templates/generate/module/dto.go.hbs +0 -36
- package/templates/generate/module/errors.go.hbs +0 -33
- package/templates/generate/module/handler.go.hbs +0 -134
- package/templates/generate/module/handler_test.go.hbs +0 -174
- package/templates/generate/module/minimal/dto.go.hbs +0 -28
- package/templates/generate/module/minimal/handler.go.hbs +0 -48
- package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
- package/templates/generate/module/minimal/service.go.hbs +0 -45
- package/templates/generate/module/minimal/service_test.go.hbs +0 -77
- package/templates/generate/module/model/model.go.hbs +0 -36
- package/templates/generate/module/repository.go.hbs +0 -103
- package/templates/generate/module/service.go.hbs +0 -108
- package/templates/generate/module/service_test.go.hbs +0 -161
|
@@ -0,0 +1,334 @@
|
|
|
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.hexagonalMarkersPresent = hexagonalMarkersPresent;
|
|
7
|
+
exports.assertHexagonalMethodAbsent = assertHexagonalMethodAbsent;
|
|
8
|
+
exports.patchHexagonalMethod = patchHexagonalMethod;
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
const marker_patch_1 = require("./marker-patch");
|
|
11
|
+
const naming_1 = require("./naming");
|
|
12
|
+
const DTO_MARKER = "// go-scaffold:dto";
|
|
13
|
+
const REPOSITORY_INTERFACE_MARKER = "// go-scaffold:repository-interface";
|
|
14
|
+
const QUERY_REPOSITORY_INTERFACE_MARKER = "// go-scaffold:query-repository-interface";
|
|
15
|
+
const REPOSITORY_METHODS_MARKER = "// go-scaffold:repository-methods";
|
|
16
|
+
const SERVICE_MARKER = "// go-scaffold:service-methods";
|
|
17
|
+
const SERVICE_INTERFACE_MARKER = "// go-scaffold:service-interface";
|
|
18
|
+
const COMMAND_MARKER = "// go-scaffold:command-methods";
|
|
19
|
+
const COMMAND_INTERFACE_MARKER = "// go-scaffold:command-interface";
|
|
20
|
+
const QUERY_MARKER = "// go-scaffold:query-methods";
|
|
21
|
+
const QUERY_INTERFACE_MARKER = "// go-scaffold:query-interface";
|
|
22
|
+
const HANDLER_ROUTES_MARKER = "// go-scaffold:handler-routes";
|
|
23
|
+
const HANDLER_FUNCS_MARKER = "// go-scaffold:handler-funcs";
|
|
24
|
+
const REPOSITORY_STUB_FIELDS_MARKER = "// go-scaffold:repository-stub-fields";
|
|
25
|
+
const REPOSITORY_STUB_METHODS_MARKER = "// go-scaffold:repository-stub-methods";
|
|
26
|
+
function isCqrs(paths) {
|
|
27
|
+
return Boolean(paths.commandPath && paths.queryPath);
|
|
28
|
+
}
|
|
29
|
+
function read(files, file) {
|
|
30
|
+
const value = files.get(file);
|
|
31
|
+
if (value !== undefined)
|
|
32
|
+
return value;
|
|
33
|
+
const content = fs_extra_1.default.readFileSync(file, "utf8");
|
|
34
|
+
files.set(file, content);
|
|
35
|
+
return content;
|
|
36
|
+
}
|
|
37
|
+
function write(files, file, content) {
|
|
38
|
+
files.set(file, content);
|
|
39
|
+
}
|
|
40
|
+
function addImport(content, importPath) {
|
|
41
|
+
return (0, marker_patch_1.ensureImport)(content, importPath);
|
|
42
|
+
}
|
|
43
|
+
function requestDTOMarker(content) {
|
|
44
|
+
if ((0, marker_patch_1.hasMarker)(content, DTO_MARKER))
|
|
45
|
+
return DTO_MARKER;
|
|
46
|
+
if ((0, marker_patch_1.hasMarker)(content, "// go-scaffold:user-dto"))
|
|
47
|
+
return "// go-scaffold:user-dto";
|
|
48
|
+
throw new Error("inbound HTTP DTO file is missing a go-scaffold DTO marker");
|
|
49
|
+
}
|
|
50
|
+
function insert(content, marker, block) {
|
|
51
|
+
return (0, marker_patch_1.insertBeforeMarker)(content, marker, block);
|
|
52
|
+
}
|
|
53
|
+
function serviceApplicationTarget(paths, type) {
|
|
54
|
+
if (isCqrs(paths)) {
|
|
55
|
+
if (type === "get")
|
|
56
|
+
return { path: paths.queryPath, marker: QUERY_MARKER, receiver: "h *QueryHandler" };
|
|
57
|
+
return { path: paths.commandPath, marker: COMMAND_MARKER, receiver: "h *CommandHandler" };
|
|
58
|
+
}
|
|
59
|
+
if (!paths.servicePath)
|
|
60
|
+
throw new Error("service module is missing application/service.go");
|
|
61
|
+
return { path: paths.servicePath, marker: SERVICE_MARKER, receiver: "s *Service" };
|
|
62
|
+
}
|
|
63
|
+
function applicationInterfaceMarker(paths, type) {
|
|
64
|
+
if (isCqrs(paths)) {
|
|
65
|
+
return type === "get"
|
|
66
|
+
? { path: paths.queryPath, marker: QUERY_INTERFACE_MARKER }
|
|
67
|
+
: { path: paths.commandPath, marker: COMMAND_INTERFACE_MARKER };
|
|
68
|
+
}
|
|
69
|
+
return { path: paths.servicePath, marker: SERVICE_INTERFACE_MARKER };
|
|
70
|
+
}
|
|
71
|
+
function methodSignature(naming, method, opts) {
|
|
72
|
+
if (opts.type === "get" && opts.getMode === "all") {
|
|
73
|
+
return `${method.pascalName}(context.Context, int, int) ([]domain.${naming.pascalName}, error)`;
|
|
74
|
+
}
|
|
75
|
+
if (opts.type === "get") {
|
|
76
|
+
return `${method.pascalName}(context.Context, string) (*domain.${naming.pascalName}, error)`;
|
|
77
|
+
}
|
|
78
|
+
if (opts.type === "post") {
|
|
79
|
+
return `${method.pascalName}(context.Context, ${method.pascalName}Input) (*domain.${naming.pascalName}, error)`;
|
|
80
|
+
}
|
|
81
|
+
return `${method.pascalName}(context.Context, uuid.UUID) error`;
|
|
82
|
+
}
|
|
83
|
+
function applicationMethod(naming, method, opts, receiver) {
|
|
84
|
+
const target = receiver.startsWith("s ") ? "s" : "h";
|
|
85
|
+
if (opts.type === "get" && opts.getMode === "all") {
|
|
86
|
+
return [
|
|
87
|
+
`func (${receiver}) ${method.pascalName}(ctx context.Context, limit, offset int) ([]domain.${naming.pascalName}, error) {`,
|
|
88
|
+
`\treturn ${target}.repo.FindAll(ctx, limit, offset)`,
|
|
89
|
+
`}`,
|
|
90
|
+
"",
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
if (opts.type === "get") {
|
|
94
|
+
const field = (0, naming_1.toCamelCase)(opts.field ?? "field");
|
|
95
|
+
const fieldPascal = (0, naming_1.toPascalCase)(opts.field ?? "field");
|
|
96
|
+
return [
|
|
97
|
+
`func (${receiver}) ${method.pascalName}(ctx context.Context, ${field} string) (*domain.${naming.pascalName}, error) {`,
|
|
98
|
+
`\treturn ${target}.repo.FindBy${fieldPascal}(ctx, ${field})`,
|
|
99
|
+
`}`,
|
|
100
|
+
"",
|
|
101
|
+
].join("\n");
|
|
102
|
+
}
|
|
103
|
+
if (opts.type === "post") {
|
|
104
|
+
return [
|
|
105
|
+
`func (${receiver}) ${method.pascalName}(ctx context.Context, in ${method.pascalName}Input) (*domain.${naming.pascalName}, error) {`,
|
|
106
|
+
`\t_ = ctx`,
|
|
107
|
+
`\t_ = in`,
|
|
108
|
+
`\treturn nil, domain.ErrNotImplemented`,
|
|
109
|
+
`}`,
|
|
110
|
+
"",
|
|
111
|
+
].join("\n");
|
|
112
|
+
}
|
|
113
|
+
return [
|
|
114
|
+
`func (${receiver}) ${method.pascalName}(ctx context.Context, id uuid.UUID) error {`,
|
|
115
|
+
`\t_ = ctx`,
|
|
116
|
+
`\t_ = id`,
|
|
117
|
+
`\treturn domain.ErrNotImplemented`,
|
|
118
|
+
`}`,
|
|
119
|
+
"",
|
|
120
|
+
].join("\n");
|
|
121
|
+
}
|
|
122
|
+
function handlerMethod(naming, method, opts, cqrs, routeReceiver, errorMapper) {
|
|
123
|
+
const receiver = cqrs ? (opts.type === "get" ? "h.queries" : "h.commands") : "h.svc";
|
|
124
|
+
if (opts.type === "get" && opts.getMode === "all") {
|
|
125
|
+
return {
|
|
126
|
+
route: `${routeReceiver}.GET("/${method.pathSegment}", h.${method.handlerName})`,
|
|
127
|
+
imports: ["net/http", "pagination"],
|
|
128
|
+
body: [
|
|
129
|
+
`func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
|
|
130
|
+
`\tp := pagination.Parse(c)`,
|
|
131
|
+
`\titems, err := ${receiver}.${method.pascalName}(c.Request.Context(), p.Limit, p.Offset)`,
|
|
132
|
+
`\tif err != nil {`,
|
|
133
|
+
`\t\tc.Error(${errorMapper}(err))`,
|
|
134
|
+
`\t\treturn`,
|
|
135
|
+
`\t}`,
|
|
136
|
+
`\tout := make([]response, len(items))`,
|
|
137
|
+
`\tfor i := range items {`,
|
|
138
|
+
`\t\tout[i] = toResponse(application.ToResponse(&items[i]))`,
|
|
139
|
+
`\t}`,
|
|
140
|
+
`\tc.JSON(http.StatusOK, p.Response(out))`,
|
|
141
|
+
`}`,
|
|
142
|
+
"",
|
|
143
|
+
].join("\n"),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (opts.type === "get") {
|
|
147
|
+
const field = (0, naming_1.toCamelCase)(opts.field ?? "field");
|
|
148
|
+
const column = (0, naming_1.toDbName)(opts.field ?? "field");
|
|
149
|
+
return {
|
|
150
|
+
route: `${routeReceiver}.GET("/${column}/:${field}", h.${method.handlerName})`,
|
|
151
|
+
imports: ["net/http"],
|
|
152
|
+
body: [
|
|
153
|
+
`func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
|
|
154
|
+
`\t${field} := c.Param("${field}")`,
|
|
155
|
+
`\tm, err := ${receiver}.${method.pascalName}(c.Request.Context(), ${field})`,
|
|
156
|
+
`\tif err != nil {`,
|
|
157
|
+
`\t\tc.Error(${errorMapper}(err))`,
|
|
158
|
+
`\t\treturn`,
|
|
159
|
+
`\t}`,
|
|
160
|
+
`\tc.JSON(http.StatusOK, toResponse(application.ToResponse(m)))`,
|
|
161
|
+
`}`,
|
|
162
|
+
"",
|
|
163
|
+
].join("\n"),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (opts.type === "post") {
|
|
167
|
+
return {
|
|
168
|
+
route: `${routeReceiver}.POST("/${method.pathSegment}", h.${method.handlerName})`,
|
|
169
|
+
imports: ["net/http", "httpx"],
|
|
170
|
+
body: [
|
|
171
|
+
`func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
|
|
172
|
+
`\tvar in ${method.pascalName}Input`,
|
|
173
|
+
`\tif err := c.ShouldBindJSON(&in); err != nil {`,
|
|
174
|
+
`\t\tc.Error(httpx.BindErr(err))`,
|
|
175
|
+
`\t\treturn`,
|
|
176
|
+
`\t}`,
|
|
177
|
+
`\tm, err := ${receiver}.${method.pascalName}(c.Request.Context(), to${method.pascalName}Input(in))`,
|
|
178
|
+
`\tif err != nil {`,
|
|
179
|
+
`\t\tc.Error(${errorMapper}(err))`,
|
|
180
|
+
`\t\treturn`,
|
|
181
|
+
`\t}`,
|
|
182
|
+
`\tc.JSON(http.StatusCreated, toResponse(application.ToResponse(m)))`,
|
|
183
|
+
`}`,
|
|
184
|
+
"",
|
|
185
|
+
].join("\n"),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
route: `${routeReceiver}.${opts.type.toUpperCase()}("/:id/${method.pathSegment}", h.${method.handlerName})`,
|
|
190
|
+
imports: ["net/http", "httpx"],
|
|
191
|
+
body: [
|
|
192
|
+
`func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
|
|
193
|
+
`\tid, ok := httpx.ParseID(c)`,
|
|
194
|
+
`\tif !ok {`,
|
|
195
|
+
`\t\treturn`,
|
|
196
|
+
`\t}`,
|
|
197
|
+
`\tif err := ${receiver}.${method.pascalName}(c.Request.Context(), id); err != nil {`,
|
|
198
|
+
`\t\tc.Error(${errorMapper}(err))`,
|
|
199
|
+
`\t\treturn`,
|
|
200
|
+
`\t}`,
|
|
201
|
+
`\tc.Status(http.StatusNoContent)`,
|
|
202
|
+
`}`,
|
|
203
|
+
"",
|
|
204
|
+
].join("\n"),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function assertNotDuplicate(content, needle, what) {
|
|
208
|
+
if (content.includes(needle))
|
|
209
|
+
throw new Error(`${what} already exists — pick a different method name`);
|
|
210
|
+
}
|
|
211
|
+
function handlerRouteReceiver(content) {
|
|
212
|
+
// Auth's protected routes use usersGroup; generated CRUD modules use the
|
|
213
|
+
// local g group. Both remain the module's inbound adapter boundary.
|
|
214
|
+
return (0, marker_patch_1.hasMarker)(content, "// go-scaffold:user-routes") ? "usersGroup" : "g";
|
|
215
|
+
}
|
|
216
|
+
function hexagonalMarkersPresent(paths) {
|
|
217
|
+
if (!fs_extra_1.default.existsSync(paths.dtoPath) || !fs_extra_1.default.existsSync(paths.requestDTOPath) || !fs_extra_1.default.existsSync(paths.portsPath) || !fs_extra_1.default.existsSync(paths.repositoryAdapterPath) || !fs_extra_1.default.existsSync(paths.handlerPath) || !fs_extra_1.default.existsSync(paths.serviceTestPath))
|
|
218
|
+
return false;
|
|
219
|
+
const handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
|
|
220
|
+
if (!(0, marker_patch_1.hasMarker)(handler, HANDLER_ROUTES_MARKER) || !(0, marker_patch_1.hasMarker)(handler, HANDLER_FUNCS_MARKER))
|
|
221
|
+
return false;
|
|
222
|
+
const dto = fs_extra_1.default.readFileSync(paths.dtoPath, "utf8");
|
|
223
|
+
const requestDTO = fs_extra_1.default.readFileSync(paths.requestDTOPath, "utf8");
|
|
224
|
+
const ports = fs_extra_1.default.readFileSync(paths.portsPath, "utf8");
|
|
225
|
+
const adapter = fs_extra_1.default.readFileSync(paths.repositoryAdapterPath, "utf8");
|
|
226
|
+
if (!(0, marker_patch_1.hasMarker)(dto, DTO_MARKER) ||
|
|
227
|
+
(!(0, marker_patch_1.hasMarker)(requestDTO, DTO_MARKER) && !(0, marker_patch_1.hasMarker)(requestDTO, "// go-scaffold:user-dto")) ||
|
|
228
|
+
!(0, marker_patch_1.hasMarker)(adapter, REPOSITORY_METHODS_MARKER))
|
|
229
|
+
return false;
|
|
230
|
+
if (isCqrs(paths)) {
|
|
231
|
+
return Boolean(paths.commandPath && paths.queryPath && (0, marker_patch_1.hasMarker)(fs_extra_1.default.readFileSync(paths.commandPath, "utf8"), COMMAND_MARKER) && (0, marker_patch_1.hasMarker)(fs_extra_1.default.readFileSync(paths.commandPath, "utf8"), COMMAND_INTERFACE_MARKER) && (0, marker_patch_1.hasMarker)(fs_extra_1.default.readFileSync(paths.queryPath, "utf8"), QUERY_MARKER) && (0, marker_patch_1.hasMarker)(fs_extra_1.default.readFileSync(paths.queryPath, "utf8"), QUERY_INTERFACE_MARKER) && (0, marker_patch_1.hasMarker)(ports, COMMAND_REPOSITORY_INTERFACE_MARKER) && (0, marker_patch_1.hasMarker)(ports, QUERY_REPOSITORY_INTERFACE_MARKER));
|
|
232
|
+
}
|
|
233
|
+
return Boolean(paths.servicePath && (0, marker_patch_1.hasMarker)(fs_extra_1.default.readFileSync(paths.servicePath, "utf8"), SERVICE_MARKER) && (0, marker_patch_1.hasMarker)(fs_extra_1.default.readFileSync(paths.servicePath, "utf8"), SERVICE_INTERFACE_MARKER) && (0, marker_patch_1.hasMarker)(ports, REPOSITORY_INTERFACE_MARKER));
|
|
234
|
+
}
|
|
235
|
+
const COMMAND_REPOSITORY_INTERFACE_MARKER = "// go-scaffold:command-repository-interface";
|
|
236
|
+
function assertHexagonalMethodAbsent(paths, method) {
|
|
237
|
+
assertNotDuplicate(fs_extra_1.default.readFileSync(paths.handlerPath, "utf8"), `func (h *Handler) ${method.handlerName}(`, `handler method "${method.handlerName}"`);
|
|
238
|
+
const appFiles = [paths.servicePath, paths.commandPath, paths.queryPath].filter((file) => Boolean(file));
|
|
239
|
+
for (const file of appFiles)
|
|
240
|
+
assertNotDuplicate(fs_extra_1.default.readFileSync(file, "utf8"), `) ${method.pascalName}(`, `application method "${method.pascalName}"`);
|
|
241
|
+
}
|
|
242
|
+
function patchHexagonalMethod(paths, naming, method, opts, goModule) {
|
|
243
|
+
assertHexagonalMethodAbsent(paths, method);
|
|
244
|
+
if (opts.type === "get" && opts.getMode !== "all" && !opts.field)
|
|
245
|
+
throw new Error("--field is required for --type get --get-mode one");
|
|
246
|
+
if (opts.type === "get" && opts.field?.toLowerCase() === "id")
|
|
247
|
+
throw new Error('--field cannot be "id" — GET /:id already exists as the default lookup');
|
|
248
|
+
const files = new Map();
|
|
249
|
+
const cqrs = isCqrs(paths);
|
|
250
|
+
const interfaceTarget = applicationInterfaceMarker(paths, opts.type);
|
|
251
|
+
let interfaceFile = read(files, interfaceTarget.path);
|
|
252
|
+
interfaceFile = insert(interfaceFile, interfaceTarget.marker, methodSignature(naming, method, opts));
|
|
253
|
+
write(files, interfaceTarget.path, interfaceFile);
|
|
254
|
+
const target = serviceApplicationTarget(paths, opts.type);
|
|
255
|
+
let applicationFile = read(files, target.path);
|
|
256
|
+
applicationFile = addImport(applicationFile, "context");
|
|
257
|
+
if (opts.type !== "get" && opts.type !== "post")
|
|
258
|
+
applicationFile = addImport(applicationFile, "github.com/google/uuid");
|
|
259
|
+
applicationFile = insert(applicationFile, target.marker, applicationMethod(naming, method, opts, target.receiver));
|
|
260
|
+
write(files, target.path, applicationFile);
|
|
261
|
+
let handler = read(files, paths.handlerPath);
|
|
262
|
+
const handlerResult = handlerMethod(naming, method, opts, cqrs, handlerRouteReceiver(handler), paths.handlerErrorMapper ?? "appError");
|
|
263
|
+
handler = insert(handler, HANDLER_ROUTES_MARKER, handlerResult.route);
|
|
264
|
+
handler = insert(handler, HANDLER_FUNCS_MARKER, handlerResult.body);
|
|
265
|
+
for (const need of handlerResult.imports)
|
|
266
|
+
handler = addImport(handler, need.startsWith("net/") ? need : `${goModule}/internal/shared/${need}`);
|
|
267
|
+
write(files, paths.handlerPath, handler);
|
|
268
|
+
if (opts.type === "post") {
|
|
269
|
+
let dto = read(files, paths.dtoPath);
|
|
270
|
+
const inputName = `${method.pascalName}Input`;
|
|
271
|
+
assertNotDuplicate(dto, `type ${inputName} struct`, `DTO "${inputName}"`);
|
|
272
|
+
dto = insert(dto, DTO_MARKER, [`type ${inputName} struct {`, `\t// TODO: add request fields`, `}`, ""].join("\n"));
|
|
273
|
+
write(files, paths.dtoPath, dto);
|
|
274
|
+
let requestDTO = read(files, paths.requestDTOPath);
|
|
275
|
+
assertNotDuplicate(requestDTO, `type ${inputName} struct`, `HTTP DTO "${inputName}"`);
|
|
276
|
+
requestDTO = addImport(requestDTO, `${goModule}/internal/app/${naming.pkg}/application`);
|
|
277
|
+
requestDTO = insert(requestDTO, requestDTOMarker(requestDTO), [
|
|
278
|
+
`type ${inputName} struct {`,
|
|
279
|
+
`\t// TODO: mirror request fields from application.${inputName} and add JSON/binding tags`,
|
|
280
|
+
`}`,
|
|
281
|
+
"",
|
|
282
|
+
`func to${inputName}(in ${inputName}) application.${inputName} {`,
|
|
283
|
+
`\t// TODO: map request fields explicitly into the application input.`,
|
|
284
|
+
`\t_ = in`,
|
|
285
|
+
`\treturn application.${inputName}{}`,
|
|
286
|
+
`}`,
|
|
287
|
+
"",
|
|
288
|
+
].join("\n"));
|
|
289
|
+
write(files, paths.requestDTOPath, requestDTO);
|
|
290
|
+
}
|
|
291
|
+
if (opts.type === "get" && opts.getMode !== "all") {
|
|
292
|
+
const fieldPascal = (0, naming_1.toPascalCase)(opts.field);
|
|
293
|
+
const fieldParam = (0, naming_1.toCamelCase)(opts.field);
|
|
294
|
+
const repoInterfaceMarker = cqrs ? QUERY_REPOSITORY_INTERFACE_MARKER : REPOSITORY_INTERFACE_MARKER;
|
|
295
|
+
let ports = read(files, paths.portsPath);
|
|
296
|
+
ports = insert(ports, repoInterfaceMarker, `FindBy${fieldPascal}(context.Context, string) (*domain.${naming.pascalName}, error)`);
|
|
297
|
+
write(files, paths.portsPath, ports);
|
|
298
|
+
let adapter = read(files, paths.repositoryAdapterPath);
|
|
299
|
+
const column = (0, naming_1.toDbName)(opts.field);
|
|
300
|
+
const modelType = paths.repositoryModelType ?? `${naming.pascalName}Model`;
|
|
301
|
+
const toDomain = paths.repositoryToDomain ?? "toDomain";
|
|
302
|
+
const errorMapper = paths.repositoryErrorMapper ?? "mapDatabaseError";
|
|
303
|
+
adapter = insert(adapter, REPOSITORY_METHODS_MARKER, [
|
|
304
|
+
`func (r *Repository) FindBy${fieldPascal}(ctx context.Context, ${fieldParam} string) (*domain.${naming.pascalName}, error) {`,
|
|
305
|
+
`\tvar row ${modelType}`,
|
|
306
|
+
`\tif err := tx.From(ctx, r.db).WithContext(ctx).First(&row, "${column} = ?", ${fieldParam}).Error; err != nil {`,
|
|
307
|
+
`\t\treturn nil, ${errorMapper}(err)`,
|
|
308
|
+
`\t}`,
|
|
309
|
+
`\treturn ${toDomain}(&row), nil`,
|
|
310
|
+
`}`,
|
|
311
|
+
"",
|
|
312
|
+
].join("\n"));
|
|
313
|
+
write(files, paths.repositoryAdapterPath, adapter);
|
|
314
|
+
let test = read(files, paths.serviceTestPath);
|
|
315
|
+
const stubField = `findBy${fieldPascal}Fn`;
|
|
316
|
+
assertNotDuplicate(test, `${stubField} func(`, `repository stub field "${stubField}"`);
|
|
317
|
+
test = insert(test, REPOSITORY_STUB_FIELDS_MARKER, `\t${stubField} func(context.Context, string) (*domain.${naming.pascalName}, error)`);
|
|
318
|
+
const stubReceiver = paths.repositoryStubReceiver ?? "s *repositoryStub";
|
|
319
|
+
const stubVariable = stubReceiver.split(" ")[0];
|
|
320
|
+
assertNotDuplicate(test, `func (${stubReceiver}) FindBy${fieldPascal}(`, `repository stub method "FindBy${fieldPascal}"`);
|
|
321
|
+
test = insert(test, REPOSITORY_STUB_METHODS_MARKER, [
|
|
322
|
+
`func (${stubReceiver}) FindBy${fieldPascal}(ctx context.Context, value string) (*domain.${naming.pascalName}, error) {`,
|
|
323
|
+
`\tif ${stubVariable}.${stubField} == nil {`,
|
|
324
|
+
`\t\tpanic("unexpected repository.FindBy${fieldPascal} call")`,
|
|
325
|
+
`\t}`,
|
|
326
|
+
`\treturn ${stubVariable}.${stubField}(ctx, value)`,
|
|
327
|
+
`}`,
|
|
328
|
+
"",
|
|
329
|
+
].join("\n"));
|
|
330
|
+
write(files, paths.serviceTestPath, test);
|
|
331
|
+
}
|
|
332
|
+
for (const [file, content] of files)
|
|
333
|
+
fs_extra_1.default.writeFileSync(file, content);
|
|
334
|
+
}
|
|
@@ -18,26 +18,28 @@ const UNUSED_API_LINE = "_ = api // dropped once `generate module` registers the
|
|
|
18
18
|
// the exact lines patchMainGo inserts for a module — one source of truth so
|
|
19
19
|
// unpatchMainGo removes precisely what patch added.
|
|
20
20
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
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.
|
|
21
|
+
// Generated features own their repository/service/handler composition. The
|
|
22
|
+
// API binary supplies infrastructure and security dependencies, then only
|
|
23
|
+
// registers the feature's handler.
|
|
29
24
|
function mainGoLines(patch) {
|
|
30
|
-
const modelAlias = `${patch.pkg}
|
|
31
|
-
const
|
|
32
|
-
const
|
|
25
|
+
const modelAlias = `${patch.pkg}postgres`; // every domain's persistence adapter is named "postgres"
|
|
26
|
+
const handlerArgs = ["db"];
|
|
27
|
+
const legacyHandlerArgs = [`${patch.pkg}Svc`];
|
|
33
28
|
if (patch.auth)
|
|
34
29
|
handlerArgs.push("cfg.JWTSecret");
|
|
30
|
+
// RBAC's Authz is owned by role/composition.go. wiring.go registers a
|
|
31
|
+
// permission-gated feature with that public capability; it no longer keeps a
|
|
32
|
+
// standalone root variable named `authz`.
|
|
35
33
|
if (patch.permission)
|
|
36
|
-
handlerArgs.push("
|
|
34
|
+
handlerArgs.push("roleComposition.Authz");
|
|
35
|
+
if (patch.auth)
|
|
36
|
+
legacyHandlerArgs.push("cfg.JWTSecret");
|
|
37
|
+
if (patch.permission)
|
|
38
|
+
legacyHandlerArgs.push("roleComposition.Authz");
|
|
37
39
|
return {
|
|
38
40
|
importLine: `"${patch.goModule}/internal/app/${patch.modulePath}"`,
|
|
39
|
-
modelImportLine: `${modelAlias} "${patch.goModule}/internal/app/${patch.modulePath}/
|
|
40
|
-
//
|
|
41
|
+
modelImportLine: `${modelAlias} "${patch.goModule}/internal/app/${patch.modulePath}/adapters/outbound/postgres"`,
|
|
42
|
+
// Development schema bootstrap creates tables but not the schema they live in — see
|
|
41
43
|
// the comment on go-scaffold:schemas in main.go.hbs. One Exec per schema,
|
|
42
44
|
// guarded by its own sentinel so two modules sharing a schema name only
|
|
43
45
|
// ever produce one line (not expected today, but cheap to keep safe).
|
|
@@ -47,16 +49,18 @@ function mainGoLines(patch) {
|
|
|
47
49
|
`}`,
|
|
48
50
|
].join("\n"),
|
|
49
51
|
schemaSentinel: `CREATE SCHEMA IF NOT EXISTS ${patch.schemaName}`,
|
|
50
|
-
migrateLine: `&${modelAlias}.${patch.pascalName}{},`,
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
52
|
+
migrateLine: `&${modelAlias}.${patch.pascalName}Model{},`,
|
|
53
|
+
// kept only so undo can clean projects generated before feature-local
|
|
54
|
+
// composition was introduced.
|
|
55
|
+
legacyServicePrefix: `${patch.pkg}Svc :=`,
|
|
56
|
+
legacyRouteLine: `${patch.pkg}.NewHandler(${legacyHandlerArgs.join(", ")}).Register(api)`,
|
|
54
57
|
// `api` is the one route group declared by main.go.hbs, prefixed with
|
|
55
58
|
// whatever apiPrefix the project chose at create time (e.g. /v1, /api,
|
|
56
59
|
// or none) — every module registers on it, there is no per-module choice.
|
|
57
|
-
// `
|
|
58
|
-
// is only ever set once that
|
|
59
|
-
|
|
60
|
+
// `roleComposition.Authz` only exists in main.go once `add rbac` has run —
|
|
61
|
+
// patch.permission is only ever set once that has been verified by the
|
|
62
|
+
// caller.
|
|
63
|
+
routeLine: `${patch.pkg}.NewHandlerFromDB(${handlerArgs.join(", ")}).Register(api)`,
|
|
60
64
|
};
|
|
61
65
|
}
|
|
62
66
|
// assertMainGoPatchable is the pre-flight for it: every marker patchMainGo
|
|
@@ -79,13 +83,13 @@ function assertMainGoPatchable(mainGoPath) {
|
|
|
79
83
|
}
|
|
80
84
|
}
|
|
81
85
|
// patchMainGo wires a newly generated module into cmd/api/wiring.go: its
|
|
82
|
-
// import, its model in the
|
|
86
|
+
// import, its model in the development bootstrap, and its route registration —
|
|
83
87
|
// via marker comments rather than a Go AST rewrite (ponytail: text insertion
|
|
84
88
|
// at a fixed marker is enough here; reach for go/ast if main.go ever needs
|
|
85
89
|
// edits markers can't express).
|
|
86
90
|
function patchMainGo(mainGoPath, patch) {
|
|
87
91
|
let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
|
|
88
|
-
const { importLine, modelImportLine, schemaLines, schemaSentinel, migrateLine,
|
|
92
|
+
const { importLine, modelImportLine, schemaLines, schemaSentinel, migrateLine, routeLine } = mainGoLines(patch);
|
|
89
93
|
// each guarded by its own sentinel so re-running after only the module
|
|
90
94
|
// folder was deleted (main.go still wired) is a no-op, not a dup that
|
|
91
95
|
// panics gin at startup.
|
|
@@ -93,9 +97,6 @@ function patchMainGo(mainGoPath, patch) {
|
|
|
93
97
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, modelImportLine, modelImportLine);
|
|
94
98
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SCHEMA_MARKER, schemaLines, schemaSentinel);
|
|
95
99
|
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);
|
|
99
100
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, ROUTE_MARKER, routeLine, routeLine);
|
|
100
101
|
content = (0, marker_patch_1.removeLines)(content, [UNUSED_API_LINE]);
|
|
101
102
|
fs_extra_1.default.writeFileSync(mainGoPath, content);
|
|
@@ -105,10 +106,11 @@ function patchMainGo(mainGoPath, patch) {
|
|
|
105
106
|
// main.go still compiles (api would otherwise be declared-and-unused).
|
|
106
107
|
function unpatchMainGo(mainGoPath, patch) {
|
|
107
108
|
let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
|
|
108
|
-
const { importLine, modelImportLine, schemaLines, migrateLine,
|
|
109
|
-
content = (0, marker_patch_1.removeLines)(content, [importLine, modelImportLine, migrateLine, routeLine]);
|
|
110
|
-
// by prefix: the service line
|
|
111
|
-
|
|
109
|
+
const { importLine, modelImportLine, schemaLines, migrateLine, legacyServicePrefix, legacyRouteLine, routeLine } = mainGoLines(patch);
|
|
110
|
+
content = (0, marker_patch_1.removeLines)(content, [importLine, modelImportLine, migrateLine, routeLine, legacyRouteLine]);
|
|
111
|
+
// by prefix: remove the named service line from projects generated before
|
|
112
|
+
// composition became feature-local.
|
|
113
|
+
content = (0, marker_patch_1.removeLinesByPrefix)(content, [legacyServicePrefix]);
|
|
112
114
|
// by contiguous block, not removeLines: every module's schema block is
|
|
113
115
|
// identical except the schema name, so a line-by-line removal would also
|
|
114
116
|
// strip another module's matching lines.
|
|
@@ -31,7 +31,13 @@ function ensureImport(content, importPath) {
|
|
|
31
31
|
// the search argument is a plain string, not a regex — collapsing any "$$"
|
|
32
32
|
// that happens to appear in importLine. A function's return value is spliced
|
|
33
33
|
// in literally, so this holds regardless of what importPath contains.
|
|
34
|
-
|
|
34
|
+
if (content.includes("import (\n")) {
|
|
35
|
+
return content.replace(/import \(\n/, () => `import (\n\t${importLine}\n`);
|
|
36
|
+
}
|
|
37
|
+
// A minimal CQRS service has no imports until the first method is added.
|
|
38
|
+
// Create a complete block after the package clause so later generated
|
|
39
|
+
// methods can use the same idempotent path.
|
|
40
|
+
return content.replace(/^(package [^\n]+\n)/, (_match, packageLine) => `${packageLine}\nimport (\n\t${importLine}\n)\n`);
|
|
35
41
|
}
|
|
36
42
|
// insertBeforeMarkerOnce: like insertBeforeMarker but a no-op if `sentinel`
|
|
37
43
|
// already appears in the file. Makes module wiring idempotent — re-running
|
|
@@ -18,7 +18,7 @@ function existingModulePackages(projectDir) {
|
|
|
18
18
|
.map((entry) => entry.name);
|
|
19
19
|
}
|
|
20
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
|
|
21
|
+
// came from "order-items". Everything else a command derives — the entity type
|
|
22
22
|
// (OrderItem), the route (/order-items), the table (order_items), the
|
|
23
23
|
// migration filename — does depend on those boundaries, so re-deriving them
|
|
24
24
|
// from the package name yields Orderitem/orderitems and code that doesn't
|
|
@@ -29,15 +29,21 @@ function existingModulePackages(projectDir) {
|
|
|
29
29
|
// prints as the next command to run. Following that instruction produced
|
|
30
30
|
// `undefined: model.Orderitem (but have OrderItem)`.
|
|
31
31
|
//
|
|
32
|
-
// model
|
|
33
|
-
//
|
|
34
|
-
//
|
|
32
|
+
// The outbound persistence model is where the generated PascalCase type
|
|
33
|
+
// survives, so kebab-casing it recovers the exact string `generate module` was
|
|
34
|
+
// originally given.
|
|
35
35
|
function pascalNameOnDisk(projectDir, pkg) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
// Split hexagonal modules keep persistence models in the outbound adapter.
|
|
37
|
+
// The package name still loses word boundaries (`orderitem`), so recover
|
|
38
|
+
// the original PascalCase entity from the generated `OrderItemModel` type
|
|
39
|
+
// before commands such as `undo module orderitem` rebuild their wiring.
|
|
40
|
+
const adapterModelPath = path_1.default.join(projectDir, "internal", "app", pkg, "adapters", "outbound", "postgres", "model.go");
|
|
41
|
+
if (fs_extra_1.default.existsSync(adapterModelPath)) {
|
|
42
|
+
const match = fs_extra_1.default.readFileSync(adapterModelPath, "utf8").match(/^type\s+([A-Z]\w*)Model\s+struct\b/m);
|
|
43
|
+
if (match)
|
|
44
|
+
return match[1];
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
41
47
|
}
|
|
42
48
|
function resolveProjectModuleNaming(projectDir, rawName) {
|
|
43
49
|
const located = (0, naming_1.resolveExistingModuleNaming)(rawName, existingModulePackages(projectDir));
|
|
@@ -52,7 +58,7 @@ function resolveProjectModuleNaming(projectDir, rawName) {
|
|
|
52
58
|
return located; // model.go holds something we can't derive a module name from
|
|
53
59
|
}
|
|
54
60
|
// Only trust the correction when it still points at the same package.
|
|
55
|
-
// A hand-renamed struct
|
|
56
|
-
//
|
|
61
|
+
// A hand-renamed struct whose type no longer matches its folder would
|
|
62
|
+
// otherwise silently retarget the command at another module.
|
|
57
63
|
return fromDisk.pkg === located.pkg ? fromDisk : located;
|
|
58
64
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.moduleProfileFor = moduleProfileFor;
|
|
4
|
+
exports.architectureForModuleProfile = architectureForModuleProfile;
|
|
5
|
+
exports.describeModuleProfile = describeModuleProfile;
|
|
6
|
+
function moduleProfileFor(moduleSurface, applicationStyle) {
|
|
7
|
+
if (moduleSurface === "minimal" && applicationStyle === "service")
|
|
8
|
+
return "lean";
|
|
9
|
+
if (moduleSurface === "crud" && applicationStyle === "service")
|
|
10
|
+
return "crud";
|
|
11
|
+
if (moduleSurface === "minimal" && applicationStyle === "cqrs")
|
|
12
|
+
return "cqrs";
|
|
13
|
+
return "advanced";
|
|
14
|
+
}
|
|
15
|
+
function architectureForModuleProfile(profile) {
|
|
16
|
+
if (profile === "crud") {
|
|
17
|
+
return { moduleSurface: "crud", applicationStyle: "service" };
|
|
18
|
+
}
|
|
19
|
+
if (profile === "cqrs") {
|
|
20
|
+
return { moduleSurface: "minimal", applicationStyle: "cqrs" };
|
|
21
|
+
}
|
|
22
|
+
return { moduleSurface: "minimal", applicationStyle: "service" };
|
|
23
|
+
}
|
|
24
|
+
function describeModuleProfile(profile) {
|
|
25
|
+
if (profile === "lean")
|
|
26
|
+
return "Lean (minimal + service)";
|
|
27
|
+
if (profile === "crud")
|
|
28
|
+
return "CRUD (CRUD surface + service)";
|
|
29
|
+
if (profile === "cqrs")
|
|
30
|
+
return "CQRS (minimal surface + CQRS)";
|
|
31
|
+
return "Advanced (choose surface + application boundary)";
|
|
32
|
+
}
|