@nakedev/go-scaffold 0.1.2 → 0.1.4

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 (131) hide show
  1. package/README.md +93 -14
  2. package/dist/commands/auth.js +129 -0
  3. package/dist/commands/create.js +3 -2
  4. package/dist/commands/generate.js +61 -2
  5. package/dist/commands/method.js +66 -15
  6. package/dist/commands/migration.js +34 -0
  7. package/dist/commands/rbac.js +103 -0
  8. package/dist/commands/remove.js +36 -20
  9. package/dist/commands/worker.js +75 -0
  10. package/dist/index.js +71 -7
  11. package/dist/prompts/create-wizard.js +6 -1
  12. package/dist/prompts/generate-wizard.js +8 -0
  13. package/dist/templates/auth-manifest.js +19 -0
  14. package/dist/templates/create-manifest.js +25 -0
  15. package/dist/templates/module-manifest.js +2 -0
  16. package/dist/templates/rbac-manifest.js +17 -0
  17. package/dist/templates/worker-manifest.js +12 -0
  18. package/dist/utils/auth-patcher.js +96 -0
  19. package/dist/utils/gocheck.js +65 -0
  20. package/dist/utils/main-patcher.js +8 -1
  21. package/dist/utils/method-patcher.js +80 -16
  22. package/dist/utils/migrations.js +30 -8
  23. package/dist/utils/module-location.js +22 -0
  24. package/dist/utils/naming.js +75 -12
  25. package/dist/utils/openapi-patcher.js +35 -1
  26. package/dist/utils/platform-patcher.js +59 -0
  27. package/dist/utils/rbac-patcher.js +277 -0
  28. package/dist/utils/smoke-run.js +31 -0
  29. package/dist/utils/version.js +24 -0
  30. package/package.json +15 -6
  31. package/scripts/smoke-test.mjs +2058 -0
  32. package/templates/add/auth/cmd/seed/main.go.hbs +76 -0
  33. package/templates/add/auth/docs/forgot-password.yaml.hbs +19 -0
  34. package/templates/add/auth/docs/google-callback.yaml.hbs +22 -0
  35. package/templates/add/auth/docs/google-login.yaml.hbs +7 -0
  36. package/templates/add/auth/docs/login.yaml.hbs +19 -0
  37. package/templates/add/auth/docs/logout.yaml.hbs +8 -0
  38. package/templates/add/auth/docs/refresh.yaml.hbs +15 -0
  39. package/templates/add/auth/docs/register.yaml.hbs +19 -0
  40. package/templates/add/auth/docs/reset-password.yaml.hbs +16 -0
  41. package/templates/add/auth/docs/schemas.yaml.hbs +58 -0
  42. package/templates/add/auth/docs/users-me-logout-all.yaml.hbs +9 -0
  43. package/templates/add/auth/docs/users-me-resend-verification.yaml.hbs +10 -0
  44. package/templates/add/auth/docs/users-me.yaml.hbs +12 -0
  45. package/templates/add/auth/docs/verify-email.yaml.hbs +16 -0
  46. package/templates/add/auth/internal/app/user/dto.go.hbs +77 -0
  47. package/templates/add/auth/internal/app/user/errors.go.hbs +36 -0
  48. package/templates/add/auth/internal/app/user/handler.go.hbs +235 -0
  49. package/templates/add/auth/internal/app/user/jwt.go.hbs +84 -0
  50. package/templates/add/auth/internal/app/user/model/identity.go.hbs +28 -0
  51. package/templates/add/auth/internal/app/user/model/user.go.hbs +22 -0
  52. package/templates/add/auth/internal/app/user/repository.go.hbs +84 -0
  53. package/templates/add/auth/internal/app/user/service.go.hbs +447 -0
  54. package/templates/add/auth/internal/app/user/service_test.go.hbs +237 -0
  55. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +159 -0
  56. package/templates/add/auth/internal/shared/middleware/auth.go.hbs +64 -0
  57. package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +44 -0
  58. package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -0
  59. package/templates/add/auth/migrations/create_identities.up.sql.hbs +11 -0
  60. package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -0
  61. package/templates/add/auth/migrations/create_users.up.sql.hbs +9 -0
  62. package/templates/add/rbac/docs/permissions.yaml.hbs +24 -0
  63. package/templates/add/rbac/docs/role-permissions.yaml.hbs +31 -0
  64. package/templates/add/rbac/docs/role.yaml.hbs +17 -0
  65. package/templates/add/rbac/docs/roles.yaml.hbs +43 -0
  66. package/templates/add/rbac/docs/schemas.yaml.hbs +37 -0
  67. package/templates/add/rbac/docs/user-set-role.yaml.hbs +23 -0
  68. package/templates/add/rbac/docs/user.yaml.hbs +16 -0
  69. package/templates/add/rbac/docs/users.yaml.hbs +23 -0
  70. package/templates/add/rbac/internal/app/role/dto.go.hbs +40 -0
  71. package/templates/add/rbac/internal/app/role/errors.go.hbs +39 -0
  72. package/templates/add/rbac/internal/app/role/handler.go.hbs +101 -0
  73. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +9 -0
  74. package/templates/add/rbac/internal/app/role/model/role.go.hbs +18 -0
  75. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +8 -0
  76. package/templates/add/rbac/internal/app/role/repository.go.hbs +96 -0
  77. package/templates/add/rbac/internal/app/role/service.go.hbs +210 -0
  78. package/templates/add/rbac/internal/app/role/service_test.go.hbs +119 -0
  79. package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +88 -0
  80. package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +88 -0
  81. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +15 -0
  82. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +35 -0
  83. package/templates/add/worker/cmd/worker/main.go.hbs +77 -0
  84. package/templates/add/worker/internal/platform/cache/redis.go.hbs +18 -0
  85. package/templates/add/worker/internal/platform/mail/mail.go.hbs +48 -0
  86. package/templates/add/worker/internal/platform/mail/task.go.hbs +52 -0
  87. package/templates/add/worker/internal/platform/queue/client.go.hbs +31 -0
  88. package/templates/add/worker/internal/platform/queue/server.go.hbs +68 -0
  89. package/templates/create/base/.env.example.hbs +20 -0
  90. package/templates/create/base/.github/workflows/ci.yml.hbs +19 -4
  91. package/templates/create/base/.gitignore.hbs +2 -0
  92. package/templates/create/base/AGENTS.md.hbs +10 -12
  93. package/templates/create/base/Makefile.hbs +39 -8
  94. package/templates/create/base/README.md.hbs +52 -12
  95. package/templates/create/base/cmd/api/main.go.hbs +26 -1
  96. package/templates/create/base/internal/platform/database/database.go.hbs +53 -0
  97. package/templates/create/base/internal/shared/config/config.go.hbs +43 -1
  98. package/templates/create/base/internal/shared/middleware/cors.go.hbs +29 -0
  99. package/templates/create/base/internal/shared/middleware/error.go.hbs +13 -1
  100. package/templates/create/base/migrations/embed.go.hbs +15 -0
  101. package/templates/create/features/docs/architecture.md.hbs +22 -0
  102. package/templates/create/features/docs/common/responses.yaml.hbs +15 -0
  103. package/templates/create/features/docs/observability/metrics.yaml.hbs +12 -0
  104. package/templates/create/features/docs/openapi.yaml.hbs +17 -5
  105. package/templates/create/features/docs/patterns.md.hbs +9 -6
  106. package/templates/create/features/docs/techstack.md.hbs +3 -0
  107. package/templates/create/features/observability/middleware/metrics.go.hbs +41 -0
  108. package/templates/create/features/observability/middleware/tracing.go.hbs +46 -0
  109. package/templates/create/features/observability/platform/telemetry/tracing.go.hbs +130 -0
  110. package/templates/generate/module/docs/item.yaml.hbs +3 -3
  111. package/templates/generate/module/handler.go.hbs +37 -6
  112. package/templates/generate/module/handler_test.go.hbs +113 -51
  113. package/templates/generate/module/migration.down.sql.hbs +1 -1
  114. package/templates/generate/module/migration.up.sql.hbs +1 -1
  115. package/templates/generate/module/minimal/handler.go.hbs +28 -4
  116. package/templates/generate/module/minimal/handler_test.go.hbs +5 -65
  117. package/templates/generate/module/minimal/service_test.go.hbs +39 -16
  118. package/templates/generate/module/model/model.go.hbs +7 -0
  119. package/templates/generate/module/permission.down.sql.hbs +5 -0
  120. package/templates/generate/module/permission.up.sql.hbs +4 -0
  121. package/templates/generate/module/repository_test.go.hbs +79 -0
  122. package/templates/generate/module/service.go.hbs +6 -4
  123. package/templates/generate/module/service_test.go.hbs +68 -17
  124. package/tests/integration/default-module.test.mjs +46 -0
  125. package/tests/integration/generator-naming.test.mjs +81 -0
  126. package/tests/integration/generator-unit-test-seams.test.mjs +91 -0
  127. package/tests/integration/legacy-method-compat.test.mjs +222 -0
  128. package/tests/integration/remove-module.test.mjs +58 -0
  129. package/tests/unit/naming.test.mjs +94 -0
  130. package/tests/unit/smoke-isolation.test.mjs +35 -0
  131. package/dist/utils/module-paths.js +0 -33
