@nakedev/go-scaffold 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -1,537 +0,0 @@
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.assertMethodAbsent = assertMethodAbsent;
7
- exports.patchMethod = patchMethod;
8
- exports.markersPresent = markersPresent;
9
- exports.markersPresentForPaths = markersPresentForPaths;
10
- const fs_extra_1 = __importDefault(require("fs-extra"));
11
- const marker_patch_1 = require("./marker-patch");
12
- const naming_1 = require("./naming");
13
- const DTO_MARKER = "// go-scaffold:dto";
14
- const AUTH_DTO_MARKER = "// go-scaffold:user-dto";
15
- const REPO_INTERFACE_MARKER = "// go-scaffold:repository-interface";
16
- const REPO_IMPL_MARKER = "// go-scaffold:repository-methods";
17
- const SERVICE_MARKER = "// go-scaffold:service-methods";
18
- const SERVICE_INTERFACE_MARKER = "// go-scaffold:service-interface";
19
- const COMMAND_REPO_INTERFACE_MARKER = "// go-scaffold:command-repository-interface";
20
- const QUERY_REPO_INTERFACE_MARKER = "// go-scaffold:query-repository-interface";
21
- const COMMAND_MARKER = "// go-scaffold:command-methods";
22
- const QUERY_MARKER = "// go-scaffold:query-methods";
23
- const COMMAND_INTERFACE_MARKER = "// go-scaffold:command-interface";
24
- const QUERY_INTERFACE_MARKER = "// go-scaffold:query-interface";
25
- const HANDLER_ROUTES_MARKER = "// go-scaffold:handler-routes";
26
- const HANDLER_FUNCS_MARKER = "// go-scaffold:handler-funcs";
27
- // Auth deliberately keeps its route/service composition files focused rather
28
- // than using the generic CRUD marker names. `generate method user ...` is
29
- // still a supported extension point, so accept both marker vocabularies.
30
- const AUTH_REPO_INTERFACE_MARKER = "// go-scaffold:user-repository-interface";
31
- const AUTH_REPO_IMPL_MARKER = "// go-scaffold:user-repository-methods";
32
- const AUTH_SERVICE_MARKER = "// go-scaffold:user-service-methods";
33
- const AUTH_HANDLER_ROUTES_MARKER = "// go-scaffold:user-routes";
34
- const AUTH_HANDLER_FUNCS_MARKER = "// go-scaffold:user-handler-funcs";
35
- const REPOSITORY_STUB_FIELDS_MARKER = "// go-scaffold:repository-stub-fields";
36
- const REPOSITORY_STUB_METHODS_MARKER = "// go-scaffold:repository-stub-methods";
37
- const LEGACY_FAKE_REPO_METHODS_MARKER = "// go-scaffold:fake-repo-methods";
38
- const AUTH_FAKE_REPO_METHODS_MARKER = "// go-scaffold:user-fake-repo-methods";
39
- const UNUSED_G_LINE = "\t_ = g\n";
40
- // writeHandler ensures whatever packages the new handler code references are
41
- // imported (a minimal module starts with only "gin" imported) and drops the
42
- // `_ = g` placeholder once a real route makes it unnecessary — a no-op on a
43
- // full module, which already imports everything and never has that line.
44
- function writeHandler(handlerPath, content, goModule, needs) {
45
- for (const pkg of needs) {
46
- const importPath = pkg.startsWith("net/") ? pkg : `${goModule}/internal/shared/${pkg}`;
47
- content = (0, marker_patch_1.ensureImport)(content, importPath);
48
- }
49
- fs_extra_1.default.writeFileSync(handlerPath, content.replace(UNUSED_G_LINE, ""));
50
- }
51
- function isCqrs(paths) {
52
- return Boolean(paths.commandPath &&
53
- paths.queryPath &&
54
- fs_extra_1.default.existsSync(paths.commandPath) &&
55
- fs_extra_1.default.existsSync(paths.queryPath));
56
- }
57
- function cqrsTarget(paths, type) {
58
- if (!isCqrs(paths))
59
- return null;
60
- if (type === "get") {
61
- return {
62
- path: paths.queryPath,
63
- implementationMarker: QUERY_MARKER,
64
- interfaceMarker: QUERY_INTERFACE_MARKER,
65
- callReceiver: "queries",
66
- };
67
- }
68
- return {
69
- path: paths.commandPath,
70
- implementationMarker: COMMAND_MARKER,
71
- interfaceMarker: COMMAND_INTERFACE_MARKER,
72
- callReceiver: "commands",
73
- };
74
- }
75
- function insertCqrsImplementation(paths, type, block, goModule) {
76
- const target = cqrsTarget(paths, type);
77
- if (!target)
78
- return;
79
- let content = fs_extra_1.default.readFileSync(target.path, "utf8");
80
- if (type !== "get") {
81
- content = (0, marker_patch_1.ensureImport)(content, `${goModule}/internal/shared/apperror`);
82
- }
83
- content = (0, marker_patch_1.insertBeforeMarker)(content, target.implementationMarker, block);
84
- fs_extra_1.default.writeFileSync(target.path, content);
85
- }
86
- function insertCqrsFacade(servicePath, block, goModule, modulePath) {
87
- let service = fs_extra_1.default.readFileSync(servicePath, "utf8");
88
- // Minimal CQRS service.go intentionally has no imports until a method is
89
- // generated. The compatibility facade still uses the normal feature input,
90
- // model, and UUID types, so make the first generated method compile too.
91
- service = (0, marker_patch_1.ensureImport)(service, "context");
92
- service = (0, marker_patch_1.ensureImport)(service, `${goModule}/internal/app/${modulePath}/model`);
93
- service = (0, marker_patch_1.ensureImport)(service, "github.com/google/uuid");
94
- service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, block);
95
- fs_extra_1.default.writeFileSync(servicePath, service);
96
- }
97
- function assertNotDuplicate(content, needle, what) {
98
- if (content.includes(needle)) {
99
- throw new Error(`${what} already exists — pick a different method name`);
100
- }
101
- }
102
- function routeCall(type) {
103
- return { get: "GET", post: "POST", put: "PUT", patch: "PATCH", delete: "DELETE" }[type];
104
- }
105
- function insertBeforeMethodMarker(content, standard, auth, block) {
106
- const marker = (0, marker_patch_1.hasMarker)(content, standard) ? standard : (0, marker_patch_1.hasMarker)(content, auth) ? auth : standard;
107
- return (0, marker_patch_1.insertBeforeMarker)(content, marker, block);
108
- }
109
- function hasMethodMarker(content, standard, auth) {
110
- return (0, marker_patch_1.hasMarker)(content, standard) || (0, marker_patch_1.hasMarker)(content, auth);
111
- }
112
- function handlerRouteReceiver(content) {
113
- return (0, marker_patch_1.hasMarker)(content, AUTH_HANDLER_ROUTES_MARKER) ? "usersGroup" : "g";
114
- }
115
- // assertMethodAbsent is the same check patchMethod runs, exported so callers
116
- // can reach it before their own pre-flight checks. A name that already exists
117
- // is the cause the caller needs to hear about, so it has to be reported ahead
118
- // of anything downstream of it (the OpenAPI document that name would collide
119
- // with, for one) rather than after.
120
- function assertMethodAbsent(paths, method) {
121
- const handlerSig = `func (h *Handler) ${method.handlerName}(`;
122
- const serviceSig = `func (s *Service) ${method.pascalName}(`;
123
- assertNotDuplicate(fs_extra_1.default.readFileSync(paths.handlerPath, "utf8"), handlerSig, `handler method "${method.handlerName}"`);
124
- assertNotDuplicate(fs_extra_1.default.readFileSync(paths.servicePath, "utf8"), serviceSig, `service method "${method.pascalName}"`);
125
- }
126
- function patchMethod(paths, naming, method, opts, goModule) {
127
- assertMethodAbsent(paths, method);
128
- if (opts.type === "get" && opts.getMode === "all") {
129
- patchGetAll(paths, naming, method, goModule);
130
- }
131
- else if (opts.type === "get") {
132
- if (!opts.field)
133
- throw new Error("--field is required for --type get --get-mode one");
134
- if (opts.field.toLowerCase() === "id") {
135
- throw new Error('--field cannot be "id" — GET /:id already exists as the default lookup');
136
- }
137
- patchGetOne(paths, naming, method, opts.field, goModule);
138
- }
139
- else if (opts.type === "post") {
140
- patchPost(paths, naming, method, goModule);
141
- }
142
- else if (opts.type === "put" || opts.type === "patch") {
143
- patchResourceAction(paths, method, opts.type, goModule, naming.pkg);
144
- }
145
- else {
146
- patchDelete(paths, method, goModule, naming.pkg);
147
- }
148
- ensureNonCqrsServiceImports(paths, goModule);
149
- patchHandlerServiceInterface(paths, naming, method, opts, goModule);
150
- }
151
- // Auth keeps service.go intentionally small, so it does not carry the
152
- // apperror import that ordinary generated CRUD services use. A later
153
- // `generate method user ...` still inserts the same error-returning method
154
- // shape; make that extension compile without reintroducing the import into
155
- // every auth-only project.
156
- function ensureNonCqrsServiceImports(paths, goModule) {
157
- if (isCqrs(paths))
158
- return;
159
- let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
160
- service = (0, marker_patch_1.ensureImport)(service, `${goModule}/internal/shared/apperror`);
161
- fs_extra_1.default.writeFileSync(paths.servicePath, service);
162
- }
163
- function patchHandlerServiceInterface(paths, naming, method, opts, goModule) {
164
- let signature;
165
- let needsModel = true;
166
- let needsUUID = false;
167
- if (opts.type === "get" && opts.getMode === "all") {
168
- signature = `${method.pascalName}(context.Context, int, int) ([]model.${naming.pascalName}, error)`;
169
- }
170
- else if (opts.type === "get") {
171
- signature = `${method.pascalName}(context.Context, string) (*model.${naming.pascalName}, error)`;
172
- }
173
- else if (opts.type === "post") {
174
- signature = `${method.pascalName}(context.Context, ${method.pascalName}Input) (*model.${naming.pascalName}, error)`;
175
- }
176
- else if (opts.type === "put" || opts.type === "patch") {
177
- signature = `${method.pascalName}(context.Context, uuid.UUID) error`;
178
- needsModel = false;
179
- needsUUID = true;
180
- }
181
- else {
182
- signature = `${method.pascalName}(context.Context, uuid.UUID) error`;
183
- needsModel = false;
184
- needsUUID = true;
185
- }
186
- let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
187
- if (!(0, marker_patch_1.hasMarker)(handler, SERVICE_INTERFACE_MARKER)) {
188
- // Projects scaffolded before the narrow service interface stored *Service
189
- // directly. The concrete type already exposes generated methods, so there
190
- // is no interface declaration to patch and no migration is required.
191
- if (handler.includes("svc *Service"))
192
- return;
193
- throw new Error(`marker "${SERVICE_INTERFACE_MARKER}" not found and Handler does not use legacy *Service wiring`);
194
- }
195
- handler = (0, marker_patch_1.insertBeforeMarker)(handler, SERVICE_INTERFACE_MARKER, signature);
196
- handler = (0, marker_patch_1.ensureImport)(handler, "context");
197
- if (needsModel) {
198
- handler = (0, marker_patch_1.ensureImport)(handler, `${goModule}/internal/app/${naming.pkg}/model`);
199
- }
200
- if (needsUUID) {
201
- handler = (0, marker_patch_1.ensureImport)(handler, "github.com/google/uuid");
202
- }
203
- fs_extra_1.default.writeFileSync(paths.handlerPath, handler);
204
- const target = cqrsTarget(paths, opts.type);
205
- if (target) {
206
- let application = fs_extra_1.default.readFileSync(target.path, "utf8");
207
- application = (0, marker_patch_1.insertBeforeMarker)(application, target.interfaceMarker, signature);
208
- fs_extra_1.default.writeFileSync(target.path, application);
209
- }
210
- }
211
- function patchGetAll(paths, naming, method, goModule) {
212
- let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
213
- const routeReceiver = handlerRouteReceiver(handler);
214
- handler = insertBeforeMethodMarker(handler, HANDLER_ROUTES_MARKER, AUTH_HANDLER_ROUTES_MARKER, `${routeReceiver}.GET("/${method.pathSegment}", h.${method.handlerName})`);
215
- handler = insertBeforeMethodMarker(handler, HANDLER_FUNCS_MARKER, AUTH_HANDLER_FUNCS_MARKER, [
216
- `func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
217
- `\tp := pagination.Parse(c)`,
218
- `\titems, err := ${isCqrs(paths) ? `h.queries.${method.pascalName}` : `h.svc.${method.pascalName}`}(c.Request.Context(), p.Limit, p.Offset)`,
219
- `\tif err != nil {`,
220
- `\t\tc.Error(err)`,
221
- `\t\treturn`,
222
- `\t}`,
223
- `\tout := make([]response, len(items))`,
224
- `\tfor i := range items {`,
225
- `\t\tout[i] = toResponse(&items[i])`,
226
- `\t}`,
227
- `\tc.JSON(http.StatusOK, p.Response(out))`,
228
- `}`,
229
- ``,
230
- ].join("\n"));
231
- writeHandler(paths.handlerPath, handler, goModule, ["net/http", "pagination"]);
232
- const queryMethod = [
233
- `func (h *QueryHandler) ${method.pascalName}(ctx context.Context, limit, offset int) ([]model.${naming.pascalName}, error) {`,
234
- `\t// TODO: add real filtering for "${method.name}" — currently reuses FindAll`,
235
- `\titems, err := h.repo.FindAll(ctx, limit, offset)`,
236
- `\tif err != nil {`,
237
- `\t\treturn nil, apperror.NewInternal(err)`,
238
- `\t}`,
239
- `\treturn items, nil`,
240
- `}`,
241
- ``,
242
- ].join("\n");
243
- const serviceFacade = [
244
- `func (s *Service) ${method.pascalName}(ctx context.Context, limit, offset int) ([]model.${naming.pascalName}, error) {`,
245
- `\treturn s.queries.${method.pascalName}(ctx, limit, offset)`,
246
- `}`,
247
- ``,
248
- ].join("\n");
249
- if (isCqrs(paths)) {
250
- insertCqrsImplementation(paths, "get", queryMethod, goModule);
251
- insertCqrsFacade(paths.servicePath, serviceFacade, goModule, naming.pkg);
252
- }
253
- else {
254
- let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
255
- service = insertBeforeMethodMarker(service, SERVICE_MARKER, AUTH_SERVICE_MARKER, queryMethod.replace("(h *QueryHandler)", "(s *Service)").replace("h.repo", "s.repo"));
256
- fs_extra_1.default.writeFileSync(paths.servicePath, service);
257
- }
258
- }
259
- function patchGetOne(paths, naming, method, rawField, goModule) {
260
- const fieldParam = (0, naming_1.toCamelCase)(rawField);
261
- const fieldPascal = (0, naming_1.toPascalCase)(rawField);
262
- const fieldColumn = (0, naming_1.toDbName)(rawField);
263
- let repo = fs_extra_1.default.readFileSync(paths.repositoryPath, "utf8");
264
- assertNotDuplicate(repo, `FindBy${fieldPascal}(`, `repository method "FindBy${fieldPascal}"`);
265
- repo = insertBeforeMethodMarker(repo, REPO_IMPL_MARKER, AUTH_REPO_IMPL_MARKER, [
266
- `func (r *Repository) FindBy${fieldPascal}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error) {`,
267
- `\tvar m model.${naming.pascalName}`,
268
- `\t// TODO: add the "${fieldColumn}" column and an index on it — this query`,
269
- `\t// compiles against any table and only fails when it runs.`,
270
- `\t//`,
271
- `\t// tx.From, like every other method here: a lookup that reached for r.db`,
272
- `\t// directly would read outside a transaction its caller had opened, and`,
273
- `\t// so miss that transaction's own uncommitted writes.`,
274
- `\tif err := tx.From(ctx, r.db).WithContext(ctx).First(&m, "${fieldColumn} = ?", ${fieldParam}).Error; err != nil {`,
275
- `\t\treturn nil, err`,
276
- `\t}`,
277
- `\treturn &m, nil`,
278
- `}`,
279
- ``,
280
- ].join("\n"));
281
- fs_extra_1.default.writeFileSync(paths.repositoryPath, repo);
282
- let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
283
- if (isCqrs(paths)) {
284
- let query = fs_extra_1.default.readFileSync(paths.queryPath, "utf8");
285
- query = (0, marker_patch_1.insertBeforeMarker)(query, QUERY_REPO_INTERFACE_MARKER, `FindBy${fieldPascal}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error)`);
286
- fs_extra_1.default.writeFileSync(paths.queryPath, query);
287
- }
288
- else {
289
- service = insertBeforeMethodMarker(service, REPO_INTERFACE_MARKER, AUTH_REPO_INTERFACE_MARKER, `FindBy${fieldPascal}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error)`);
290
- }
291
- // The repository interface just grew, so the test double needs a matching
292
- // method or focused service tests stop compiling. New projects use a
293
- // function-backed stub; projects scaffolded before that refactor retain the
294
- // old fakeRepo marker and error behavior.
295
- let serviceTest = fs_extra_1.default.readFileSync(paths.serviceTestPath, "utf8");
296
- if ((0, marker_patch_1.hasMarker)(serviceTest, REPOSITORY_STUB_FIELDS_MARKER) &&
297
- (0, marker_patch_1.hasMarker)(serviceTest, REPOSITORY_STUB_METHODS_MARKER)) {
298
- serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, REPOSITORY_STUB_FIELDS_MARKER, `findBy${fieldPascal}Fn func(context.Context, string) (*model.${naming.pascalName}, error)`);
299
- serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, REPOSITORY_STUB_METHODS_MARKER, [
300
- `//nolint:unused`,
301
- `func (s *repositoryStub) FindBy${fieldPascal}(ctx context.Context, value string) (*model.${naming.pascalName}, error) {`,
302
- `\tif s.findBy${fieldPascal}Fn == nil {`,
303
- `\t\tpanic("unexpected repository.FindBy${fieldPascal} call")`,
304
- `\t}`,
305
- `\treturn s.findBy${fieldPascal}Fn(ctx, value)`,
306
- `}`,
307
- ``,
308
- ].join("\n"));
309
- }
310
- else if ((0, marker_patch_1.hasMarker)(serviceTest, AUTH_FAKE_REPO_METHODS_MARKER)) {
311
- serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, AUTH_FAKE_REPO_METHODS_MARKER, [
312
- `//nolint:unused`,
313
- `func (f *fakeRepo) FindBy${fieldPascal}(context.Context, string) (*model.${naming.pascalName}, error) {`,
314
- ` return nil, gorm.ErrRecordNotFound`,
315
- `}`,
316
- ``,
317
- ].join("\n"));
318
- }
319
- else if ((0, marker_patch_1.hasMarker)(serviceTest, LEGACY_FAKE_REPO_METHODS_MARKER)) {
320
- serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, LEGACY_FAKE_REPO_METHODS_MARKER, [
321
- `//nolint:unused`,
322
- `func (f *fakeRepo) FindBy${fieldPascal}(context.Context, string) (*model.${naming.pascalName}, error) {`,
323
- `\tif f.err != nil {`,
324
- `\t\treturn nil, f.err`,
325
- `\t}`,
326
- `\treturn f.m, nil`,
327
- `}`,
328
- ``,
329
- ].join("\n"));
330
- }
331
- else {
332
- throw new Error(`neither current repository stub markers nor legacy "${LEGACY_FAKE_REPO_METHODS_MARKER}" found in ${paths.serviceTestPath}`);
333
- }
334
- fs_extra_1.default.writeFileSync(paths.serviceTestPath, serviceTest);
335
- const queryMethod = [
336
- `func (h *QueryHandler) ${method.pascalName}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error) {`,
337
- `\tm, err := h.repo.FindBy${fieldPascal}(ctx, ${fieldParam})`,
338
- `\tif err != nil {`,
339
- `\t\treturn nil, wrapFindErr(err)`,
340
- `\t}`,
341
- `\treturn m, nil`,
342
- `}`,
343
- ``,
344
- ].join("\n");
345
- const serviceFacade = [
346
- `func (s *Service) ${method.pascalName}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error) {`,
347
- `\treturn s.queries.${method.pascalName}(ctx, ${fieldParam})`,
348
- `}`,
349
- ``,
350
- ].join("\n");
351
- if (isCqrs(paths)) {
352
- insertCqrsImplementation(paths, "get", queryMethod, goModule);
353
- insertCqrsFacade(paths.servicePath, serviceFacade, goModule, naming.pkg);
354
- }
355
- else {
356
- service = insertBeforeMethodMarker(service, SERVICE_MARKER, AUTH_SERVICE_MARKER, queryMethod.replace("(h *QueryHandler)", "(s *Service)").replace("h.repo", "s.repo"));
357
- fs_extra_1.default.writeFileSync(paths.servicePath, service);
358
- }
359
- let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
360
- const routeReceiver = handlerRouteReceiver(handler);
361
- handler = insertBeforeMethodMarker(handler, HANDLER_ROUTES_MARKER, AUTH_HANDLER_ROUTES_MARKER, `${routeReceiver}.GET("/${fieldColumn}/:${fieldParam}", h.${method.handlerName})`);
362
- handler = insertBeforeMethodMarker(handler, HANDLER_FUNCS_MARKER, AUTH_HANDLER_FUNCS_MARKER, [
363
- `func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
364
- `\t${fieldParam} := c.Param("${fieldParam}")`,
365
- `\tm, err := ${isCqrs(paths) ? `h.queries.${method.pascalName}` : `h.svc.${method.pascalName}`}(c.Request.Context(), ${fieldParam})`,
366
- `\tif err != nil {`,
367
- `\t\tc.Error(err)`,
368
- `\t\treturn`,
369
- `\t}`,
370
- `\tc.JSON(http.StatusOK, toResponse(m))`,
371
- `}`,
372
- ``,
373
- ].join("\n"));
374
- writeHandler(paths.handlerPath, handler, goModule, ["net/http"]);
375
- }
376
- function patchPost(paths, naming, method, goModule) {
377
- const inputName = `${method.pascalName}Input`;
378
- let dto = fs_extra_1.default.readFileSync(paths.dtoPath, "utf8");
379
- assertNotDuplicate(dto, `type ${inputName} struct`, `DTO "${inputName}"`);
380
- dto = insertBeforeMethodMarker(dto, DTO_MARKER, AUTH_DTO_MARKER, [`type ${inputName} struct {`, `\t// TODO: add request fields`, `}`, ``].join("\n"));
381
- fs_extra_1.default.writeFileSync(paths.dtoPath, dto);
382
- let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
383
- const routeReceiver = handlerRouteReceiver(handler);
384
- handler = insertBeforeMethodMarker(handler, HANDLER_ROUTES_MARKER, AUTH_HANDLER_ROUTES_MARKER, `${routeReceiver}.POST("/${method.pathSegment}", h.${method.handlerName})`);
385
- handler = insertBeforeMethodMarker(handler, HANDLER_FUNCS_MARKER, AUTH_HANDLER_FUNCS_MARKER, [
386
- `func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
387
- `\tvar in ${inputName}`,
388
- `\tif err := c.ShouldBindJSON(&in); err != nil {`,
389
- `\t\tc.Error(httpx.BindErr(err))`,
390
- `\t\treturn`,
391
- `\t}`,
392
- `\tm, err := ${isCqrs(paths) ? `h.commands.${method.pascalName}` : `h.svc.${method.pascalName}`}(c.Request.Context(), in)`,
393
- `\tif err != nil {`,
394
- `\t\tc.Error(err)`,
395
- `\t\treturn`,
396
- `\t}`,
397
- `\tc.JSON(http.StatusCreated, toResponse(m))`,
398
- `}`,
399
- ``,
400
- ].join("\n"));
401
- writeHandler(paths.handlerPath, handler, goModule, ["net/http", "httpx"]);
402
- const commandMethod = [
403
- `func (h *CommandHandler) ${method.pascalName}(ctx context.Context, in ${inputName}) (*model.${naming.pascalName}, error) {`,
404
- `\t// TODO: implement "${method.name}" — this stub does nothing yet`,
405
- `\t_ = in`,
406
- `\treturn nil, apperror.NewInternal()`,
407
- `}`,
408
- ``,
409
- ].join("\n");
410
- const serviceFacade = [
411
- `func (s *Service) ${method.pascalName}(ctx context.Context, in ${inputName}) (*model.${naming.pascalName}, error) {`,
412
- `\treturn s.commands.${method.pascalName}(ctx, in)`,
413
- `}`,
414
- ``,
415
- ].join("\n");
416
- if (isCqrs(paths)) {
417
- insertCqrsImplementation(paths, "post", commandMethod, goModule);
418
- insertCqrsFacade(paths.servicePath, serviceFacade, goModule, naming.pkg);
419
- }
420
- else {
421
- let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
422
- service = insertBeforeMethodMarker(service, SERVICE_MARKER, AUTH_SERVICE_MARKER, commandMethod.replace("(h *CommandHandler)", "(s *Service)").replace("h.repo", "s.repo"));
423
- fs_extra_1.default.writeFileSync(paths.servicePath, service);
424
- }
425
- }
426
- function patchResourceAction(paths, method, type, goModule, modulePath) {
427
- let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
428
- const routeReceiver = handlerRouteReceiver(handler);
429
- handler = insertBeforeMethodMarker(handler, HANDLER_ROUTES_MARKER, AUTH_HANDLER_ROUTES_MARKER, `${routeReceiver}.${routeCall(type)}("/:id/${method.pathSegment}", h.${method.handlerName})`);
430
- handler = insertBeforeMethodMarker(handler, HANDLER_FUNCS_MARKER, AUTH_HANDLER_FUNCS_MARKER, [
431
- `func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
432
- `\tid, ok := httpx.ParseID(c)`,
433
- `\tif !ok {`,
434
- `\t\treturn`,
435
- `\t}`,
436
- `\tif err := ${isCqrs(paths) ? `h.commands.${method.pascalName}` : `h.svc.${method.pascalName}`}(c.Request.Context(), id); err != nil {`,
437
- `\t\tc.Error(err)`,
438
- `\t\treturn`,
439
- `\t}`,
440
- `\tc.Status(http.StatusNotImplemented)`,
441
- `}`,
442
- ``,
443
- ].join("\n"));
444
- writeHandler(paths.handlerPath, handler, goModule, ["net/http", "httpx"]);
445
- const commandMethod = [
446
- `func (h *CommandHandler) ${method.pascalName}(ctx context.Context, id uuid.UUID) error {`,
447
- `\t_ = ctx`,
448
- `\t_ = id`,
449
- `\treturn apperror.NewNotImplemented()`,
450
- `}`,
451
- ``,
452
- ].join("\n");
453
- const serviceFacade = [
454
- `func (s *Service) ${method.pascalName}(ctx context.Context, id uuid.UUID) error {`,
455
- `\treturn s.commands.${method.pascalName}(ctx, id)`,
456
- `}`,
457
- ``,
458
- ].join("\n");
459
- if (isCqrs(paths)) {
460
- insertCqrsImplementation(paths, type, commandMethod, goModule);
461
- insertCqrsFacade(paths.servicePath, serviceFacade, goModule, modulePath);
462
- }
463
- else {
464
- let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
465
- service = insertBeforeMethodMarker(service, SERVICE_MARKER, AUTH_SERVICE_MARKER, commandMethod.replace("(h *CommandHandler)", "(s *Service)"));
466
- fs_extra_1.default.writeFileSync(paths.servicePath, service);
467
- }
468
- }
469
- function patchDelete(paths, method, goModule, modulePath) {
470
- let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
471
- const routeReceiver = handlerRouteReceiver(handler);
472
- handler = insertBeforeMethodMarker(handler, HANDLER_ROUTES_MARKER, AUTH_HANDLER_ROUTES_MARKER, `${routeReceiver}.DELETE("/:id/${method.pathSegment}", h.${method.handlerName})`);
473
- handler = insertBeforeMethodMarker(handler, HANDLER_FUNCS_MARKER, AUTH_HANDLER_FUNCS_MARKER, [
474
- `func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
475
- `\tid, ok := httpx.ParseID(c)`,
476
- `\tif !ok {`,
477
- `\t\treturn`,
478
- `\t}`,
479
- `\tif err := ${isCqrs(paths) ? `h.commands.${method.pascalName}` : `h.svc.${method.pascalName}`}(c.Request.Context(), id); err != nil {`,
480
- `\t\tc.Error(err)`,
481
- `\t\treturn`,
482
- `\t}`,
483
- `\tc.Status(http.StatusNoContent)`,
484
- `}`,
485
- ``,
486
- ].join("\n"));
487
- writeHandler(paths.handlerPath, handler, goModule, ["net/http", "httpx"]);
488
- const commandMethod = [
489
- `func (h *CommandHandler) ${method.pascalName}(ctx context.Context, id uuid.UUID) error {`,
490
- `\t// TODO: implement "${method.name}" — this stub does nothing yet`,
491
- `\treturn apperror.NewInternal()`,
492
- `}`,
493
- ``,
494
- ].join("\n");
495
- const serviceFacade = [
496
- `func (s *Service) ${method.pascalName}(ctx context.Context, id uuid.UUID) error {`,
497
- `\treturn s.commands.${method.pascalName}(ctx, id)`,
498
- `}`,
499
- ``,
500
- ].join("\n");
501
- if (isCqrs(paths)) {
502
- insertCqrsImplementation(paths, "delete", commandMethod, goModule);
503
- insertCqrsFacade(paths.servicePath, serviceFacade, goModule, modulePath);
504
- }
505
- else {
506
- let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
507
- service = insertBeforeMethodMarker(service, SERVICE_MARKER, AUTH_SERVICE_MARKER, commandMethod.replace("(h *CommandHandler)", "(s *Service)"));
508
- fs_extra_1.default.writeFileSync(paths.servicePath, service);
509
- }
510
- }
511
- function markersPresent(handlerPath, servicePath) {
512
- return markersPresentForPaths({ handlerPath, servicePath });
513
- }
514
- function markersPresentForPaths(paths) {
515
- const handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
516
- const service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
517
- const baseMarkers = hasMethodMarker(handler, HANDLER_ROUTES_MARKER, AUTH_HANDLER_ROUTES_MARKER) &&
518
- hasMethodMarker(handler, HANDLER_FUNCS_MARKER, AUTH_HANDLER_FUNCS_MARKER) &&
519
- ((0, marker_patch_1.hasMarker)(handler, SERVICE_INTERFACE_MARKER) || handler.includes("svc *Service")) &&
520
- hasMethodMarker(service, SERVICE_MARKER, AUTH_SERVICE_MARKER) &&
521
- hasMethodMarker(service, REPO_INTERFACE_MARKER, AUTH_REPO_INTERFACE_MARKER);
522
- const hasCommand = Boolean(paths.commandPath && fs_extra_1.default.existsSync(paths.commandPath));
523
- const hasQuery = Boolean(paths.queryPath && fs_extra_1.default.existsSync(paths.queryPath));
524
- if (!hasCommand && !hasQuery)
525
- return baseMarkers;
526
- if (!hasCommand || !hasQuery)
527
- return false;
528
- const commands = fs_extra_1.default.readFileSync(paths.commandPath, "utf8");
529
- const queries = fs_extra_1.default.readFileSync(paths.queryPath, "utf8");
530
- return (baseMarkers &&
531
- (0, marker_patch_1.hasMarker)(commands, COMMAND_MARKER) &&
532
- (0, marker_patch_1.hasMarker)(commands, COMMAND_INTERFACE_MARKER) &&
533
- (0, marker_patch_1.hasMarker)(commands, COMMAND_REPO_INTERFACE_MARKER) &&
534
- (0, marker_patch_1.hasMarker)(queries, QUERY_MARKER) &&
535
- (0, marker_patch_1.hasMarker)(queries, QUERY_INTERFACE_MARKER) &&
536
- (0, marker_patch_1.hasMarker)(queries, QUERY_REPO_INTERFACE_MARKER));
537
- }
@@ -1,88 +0,0 @@
1
- package user
2
-
3
- import (
4
- "context"
5
- "time"
6
-
7
- "{{goModule}}/internal/app/user/application"
8
-
9
- "github.com/google/uuid"
10
- )
11
-
12
- // Dependencies is the explicit composition contract for the auth application.
13
- // Each port represents one lifecycle or capability, so a new adapter does not
14
- // have to implement unrelated token operations just to be wired in.
15
- type Dependencies struct {
16
- Repository UserRepository
17
- RefreshTokens RefreshTokenStore
18
- OAuthTransactions OAuthTransactionStore
19
- RecoveryTokens RecoveryTokenStore
20
- MFA MFAStore
21
- Mailer AuthMailer
22
- Providers ProviderRegistry
23
- Roles RoleChecker
24
- Clock func() time.Time
25
- }
26
-
27
- // MFASettings is the operator-side MFA policy. Enabled is a capability
28
- // switch; each user's enrollment is still disabled until that user confirms a
29
- // TOTP code. Keeping both switches lets an operator turn the feature off
30
- // without deleting user enrollments.
31
- type MFASettings struct {
32
- Enabled bool
33
- Issuer string
34
- EncryptionKey string
35
- ChallengeTTL time.Duration
36
- TOTPWindow int
37
- RecoveryCodeCount int
38
- }
39
-
40
- // AuthConfig contains only auth policy. The application layer intentionally
41
- // does not depend on the generated project's shared config package.
42
- type AuthConfig struct {
43
- JWTSecret string
44
- JWTAccessTTL time.Duration
45
- JWTRefreshTTL time.Duration
46
- JWTRefreshMaxTTL time.Duration
47
- OAuthStateTTL time.Duration
48
- PasswordResetTTL time.Duration
49
- PasswordResetURL string
50
- EmailVerifyTTL time.Duration
51
- EmailVerifyURL string
52
- MFA MFASettings
53
- }
54
-
55
- // ProviderRegistry is the application-facing lookup port for external login
56
- // providers. The concrete registry lives in the application package; callers
57
- // may supply a test double or another registry without coupling Service to
58
- // that implementation.
59
- type ProviderRegistry interface {
60
- Lookup(name string) (application.LoginProvider, bool)
61
- }
62
-
63
- // MFAEnrollment is the storage-neutral representation returned by MFAStore.
64
- // The secret is always encrypted at rest; it is never serialized by an API
65
- // response.
66
- type MFAEnrollment struct {
67
- EncryptedSecret string
68
- Enabled bool
69
- }
70
-
71
- // MFAChallenge is a short-lived, one-use pre-session challenge. The raw
72
- // challenge is returned only to the client; stores receive its hash.
73
- type MFAChallenge struct {
74
- UserID uuid.UUID
75
- ExpiresAt time.Time
76
- }
77
-
78
- // MFAStore owns durable MFA state. Implementations must make challenge and
79
- // recovery-code consumption atomic so concurrent requests cannot reuse them.
80
- type MFAStore interface {
81
- GetEnrollment(ctx context.Context, userID uuid.UUID) (MFAEnrollment, bool, error)
82
- PutPendingEnrollment(ctx context.Context, userID uuid.UUID, encryptedSecret string) error
83
- ConfirmEnrollment(ctx context.Context, userID uuid.UUID, encryptedSecret string, recoveryCodeHashes []string) error
84
- Disable(ctx context.Context, userID uuid.UUID) error
85
- CreateChallenge(ctx context.Context, hash string, challenge MFAChallenge) error
86
- ConsumeChallenge(ctx context.Context, hash string) (MFAChallenge, bool, error)
87
- ConsumeRecoveryCode(ctx context.Context, userID uuid.UUID, hash string) (bool, error)
88
- }