@nakedev/go-scaffold 0.4.3 → 0.5.1
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 +567 -513
- package/dist/commands/auth.js +12 -1
- package/dist/commands/check.js +281 -0
- package/dist/commands/create.js +2 -1
- package/dist/commands/generate.js +5 -2
- package/dist/commands/method.js +66 -70
- package/dist/commands/observability.js +4 -53
- package/dist/commands/rbac.js +19 -8
- package/dist/commands/undo.js +6 -3
- package/dist/commands/worker.js +14 -4
- package/dist/index.js +13 -1
- package/dist/templates/auth-manifest.js +46 -45
- package/dist/templates/create-manifest.js +4 -0
- package/dist/templates/module-manifest.js +82 -60
- package/dist/templates/rbac-manifest.js +15 -11
- package/dist/templates/worker-manifest.js +4 -1
- package/dist/types.js +2 -0
- package/dist/utils/auth-patcher.js +22 -22
- package/dist/utils/config.js +24 -5
- package/dist/utils/docs-patcher.js +68 -0
- package/dist/utils/hexagonal-method-patcher.js +334 -0
- package/dist/utils/main-patcher.js +3 -3
- package/dist/utils/module-location.js +17 -11
- package/dist/utils/platform-patcher.js +27 -0
- package/dist/utils/rbac-patcher.js +73 -216
- package/package.json +1 -1
- package/templates/add/auth/cmd/seed/main.go.hbs +2 -0
- package/templates/add/auth/internal/app/user/{browser_policy.go.hbs → adapters/inbound/http/browser_policy.go.hbs} +3 -3
- package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
- package/templates/add/auth/internal/app/user/{handler.go.hbs → adapters/inbound/http/handler.go.hbs} +103 -8
- package/templates/add/auth/internal/app/user/{handler_local.go.hbs → adapters/inbound/http/handler_local.go.hbs} +8 -7
- package/templates/add/auth/internal/app/user/{handler_mfa.go.hbs → adapters/inbound/http/handler_mfa.go.hbs} +6 -6
- package/templates/add/auth/internal/app/user/{handler_oauth.go.hbs → adapters/inbound/http/handler_oauth.go.hbs} +17 -17
- package/templates/add/auth/internal/app/user/{handler_recovery.go.hbs → adapters/inbound/http/handler_recovery.go.hbs} +4 -4
- package/templates/add/auth/internal/app/user/{handler_test.go.hbs → adapters/inbound/http/handler_test.go.hbs} +52 -31
- package/templates/add/auth/internal/app/user/{handler_user.go.hbs → adapters/inbound/http/handler_user.go.hbs} +4 -4
- package/templates/add/auth/internal/app/user/{session_cookie.go.hbs → adapters/inbound/http/session_cookie.go.hbs} +5 -3
- 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/{mfa_store.go.hbs → adapters/outbound/postgres/mfa_store.go.hbs} +22 -20
- package/templates/add/auth/internal/app/user/{mfa_store_test.go.hbs → adapters/outbound/postgres/mfa_store_test.go.hbs} +7 -7
- 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/{tokenstore_pg.go.hbs → adapters/outbound/postgres/tokenstore_pg.go.hbs} +36 -32
- package/templates/add/auth/internal/app/user/{tokenstore_pg_test.go.hbs → adapters/outbound/postgres/tokenstore_pg_test.go.hbs} +10 -3
- package/templates/add/auth/internal/app/user/{tokenstore_recovery.go.hbs → adapters/outbound/postgres/tokenstore_recovery.go.hbs} +29 -3
- package/templates/add/auth/internal/app/user/{tokenstore_redis.go.hbs → adapters/outbound/redis/tokenstore.go.hbs} +43 -36
- package/templates/add/auth/internal/app/user/{tokenstore_redis_test.go.hbs → adapters/outbound/redis/tokenstore_test.go.hbs} +14 -3
- 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/{external_login.go.hbs → application/external_login.go.hbs} +30 -40
- package/templates/add/auth/internal/app/user/{jwt.go.hbs → application/jwt.go.hbs} +6 -3
- package/templates/add/auth/internal/app/user/{local_auth.go.hbs → application/local_auth.go.hbs} +17 -19
- package/templates/add/auth/internal/app/user/{mfa_service.go.hbs → application/mfa_service.go.hbs} +27 -28
- package/templates/add/auth/internal/app/user/{mfa_service_test.go.hbs → application/mfa_service_test.go.hbs} +12 -11
- package/templates/add/auth/internal/app/user/application/oauth.go.hbs +3 -3
- package/templates/add/auth/internal/app/user/{provider_test.go.hbs → application/provider_test.go.hbs} +55 -56
- package/templates/add/auth/internal/app/user/application/recovery.go.hbs +16 -47
- package/templates/add/auth/internal/app/user/{recovery_service.go.hbs → application/recovery_service.go.hbs} +13 -15
- package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
- package/templates/add/auth/internal/app/user/{service_test.go.hbs → application/service_test.go.hbs} +94 -75
- package/templates/add/auth/internal/app/user/{sessions.go.hbs → application/sessions.go.hbs} +14 -14
- package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
- package/templates/add/auth/internal/app/user/{user_query.go.hbs → application/user_query.go.hbs} +16 -16
- package/templates/add/auth/internal/app/user/composition.go.hbs +89 -86
- 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/migrations/create_auth_tokens.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_users.up.sql.hbs +2 -1
- 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 +23 -10
- 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 +54 -36
- package/templates/create/base/.golangci.yml.hbs +2 -2
- package/templates/create/base/AGENTS.md.hbs +55 -31
- package/templates/create/base/README.md.hbs +77 -22
- package/templates/create/base/cmd/api/wiring.go.hbs +3 -2
- package/templates/create/base/internal/composition/doc.go.hbs +7 -0
- package/templates/create/features/docs/architecture.md.hbs +64 -26
- package/templates/create/features/docs/patterns.md.hbs +117 -90
- package/templates/create/features/docs/techstack.md.hbs +17 -2
- 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 -537
- package/templates/add/auth/internal/app/user/contracts.go.hbs +0 -88
- package/templates/add/auth/internal/app/user/dto.go.hbs +0 -134
- package/templates/add/auth/internal/app/user/errors.go.hbs +0 -68
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -45
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -32
- package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
- package/templates/add/auth/internal/app/user/model/mfa_challenge.go.hbs +0 -17
- package/templates/add/auth/internal/app/user/model/mfa_enrollment.go.hbs +0 -20
- package/templates/add/auth/internal/app/user/model/mfa_recovery_code.go.hbs +0 -17
- package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -31
- package/templates/add/auth/internal/app/user/repository.go.hbs +0 -139
- package/templates/add/auth/internal/app/user/service.go.hbs +0 -135
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -58
- 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/commands.go.hbs +0 -95
- package/templates/generate/module/composition.go.hbs +0 -23
- package/templates/generate/module/cqrs_test.go.hbs +0 -7
- 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 -179
- package/templates/generate/module/handler_test.go.hbs +0 -174
- package/templates/generate/module/minimal/commands.go.hbs +0 -34
- package/templates/generate/module/minimal/dto.go.hbs +0 -28
- package/templates/generate/module/minimal/handler.go.hbs +0 -82
- package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
- package/templates/generate/module/minimal/queries.go.hbs +0 -45
- package/templates/generate/module/minimal/service.go.hbs +0 -71
- 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/queries.go.hbs +0 -62
- package/templates/generate/module/repository.go.hbs +0 -103
- package/templates/generate/module/service.go.hbs +0 -164
- package/templates/generate/module/service_test.go.hbs +0 -161
package/dist/commands/auth.js
CHANGED
|
@@ -19,6 +19,7 @@ const openapi_patcher_1 = require("../utils/openapi-patcher");
|
|
|
19
19
|
const gocheck_1 = require("../utils/gocheck");
|
|
20
20
|
const gomod_patcher_1 = require("../utils/gomod-patcher");
|
|
21
21
|
const auth_wizard_1 = require("../prompts/auth-wizard");
|
|
22
|
+
const docs_patcher_1 = require("../utils/docs-patcher");
|
|
22
23
|
// URL (relative to the api prefix) -> docs file (relative to docs/) for every
|
|
23
24
|
// route `add auth` registers — kept next to AUTH_FILES's route list so the
|
|
24
25
|
// two are easy to eyeball together when a route changes.
|
|
@@ -156,7 +157,17 @@ async function addAuth(store = "postgres", projectDir = process.cwd(), browserTo
|
|
|
156
157
|
// parse-only: jwt/oauth2/bcrypt aren't in go.mod until the `go mod tidy`
|
|
157
158
|
// printed below, so `go vet` can't be the gate here.
|
|
158
159
|
(0, gocheck_1.assertStillParses)(projectDir, parsedBefore, "added auth");
|
|
159
|
-
(0,
|
|
160
|
+
const staleDocs = (0, docs_patcher_1.refreshProjectDocs)(projectDir, config, { auth: true, authStore: store });
|
|
161
|
+
if (staleDocs.length)
|
|
162
|
+
docsMessage += (0, docs_patcher_1.docsRefreshWarning)(staleDocs, "add auth");
|
|
163
|
+
(0, config_1.writeConfig)(projectDir, {
|
|
164
|
+
...config,
|
|
165
|
+
features: { ...config.features, auth: true, authStore: store },
|
|
166
|
+
modules: {
|
|
167
|
+
...config.modules,
|
|
168
|
+
user: { surface: "minimal", applicationStyle: "service", boundary: "hexagonal", packageLayout: "split" },
|
|
169
|
+
},
|
|
170
|
+
});
|
|
160
171
|
console.log(picocolors_1.default.green("\nadded internal/app/user/, internal/shared/middleware/auth.go, and cmd/seed"));
|
|
161
172
|
console.log(worker
|
|
162
173
|
? "verification + password-reset mail goes through the queue"
|
|
@@ -0,0 +1,281 @@
|
|
|
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.checkProject = checkProject;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
const config_1 = require("../utils/config");
|
|
11
|
+
const forbiddenByLayer = {
|
|
12
|
+
domain: [
|
|
13
|
+
/github\.com\/gin-gonic\/gin/,
|
|
14
|
+
/gorm\.io\//,
|
|
15
|
+
/net\/http/,
|
|
16
|
+
/internal\/(shared|platform)\//,
|
|
17
|
+
/redis/,
|
|
18
|
+
],
|
|
19
|
+
application: [
|
|
20
|
+
/github\.com\/gin-gonic\/gin/,
|
|
21
|
+
/gorm\.io\//,
|
|
22
|
+
/database\/sql/,
|
|
23
|
+
/internal\/platform\//,
|
|
24
|
+
/internal\/shared\/(apperror|httpx|middleware|tx|dberr)\//,
|
|
25
|
+
/redis/,
|
|
26
|
+
],
|
|
27
|
+
ports: [
|
|
28
|
+
/github\.com\/gin-gonic\/gin/,
|
|
29
|
+
/gorm\.io\//,
|
|
30
|
+
/database\/sql/,
|
|
31
|
+
/internal\/platform\//,
|
|
32
|
+
/internal\/shared\/(apperror|httpx|middleware|tx|dberr)\//,
|
|
33
|
+
/redis/,
|
|
34
|
+
],
|
|
35
|
+
inbound: [/gorm\.io\//, /internal\/platform\//],
|
|
36
|
+
outbound: [/github\.com\/gin-gonic\/gin/, /internal\/shared\/httpx\//, /internal\/shared\/middleware\//],
|
|
37
|
+
};
|
|
38
|
+
// A split module is not hexagonal merely because its folders have the right
|
|
39
|
+
// names. Keep dependencies flowing toward the application core. Composition
|
|
40
|
+
// is the only module-local exception because it is the explicit wiring edge.
|
|
41
|
+
const forbiddenInternalLayers = {
|
|
42
|
+
domain: ["application", "ports", "inbound", "outbound", "composition"],
|
|
43
|
+
ports: ["application", "inbound", "outbound", "composition"],
|
|
44
|
+
application: ["inbound", "outbound", "composition"],
|
|
45
|
+
inbound: ["outbound", "composition"],
|
|
46
|
+
outbound: ["application", "inbound", "composition"],
|
|
47
|
+
};
|
|
48
|
+
function goFiles(dir) {
|
|
49
|
+
if (!fs_extra_1.default.existsSync(dir))
|
|
50
|
+
return [];
|
|
51
|
+
const files = [];
|
|
52
|
+
for (const entry of fs_extra_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
53
|
+
const entryPath = path_1.default.join(dir, entry.name);
|
|
54
|
+
if (entry.isDirectory())
|
|
55
|
+
files.push(...goFiles(entryPath));
|
|
56
|
+
else if (entry.isFile() && entry.name.endsWith(".go"))
|
|
57
|
+
files.push(entryPath);
|
|
58
|
+
}
|
|
59
|
+
return files;
|
|
60
|
+
}
|
|
61
|
+
function directoriesNamed(dir, names) {
|
|
62
|
+
if (!fs_extra_1.default.existsSync(dir))
|
|
63
|
+
return [];
|
|
64
|
+
const found = [];
|
|
65
|
+
for (const entry of fs_extra_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
66
|
+
if (!entry.isDirectory())
|
|
67
|
+
continue;
|
|
68
|
+
const entryPath = path_1.default.join(dir, entry.name);
|
|
69
|
+
if (names.has(entry.name))
|
|
70
|
+
found.push(entryPath);
|
|
71
|
+
found.push(...directoriesNamed(entryPath, names));
|
|
72
|
+
}
|
|
73
|
+
return found;
|
|
74
|
+
}
|
|
75
|
+
function imports(content) {
|
|
76
|
+
const found = [];
|
|
77
|
+
for (const match of content.matchAll(/^\s*(?:import\s+)?(?:[._\w]+\s+)?"([^"]+)"\s*$/gm))
|
|
78
|
+
found.push(match[1]);
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
81
|
+
function internalLayer(imported, moduleName) {
|
|
82
|
+
const match = imported.match(/(?:^|\/)internal\/app\/([^/]+)\/(.+)$/);
|
|
83
|
+
if (!match || match[1] !== moduleName)
|
|
84
|
+
return undefined;
|
|
85
|
+
const relative = match[2];
|
|
86
|
+
if (relative === "domain" || relative.startsWith("domain/"))
|
|
87
|
+
return "domain";
|
|
88
|
+
if (relative === "application" || relative.startsWith("application/"))
|
|
89
|
+
return "application";
|
|
90
|
+
if (relative === "ports" || relative.startsWith("ports/"))
|
|
91
|
+
return "ports";
|
|
92
|
+
if (relative === "adapters/inbound" || relative.startsWith("adapters/inbound/"))
|
|
93
|
+
return "inbound";
|
|
94
|
+
if (relative === "adapters/outbound" || relative.startsWith("adapters/outbound/"))
|
|
95
|
+
return "outbound";
|
|
96
|
+
if (relative === "composition.go" || relative === "composition")
|
|
97
|
+
return "composition";
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
function siblingModule(imported, current) {
|
|
101
|
+
const match = imported.match(/(?:^|\/)internal\/app\/([^/]+)(?:\/|$)/);
|
|
102
|
+
if (!match || match[1] === current)
|
|
103
|
+
return undefined;
|
|
104
|
+
return match[1];
|
|
105
|
+
}
|
|
106
|
+
function appImport(imported) {
|
|
107
|
+
const match = imported.match(/(?:^|\/)internal\/app\/([^/]+)(?:\/(.*))?$/);
|
|
108
|
+
if (!match)
|
|
109
|
+
return undefined;
|
|
110
|
+
return { module: match[1], privatePath: match[2] ?? "" };
|
|
111
|
+
}
|
|
112
|
+
function filesForLayer(moduleDir, layer) {
|
|
113
|
+
if (layer === "composition")
|
|
114
|
+
return goFiles(moduleDir).filter((file) => path_1.default.basename(file) === "composition.go");
|
|
115
|
+
const relative = layer === "inbound"
|
|
116
|
+
? path_1.default.join("adapters", "inbound", "http")
|
|
117
|
+
: layer === "outbound"
|
|
118
|
+
? path_1.default.join("adapters", "outbound")
|
|
119
|
+
: layer;
|
|
120
|
+
return goFiles(path_1.default.join(moduleDir, relative));
|
|
121
|
+
}
|
|
122
|
+
function moduleNames(projectDir, config) {
|
|
123
|
+
const appDir = path_1.default.join(projectDir, "internal", "app");
|
|
124
|
+
const discovered = fs_extra_1.default.existsSync(appDir)
|
|
125
|
+
? fs_extra_1.default.readdirSync(appDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name)
|
|
126
|
+
: [];
|
|
127
|
+
return [...new Set([...Object.keys(config.modules), ...discovered])].sort();
|
|
128
|
+
}
|
|
129
|
+
function expectedFiles(moduleDir, module) {
|
|
130
|
+
const required = [
|
|
131
|
+
"composition.go",
|
|
132
|
+
"domain/entity.go",
|
|
133
|
+
"domain/errors.go",
|
|
134
|
+
"ports/repository.go",
|
|
135
|
+
"application/dto.go",
|
|
136
|
+
"adapters/inbound/http/dto.go",
|
|
137
|
+
"adapters/inbound/http/handler.go",
|
|
138
|
+
"adapters/outbound/postgres/model.go",
|
|
139
|
+
"adapters/outbound/postgres/repository.go",
|
|
140
|
+
];
|
|
141
|
+
if (module.applicationStyle === "service")
|
|
142
|
+
required.push("application/service.go");
|
|
143
|
+
else
|
|
144
|
+
required.push("application/commands.go", "application/queries.go");
|
|
145
|
+
return required.map((file) => path_1.default.join(moduleDir, file));
|
|
146
|
+
}
|
|
147
|
+
function checkModule(projectDir, config, name, module) {
|
|
148
|
+
const errors = [];
|
|
149
|
+
const moduleDir = path_1.default.join(projectDir, "internal", "app", name);
|
|
150
|
+
if (!module) {
|
|
151
|
+
errors.push(`internal/app/${name}: missing module metadata in go-scaffold.config.json`);
|
|
152
|
+
return errors;
|
|
153
|
+
}
|
|
154
|
+
if (module.boundary !== "hexagonal" || module.packageLayout !== "split") {
|
|
155
|
+
errors.push(`internal/app/${name}: module must declare boundary=hexagonal and packageLayout=split`);
|
|
156
|
+
}
|
|
157
|
+
for (const forbidden of directoriesNamed(moduleDir, new Set(["model"]))) {
|
|
158
|
+
errors.push(`${path_1.default.relative(projectDir, forbidden)}: feature-level model package is forbidden; keep persistence models in the outbound adapter`);
|
|
159
|
+
}
|
|
160
|
+
for (const forbidden of directoriesNamed(moduleDir, new Set(["compat"]))) {
|
|
161
|
+
errors.push(`${path_1.default.relative(projectDir, forbidden)}: feature-level compat directory is forbidden; keep legacy compatibility out of the canonical split architecture`);
|
|
162
|
+
}
|
|
163
|
+
for (const file of expectedFiles(moduleDir, module)) {
|
|
164
|
+
if (!fs_extra_1.default.existsSync(file))
|
|
165
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: required by the split module contract`);
|
|
166
|
+
}
|
|
167
|
+
const applicationDir = path_1.default.join(moduleDir, "application");
|
|
168
|
+
if (module.applicationStyle === "cqrs" && fs_extra_1.default.existsSync(path_1.default.join(applicationDir, "service.go"))) {
|
|
169
|
+
errors.push(`internal/app/${name}: CQRS module must not contain application/service.go; use commands.go and queries.go`);
|
|
170
|
+
}
|
|
171
|
+
if (module.applicationStyle === "cqrs" && fs_extra_1.default.existsSync(path_1.default.join(applicationDir, "service_test.go"))) {
|
|
172
|
+
errors.push(`internal/app/${name}: CQRS module must not contain application/service_test.go; use cqrs_test.go`);
|
|
173
|
+
}
|
|
174
|
+
if (module.applicationStyle === "service" && (fs_extra_1.default.existsSync(path_1.default.join(applicationDir, "commands.go")) || fs_extra_1.default.existsSync(path_1.default.join(applicationDir, "queries.go")))) {
|
|
175
|
+
errors.push(`internal/app/${name}: service module must not contain application/commands.go or queries.go`);
|
|
176
|
+
}
|
|
177
|
+
if (module.applicationStyle === "service" && fs_extra_1.default.existsSync(path_1.default.join(applicationDir, "cqrs_test.go"))) {
|
|
178
|
+
errors.push(`internal/app/${name}: service module must not contain application/cqrs_test.go; use service_test.go`);
|
|
179
|
+
}
|
|
180
|
+
for (const [layer, patterns] of Object.entries(forbiddenByLayer)) {
|
|
181
|
+
for (const file of filesForLayer(moduleDir, layer)) {
|
|
182
|
+
const content = fs_extra_1.default.readFileSync(file, "utf8");
|
|
183
|
+
for (const imported of imports(content)) {
|
|
184
|
+
const rule = patterns.find((pattern) => pattern.test(imported));
|
|
185
|
+
if (rule)
|
|
186
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: ${layer} layer imports forbidden dependency ${imported}`);
|
|
187
|
+
const importedLayer = internalLayer(imported, name);
|
|
188
|
+
if (importedLayer && forbiddenInternalLayers[layer].includes(importedLayer)) {
|
|
189
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: ${layer} layer imports forbidden internal layer ${imported}`);
|
|
190
|
+
}
|
|
191
|
+
const other = siblingModule(imported, name);
|
|
192
|
+
if (other) {
|
|
193
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: imports sibling module ${other}; use a port and process composition`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (layer === "application" && path_1.default.basename(file) === "dto.go" && content.includes("json:")) {
|
|
197
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: application DTOs must be transport-neutral; keep JSON tags in adapters/inbound/http/dto.go`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const file of filesForLayer(moduleDir, "composition")) {
|
|
202
|
+
const content = fs_extra_1.default.readFileSync(file, "utf8");
|
|
203
|
+
for (const imported of imports(content)) {
|
|
204
|
+
const other = siblingModule(imported, name);
|
|
205
|
+
if (other) {
|
|
206
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: imports sibling module ${other}; use a port and process composition`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const file of goFiles(moduleDir)) {
|
|
211
|
+
const content = fs_extra_1.default.readFileSync(file, "utf8");
|
|
212
|
+
for (const imported of imports(content)) {
|
|
213
|
+
if (/\/internal\/app\/[^/]+\/model(?:\/|$)/.test(imported)) {
|
|
214
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: imports a forbidden feature-level model package ${imported}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const rootGoFiles = fs_extra_1.default.existsSync(moduleDir)
|
|
219
|
+
? fs_extra_1.default
|
|
220
|
+
.readdirSync(moduleDir, { withFileTypes: true })
|
|
221
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".go"))
|
|
222
|
+
.map((entry) => path_1.default.join(moduleDir, entry.name))
|
|
223
|
+
: [];
|
|
224
|
+
for (const file of rootGoFiles) {
|
|
225
|
+
if (path_1.default.basename(file) !== "composition.go") {
|
|
226
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: root module package may only contain composition.go`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return errors;
|
|
230
|
+
}
|
|
231
|
+
function checkProcessComposition(projectDir) {
|
|
232
|
+
const errors = [];
|
|
233
|
+
const compositionDir = path_1.default.join(projectDir, "internal", "composition");
|
|
234
|
+
const docPath = path_1.default.join(compositionDir, "doc.go");
|
|
235
|
+
if (!fs_extra_1.default.existsSync(docPath)) {
|
|
236
|
+
errors.push("internal/composition/doc.go: required process-level composition package");
|
|
237
|
+
}
|
|
238
|
+
for (const file of goFiles(compositionDir)) {
|
|
239
|
+
const content = fs_extra_1.default.readFileSync(file, "utf8");
|
|
240
|
+
for (const imported of imports(content)) {
|
|
241
|
+
const feature = appImport(imported);
|
|
242
|
+
if (feature?.privatePath) {
|
|
243
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: process composition must import the public internal/app/${feature.module} package, not private path ${imported}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// wiring.go is allowed to see every feature package because it is the
|
|
248
|
+
// executable's root. Other cmd/api files must not become hidden
|
|
249
|
+
// cross-feature adapters; those belong here so integration tests can reuse
|
|
250
|
+
// the same mapping without importing package main.
|
|
251
|
+
const apiDir = path_1.default.join(projectDir, "cmd", "api");
|
|
252
|
+
for (const file of goFiles(apiDir)) {
|
|
253
|
+
const base = path_1.default.basename(file);
|
|
254
|
+
if (base === "main.go" || base === "wiring.go")
|
|
255
|
+
continue;
|
|
256
|
+
for (const imported of imports(fs_extra_1.default.readFileSync(file, "utf8"))) {
|
|
257
|
+
if (appImport(imported)) {
|
|
258
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: process-level feature adapters belong in internal/composition; keep cmd/api for entrypoint and wiring`);
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return errors;
|
|
264
|
+
}
|
|
265
|
+
function checkProject(projectDir = process.cwd()) {
|
|
266
|
+
const config = (0, config_1.readConfig)(projectDir);
|
|
267
|
+
const errors = [];
|
|
268
|
+
if (config.architecture.style !== "modular-monolith")
|
|
269
|
+
errors.push("architecture.style must be modular-monolith");
|
|
270
|
+
if (config.architecture.boundary !== "hexagonal")
|
|
271
|
+
errors.push("architecture.boundary must be hexagonal");
|
|
272
|
+
if (config.architecture.packageLayout !== "split")
|
|
273
|
+
errors.push("architecture.packageLayout must be split");
|
|
274
|
+
errors.push(...checkProcessComposition(projectDir));
|
|
275
|
+
for (const name of moduleNames(projectDir, config))
|
|
276
|
+
errors.push(...checkModule(projectDir, config, name, config.modules[name]));
|
|
277
|
+
if (errors.length) {
|
|
278
|
+
throw new Error(`${picocolors_1.default.red("architecture check failed")}:\n${errors.map((error) => ` - ${error}`).join("\n")}`);
|
|
279
|
+
}
|
|
280
|
+
console.log(picocolors_1.default.green(`architecture check passed: ${moduleNames(projectDir, config).length} split module(s), hexagonal boundary`));
|
|
281
|
+
}
|
package/dist/commands/create.js
CHANGED
|
@@ -14,6 +14,7 @@ const naming_1 = require("../utils/naming");
|
|
|
14
14
|
const create_wizard_1 = require("../prompts/create-wizard");
|
|
15
15
|
const version_1 = require("../utils/version");
|
|
16
16
|
const types_1 = require("../types");
|
|
17
|
+
const config_2 = require("../utils/config");
|
|
17
18
|
const observability_1 = require("./observability");
|
|
18
19
|
const module_profile_1 = require("../utils/module-profile");
|
|
19
20
|
async function createProject(rawName, opts) {
|
|
@@ -81,7 +82,7 @@ async function createProject(rawName, opts) {
|
|
|
81
82
|
await (0, template_renderer_1.applyTemplateEntries)(projectDir, create_manifest_1.CREATE_MANIFEST, context);
|
|
82
83
|
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
83
84
|
(0, config_1.writeConfig)(projectDir, {
|
|
84
|
-
schemaVersion:
|
|
85
|
+
schemaVersion: config_2.CONFIG_SCHEMA_VERSION,
|
|
85
86
|
projectName,
|
|
86
87
|
goModule,
|
|
87
88
|
apiPrefix,
|
|
@@ -59,6 +59,7 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
59
59
|
modulePath,
|
|
60
60
|
auth: opts.auth,
|
|
61
61
|
permission: opts.permission,
|
|
62
|
+
full: opts.full,
|
|
62
63
|
cqrs: opts.cqrs,
|
|
63
64
|
moduleSurface: opts.full ? "crud" : "minimal",
|
|
64
65
|
applicationStyle: opts.cqrs ? "cqrs" : "service",
|
|
@@ -148,13 +149,15 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
148
149
|
[modulePath]: {
|
|
149
150
|
surface: opts.full ? "crud" : "minimal",
|
|
150
151
|
applicationStyle: opts.cqrs ? "cqrs" : "service",
|
|
152
|
+
boundary: "hexagonal",
|
|
153
|
+
packageLayout: "split",
|
|
151
154
|
},
|
|
152
155
|
},
|
|
153
156
|
});
|
|
154
157
|
const routePath = config.apiPrefix ? `/${config.apiPrefix}/${naming.plural}` : `/${naming.plural}`;
|
|
155
158
|
console.log(picocolors_1.default.green(`\ngenerated internal/app/${modulePath}/`));
|
|
156
159
|
if (opts.cqrs) {
|
|
157
|
-
console.log("application boundary: CQRS command/query handlers in commands.go and queries.go");
|
|
160
|
+
console.log("application boundary: CQRS command/query handlers in application/commands.go and application/queries.go");
|
|
158
161
|
}
|
|
159
162
|
console.log(`recorded module defaults: ${opts.full ? "crud" : "minimal"} + ${opts.cqrs ? "cqrs" : "service"}`);
|
|
160
163
|
if (opts.full) {
|
|
@@ -184,6 +187,6 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
184
187
|
}
|
|
185
188
|
if (docsMessage)
|
|
186
189
|
console.log(docsMessage);
|
|
187
|
-
console.log(picocolors_1.default.dim(`\nnext: add real fields to
|
|
190
|
+
console.log(picocolors_1.default.dim(`\nnext: add real fields to domain/entity.go, application/dto.go, adapters/inbound/http/dto.go, and the outbound persistence model, then run \`go build ./...\` and apply the migration ` +
|
|
188
191
|
`(use the development bootstrap locally, or \`migrate -path migrations -database "$DB_DSN" up\` before production)`));
|
|
189
192
|
}
|
package/dist/commands/method.js
CHANGED
|
@@ -10,13 +10,13 @@ const picocolors_1 = __importDefault(require("picocolors"));
|
|
|
10
10
|
const config_1 = require("../utils/config");
|
|
11
11
|
const naming_1 = require("../utils/naming");
|
|
12
12
|
const module_location_1 = require("../utils/module-location");
|
|
13
|
-
const method_patcher_1 = require("../utils/method-patcher");
|
|
14
13
|
const gocheck_1 = require("../utils/gocheck");
|
|
15
14
|
const template_renderer_1 = require("../utils/template-renderer");
|
|
16
15
|
const migrations_1 = require("../utils/migrations");
|
|
17
16
|
const openapi_patcher_1 = require("../utils/openapi-patcher");
|
|
18
17
|
const generate_wizard_1 = require("../prompts/generate-wizard");
|
|
19
|
-
|
|
18
|
+
const hexagonal_method_patcher_1 = require("../utils/hexagonal-method-patcher");
|
|
19
|
+
// URL path registered in the inbound adapter, excluding the project-wide API
|
|
20
20
|
// prefix so it can be passed directly to patchOpenapiIndexRaw.
|
|
21
21
|
function methodRoutePath(naming, method, type, getMode, field) {
|
|
22
22
|
const base = `/${naming.plural}`;
|
|
@@ -62,109 +62,105 @@ function methodOpenapiDocument(naming, method, type, getMode, field) {
|
|
|
62
62
|
}
|
|
63
63
|
return [...pathParameters, ...operation, ""].join("\n");
|
|
64
64
|
}
|
|
65
|
-
async function
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
async function generateHexagonalMethod(config, naming, methodNameArg, opts, projectDir) {
|
|
66
|
+
const moduleDir = path_1.default.join(projectDir, "internal", "app", naming.pkg);
|
|
67
|
+
const moduleConfig = config.modules[naming.pkg];
|
|
68
|
+
if (!moduleConfig) {
|
|
69
|
+
throw new Error("module " + naming.pkg + " has no split-layout metadata in go-scaffold.config.json; run go-scaffold check first");
|
|
70
|
+
}
|
|
71
|
+
const cqrs = moduleConfig.applicationStyle === "cqrs";
|
|
72
|
+
const authModule = naming.pkg === "user" && fs_extra_1.default.existsSync(path_1.default.join(moduleDir, "application", "contracts.go"));
|
|
70
73
|
const paths = {
|
|
71
|
-
dtoPath: path_1.default.join(moduleDir, "dto.go"),
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
servicePath: path_1.default.join(moduleDir, "service.go"),
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
dtoPath: path_1.default.join(moduleDir, "application", "dto.go"),
|
|
75
|
+
requestDTOPath: path_1.default.join(moduleDir, "adapters", "inbound", "http", "dto.go"),
|
|
76
|
+
portsPath: path_1.default.join(moduleDir, "ports", "repository.go"),
|
|
77
|
+
repositoryAdapterPath: path_1.default.join(moduleDir, "adapters", "outbound", "postgres", "repository.go"),
|
|
78
|
+
servicePath: cqrs ? undefined : path_1.default.join(moduleDir, "application", "service.go"),
|
|
79
|
+
commandPath: cqrs ? path_1.default.join(moduleDir, "application", "commands.go") : undefined,
|
|
80
|
+
queryPath: cqrs ? path_1.default.join(moduleDir, "application", "queries.go") : undefined,
|
|
81
|
+
handlerPath: path_1.default.join(moduleDir, "adapters", "inbound", "http", "handler.go"),
|
|
82
|
+
serviceTestPath: path_1.default.join(moduleDir, "application", moduleConfig.applicationStyle === "cqrs" ? "cqrs_test.go" : "service_test.go"),
|
|
83
|
+
...(authModule
|
|
84
|
+
? {
|
|
85
|
+
repositoryModelType: "User",
|
|
86
|
+
repositoryToDomain: "toDomainUser",
|
|
87
|
+
repositoryErrorMapper: "persistenceError",
|
|
88
|
+
repositoryStubReceiver: "f *fakeRepo",
|
|
89
|
+
handlerErrorMapper: "toHTTPError",
|
|
90
|
+
}
|
|
91
|
+
: {}),
|
|
78
92
|
};
|
|
79
|
-
const
|
|
93
|
+
const required = [
|
|
80
94
|
paths.dtoPath,
|
|
81
|
-
paths.
|
|
82
|
-
paths.
|
|
95
|
+
paths.requestDTOPath,
|
|
96
|
+
paths.portsPath,
|
|
97
|
+
paths.repositoryAdapterPath,
|
|
83
98
|
paths.handlerPath,
|
|
84
99
|
paths.serviceTestPath,
|
|
100
|
+
...(cqrs ? [paths.commandPath, paths.queryPath] : [paths.servicePath]),
|
|
85
101
|
];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (cqrsPaths.length === 1) {
|
|
94
|
-
throw new Error(`module "${naming.pkg}" has an incomplete CQRS boundary at ${moduleDir} — ` +
|
|
95
|
-
`commands.go and queries.go must be present together`);
|
|
102
|
+
const missing = required.filter((file) => !fs_extra_1.default.existsSync(file));
|
|
103
|
+
if (missing.length) {
|
|
104
|
+
throw new Error("module " +
|
|
105
|
+
naming.pkg +
|
|
106
|
+
" is not a complete hexagonal split module (missing " +
|
|
107
|
+
missing.map((file) => path_1.default.relative(projectDir, file)).join(", ") +
|
|
108
|
+
"); run go-scaffold check");
|
|
96
109
|
}
|
|
97
110
|
const type = opts.type ?? (await (0, generate_wizard_1.promptMethodType)());
|
|
98
|
-
if (opts.getMode && type !== "get")
|
|
111
|
+
if (opts.getMode && type !== "get")
|
|
99
112
|
throw new Error("--get-mode can only be used with --type get");
|
|
100
|
-
|
|
101
|
-
if (opts.field && !(type === "get" && opts.getMode === "one")) {
|
|
113
|
+
if (opts.field && !(type === "get" && opts.getMode === "one"))
|
|
102
114
|
throw new Error("--field can only be used with --type get --get-mode one");
|
|
103
|
-
}
|
|
104
115
|
const getMode = type === "get" ? opts.getMode ?? (await (0, generate_wizard_1.promptGetMode)()) : undefined;
|
|
105
116
|
const field = type === "get" && getMode === "one" ? opts.field ?? (await (0, generate_wizard_1.promptLookupField)()) : undefined;
|
|
106
|
-
// field becomes a Go param name (`func (...)(ctx, <field> string)`) and a
|
|
107
|
-
// column name in the generated `WHERE <field> = ?`
|
|
108
117
|
if (field)
|
|
109
118
|
(0, naming_1.assertGoIdentifier)((0, naming_1.toCamelCase)(field), "lookup field");
|
|
110
119
|
const method = (0, naming_1.resolveMethodNaming)(methodNameArg ?? (await (0, generate_wizard_1.promptMethodName)()));
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// missing service marker used to leave two files patched and the method
|
|
114
|
-
// permanently un-retryable (assertNotDuplicate then sees it as existing).
|
|
115
|
-
if (!(0, method_patcher_1.markersPresentForPaths)(paths)) {
|
|
116
|
-
throw new Error(`internal/app/${naming.pkg} is missing the marker comments \`generate method\` patches at.\n` +
|
|
117
|
-
`handler.go and service.go must both still carry their \`// go-scaffold:*\` markers —\n` +
|
|
118
|
-
`restore them, or add this method by hand.`);
|
|
120
|
+
if (!(0, hexagonal_method_patcher_1.hexagonalMarkersPresent)(paths)) {
|
|
121
|
+
throw new Error("internal/app/" + naming.pkg + " is missing a split-layout generate-method marker; restore the go-scaffold markers or edit this endpoint by hand");
|
|
119
122
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// gets the blame on a docs-enabled project and the real cause (pick another
|
|
123
|
-
// method name) is the one thing the message doesn't say.
|
|
124
|
-
(0, method_patcher_1.assertMethodAbsent)(paths, method);
|
|
125
|
-
const docsRelativePath = config.features.openapiDocs
|
|
126
|
-
? `${naming.plural}/methods/${method.pathSegment}.yaml`
|
|
127
|
-
: undefined;
|
|
123
|
+
(0, hexagonal_method_patcher_1.assertHexagonalMethodAbsent)(paths, method);
|
|
124
|
+
const docsRelativePath = config.features.openapiDocs ? naming.plural + "/methods/" + method.pathSegment + ".yaml" : undefined;
|
|
128
125
|
if (docsRelativePath && fs_extra_1.default.existsSync(path_1.default.join(projectDir, "docs", docsRelativePath))) {
|
|
129
|
-
throw new Error(
|
|
126
|
+
throw new Error("OpenAPI method document already exists: docs/" + docsRelativePath);
|
|
130
127
|
}
|
|
131
128
|
const checkBefore = (0, gocheck_1.typeChecks)(projectDir);
|
|
132
|
-
(0,
|
|
129
|
+
(0, hexagonal_method_patcher_1.patchHexagonalMethod)(paths, naming, method, { type, getMode, field }, config.goModule);
|
|
133
130
|
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
134
131
|
(0, gocheck_1.assertNoDrift)(projectDir, checkBefore, config);
|
|
135
|
-
// A --field lookup queries a column the table doesn't have yet. GORM builds
|
|
136
|
-
// that SQL at runtime, so nothing before the first real request notices:
|
|
137
|
-
// the build passes, vet passes, the generated tests pass. Ship the column
|
|
138
|
-
// and its index with the code that needs them.
|
|
139
132
|
let fieldMigration = "";
|
|
140
133
|
if (field) {
|
|
141
134
|
const migrationsDir = path_1.default.join(projectDir, "migrations");
|
|
142
135
|
fieldMigration = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
143
136
|
const fieldColumn = (0, naming_1.toDbName)(field);
|
|
144
137
|
await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
|
|
145
|
-
{ template: "generate/module/field-column.up.sql.hbs", output: path_1.default.join("migrations",
|
|
146
|
-
{ template: "generate/module/field-column.down.sql.hbs", output: path_1.default.join("migrations",
|
|
147
|
-
], { ...naming,
|
|
138
|
+
{ template: "generate/module/field-column.up.sql.hbs", output: path_1.default.join("migrations", fieldMigration + "_add_" + naming.tableName + "_" + fieldColumn + ".up.sql") },
|
|
139
|
+
{ template: "generate/module/field-column.down.sql.hbs", output: path_1.default.join("migrations", fieldMigration + "_add_" + naming.tableName + "_" + fieldColumn + ".down.sql") },
|
|
140
|
+
], { ...naming, methodName: method.name, fieldColumn });
|
|
148
141
|
}
|
|
149
142
|
if (fieldMigration) {
|
|
150
|
-
console.log(
|
|
151
|
-
`(adds the column the lookup queries, plus an index on it — check the type before applying)`);
|
|
143
|
+
console.log("migration: migrations/" + fieldMigration + "_add_" + naming.tableName + "_" + (0, naming_1.toDbName)(field) + ".{up,down}.sql (adds the column the lookup queries, plus an index on it; check the type before applying)");
|
|
152
144
|
}
|
|
153
145
|
if (docsRelativePath) {
|
|
154
146
|
const docsPath = path_1.default.join(projectDir, "docs", docsRelativePath);
|
|
155
147
|
fs_extra_1.default.outputFileSync(docsPath, methodOpenapiDocument(naming, method, type, getMode, field));
|
|
156
148
|
(0, openapi_patcher_1.patchOpenapiIndexRaw)(path_1.default.join(projectDir, "docs", "openapi.yaml"), config.apiPrefix, [
|
|
157
|
-
{
|
|
158
|
-
urlPath: methodRoutePath(naming, method, type, getMode, field),
|
|
159
|
-
file: `./${docsRelativePath}`,
|
|
160
|
-
},
|
|
149
|
+
{ urlPath: methodRoutePath(naming, method, type, getMode, field), file: "./" + docsRelativePath },
|
|
161
150
|
]);
|
|
162
151
|
}
|
|
163
|
-
console.log(picocolors_1.default.green(
|
|
164
|
-
console.log(
|
|
165
|
-
if (docsRelativePath)
|
|
166
|
-
console.log(picocolors_1.default.green(
|
|
152
|
+
console.log(picocolors_1.default.green("\nadded \"" + method.name + "\" to internal/app/" + naming.pkg + "/"));
|
|
153
|
+
console.log("route: " + routeHint(naming, method, type, config.apiPrefix, getMode, field));
|
|
154
|
+
if (docsRelativePath)
|
|
155
|
+
console.log(picocolors_1.default.green("docs: docs/" + docsRelativePath + " (wired into docs/openapi.yaml)"));
|
|
156
|
+
const implementationFile = cqrs ? "application/" + (type === "get" ? "queries.go" : "commands.go") : "application/service.go";
|
|
157
|
+
console.log(picocolors_1.default.dim("\nnext: fill in the TODO in " + implementationFile + ", then go build ./... / go test ./..."));
|
|
158
|
+
}
|
|
159
|
+
async function generateMethod(moduleNameArg, methodNameArg, opts, projectDir = process.cwd()) {
|
|
160
|
+
const config = (0, config_1.readConfig)(projectDir);
|
|
161
|
+
const naming = (0, module_location_1.resolveProjectModuleNaming)(projectDir, moduleNameArg ?? (await (0, generate_wizard_1.promptExistingModule)((0, module_location_1.existingModulePackages)(projectDir), "add a method to")));
|
|
162
|
+
if (config.architecture.packageLayout !== "split") {
|
|
163
|
+
throw new Error("this scaffold only supports the canonical hexagonal split layout");
|
|
167
164
|
}
|
|
168
|
-
|
|
169
|
-
console.log(picocolors_1.default.dim(`\nnext: fill in the TODO in ${implementationFile}, then \`go build ./...\` / \`go test ./...\``));
|
|
165
|
+
await generateHexagonalMethod(config, naming, methodNameArg, opts, projectDir);
|
|
170
166
|
}
|
|
@@ -13,6 +13,7 @@ const observability_manifest_1 = require("../templates/observability-manifest");
|
|
|
13
13
|
const observability_patcher_1 = require("../utils/observability-patcher");
|
|
14
14
|
const gocheck_1 = require("../utils/gocheck");
|
|
15
15
|
const gomod_patcher_1 = require("../utils/gomod-patcher");
|
|
16
|
+
const docs_patcher_1 = require("../utils/docs-patcher");
|
|
16
17
|
// addObservability scaffolds Prometheus metrics (GET /metrics) and
|
|
17
18
|
// OpenTelemetry tracing for Gin + GORM, wiring both into the files `create`
|
|
18
19
|
// already wrote. Opt-in and separate from `create`: most projects don't want
|
|
@@ -52,7 +53,7 @@ async function addObservability(projectDir = process.cwd(), opts = {}) {
|
|
|
52
53
|
// docs saying `enabled` while the config it never reached still said false —
|
|
53
54
|
// a disagreement no later command could converge, since the "already added"
|
|
54
55
|
// guard then blocks a re-run.
|
|
55
|
-
const staleDocs =
|
|
56
|
+
const staleDocs = (0, docs_patcher_1.refreshProjectDocs)(projectDir, config, { observability: true });
|
|
56
57
|
(0, config_1.writeConfig)(projectDir, { ...config, features: { ...config.features, observability: true } });
|
|
57
58
|
if (opts.silent)
|
|
58
59
|
return;
|
|
@@ -66,57 +67,7 @@ async function addObservability(projectDir = process.cwd(), opts = {}) {
|
|
|
66
67
|
if (config.features.worker) {
|
|
67
68
|
console.log(picocolors_1.default.yellow("cmd/worker is not instrumented — it initialises no tracer provider, so its spans are dropped and it exposes no /metrics"));
|
|
68
69
|
}
|
|
69
|
-
if (staleDocs.length)
|
|
70
|
-
|
|
71
|
-
// match against today's template, and techstack.md embeds pinned
|
|
72
|
-
// dependency versions — so every release that bumps one makes every
|
|
73
|
-
// project scaffolded before it look edited. Which is exactly the
|
|
74
|
-
// population this function exists for.
|
|
75
|
-
console.log(picocolors_1.default.yellow(`\nnote: couldn't safely rewrite ${staleDocs.join(" and ")} — ` +
|
|
76
|
-
`${staleDocs.length > 1 ? "they don't" : "it doesn't"} match what this go-scaffold\n` +
|
|
77
|
-
` would have generated, so ${staleDocs.length > 1 ? "they were" : "it was"} left alone rather than overwriting your edits.\n` +
|
|
78
|
-
` ${staleDocs.length > 1 ? "They still describe" : "It still describes"} this project as having no metrics or tracing; update by hand.`));
|
|
79
|
-
}
|
|
70
|
+
if (staleDocs.length)
|
|
71
|
+
console.log(picocolors_1.default.yellow((0, docs_patcher_1.docsRefreshWarning)(staleDocs, "add observability")));
|
|
80
72
|
console.log(picocolors_1.default.dim("\nnext: go mod tidy, then set OTEL_EXPORTER_OTLP_ENDPOINT to export traces (empty = tracing no-ops, /metrics works either way)"));
|
|
81
73
|
}
|
|
82
|
-
// The architecture docs gate their observability sections on a `create`-time
|
|
83
|
-
// flag, so adding the feature afterwards used to leave techstack.md saying
|
|
84
|
-
// `disabled` and architecture.md missing the section entirely — while the
|
|
85
|
-
// README promises `create --observability` and this command produce the same
|
|
86
|
-
// project.
|
|
87
|
-
//
|
|
88
|
-
// Re-rendering from the templates rather than string-patching keeps the prose
|
|
89
|
-
// in one place (the .hbs), but would also silently discard a user's edits. So
|
|
90
|
-
// it first renders what the file *should* look like today, with observability
|
|
91
|
-
// still off: only a byte-for-byte match proves nobody has touched it. Returns
|
|
92
|
-
// the docs it declined to overwrite, for the caller to warn about.
|
|
93
|
-
function refreshArchitectDocs(projectDir, config) {
|
|
94
|
-
const docs = [
|
|
95
|
-
{ template: "create/features/docs/architecture.md.hbs", output: path_1.default.join("docs", "architect", "architecture.md") },
|
|
96
|
-
{ template: "create/features/docs/techstack.md.hbs", output: path_1.default.join("docs", "architect", "techstack.md") },
|
|
97
|
-
];
|
|
98
|
-
// only what the two templates actually reference — projectName, apiPrefix,
|
|
99
|
-
// architecture defaults and the feature flags
|
|
100
|
-
const base = { projectName: config.projectName, apiPrefix: config.apiPrefix, ...config.architecture, ...config.features };
|
|
101
|
-
const root = (0, template_renderer_1.getTemplatesRoot)();
|
|
102
|
-
const skipped = [];
|
|
103
|
-
for (const doc of docs) {
|
|
104
|
-
const outputPath = path_1.default.join(projectDir, doc.output);
|
|
105
|
-
if (!fs_extra_1.default.existsSync(outputPath))
|
|
106
|
-
continue;
|
|
107
|
-
const source = fs_extra_1.default.readFileSync(path_1.default.join(root, doc.template), "utf8");
|
|
108
|
-
// compare with line endings normalised — a Windows checkout with
|
|
109
|
-
// core.autocrlf=true would otherwise never match, and the feature would
|
|
110
|
-
// silently never fire there
|
|
111
|
-
const onDisk = lf(fs_extra_1.default.readFileSync(outputPath, "utf8"));
|
|
112
|
-
if (onDisk !== lf((0, template_renderer_1.renderString)(source, { ...base, observability: false }))) {
|
|
113
|
-
skipped.push(doc.output);
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
fs_extra_1.default.writeFileSync(outputPath, (0, template_renderer_1.renderString)(source, { ...base, observability: true }));
|
|
117
|
-
}
|
|
118
|
-
return skipped;
|
|
119
|
-
}
|
|
120
|
-
function lf(text) {
|
|
121
|
-
return text.replace(/\r\n/g, "\n");
|
|
122
|
-
}
|