@@ -0,0 +1,222 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ renameSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import path from "node:path";
15
+ import test from "node:test";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const ROOT = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
19
+ const CLI = path.join(ROOT, "bin", "go-scaffold.js");
20
+
21
+ function run(command, args, cwd) {
22
+ return execFileSync(command, args, {
23
+ cwd,
24
+ encoding: "utf8",
25
+ stdio: ["ignore", "pipe", "pipe"],
26
+ });
27
+ }
28
+
29
+ function rewriteGoTree(dir, rewrite) {
30
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
31
+ const target = path.join(dir, entry.name);
32
+ if (entry.isDirectory()) rewriteGoTree(target, rewrite);
33
+ else if (entry.name.endsWith(".go")) {
34
+ writeFileSync(target, rewrite(readFileSync(target, "utf8")));
35
+ }
36
+ }
37
+ }
38
+
39
+ function convertToLegacyPluralModule(project) {
40
+ const canonicalDir = path.join(project, "internal", "app", "order");
41
+ const legacyDir = path.join(project, "internal", "app", "orders");
42
+ renameSync(canonicalDir, legacyDir);
43
+ rewriteGoTree(legacyDir, (source) =>
44
+ source
45
+ .replace(/^package order$/m, "package orders")
46
+ .replaceAll("sample/internal/app/order/model", "sample/internal/app/orders/model")
47
+ .replaceAll("model.Order", "model.Orders")
48
+ .replaceAll("ErrOrder", "ErrOrders")
49
+ .replace("type Order struct", "type Orders struct")
50
+ .replaceAll("func (Order)", "func (Orders)")
51
+ .replace('return "orders"', 'return "orderses"')
52
+ .replace('Group("/orders"', 'Group("/orderses"')
53
+ );
54
+
55
+ const mainPath = path.join(project, "cmd", "api", "main.go");
56
+ const legacyMain = readFileSync(mainPath, "utf8")
57
+ .replaceAll("sample/internal/app/order", "sample/internal/app/orders")
58
+ .replaceAll("ordermodel.Order", "ordersmodel.Orders")
59
+ .replaceAll("ordermodel", "ordersmodel")
60
+ .replaceAll("order.New", "orders.New")
61
+ .replaceAll("orderRepo", "ordersRepo")
62
+ .replaceAll("orderService", "ordersService")
63
+ .replaceAll("orderHandler", "ordersHandler");
64
+ writeFileSync(mainPath, legacyMain);
65
+
66
+ const docsDir = path.join(project, "docs");
67
+ const canonicalDocs = path.join(docsDir, "orders");
68
+ const legacyDocs = path.join(docsDir, "orderses");
69
+ if (existsSync(canonicalDocs)) {
70
+ renameSync(canonicalDocs, legacyDocs);
71
+ for (const name of readdirSync(legacyDocs)) {
72
+ const target = path.join(legacyDocs, name);
73
+ if (!name.endsWith(".yaml")) continue;
74
+ writeFileSync(target, readFileSync(target, "utf8").replaceAll("Order", "Orders"));
75
+ }
76
+ }
77
+ const openapiPath = path.join(docsDir, "openapi.yaml");
78
+ if (existsSync(openapiPath)) {
79
+ writeFileSync(
80
+ openapiPath,
81
+ readFileSync(openapiPath, "utf8")
82
+ .replaceAll("/v1/orders", "/v1/orderses")
83
+ .replaceAll("./orders/", "./orderses/")
84
+ .replaceAll("OrderCreate", "OrdersCreate")
85
+ .replaceAll("OrderUpdate", "OrdersUpdate")
86
+ .replaceAll("OrderResponse", "OrdersResponse")
87
+ );
88
+ }
89
+
90
+ const migrationsDir = path.join(project, "migrations");
91
+ for (const name of readdirSync(migrationsDir)) {
92
+ if (!name.includes("_create_orders.")) continue;
93
+ const source = path.join(migrationsDir, name);
94
+ const target = path.join(migrationsDir, name.replace("_create_orders.", "_create_orderses."));
95
+ writeFileSync(target, readFileSync(source, "utf8").replaceAll("orders", "orderses"));
96
+ rmSync(source);
97
+ }
98
+ return legacyDir;
99
+ }
100
+
101
+ test("generate module refuses to duplicate a legacy plural package", () => {
102
+ const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-legacy-duplicate-"));
103
+ try {
104
+ run("node", [CLI, "create", "sample", "--defaults", "--no-docker"], scratch);
105
+ const project = path.join(scratch, "sample");
106
+ mkdirSync(path.join(project, "internal", "app", "orders"), { recursive: true });
107
+ writeFileSync(path.join(project, "internal", "app", "orders", "legacy.go"), "package orders\n");
108
+
109
+ assert.throws(
110
+ () => run("node", [CLI, "generate", "module", "orders", "--full"], project),
111
+ /internal\/app\/orders.*already exists/
112
+ );
113
+ assert.equal(existsSync(path.join(project, "internal", "app", "order")), false);
114
+ } finally {
115
+ rmSync(scratch, { recursive: true, force: true });
116
+ }
117
+ });
118
+
119
+ test("remove module locates legacy plural packages and preserves their migrations", () => {
120
+ const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-legacy-remove-"));
121
+ try {
122
+ run("node", [CLI, "create", "sample", "--defaults", "--no-docker"], scratch);
123
+ const project = path.join(scratch, "sample");
124
+ run("node", [CLI, "generate", "module", "orders", "--full"], project);
125
+ const legacyDir = convertToLegacyPluralModule(project);
126
+ const migrationsDir = path.join(project, "migrations");
127
+ const migrationsBefore = readdirSync(migrationsDir).filter((name) => name.includes("_create_orderses."));
128
+
129
+ const output = run("node", [CLI, "remove", "module", "orders", "--yes"], project);
130
+
131
+ assert.equal(existsSync(legacyDir), false);
132
+ for (const name of migrationsBefore) assert.equal(existsSync(path.join(migrationsDir, name)), true);
133
+ assert.doesNotMatch(readFileSync(path.join(project, "cmd", "api", "main.go"), "utf8"), /internal\/app\/orders/);
134
+ assert.doesNotMatch(readFileSync(path.join(project, "docs", "openapi.yaml"), "utf8"), /orderses/);
135
+ assert.match(output, /preserved migration history/);
136
+ } finally {
137
+ rmSync(scratch, { recursive: true, force: true });
138
+ }
139
+ });
140
+
141
+ test("generate method locates legacy plural packages and concrete handlers", () => {
142
+ const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-legacy-method-"));
143
+ try {
144
+ run("node", [CLI, "create", "sample", "--defaults", "--no-docker"], scratch);
145
+ const project = path.join(scratch, "sample");
146
+ run("node", [CLI, "generate", "module", "orders", "--full"], project);
147
+
148
+ const legacyDir = convertToLegacyPluralModule(project);
149
+
150
+ const handlerPath = path.join(legacyDir, "handler.go");
151
+ const modernHandler = readFileSync(handlerPath, "utf8");
152
+ const legacyHandler = modernHandler
153
+ .replace(/type service interface \{[\s\S]*?\/\/ go-scaffold:service-interface\n\}\n\n/, "")
154
+ .replace('\t"context"\n', "")
155
+ .replace('\t"sample/internal/app/orders/model"\n', "")
156
+ .replace('\t"github.com/google/uuid"\n', "")
157
+ .replace("svc service", "svc *Service")
158
+ .replace("func NewHandler(svc service", "func NewHandler(svc *Service");
159
+ writeFileSync(handlerPath, legacyHandler);
160
+ writeFileSync(path.join(legacyDir, "handler_test.go"), "package orders\n");
161
+
162
+ const serviceTestPath = path.join(legacyDir, "service_test.go");
163
+ writeFileSync(
164
+ serviceTestPath,
165
+ `package orders
166
+
167
+ import (
168
+ "context"
169
+ "testing"
170
+
171
+ "sample/internal/app/orders/model"
172
+
173
+ "github.com/google/uuid"
174
+ )
175
+
176
+ type fakeRepo struct {
177
+ err error
178
+ m *model.Orders
179
+ }
180
+
181
+ func (f *fakeRepo) Create(context.Context, *model.Orders) error { return f.err }
182
+ func (f *fakeRepo) FindAll(context.Context, int, int) ([]model.Orders, error) { return nil, f.err }
183
+ func (f *fakeRepo) FindByID(context.Context, uuid.UUID) (*model.Orders, error) {
184
+ if f.err != nil {
185
+ return nil, f.err
186
+ }
187
+ return f.m, nil
188
+ }
189
+ func (f *fakeRepo) Update(context.Context, *model.Orders) error { return f.err }
190
+ func (f *fakeRepo) Delete(context.Context, uuid.UUID) error { return f.err }
191
+
192
+ // go-scaffold:fake-repo-methods
193
+
194
+ func TestGeneratedLegacyFindByStatusReturnsFixture(t *testing.T) {
195
+ want := &model.Orders{ID: uuid.New()}
196
+ svc := NewService(&fakeRepo{m: want})
197
+ got, err := svc.FindByStatus(context.Background(), "active")
198
+ if err != nil {
199
+ t.Fatalf("unexpected error: %v", err)
200
+ }
201
+ if got != want {
202
+ t.Fatalf("expected legacy fake fixture %p, got %p", want, got)
203
+ }
204
+ }
205
+ `
206
+ );
207
+
208
+ run(
209
+ "node",
210
+ [CLI, "generate", "method", "order", "findByStatus", "--type", "get", "--get-mode", "one", "--field", "status"],
211
+ project
212
+ );
213
+ assert.match(
214
+ readFileSync(serviceTestPath, "utf8"),
215
+ /\/\/nolint:unused\nfunc \(f \*fakeRepo\) FindByStatus/
216
+ );
217
+ run("go", ["mod", "tidy"], project);
218
+ run("go", ["test", "./..."], project);
219
+ } finally {
220
+ rmSync(scratch, { recursive: true, force: true });
221
+ }
222
+ });
@@ -0,0 +1,58 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import {
4
+ existsSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ rmSync,
9
+ } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import path from "node:path";
12
+ import test from "node:test";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const ROOT = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
16
+ const CLI = path.join(ROOT, "bin", "go-scaffold.js");
17
+
18
+ function runCLI(cwd, ...args) {
19
+ return execFileSync("node", [CLI, ...args], {
20
+ cwd,
21
+ encoding: "utf8",
22
+ stdio: ["ignore", "pipe", "pipe"],
23
+ });
24
+ }
25
+
26
+ function createMigrations(project, table) {
27
+ return readdirSync(path.join(project, "migrations")).filter((entry) =>
28
+ entry.includes(`_create_${table}.`)
29
+ );
30
+ }
31
+
32
+ test("remove module preserves immutable migrations and re-add reuses them", () => {
33
+ const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-remove-"));
34
+ try {
35
+ runCLI(scratch, "create", "sample", "--defaults", "--no-docker");
36
+ const project = path.join(scratch, "sample");
37
+ runCLI(project, "generate", "module", "widgets");
38
+ runCLI(project, "generate", "method", "widgets", "approve", "--type", "patch");
39
+
40
+ const before = createMigrations(project, "widgets");
41
+ assert.equal(before.length, 2);
42
+ const openapiPath = path.join(project, "docs", "openapi.yaml");
43
+ assert.match(readFileSync(openapiPath, "utf8"), /\/v1\/widgets\/\{id\}\/approve:/);
44
+
45
+ const output = runCLI(project, "remove", "module", "widgets", "--yes");
46
+
47
+ assert.equal(existsSync(path.join(project, "internal", "app", "widget")), false);
48
+ assert.deepEqual(createMigrations(project, "widgets"), before);
49
+ assert.match(output, /preserved migration history/);
50
+ assert.match(output, /generate migration drop_widgets/);
51
+ assert.doesNotMatch(readFileSync(openapiPath, "utf8"), /\.\/widgets\//);
52
+
53
+ runCLI(project, "generate", "module", "widgets");
54
+ assert.deepEqual(createMigrations(project, "widgets"), before);
55
+ } finally {
56
+ rmSync(scratch, { recursive: true, force: true });
57
+ }
58
+ });
@@ -0,0 +1,94 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import naming from "../../dist/utils/naming.js";
5
+
6
+ const { resolveExistingModuleNaming, resolveModuleNaming } = naming;
7
+
8
+ const cases = [
9
+ {
10
+ inputs: ["order", "orders"],
11
+ expected: {
12
+ name: "order",
13
+ pkg: "order",
14
+ pascalName: "Order",
15
+ plural: "orders",
16
+ tableName: "orders",
17
+ errorPrefix: "ORDER",
18
+ },
19
+ },
20
+ {
21
+ inputs: ["category", "categories"],
22
+ expected: {
23
+ name: "category",
24
+ pkg: "category",
25
+ pascalName: "Category",
26
+ plural: "categories",
27
+ tableName: "categories",
28
+ errorPrefix: "CATEGORY",
29
+ },
30
+ },
31
+ {
32
+ inputs: ["status", "statuses"],
33
+ expected: {
34
+ name: "status",
35
+ pkg: "status",
36
+ pascalName: "Status",
37
+ plural: "statuses",
38
+ tableName: "statuses",
39
+ errorPrefix: "STATUS",
40
+ },
41
+ },
42
+ {
43
+ inputs: ["order-item", "order-items"],
44
+ expected: {
45
+ name: "order-item",
46
+ pkg: "orderitem",
47
+ pascalName: "OrderItem",
48
+ plural: "order-items",
49
+ tableName: "order_items",
50
+ errorPrefix: "ORDER_ITEM",
51
+ },
52
+ },
53
+ ];
54
+
55
+ for (const { inputs, expected } of cases) {
56
+ for (const input of inputs) {
57
+ test(`resolveModuleNaming normalizes ${input}`, () => {
58
+ assert.deepEqual(resolveModuleNaming(input), expected);
59
+ });
60
+ }
61
+ }
62
+
63
+ test("resolveExistingModuleNaming finds a pre-canonical plural package", () => {
64
+ const expected = {
65
+ name: "orders",
66
+ pkg: "orders",
67
+ pascalName: "Orders",
68
+ plural: "orderses",
69
+ tableName: "orderses",
70
+ errorPrefix: "ORDERS",
71
+ };
72
+ assert.deepEqual(resolveExistingModuleNaming("orders", ["orders"]), expected);
73
+ assert.deepEqual(resolveExistingModuleNaming("order", ["orders"]), expected);
74
+ });
75
+
76
+ test("resolveExistingModuleNaming can locate a legacy package whose singular is now reserved", () => {
77
+ assert.deepEqual(resolveExistingModuleNaming("types", ["types"]), {
78
+ name: "types",
79
+ pkg: "types",
80
+ pascalName: "Types",
81
+ plural: "typeses",
82
+ tableName: "typeses",
83
+ errorPrefix: "TYPES",
84
+ });
85
+ });
86
+
87
+ test("resolveExistingModuleNaming rejects canonical and legacy packages together", () => {
88
+ for (const input of ["order", "orders"]) {
89
+ assert.throws(
90
+ () => resolveExistingModuleNaming(input, ["order", "orders"]),
91
+ /ambiguous module.*internal\/app\/order.*internal\/app\/orders/
92
+ );
93
+ }
94
+ });
@@ -0,0 +1,35 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { createSmokeRunConfig } from "../../dist/utils/smoke-run.js";
5
+
6
+ test("smoke runs receive isolated database, app port, database port, log namespace, and resource owner token", () => {
7
+ const alpha = createSmokeRunConfig("alpha-101", 41001, 54101);
8
+ const beta = createSmokeRunConfig("beta-202", 41002, 54102);
9
+
10
+ assert.match(alpha.dbName, /^go_scaffold_smoke_alpha_101$/);
11
+ assert.match(beta.dbName, /^go_scaffold_smoke_beta_202$/);
12
+ assert.notEqual(alpha.dbName, beta.dbName);
13
+ assert.equal(alpha.dbHost, "127.0.0.1");
14
+ assert.equal(alpha.dbPort, 54101);
15
+ assert.equal(beta.dbPort, 54102);
16
+ assert.notEqual(alpha.dbPort, beta.dbPort);
17
+ assert.match(alpha.dbDsn, /127\.0\.0\.1:54101\/go_scaffold_smoke_alpha_101\?sslmode=disable$/);
18
+
19
+ assert.equal(alpha.port, 41001);
20
+ assert.equal(beta.port, 41002);
21
+ assert.notEqual(alpha.baseURL, beta.baseURL);
22
+ assert.equal(alpha.baseURL, "http://127.0.0.1:41001");
23
+
24
+ assert.notEqual(alpha.logPrefix, beta.logPrefix);
25
+ assert.match(alpha.logPrefix, /go-scaffold-smoke-alpha_101$/);
26
+ assert.equal(alpha.ownerToken, "go-scaffold-smoke-alpha_101");
27
+ assert.equal(alpha.dockerLabel, "go-scaffold.smoke.owner=go-scaffold-smoke-alpha_101");
28
+ assert.equal(alpha.containerNamePrefix, "go-scaffold-smoke-alpha_101");
29
+ assert.notEqual(alpha.ownerToken, beta.ownerToken);
30
+ });
31
+
32
+ test("smoke config rejects invalid PostgreSQL ports", () => {
33
+ assert.throws(() => createSmokeRunConfig("alpha", 41001, 0), /invalid smoke-test PostgreSQL port: 0/);
34
+ assert.throws(() => createSmokeRunConfig("alpha", 41001, 65536), /invalid smoke-test PostgreSQL port: 65536/);
35
+ });
@@ -1,33 +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.listVersionFolders = listVersionFolders;
7
- exports.versionsContainingModule = versionsContainingModule;
8
- exports.nextVersionName = nextVersionName;
9
- const fs_extra_1 = __importDefault(require("fs-extra"));
10
- const path_1 = __importDefault(require("path"));
11
- // folder-based domain versioning lays modules out as internal/app/v<n>/<pkg>.
12
- // listVersionFolders returns the existing v<n> dirs, sorted ascending.
13
- function listVersionFolders(projectDir) {
14
- const appDir = path_1.default.join(projectDir, "internal", "app");
15
- if (!fs_extra_1.default.existsSync(appDir))
16
- return [];
17
- return fs_extra_1.default
18
- .readdirSync(appDir, { withFileTypes: true })
19
- .filter((e) => e.isDirectory() && /^v\d+$/.test(e.name))
20
- .map((e) => e.name)
21
- .sort((a, b) => parseInt(a.slice(1), 10) - parseInt(b.slice(1), 10));
22
- }
23
- // version folders that actually contain the given module (identified by its
24
- // handler.go) — a module can legitimately live in more than one version at
25
- // once (that's the point of versioning), so callers that need exactly one
26
- // (generate method, remove module) must pick among the matches.
27
- function versionsContainingModule(projectDir, pkg) {
28
- return listVersionFolders(projectDir).filter((v) => fs_extra_1.default.existsSync(path_1.default.join(projectDir, "internal", "app", v, pkg, "handler.go")));
29
- }
30
- function nextVersionName(versions) {
31
- const max = versions.reduce((m, v) => Math.max(m, parseInt(v.slice(1), 10) || 0), 0);
32
- return `v${max + 1}`;
33
- }