Skip to content

Commit 58eb2bd

Browse files
committed
feat(database): implement app-scoped migration management and configuration
- Introduced AppDatabaseConfig to manage app-specific database settings, including migrations and seeds paths. - Added tests for loading app configurations with and without database sections. - Enhanced DatabasePlugin to support app-specific migration paths and table names. - Updated migration commands to accept an app name for scoped operations, improving flexibility in multi-app environments. - Implemented comprehensive tests for migration path resolution and app name sanitization. These changes enhance the migration management capabilities, allowing for better organization and control over database migrations in multi-application setups.
1 parent 51ad0b2 commit 58eb2bd

11 files changed

Lines changed: 1306 additions & 96 deletions

File tree

cmd/forge/config/app_config.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,35 @@ import (
1313
// This is different from ForgeConfig (project-level) and is used
1414
// to configure individual applications within a Forge project.
1515
type AppConfig struct {
16-
App AppSection `yaml:"app"`
17-
Dev AppDevConfig `yaml:"dev,omitempty"`
18-
Build AppBuildConfig `yaml:"build,omitempty"`
16+
App AppSection `yaml:"app"`
17+
Dev AppDevConfig `yaml:"dev,omitempty"`
18+
Build AppBuildConfig `yaml:"build,omitempty"`
19+
Database AppDatabaseConfig `yaml:"database,omitempty"`
1920

2021
// Internal fields
2122
AppDir string `yaml:"-"` // Directory containing the app's .forge.yaml
2223
ConfigPath string `yaml:"-"` // Full path to app's .forge.yaml
2324
}
2425

26+
// AppDatabaseConfig defines app-specific database configuration.
27+
// This allows each app to override the default migration path or
28+
// reference a specific named connection from the project config.
29+
type AppDatabaseConfig struct {
30+
MigrationsPath string `yaml:"migrations_path,omitempty"` // Override migration directory (relative to app dir)
31+
SeedsPath string `yaml:"seeds_path,omitempty"` // Override seeds directory (relative to app dir)
32+
Connection string `yaml:"connection,omitempty"` // Reference a named connection from project config
33+
}
34+
35+
// GetMigrationsPath returns the app-specific migrations path, or empty if not set.
36+
func (d *AppDatabaseConfig) GetMigrationsPath() string {
37+
return d.MigrationsPath
38+
}
39+
40+
// GetSeedsPath returns the app-specific seeds path, or empty if not set.
41+
func (d *AppDatabaseConfig) GetSeedsPath() string {
42+
return d.SeedsPath
43+
}
44+
2545
// AppSection defines app metadata.
2646
type AppSection struct {
2747
Name string `yaml:"name"`
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package config
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestAppDatabaseConfig_GetMigrationsPath(t *testing.T) {
13+
t.Run("returns configured path", func(t *testing.T) {
14+
cfg := AppDatabaseConfig{MigrationsPath: "./db/migrations"}
15+
assert.Equal(t, "./db/migrations", cfg.GetMigrationsPath())
16+
})
17+
18+
t.Run("returns empty when not set", func(t *testing.T) {
19+
cfg := AppDatabaseConfig{}
20+
assert.Equal(t, "", cfg.GetMigrationsPath())
21+
})
22+
}
23+
24+
func TestAppDatabaseConfig_GetSeedsPath(t *testing.T) {
25+
t.Run("returns configured path", func(t *testing.T) {
26+
cfg := AppDatabaseConfig{SeedsPath: "./db/seeds"}
27+
assert.Equal(t, "./db/seeds", cfg.GetSeedsPath())
28+
})
29+
30+
t.Run("returns empty when not set", func(t *testing.T) {
31+
cfg := AppDatabaseConfig{}
32+
assert.Equal(t, "", cfg.GetSeedsPath())
33+
})
34+
}
35+
36+
func TestLoadAppConfig_WithDatabaseSection(t *testing.T) {
37+
tmpDir := t.TempDir()
38+
39+
configContent := `app:
40+
name: api-gateway
41+
type: web
42+
version: "1.0.0"
43+
database:
44+
migrations_path: ./custom/migrations
45+
seeds_path: ./custom/seeds
46+
connection: production
47+
`
48+
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, ".forge.yaml"), []byte(configContent), 0644))
49+
50+
cfg, err := LoadAppConfig(tmpDir)
51+
require.NoError(t, err)
52+
53+
assert.Equal(t, "api-gateway", cfg.App.Name)
54+
assert.Equal(t, "web", cfg.App.Type)
55+
assert.Equal(t, "./custom/migrations", cfg.Database.MigrationsPath)
56+
assert.Equal(t, "./custom/seeds", cfg.Database.SeedsPath)
57+
assert.Equal(t, "production", cfg.Database.Connection)
58+
}
59+
60+
func TestLoadAppConfig_WithoutDatabaseSection(t *testing.T) {
61+
tmpDir := t.TempDir()
62+
63+
configContent := `app:
64+
name: worker
65+
type: worker
66+
`
67+
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, ".forge.yaml"), []byte(configContent), 0644))
68+
69+
cfg, err := LoadAppConfig(tmpDir)
70+
require.NoError(t, err)
71+
72+
assert.Equal(t, "worker", cfg.App.Name)
73+
// Database config should be zero-valued
74+
assert.Equal(t, "", cfg.Database.MigrationsPath)
75+
assert.Equal(t, "", cfg.Database.SeedsPath)
76+
assert.Equal(t, "", cfg.Database.Connection)
77+
}
78+
79+
func TestLoadAppConfig_YmlExtension(t *testing.T) {
80+
tmpDir := t.TempDir()
81+
82+
configContent := `app:
83+
name: my-service
84+
type: web
85+
database:
86+
migrations_path: ./migrations
87+
`
88+
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, ".forge.yml"), []byte(configContent), 0644))
89+
90+
cfg, err := LoadAppConfig(tmpDir)
91+
require.NoError(t, err)
92+
93+
assert.Equal(t, "my-service", cfg.App.Name)
94+
assert.Equal(t, "./migrations", cfg.Database.MigrationsPath)
95+
}
96+
97+
func TestLoadAppConfig_NotFound(t *testing.T) {
98+
tmpDir := t.TempDir()
99+
100+
_, err := LoadAppConfig(tmpDir)
101+
assert.Error(t, err)
102+
assert.Contains(t, err.Error(), "no .forge.yaml or .forge.yml found")
103+
}
104+
105+
func TestLoadAppConfig_InvalidYAML(t *testing.T) {
106+
tmpDir := t.TempDir()
107+
108+
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, ".forge.yaml"), []byte("invalid: yaml: content: ["), 0644))
109+
110+
_, err := LoadAppConfig(tmpDir)
111+
assert.Error(t, err)
112+
}
113+
114+
func TestLoadAppConfig_InternalFieldsSet(t *testing.T) {
115+
tmpDir := t.TempDir()
116+
117+
configContent := `app:
118+
name: test-app
119+
`
120+
configPath := filepath.Join(tmpDir, ".forge.yaml")
121+
require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0644))
122+
123+
cfg, err := LoadAppConfig(tmpDir)
124+
require.NoError(t, err)
125+
126+
assert.Equal(t, tmpDir, cfg.AppDir)
127+
assert.Equal(t, configPath, cfg.ConfigPath)
128+
}
129+
130+
func TestLoadAppConfig_DatabaseMigrationsPathOnly(t *testing.T) {
131+
tmpDir := t.TempDir()
132+
133+
configContent := `app:
134+
name: api
135+
database:
136+
migrations_path: ./db/migrate
137+
`
138+
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, ".forge.yaml"), []byte(configContent), 0644))
139+
140+
cfg, err := LoadAppConfig(tmpDir)
141+
require.NoError(t, err)
142+
143+
assert.Equal(t, "./db/migrate", cfg.Database.GetMigrationsPath())
144+
assert.Equal(t, "", cfg.Database.GetSeedsPath())
145+
assert.Equal(t, "", cfg.Database.Connection)
146+
}

0 commit comments

Comments
 (0)