@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.
Files changed (164) hide show
  1. package/README.md +567 -513
  2. package/dist/commands/auth.js +12 -1
  3. package/dist/commands/check.js +281 -0
  4. package/dist/commands/create.js +2 -1
  5. package/dist/commands/generate.js +5 -2
  6. package/dist/commands/method.js +66 -70
  7. package/dist/commands/observability.js +4 -53
  8. package/dist/commands/rbac.js +19 -8
  9. package/dist/commands/undo.js +6 -3
  10. package/dist/commands/worker.js +14 -4
  11. package/dist/index.js +13 -1
  12. package/dist/templates/auth-manifest.js +46 -45
  13. package/dist/templates/create-manifest.js +4 -0
  14. package/dist/templates/module-manifest.js +82 -60
  15. package/dist/templates/rbac-manifest.js +15 -11
  16. package/dist/templates/worker-manifest.js +4 -1
  17. package/dist/types.js +2 -0
  18. package/dist/utils/auth-patcher.js +22 -22
  19. package/dist/utils/config.js +24 -5
  20. package/dist/utils/docs-patcher.js +68 -0
  21. package/dist/utils/hexagonal-method-patcher.js +334 -0
  22. package/dist/utils/main-patcher.js +3 -3
  23. package/dist/utils/module-location.js +17 -11
  24. package/dist/utils/platform-patcher.js +27 -0
  25. package/dist/utils/rbac-patcher.js +73 -216
  26. package/package.json +1 -1
  27. package/templates/add/auth/cmd/seed/main.go.hbs +2 -0
  28. package/templates/add/auth/internal/app/user/{browser_policy.go.hbs → adapters/inbound/http/browser_policy.go.hbs} +3 -3
  29. package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
  30. package/templates/add/auth/internal/app/user/{handler.go.hbs → adapters/inbound/http/handler.go.hbs} +103 -8
  31. package/templates/add/auth/internal/app/user/{handler_local.go.hbs → adapters/inbound/http/handler_local.go.hbs} +8 -7
  32. package/templates/add/auth/internal/app/user/{handler_mfa.go.hbs → adapters/inbound/http/handler_mfa.go.hbs} +6 -6
  33. package/templates/add/auth/internal/app/user/{handler_oauth.go.hbs → adapters/inbound/http/handler_oauth.go.hbs} +17 -17
  34. package/templates/add/auth/internal/app/user/{handler_recovery.go.hbs → adapters/inbound/http/handler_recovery.go.hbs} +4 -4
  35. package/templates/add/auth/internal/app/user/{handler_test.go.hbs → adapters/inbound/http/handler_test.go.hbs} +52 -31
  36. package/templates/add/auth/internal/app/user/{handler_user.go.hbs → adapters/inbound/http/handler_user.go.hbs} +4 -4
  37. package/templates/add/auth/internal/app/user/{session_cookie.go.hbs → adapters/inbound/http/session_cookie.go.hbs} +5 -3
  38. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt.go.hbs +35 -0
  39. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt_test.go.hbs +20 -0
  40. package/templates/add/auth/internal/app/user/{mfa_store.go.hbs → adapters/outbound/postgres/mfa_store.go.hbs} +22 -20
  41. package/templates/add/auth/internal/app/user/{mfa_store_test.go.hbs → adapters/outbound/postgres/mfa_store_test.go.hbs} +7 -7
  42. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +84 -0
  43. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +211 -0
  44. package/templates/add/auth/internal/app/user/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +18 -19
  45. package/templates/add/auth/internal/app/user/{tokenstore_pg.go.hbs → adapters/outbound/postgres/tokenstore_pg.go.hbs} +36 -32
  46. package/templates/add/auth/internal/app/user/{tokenstore_pg_test.go.hbs → adapters/outbound/postgres/tokenstore_pg_test.go.hbs} +10 -3
  47. package/templates/add/auth/internal/app/user/{tokenstore_recovery.go.hbs → adapters/outbound/postgres/tokenstore_recovery.go.hbs} +29 -3
  48. package/templates/add/auth/internal/app/user/{tokenstore_redis.go.hbs → adapters/outbound/redis/tokenstore.go.hbs} +43 -36
  49. package/templates/add/auth/internal/app/user/{tokenstore_redis_test.go.hbs → adapters/outbound/redis/tokenstore_test.go.hbs} +14 -3
  50. package/templates/add/auth/internal/app/user/application/contracts.go.hbs +52 -0
  51. package/templates/add/auth/internal/app/user/application/dto.go.hbs +75 -0
  52. package/templates/add/auth/internal/app/user/application/errors.go.hbs +62 -0
  53. package/templates/add/auth/internal/app/user/{external_login.go.hbs → application/external_login.go.hbs} +30 -40
  54. package/templates/add/auth/internal/app/user/{jwt.go.hbs → application/jwt.go.hbs} +6 -3
  55. package/templates/add/auth/internal/app/user/{local_auth.go.hbs → application/local_auth.go.hbs} +17 -19
  56. package/templates/add/auth/internal/app/user/{mfa_service.go.hbs → application/mfa_service.go.hbs} +27 -28
  57. package/templates/add/auth/internal/app/user/{mfa_service_test.go.hbs → application/mfa_service_test.go.hbs} +12 -11
  58. package/templates/add/auth/internal/app/user/application/oauth.go.hbs +3 -3
  59. package/templates/add/auth/internal/app/user/{provider_test.go.hbs → application/provider_test.go.hbs} +55 -56
  60. package/templates/add/auth/internal/app/user/application/recovery.go.hbs +16 -47
  61. package/templates/add/auth/internal/app/user/{recovery_service.go.hbs → application/recovery_service.go.hbs} +13 -15
  62. package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
  63. package/templates/add/auth/internal/app/user/{service_test.go.hbs → application/service_test.go.hbs} +94 -75
  64. package/templates/add/auth/internal/app/user/{sessions.go.hbs → application/sessions.go.hbs} +14 -14
  65. package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
  66. package/templates/add/auth/internal/app/user/{user_query.go.hbs → application/user_query.go.hbs} +16 -16
  67. package/templates/add/auth/internal/app/user/composition.go.hbs +89 -86
  68. package/templates/add/auth/internal/app/user/domain/entity.go.hbs +41 -0
  69. package/templates/add/auth/internal/app/user/domain/errors.go.hbs +32 -0
  70. package/templates/add/auth/internal/app/user/ports/password.go.hbs +9 -0
  71. package/templates/add/auth/internal/app/user/ports/repository.go.hbs +90 -0
  72. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +1 -1
  73. package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
  74. package/templates/add/auth/migrations/create_users.up.sql.hbs +2 -1
  75. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +142 -0
  76. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler_test.go.hbs +19 -0
  77. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/model.go.hbs +48 -0
  78. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +127 -0
  79. package/templates/add/rbac/internal/app/role/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  80. package/templates/add/rbac/internal/app/role/application/dto.go.hbs +47 -0
  81. package/templates/add/rbac/internal/app/role/application/errors.go.hbs +19 -0
  82. package/templates/add/rbac/internal/app/role/application/service.go.hbs +157 -0
  83. package/templates/add/rbac/internal/app/role/{service_test.go.hbs → application/service_test.go.hbs} +26 -19
  84. package/templates/add/rbac/internal/app/role/composition.go.hbs +23 -10
  85. package/templates/add/rbac/internal/app/role/domain/entity.go.hbs +23 -0
  86. package/templates/add/rbac/internal/app/role/domain/errors.go.hbs +26 -0
  87. package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +25 -0
  88. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +3 -11
  89. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -6
  90. package/templates/add/worker/internal/platform/queue/river_test.go.hbs +84 -0
  91. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +54 -36
  92. package/templates/create/base/.golangci.yml.hbs +2 -2
  93. package/templates/create/base/AGENTS.md.hbs +55 -31
  94. package/templates/create/base/README.md.hbs +77 -22
  95. package/templates/create/base/cmd/api/wiring.go.hbs +3 -2
  96. package/templates/create/base/internal/composition/doc.go.hbs +7 -0
  97. package/templates/create/features/docs/architecture.md.hbs +64 -26
  98. package/templates/create/features/docs/patterns.md.hbs +117 -90
  99. package/templates/create/features/docs/techstack.md.hbs +17 -2
  100. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.go.hbs +45 -0
  101. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.minimal.go.hbs +28 -0
  102. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +182 -0
  103. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.minimal.go.hbs +83 -0
  104. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_crud_test.go.hbs +18 -0
  105. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_test.go.hbs +30 -0
  106. package/templates/generate/module/hexagonal/adapters/outbound/postgres/model.go.hbs +37 -0
  107. package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +95 -0
  108. package/templates/generate/module/{repository_test.go.hbs → hexagonal/adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  109. package/templates/generate/module/hexagonal/application/commands.crud.go.hbs +54 -0
  110. package/templates/generate/module/hexagonal/application/commands.go.hbs +25 -0
  111. package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +66 -0
  112. package/templates/generate/module/hexagonal/application/dto.go.hbs +35 -0
  113. package/templates/generate/module/hexagonal/application/dto.minimal.go.hbs +25 -0
  114. package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +33 -0
  115. package/templates/generate/module/hexagonal/application/queries.go.hbs +25 -0
  116. package/templates/generate/module/hexagonal/application/service.crud.go.hbs +73 -0
  117. package/templates/generate/module/hexagonal/application/service.go.hbs +29 -0
  118. package/templates/generate/module/hexagonal/application/service_test.go.hbs +62 -0
  119. package/templates/generate/module/hexagonal/composition.go.hbs +27 -0
  120. package/templates/generate/module/hexagonal/domain/entity.go.hbs +20 -0
  121. package/templates/generate/module/hexagonal/domain/errors.go.hbs +11 -0
  122. package/templates/generate/module/hexagonal/ports/repository.go.hbs +38 -0
  123. package/templates/generate/module/migration.up.sql.hbs +1 -1
  124. package/dist/utils/method-patcher.js +0 -537
  125. package/templates/add/auth/internal/app/user/contracts.go.hbs +0 -88
  126. package/templates/add/auth/internal/app/user/dto.go.hbs +0 -134
  127. package/templates/add/auth/internal/app/user/errors.go.hbs +0 -68
  128. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -45
  129. package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -32
  130. package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
  131. package/templates/add/auth/internal/app/user/model/mfa_challenge.go.hbs +0 -17
  132. package/templates/add/auth/internal/app/user/model/mfa_enrollment.go.hbs +0 -20
  133. package/templates/add/auth/internal/app/user/model/mfa_recovery_code.go.hbs +0 -17
  134. package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -31
  135. package/templates/add/auth/internal/app/user/repository.go.hbs +0 -139
  136. package/templates/add/auth/internal/app/user/service.go.hbs +0 -135
  137. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -58
  138. package/templates/add/rbac/internal/app/role/dto.go.hbs +0 -45
  139. package/templates/add/rbac/internal/app/role/errors.go.hbs +0 -39
  140. package/templates/add/rbac/internal/app/role/handler.go.hbs +0 -104
  141. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +0 -12
  142. package/templates/add/rbac/internal/app/role/model/role.go.hbs +0 -22
  143. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +0 -11
  144. package/templates/add/rbac/internal/app/role/repository.go.hbs +0 -97
  145. package/templates/add/rbac/internal/app/role/service.go.hbs +0 -217
  146. package/templates/generate/module/commands.go.hbs +0 -95
  147. package/templates/generate/module/composition.go.hbs +0 -23
  148. package/templates/generate/module/cqrs_test.go.hbs +0 -7
  149. package/templates/generate/module/dto.go.hbs +0 -36
  150. package/templates/generate/module/errors.go.hbs +0 -33
  151. package/templates/generate/module/handler.go.hbs +0 -179
  152. package/templates/generate/module/handler_test.go.hbs +0 -174
  153. package/templates/generate/module/minimal/commands.go.hbs +0 -34
  154. package/templates/generate/module/minimal/dto.go.hbs +0 -28
  155. package/templates/generate/module/minimal/handler.go.hbs +0 -82
  156. package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
  157. package/templates/generate/module/minimal/queries.go.hbs +0 -45
  158. package/templates/generate/module/minimal/service.go.hbs +0 -71
  159. package/templates/generate/module/minimal/service_test.go.hbs +0 -77
  160. package/templates/generate/module/model/model.go.hbs +0 -36
  161. package/templates/generate/module/queries.go.hbs +0 -62
  162. package/templates/generate/module/repository.go.hbs +0 -103
  163. package/templates/generate/module/service.go.hbs +0 -164
  164. package/templates/generate/module/service_test.go.hbs +0 -161
@@ -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 = 1;
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. Older projects have
78
- * no architecture/modules keys yet; normalising them here lets every command
79
- * consume one stable shape while keeping the old config file compatible.
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 ? undefined : has("internal", "app", "user", "tokenstore_pg.go") ? "postgres" : "redis",
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}model`; // every domain's model subpackage is named "model"
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}/model"`,
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 model type
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/model.go is where the boundaries survive — the struct name is
33
- // PascalCase, so kebab-casing it recovers the exact string `generate module`
34
- // was originally given.
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
- const modelPath = path_1.default.join(projectDir, "internal", "app", pkg, "model", "model.go");
37
- if (!fs_extra_1.default.existsSync(modelPath))
38
- return null;
39
- const match = fs_extra_1.default.readFileSync(modelPath, "utf8").match(/^type\s+([A-Z]\w*)\s+struct\b/m);
40
- return match ? match[1] : null;
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, or a legacy module whose type never matched its
56
- // folder, would otherwise silently retarget the command at another module.
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