@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/utils/config.js
CHANGED
|
@@ -15,7 +15,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
15
15
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
16
16
|
const types_1 = require("../types");
|
|
17
17
|
const CONFIG_FILE = "go-scaffold.config.json";
|
|
18
|
-
exports.CONFIG_SCHEMA_VERSION =
|
|
18
|
+
exports.CONFIG_SCHEMA_VERSION = 2;
|
|
19
19
|
function configPath(projectDir) {
|
|
20
20
|
return path_1.default.join(projectDir, CONFIG_FILE);
|
|
21
21
|
}
|
|
@@ -74,9 +74,10 @@ function readConfig(projectDir) {
|
|
|
74
74
|
return normalizeProjectConfig({ ...config, features: features }, projectDir);
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
|
-
* Validate and fill defaults for the project manifest.
|
|
78
|
-
*
|
|
79
|
-
*
|
|
77
|
+
* Validate and fill defaults for the project manifest. Projects that omit the
|
|
78
|
+
* architecture/modules keys can still be resolved safely; an explicit schema
|
|
79
|
+
* from before the split-layout contract is rejected so commands never write a
|
|
80
|
+
* mixed old/new tree.
|
|
80
81
|
*/
|
|
81
82
|
function normalizeProjectConfig(raw, projectDir) {
|
|
82
83
|
const projectName = requiredString(raw.projectName, "projectName");
|
|
@@ -86,6 +87,10 @@ function normalizeProjectConfig(raw, projectDir) {
|
|
|
86
87
|
if (!Number.isInteger(schemaVersion) || schemaVersion < 1) {
|
|
87
88
|
throw new Error(`${CONFIG_FILE} has invalid schemaVersion "${String(schemaVersion)}" — expected a positive integer`);
|
|
88
89
|
}
|
|
90
|
+
if (schemaVersion < exports.CONFIG_SCHEMA_VERSION) {
|
|
91
|
+
throw new Error(`${CONFIG_FILE} uses legacy schemaVersion ${schemaVersion}; go-scaffold ${exports.CONFIG_SCHEMA_VERSION} requires the hexagonal split layout. ` +
|
|
92
|
+
`Keep using go-scaffold 0.4.x for that project, or migrate its modules to internal/app/<module>/{domain,application,ports,adapters} before upgrading.`);
|
|
93
|
+
}
|
|
89
94
|
if (schemaVersion > exports.CONFIG_SCHEMA_VERSION) {
|
|
90
95
|
throw new Error(`${CONFIG_FILE} uses schemaVersion ${schemaVersion}, but this CLI supports up to ${exports.CONFIG_SCHEMA_VERSION} — upgrade go-scaffold first`);
|
|
91
96
|
}
|
|
@@ -119,10 +124,14 @@ function normalizeArchitecture(raw) {
|
|
|
119
124
|
...value,
|
|
120
125
|
};
|
|
121
126
|
assertOneOf(architecture.style, "architecture.style", ["modular-monolith"]);
|
|
127
|
+
assertOneOf(architecture.boundary, "architecture.boundary", ["hexagonal"]);
|
|
128
|
+
assertOneOf(architecture.packageLayout, "architecture.packageLayout", ["split"]);
|
|
122
129
|
assertOneOf(architecture.defaultModuleSurface, "architecture.defaultModuleSurface", ["minimal", "crud"]);
|
|
123
130
|
assertOneOf(architecture.defaultApplicationStyle, "architecture.defaultApplicationStyle", ["service", "cqrs"]);
|
|
124
131
|
return {
|
|
125
132
|
style: architecture.style,
|
|
133
|
+
boundary: architecture.boundary,
|
|
134
|
+
packageLayout: architecture.packageLayout,
|
|
126
135
|
defaultModuleSurface: architecture.defaultModuleSurface,
|
|
127
136
|
defaultApplicationStyle: architecture.defaultApplicationStyle,
|
|
128
137
|
};
|
|
@@ -139,11 +148,17 @@ function normalizeModules(raw) {
|
|
|
139
148
|
}
|
|
140
149
|
if (!isRecord(entry))
|
|
141
150
|
throw new Error(`${CONFIG_FILE}.modules.${name} must be a JSON object`);
|
|
151
|
+
const boundary = entry.boundary ?? types_1.DEFAULT_ARCHITECTURE_CONFIG.boundary;
|
|
152
|
+
const packageLayout = entry.packageLayout ?? types_1.DEFAULT_ARCHITECTURE_CONFIG.packageLayout;
|
|
142
153
|
assertOneOf(entry.surface, `modules.${name}.surface`, ["minimal", "crud"]);
|
|
143
154
|
assertOneOf(entry.applicationStyle, `modules.${name}.applicationStyle`, ["service", "cqrs"]);
|
|
155
|
+
assertOneOf(boundary, `modules.${name}.boundary`, ["hexagonal"]);
|
|
156
|
+
assertOneOf(packageLayout, `modules.${name}.packageLayout`, ["split"]);
|
|
144
157
|
modules[name] = {
|
|
145
158
|
surface: entry.surface,
|
|
146
159
|
applicationStyle: entry.applicationStyle,
|
|
160
|
+
boundary: boundary,
|
|
161
|
+
packageLayout: packageLayout,
|
|
147
162
|
};
|
|
148
163
|
}
|
|
149
164
|
return modules;
|
|
@@ -207,7 +222,11 @@ function detectFeatures(projectDir) {
|
|
|
207
222
|
// file it wrote — same trick as the queue adapter above. Projects from
|
|
208
223
|
// before the option existed have neither name and read as "redis",
|
|
209
224
|
// which is what they in fact are.
|
|
210
|
-
authStore: !auth
|
|
225
|
+
authStore: !auth
|
|
226
|
+
? undefined
|
|
227
|
+
: has("internal", "app", "user", "adapters", "outbound", "postgres", "tokenstore_pg.go")
|
|
228
|
+
? "postgres"
|
|
229
|
+
: "redis",
|
|
211
230
|
rbac: has("internal", "app", "role") && has("internal", "shared", "middleware", "authz.go"),
|
|
212
231
|
observability: has("internal", "platform", "telemetry"),
|
|
213
232
|
};
|
|
@@ -0,0 +1,68 @@
|
|
|
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.refreshProjectDocs = refreshProjectDocs;
|
|
7
|
+
exports.docsRefreshWarning = docsRefreshWarning;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
const naming_1 = require("./naming");
|
|
11
|
+
const template_renderer_1 = require("./template-renderer");
|
|
12
|
+
const PROJECT_DOCS = [
|
|
13
|
+
{ template: "create/base/README.md.hbs", output: "README.md" },
|
|
14
|
+
{ template: "create/features/docs/architecture.md.hbs", output: path_1.default.join("docs", "architect", "architecture.md") },
|
|
15
|
+
{ template: "create/features/docs/techstack.md.hbs", output: path_1.default.join("docs", "architect", "techstack.md") },
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Refresh generated project docs after an incremental feature is installed.
|
|
19
|
+
*
|
|
20
|
+
* A generated doc is only safe to rewrite when its bytes still match the
|
|
21
|
+
* version rendered from the project's current config. This keeps feature
|
|
22
|
+
* commands from destroying a maintainer's edits while still preventing the
|
|
23
|
+
* common stale-doc state where config says a feature is enabled but README or
|
|
24
|
+
* architect docs still describe the base skeleton.
|
|
25
|
+
*/
|
|
26
|
+
function refreshProjectDocs(projectDir, config, featureOverrides) {
|
|
27
|
+
const beforeFeatures = { ...config.features };
|
|
28
|
+
const afterFeatures = { ...beforeFeatures, ...featureOverrides };
|
|
29
|
+
const beforeContext = {
|
|
30
|
+
projectName: config.projectName,
|
|
31
|
+
dbName: (0, naming_1.toDbName)(config.projectName),
|
|
32
|
+
apiPrefix: config.apiPrefix,
|
|
33
|
+
...config.architecture,
|
|
34
|
+
...beforeFeatures,
|
|
35
|
+
};
|
|
36
|
+
const afterContext = {
|
|
37
|
+
projectName: config.projectName,
|
|
38
|
+
dbName: (0, naming_1.toDbName)(config.projectName),
|
|
39
|
+
apiPrefix: config.apiPrefix,
|
|
40
|
+
...config.architecture,
|
|
41
|
+
...afterFeatures,
|
|
42
|
+
};
|
|
43
|
+
const root = (0, template_renderer_1.getTemplatesRoot)();
|
|
44
|
+
const skipped = [];
|
|
45
|
+
for (const doc of PROJECT_DOCS) {
|
|
46
|
+
const outputPath = path_1.default.join(projectDir, doc.output);
|
|
47
|
+
if (!fs_extra_1.default.existsSync(outputPath))
|
|
48
|
+
continue;
|
|
49
|
+
const source = fs_extra_1.default.readFileSync(path_1.default.join(root, doc.template), "utf8");
|
|
50
|
+
const onDisk = normalizeLineEndings(fs_extra_1.default.readFileSync(outputPath, "utf8"));
|
|
51
|
+
const expectedBefore = normalizeLineEndings((0, template_renderer_1.renderString)(source, beforeContext));
|
|
52
|
+
if (onDisk !== expectedBefore) {
|
|
53
|
+
skipped.push(doc.output);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
fs_extra_1.default.writeFileSync(outputPath, (0, template_renderer_1.renderString)(source, afterContext));
|
|
57
|
+
}
|
|
58
|
+
return skipped;
|
|
59
|
+
}
|
|
60
|
+
function docsRefreshWarning(skipped, feature) {
|
|
61
|
+
if (skipped.length === 0)
|
|
62
|
+
return "";
|
|
63
|
+
const files = skipped.join(", ");
|
|
64
|
+
return `\nwarning: ${files} did not match the generated version, so ${feature} left them untouched — update them by hand`;
|
|
65
|
+
}
|
|
66
|
+
function normalizeLineEndings(text) {
|
|
67
|
+
return text.replace(/\r\n/g, "\n");
|
|
68
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -22,7 +22,7 @@ const UNUSED_API_LINE = "_ = api // dropped once `generate module` registers the
|
|
|
22
22
|
// API binary supplies infrastructure and security dependencies, then only
|
|
23
23
|
// registers the feature's handler.
|
|
24
24
|
function mainGoLines(patch) {
|
|
25
|
-
const modelAlias = `${patch.pkg}
|
|
25
|
+
const modelAlias = `${patch.pkg}postgres`; // every domain's persistence adapter is named "postgres"
|
|
26
26
|
const handlerArgs = ["db"];
|
|
27
27
|
const legacyHandlerArgs = [`${patch.pkg}Svc`];
|
|
28
28
|
if (patch.auth)
|
|
@@ -38,7 +38,7 @@ function mainGoLines(patch) {
|
|
|
38
38
|
legacyHandlerArgs.push("roleComposition.Authz");
|
|
39
39
|
return {
|
|
40
40
|
importLine: `"${patch.goModule}/internal/app/${patch.modulePath}"`,
|
|
41
|
-
modelImportLine: `${modelAlias} "${patch.goModule}/internal/app/${patch.modulePath}/
|
|
41
|
+
modelImportLine: `${modelAlias} "${patch.goModule}/internal/app/${patch.modulePath}/adapters/outbound/postgres"`,
|
|
42
42
|
// Development schema bootstrap creates tables but not the schema they live in — see
|
|
43
43
|
// the comment on go-scaffold:schemas in main.go.hbs. One Exec per schema,
|
|
44
44
|
// guarded by its own sentinel so two modules sharing a schema name only
|
|
@@ -49,7 +49,7 @@ function mainGoLines(patch) {
|
|
|
49
49
|
`}`,
|
|
50
50
|
].join("\n"),
|
|
51
51
|
schemaSentinel: `CREATE SCHEMA IF NOT EXISTS ${patch.schemaName}`,
|
|
52
|
-
migrateLine: `&${modelAlias}.${patch.pascalName}{},`,
|
|
52
|
+
migrateLine: `&${modelAlias}.${patch.pascalName}Model{},`,
|
|
53
53
|
// kept only so undo can clean projects generated before feature-local
|
|
54
54
|
// composition was introduced.
|
|
55
55
|
legacyServicePrefix: `${patch.pkg}Svc :=`,
|
|
@@ -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
|
}
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.patchComposeForRedis = patchComposeForRedis;
|
|
7
7
|
exports.patchCiForRedis = patchCiForRedis;
|
|
8
|
+
exports.patchCiForRiver = patchCiForRiver;
|
|
8
9
|
exports.patchConfigForRedis = patchConfigForRedis;
|
|
9
10
|
exports.patchConfigForWorker = patchConfigForWorker;
|
|
10
11
|
exports.patchConfigForSMTP = patchConfigForSMTP;
|
|
@@ -100,6 +101,32 @@ function patchCiForRedis(ciPath) {
|
|
|
100
101
|
}
|
|
101
102
|
fs_extra_1.default.writeFileSync(ciPath, patched);
|
|
102
103
|
}
|
|
104
|
+
// patchCiForRiver makes the generated worker round-trip test runnable in CI.
|
|
105
|
+
// River's schema is versioned by River itself, separately from this project's
|
|
106
|
+
// migrations/, so applying only the application migrations leaves the test
|
|
107
|
+
// unable to exercise the worker.
|
|
108
|
+
function patchCiForRiver(ciPath) {
|
|
109
|
+
if (!fs_extra_1.default.existsSync(ciPath))
|
|
110
|
+
return;
|
|
111
|
+
const content = fs_extra_1.default.readFileSync(ciPath, "utf8");
|
|
112
|
+
if (content.includes("river-migrate-test") || content.includes("Apply River migrations"))
|
|
113
|
+
return;
|
|
114
|
+
const testStep = content.split("\n").find((line) => line.trimStart().startsWith("- name: Test (required PostgreSQL integration tests cannot skip)"));
|
|
115
|
+
const dsn = content.match(/^\s+DB_DSN:\s+(.+)$/m)?.[1]?.trim();
|
|
116
|
+
if (!testStep || !dsn) {
|
|
117
|
+
console.error(picocolors_1.default.yellow(`skipped adding River migrations to ${ciPath} — the generated migration DSN or PostgreSQL test step is missing.\n` +
|
|
118
|
+
`Add a River migration step before the required PostgreSQL test step by hand.`));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const step = [
|
|
122
|
+
" - name: Apply River migrations to the test database",
|
|
123
|
+
" env:",
|
|
124
|
+
` TEST_DB_DSN: ${dsn}`,
|
|
125
|
+
' run: go run github.com/riverqueue/river/cmd/river@v0.43.0 migrate-up --line main --database-url "$TEST_DB_DSN"',
|
|
126
|
+
"",
|
|
127
|
+
].join("\n");
|
|
128
|
+
fs_extra_1.default.writeFileSync(ciPath, content.replace(testStep, `${step}${testStep}`));
|
|
129
|
+
}
|
|
103
130
|
// patchConfigForRedis adds RedisURL to Config and its env() load to Load().
|
|
104
131
|
// Split out from the worker patch because Redis is no longer the worker's
|
|
105
132
|
// concern by default — `add auth` needs it for the refresh-token store even
|