@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.
- package/README.md +93 -14
- package/dist/commands/auth.js +129 -0
- package/dist/commands/create.js +3 -2
- package/dist/commands/generate.js +61 -2
- package/dist/commands/method.js +66 -15
- package/dist/commands/migration.js +34 -0
- package/dist/commands/rbac.js +103 -0
- package/dist/commands/remove.js +36 -20
- package/dist/commands/worker.js +75 -0
- package/dist/index.js +71 -7
- package/dist/prompts/create-wizard.js +6 -1
- package/dist/prompts/generate-wizard.js +8 -0
- package/dist/templates/auth-manifest.js +19 -0
- package/dist/templates/create-manifest.js +25 -0
- package/dist/templates/module-manifest.js +2 -0
- package/dist/templates/rbac-manifest.js +17 -0
- package/dist/templates/worker-manifest.js +12 -0
- package/dist/utils/auth-patcher.js +96 -0
- package/dist/utils/gocheck.js +65 -0
- package/dist/utils/main-patcher.js +8 -1
- package/dist/utils/method-patcher.js +80 -16
- package/dist/utils/migrations.js +30 -8
- package/dist/utils/module-location.js +22 -0
- package/dist/utils/naming.js +75 -12
- package/dist/utils/openapi-patcher.js +35 -1
- package/dist/utils/platform-patcher.js +59 -0
- package/dist/utils/rbac-patcher.js +277 -0
- package/dist/utils/smoke-run.js +31 -0
- package/dist/utils/version.js +24 -0
- package/package.json +15 -6
- package/scripts/smoke-test.mjs +2058 -0
- package/templates/add/auth/cmd/seed/main.go.hbs +76 -0
- package/templates/add/auth/docs/forgot-password.yaml.hbs +19 -0
- package/templates/add/auth/docs/google-callback.yaml.hbs +22 -0
- package/templates/add/auth/docs/google-login.yaml.hbs +7 -0
- package/templates/add/auth/docs/login.yaml.hbs +19 -0
- package/templates/add/auth/docs/logout.yaml.hbs +8 -0
- package/templates/add/auth/docs/refresh.yaml.hbs +15 -0
- package/templates/add/auth/docs/register.yaml.hbs +19 -0
- package/templates/add/auth/docs/reset-password.yaml.hbs +16 -0
- package/templates/add/auth/docs/schemas.yaml.hbs +58 -0
- package/templates/add/auth/docs/users-me-logout-all.yaml.hbs +9 -0
- package/templates/add/auth/docs/users-me-resend-verification.yaml.hbs +10 -0
- package/templates/add/auth/docs/users-me.yaml.hbs +12 -0
- package/templates/add/auth/docs/verify-email.yaml.hbs +16 -0
- package/templates/add/auth/internal/app/user/dto.go.hbs +77 -0
- package/templates/add/auth/internal/app/user/errors.go.hbs +36 -0
- package/templates/add/auth/internal/app/user/handler.go.hbs +235 -0
- package/templates/add/auth/internal/app/user/jwt.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +28 -0
- package/templates/add/auth/internal/app/user/model/user.go.hbs +22 -0
- package/templates/add/auth/internal/app/user/repository.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/service.go.hbs +447 -0
- package/templates/add/auth/internal/app/user/service_test.go.hbs +237 -0
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +159 -0
- package/templates/add/auth/internal/shared/middleware/auth.go.hbs +64 -0
- package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +44 -0
- package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_identities.up.sql.hbs +11 -0
- package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_users.up.sql.hbs +9 -0
- package/templates/add/rbac/docs/permissions.yaml.hbs +24 -0
- package/templates/add/rbac/docs/role-permissions.yaml.hbs +31 -0
- package/templates/add/rbac/docs/role.yaml.hbs +17 -0
- package/templates/add/rbac/docs/roles.yaml.hbs +43 -0
- package/templates/add/rbac/docs/schemas.yaml.hbs +37 -0
- package/templates/add/rbac/docs/user-set-role.yaml.hbs +23 -0
- package/templates/add/rbac/docs/user.yaml.hbs +16 -0
- package/templates/add/rbac/docs/users.yaml.hbs +23 -0
- package/templates/add/rbac/internal/app/role/dto.go.hbs +40 -0
- package/templates/add/rbac/internal/app/role/errors.go.hbs +39 -0
- package/templates/add/rbac/internal/app/role/handler.go.hbs +101 -0
- package/templates/add/rbac/internal/app/role/model/permission.go.hbs +9 -0
- package/templates/add/rbac/internal/app/role/model/role.go.hbs +18 -0
- package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +8 -0
- package/templates/add/rbac/internal/app/role/repository.go.hbs +96 -0
- package/templates/add/rbac/internal/app/role/service.go.hbs +210 -0
- package/templates/add/rbac/internal/app/role/service_test.go.hbs +119 -0
- package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +88 -0
- package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +88 -0
- package/templates/add/rbac/migrations/add_roles.down.sql.hbs +15 -0
- package/templates/add/rbac/migrations/add_roles.up.sql.hbs +35 -0
- package/templates/add/worker/cmd/worker/main.go.hbs +77 -0
- package/templates/add/worker/internal/platform/cache/redis.go.hbs +18 -0
- package/templates/add/worker/internal/platform/mail/mail.go.hbs +48 -0
- package/templates/add/worker/internal/platform/mail/task.go.hbs +52 -0
- package/templates/add/worker/internal/platform/queue/client.go.hbs +31 -0
- package/templates/add/worker/internal/platform/queue/server.go.hbs +68 -0
- package/templates/create/base/.env.example.hbs +20 -0
- package/templates/create/base/.github/workflows/ci.yml.hbs +19 -4
- package/templates/create/base/.gitignore.hbs +2 -0
- package/templates/create/base/AGENTS.md.hbs +10 -12
- package/templates/create/base/Makefile.hbs +39 -8
- package/templates/create/base/README.md.hbs +52 -12
- package/templates/create/base/cmd/api/main.go.hbs +26 -1
- package/templates/create/base/internal/platform/database/database.go.hbs +53 -0
- package/templates/create/base/internal/shared/config/config.go.hbs +43 -1
- package/templates/create/base/internal/shared/middleware/cors.go.hbs +29 -0
- package/templates/create/base/internal/shared/middleware/error.go.hbs +13 -1
- package/templates/create/base/migrations/embed.go.hbs +15 -0
- package/templates/create/features/docs/architecture.md.hbs +22 -0
- package/templates/create/features/docs/common/responses.yaml.hbs +15 -0
- package/templates/create/features/docs/observability/metrics.yaml.hbs +12 -0
- package/templates/create/features/docs/openapi.yaml.hbs +17 -5
- package/templates/create/features/docs/patterns.md.hbs +9 -6
- package/templates/create/features/docs/techstack.md.hbs +3 -0
- package/templates/create/features/observability/middleware/metrics.go.hbs +41 -0
- package/templates/create/features/observability/middleware/tracing.go.hbs +46 -0
- package/templates/create/features/observability/platform/telemetry/tracing.go.hbs +130 -0
- package/templates/generate/module/docs/item.yaml.hbs +3 -3
- package/templates/generate/module/handler.go.hbs +37 -6
- package/templates/generate/module/handler_test.go.hbs +113 -51
- package/templates/generate/module/migration.down.sql.hbs +1 -1
- package/templates/generate/module/migration.up.sql.hbs +1 -1
- package/templates/generate/module/minimal/handler.go.hbs +28 -4
- package/templates/generate/module/minimal/handler_test.go.hbs +5 -65
- package/templates/generate/module/minimal/service_test.go.hbs +39 -16
- package/templates/generate/module/model/model.go.hbs +7 -0
- package/templates/generate/module/permission.down.sql.hbs +5 -0
- package/templates/generate/module/permission.up.sql.hbs +4 -0
- package/templates/generate/module/repository_test.go.hbs +79 -0
- package/templates/generate/module/service.go.hbs +6 -4
- package/templates/generate/module/service_test.go.hbs +68 -17
- package/tests/integration/default-module.test.mjs +46 -0
- package/tests/integration/generator-naming.test.mjs +81 -0
- package/tests/integration/generator-unit-test-seams.test.mjs +91 -0
- package/tests/integration/legacy-method-compat.test.mjs +222 -0
- package/tests/integration/remove-module.test.mjs +58 -0
- package/tests/unit/naming.test.mjs +94 -0
- package/tests/unit/smoke-isolation.test.mjs +35 -0
- 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
|
-
}
|