@nakedev/go-scaffold 0.1.4 → 0.3.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.
- 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 +59 -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 +8 -4
- 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
|
@@ -2,15 +2,23 @@ package {{pkg}}
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
|
+
"errors"
|
|
5
6
|
|
|
6
7
|
"{{goModule}}/internal/app/{{modulePath}}/model"
|
|
8
|
+
"{{goModule}}/internal/shared/tx"
|
|
7
9
|
|
|
8
10
|
"github.com/google/uuid"
|
|
9
11
|
"gorm.io/gorm"
|
|
10
12
|
)
|
|
11
13
|
|
|
14
|
+
// ErrStaleVersion means the row moved on since the caller read it — someone
|
|
15
|
+
// else saved first. Returned instead of overwriting their work.
|
|
16
|
+
var ErrStaleVersion = errors.New("stale version")
|
|
17
|
+
|
|
12
18
|
// Repository = data access for {{pkg}} (the only place that touches the DB for this domain)
|
|
13
|
-
// every method takes ctx, so a cancelled request cancels the query too
|
|
19
|
+
// every method takes ctx, so a cancelled request cancels the query too — and
|
|
20
|
+
// so tx.From can pick up a transaction opened by the caller, letting two
|
|
21
|
+
// repositories commit together without changing any signature here.
|
|
14
22
|
type Repository struct {
|
|
15
23
|
db *gorm.DB
|
|
16
24
|
}
|
|
@@ -20,30 +28,76 @@ func NewRepository(db *gorm.DB) *Repository {
|
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
func (r *Repository) Create(ctx context.Context, m *model.{{pascalName}}) error {
|
|
23
|
-
return r.db.WithContext(ctx).Create(m).Error
|
|
31
|
+
return tx.From(ctx, r.db).WithContext(ctx).Create(m).Error
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
func (r *Repository) FindAll(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error) {
|
|
27
35
|
var items []model.{{pascalName}}
|
|
28
|
-
err := r.db.WithContext(ctx).Order("id desc").Limit(limit).Offset(offset).Find(&items).Error
|
|
36
|
+
err := tx.From(ctx, r.db).WithContext(ctx).Order("id desc").Limit(limit).Offset(offset).Find(&items).Error
|
|
29
37
|
return items, err
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
func (r *Repository) FindByID(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
|
|
33
41
|
var m model.{{pascalName}}
|
|
34
42
|
// "id = ?" explicit — PK is a UUID, not an int, this avoids GORM misinterpreting the arg
|
|
35
|
-
if err := r.db.WithContext(ctx).First(&m, "id = ?", id).Error; err != nil {
|
|
43
|
+
if err := tx.From(ctx, r.db).WithContext(ctx).First(&m, "id = ?", id).Error; err != nil {
|
|
36
44
|
return nil, err
|
|
37
45
|
}
|
|
38
46
|
return &m, nil
|
|
39
47
|
}
|
|
40
48
|
|
|
49
|
+
// Update applies m only if the version it carries still matches the stored
|
|
50
|
+
// row, then bumps it. Two clients that both loaded version 3 can't silently
|
|
51
|
+
// overwrite each other: the second one gets ErrStaleVersion instead of the
|
|
52
|
+
// first one's changes disappearing with no error anywhere.
|
|
41
53
|
func (r *Repository) Update(ctx context.Context, m *model.{{pascalName}}) error {
|
|
42
|
-
|
|
54
|
+
expected := m.Version
|
|
55
|
+
m.Version = expected + 1
|
|
56
|
+
|
|
57
|
+
res := tx.From(ctx, r.db).WithContext(ctx).
|
|
58
|
+
Model(&model.{{pascalName}}{}).
|
|
59
|
+
Where("id = ? AND version = ?", m.ID, expected).
|
|
60
|
+
// Select("*") because Updates on a struct skips zero values, which
|
|
61
|
+
// would quietly ignore a field the caller meant to clear.
|
|
62
|
+
Select("*").Omit("id", "created_at").
|
|
63
|
+
Updates(m)
|
|
64
|
+
if res.Error != nil {
|
|
65
|
+
m.Version = expected
|
|
66
|
+
return res.Error
|
|
67
|
+
}
|
|
68
|
+
if res.RowsAffected == 0 {
|
|
69
|
+
m.Version = expected
|
|
70
|
+
return ErrStaleVersion
|
|
71
|
+
}
|
|
72
|
+
return nil
|
|
43
73
|
}
|
|
44
74
|
|
|
75
|
+
// Delete reports gorm.ErrRecordNotFound when the row wasn't there, so a
|
|
76
|
+
// DELETE on an id that never existed answers 404 like GET does — a plain
|
|
77
|
+
// GORM delete affects zero rows and returns no error, which had the same
|
|
78
|
+
// request answer 204 as a real deletion.
|
|
45
79
|
func (r *Repository) Delete(ctx context.Context, id uuid.UUID) error {
|
|
46
|
-
|
|
80
|
+
res := tx.From(ctx, r.db).WithContext(ctx).Delete(&model.{{pascalName}}{}, "id = ?", id)
|
|
81
|
+
if res.Error != nil {
|
|
82
|
+
return res.Error
|
|
83
|
+
}
|
|
84
|
+
if res.RowsAffected == 0 {
|
|
85
|
+
return gorm.ErrRecordNotFound
|
|
86
|
+
}
|
|
87
|
+
return nil
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Count is the total the list endpoint needs to say "page 3 of 12" — see
|
|
91
|
+
// pagination.ResponseWithTotal. Separate from FindAll because it costs a second
|
|
92
|
+
// query: an endpoint that only ever scrolls shouldn't pay for a count nobody
|
|
93
|
+
// reads.
|
|
94
|
+
//
|
|
95
|
+
// Apply the same WHERE clause here that FindAll uses once you add filtering,
|
|
96
|
+
// or the total will describe a different set than the rows.
|
|
97
|
+
func (r *Repository) Count(ctx context.Context) (int64, error) {
|
|
98
|
+
var total int64
|
|
99
|
+
err := tx.From(ctx, r.db).WithContext(ctx).Model(&model.{{pascalName}}{}).Count(&total).Error
|
|
100
|
+
return total, err
|
|
47
101
|
}
|
|
48
102
|
|
|
49
103
|
// go-scaffold:repository-methods
|
|
@@ -2,6 +2,7 @@ package {{pkg}}
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
|
+
"errors"
|
|
5
6
|
"os"
|
|
6
7
|
"testing"
|
|
7
8
|
|
|
@@ -77,3 +78,32 @@ func TestRepository_CreateFindDelete(t *testing.T) {
|
|
|
77
78
|
t.Fatalf("delete: %v", err)
|
|
78
79
|
}
|
|
79
80
|
}
|
|
81
|
+
|
|
82
|
+
// Two clients both hold version 1; the second save must be refused instead of
|
|
83
|
+
// overwriting the first. Runs against the real database because the guard is
|
|
84
|
+
// a WHERE clause — a stub would only be testing itself.
|
|
85
|
+
func TestRepository_Update_RefusesStaleVersion(t *testing.T) {
|
|
86
|
+
repo := NewRepository(repositoryDBForTest(t))
|
|
87
|
+
ctx := context.Background()
|
|
88
|
+
item := &model.{{pascalName}}{ID: uuid.New()}
|
|
89
|
+
|
|
90
|
+
if err := repo.Create(ctx, item); err != nil {
|
|
91
|
+
t.Fatalf("create: %v", err)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
first := &model.{{pascalName}}{ID: item.ID, Version: item.Version}
|
|
95
|
+
if err := repo.Update(ctx, first); err != nil {
|
|
96
|
+
t.Fatalf("first update: %v", err)
|
|
97
|
+
}
|
|
98
|
+
if first.Version != item.Version+1 {
|
|
99
|
+
t.Fatalf("want version bumped to %d, got %d", item.Version+1, first.Version)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
stale := &model.{{pascalName}}{ID: item.ID, Version: item.Version}
|
|
103
|
+
if err := repo.Update(ctx, stale); !errors.Is(err, ErrStaleVersion) {
|
|
104
|
+
t.Fatalf("want ErrStaleVersion for the second writer, got %v", err)
|
|
105
|
+
}
|
|
106
|
+
if stale.Version != item.Version {
|
|
107
|
+
t.Fatalf("a refused update must leave the caller's version alone, got %d", stale.Version)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -68,8 +68,14 @@ func (s *Service) Update(ctx context.Context, id uuid.UUID, in updateInput) (*mo
|
|
|
68
68
|
return nil, wrapFindErr(err)
|
|
69
69
|
}
|
|
70
70
|
// TODO: apply real fields from in before saving
|
|
71
|
-
|
|
71
|
+
// The version the client read comes from the request, not from the row we
|
|
72
|
+
// just loaded — comparing the row against itself would always succeed and
|
|
73
|
+
// defeat the check.
|
|
74
|
+
m.Version = in.Version
|
|
72
75
|
if err := s.repo.Update(ctx, m); err != nil {
|
|
76
|
+
if errors.Is(err, ErrStaleVersion) {
|
|
77
|
+
return nil, errStale()
|
|
78
|
+
}
|
|
73
79
|
if dberr.IsDuplicate(err) {
|
|
74
80
|
return nil, errConflict()
|
|
75
81
|
}
|
|
@@ -80,6 +86,9 @@ func (s *Service) Update(ctx context.Context, id uuid.UUID, in updateInput) (*mo
|
|
|
80
86
|
|
|
81
87
|
func (s *Service) Delete(ctx context.Context, id uuid.UUID) error {
|
|
82
88
|
if err := s.repo.Delete(ctx, id); err != nil {
|
|
89
|
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
90
|
+
return errNotFound()
|
|
91
|
+
}
|
|
83
92
|
// deleting a {{pkg}} that's still referenced elsewhere → FK RESTRICT trips = 409, not 500
|
|
84
93
|
if dberr.IsForeignKey(err) {
|
|
85
94
|
return errHasReferences()
|
|
@@ -114,3 +114,48 @@ func TestService_List_DBError_Becomes500(t *testing.T) {
|
|
|
114
114
|
t.Fatalf("want 500, got %d", got)
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
// The check that stops one user's save from silently erasing another's: the
|
|
119
|
+
// repository reports the row moved on, the client must see 409 rather than a
|
|
120
|
+
// 500 or, worse, a success.
|
|
121
|
+
func TestService_Update_StaleVersion(t *testing.T) {
|
|
122
|
+
repo := &repositoryStub{
|
|
123
|
+
findByIDFn: func(_ context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
|
|
124
|
+
return &model.{{pascalName}}{ID: id, Version: 7}, nil
|
|
125
|
+
},
|
|
126
|
+
updateFn: func(context.Context, *model.{{pascalName}}) error {
|
|
127
|
+
return ErrStaleVersion
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
svc := NewService(repo)
|
|
131
|
+
|
|
132
|
+
_, err := svc.Update(context.Background(), uuid.New(), updateInput{Version: 3})
|
|
133
|
+
|
|
134
|
+
if got := status(t, err); got != 409 {
|
|
135
|
+
t.Fatalf("want 409 stale, got %d", got)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The version compared against the database has to be the one the client sent,
|
|
140
|
+
// not the one just read back — comparing the row against itself would always
|
|
141
|
+
// match and the guard would never fire.
|
|
142
|
+
func TestService_Update_SendsClientVersion(t *testing.T) {
|
|
143
|
+
var seen int
|
|
144
|
+
repo := &repositoryStub{
|
|
145
|
+
findByIDFn: func(_ context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
|
|
146
|
+
return &model.{{pascalName}}{ID: id, Version: 7}, nil
|
|
147
|
+
},
|
|
148
|
+
updateFn: func(_ context.Context, m *model.{{pascalName}}) error {
|
|
149
|
+
seen = m.Version
|
|
150
|
+
return nil
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
svc := NewService(repo)
|
|
154
|
+
|
|
155
|
+
if _, err := svc.Update(context.Background(), uuid.New(), updateInput{Version: 3}); err != nil {
|
|
156
|
+
t.Fatalf("update: %v", err)
|
|
157
|
+
}
|
|
158
|
+
if seen != 3 {
|
|
159
|
+
t.Fatalf("want the client's version 3 to reach the repository, got %d", seen)
|
|
160
|
+
}
|
|
161
|
+
}
|
package/dist/commands/remove.js
DELETED
|
@@ -1,88 +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.removeModule = removeModule;
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
-
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
-
const prompts_1 = require("@inquirer/prompts");
|
|
11
|
-
const config_1 = require("../utils/config");
|
|
12
|
-
const module_location_1 = require("../utils/module-location");
|
|
13
|
-
const main_patcher_1 = require("../utils/main-patcher");
|
|
14
|
-
const openapi_patcher_1 = require("../utils/openapi-patcher");
|
|
15
|
-
const template_renderer_1 = require("../utils/template-renderer");
|
|
16
|
-
const generate_wizard_1 = require("../prompts/generate-wizard");
|
|
17
|
-
// removeModule deletes application code and reverses generated wiring while
|
|
18
|
-
// preserving immutable migration history and table data. Destructive schema
|
|
19
|
-
// removal must be an explicit new migration, never a deletion of an applied one.
|
|
20
|
-
async function removeModule(rawName, opts, projectDir = process.cwd()) {
|
|
21
|
-
const config = (0, config_1.readConfig)(projectDir);
|
|
22
|
-
const naming = (0, module_location_1.resolveProjectModuleNaming)(projectDir, rawName ?? (await (0, generate_wizard_1.promptModuleName)()));
|
|
23
|
-
const modulePath = naming.pkg;
|
|
24
|
-
const moduleDir = path_1.default.join(projectDir, "internal", "app", modulePath);
|
|
25
|
-
if (!fs_extra_1.default.existsSync(moduleDir)) {
|
|
26
|
-
throw new Error(`module "${naming.pkg}" not found at internal/app/${modulePath} — nothing to remove`);
|
|
27
|
-
}
|
|
28
|
-
if (!opts.yes) {
|
|
29
|
-
const ok = await (0, prompts_1.confirm)({
|
|
30
|
-
message: `Remove module "${naming.pkg}"? Deletes internal/app/${modulePath}/ and its docs, ` +
|
|
31
|
-
`then un-wires main.go/openapi.yaml. Existing migrations, table, and data are preserved.`,
|
|
32
|
-
default: false,
|
|
33
|
-
});
|
|
34
|
-
if (!ok)
|
|
35
|
-
throw new Error("removal cancelled");
|
|
36
|
-
}
|
|
37
|
-
// detect --auth/--permission from the generated handler.go itself (not
|
|
38
|
-
// stored anywhere else) — unpatchMainGo needs the exact same flags used at
|
|
39
|
-
// generate-time to reconstruct the identical line it's removing.
|
|
40
|
-
const handlerGoPath = path_1.default.join(moduleDir, "handler.go");
|
|
41
|
-
let auth;
|
|
42
|
-
let permission;
|
|
43
|
-
if (fs_extra_1.default.existsSync(handlerGoPath)) {
|
|
44
|
-
const handlerContent = fs_extra_1.default.readFileSync(handlerGoPath, "utf8");
|
|
45
|
-
auth = /jwtSecret\s+string/.test(handlerContent);
|
|
46
|
-
permission = handlerContent.match(/h\.authz\.Require\("([^"]+)"\)/)?.[1];
|
|
47
|
-
}
|
|
48
|
-
// 1. the domain package
|
|
49
|
-
fs_extra_1.default.removeSync(moduleDir);
|
|
50
|
-
// 2. main.go wiring
|
|
51
|
-
(0, main_patcher_1.unpatchMainGo)(path_1.default.join(projectDir, "cmd", "api", "main.go"), {
|
|
52
|
-
goModule: config.goModule,
|
|
53
|
-
modulePath,
|
|
54
|
-
pkg: naming.pkg,
|
|
55
|
-
pascalName: naming.pascalName,
|
|
56
|
-
auth,
|
|
57
|
-
permission,
|
|
58
|
-
});
|
|
59
|
-
// 3. openapi index + per-module docs
|
|
60
|
-
const openapiPath = path_1.default.join(projectDir, "docs", "openapi.yaml");
|
|
61
|
-
if (fs_extra_1.default.existsSync(openapiPath)) {
|
|
62
|
-
(0, openapi_patcher_1.unpatchOpenapiIndex)(openapiPath, naming, config.apiPrefix);
|
|
63
|
-
fs_extra_1.default.removeSync(path_1.default.join(projectDir, "docs", naming.plural));
|
|
64
|
-
}
|
|
65
|
-
// 4. Migration history is immutable. A migration may already be recorded in
|
|
66
|
-
// schema_migrations on production databases, so deleting its file would make
|
|
67
|
-
// existing and fresh environments disagree and can prevent the app booting.
|
|
68
|
-
// Re-generating the same module reuses this create migration.
|
|
69
|
-
const migrationsDir = path_1.default.join(projectDir, "migrations");
|
|
70
|
-
const preservedMigrations = fs_extra_1.default.existsSync(migrationsDir)
|
|
71
|
-
? fs_extra_1.default.readdirSync(migrationsDir).filter((f) => f.endsWith(`_create_${naming.plural}.up.sql`) ||
|
|
72
|
-
f.endsWith(`_create_${naming.plural}.down.sql`) ||
|
|
73
|
-
f.endsWith(`_add_${naming.plural}_permission.up.sql`) ||
|
|
74
|
-
f.endsWith(`_add_${naming.plural}_permission.down.sql`))
|
|
75
|
-
: [];
|
|
76
|
-
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
77
|
-
console.log(picocolors_1.default.green(`\nremoved module "${naming.pkg}"`));
|
|
78
|
-
console.log(` deleted internal/app/${modulePath}/`);
|
|
79
|
-
console.log(` un-wired cmd/api/main.go`);
|
|
80
|
-
if (fs_extra_1.default.existsSync(openapiPath))
|
|
81
|
-
console.log(` un-wired docs/openapi.yaml + deleted docs/${naming.plural}/`);
|
|
82
|
-
if (preservedMigrations.length) {
|
|
83
|
-
console.log(` preserved migration history: ${preservedMigrations.join(", ")}`);
|
|
84
|
-
}
|
|
85
|
-
console.log(picocolors_1.default.yellow(`\nnote: the ${naming.tableName} table and its data are untouched. ` +
|
|
86
|
-
`To remove them safely, run \`go-scaffold generate migration drop_${naming.tableName}\` ` +
|
|
87
|
-
`and write an explicit up/down migration.`));
|
|
88
|
-
}
|