@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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -44
  3. package/dist/commands/auth.js +116 -11
  4. package/dist/commands/create.js +13 -1
  5. package/dist/commands/generate.js +21 -11
  6. package/dist/commands/method.js +32 -3
  7. package/dist/commands/observability.js +114 -0
  8. package/dist/commands/rbac.js +19 -2
  9. package/dist/commands/undo.js +331 -0
  10. package/dist/commands/worker.js +92 -32
  11. package/dist/index.js +366 -63
  12. package/dist/prompts/auth-wizard.js +29 -0
  13. package/dist/prompts/create-wizard.js +5 -2
  14. package/dist/prompts/generate-wizard.js +57 -0
  15. package/dist/prompts/worker-wizard.js +25 -0
  16. package/dist/templates/auth-manifest.js +20 -3
  17. package/dist/templates/create-manifest.js +13 -20
  18. package/dist/templates/observability-manifest.js +24 -0
  19. package/dist/templates/rbac-manifest.js +1 -0
  20. package/dist/templates/worker-manifest.js +23 -6
  21. package/dist/utils/auth-patcher.js +96 -21
  22. package/dist/utils/config.js +58 -10
  23. package/dist/utils/gocheck.js +57 -5
  24. package/dist/utils/golangci-patcher.js +73 -0
  25. package/dist/utils/gomod-patcher.js +53 -0
  26. package/dist/utils/main-patcher.js +58 -4
  27. package/dist/utils/marker-patch.js +125 -3
  28. package/dist/utils/method-patcher.js +17 -2
  29. package/dist/utils/module-location.js +37 -1
  30. package/dist/utils/naming.js +50 -2
  31. package/dist/utils/observability-patcher.js +107 -0
  32. package/dist/utils/platform-patcher.js +98 -12
  33. package/dist/utils/rbac-patcher.js +60 -10
  34. package/package.json +3 -5
  35. package/templates/add/auth/internal/app/user/errors.go.hbs +7 -0
  36. package/templates/add/auth/internal/app/user/handler.go.hbs +64 -23
  37. package/templates/add/auth/internal/app/user/jwt.go.hbs +31 -7
  38. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +39 -0
  39. package/templates/add/auth/internal/app/user/model/identity.go.hbs +3 -0
  40. package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +26 -0
  41. package/templates/add/auth/internal/app/user/model/user.go.hbs +9 -1
  42. package/templates/add/auth/internal/app/user/repository.go.hbs +64 -11
  43. package/templates/add/auth/internal/app/user/repository_test.go.hbs +192 -0
  44. package/templates/add/auth/internal/app/user/service.go.hbs +105 -21
  45. package/templates/add/auth/internal/app/user/service_test.go.hbs +81 -2
  46. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +9 -138
  47. package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +144 -0
  48. package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +147 -0
  49. package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +21 -19
  50. package/templates/add/auth/internal/shared/middleware/ratelimit_memory.go.hbs +63 -0
  51. package/templates/add/auth/internal/shared/middleware/ratelimit_redis.go.hbs +32 -0
  52. package/templates/add/auth/migrations/create_auth_tokens.down.sql.hbs +1 -0
  53. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +16 -0
  54. package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -1
  55. package/templates/add/auth/migrations/create_identities.up.sql.hbs +9 -5
  56. package/templates/add/auth/migrations/create_login_throttle.down.sql.hbs +1 -0
  57. package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +10 -0
  58. package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -1
  59. package/templates/add/auth/migrations/create_users.up.sql.hbs +13 -2
  60. package/templates/add/rbac/internal/app/role/dto.go.hbs +8 -3
  61. package/templates/add/rbac/internal/app/role/handler.go.hbs +4 -1
  62. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +3 -0
  63. package/templates/add/rbac/internal/app/role/model/role.go.hbs +4 -0
  64. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +3 -0
  65. package/templates/add/rbac/internal/app/role/repository.go.hbs +12 -11
  66. package/templates/add/rbac/internal/app/role/repository_test.go.hbs +176 -0
  67. package/templates/add/rbac/internal/app/role/service.go.hbs +7 -0
  68. package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +19 -0
  69. package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +1 -1
  70. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +5 -5
  71. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -11
  72. package/templates/add/worker/cmd/worker/main.go.hbs +30 -24
  73. package/templates/add/worker/internal/platform/mail/mail.go.hbs +21 -0
  74. package/templates/add/worker/internal/platform/mail/task.go.hbs +31 -34
  75. package/templates/add/worker/internal/platform/queue/asynq.go.hbs +140 -0
  76. package/templates/add/worker/internal/platform/queue/queue.go.hbs +87 -0
  77. package/templates/add/worker/internal/platform/queue/river.go.hbs +148 -0
  78. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +59 -12
  79. package/templates/create/base/.dockerignore.hbs +13 -0
  80. package/templates/create/base/.env.example.hbs +18 -9
  81. package/templates/create/base/.github/dependabot.yml.hbs +20 -0
  82. package/templates/create/base/.github/workflows/ci.yml.hbs +12 -2
  83. package/templates/create/base/.golangci.yml.hbs +27 -0
  84. package/templates/create/base/AGENTS.md.hbs +8 -4
  85. package/templates/create/base/Dockerfile.hbs +42 -0
  86. package/templates/create/base/Makefile.hbs +43 -13
  87. package/templates/create/base/README.md.hbs +45 -8
  88. package/templates/create/base/cmd/api/main.go.hbs +17 -130
  89. package/templates/create/base/cmd/api/wiring.go.hbs +161 -0
  90. package/templates/create/base/go.mod.hbs +4 -4
  91. package/templates/create/base/internal/platform/database/database.go.hbs +30 -11
  92. package/templates/create/base/internal/shared/config/config.go.hbs +13 -7
  93. package/templates/create/base/internal/shared/pagination/pagination.go.hbs +11 -0
  94. package/templates/create/base/internal/shared/tx/tx.go.hbs +47 -0
  95. package/templates/create/base/redocly.yaml.hbs +21 -0
  96. package/templates/create/features/docs/architecture.md.hbs +32 -11
  97. package/templates/create/features/docs/openapi.yaml.hbs +0 -4
  98. package/templates/create/features/docs/patterns.md.hbs +82 -8
  99. package/templates/create/features/docs/techstack.md.hbs +8 -3
  100. package/templates/generate/module/dto.go.hbs +8 -1
  101. package/templates/generate/module/errors.go.hbs +5 -0
  102. package/templates/generate/module/field-column.down.sql.hbs +2 -0
  103. package/templates/generate/module/field-column.up.sql.hbs +15 -0
  104. package/templates/generate/module/handler_test.go.hbs +8 -1
  105. package/templates/generate/module/migration.down.sql.hbs +3 -1
  106. package/templates/generate/module/migration.up.sql.hbs +7 -2
  107. package/templates/generate/module/minimal/dto.go.hbs +3 -1
  108. package/templates/generate/module/model/model.go.hbs +10 -1
  109. package/templates/generate/module/permission.up.sql.hbs +3 -1
  110. package/templates/generate/module/repository.go.hbs +60 -6
  111. package/templates/generate/module/repository_test.go.hbs +30 -0
  112. package/templates/generate/module/service.go.hbs +10 -1
  113. package/templates/generate/module/service_test.go.hbs +45 -0
  114. package/dist/commands/remove.js +0 -88
  115. package/scripts/smoke-test.mjs +0 -2058
  116. package/templates/add/worker/internal/platform/queue/client.go.hbs +0 -31
  117. package/templates/add/worker/internal/platform/queue/server.go.hbs +0 -68
  118. package/tests/integration/default-module.test.mjs +0 -46
  119. package/tests/integration/generator-naming.test.mjs +0 -81
  120. package/tests/integration/generator-unit-test-seams.test.mjs +0 -91
  121. package/tests/integration/legacy-method-compat.test.mjs +0 -222
  122. package/tests/integration/remove-module.test.mjs +0 -58
  123. package/tests/unit/naming.test.mjs +0 -94
  124. 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
- return r.db.WithContext(ctx).Save(m).Error
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
- return r.db.WithContext(ctx).Delete(&model.{{pascalName}}{}, "id = ?", id).Error
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
- _ = in
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
+ }
@@ -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
- }