@nakedev/go-scaffold 0.1.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +133 -44
- package/dist/commands/auth.js +116 -11
- package/dist/commands/create.js +13 -1
- package/dist/commands/generate.js +21 -11
- package/dist/commands/method.js +32 -3
- package/dist/commands/observability.js +114 -0
- package/dist/commands/rbac.js +19 -2
- package/dist/commands/undo.js +331 -0
- package/dist/commands/worker.js +92 -32
- package/dist/index.js +366 -63
- package/dist/prompts/auth-wizard.js +29 -0
- package/dist/prompts/create-wizard.js +5 -2
- package/dist/prompts/generate-wizard.js +57 -0
- package/dist/prompts/worker-wizard.js +25 -0
- package/dist/templates/auth-manifest.js +20 -3
- package/dist/templates/create-manifest.js +13 -20
- package/dist/templates/observability-manifest.js +24 -0
- package/dist/templates/rbac-manifest.js +1 -0
- package/dist/templates/worker-manifest.js +23 -6
- package/dist/utils/auth-patcher.js +96 -21
- package/dist/utils/config.js +58 -10
- package/dist/utils/gocheck.js +57 -5
- package/dist/utils/golangci-patcher.js +73 -0
- package/dist/utils/gomod-patcher.js +53 -0
- package/dist/utils/main-patcher.js +58 -4
- package/dist/utils/marker-patch.js +125 -3
- package/dist/utils/method-patcher.js +17 -2
- package/dist/utils/module-location.js +37 -1
- package/dist/utils/naming.js +50 -2
- package/dist/utils/observability-patcher.js +107 -0
- package/dist/utils/platform-patcher.js +98 -12
- package/dist/utils/rbac-patcher.js +60 -10
- package/package.json +3 -5
- package/templates/add/auth/internal/app/user/errors.go.hbs +7 -0
- package/templates/add/auth/internal/app/user/handler.go.hbs +64 -23
- package/templates/add/auth/internal/app/user/jwt.go.hbs +31 -7
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +39 -0
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +3 -0
- package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +26 -0
- package/templates/add/auth/internal/app/user/model/user.go.hbs +9 -1
- package/templates/add/auth/internal/app/user/repository.go.hbs +64 -11
- package/templates/add/auth/internal/app/user/repository_test.go.hbs +192 -0
- package/templates/add/auth/internal/app/user/service.go.hbs +105 -21
- package/templates/add/auth/internal/app/user/service_test.go.hbs +81 -2
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +9 -138
- package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +144 -0
- package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +147 -0
- package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +21 -19
- package/templates/add/auth/internal/shared/middleware/ratelimit_memory.go.hbs +63 -0
- package/templates/add/auth/internal/shared/middleware/ratelimit_redis.go.hbs +32 -0
- package/templates/add/auth/migrations/create_auth_tokens.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +16 -0
- package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_identities.up.sql.hbs +9 -5
- package/templates/add/auth/migrations/create_login_throttle.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +10 -0
- package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_users.up.sql.hbs +13 -2
- package/templates/add/rbac/internal/app/role/dto.go.hbs +8 -3
- package/templates/add/rbac/internal/app/role/handler.go.hbs +4 -1
- package/templates/add/rbac/internal/app/role/model/permission.go.hbs +3 -0
- package/templates/add/rbac/internal/app/role/model/role.go.hbs +4 -0
- package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +3 -0
- package/templates/add/rbac/internal/app/role/repository.go.hbs +12 -11
- package/templates/add/rbac/internal/app/role/repository_test.go.hbs +176 -0
- package/templates/add/rbac/internal/app/role/service.go.hbs +7 -0
- package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +19 -0
- package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +1 -1
- package/templates/add/rbac/migrations/add_roles.down.sql.hbs +5 -5
- package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -11
- package/templates/add/worker/cmd/worker/main.go.hbs +30 -24
- package/templates/add/worker/internal/platform/mail/mail.go.hbs +21 -0
- package/templates/add/worker/internal/platform/mail/task.go.hbs +31 -34
- package/templates/add/worker/internal/platform/queue/asynq.go.hbs +140 -0
- package/templates/add/worker/internal/platform/queue/queue.go.hbs +87 -0
- package/templates/add/worker/internal/platform/queue/river.go.hbs +148 -0
- package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +71 -12
- package/templates/create/base/.dockerignore.hbs +13 -0
- package/templates/create/base/.env.example.hbs +18 -9
- package/templates/create/base/.github/dependabot.yml.hbs +20 -0
- package/templates/create/base/.github/workflows/ci.yml.hbs +12 -2
- package/templates/create/base/.golangci.yml.hbs +27 -0
- package/templates/create/base/AGENTS.md.hbs +21 -7
- package/templates/create/base/Dockerfile.hbs +42 -0
- package/templates/create/base/Makefile.hbs +43 -13
- package/templates/create/base/README.md.hbs +45 -8
- package/templates/create/base/cmd/api/main.go.hbs +17 -130
- package/templates/create/base/cmd/api/wiring.go.hbs +161 -0
- package/templates/create/base/go.mod.hbs +4 -4
- package/templates/create/base/internal/platform/database/database.go.hbs +30 -11
- package/templates/create/base/internal/shared/config/config.go.hbs +13 -7
- package/templates/create/base/internal/shared/pagination/pagination.go.hbs +11 -0
- package/templates/create/base/internal/shared/tx/tx.go.hbs +47 -0
- package/templates/create/base/redocly.yaml.hbs +21 -0
- package/templates/create/features/docs/architecture.md.hbs +32 -11
- package/templates/create/features/docs/openapi.yaml.hbs +0 -4
- package/templates/create/features/docs/patterns.md.hbs +82 -8
- package/templates/create/features/docs/techstack.md.hbs +8 -3
- package/templates/generate/module/dto.go.hbs +8 -1
- package/templates/generate/module/errors.go.hbs +5 -0
- package/templates/generate/module/field-column.down.sql.hbs +2 -0
- package/templates/generate/module/field-column.up.sql.hbs +15 -0
- package/templates/generate/module/handler_test.go.hbs +8 -1
- package/templates/generate/module/migration.down.sql.hbs +3 -1
- package/templates/generate/module/migration.up.sql.hbs +7 -2
- package/templates/generate/module/minimal/dto.go.hbs +3 -1
- package/templates/generate/module/model/model.go.hbs +10 -1
- package/templates/generate/module/permission.up.sql.hbs +3 -1
- package/templates/generate/module/repository.go.hbs +60 -6
- package/templates/generate/module/repository_test.go.hbs +30 -0
- package/templates/generate/module/service.go.hbs +10 -1
- package/templates/generate/module/service_test.go.hbs +45 -0
- package/dist/commands/remove.js +0 -88
- package/scripts/smoke-test.mjs +0 -2058
- package/templates/add/worker/internal/platform/queue/client.go.hbs +0 -31
- package/templates/add/worker/internal/platform/queue/server.go.hbs +0 -68
- package/tests/integration/default-module.test.mjs +0 -46
- package/tests/integration/generator-naming.test.mjs +0 -81
- package/tests/integration/generator-unit-test-seams.test.mjs +0 -91
- package/tests/integration/legacy-method-compat.test.mjs +0 -222
- package/tests/integration/remove-module.test.mjs +0 -58
- package/tests/unit/naming.test.mjs +0 -94
- package/tests/unit/smoke-isolation.test.mjs +0 -35
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
package queue
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"github.com/hibiken/asynq"
|
|
5
|
-
)
|
|
6
|
-
|
|
7
|
-
// Client enqueues tasks to Redis. Created by cmd/api; each domain or platform
|
|
8
|
-
// package defines its own task types and calls Client.Enqueue.
|
|
9
|
-
type Client struct {
|
|
10
|
-
inner *asynq.Client
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
func NewClient(redisURL string) (*Client, error) {
|
|
14
|
-
opt, err := asynq.ParseRedisURI(redisURL)
|
|
15
|
-
if err != nil {
|
|
16
|
-
return nil, err
|
|
17
|
-
}
|
|
18
|
-
return &Client{inner: asynq.NewClient(opt)}, nil
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
func (c *Client) Enqueue(task *asynq.Task, opts ...asynq.Option) (string, error) {
|
|
22
|
-
info, err := c.inner.Enqueue(task, opts...)
|
|
23
|
-
if err != nil {
|
|
24
|
-
return "", err
|
|
25
|
-
}
|
|
26
|
-
return info.ID, nil
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
func (c *Client) Close() error {
|
|
30
|
-
return c.inner.Close()
|
|
31
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
package queue
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
"fmt"
|
|
6
|
-
"log/slog"
|
|
7
|
-
|
|
8
|
-
"github.com/hibiken/asynq"
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
// Server runs the background worker. Created by cmd/worker; handlers register
|
|
12
|
-
// their task types on the mux before Start is called.
|
|
13
|
-
type Server struct {
|
|
14
|
-
inner *asynq.Server
|
|
15
|
-
mux *asynq.ServeMux
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// slogAdapter wraps *slog.Logger to implement asynq.Logger. Asynq calls these
|
|
19
|
-
// like the stdlib `log` package (a plain message, sometimes several args
|
|
20
|
-
// meant to be concatenated) — fmt.Sprint joins them into one message string
|
|
21
|
-
// rather than passing them as slog key-value pairs, which would otherwise
|
|
22
|
-
// misinterpret a lone message argument as a key with no value.
|
|
23
|
-
type slogAdapter struct {
|
|
24
|
-
inner *slog.Logger
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
func (a *slogAdapter) Debug(args ...interface{}) { a.inner.Debug(fmt.Sprint(args...)) }
|
|
28
|
-
func (a *slogAdapter) Info(args ...interface{}) { a.inner.Info(fmt.Sprint(args...)) }
|
|
29
|
-
func (a *slogAdapter) Warn(args ...interface{}) { a.inner.Warn(fmt.Sprint(args...)) }
|
|
30
|
-
func (a *slogAdapter) Error(args ...interface{}) { a.inner.Error(fmt.Sprint(args...)) }
|
|
31
|
-
func (a *slogAdapter) Fatal(args ...interface{}) { a.inner.Error(fmt.Sprint(args...)) }
|
|
32
|
-
|
|
33
|
-
func NewServer(redisURL string) (*Server, error) {
|
|
34
|
-
opt, err := asynq.ParseRedisURI(redisURL)
|
|
35
|
-
if err != nil {
|
|
36
|
-
return nil, err
|
|
37
|
-
}
|
|
38
|
-
srv := &Server{
|
|
39
|
-
inner: asynq.NewServer(opt, asynq.Config{
|
|
40
|
-
Concurrency: 10,
|
|
41
|
-
Logger: &slogAdapter{inner: slog.Default()},
|
|
42
|
-
}),
|
|
43
|
-
mux: asynq.NewServeMux(),
|
|
44
|
-
}
|
|
45
|
-
// Global error handler for tasks that panic or return a non-Retryable error.
|
|
46
|
-
srv.mux.Use(func(next asynq.Handler) asynq.Handler {
|
|
47
|
-
return asynq.HandlerFunc(func(ctx context.Context, task *asynq.Task) error {
|
|
48
|
-
err := next.ProcessTask(ctx, task)
|
|
49
|
-
if err != nil {
|
|
50
|
-
slog.Error("task failed", "type", task.Type(), "error", err)
|
|
51
|
-
}
|
|
52
|
-
return err
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
return srv, nil
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
func (s *Server) Handle(pattern string, handler asynq.Handler) {
|
|
59
|
-
s.mux.Handle(pattern, handler)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
func (s *Server) Start() error {
|
|
63
|
-
return s.inner.Start(s.mux)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
func (s *Server) Shutdown() {
|
|
67
|
-
s.inner.Shutdown()
|
|
68
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import test from "node:test";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
|
|
9
|
-
const ROOT = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
|
|
10
|
-
const CLI = path.join(ROOT, "bin", "go-scaffold.js");
|
|
11
|
-
|
|
12
|
-
function runCLI(cwd, ...args) {
|
|
13
|
-
return execFileSync("node", [CLI, ...args], {
|
|
14
|
-
cwd,
|
|
15
|
-
encoding: "utf8",
|
|
16
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
test("generate module defaults to a safe minimal module", () => {
|
|
21
|
-
const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-default-module-"));
|
|
22
|
-
try {
|
|
23
|
-
runCLI(scratch, "create", "sample", "--defaults", "--no-docker");
|
|
24
|
-
const project = path.join(scratch, "sample");
|
|
25
|
-
|
|
26
|
-
const output = runCLI(project, "generate", "module", "orders");
|
|
27
|
-
const handler = readFileSync(
|
|
28
|
-
path.join(project, "internal", "app", "order", "handler.go"),
|
|
29
|
-
"utf8"
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
assert.match(output, /registered empty route group/);
|
|
33
|
-
assert.doesNotMatch(handler, /g\.POST\(/);
|
|
34
|
-
assert.equal(existsSync(path.join(project, "docs", "orders")), false);
|
|
35
|
-
|
|
36
|
-
const legacyOutput = runCLI(project, "generate", "module", "widgets", "--no-full");
|
|
37
|
-
const legacyHandler = readFileSync(
|
|
38
|
-
path.join(project, "internal", "app", "widget", "handler.go"),
|
|
39
|
-
"utf8"
|
|
40
|
-
);
|
|
41
|
-
assert.match(legacyOutput, /registered empty route group/);
|
|
42
|
-
assert.doesNotMatch(legacyHandler, /g\.POST\(/);
|
|
43
|
-
} finally {
|
|
44
|
-
rmSync(scratch, { recursive: true, force: true });
|
|
45
|
-
}
|
|
46
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
3
|
-
import {
|
|
4
|
-
mkdtempSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
readdirSync,
|
|
7
|
-
rmSync,
|
|
8
|
-
} from "node:fs";
|
|
9
|
-
import { tmpdir } from "node:os";
|
|
10
|
-
import path from "node:path";
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
13
|
-
|
|
14
|
-
const ROOT = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
|
|
15
|
-
const CLI = path.join(ROOT, "bin", "go-scaffold.js");
|
|
16
|
-
|
|
17
|
-
function runCLI(cwd, ...args) {
|
|
18
|
-
return execFileSync("node", [CLI, ...args], {
|
|
19
|
-
cwd,
|
|
20
|
-
encoding: "utf8",
|
|
21
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function read(root, relativePath) {
|
|
26
|
-
return readFileSync(path.join(root, relativePath), "utf8");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function migration(root, suffix) {
|
|
30
|
-
const filename = readdirSync(path.join(root, "migrations")).find((entry) =>
|
|
31
|
-
entry.endsWith(suffix)
|
|
32
|
-
);
|
|
33
|
-
assert.ok(filename, `missing migration ending with ${suffix}`);
|
|
34
|
-
return read(root, path.join("migrations", filename));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
test("plural module input produces a singular entity with one explicit table name", () => {
|
|
38
|
-
const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-naming-"));
|
|
39
|
-
try {
|
|
40
|
-
runCLI(scratch, "create", "sample", "--defaults", "--no-docker");
|
|
41
|
-
const project = path.join(scratch, "sample");
|
|
42
|
-
runCLI(project, "generate", "module", "orders");
|
|
43
|
-
|
|
44
|
-
const model = read(project, "internal/app/order/model/model.go");
|
|
45
|
-
assert.match(model, /type Order struct/);
|
|
46
|
-
assert.match(
|
|
47
|
-
model,
|
|
48
|
-
/func \(Order\) TableName\(\) string \{\s*return "orders"\s*\}/
|
|
49
|
-
);
|
|
50
|
-
assert.match(migration(project, "_create_orders.up.sql"), /CREATE TABLE orders/);
|
|
51
|
-
assert.match(read(project, "internal/app/order/handler.go"), /Group\("\/orders"/);
|
|
52
|
-
} finally {
|
|
53
|
-
rmSync(scratch, { recursive: true, force: true });
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("multi-word module keeps URL words and uses snake_case SQL identifiers", () => {
|
|
58
|
-
const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-multiword-"));
|
|
59
|
-
try {
|
|
60
|
-
runCLI(scratch, "create", "sample", "--defaults", "--no-docker");
|
|
61
|
-
const project = path.join(scratch, "sample");
|
|
62
|
-
runCLI(project, "generate", "module", "order-items");
|
|
63
|
-
|
|
64
|
-
const model = read(project, "internal/app/orderitem/model/model.go");
|
|
65
|
-
assert.match(model, /type OrderItem struct/);
|
|
66
|
-
assert.match(
|
|
67
|
-
model,
|
|
68
|
-
/func \(OrderItem\) TableName\(\) string \{\s*return "order_items"\s*\}/
|
|
69
|
-
);
|
|
70
|
-
assert.match(
|
|
71
|
-
migration(project, "_create_order-items.up.sql"),
|
|
72
|
-
/CREATE TABLE order_items/
|
|
73
|
-
);
|
|
74
|
-
assert.match(
|
|
75
|
-
read(project, "internal/app/orderitem/handler.go"),
|
|
76
|
-
/Group\("\/order-items"/
|
|
77
|
-
);
|
|
78
|
-
} finally {
|
|
79
|
-
rmSync(scratch, { recursive: true, force: true });
|
|
80
|
-
}
|
|
81
|
-
});
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import test from "node:test";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
|
|
9
|
-
const ROOT = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
|
|
10
|
-
const CLI = path.join(ROOT, "bin", "go-scaffold.js");
|
|
11
|
-
|
|
12
|
-
function run(command, args, cwd) {
|
|
13
|
-
return execFileSync(command, args, {
|
|
14
|
-
cwd,
|
|
15
|
-
encoding: "utf8",
|
|
16
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function read(project, relativePath) {
|
|
21
|
-
return readFileSync(path.join(project, relativePath), "utf8");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
test("generated modules have scalable service and handler unit-test seams", () => {
|
|
25
|
-
const scratch = mkdtempSync(path.join(tmpdir(), "go-scaffold-unit-seams-"));
|
|
26
|
-
try {
|
|
27
|
-
run("node", [CLI, "create", "sample", "--defaults", "--no-docker"], scratch);
|
|
28
|
-
const project = path.join(scratch, "sample");
|
|
29
|
-
run("node", [CLI, "generate", "module", "orders", "--full"], project);
|
|
30
|
-
|
|
31
|
-
const handler = read(project, "internal/app/order/handler.go");
|
|
32
|
-
assert.match(handler, /type service interface \{/);
|
|
33
|
-
assert.match(handler, /svc service/);
|
|
34
|
-
assert.doesNotMatch(handler, /svc \*Service/);
|
|
35
|
-
assert.match(handler, /Create\(context\.Context, createInput\)/);
|
|
36
|
-
assert.match(handler, /Update\(context\.Context, uuid\.UUID, updateInput\)/);
|
|
37
|
-
|
|
38
|
-
const service = read(project, "internal/app/order/service.go");
|
|
39
|
-
assert.match(service, /Create\(ctx context\.Context, in createInput\)/);
|
|
40
|
-
assert.match(service, /Update\(ctx context\.Context, id uuid\.UUID, in updateInput\)/);
|
|
41
|
-
|
|
42
|
-
const serviceTest = read(project, "internal/app/order/service_test.go");
|
|
43
|
-
assert.match(serviceTest, /type repositoryStub struct/);
|
|
44
|
-
assert.match(serviceTest, /createFn\s+func/);
|
|
45
|
-
assert.match(serviceTest, /findByIDFn\s+func/);
|
|
46
|
-
assert.match(serviceTest, /updateFn\s+func/);
|
|
47
|
-
assert.doesNotMatch(serviceTest, /type fakeRepo struct/);
|
|
48
|
-
|
|
49
|
-
const handlerTest = read(project, "internal/app/order/handler_test.go");
|
|
50
|
-
assert.match(handlerTest, /type serviceStub struct/);
|
|
51
|
-
assert.match(handlerTest, /createFn\s+func/);
|
|
52
|
-
assert.doesNotMatch(handlerTest, /gorm\.io\/driver\/postgres/);
|
|
53
|
-
assert.doesNotMatch(handlerTest, /TEST_DB_DSN/);
|
|
54
|
-
|
|
55
|
-
const repositoryTest = read(project, "internal/app/order/repository_test.go");
|
|
56
|
-
assert.match(repositoryTest, /REQUIRE_TEST_DB/);
|
|
57
|
-
assert.match(repositoryTest, /TEST_DB_DSN/);
|
|
58
|
-
assert.doesNotMatch(repositoryTest, /AutoMigrate/);
|
|
59
|
-
assert.doesNotMatch(repositoryTest, /DropTable/);
|
|
60
|
-
|
|
61
|
-
const ci = read(project, ".github/workflows/ci.yml");
|
|
62
|
-
assert.match(ci, /migrate -path migrations/);
|
|
63
|
-
assert.match(ci, /REQUIRE_TEST_DB: "true"/);
|
|
64
|
-
assert.match(ci, /TEST_DB_DSN:/);
|
|
65
|
-
|
|
66
|
-
const itemDocs = read(project, "docs/orders/item.yaml");
|
|
67
|
-
const deleteContract = itemDocs.slice(itemDocs.indexOf("delete:"));
|
|
68
|
-
assert.doesNotMatch(deleteContract, /"404"/);
|
|
69
|
-
|
|
70
|
-
run("node", [CLI, "generate", "method", "orders", "approve", "--type", "patch"], project);
|
|
71
|
-
const openapi = read(project, "docs/openapi.yaml");
|
|
72
|
-
assert.match(openapi, /\/v1\/orders\/\{id\}\/approve:/);
|
|
73
|
-
const approveDocs = read(project, "docs/orders/methods/approve.yaml");
|
|
74
|
-
assert.match(approveDocs, /^parameters:/);
|
|
75
|
-
assert.match(approveDocs, /^patch:/m);
|
|
76
|
-
assert.match(approveDocs, /operationId: approveOrder/);
|
|
77
|
-
assert.doesNotMatch(approveDocs, /requestBody:/);
|
|
78
|
-
|
|
79
|
-
run("node", [CLI, "generate", "method", "orders", "submit", "--type", "post"], project);
|
|
80
|
-
const submitDocs = read(project, "docs/orders/methods/submit.yaml");
|
|
81
|
-
assert.doesNotMatch(submitDocs, /^parameters:/);
|
|
82
|
-
assert.doesNotMatch(submitDocs, /in: path/);
|
|
83
|
-
assert.match(submitDocs, /^post:/m);
|
|
84
|
-
assert.match(submitDocs, /requestBody:/);
|
|
85
|
-
|
|
86
|
-
run("go", ["mod", "tidy"], project);
|
|
87
|
-
run("go", ["test", "./..."], project);
|
|
88
|
-
} finally {
|
|
89
|
-
rmSync(scratch, { recursive: true, force: true });
|
|
90
|
-
}
|
|
91
|
-
});
|
|
@@ -1,222 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
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
|
-
});
|